存货文,积了好久,懒得发233,今天发下。
本文写的很垃圾,建议学过基本的之后,当笔记看就好了。本文写了点可能比较新奇的东西。
想认真学的还是去看文末的Ref吧。
前提:和回显类似。得先RCE
好处:规避静态文件的查杀
原理:Java Web不像apache + php模式,每一次请求都是生成新的php实例。Java Web是长期运行的(同理的还有.net、go、python这些)。Web程序必定会有相关的变量、逻辑进行请求分发的操作。当我们RCE之后,若能控制进行请求分发的变量,便能控制请求分发的逻辑。
常见的操作有:新增控制器、修改控制器、添加拦截器等
步骤:
- 构造恶意类
- 获取上下文对象
- 拿到Filter、Servlet这些东西的注册类
- 往注册类里注册恶意类
SpringBoot
本例环境为:SpringBoot2.6.1
获取上下文对象
上下文对象中存放了大量的bean。大部分是Spring运行依赖的类对象。拿到这些bean。相当于成功了一半。
getAttribute方式
1 | ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); |
1 | ServletRequestAttributes requestAttributes1 = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); |
WebApplicationContextUtils方式
1 | ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); |
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 | processedRequest = checkMultipart(request); //组装请求为Multipart |
getHandler()
方法
getHandler()
是比较重要的方法。主要逻辑是根据请求url,找对应的处理器,也就是HandlerMapping的适配。
HandlerMapping
如下
1 | RequestMappingHandlerMapping |
HandlerMapping#getHandler()
会调用AbstractHandlerMapping#getHandlerInternal()
。对于这个方法的实现。有可分为三个抽象类
1 | AbstractHandlerMethodMapping |
深究后可以发现,前两个抽象类都存在着路由映射属性。而RouterFunctionMapping
比较特殊,后文说。
AbstractHandlerMethodMapping
和AbstractUrlHandlerMapping
大同小异。只是最后的映射Map不一样而已。
1 | //AbstractHandlerMethodMapping |
注入马的思路:操作路由映射的Map。控制程序的路由走向。将路由映射到恶意类的恶意方法中
那马为什么要加内存二字呢?因为我们可以通过defineClass()
,直接将字节码还原成对象实例。无文件落地。
找Map赋值位置的思路:找到Map变量,搜索赋值点。最终找到这两个方法,对于上文的两个抽象类。
1 | AbstractHandlerMethodMapping$MappingRegistry#register(T mapping, Object handler, Method method) |
那接下来简单了。调用这两接口。就可以注册任意的路由映射了。
AbstractHandlerMethodMapping
注册内存马
AbstractHandlerMethodMapping的注册固然可以使用registerMapping()
。但是他会记录一个logger。不优雅
AbstractHandlerMethodMapping#registerMapping
1 | public void registerMapping(T mapping, Object handler, Method 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 | protected Object getHandlerInternal(HttpServletRequest servletRequest) throws Exception { |
找RouterFunctionMapping
的继承类,找到一个RouterFunctions$ResourcesRouterFunction
该类有个Function lookupFunction
属性。可以存放lambda。
1 | private static class ResourcesRouterFunction extends AbstractRouterFunction<ServerResponse> { |
是不是我们可以在自定义个恶意的lambda类呢?是的,完全可以。
实测是能用的。
关于内存马检测
看了下4ra1n师傅的检测。似乎只检测了RequestMappingHandlerMapping
里的mappingRegistry
- 2021.12.06
那我们可以通过注入AbstractUrlHandlerMapping
和RouterFunctionMapping
内存马即可规避检测
隐蔽方式
把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 | private FilterRegistration.Dynamic addFilter(String filterName, |
那么动态注册Filter的思路也很明显了:反射修改context.getState()
的值,让其值为LifecycleState.STARTING_PREP
。就可以正常执行addFilter()
的逻辑了
反射路径:
1 | final ApplicationContext.context |
添加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 | public final void invoke(Request request, Response response){ |
看进新建的ApplicationFilterFactory.createFilterChain()
,该方法依据context
上下文变量查找对应的Filter
。若filterMap
在上下文中有存放,就会新建一个ApplicationFilterChain
1 | public static ApplicationFilterChain createFilterChain(ServletRequest request, |
Filter生效
要让Filter真正生效,需要修改StandardContext.filterConfigs
的属性。往里头插入新增的Filter。但这个暂且没看到哪里有赋值点。目测只能手动反射,为其新增一个Filter
在Debug中看到ApplicationFilterConfig
的属性有点复杂,怎么构造呢?去看看源码中ApplicationFilterConfig
是怎么被构建的,找找是否有Factory类或者create方法。找到*StandardContext#filterStart()*。发现直接new就好了。
1 | ApplicationFilterConfig filterConfig = |
看来还需要构造多一个FilterDef
。这个类没那么复杂,直接调用setXX()
就可以完成属性赋值了。
完整POC
思路:
- 临时修改
LifecycleBase.state
为LifecycleState.STARTING_PREP
- 通过
ApplicationContextFacade.addFilter()
注册一个Filter。 - 注册后设置Filter,指定其匹配路径
- 反射修改
StandardContext.filterConfigs
,新增Filter,使前面注册的Filter生效 - 最后记得将
LifecycleBase.state
改回LifecycleState.STARTED
定义一个FIlter。但不在web.xml
中配置
1 | package com.evil; |
Servlet中动态注册Filter
1 | //[+] 获取需要的属性StandardContext |
效果:

实战内存马角度
三梦师傅用的是“修改程序逻辑,初始化静态变量”这种方式拿request,然后顺利拿到context的。但由于这种方式对于Filter类型的程序不太友好,下文使用“Tomcat7另一个静态变量”这种方式,来获取context。
打内存马,最常用的就是defineClass()
。那我们来实现下。整一个具有CC11依赖的Tomcat。
CC11是用TemplatesImpl
来defineClass()
的。但是这里有一个小坑。我们来看:
1 | private void defineTransletClasses() |
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 | if (pos < n) { |
继续往前看,这些东西是哪里被赋值的呢?找到*StandardWrapperValve#invoke()*。有一行:Filter的调用都是根据filterChain
的值,进行依次调用的。
1 | ApplicationFilterChain filterChain = |
找找filterChain
如何被赋值的
ApplicationFilterFactory#createFilterChain()
1 | StandardContext context = (StandardContext) wrapper.getParent(); |
在*filterChain.addFilter()*中,即存在本小节开头说的,几个关键属性的赋值
1 | filters[n++] = filterConfig; |
看到这里。可以整理出流程:
- 在每一个request进来时,都会进入StandardWrapperValve#invoke()。该方法会调用ApplicationFilterFactory.createFilterChain组装
filterChain
filterChain
根据StandardContext.filterMaps
和StandardContext.filterConfigs
组装- 组装完毕后,根据
filterChain
,依次进行Filter的调用
那么我们的控制思路就是:由于可获取StandardContext
,基于StandardContext
反射修改filterMaps
。以此将内存马打入
为了适配不同版本的Tomcat。需要额外进行些处理。
Tomcat6
FilterDef有点不一样,没用setFilter()。他是根据FIlterClass动态加载的
ApplicationFilterFactory#createFilterChain()
1 | isCometFilter = filterConfig.getFilter() instanceof CometFilter; //根据filterDef.filterClass动态ClassLoader加载 |
直接反射设置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 | "context=/tomcat1_war_exploded,host=localhost,type=Manager" -> {com.sun.jmx.mbeanserver.NamedObject@3592} |
里头的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备份了。写的很烂就不公开了。想瞄瞄的话问小盘盘要吧。