【Bugku CTF】 Web —— welcome to bugkuctf
0x00 前言
此题运用了许多 PHP 技巧,解题思路惊艳,特此想分享一下思考过程。与其说此题是 Web 题,不如说是道 PHP 技巧题,难度中等,要求有扎实的 PHP 基础。
涉及到的技巧有 PHP 伪协议、魔术方法、对象的序列化与反序列化等,相关链接如下:

0x01 PHP 伪协议获取源码
点开链接,依然按常规套路只有一句话:you are not the number of bugku ! ,继续查看源码发现线索:

这里需要满足两点:
- 构造合适的 URL 查询变量
txt使得 PHP 变量$user满足 if 语句的条件 - 令变量
$file通过文件包含函数include()获取指定页面的源码
注意,此处还未涉及到查询变量 password。
php://input
在 if 语句中,通过 file_get_contents() 函数读取变量 $user 的值后,再与字符串 welcome to the bugkuctf 比较。
因此可借助伪协议 php:// 中的 php://input 访问原始请求数据中的只读流。这里令 $user = "php://input",并在请求主体中提交字符串 welcome to the bugkuctf。
php://filter
在 include($file); //hint.php 语句中,相当于把变量 $file 的值在该语句的位置读入,结合后面的提示,应该是要把 hint.php 页面的源码以某种形式包含在当前页面内。
因此可利用 php://filter 以某种方式筛选过滤特定的数据流。这里令 $file = php://filter/read=convert.base64-encode/resource=hint.php,意思是以 Base64 编码的方式过滤出 hint.php 页面的源码,也可以把它理解为一个具有 URI 形式的特殊函数,该函数能把 hint.php 源码以 Base64 编码的方式读入。
根据以上两种 PHP 伪协议的分析,构造出以下 payload 即得 hint.php 源码的 Base64 编码:

解码后得到 hint.php 源码:
1 |
|
根据注释发现了藏 flag 的页面,再次照葫芦画瓢去偷看 flag.php 源码:

…果然没那么简单,唔,卡住了,怎么办?
别忘了还有主页面啊!利用主页面的功能也能读取自身的源码,这一点很容易被遗忘,所以这次很顺利就获得了 index.php 的源码(注意,路径末尾不带 php 文件的 URL http://120.24.86.145:8006/test1/,一般默认就是访问 http://120.24.86.145:8006/test1/index.php):
1 |
|
注意到第 8 行将 $file 变量中的 flag 字符串过滤了,印证了前面的结果。到此,我们顺利地将此题变成一道 PHP 白盒审计题了,现在才是此题最有意思的地方,如何利用已知的 hint.php 与 index.php 的源码去获取 flag.php 的内容?请继续往下看。
0x02 序列化构造 payload
要理清 hint.php 与 index.php 之间的关系,首先要理解 hint.php 中的 __tostring() 函数是何方神圣。
魔术方法 __toString()
「写在前面」:PHP 中变量与方法的命名一般遵循小驼峰式命名法(Lower Camel Case),类的命名一般遵循大驼峰式命名法(Upper Camel Case),也称帕斯卡命名法(Pascal Case),并且普通变量、超级全局变量、常量、数组索引等区分大小写,而函数名、方法名、类名、魔术变量等不区分大小写,但最好使用与定义一样的大小写名字。详情可参考:
因此看到此魔术方法定义为 __toString(),在源码中却是 __tostring() 就不足为奇了。
魔术方法 __toString() 定义在类中,在该类的对象被当成字符串打印时执行,并且必须返回一个字符串,否则出现报错。
因此在 hint.php 中,当 Flag 类的对象被打印时,将获取 $file 变量(注意此处的 $file 与 index.php 中的不同)中文件的内容并输出,最后返回字符串 good。
对象的序列化
再来看看 index.php 的 11-15 行,满足了上面所有 if 语句的条件后,先包含变量 $file,再反序列化变量 $password 后输出。此段代码是本题的核心,理解了就能找到 index.php 与 hint.php 之间的联系,从而构造出 payload。
核心思想如下:先用 include() 函数包含 hint.php,从而引入 Flag 类;再将该类对象的序列化字符串赋值给 $passsword,且令对象的成员变量 $file = flag.php, 所以在反序列化后得到上述对象;最后用 echo 打印该对象,从而触发 __toString() 函数,输出 flag.php 的值。
通过以下代码可获得成员变量 $file = flag.php 的 Flag 对象的序列化字符串:O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
1 |
|
根据以上所有的分析,可以将 payload 总结如下:
http://120.24.86.145:8006/test1/?txt=php://input&file=hint.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
最后别忘了在请求主体中提交字符串 welcome to the bugkuctf 喔,提交后得到 flag:

若有不足或错误之处劳烦指出,欢迎有疑问的朋友前来留言讨论。