函数学习
strpos — 查找字符串首次出现的位置
作用:主要是用来查找字符在字符串中首次出现的位置。
结构:int strpos ( string $haystack , mixed $needle [, int $offset = 0 ] )
Demo学习
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php 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']); ?>
|
在第12 ~ 13中,程序通过格式化字符串用xml存储用户登录信息,在第9 ~ 10行对提交的参数username
和password
进行简单的过滤,禁止输入< >
符号防止注入。但过略并不严谨,导致可以绕过。
strpos()
函数会返回查找字符串的下标,如果不存在则为false
1 2 3 4 5
| <?php var_dump(strpos('abcde', 'a')); var_dump(strpos('abcde', 'b')); var_dump(strpos('abcde', 'x')); ?>
|
由图中可以看到,查找的字符串如果位于第一位,则返回为0,但在PHP中,0和false取反均为true。
1 2
| Payload: user=<"><injected-tag%20property="&pass=<injected-tag>
|
案例分析
案例选取为DeDecms V5.7SP2正式版,该CMS存在任意用户密码重置漏洞。
先看什么是PHP弱类型比较,在PHP中有两种比较符号==
和===
。
在进行比较的时候,会先将字符串类型转换成相同,再比较
在进行比较的时候,会先判断两种字符串的类型是否相同,再比较
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php
var_dump('abc'== 1); var_dump('1abc'== 1); var_dump('abc'== 0); var_dump('1abc'== 0); var_dump(null == 0); var_dump('abc'=== 1); var_dump('1abc'=== 1); var_dump('abc'=== 0); var_dump('1abc'=== 0);
?>
|
漏洞的触发点在 member/resetpassword.php
文件中,由于对接收的参数safeanswer
没有进行严格的类型判断,导致可以使用弱类型比较绕过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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(); }
}
|
简单分析这段代码。当$dopost
参数值为safequestion
时,通过$mid
值代入sql语句中查询对应安全问题、安全答案、用户id和邮箱。在第11行中,如果所传入的问题和答案非空,并且登录查询到的安全问题和答案相同则进行sn
函数中。但这里进行比较的是==
而不是===
,所以是可以进行绕过。
假设用户没有设置安全问题和答案,默认情况下安全问题值为$row['safequestion']='0'
,安全答案值为$row['safeanswer']=null
。如果提交空参数的话,$safequestion
和$safeanswer
值均为空字符串。那么第11行中if表达式就变成了if('0'=='' && null == '')
,即if(false && true)
。所以,只要让$row['safequestion'] == $safequestion
为True
就可以正确进入sn
函数中。
通过上面PHP弱类型的学习,这里可以简单绕过:
1 2 3 4 5 6 7
| <?php
var_dump('0' == '0e1'); var_dump('0' == '0.0'); var_dump('0' == '0.');
?>
|
接下来进入sn
函数,文件路径在member/inc/inc_pwd_functions.php
中。
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'); } }
|
在sn
函数中,会通过$mind
查询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
| 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') { sendmail($mailto,$mailtitle,$mailbody,$headers); return ShowMsg('EMAIL修改验证码已经发送到原来的邮箱请查收', 'login.php','','5000'); } else if ($send == 'N') { return ShowMsg('稍后跳转到修改页', $cfg_basehost.$cfg_memberurl."/resetpassword.php?dopost=getpasswd&id=".$mid."&key=".$randval); } } else { return ShowMsg('对不起修改失败,请联系管理员', 'login.php'); } }
|
在newmail
函数中,由于是第一次找回密码,所以$send='N'
,进入对应的if分支,漏洞触发点存在此处,通过ShowMsg
打印出$key
值。$key
值在第4行中随机生成并进行MD5
加密,然后插入到dede_pwd_tmp
表中,但这里直接打印拿到$key
值。回到resetpassword.php
文件查看dopost=getpasswd
处操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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(); }
|
首先会判断当前$mid
用户是否执行过重置密码,如果$row
不为空继续执行以下操作:
1 2 3 4 5 6 7 8 9 10 11 12
| 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"); }
|
首先判断当前时间是否超过dede_pwd_tmp
表中有效时间,符合要求打开密码修改页面,进入重置密码最后一步。
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
| 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; }
|
在第6行中判断传入的$key
值和dede_pwd_tmp
表中的$row['key']
值是否相同,如果相同就完成密码重置。
案例复现
注册test
账号,其对应密码hash为ceb6c970658f31504a901b89dcd3e461
访问第一步中的Payload:
1
| http://127.0.0.1/dede/member/resetpassword.php?dopost=safequestion&safequestion=0.0&safeanswer=&id=2
|
此处id
为test用户对应的mid
值
获取到key
值,拿出来拼接访问第二步Payload:
1
| http://127.0.0.1/dede/member/resetpassword.php?dopost=getpasswd&id=2&key=dlY0Zq3g
|
密码重置成功。
案例学习
漏洞点位于api.php
文件中buy
函数处。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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++; } }
|
彩票号码$win_numbers
为随机生成的7位数字。在第9~11行中,用户提交的$numbers
和$win_numbers
每一位数进行对比,而在第10行中使用了==
进行比较。
1 2 3 4 5 6
| <?php
var_dump(true == 1); var_dump(true == 0);
?>
|
除了0
、false
、null
以外均为 true
,所以使用 true
和数字进行比较,返回的值肯定是 true
。用户提交数据是以json
格式上传,所以提交7个true
元素的数组即可。如果随机数生成了0,则多购买几轮彩票就可以购买flag
。
钱够了直接购买就可以了。