前言
这两天调试 Shiro550 ( CVE-2016-4437 ) 这个反序列化漏洞 , 这个漏洞近期有很多前辈已经分析过了 , 因此这里只是简单记录下我的调试过程.
Apache Shiro550
Apache Shiro550 简介
Apache Shiro 是一个功能强大且易于使用的 Java 安全框架,它用于处理身份验证,授权,加密和会话管理 .
这个安全框架的内容是非常多的 , 感兴趣的师傅可以查阅 Application Security With Apache Shiro 一文 , 本章的核心仅在 Shiro550 上 , 因此不对框架本身进行深入研究 ,
那么到底什么是 Shiro550 呢 ? 实际上 它源自于 Tim Stibbs 前辈在 Apache.org 上提交的一个 Issue.
在默认情况下 , Apache Shiro 使用
CookieRememberMeManager
对用户身份进行序列化/反序列化 , 加密/解密 和 编码/解码 , 以供以后检索 .因此 , 当 Apache Shiro 接收到未经身份验证的用户请求时 , 会执行以下操作来寻找他们被记住的身份.
- 从请求数据包中提取 Cookie 中 rememberMe 字段的值
- 对提取的 Cookie 值进行 Base64 解码
- 对 Base64 解码后的值进行 AES 解密
- 对解密后的字节数组调用
ObjectInputStream.readObject()
方法来反序列化.但是默认AES加密密钥是 "硬编码" 在代码中的 . 因此 , 如果服务端采用默认加密密钥 , 那么攻击者就可以构造一个恶意对象 , 并对其进行序列化 , AES加密 , Base64编码 , 将其作为 Cookie 中 rememberMe 字段值发送 . Apache Shiro 在接收到请求时会反序列化恶意对象 , 从而执行攻击者指定的任意代码 .
这么看来 , 该漏洞实际上是因为加密密钥泄露导致的 , 下面我们来调试一下漏洞代码
Apache Shiro550 测试环境搭建
-
首先我们需要漏洞对应版本的 Shiro 源代码.
// Git Clone Shiro 源代码
git clone https://github.com/apache/shiro.git
// 切换分支到 Shiro-1.2.4( 存在漏洞的版本 )
git checkout shiro-root-1.2.4
// 检查当前分支版本
git status
-
拿到 shiro1.2.4 的源码后 , 我们需要修改 Maven 配置 , 并通过 IDEA 导入
在对应位置新增一行
<version>1.2</version>
.Shiro 目录下有多个 pom.xml 文件 , 注意需要修改的是
shiro/sample/web/pom.xml
文件.修改完毕后 , 直接拿 idea 导入或者打开 shiro 目录即可
-
导入后会 Maven 会自动下载依赖 , 依赖下载完毕后需要手工部署 Tomcat Server
这里的测试环境为 Tomcat7 + JDK7 .
直接部署 Tomcat 会出现
No artifacts marked for deployment
的告警信息 , 点击旁边的 fix , 并选中samples-web:war
, 在 Deployment 一栏即可看到对应的 artifacts .至此 , Shrio550 的测试环境就已经搭建完毕了.
Apache Shiro550 复现( YSoSerial URLDNS Paylaod )
目前服务端仅部署了 Shiro1.2.4 , 没有导入 Apache CommonsCollections 组件 , 因此先用 YSoSerial URLDNS Payload 来验证一下反序列化漏洞是否存在.
网上很容易找到 Shiro550 的漏洞利用代码 , 目前我们不研究其构造 , 仅需要把对应的 YSoSerial Payload 改为 URLDNS Payload 即可 . DNS接收平台可以选择 DNSLog 或者 Ceye 等.
在生成可利用的 Payload 后 , 可以开启 Apache 服务 , 打开 BurpSuite , 并拦截 Shiro 中的登录请求 .
在 login.jsp 页面有四组默认的用户名和口令 , 随便选一组登录即可 , 注意勾选 Remember Me 选项框.
用 BurpSuite 拦截 POST 请求后 , 转发到 Repeater 模块 , 并在 HTTP Cookie 字段注入刚才生成的 Payload 即可.
此时回到 Ceye.io , 如果看到存在 DNS 查询请求 , 则说明 Shiro550 漏洞复现成功.
现在 , 已经可以证明服务端存在 Shiro550 反序列化漏洞.
Apache Shiro550 加密分析
在研究 Shiro550 漏洞原理之前 , 我们需要先分析一下整个加密流程 , 这部分内容涉及漏洞利用代码的构造.
那么从哪里入手呢? Shiro550 的 Issue 中有提到 CookieRememberMeManager 这个类.
在 IDEA 中通过 Shift+Shift
找到这个类 , 然后 Ctrl+F12
查看该类中定义的所有方法与字段.
其中有两个方法非常显眼 , 即 rememberedSerializedIdentity()
方法和 getRememberSerializedIdentity()
方法 , 可以猜测它们是成对应关系 , 分别用于加密和解密.
既然现在是分析加密阶段 , 那么核心方法就是 rememberedSerializedIdentity()
方法了.
调试定位点 : CookieRememberMeManager.rememberedSerializedIdentity()
这次我们先运行 Apache 服务 , 在 rememberedSerializedIdentity()
方法中下断点 , 然后提交一个正常的注册请求.
程序成功停在了断点 , 从函数调用栈不难发现 , 此时已经通过了登录验证 , 那么让我们回溯一下登录过程
-
AuthenticatingFilter.executeLogin()
通过该方法名可知 , 这里会执行登录操作. 参数 Token 中可以看到我们输入的登录信息
-
DefaultSecurityManager.login()
在该方法中会调用
AuthenticatingSecurityManager.authenticate()
方法来查询数据库 , 验证当前用户是否存在 . 如果验证通过, 则会调用DefaultSecurityManager.onSuccessfulLogin()
方法 , 否则调用DefaultSecurityManager.onFailedLogin()
方法.很明显验证是通过的 , 因此跟进
onSuccessfulLogin()
方法. -
DefaultSecurityManager.rememberMeSuccessfulLogin()
onSuccessfulLogin()
方法会调用rememberMeSuccessfulLogin()
方法该方法会先调用
getRememberMeManager()
方法来返回一个 CookieRememberMeManager 类型的参数 rmm , 然后判断 rmm 是否为空, 如果不为空则调用RememberMeManager.onSuccessfulLogin()
方法.我们能从这个 rmm 参数中了解到很多信息 , 例如加密算法 , 加密模式 , 填充方式等
加密算法 : AES
加密模式 : CBC
填充方式 : PKCS5Padding
RememberMe 功能触发点 : AbstractRememberMeManager.onSuccessfulLogin()
-
AbstractRememberMeManager.onSuccessfulLogin()
根据注释信息可知 , 该方法会清除先前的认证信息 , 并根据
isRememberMe()
方法的返回值来决定是否重新生成认证信息.那么
isRememberMe()
方法很有可能就是用来判断用户在登录时是否勾选 Remember Me 选项框的 . 我们跟进该方法.该方法的返回值为三个表达式的与操作 . 前两个表达式都很好理解 . 关键点是第三个表达式 , 该表达式会调用
RememberMeAuthenticationToken.isRememberMe()
方法.我们令程序断在此处 , 然后 F7 单步调试 , 会发现这里
isRememberMe()
方法的返回值其实就是根据用户在登录时是否勾选 Remember Me 选项框来决定的.因此 , 如果用户在登录时勾选 Remember Me 选项框 , 那么这里
isRememberMe()
方法就会返回 True , 程序才会去调用AbstractRememberMeManager.rememberIdentity()
方法.
加密过程触发点 : AbstractRememberMeManager.rememberIdentity()
-
AbstractRememberMeManager.rememberIdentity()
接下来进入到
rememberIdentity()
方法中 . 该方法会先通过convertPrincipalsToBytes()
方法获取一个字节数组.我们跟进该方法 , 该方法定义如下:
serialize()
方法返回了一个字节数组 , 该数组以[ -84 , -19 ]
开头 , 这其实是标准的 Java 序列化数据. 下面是一个测试 demo .然后程序会通过
getCipherService()
方法来获取密码服务的接口 , 如果成功访问接口 , 则调用该接口的encrypt()
方法对序列化数据进行加密.根据
encrypt()
方法的定义 , 我们可以得知该方法的第二个参数为加密密钥 , 那么现在来看一下密钥是如何获取的. -
AbstractRememberMeManager.getEncryptionCipherKey
该方法用于返回私有变量 encryptionCipherKey 的值
跟进该变量 , 可以发现该变量在
setEncryptionCipherKey()
方案中被赋值
-
AbstractRememberMeManager.setEncryptionCipherKey
该方法中会通过传入的参数对
this.encryptionCipherKey
进行赋值.通过查看函数调用关系 , 可以得知在
setCipherKey()
方法中调用了该方法 , 我们跟踪setCipherKey()
方法 . -
AbstractRememberMeManager.setCipherKey()
该方法中会调用
setEncryptionCipherKey()
方法 , 并传入参数 , 我们跟踪该方法 , 查看参数是怎么来的.通过查看函数调用关系 , 可以得知在 AbstractRememberMeManager 构造方法中调用了
setCipherKey()
方法 , 并且传入了一个静态变量(DEFAULT_CIPHER_KEY_BYTES
)作为参数. -
DEFAULT_CIPHER_KEY_BYTES
DEFAULT_CIPHER_KEY_BYTES 变量的值实际上就是网上说的默认密钥 , 可见它直接被明文写在代码中 . 因此产生了 Shiro550 漏洞.
Cookie写入点 : CookieRememberMeManager.rememberSerializedIdentity()
程序随后会进入 CookieRememberMeManager.rememberSerializedIdentity()
方法中 , 该方法会对AES加密后的序列化数据进行 Base64 编码 , 然后将其作为 HTTP Cookie 中 rememberMe 字段的值.
至此 , 整个加密过程就已经分析完了 , 我们已经知道 Shiro 后端是如何生成 RememberMe Cookie 的 . 下面来分析一下解密过程.
Apache Shiro 解密分析
由于篇幅原因 , 分析解密过程中我们直接拿 YSoSerial URLDNS Payload 来打 , 来看一看序列化数据时如何被反序列化并执行 .
相对的 , 在分析解密过程时 , 我们将 getRememberedSerializedIdentity()
方法作为切入点 , 直接在该方法处下端点 , 并通过 BurpSuite Repeater 模块重发携带 YSoSerial URLDNS Payload 的登录请求.
调试定位点 : CookieRememberMeManager.getRememberedSerializedIdentity()
如果不出意外 , 程序会断在该方法中 , 函数调用栈如下所示:
继续单步调试 , 程序会从请求数据包中提取 RememberMe Cookie 的键值 , 然后判断该值是否为 "deleteMe
" , 如果 Cookie 值为 "deleteMe
" , 则直接返回 null , 不再进行后续操作.'
如果 RememberMe Cookie 值不为 "deleteMe
" , 则程序会判断 Cookie 值是否为空 , 如果不为空则会调用 Base64.decode()
方法对 Cookie 值进行解码 , 并将解码后的字节数组返回.
解密过程触发点 : AbstractRememberMeManager.getRememberedPrincipals()
该方法中会判断上一步返回的字节数组是否为空 , 如果不为空则调用 convertBytesToPrincipals()
方法对其进行处理.
跟进该方法 , 可以得知这一步是 AES 解密过程 , 程序调用 decrypt()
方法进行解密 , 解密后返回序列化数据.
这里的内容不用多说 , 与加密过程是完全一致的 , 包括获取了同一个密钥.
反序列化过程触发点 : AbstractRememberMeManager.deserialize()
上一步我们拿到了一组序列化数据 , 下面程序会对其进行反序列化处理
跟进该方法后 , 发现程序仅会判断序列化数据是否为空 , 如果不为空则直接调用 ObjectInputStream.readObject()
方法反序列化 !
接下来的流程不用多说 , 根据 Java 反序列化漏洞(6) – 解密 YSoSerial : URLDNS POP Chain 一文中提到的关于 JDK1.7 下 YSoSerial URLDNS Payload 的函数调用栈 , 我们直接将断点打在 java.net.InetAddress.getAllByName0()
方法中.
通过 F9 可以直接跳到断点处 , 可以看到 Shiro 在反序列化时会执行恶意代码 , 查询攻击者指定的域名.
反序列化时的函数调用栈如下所示 :
这部分内容的过程分析可以参考上文链接
总结
本章我们分析了 Apache Shrio550 反序列化漏洞 , 这个漏洞的原理并不复杂 , 而且调试过程还是挺有趣的 , 是一个比较好的练习项目.