HelloCTF RCE lv8靶场 · 命令执行 · 重定向 —— Writeup
| 项目 | 内容 |
|---|---|
| 题名 | HelloCTF - RCE 靶场:命令执行 - 重定向 |
| 靶场地址 | http://80-cb079509-7e51-4321-928e-f2b307c31f76.challenge.ctfplus.cn/ |
| 题型 | PHP 命令执行 + 输出重定向屏蔽 |
| 环境 | nginx/1.20.2 + PHP-FPM,运行身份 www-data(uid=82) |
| 最终 payload | ?cmd=cat%20/flag%3B%23 |
| Flag | Geesec{7502f60c-5f96-4f70-9c13-34bb164bad3b} |
| 解题日期 | 2026-11-06 |
一、题目源码
访问首页 GET /,服务端直接 highlight_file(__FILE__),把源码高亮打印了出来:
<?php
/*
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-08-11 14:34
# @Repo: github.com/ProbiusOfficial/RCE-labs
# @link: hello-ctf.com
--- HelloCTF - RCE靶场 : 命令执行 - 重定向 ---
大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回到您的终端。
一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端。
同样,一个命令通常将其输出写入到标准输出,默认情况下,这也是你的终端
—— 这些是命令有回显的基础。
如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null:
$ command > /dev/null
/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,
那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到"禁止输出"的效果。
如果希望屏蔽 stdout 和 stderr,可以这样写:
$ command > /dev/null 2>&1
*/
function hello_shell($cmd){
/*>/dev/null 将不会有任何回显,但会回显错误,加上 2>&1 后连错误也会被屏蔽掉*/
system($cmd.">/dev/null 2>&1");
}
isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;
highlight_file(__FILE__);
?>
二、源码分析:四个关键事实
| 代码 | 抄出的事实 | 对解题的影响 |
|---|---|---|
isset($_GET['cmd']) | 参数名 cmd,GET 方式传入 | 入口是 /?cmd=xxx,不是 /cmd=?... |
system($cmd) | 命令执行点,无过滤、无白名单、无长度限制 | 直接打命令,不用绕 WAF |
.">/dev/null 2>&1" | 用户输入被拼接上一段重定向,输出全部丢弃 | 本题唯一障碍 |
highlight_file(__FILE__) | 每次响应末尾都会再打印一遍源码 | 干扰项,要会从响应里分辨命令输出 |
为什么输出会消失
system() 底层执行的是 /bin/sh -c "拼好的整条字符串"。传入 ls / 时,shell 实际看到的是:
ls / >/dev/null 2>&1| 片段 | 作用 |
|---|---|
>/dev/null | 把 stdout 改指向黑洞,内容直接被丢弃 |
2>&1 | 把 stderr 也指向 stdout(此刻已指向黑洞),错误一并吞掉 |
核心语法点:重定向属于「它前面那一条简单命令」,不属于整个字符串。
a ; b >/dev/null # 只吞 b 的输出,a 正常回显这就是破题的口子 —— 只要让 >/dev/null 2>&1 挂到一条「没用的命令」上,而不是挂在我的 cat 后面。
三、两个典型卡点
卡点 1:请求路径写错
错误写法(实测翻车):
GET /cmd=?ls%20/la HTTP/1.1- 路径被写成
/cmd=,nginx 找不到该文件,走index.php兜底 - 参数名
cmd丢失,ls /la成了没有键名的查询串 - 结果:永远只返回首页源码,看起来「什么都没发生」
正确写法:
GET /?cmd=ls%20/la HTTP/1.1卡点 2:把「无回显」误判为「参数没生效」
| 现象 | 错误结论 | 正确结论 |
|---|---|---|
?cmd=id 只返回源码,没有 uid= | 参数名错了 / 要用 POST / 要访问 cmd.php | 命令已执行,只是 stdout 被 >/dev/null 搬走了 |
这一点从源码就能直接判定,不需要做实验。源码已经写明输出被吞,所以无回显是预期行为。
四、绕过原理:两种手敲方案
方案 A:注释符截断(推荐,最短)
payload: cat /flag;#
拼接后: cat /flag;#>/dev/null 2>&1POSIX shell 中,# 出现在一个词的开头(前面是行首、空白,或 ; & ` | (` 这类元字符)时,就开启注释直到行尾。 |
这里 # 紧跟在 ; 之后,满足条件,后面那串 >/dev/null 2>&1 被整段忽略,不再是语法元素,cat 的 stdout 保持连在 HTTP 响应体上。
方案 B:换行截断
payload: cat /flag%0A (%0A = \n)
拼接后: cat /flag
>/dev/null 2>&1换行把一条命令拆成两条。第二行是「只有重定向、没有命令词」的合法空命令 —— shell 执行完重定向后什么也不做,退出码 0。第一行的输出正常吐出。
无效思路(别浪费时间)
| 思路 | 为什么失败 |
|---|---|
cat /flag 1>&2 | 输出转到 stderr,但 2>&1 随后把 stderr 也指向了 /dev/null |
cat /flag | tee | tee 的输出仍走 stdout,照样被吞 |
cat /dev/null | 特殊文件,读到的永远是空 |
| 故意报错探路 | PHP-FPM 下 system() 的 stderr 进 web server 的 error log,不进 HTTP 响应体(实测:加不加绕过都看不到报错) |
五、完整解题步骤(全部实测)
| 步 | 请求 | 返回 | 推出什么 |
|---|---|---|---|
| 1 | GET / | 高亮的 PHP 源码 | 题面即源码,参数名 cmd |
| 2 | ?cmd=id | 只有源码,无 uid= | 命令执行了但输出被吞,属预期 |
| 3 | ?cmd=id%3B%23 | uid=82(www-data) gid=82(www-data) | ① 参数生效 ② 命令执行 ③ 绕过有效,三件事一次验证 |
| 4 | ?cmd=ls%20/%3B%23 | etc flag home lib ... | 根目录下有 flag |
| 5 | ?cmd=ls%20-l%20/%3B%23 | -rwxr--r-- 1 root root 45 flag | 是文件不是目录,45 字节,全局可读 → 直接 cat |
| 6 | ?cmd=cat%20/flag%3B%23 | Geesec{7502f60c-...} | 拿到 flag |
第 5 步别省 —— ls -l 决定你后面是 cat 还是 cd 进去。
可直接粘进地址栏的完整 URL
http://80-cb079509-7e51-4321-928e-f2b307c31f76.challenge.ctfplus.cn/?cmd=cat%20/flag%3B%23六、Payload 编码对照表
手敲时的三个必背点:
| 字符 | 必须写成 | 原因 |
|---|---|---|
# | %23 | 浏览器会把它当 fragment 锚点,后面的内容根本不会发给服务器 |
| 空格 | %20 | 否则请求行被拆断,直接 400 |
; | %3B | query 中虽合法,但编码后最稳;Burp Repeater 里可直接写 ; |
在 Burp Repeater 里手敲时,请求行写成:
GET /?cmd=cat%20/flag%3B%23 HTTP/1.1不要指望 Burp 帮你编码。
七、读响应的技巧
这个页面的响应结构永远是:
[命令输出纯文本] ← 你要的东西在这里
<code><span ...>源码 ← highlight_file 打的,永远存在
</code>命令输出在响应最顶部、HTML 之前。在浏览器里看到一大坨彩色 HTML 就以为「失败了」,是新手最常见的误判 —— flag 通常就在第一行。
八、从「注释」到「payload」的推导链
题目注释不是「flag 在哪」的提示,而是把要绕过的机制讲给你听。逐句对照:
| 注释原话 | 暴露的事实 | 对 payload 的意义 |
|---|---|---|
| 「命令通常将其输出写入标准输出,默认这也是你的终端 —— 这些是命令有回显的基础」 | 回显通道 = stdout | 本题只有 stdout 一条路,别指望 stderr |
「command > /dev/null,写入的内容都会被丢弃」 | 屏蔽手段 = stdout 改指向黑洞 | 输出没消失,是被搬走了 |
「/dev/null 是特殊文件,尝试读取什么也读不到」 | 排除 cat /dev/null | 少走一条死路 |
「/>/dev/null 将不会有任何回显,但会回显错误,加上 2>&1 后连错误也会被屏蔽掉」 | 作者已堵「报错泄漏」 | 排除「故意报错探路」 |
system($cmd.">/dev/null 2>&1") | 尾巴被硬编码拼接,字节完全已知 | 不用猜,可精确针对这串字节做手术 |
推导三步:
① 注释说「stdout 连终端才是回显的基础」
→ cat /flag 没输出,不是没执行,是 stdout 被改道了
② 代码说「$cmd 后面拼了 >/dev/null 2>&1」
→ 这段字节在语法上只能附着在「最后一个简单命令」上
cat /flag >/dev/null 2>&1 ← 只能附在 cat 上,必死
③ 让它「附不上去」即可
cat /flag ; # >/dev/null 2>&1 ← # 让后半截变注释
cat /flag \n >/dev/null 2>&1 ← \n 让后半截附在空命令上关键在第 ② 步:注释告诉你尾巴是「拼接」的、是「后缀」。既是后缀,就一定只有最后一个简单命令能承接它。找到这个语法归属点,才有 ; + # 的切入点。
九、可复用的方法论
碰到「命令能执行但没回显」的题,按这个顺序想:
| 顺序 | 要问的问题 | 对应手法 |
|---|---|---|
| 1 | 能不能让拼在尾部的重定向失效? | ;# 注释、%0A 换行 |
| 2 | 能不能把输出写到可访问的地方? | > /var/www/html/x、写日志 |
| 3 | 能不能带外传出去? | curl / ping / DNSLog |
| 4 | 有没有别的回显通道? | 报错、时间盲注(sleep)、退出码 |
本题属于第 1 类,最短也最干净。
看到源码里带大段注释的靶场,别当「题面废话」跳过,按这个顺序读:
- 注释在讲哪个 Linux/Shell 概念?(这里是重定向)
- 这个概念在这段代码里被用来干什么?(屏蔽我的回显)
- 这个概念在语法上的边界在哪?(重定向只作用于一条简单命令)
- 边界处就是突破口。
十、汇总表
| 项目 | 内容 |
|---|---|
| 题型 | PHP 命令执行 + 输出重定向屏蔽 |
| 入口 | GET /?cmd=<命令>(参数名来自 $_GET['cmd']) |
| 执行点 | system($cmd.">/dev/null 2>&1"),无任何过滤 |
| 核心障碍 | 输出被拼接重定向到 /dev/null,stdout + stderr 双通道全灭 |
| 突破依据 | 重定向只能附着在最后一条简单命令上;; + # 让它失去承接对象 |
| 验证顺序 | id 验证 → ls / 定位 → ls -l 确认属性 → cat 取值 |
| 最优 payload | ?cmd=cat%20/flag%3B%23 |
| 备用 payload | ?cmd=cat%20/flag%0A |
| 无效思路 | 1>&2、|tee、cat /dev/null、靠报错探路 |
| 编码要点 | #→%23、空格→%20、;→%3B |
| 响应阅读 | 命令输出在响应最前,源码高亮在最后 |
| Flag | Geesec{7502f60c-5f96-4f70-9c13-34bb164bad3b} |
一句话总结:源码把「怎么屏蔽我」写明了,我只需要在它屏蔽的语法边界上插一个 ; 和一个 #。
- 本文链接: https://quinc.cn/posts/helloctf_rce_labs_lv8_writeup/
- 版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。

