内容纲要

前言

这两天调试 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 测试环境搭建

  1. 首先我们需要漏洞对应版本的 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

  2. 拿到 shiro1.2.4 的源码后 , 我们需要修改 Maven 配置 , 并通过 IDEA 导入

    在对应位置新增一行 <version>1.2</version> .

    Shiro 目录下有多个 pom.xml 文件 , 注意需要修改的是 shiro/sample/web/pom.xml 文件.

    修改完毕后 , 直接拿 idea 导入或者打开 shiro 目录即可

  3. 导入后会 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() 方法中下断点 , 然后提交一个正常的注册请求.

程序成功停在了断点 , 从函数调用栈不难发现 , 此时已经通过了登录验证 , 那么让我们回溯一下登录过程

  1. AuthenticatingFilter.executeLogin()

    通过该方法名可知 , 这里会执行登录操作. 参数 Token 中可以看到我们输入的登录信息

  2. DefaultSecurityManager.login()

    在该方法中会调用 AuthenticatingSecurityManager.authenticate() 方法来查询数据库 , 验证当前用户是否存在 . 如果验证通过, 则会调用 DefaultSecurityManager.onSuccessfulLogin() 方法 , 否则调用 DefaultSecurityManager.onFailedLogin() 方法.

    很明显验证是通过的 , 因此跟进 onSuccessfulLogin() 方法.

  3. DefaultSecurityManager.rememberMeSuccessfulLogin()

    onSuccessfulLogin() 方法会调用 rememberMeSuccessfulLogin() 方法

    该方法会先调用 getRememberMeManager() 方法来返回一个 CookieRememberMeManager 类型的参数 rmm , 然后判断 rmm 是否为空, 如果不为空则调用 RememberMeManager.onSuccessfulLogin() 方法.

    我们能从这个 rmm 参数中了解到很多信息 , 例如加密算法 , 加密模式 , 填充方式等

    加密算法 : AES
    加密模式 : CBC
    填充方式 : PKCS5Padding


RememberMe 功能触发点 : AbstractRememberMeManager.onSuccessfulLogin()

  1. AbstractRememberMeManager.onSuccessfulLogin()

    根据注释信息可知 , 该方法会清除先前的认证信息 , 并根据 isRememberMe() 方法的返回值来决定是否重新生成认证信息.

    那么 isRememberMe() 方法很有可能就是用来判断用户在登录时是否勾选 Remember Me 选项框的 . 我们跟进该方法.

    该方法的返回值为三个表达式的与操作 . 前两个表达式都很好理解 . 关键点是第三个表达式 , 该表达式会调用 RememberMeAuthenticationToken.isRememberMe() 方法.

    我们令程序断在此处 , 然后 F7 单步调试 , 会发现这里 isRememberMe() 方法的返回值其实就是根据用户在登录时是否勾选 Remember Me 选项框来决定的.

    因此 , 如果用户在登录时勾选 Remember Me 选项框 , 那么这里 isRememberMe() 方法就会返回 True , 程序才会去调用 AbstractRememberMeManager.rememberIdentity() 方法.


加密过程触发点 : AbstractRememberMeManager.rememberIdentity()

  1. AbstractRememberMeManager.rememberIdentity()

    接下来进入到 rememberIdentity() 方法中 . 该方法会先通过 convertPrincipalsToBytes() 方法获取一个字节数组.

    我们跟进该方法 , 该方法定义如下:

    serialize() 方法返回了一个字节数组 , 该数组以 [ -84 , -19 ] 开头 , 这其实是标准的 Java 序列化数据. 下面是一个测试 demo .

    然后程序会通过 getCipherService() 方法来获取密码服务的接口 , 如果成功访问接口 , 则调用该接口的 encrypt() 方法对序列化数据进行加密.

    根据 encrypt() 方法的定义 , 我们可以得知该方法的第二个参数为加密密钥 , 那么现在来看一下密钥是如何获取的.

  2. AbstractRememberMeManager.getEncryptionCipherKey

    该方法用于返回私有变量 encryptionCipherKey 的值

    跟进该变量 , 可以发现该变量在 setEncryptionCipherKey() 方案中被赋值


  1. AbstractRememberMeManager.setEncryptionCipherKey

    该方法中会通过传入的参数对 this.encryptionCipherKey 进行赋值.

    通过查看函数调用关系 , 可以得知在 setCipherKey() 方法中调用了该方法 , 我们跟踪 setCipherKey() 方法 .

  2. AbstractRememberMeManager.setCipherKey()

    该方法中会调用 setEncryptionCipherKey() 方法 , 并传入参数 , 我们跟踪该方法 , 查看参数是怎么来的.

    通过查看函数调用关系 , 可以得知在 AbstractRememberMeManager 构造方法中调用了 setCipherKey() 方法 , 并且传入了一个静态变量( DEFAULT_CIPHER_KEY_BYTES )作为参数.

  3. 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 反序列化漏洞 , 这个漏洞的原理并不复杂 , 而且调试过程还是挺有趣的 , 是一个比较好的练习项目.

最后修改日期:2020年11月23日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。