本文首发于先知:https://xz.aliyun.com/t/6914
漏洞分析
FastJson1.2.24 RCE
在分析1.2.47的RCE之前先对FastJson1.2.24版本中的RCE进行一个简单的漏洞链分析,为在本篇文章后面1.2.47中漏洞的调用过程做个铺垫。在本文中的1.2.24的payload只研究针对类jdbcRowSetImpl的,因为针对templatesImpl的payload而言在高版本java中要开启Feature.SupportNonPublicField才能进行对非共有属性的反序列化处理,因此是存在一定的限制的。因此针对类jdbcRowSetImpl的payload更具有通用型一些。本中所示代码及jar包请见附件。
exp.java
Exploit.class
首先在本地用marshalsec起一个ldap服务,用rmi也可以,只需将datasourcename中的服务更改为rmi即可
本文中环境为jdk1.8.0,首先在parse处下一个断点然后运行exp.java,本来打算正向找到setvalue函数,可是fastjson扫描json字符串的过程及反序列化时处理jdbcrowsetimpl类处处理流程过于繁琐,中间不知道得跟进多少个F7,很容易让人没有耐心继续调试分析,遂这里直接关注漏洞的核心点,即我们在setDataSourceName处下断点
即此时将从我们payload中指定的DataSourceName中去加载工厂类
在setautoconnect函数中调用了connect()函数,跟进后此时就能看到熟悉的lookup函数啦,我们知道jndi注入攻击中从远程加载恶意工厂类即是我们控制了lookup的入口参数,即控制了远程工厂类的加载地址,即此处即为关键点,F7步入
此时getDataSourceName()的返回值也可以看到即为我们所指定的恶意工厂类地址
然后跟进lookup看看如何调用实例化,这里调用了getURLOrDefaultInitCtx(name).lookup()函数
此时就到了java的命名服务管理的类,此时调用getURLContext函数请求ldap,进一步在其中调用getURLObject来通过从远程的ldap服务获取Context对象
最终完成exploit.class类的实例化,也就是工厂类的实例化,熟悉的getObjectInstance(),此时就完成了反序列化,从而触发exploit里的构造函数
整个漏洞的函数调用栈如下图所示
以下是一些针对fastjson不同版本的payload,可以看到bypass其实在@type类的前面加上L或者[,这都是因为fastjson在处理域时会将扫描到的这些字符进行去除
FastJson1.2.47 RCE
漏洞影响版本:fastjson<1.2.51
exp.java
Exploit.class与1.2.24相同,在1.2.24以后fastjson默认关闭了反序列化任意类的操作,增加了checkautotype,也就是指定要用来反序列化的类不能够在一些黑名单中,从而做了一定的限制。此次漏洞利用的核心点是java.lang.class这个java的基础类,在fastjson 1.2.48以前的版本没有做该类做任何限制,加上代码的一些逻辑缺陷,造成黑名单以及autotype的绕过。我们仍然在parse处下断点,这里正向F7跟进到如下图所示
在DefaultJSONParser中,对json字符串进行扫描解析,此时解析到key值为a,接下来就对a中的字段进行解析,这里解析到@type以后,进而扫描到typename为java.lang.class,此时将调用parseconfig类的checkautotype函数来检测要反序列化的类。
接着在checkautotype函数中此时从mappings中或者deserializers.findClass()方法来获取反序列化对象的对应类
这里实际上在findClass函数中的this.bucktet中去找要反序列化的类,而在bucket变量中包含了大量的基础类
这里关键点在这几句代码,找到的类通过getName()函数调用后就能获得类的类名,然后将该类名与传入的要反序列化的类名比较,若相等,即找到,则直接返回该类,这里返回值为类类型。
接着回到IedentityHashMap的checkautotype函数中返回java.lang.class类
进一步回到com/alibaba/fastjson/parser/DefaultJSONParser.class中调用了对应的序列化处理类,也就是有了要反序列化的类,此时咋处理这类就是接下来的操作,获取的类为com.alibaba.fastjson.serializer.MiscCodec
接下来到364行即开始调用MiscCodec的deserialze函数
在其deserialze函数中可以看到其首先取得了我们payload字符串中var对应的值,也就是我们要利用的JdbcRowSetImpl类
接着对我们要反序列化的类进行一个类型判断,一直到303行,此时类满足判断,即java.lang.class为类类型的类,所以直接返回strVal,也就是恶意类
在TypeUtils类中可以看到此时loadclass函数的cache默认为true,这也是个重要的导致bypass的地方
那么这里首先loadclass尝试在mapping中取加载java.lang.class要引入的类,要是没有找到的话,此时将通过mapping.put方法将该类放到mapping中,那么不需要autotype就完成了对恶意类的加载
完成了对第一部分的json的字符串的解析已经成功加载了恶意类,因此此时json字符串继续向后扫描,扫描到b,此时处理b对应的值,当扫描到@type时继续调用checkautotype函数
那么因为之前我们已经通过java.lang.class的加载类的功能将jdbcrowsetimpl类加载到了mappings中,因此这里getclassfrommapping就能够返回我们的恶意类
接着又回到Defaultjsonparse类中
接着对于jdbcrowsetimpl类,调用fastjsonDeserialize_1_jdbcrowsetimpl类来对该类进行解析调用,那么接下来因为已经bypass了黑名单和autotype,因此之后rce的流程和1.2.24的一样了
还是回到熟悉的setAutoCommit函数,具体流程见上面1.2.24的描述,最终能够触发calc,此时用mashalsec打ldap一样是可以的
整个漏洞的函数调用栈如下图所示
漏洞修复
在1.2.48版本的补丁中,首先黑名单做了更新,其中就包含了java.lang.class这个类,并且MiscCodec中也将传入的cache参数置为了false,这样通过payload中a部分的java.lang.class引入JdbcRowSetImpl类,b部分通过
mappings获取JdbcRowSetImpl类的方法就失效了。
参考
https://www.anquanke.com/post/id/181874
https://www.kingkk.com/2019/07/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-1-2-24-1-2-48/
https://saucer-man.com/information_security/346.html