Tomcat Filter之动态注入
前言
最近,看到好多不错的关于“无文件Webshell”的文章,对其中利用上下文动态的注入Filter
的技术做了一下简单验证,写一下测试总结,不依赖任何框架,仅想学习一下tomcat的filter。
先放几篇大佬的文章:
- Tomcat中一种半通用回显方法
- tomcat结合shiro无文件webshell的技术研究以及检测方法
- Tomcat通用回显学习
- 基于全局储存的新思路 | Tomcat的一种通用回显方法研究
- threedr3am/ysoserial
Filter介绍
详细介绍略,简单记录一下我的理解:
- 过滤器(Filter):用来对指定的URL进行过滤处理,类似
.net core
里的中间件,例如登录验证过滤器可以用来限制资源的未授权访问; - 过滤链(FilterChain):通过URL匹配动态将所有符合URL规则的过滤器共同组成一个过滤链,顺序有先后,类似
.net core
的管道,不过区别在于过滤链是单向的,管道是双向;
同Servlet,一般Filter的配置方式:
- web.xml
- @WebFilter修饰
Filter注册调用流程
新建一个登录验证的Filter: SessionFilter.java
package com.reinject.MyFilter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* 判断用户是否登录,未登录则退出系统
*/
@WebFilter(filterName = "SessionFilter", urlPatterns = "/*",
initParams = {@WebInitParam(name = "logonStrings", value = "index.jsp;addFilter.jsp"),
@WebInitParam(name = "includeStrings", value = ".jsp"),
@WebInitParam(name = "redirectPath", value = "/index.jsp"),
@WebInitParam(name = "disabletestfilter", value = "N")})
public class SessionFilter implements Filter {
public FilterConfig config;
public void destroy() {
this.config = null;
}
public static boolean isContains(String container, String[] regx) {
boolean result = false;
for (int i = 0; i < regx.length; i++) {
if (container.indexOf(regx[i]) != -1) {
return true;
}
}
return result;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest hrequest = (HttpServletRequest)request;
HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper((HttpServletResponse) response);
String logonStrings = config.getInitParameter("logonStrings"); // 登录登陆页面
String includeStrings = config.getInitParameter("includeStrings"); // 过滤资源后缀参数
String redirectPath = hrequest.getContextPath() + config.getInitParameter("redirectPath");// 没有登陆转向页面
String disabletestfilter = config.getInitParameter("disabletestfilter");// 过滤器是否有效
if (disabletestfilter.toUpperCase().equals("Y")) { // 过滤无效
chain.doFilter(request, response);
return;
}
String[] logonList = logonStrings.split(";");
String[] includeList = includeStrings.split(";");
if (!this.isContains(hrequest.getRequestURI(), includeList)) {// 只对指定过滤参数后缀进行过滤
chain.doFilter(request, response);
return;
}
if (this.isContains(hrequest.getRequestURI(), logonList)) {// 对登录页面不进行过滤
chain.doFilter(request, response);
return;
}
String user = ( String ) hrequest.getSession().getAttribute("useronly");//判断用户是否登录
if (user == null) {
wrapper.sendRedirect(redirectPath);
return;
}else {
chain.doFilter(request, response);
return;
}
}
public void init(FilterConfig filterConfig) throws ServletException {
config = filterConfig;
}
}
观察一个正常请求的函数栈:
_jspService:14, index_jsp (org.apache.jsp)
service:70, HttpJspBase (org.apache.jasper.runtime)
service:731, HttpServlet (javax.servlet.http)
service:439, JspServletWrapper (org.apache.jasper.servlet)
serviceJspFile:395, JspServlet (org.apache.jasper.servlet)
service:339, JspServlet (org.apache.jasper.servlet)
service:731, HttpServlet (javax.servlet.http)
internalDoFilter:303, ApplicationFilterChain (org.apache.catalina.core)
doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:241, ApplicationFilterChain (org.apache.catalina.core)
doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
doFilter:66, SessionFilter (com.reinject.MyFilter)
internalDoFilter:241, ApplicationFilterChain (org.apache.catalina.core)
doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
invoke:218, StandardWrapperValve (org.apache.catalina.core)
invoke:122, StandardContextValve (org.apache.catalina.core)
invoke:505, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:169, StandardHostValve (org.apache.catalina.core)
invoke:103, ErrorReportValve (org.apache.catalina.valves)
invoke:956, AccessLogValve (org.apache.catalina.valves)
invoke:116, StandardEngineValve (org.apache.catalina.core)
service:442, CoyoteAdapter (org.apache.catalina.connector)
process:1082, AbstractHttp11Processor (org.apache.coyote.http11)
process:623, AbstractProtocol$AbstractConnectionHandler (org.apache.coyote)
run:316, JIoEndpoint$SocketProcessor (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)
找到最开始的ApplicationFilterChain
位置,调用者是StandardWrapperValve
的invoke
,再观察invoke
代码不难看出是用ApplicationFilterFactory
动态生成的ApplicationFilterChain
:
// Create the filter chain for this request
ApplicationFilterFactory factory =
ApplicationFilterFactory.getInstance();
ApplicationFilterChain filterChain =
factory.createFilterChain(request, wrapper, servlet);
createFilterChain
根据xml配置动态生成一个过滤链,部分代码如下:
// 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
String servletName = wrapper.getName();
// Add the relevant path-mapped filters to this filter chain
for (int i = 0; i < filterMaps.length; i++) {
if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMaps[i], requestPath))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
boolean isCometFilter = false;
if (comet) {
try {
isCometFilter = filterConfig.getFilter() instanceof CometFilter;
} catch (Exception e) {
// Note: The try catch is there because getFilter has a lot of
// declared exceptions. However, the filter is allocated much
// earlier
Throwable t = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(t);
}
if (isCometFilter) {
filterChain.addFilter(filterConfig);
}
} else {
filterChain.addFilter(filterConfig);
}
}
所有的filter
可以通过context.findFilterMaps()
方法获取,FilterMap结构如下:
FilterMap
中存放了所有filter
相关的信息包括filterName
和urlPattern
。
有了这些之后,使用matchFiltersURL
函数将每个filter
和当前URL
进行匹配,匹配成功的通过context.findFilterConfig
获取filterConfig
,filterConfig
结构如下:
之后将filterConfig
添加到filterChain
中,最后回到StandardWrapperValve
中调用doFilter
进入过滤阶段。
这个图(@宽字节安全)能够很清晰的看到整个filter流程:
通过上面的流程,可知所有的filter
信息都是从context(StandardContext)
获取到的,所以假如可以获取到这个context
就可以通过反射的方式修改filterMap
和filterConfig
从而达到动态注册filter的目的。
获取context
打开jconsole
,获取tomcat
的Mbean
:
感觉其中好多地方都可以获取到context
,比如RequestProcessor
、Resource
、ProtocolHandler
、WebappClassLoader
、Value
。
Value获取
代码:
MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
// 获取mbsInterceptor
Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
field.setAccessible(true);
Object mbsInterceptor = field.get(mBeanServer);
// 获取repository
field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
field.setAccessible(true);
Object repository = field.get(mbsInterceptor);
// 获取domainTb
field = Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb");
field.setAccessible(true);
HashMap<String, Map<String, NamedObject>> domainTb = (HashMap<String,Map<String,NamedObject>>)field.get(repository);
// 获取domain
NamedObject nonLoginAuthenticator = domainTb.get("Catalina").get("context=/,host=localhost,name=NonLoginAuthenticator,type=Valve");
field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
field.setAccessible(true);
Object object = field.get(nonLoginAuthenticator);
// 获取resource
field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
field.setAccessible(true);
Object resource = field.get(object);
// 获取context
field = Class.forName("org.apache.catalina.authenticator.AuthenticatorBase").getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(resource);
反射弧:mBeanServer->mbsInterceptor->repository->domainTb->nonLoginAuthenticator->resource->context
。
通过StandardContext注册filter
通过filter流程分析可知,注册filter需要两步:
- 修改
filterConfigs
; - 将filter插到
filterMaps
的0
位置;
在此之前,先看一下我们比较关心的context中三个成员变量:
- filterConfigs:filterConfig的数组
- filterRefs:filterRef的数组
- filterMaps:filterMap的数组
filterConfig
的结构之前看过,filterConfig.filterRef
实际和context.filterRef
指向的地址一样:
Expression: ((StandardContext) context).filterConfigs.get("SessionFilter").filterDef == ((StandardContext) context).filterDefs.get("SessionFilter");
从StandardContext
类的方法看,可以调用StandardContext.addFilterDef()
修改filterRefs
,然后调用StandardContext.filterStart()
函数会自动根据filterDefs
重新生成filterConfigs
:
filterConfigs.clear();
for (Entry<String, FilterDef> entry : filterDefs.entrySet()) {
String name = entry.getKey();
if (getLogger().isDebugEnabled())
getLogger().debug(" Starting filter '" + name + "'");
ApplicationFilterConfig filterConfig = null;
try {
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;
}
}
综上,修改filterRefs
和filterConfigs
的代码如下:
// Gen filterDef
filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
// Add filterDef
context.addFilterDef(filterDef);
// Refresh filterConfigs
context.filterStart();
改filterMaps
就简单了,添加上去改一下顺序加到0
位置:
// filterMap
filterMap.setFilterName(filterName);
filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST));
filterMap.addURLPattern(filterUrlPatern);
context.addFilterMap(filterMap);
// Order
Object[] filterMaps = context.findFilterMaps();
Object[] tmpFilterMaps = new Object[filterMaps.length];
int index = 1;
for (int i = 0; i < filterMaps.length; i++)
{
FilterMap f = (FilterMap) filterMaps[i];
if (f.getFilterName().equalsIgnoreCase(filterName)) {
tmpFilterMaps[0] = f;
} else {
tmpFilterMaps[index++] = f;
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
通过ApplicationContext注册filter
多次调试发现有多处context,上面一直用的都是StandardContext
,观察该结构发现还有一个私有变量context
,类型为ApplicationContext
,通过他的定义发现其实就是一个ServletContext
:
public class ApplicationContext implements ServletContext {
}
该结构中也有一些filter
操作的方法:
public Map<String, ? extends FilterRegistration> getFilterRegistrations() {}
public FilterRegistration getFilterRegistration(String filterName) {}
public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) {}
这三个函数返回值都是FilterRegistration
,看一下结构:
public class ApplicationFilterRegistration implements FilterRegistration.Dynamic {
public void addMappingForServletNames(EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, String... servletNames) {}
public void addMappingForUrlPatterns(EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, String... urlPatterns) {}
public Collection<String> getServletNameMappings() {}
public Collection<String> getUrlPatternMappings() {}
public String getClassName() {}
public String getInitParameter(String name) {}
public Map<String, String> getInitParameters() {}
public String getName() {}
public boolean setInitParameter(String name, String value) {}
public Set<String> setInitParameters(Map<String, String> initParameters) {}
public void setAsyncSupported(boolean asyncSupported) {}
}
很明显打包了一些常用的注册Filter
的函数,所以可以使用ApplicationContext
和FilterRegistration
进行注册,测试代码如下:
// Define
ApplicationContext applicationContext = new ApplicationContext(standardContext);
Filter filter = new TestApplicationContextAddFilter();
// Registe Filter
FilterRegistration.Dynamic filterRegistration = applicationContext.addFilter(filterName, filter);
// Create Map for urlPattern
filterRegistration.addMappingForUrlPatterns(EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{urlPatern});
// Order
Object[] filterMaps = standardContext.findFilterMaps();
Object[] tmpFilterMaps = new Object[filterMaps.length];
int index = 1;
for (int i = 0; i < filterMaps.length; i++)
{
FilterMap f = (FilterMap) filterMaps[i];
if (f.getFilterName().equalsIgnoreCase(filterName)) {
tmpFilterMaps[0] = f;
} else {
tmpFilterMaps[index++] = f;
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
很不幸,有IllegalStateException
异常:
严重: Servlet.service() for servlet [HelloWorldServlet] in context with path [] threw exception [Servlet execution threw an exception] with root cause
java.lang.IllegalStateException: Filters can not be added to context as the context has been initialised
at org.apache.catalina.core.ApplicationContext.addFilter(ApplicationContext.java:1005)
at org.apache.catalina.core.ApplicationContext.addFilter(ApplicationContext.java:970)
at com.reinject.test.TestApplicationContextAddFilter.<clinit>(TestApplicationContextAddFilter.java:61)
at com.reinject.MyServlet.HelloWorldServlet.doGet(HelloWorldServlet.java:50)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:624)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
通过观察AddFilter
报错的位置,发现是对standardContext
的state
校验的时候不达标抛出的异常:
if (!context.getState().equals(LifecycleState.STARTING_PREP)) {
//TODO Spec breaking enhancement to ignore this restriction
throw new IllegalStateException(
sm.getString("applicationContext.addFilter.ise",
getContextPath()));
}
那么可以先修改一下state
为LifecycleState.STARTING_PREP
:
java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
再运行正常:
不过测试发现如果state
不改回来,之后访问所有页面都会503
:
综上:
// Fix State
java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
// Define
ApplicationContext applicationContext = new ApplicationContext(standardContext);
Filter filter = new TestApplicationContextAddFilter();
// Registe Filter
FilterRegistration.Dynamic filterRegistration = applicationContext.addFilter(filterName, filter);
// Create Map for urlPattern
filterRegistration.addMappingForUrlPatterns(EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{urlPatern});
// Restore State
stateField = org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
// Order
Object[] filterMaps = standardContext.findFilterMaps();
Object[] tmpFilterMaps = new Object[filterMaps.length];
int index = 1;
for (int i = 0; i < filterMaps.length; i++)
{
FilterMap f = (FilterMap) filterMaps[i];
if (f.getFilterName().equalsIgnoreCase(filterName)) {
tmpFilterMaps[0] = f;
} else {
tmpFilterMaps[index++] = f;
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
实验过程中的代码
获取 方式,git clone https://github.com/cnsimo/TomcatFilterInject.git
部署方式,idea + tomcat7.0.70
。
添加tomcat7.0.70/lib
为依赖。
Tomcat Filter之动态注入的更多相关文章
- Java 扫描实现 Ioc 动态注入,过滤器根据访问url调用自定义注解标记的类及其方法
扫描实现 Ioc 动态注入 参考: http://www.private-blog.com/2017/11/16/java-%e6%89%ab%e6%8f%8f%e5%ae%9e%e7%8e%b0-i ...
- 基于ImportBeanDefinitionRegistrar和FactoryBean动态注入Bean到Spring容器中
基于ImportBeanDefinitionRegistrar和FactoryBean动态注入Bean到Spring容器中 一.背景 二.实现方案 1.基于@ComponentScan注解实现 2.基 ...
- .Net Core MVC 网站开发(Ninesky) 2.3、项目架构调整(续)-使用配置文件动态注入
上次实现了依赖注入,但是web项目必须要引用业务逻辑层和数据存储层的实现,项目解耦并不完全:另一方面,要同时注入业务逻辑层和数据访问层,注入的服务直接写在Startup中显得非常臃肿.理想的方式是,w ...
- [ASP.NET MVC] 利用动态注入HTML的方式来设计复杂页面
原文:[ASP.NET MVC] 利用动态注入HTML的方式来设计复杂页面 随着最终用户对用户体验需求的不断提高,实际上我们很多情况下已经在按照桌面应用的标准来设计Web应用,甚至很多Web页面本身就 ...
- 教你怎么用Mono Cecil - 动态注入 (注意代码的注释)
原文 教你怎么用Mono Cecil - 动态注入 (注意代码的注释) 使用 Mono Cecil 进行反编译:using Mono.Cecil; using Mono.Cecil.Cil; //.. ...
- iOS中动态注入JavaScript方法。动态给html标签添加事件
项目中有这样一种需求,给html5网页中图片添加点击事件,并且弹出弹出点击的对应的图片,并且可以保持图片到本地 应对这样的需求你可能会想到很多方法来实现. 1. 最简单的方法就是在html5中添加图片 ...
- 使用Autofac动态注入启动Api服务
Autofac Autofac(https://autofac.org/)是一款.NET的IOC组件,它可以和Owin, Web Api, ASP.NET MVC, .NET Core完美结合,帮助开 ...
- java高级-动态注入替换类Instrumentation
介绍 利用java.lang.instrument(容器类) 做动态 Instrumentation(执行容器) 是 Java SE 5 的新特性. 使用 Instrumentation,开发者可以构 ...
- Java Filter过滤xss注入非法参数的方法
http://blog.csdn.NET/feng_an_qi/article/details/45666813 Java Filter过滤xss注入非法参数的方法 web.xml: <filt ...
随机推荐
- Java实现 蓝桥杯 历届试题 蚂蚁感冒
问题描述 长100厘米的细长直杆子上有n只蚂蚁.它们的头有的朝左,有的朝右. 每只蚂蚁都只能沿着杆子向前爬,速度是1厘米/秒. 当两只蚂蚁碰面时,它们会同时掉头往相反的方向爬行. 这些蚂蚁中,有1只蚂 ...
- java实现第六届蓝桥杯立方尾不变
立方尾不变 立方尾不变 有些数字的立方的末尾正好是该数字本身. 比如:1,4,5,6,9,24,25,- 请你计算一下,在10000以内的数字中(指该数字,并非它立方后的数值),符合这个特征的正整数一 ...
- java实现第四届蓝桥杯梅森素数
梅森素数 题目描述 如果一个数字的所有真因子之和等于自身,则称它为"完全数"或"完美数" 例如:6 = 1 + 2 + 3 28 = 1 + 2 + 4 + 7 ...
- 架构C02-商业模式与架构设计
商业模式与架构设计:A段架构与B段架构 <思考软件创新设计:A段架构师思考技术> A段架构师必须具备鲜活的创新思维,睿智的策略思考,犀利的洞察力和灵活的战术才能把握稍纵即逝的商机 ...
- React、Vue添加全局的请求进度条(nprogress)
全局的请求进度条,我们可以使用nprogress来实现,效果如下: 首先需要安装插件: npm i nprogress -S 然后使用的时候主要有两种方式,第一种是切换页面的时候,第二种则是请求接口的 ...
- 03 . Prometheus监控容器和HTTP探针应用
Eeporter是什么及来源? 是什么? 广义上讲所有可以向Prometheus提供监控样本数据的程序都可以被称为一个Exporter.而Exporter的一个实例称为target,如下所示,Prom ...
- 一台电脑如何使用多个git账号?
git相信大家都在用,一般公司有一个账号,放公司自己架的服务器中,员工自己还有一个github或者gitee的账号,存放自己的一些私有代码.本篇文章总结一下,本人在公司开发机上,使用多个git账号的干 ...
- git新手入门问题总结
git新手入门问题总结 前言 本人为2019年6月份刚刚毕业,大三暑假中旬来到上海,实习时间大致为十个月,在这十个月里面学到了许多关于git使用方面的知识 经常会逛开源中国水水动态,看看技术帖子学习知 ...
- 一个小小的即时显示当前时间的jqurey控件
效果: <div class="nowTime"> <span></span>年 <span></span>月 < ...
- 兄弟打印机MFC代码示范
m_strModel.LoadString(IDS_MODEL_STRING); //IDS_MODEL_STRING,字符串控件的ID,资源视图-String Table里面设置 m_strSour ...