This is a very typical operation of dynamically loading bytecode, which can execute arbitrary code in JDK8.
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;
public class evilByteClassloader {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
String evilClassBase64 = "Base64 encoding of malicious bytecode";
byte[] decode = Base64.getDecoder().decode(evilClassBase64);
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class evilClassloader = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), decode, 0, decode.length);
evilClassloader.newInstance();
{}
{}
However, with the update of java versions, this method has been limited.
Modularization of JDK9
Oracle official documentation:Java Platform, Standard Edition What’s New in Oracle JDK 9, Release 9
Java 9 introduced the modularization system (Project Jigsaw). The main purpose of modularization is to improve the maintainability, scalability, and security of Java, while improving the construction and management of large applications and libraries.
Module: A module is a code unit with a clear boundary, containing a set of related packages, classes, and resources. Each module has a description file (
module-info.java
),used to declare the name, dependencies, and exported packages of the module.
In Java 9, each module is defined by amodule-info.java
file to define. This file is located in the root directory of the module and contains the metadata of the module. Here is an example:
module com.example.myModule {
exports com.example.myModule.api; // Export public API
requires java.sql; // Declare dependency on the java.sql module
{}
Java 9 also modularized the JDK itself
java.base
: Core Java class library, all modules depend on this module.java.sql
: Modules related to database connections.java.xml
: Modules related to XML processing.
Modularization can be likened to building a house, dividing it into multiple rooms with clear functions. Each room (module) is responsible for specific tasks, such as the living room for receiving guests, the kitchen for cooking, etc., making the entire house (software project) structure clear, easy to understand and maintain. Modules do not interfere with each other, reducing functional conflicts, allowing for independent development and testing, and improving development efficiency. In addition, modularization can be loaded on demand, saving resources, and making maintenance work simpler, only repairing the module that has a problem without affecting other parts.
Impact
In fact, this update does not have much impact on the above code. We can still dynamically load bytecode, but a warning message will be output during code execution, reminding you that you are performing illegal reflection access. This will not prevent code execution, but it will remind you that this practice may be prohibited in the future.
警告:已发生非法反射访问操作
警告:EvilClassLoader.evilByteClassloader(文件路径:/D:/java_local/Temp/target/classes/)通过非法反射访问方法java.lang.ClassLoader.defineClass(byte[],int,int)
警告:请考虑向EvilClassLoader.evilByteClassloader的维护者报告此问题
警告:使用--illegal-access=warn启用进一步非法反射访问操作的警告
警告:在未来的版本中,所有非法访问操作都将被拒绝
JDK17的强封装
某些工具和库使用反射来访问仅限内部使用的JDK部分。这种反射的使用对JDK的安全性和可维护性产生负面影响。为了帮助迁移,从JDK 9到JDK 16允许这种反射继续,但会发出关于非法反射访问的警告。然而,JDK 17是强封装的,因此默认情况下不再允许这种反射。
According toOracle documentationFor security reasons, starting from JDK 17, strong encapsulation is used for java itself, called Strong Encapsulation in the original text. Any reflection on non-public variables and methods in java.* will throw an InaccessibleObjectException exception.
JDK documentationTwo reasons for encapsulating java api are explained:
Reflection on java code is not safe, for example, it can call the defineClass method of ClassLoader, which can inject arbitrary code into the program at runtime.
These non-public APIs of java itself are non-standard, and relying on the use of this API will bring a burden to the maintenance of JDK.
So starting from JDK 9, restrictions on reflection of java api were prepared, and it was officially disabled until JDK 17.
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @404b9385
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
at EvilClassLoader.evilByteClassloader.main(evilByteClassloader.java:13)
This information indicates:
defineClass
methods are declared asprotected
, and it belongs tojava.base
module.java.base
The module has not openedjava.lang
The package is opened to unnamed modules (i.e., modules that are not explicitly declared).
You can open the moudle-info.class file of the java.base module to see, there is no opens instruction used, which means that no resources are opened for reflection access by external modules.
Here is an explanation of some of the keywords:
module
Define: Used to declare a module. Each module has a unique name.
Example:
module com.example.myModule {
{}
requires
Define: Used to declare other modules that the current module depends on. A module can depend on one or more modules.
Example:
module com.example.myModule {
requires com.example.otherModule;
{}
exports
Define: Used to declare the packages that the current module wants to make public. Through
exports
The public classes and interfaces in the declared package can be accessed by other modules.Example:
module com.example.myModule {
exports com.example.myModule.api;
{}
opens
Define: with
exports
Similarly, but allows other modules to access the specified package through reflection. Suitable for situations that need to dynamically access classes and members, such as serialization and dependency injection.Example:
module com.example.myModule {
opens com.example.myModule.internal to com.example.otherModule;
{}
And the dynamic class loading call is reflection call java.lang.ClassLoader#defineClass, which is located under the java.base module. This module does not allow reflection calls from external modules, so the above code cannot successfully dynamically load bytecode.
Bypass
Of course, we can still bypass the above restrictions.
We can easily trace the problem to the following function based on the error message.
private boolean checkCanSetAccessible(Class<?> caller,
Class<?> declaringClass,
boolean throwExceptionIfDenied) {
if (caller == MethodHandle.class) {
throw new IllegalCallerException(); // should not happen
{}
Module callerModule = caller.getModule();
Module declaringModule = declaringClass.getModule();
if (callerModule == declaringModule) return true;
if (callerModule == Object.class.getModule()) return true;
if (!declaringModule.isNamed()) return true;
String pn = declaringClass.getPackageName();
int modifiers;
if (this instanceof Executable) {
modifiers = ((Executable) this).getModifiers();
} else {}}
modifiers = ((Field) this).getModifiers();
{}
// class is public and package is exported to caller
boolean isClassPublic = Modifier.isPublic(declaringClass.getModifiers());
if (isClassPublic && declaringModule.isExported(pn, callerModule)) {
// member is public
if (Modifier.isPublic(modifiers)) {
return true;
{}
// member is protected-static
if (Modifier.isProtected(modifiers)
&& Modifier.isStatic(modifiers)
&& isSubclassOf(caller, declaringClass)) {
return true;
{}
{}
// package is open to caller
if (declaringModule.isOpen(pn, callerModule)) {
return true;
{}
if (throwExceptionIfDenied) {
// not accessible
String msg = "Unable to make ";
if (this instanceof Field)
msg += "field ";
msg += this + " accessible: " + declaringModule + " does not \"";
if (isClassPublic && Modifier.isPublic(modifiers))
msg += "exports";
else
msg += "opens";
msg += " " + pn + "\" to " + callerModule;
InaccessibleObjectException e = new InaccessibleObjectException(msg);
if (printStackTraceWhenAccessFails()) {
e.printStackTrace(System.err);
{}
throw e;
{}
return false;
{}
This code is part of Java's reflection mechanism, mainly used to check whether a class or member (such as a method, field) can be accessed via reflection. Its function is to determine whether a caller (caller
)whether it has permission to access a declared class or member (declaringClass
).
In this code snippet,checkCanSetAccessible
There are multiple conditions that can returntrue
,means that the caller can access the specified class or member. The following are all methods that can returntrue
Summary of the situation:
same module:if
callerModule
anddeclaringModule
is the same module (callerModule == declaringModule
),returnstrue
unnamed module:if
callerModule
isObject
the module of the class (unnamed module), returnstrue
(callerModule == Object.class.getModule()
).the declarer of the unnamed module:if
declaringModule
is not a named module (!declaringModule.isNamed()
),returnstrue
public class and exported package:if
declaringClass
is a public class (isClassPublic
fortrue
)and the declaring module exports the package (declaringModule.isExported(pn, callerModule)
):if the member is public (Modifier.isPublic(modifiers)
),returnstrue
。Or if the member is a protected static member (Modifier.isProtected(modifiers) && Modifier.isStatic(modifiers)
)and the caller is a subclass of the declaring class (isSubclassOf(caller, declaringClass)
),returnstrue
the open package:if
declaringModule
for the package (pn
)is open (declaringModule.isOpen(pn, callerModule)
),returnstrue
We can focus on the first case, that iscallerModule
anddeclaringModule
is the same module. Because its comparison is to get the calling classClass
object'sMoudle
Property to compare with the declared classClass
object'smodule
Property to make a comparison, that is, the current class we are running andClassLoader
of theClass
object'smodule
Property for comparison, if we can modify the current classClass
object'smodule
Property, set andClassLoader
of theClass
object'smodule
property is the same, then you can returntrue
, that is, the reflection call of the current class runningClassLoader
of thedefineclass
method.
So how did it do it? Don't be in a hurry, we need to introduce a class because the above operations will be implemented based on the methods of this class.
the Unsafe class
Unsafe
is located insun.misc
is a class under the package, which mainly provides some methods for executing low-level, unsafe operations, such as direct access to system memory resources, autonomous management of memory resources, etc. These methods play a very important role in improving the running efficiency of Java and enhancing the underlying resource operation ability of Java language. However, due toUnsafe
class enables Java language to have the ability to operate memory space similar to C language pointers, which undoubtedly also increases the risk of program-related pointer problems. The excessive and incorrect use ofUnsafe
class will increase the probability of program errors, making Java, a language that is originally safe, no longer 'safe'. Therefore,Unsafe
The use of it must be cautious. — Excerpt:Java Magic Class Unsafe Detailed Explanation | JavaGuide
This class exactly provides the modification of the module of the Class object.
Unsafe existsdefineClass
anddefineAnonymousClass
However, they were all deleted after JDK17. We will not discuss them andClassLoader#defineclass
The functions are the same.
We focus on the following methods:
objectFieldOffset
The main function is to use reflection to get the field offset
public long objectFieldOffset(Field f) {
if (f == null) {
throw new NullPointerException();
{}
Class<?> declaringClass = f.getDeclaringClass();
if (declaringClass.isHidden()) {
throw new UnsupportedOperationException("can't get field offset on a hidden class: " + f);
{}
if (declaringClass.isRecord()) {
throw new UnsupportedOperationException("can't get field offset on a record class: " + f);
{}
return theInternalUnsafe.objectFieldOffset(f);
{}
getAndSetObject
It retrieves the current value of an object at a specific memory offset and replaces it with a new value.
public final Object getAndSetObject(Object o, long offset, Object newValue) {
return theInternalUnsafe.getAndSetReference(o, offset, newValue);
{}
putObject
It is used to write an object (or reference) to the specified memory offset of an object
public void putObject(Object o, long offset, Object x) {
theInternalUnsafe.putReference(o, offset, x);
{}
The putObject and getAndSetObject methods are very similar, butgetAndSetObject
It will return the old value that was replaced, of course, there are also some differences in detail, such asgetAndSetObject
It will use atomic operations to ensure simultaneous execution of reading and writing. However, in terms of modifying the module attribute of the Class object of the class, both can be used.
I have been talking about the module of the Class object of the class, that is to say, the module attribute is in the Class class, when the class is loaded into the virtual machine, a Class object of the class will be generated, and there is only one existing.
You can obtain an Unsafe object using the following code
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
We can directly reflect to obtain this class because the module where the Unsafe class is locatedjdk.unsupported
ofmoudle-info.class
file, using the opens instruction to exposesun.misc
package, because we can perform reflection operations on the classes under this package, naturally includingsun.misc.Unsafe
Then let's use the above knowledge to write dynamic bytecode loading for JDK versions 17 and above.
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;
public class JDKbypass {
public static void main(String[] args) throws Exception {
String evilClassBase64 = "Base64 encoding of malicious bytecode";
byte[] bytes = Base64.getDecoder().decode(evilClassBase64);
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
Class currentClass = JDKbypass.class;
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
//Use putObject method to set module
unsafe.putObject(currentClass, offset, baseModule);
//Use getAndSetObject method to set module
//unsafe.getAndSetObject(currentClass,offset,baseModule);
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
((Class) method.invoke(ClassLoader.getSystemClassLoader(), bytes, 0, bytes.length)).newInstance();
{}
{}

评论已关闭