Java代码审计-shiro反序列化漏洞分析

前言

今天参考网上各位大神的文章,分析一下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底层的加解密和填充原理,这里不再叙述。其他攻击过程原理与第二部分代码分析类似。
后面有精力再详细研究!

四、总结

发现分析底层代码,一点点探索漏洞产生的愿意真的有意思,后续有时间再继续分析。加油!

参考

  • 23
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值