这篇文章发表于 1193 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
在CTF的web方向中有一类简单的PHP代码审计的题目——大多像闯关一样,被人一点点拼起来的感觉——就比如那种PHP的弱类型比较,一旦不成功就die
……其中还可能结合小tricks,网上的资料查起来相对零散,于是本文就针对这些相对基础或者说是简单的代码审计类题目吧,请自行揣摩。
部分内容觉得没写的必要,所以不是非常全面,请谅解……但我会在有必要的时候会追加些东西。
PHP弱类型
一般这部分内容都是等式判断为==
的情况,因为==
会把等式两边的内容转化为相同类型后再进行比较,因此有了操作的空间。
is_numeric
1 |
|
很显然,1337b
在is_numeric
函数中被判断为非数字,但是却在后面的数学运算中被视为1337
。
json_deocde
1 |
|
这里应用到了0=="*********"
这种形式绕过,属实是典中典。拥有类似问题的函数还有array_search
和is_array
,可参见该文章。
strcmp
1 |
|
因为strcmp
函数期待其中的参数均为字符串,但由于传入了一个数组类型的内容,于是strcmp
比较出错,返回结果为null
,因此在和0
的弱类型比较中判定为相等。
intval
这个典型例子如下(PHP版本在7.0以下):
1 |
|
传入的1e3
在前者中会被理解为1
,但是会在后者中被理解为1001
(因为后者的科学计数法在加上1
后会变成正常的数字,再被intval
函数处理)。
hash比较
这里的内容相对多些,我们就以md5
函数为例来看看。
1 |
|
这里首先想到的一般是哈希碰撞,但想实现这个还是比较困难。于是我们继续利用弱类型。
第一个利用方式其实还利用到了科学计数法,原理就是md5('QNKCDZO') == '0e830400451993494058024219903391' == 0 == '0e462097431906509019562988736854' == md5('240610708')
。
第二个利用方式利用到了md5(array)
的返回为null
,因而轻松实现了绕过。但并不是所有时候都可以用这种方法,比如下面这种情况。
1 |
|
针对这道题,就只能使用第一种利用方式进行爆破,不是哈希碰撞!给出的Python脚本如下:
1 | #!/usr/bin/python3 |
其实这里能够延伸的东西很多了,比如利用md5
完成SQL注入就需要我们去爆破以获得含有形如'or'
这类字符串,这些变化灵活运用即可。
那如果对本节第一个程序稍加改进,又会怎么样呢?
1 |
|
可见这里的解法采用了哈希碰撞……别问我这个字符串怎么构造出来的,问就是网上查的233……
另外,如果把题目改改,比如这里的s1
和s2
是通过反序列化传进去的(因为GET
和POST
方法传进去的都只是字符串),那么可以考虑NAN
(非数字)和INF
(无穷大),在md5
函数处理它们的时候,是将其直接转换为字符串"NAN"
和字符串"INF"
使用的,但它们与任何数据类型(除了true
)做强类型或弱类型比较均为false
,甚至NAN===NAN
都是false
,但md5('NaN')===md5('NaN')
为true
。
offset取值特性
这部分内容有变量覆盖的效果,但非常有限,仅仅能够影响字符串中的第一个字符。
1 |
|
%0?
这个标题可能让人感觉看不懂,这部分主要是介绍%0a
、%00
等的利用,由于它们可能被用于截断,也可能被用于其他方式的绕过,因此难以一言蔽之,所以就取了这个愚蠢的标题2333。
preg_match
1 |
|
这里需要注意%0A
其实是换行,而preg_match
函数默认匹配第一行,于是直接实现了绕过。但是后者strpos
能够检测到传入字符串中的flag
(如果还想绕过strpos
,用数组就行了)。
像preg_match
这样的函数还有preg_match_all
。
ereg
1 |
|
这里用到了%00
截断(当ereg
函数读到 %00
的时候,就认为已经读到字符串的结尾而结束,后面的东西就全部无视了)。
$_SERVER[‘QUERY_STRING’]
对付这个有很多花招,但具体用啥还得看各种情况。比如以下一个简单的例子。
1 |
|
$_SERVER['QUERY_STRING']
不会进行URLDecode。URL编码可以参考这里,建议不要再浏览器里直接输入这个payload(可能会帮你自动编码),可以使用curl
命令或者HackBar
。注意:以上判断中后者如果是用strpos来判断的话就没办法绕。
接下来的样例出自[MRCTF2020]套娃
。
1 |
|
对于第二个判断,就是对preg_match
中提到的%0a
的利用;而对于第一个判断的绕过,就需要提到PHP解析查询字符串的方式。以下图片引自该文章,它较详细地探索了parser_str
对字符串的处理。
变量覆盖
虽然觉得这个是某一类的漏洞,但在这类简单题中还是时不时会遇到,所以写一下。
覆盖的话,当然是要覆盖后得到自己希望的值啦,甚至一些时候可能会考虑将变量覆盖为null
。
parse_str
刚刚在$_SERVER['QUERY_STRING']
里面提到了parse_str
,parse_str
本身只是将字符串内容解析到变量中,但也有一些简单的问题,后果便是变量覆盖。
1 |
|
解决这个漏洞的方法可以指定parse_str
的第二个参数。另外,类似的函数还有mb_parse_str
。
extract
1 |
|
extract
函数效果是将数组中的变量导入,但却可以完成变量的覆盖。
$$
这个很经典,程序员原本只是希望能够将获取到的数组键名作为变量,数组中的键值作为变量的值,但很可能会出现奇怪的后果。接下来的样例出自[BugsBunnyCTF2017]SimplePHP
。
1 |
|
最开始的想法肯定是通过$flag
来输出答案,但是显然在这个题里面变量已经被释放掉了,所以需要通过$_403
或者$_200
来输出答案,这就需要用到变量覆盖。
传入以上的payload,在循环1结束之后有$_200=$flag
;在循环2结束之后有$flag="anyword"
,这时的$flag
已经没意义了,但是$_200
已经将答案取出来了。
文件路径
这些是针对文件路径方面的攻击,一般要结合file_get_contents
或者file_put_contents
或者highlight_file
这种函数。
常用php://filter过滤器
无过滤器
php://filter/resource=
字符串过滤器
php://filter/read=string.rot13/resource=
php://filter/read=string.toupper/resource=
php://filter/read=string.tolower/resource=
php://filter/read=string.string_tags/resource=
转换过滤器
这里的内容比较常用。
php://filter/read=convert.base64-encode/resource=
php://filter/read=convert.quoted-printable-encode/resource=
php://filter/write=convert.base64-encode/resource=
更多内容可参考该文章。
这里再给个例题[BSidesCF 2020]Had a bad day
。
1 |
|
相应的payload可为以下内容:
1 | ?category=php://filter/read=convert.base64-encode/woofers/resource=flag |
不增加其他伪协议的内容了,更多的payload可参见该文章。
is_file
1 |
|
其中/proc/self/root/
是指向/
的符号链接,这个payload在对付require_once
函数也有着一定的效果,具体可见此网站。
basename
这里的漏洞在高版本的PHP中得到了修复。该样例选自[Zer0pts2020]Can you guess it?
,题中的哈希其实只是障眼法==。
1 |
|
重点其实在正则绕过和对basename
函数的利用上。其中basename
函数会返回访问路径中的文件名部分。但如果没有%81
这样的字符的干扰的话,由于$_SERVER['PHP_SELF']
处理后的结果为/index.php/config.php
,会被正则表达式刷掉。如果能够将结果拓展为形如/index.php/config.php/a
这样的路径,那么我们就不用担心正则表达式的问题——查看此文章,发现只要将以上的a
替换为%80
~`%ff`中的任意一个字符即可。
其他
这里记录些其他的细碎知识点。
$_REQUEST
在同时接收GET
和POST
参数时,POST
的优先级更高,如果GET和POST都有相同的参数,在检测时POST的值就会覆盖GET的值;=
的运算符等级比and
高;%09
在PHP的命令执行中可以作为空格的替代。
参考文献
https://www.freebuf.com/articles/web/261802.html
https://lazzzaro.github.io/2020/05/18/web-PHP%E7%BB%95%E8%BF%87%E5%A7%BF%E5%8A%BF/index.html