zxc-cppnb

导航

 

在 B 站看到一个 Vim modeline 漏洞,我研究了一下它的原理

注意vim9.1.2100(可能在这之前)就已经更新了,vim有一个modelineexpr如果不开启vim的modeline无法执行表达式,不过可以打开实验一下,

echo "set modelineexpr">>"~/.bashrc" 

别忘了最后关了

今天在 B 站上看到有人发现了 Vim 的一个 modeline 漏洞。点进去一看,映入眼帘的就是一行熟悉的 modeline 语法:

/* vim: set showpanel=2 tabpanel=%{%autocmd_add([{'event'\:'CursorMoved','pattern'\:'*','cmd'\:'call\ system("{touch,a}")','once'\:1}])%}: */

modeline 是什么?

Vim 的 modeline 功能允许你在文件的开头或结尾嵌入一行配置,让 Vim 打开这个文件时自动设置一些选项。通常用来保证文件的显示效果(比如缩进、换行等),在标准的 vimrc 文件底部也经常能看到它。

想知道具体语法?直接在 Vim 里用 :h modeline 就能查。我帮你把文档内容总结一下:

两种语法格式

  1. [text{white}]{vi:|vim:|ex:}[white]{options}
  2. [text{white}]{vi:|vim:|Vim:|ex:}[white]se[t] {options}:[text]

上面那个漏洞用的是第二种。如果换成第一种,可以写成这样:

// vim: showpanel=2 tabpanel=%{%autocmd_add([{'event'\:'CursorMoved','pattern'\:'*','cmd'\:'call\ system("{touch,a}")','once'\:1}])%}

为什么要用 tabpanel%{%...%}

原 Po 用了 tabpanel,其实更通用的写法是用 statusline(在文档上可以看到两者语法一样,详情:h tabpanel和:h statusline)。
因为 tabpanel 需要先把 showpanel=2 打开,而 statusline 没有这个限制:

// vim: statusline=%{%autocmd_add([{'event'\:'CursorMoved','pattern'\:'*','cmd'\:'call\ system("{touch,a}")','once'\:1}])%}

Vim 文档(:h statusline)里说:%{expr} 会对 Vimscript 表达式求值并返回结果;而 %{%expr%} 则会将求值结果再交给 expand() 展开(详情 :h expand())。
那为什么不直接用 %{expr}?我试了一下,发现 %{expr} 里面不允许出现 } 字符,而且用反斜杠转义也没用。所以 %{%expr%} 才是更通用的选择。

表达式里在干什么?

%{% ... %} 里面的内容是一个 Vimscript 表达式,它的核心是调用 autocmd_add() 函数。

autocmd_add() 用于动态添加自动命令(autocmd)。自动命令就像钩子,可以在特定事件发生时执行一段代码(Vimscript 或 shell 命令,比如 system()!)。

这个函数要求传入一个列表,列表里是一个字典,所以你会看到 [{}] 这种嵌套结构。字典的键值对解释如下:

event 触发事件,这里是 CursorMoved – 只要光标移动就触发
pattern 匹配哪些文件,* 表示任意文件
cmd 要执行的 Vimscript 命令,用 call system("{touch,a}") 执行 shell 命令
once 非零值表示这个自动命令只执行一次

其中 system("{touch,a}") 是 Bash 的 brace expansion 语法,会展开为 touch a。你也可以直接写 "touch\ a",记得转义空格,因为modeline语法以空格隔开选项。

另外,字典里的冒号为什么写成 \:?因为第二种 modeline 语法本身以 : 结尾,为了避免提前结束,需要用 \: 转义。

为什么要绕这么大一个弯子?

你可能想问:为什么不直接在 modeline 里写 system()job_start()
因为 modeline 是一个受限环境,这类危险函数被禁止直接调用。而通过 autocmd_add() 创建一个自动命令,等文件打开后再触发,就巧妙绕过了这个限制。

比如上面这个例子,只要你打开那个文件,然后移动一下光标,就会自动执行 touch a 命令。当然,你也可以换成其他事件,比如 ExitPre(在 Vim 退出前执行)。
关于更多的事件可以看文档:h autocmd-events-abc

最后说两句

这个“漏洞”本质上是利用 modeline 结合自动命令来实现代码执行,算是一个经典的 Vim 安全风险提醒。平时打开来源不明的文件时,最好禁用 modeline(set nomodeline),或者使用更安全的 securemodelines 插件。
好的,我在文章的“为什么要绕这么大一个弯子?”后面加一段关于 root 用户的说明。下面是新增的内容,你可以直接插入到原文相应位置。


那 root 用户能执行吗?

默认情况下,不能。

Vim 的文档明确写了:当 Vim 以 root 身份运行时,modeline 功能是默认关闭的。这是出于安全考虑,防止恶意文件在 root 权限下自动执行危险配置。

除非 root 用户主动修改了 vimrc,在里面加上 set modeline,否则即使文件里嵌入了上述 modeline 代码,也不会被解析和执行。

所以,这个漏洞的“杀伤力”在 root 用户那里基本为零。但普通用户还是要小心,别乱打开来源不明的文件。

关于这篇文章的闹剧

我以为我的vim9.1.2100有这个漏洞,然后才进行解析的。结果发现原来半年前我把modelineexpr选项开启了。

那时候我和一个人讨论vim自动执行命令的问题,然后我打开的modelineexpr这一下忘记关了,半年后出来坑我一下。

这也告诉我们危险的选项开了记得删掉,或许不是别人会利用,而是几个月后忘记这件事的自己利用它,兴奋的发随笔。

不过这也让我更精进modeline语法了。

posted on 2026-04-15 11:52  Cpp_Nb  阅读(27)  评论(0)    收藏  举报