溯源imap_open 是如何绕过disable_function

栏目: 服务器 · 发布时间: 6年前

内容简介:遇见了一个基本全命令执行函数禁止的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。

溯源imap_open 是如何绕过disable_function

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

溯源imap_open 是如何绕过disable_function

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

溯源imap_open 是如何绕过disable_function

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Agile Web Development with Rails 4

Agile Web Development with Rails 4

Sam Ruby、Dave Thomas、David Heinemeier Hansson / Pragmatic Bookshelf / 2013-10-11 / USD 43.95

Ruby on Rails helps you produce high-quality, beautiful-looking web applications quickly. You concentrate on creating the application, and Rails takes care of the details. Tens of thousands of deve......一起来看看 《Agile Web Development with Rails 4》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具