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)。之后在这个方向上进行漏洞挖掘,估计会有不小的收获。