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