溯源imap_open 是如何绕过disable_function

2019-05-13 9,282


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。

图片2.png

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 结果可能会看的更清楚

图片1.png

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 整个过程的调用链

123123.png

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

 

本文作者:Topsec-SRC

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/105606.html

Tags:
评论  (0)
快来写下你的想法吧!

Topsec-SRC

文章数:8 积分: 100

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号