文章目录
前言
今天参考网上各位大神的文章,分析一下Shiro的反序列化的漏洞原理,收获颇多。
注:本文所记内容仅用于个人学习,严禁利用文中技术进行任何非法行为,所造成一切严重后果自负!
一、什么是Shiro反序列化漏洞
1.Shiro框架介绍
Shiro是Apache的一个强大且易用的Java安全框架,用于执行身份验证、授权、密码和会话管理。使用 Shiro 易于理解的 API,可以快速轻松地对应用程序进行保护
2.Shiro-550介绍
- Shiro-550反序列化漏洞(CVE-2016-4437)
- shiro-550主要是由shiro的rememberMe内容反序列化导致的命令执行漏洞,造成的原因是默认加密密钥是硬编码在shiro源码中,任何有权访问源代码的人都可以知道默认加密密钥。 于是攻击者可以创建一个恶意对象,对其进行序列化、编码,然后将其作为cookie的rememberMe字段内容发送,Shiro 将对其解码和反序列化,导致服务器运行一些恶意代码。
- 利用条件
- 需要知道AES加密的Key
- 且目标服务器含有可利用的攻击链
3.Shiro-721介绍
- Apache Shiro Padding Oracle Attack CVE-2019-12422
- 在用户进行登录的时候,Apache Shiro提供RemenberMe功能,可以存储cookie,期间使用的是AES-128-CBC进行加密,可以通过Padding Oracle加密生成的攻击代码来重新构造一个恶意的rememberMe字段,重新请求网站,进行反序列化攻击,最终导致任意代码的执行。
- 本次漏洞实际并不是针对 shiro 代码逻辑的漏洞,而是针对 shiro 使用的 AES-128-CBC 加密模式的攻击
- 影响版本
- Apache < 1.4.2
- 利用条件
- 知道已经登陆用户的合法cookie
- 且目标服务器含有可利用的攻击链就可以进行漏洞利用
二、Shiro-550漏洞分析
注:这个漏洞是Shiro的1.2.4的版本
1.加密分析
漏洞产生点在CookieRememberMeManager该位置,来看到rememberSerializedIdentity方法。
该方法的作用为使用Base64对指定的序列化字节数组进行编码,并将Base64编码的字符串设置为cookie值。
接下来往上追溯,看看是哪些调用了这个方法。发现了rememberIdentity方法
这个方法没有太多信息,继续看谁调用了rememberIdentity方法。
接下来接着看谁调用了,一步一步往上面找
就发现了这个onSuccessfulLogin方法
我们来看一下isRememberMe方法
发现,这个方法返回值是boolean 他的目的是判断用户在前端是否选择了Remember Me选项
至此,就大致倒着往回追溯了 登录的过程。
接下来正向捋回去,
onSuccessfulLogin
-isRememberMe(token)检验是否选择了remember me
-rememberIdentity(subject, token, info);//到这个函数,传入了前面传来的三个参数
-getIdentityToRemember(subject, authcInfo)//这个函数返回PrincipalCollection对象
-rememberIdentity(subject, principals)//调用到这里
-convertPrincipalsToBytes(accountPrincipals)//这个函数得到bytes[]
-serialize(PrincipalCollection principals)//这里进行了序列化 得到bytes[]
-getCipherService()//获取的是加密模式
-byte[] encrypt(byte[] serialized) //加密的模块
-cipherService.encrypt(serialized, getEncryptionCipherKey());//这里是加密的模块的调用
-getEncryptionCipherKey()//看一下调用获取秘钥的逻辑
-去看EncryptionCipherKey变量和 构造函数 发现小秘密
-rememberSerializedIdentity(subject, bytes)
正向过去,一些上述回溯没有审查到的方法,列在了下面进行分析。
如下图,序列化的底层实现 还是利用了java本身提供的序列化,返回序列化后的bytes数组形式。
如下图,convertPrincipalsToBytes(accountPrincipals)方法 最终得到的是加密后的bytes
- 整体逻辑就是先对传入的principals进行序列化,然后判断加密模式是否为空,不为空进行加密,,最终返回加密的结果
然后,我们看下encrypt(bytes)加密方法的逻辑
- 这个方法的大致就是使用cipherService的encrypt方法,里面传入了要加密的序列和加密秘钥,最终结果赋值给value,并返回。
发现getEncryptionCipherkey()是一个getter方法,他也有对应的setter方法
去看一下 这个变量 然后看一下构造函数 发现了真相
相关变量如下:
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="); //默认的秘钥
/**
* Serializer to use for converting PrincipalCollection instances to/from byte arrays
*/
private Serializer<PrincipalCollection> serializer;
/**
* Cipher to use for encrypting/decrypting serialized byte arrays for added security
*/
private CipherService cipherService; //判断加密模式
/**
* Cipher encryption key to use with the Cipher when encrypting data
*/
private byte[] encryptionCipherKey; //加密序列key
/**
* Cipher decryption key to use with the Cipher when decrypting data
*/
private byte[] decryptionCipherKey;//解密序列key
观察下图构造函数,可以看到在这里进行了一些赋值和设定
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);这里设定了为默认密钥
再次查看setCipherKey 发现玄机 里面调用了 加密解密密钥的set方法
不得不说,这代码写的真好,一环扣一环。
至此我们得到了 里面加解密的秘钥 采用的AES加解密 秘钥是写死的
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
分析到这了先,累了。
2.解密分析
从这步看起,subjectContext的值便为rememberMe字段
的值,我们发现对其调用了getRememberedSerializedIdentity方法
1)接着看getRememberedSerializedIdentity方法。
-
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext)方法
-
截取其部分关键代码 如下
-
分析代码 发现这个代码主要功能,对传进来的rememberMe字段进行base64解密,然后执行代码return decoded;,返回解密结果。
接下来深入看看每个画框的代码都是干什么的 当然这些纯属个人好奇 想看看细节 不分析也不影响理解 -
readValue(request,response)
-
ensurePadding(base64)
-
decode(64)
先看readValue()方法
-
从传进来的请求中 找到并返回cookie中的值
再看ensurePadding方法 -
这里就是检查长度是否被4整除,如果不能整除,就在末尾填相应的"="
-
那么为什么这么做呢?这个需要去了解一下base64的原理 这里不再叙述
decode代码不再叙述,进去看之后,是解密的流程,不再细分析
所以总结
- getRememberedSerializedIdentity方法就是对传进来的参数进行了base64解密 然后最终返回了bytes[]数组形式
2)接下来,我们继续往下分析convertBytesToPrincipals(bytes, subjectContext)方法
发现有两个需要往下细看的方法
- decrypt(bytes)
- deserialize(bytes)
先看下decrypt方法
- 这里方法看起来就有些眼熟了 想起来再分析加密流程里 也有类似的代码
对getDecryptionCipherKey()方法不再详细分析,其功能就是获取了解密的key 这个key是固定的 和前面加密时的分析一致
然后对decrypt方法传入加密信息和key两个参数,进行解密
具体解密方法里的不再分析
接下来我们看deserialize方法
继续向下分析deserialize方法
看!发现了啥小秘密!
然后便会调用代码T deserialized = (T) ois.readObject();
-
这里调用了java本身的readObjuect方法
-
这里可以对我们传入的payload解密后进行反序列化,然后代码执行
三、Shiro-721漏洞分析
针对 shiro 使用的 AES-128-CBC 加密模式的攻击,需要去分析AES底层的加解密和填充原理,这里不再叙述。其他攻击过程原理与第二部分代码分析类似。
后面有精力再详细研究!
四、总结
发现分析底层代码,一点点探索漏洞产生的愿意真的有意思,后续有时间再继续分析。加油!