2020-06-14
1.6k
PHP代码审计学习Day4——strpos函数缺陷
0x01 False Beard
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Login { public function __construct($user, $pass) { $this->loginViaXml($user, $pass); }
public function loginViaXml($user, $pass) { if ( (!strpos($user, '<') || !strpos($user, '>')) && (!strpos($pass, '<') || !strpos($pass, '>')) ) { $format = '<?xml version="1.0"?>' . '<user v="%s"/><pass v="%s"/>'; $xml = sprintf($format, $user, $pass); $xmlElement = new SimpleXMLElement($xml); $this->login($xmlElement); } } }
new Login($_POST['username'], $_POST['password']);
|
这段程序使用格式化字符串的方式,用XML结构存储用户的登录信息,这种情况容易造成XML注入。第8行和第9行使用了strpos
函数来防止用户输入的参数包含<
和 >
这个两个符号。先来看看strpos
函数的定义:

在这道题目中,开发者只考虑到 strpos 函数返回 false 的情况,却忽略了匹配到的字符在首位时会返回 0 的情况,因为 false 和 0 的取反均为 true,这样就可以通过闭合"
的方式来注入XML。Payload如下:
1
| username="/><injected-tag%20property="&password=<injected-tag>
|
但是这段代码SimpleXMLElement
并没有指定LIBXML_NOENT参数,从而不能读取外部实体,不知道在这个地方这样的XML注入能有什么危害。
0x02 实例分析
漏洞利用
首先在后台开启会员功能:

注册两个会员,并且不能设置密保问题:

在登录账号test1的情况下访问下面链接:
1
| http://test.com/PHP-Audit-Labs/Day4/dedecmsmember/resetpassword.php?dopost=safequestion&safequestion=0.0&safeanswer=&id=3
|
burp抓包重放:

然后带着id与key访问:
1
| http://test.com/PHP-Audit-Labs/Day4/dedecms/member/resetpassword.php?dopost=getpasswd&id=3&key=PFhZR7Go
|

自动填充了test2,那么我们就可以任意用户密码重置。
代码分析
根据漏洞url定位到member\resetpassword.php
的safequestion
操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| else if($dopost == "safequestion") { $mid = preg_replace("#[^0-9]#", "", $id); $sql = "SELECT safequestion,safeanswer,userid,email FROM #@__member WHERE mid = '$mid'"; $row = $db->GetOne($sql); if(empty($safequestion)) $safequestion = ''; if(empty($safeanswer)) $safeanswer = ''; if($row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer) { sn($mid, $row['userid'], $row['email'], 'N'); exit(); } else { ShowMsg("对不起,您的安全问题或答案回答错误","-1"); exit(); }
}
|
这里先根据传入的id
参数查询对应用户的密保问题答案
、userid
、 邮箱
等信息,接着下面进行判断,如果传入的$safequestion
与$safeanswer
非空且与之前设置的相等,就进入sn()
函数操作 它这里用的是 ==
而非 ===
来判断,所以这里是可以绕过的:

那么这里我们就可以用safequestion=0.0&safeanswer=
即可使$row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer)
为true,进入sn()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function sn($mid,$userid,$mailto, $send = 'Y') { global $db; $tptim= (60*10); $dtime = time(); $sql = "SELECT * FROM #@__pwd_tmp WHERE mid = '$mid'"; $row = $db->GetOne($sql); if(!is_array($row)) { newmail($mid,$userid,$mailto,'INSERT',$send); } elseif($dtime - $tptim > $row['mailtime']) { newmail($mid,$userid,$mailto,'UPDATE',$send); } else { return ShowMsg('对不起,请10分钟后再重新申请', 'login.php'); } }
|
这里代码逻辑是先根据id
从dede_pwd_tmp
数据表中判断是否有对应的密码记录,若账号为第一次修改密码,这里的$row
就会空,进入newmail()
函数,执行insert()
操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function newmail($mid, $userid, $mailto, $type, $send) { ... $randval = random(8); ... if($type == 'INSERT') { $key = md5($randval); $sql = "INSERT INTO `#@__pwd_tmp` (`mid` ,`membername` ,`pwd` ,`mailtime`)VALUES ('$mid', '$userid', '$key', '$mailtime');"; if($db->ExecuteNoneQuery($sql)) { if($send == 'Y') { ... } else if ($send == 'N') { return ShowMsg('稍后跳转到修改页', $cfg_basehost.$cfg_memberurl."/resetpassword.php?dopost=getpasswd&id=".$mid."&key=".$randval); } } else { } }
|
先生成一个8位的随机密码并赋值给$randval
,然后将其用md5加密,存储到dede__pwd_tmp
表中,接着到了漏洞的触发点,进入$send == 'N'
的操作,将未经md5加密的$randval
传给了用户。
那么这里拼接的url就为:
1
| http://test.com/PHP-Audit-Labs/Day4/dedecms/member/resetpassword.php?dopost=getpasswd&id=3&key=PFhZR7Go
|
继续跟进dopost=getpasswd
的操作:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| else if($dopost == "getpasswd") { if(empty($id)) { ShowMsg("对不起,请不要非法提交","login.php"); exit(); } $mid = preg_replace("#[^0-9]#", "", $id); $row = $db->GetOne("SELECT * FROM #@__pwd_tmp WHERE mid = '$mid'"); if(empty($row)) { ShowMsg("对不起,请不要非法提交","login.php"); exit(); } if(empty($setp)) { $tptim= (60*60*24*3); $dtime = time(); if($dtime - $tptim > $row['mailtime']) { $db->executenonequery("DELETE FROM `#@__pwd_tmp` WHERE `md` = '$id';"); ShowMsg("对不起,临时密码修改期限已过期","login.php"); exit(); } require_once(dirname(__FILE__)."/templets/resetpassword2.htm"); } elseif($setp == 2) { if(isset($key)) $pwdtmp = $key;
$sn = md5(trim($pwdtmp)); if($row['pwd'] == $sn) { if($pwd != "") { if($pwd == $pwdok) { $pwdok = md5($pwdok); $sql = "DELETE FROM `#@__pwd_tmp` WHERE `mid` = '$id';"; $db->executenonequery($sql); $sql = "UPDATE `#@__member` SET `pwd` = '$pwdok' WHERE `mid` = '$id';"; if($db->executenonequery($sql)) { showmsg('更改密码成功,请牢记新密码', 'login.php'); exit; } } } showmsg('对不起,新密码为空或填写不一致', '-1'); exit; } showmsg('对不起,临时密码错误', '-1'); exit; } }
|
这里先判断id
是否执行过重置密码的操作如果没有则退出,接着进入了empty($setp)
的操作,判断是否超过修改期限,最后包含了resetpassword2.htm
:

页面设置了setp为2,进入$setp == 2
的操作,这里判断了$sn
与dede_pwd_tmp
的pwd
值($key
)是否相等,因为
$sn=md5($randval)=$row['pwd']
,这样就可以重置密码了。
0x03 练习题
题目链接: https://pan.baidu.com/s/1pHjOVK0Ib-tjztkgBxe3nQ 密码: 59t2
这道题之前做过,问题出现在api.php
中的buy
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function buy($req){ require_registered(); require_min_money(2);
$money = $_SESSION['money']; $numbers = $req['numbers']; $win_numbers = random_win_nums(); $same_count = 0; for($i=0; $i<7; $i++){ if($numbers[$i] == $win_numbers[$i]){ $same_count++; } } ...... }
|
关键点在第10行代码,它使用==
进行比较,而语言定义,除了 0、false、null 以外均为 true ,所以使用 true 和数字进行比较,返回的值肯定是 true,所以我们抓包修改数据提交7个true,如下图:
