在最近遇见的CTF题中有很多都禁了所有有关命令执行的函数一类的题,如何绕过disable_function执行命令确实是一个问题,对于想要绕过disable_function的方法,有下面几种:
利用环境变量LD_PRELOAD
ImageMagick
mail函数
imagecreatefromgd2
破壳bash漏洞
- imap_open
最新的方法肯定是imap_open,于是找来几篇imap_open关于绕过disable_function的文章来研究一下,最近也有类似用到imap的CTF题。看了2篇外文,2篇中文。在知道基本的原理的基础上,发现都讲的差不错,然而有些细节方面都没有我想要的,甚至某些地方有一些出入。我想要知道为什么imap
会走到ssh
上,我注意到在php
函数imap_open
的第一个$mailbox
参数中多了一个}
,没有它似乎也没有外部的execve
调用,我也想知道内部是如何分隔$mailbox
参数的,带着这样的疑问,便有了此文,把php
内核调用imap-2007f
的mail_open过程看了一边,终于理顺了。纸上得来终觉浅!
0x01 imap_open 整个调用链的分析
0x00 PHP调用mail_open的过程
php
内部并没有自己实现imap
过程,用的是c下imap-2007f
的库。
1 | 0x1 PHP_FUNCTION(imap_open) -> php_imap_do_open() -> mail_open(); |
这是PHP
内的调用过程,全程没有对其第一个参数$mailbox进行处理,传给了mail_open()
1 | imap_stream = mail_open(NIL, ZSTR_VAL(mailbox), flags); |
0x01 imap-2007f下mail_open执行过程
进入imap-2007f的库函数下mail_open() imap-2007f/src/c-client/mail.c:1186
中 name
即 $mailbox
1 | switch (name[0]) { |
进入 switch
如果mailbox
开头是#
,会进行特别的处理,类似于hook预处理。
或者进入default
这是我们要进的位置。
0x02 驱动选择过程
->mail_valid() imap-2007f/src/c-client/mail.c:1257
选择要使用的邮件驱动 包括 imapdriver
,pop3driver
,nntpdriver
,dummydriver
首先会有一个基础的判断mailbox
不允许带 \n\r
,前面会通过mail_link()
将这些驱动注册到全局变量factory
1 | for (factory = maildrivers; factory && |
通过遍历factory
,由各个驱动的valid
函数来进行决定用哪个驱动。最后返回选择好的驱动。比如imap_vaild()
都会通过 mail_vaild_net_parse()
分割mailbox
通过得到的 service
和 驱动的名字比较。若相符则正常返回。这个函数是重点后面详细讲,返回选择好的驱动返回,接着走。
1 | d ? mail_open_work (d,stream,name,options) : stream; |
-> mail_open_work( ) imap-2007f/src/c-client/mail.c 1260:
选择好驱动以后,进入mail_open_work(),
因为传入stream
为nil
,需要初始化一个新的stream
结构,然后调用相应mail驱动的open()
函数,这里是imap_open()
1 | return ((*d->open)(stream)) ? stream : mail_close(stream); |
0x03 imap_open 执行过程
-> imap_open() imap-2007f/src/c-client/imap4r1.c:783
这里首先会进入mail_valid_net_parse()
这个刚才函数,这里是为了将$mailbox
分割出一个NETMBX
结构
1 | typedef struct net_mailbox { |
其中包括后面需要用到的各种参数。也是后面判断进入各种流程的重要依据.
->mail_valid_net_parse() -> mail_valid_net_parse_work() imap-2007f/src/c-client/mail.c 734
1 | if (*name++ != '{') return NIL; |
重点看host
,前面都是基础的判断条件,去掉了[]
包裹的内容。
以“/:}”
中第一个出现的字符标志为hostname
的结束符, 却最后又以}
判断 server
部分的结束,}
之后判定为邮箱的名字。当出现一种情况}
同时是hostname
的结束符,也是整个server
部分的结束符。当出现这种下,中间存在目标邮箱地址的端口号和指定的flag都会被略过,会出现下面if判断条件不成立。
1 | if (t - v) // t-v == 0 |
因为不会进入接下的if
语句,就不会进行目标邮箱server的端口赋值即mb->port
和各种/flag
的判断和对mb
的赋值 。即使原来的mailbox
里flag参数中存在/norsh
也不会起作用。这个分割函数其实很有意思,因为没有进入if
语句 也没有得到相应的service
参数来选择对应的驱动,但是会自动赋值为调用者的service
名字,前面说了各个mail驱动的vaild
的函数会选择是否适用于当前mailbox
,都会默认传递自己的service
的名字,可以说4个驱动在这种情况都是可以用的,但是很巧的是 imapdriver
是第一个注册的。所以理所当然的走到了imap
下。
1 | if (!*mb->service) strcpy (mb->service,service); |
回到 ->imap_open
经过分割得到的mb
参数。因为没有进入if
语句会丢失很多赋值过程
1 | if (mb.dbgflag) stream->debug = T; |
列如这些都不会进如 if,因为/flag
都没有判断。
1 | if (stream->anonymous || mb.port || mb.sslflag || mb.tlsflag) |
相应的 stream->anonymous mb.port mb.sslflag mb.tlsflag
都是NIL
1 | else if (reply = imap_rimap (stream,"*imap",&mb,usr,tmp)); |
0x04 imap_rimap 执行过程
->imap_rimap imap-2007f/src/c-client/imap4r1.c:1022
1 | if (!mb->norsh && (tstream = net_aopen (NIL,mb,service,usr))) |
-> net_aopen() /root/bypass_disable_function/imap-2007f/src/c-client/mail.c:6218
1 | if (!dv) dv = &tcpdriver; |
-> tcp_aopen(unix) imap-2007f/src/osdep/unix/tcp_unix.c imap-2007f/src/osdep/unix/tcp_unix.c:330
1 |
|
关于rsh
的寻址问题其他文章也讲的很清楚了。SSHPATH
和RSHPATH
都先从/etc/c-client.cf
的配置文件得到,一般都是为空,但RSHPATH
在编译的时候可以从makefile
里面可以被指定为 /usr/bin/rsh
,在debian
下rsh
又是指向 ssh
。
1 | else if (rshpath && (ti = rshtimeout)) { |
rshcommand
是默认nil
,这里赋值。
1 | else sprintf (tmp,rshcommand,rshpath,host,mb->user[0] ? mb->user : myusername (),service); |
写入tmp
里面,rshpath
为 /usr/bin/rsh
,host
为mb->host
经过tcp_canonical
并通过dns
解析,返回原值。user 为 myusername()
调用者是谁,我这里是root
,service 即“imap”
1 | tmp` 应为 `“rshpath mb->host -l myusername() exec /usr/sbin/rimapd” |
这个地方也很关键,是分割path
和args
的位置,这里也有跟国内的两篇的文章有出入,并不是他们说的不能用$mailbox
包含空格 ,因为会被转义,?????,这是转义吗?这是以空格为标志分割命令执行的参数。所以这里不能用空格 可以用 \t
和 $IFS
来代替,
其最后execv (path,argv)
即
1 | execv("/usr/bin/rsh",["/usr/bin/rsh",host ...] |
这里已经可以通过修改hostname
达到参数注入,同样ssh 也有那么一个参数可以执行任意命令-oProxyCommand
,避免不要的麻烦,可以base64
,因为涉及到getshell 可能会出现写路径出现/
,因为前面说了/
同样是可以判定hostname
的结束符,这样把}
提前就没有意义了。下面的strace 结果可能会看的更清楚
0x02 Payload && 官方修复分析
附上官方验证性payload ,至于修复php官方 默认将rsh和ssh 的连接超时设置为了0
1 | STD_PHP_INI_BOOLEAN("imap.enable_insecure_rsh", "0", PHP_INI_SYSTEM, OnUpdateBool, enable_rsh, zend_imap_globals, imap_globals) |
rshtimeout
和 sshtimeout
同样也可以来源于 /etc/c-client.cf
,一般情况这个文件是空的,同样在tcp_unxi.c 中也对这个两个值进行了赋值,默认都为15.
在 tcp_aopen() imap-2007f/src/osdep/unix/tcp.unix.c 349
1 | else if (rshpath && (ti = rshtimeout)) return NIL |
这样一来就给限制住了,直接返回nil
. 下面是官方测试用的payload
1 | $payload = "echo 'BUG'> " . __DIR__ . '/__bug'; |
0x03 对其整个过程的思考和总结
在php 使用imap_open
的时候一定需要注意 :第一个参数$mailbox
php本身并没有对其进行检验是否符合格式的操作。只是判断了第一个字符是不是{
,在c的imap-2007f库中虽然进行了参数化,但在处理上还是有一定的问题。
比如在最后tcp_aopen() imap-2007f/src/osdep/unix/tcp.unix.c:371
中
1 | strcpy(host,tcp_canonical(mb->host)) |
tcp_canonical
仅仅对hostname
进行了 ip_nametoadress
其本身就是调用了gethostname
,若解析不了则原hostname
返回,这过程是不是存在一点问题?
所以在使用imap_open
中对hostname
一定要严格限定,应过滤相应的/
}
这些字符。
在选择外部调用的问题上,是否应该完全交付处理,这问题出在信任链上。
0x04 整个过程的调用链
0x05 参考连接
https://lab.wallarm.com/rce-in-php-or-how-to-bypass-disable-functions-in-php-installations-6ccdbf4f52bb
https://nosec.org/home/detail/2044.html
https://xz.aliyun.com/t/4113
https://github.com/asmlib/imap-2007f