浅谈Shiro反序列化获取Key的方式

  • A+

关于Apache Shiro反序列化

  在shiro≤1.2.4版本,默认使⽤了CookieRememberMeManager,由于AES使用的key泄露,导致反序列化的cookie可控,从而引发反序列化攻击。(理论上只要AES加密钥泄露,都会导致反序列化漏洞)
  利用的两个关键条件是key和可用gadget。1.2.4版本默认key为kPH+bIxk5D2deZiIxcaaaA==,当然也可以通过下面的方式自定义key:
```java
private static final String ENCRYPTION_KEY = "3AvVhmFLUs0KTA3Kprsdag==";
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememMeCookie());
// remeberMe cookie 加密的密钥 各个项目不一样 默认AES算法 密钥长度(128 256 512)
cookieRememberMeManager.setCipherKey(Base64.decode(ENCRYPTION_KEY));
return cookieRememberMeManager;
}

```
  下面结合实战以及shiro的CookieRememberMeManaer的调用过程,浅谈获取Key的几种方式。

Shiro key的获取方式

结合Dnslog与URLDNS

  在进行漏洞探测的时候,一般会使用ysoserial-URLDNS-gadget结合dnslog进⾏检测,其不受JDK版本和相关的安全策略影响, 除非存在网络限制DNS不能出网。
  通过判断dnslog是否收到对应的请求,判断漏洞是否存在。这是获取key比较实用方法,通过在dnslog域名前加⼊对应key的randomNum,结合对应的dnslog记录,即可获取到应用对应的Shiro key了。
  例如下图通过结合Dnslog与URLDNS成果枚举出当前应用的key为kPH+bIxk5D2deZiIxcaaaA==:

图片.png

利用时间延迟或报错

  结合Dnslog与URLDNS方法有一个前提是DNS能出网。那么在不出网的情况下就需要找一个替代的方案了。结合SQL盲注的思路,可以考虑执行如下代码结合时间延迟进行判断,若系统是linux系统,则睡眠10s:
java
try{
if(!(System.getProperty("os.name").toLowerCase().contains("win"))){
Thread.currentThread().sleep(10000L);
}
} catch(Exception e){}

  同理,可以考虑结合触发Java异常进⾏判断,若系统返回对应的报错系统,或者返回通用的报错提示,说明当前的key和gadget组合是成功的:
java
String result = "shiro-Vul-Discover";
throw new NoClassDefFoundError(new String(result));

  上述的思路是通过执行相关的恶意代码来进行判断的,那么就需要在有相关的gadget的前提下才能进行key的枚举了。
  例如下面的案例,使用CommonsBeanutils1结合时间延迟的方式成功枚举出当前key为4AvVhmFLUs0KTA3Kprsdag==:

图片.png
   同理,也可以使用报错的方式进行key的枚举:

图片.png
  这种方法的话存在一个比较棘手的点:
* 枚举的次数多,耗时长

  因为要结合可用gadget执行相关代码进行判断,那么假设字典的key个数为100个,那么枚举的次数就是gadget与key的笛卡尔积(10个gadget就耀枚举1000次),以下是一些常用的gadget:
URLDNS
CommonsBeanutils1
CommonsCollections*
JRMPClient
JRMPListener
C3P0
Spring1
......

* 自动化不稳定

  例如部分场景报错时会统一返回登陆页面,在实际利用中很多情况下也仅仅是在登陆页面的接口进行检测,那么就可能会出现漏报误报的情况。
  所以在DNS不出网的情况下,这种方式比较繁琐。

结合CookieRememberMeManaer

  shiro提供了记住我(RememberMe)的功能,关闭了浏览器下次再打开时还是能保存身份信息,使得无需再登录即可访问。
  在登陆成功时,如果启用了RememberMe功能,shiro会在CookieRememberMeManaer类中将cookie中rememberMe字段内容进行序列化、AES加密、Base64编码操作。然后保存在cookie中。在关闭浏览器后,重新访问对应的业务接口,此时就是反过来的操作,解码,解密,然后序列化。最后获取到当前用户的身份信息。
  简单看看具体的代码实现,看看能不能找到相关的思路来解决枚举key的问题。
  在获取到rememberMe后,会调用getRememberedPrincipals方法解密反序列化,得到用户凭证组信息:
java
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext)
{
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try
{
return rmm.getRememberedPrincipals(subjectContext);
}
......
}

  getRememberedPrincipals的具体实现:
java
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext)
{
PrincipalCollection principals = null;
try
{
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
if ((bytes != null) && (bytes.length > 0)) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
}
catch (RuntimeException re)
{
principals = onRememberedPrincipalFailure(re, subjectContext);
}
return principals;
}

  在getRememberedSerializedIdentity方法里主要是对cookie里的相关内容进行base64解码,然后调用convertBytesToPrincipals方法进行解密操作:
java
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext)
{
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}

  解密后就是对应的反序列化以及生成对应的用户凭证组的信息了。
  在调用上述方式时,如果抛出异常,则会调用onRememberedPrincipalFailure方法:
java
principals = onRememberedPrincipalFailure(re, subjectContext);

  查看onRememberedPrincipalFailure的具体实现:
```java
protected PrincipalCollection onRememberedPrincipalFailure(RuntimeException e, SubjectContext context)
{
if (log.isDebugEnabled()) {
log.debug("There was a failure while trying to retrieve remembered principals. This could be due to a configuration problem or corrupted principals. This could also be due to a recently changed encryption key. The remembered identity will be forgotten and not used for this request.", e);
}
forgetIdentity(context);

throw e;

}

  里面调用的是CookieRememberMeManager类的forgetIdentity方法:java
public void forgetIdentity(SubjectContext subjectContext)
{
if (WebUtils.isHttp(subjectContext))
{
HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
forgetIdentity(request, response);
}
}

private void forgetIdentity(HttpServletRequest request, HttpServletResponse response)
{
getCookie().removeFrom(request, response);
}
  然后调用removeFrom方法,这里具体是设置对应的response header,也就是常见的rememberMe=deleteMe:java
public void removeFrom(HttpServletRequest request, HttpServletResponse response)
{
String name = getName();
String value = "deleteMe";
String comment = null;
String domain = getDomain();
String path = calculatePath(request);
int maxAge = 0;
int version = getVersion();
boolean secure = isSecure();
boolean httpOnly = false;

addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);

log.trace("Removed '{}' cookie by setting maxAge=0", name);

}
  结合前面的简单分析,可以知道,当从cookie中获取到rememberMe字段时,通过一系列的解码解密反序列化,成功话的会则得到用户凭证组信息。否则在response中返回Set-Cookie:rememberMe=deleteMe。
  也就说,可以尝试构造好一个包含用户凭证组信息的伪造rememberMe的值,然后经过AES加密后进行请求。经过一系列的解码解密操作后,若此时返回包不返回Set-Cookie: rememberMe=deleteMe,说明当前的key是正确的。可以以此作为判断标准,使用不同密钥对这串序列化数据进行加密并发包,即可快速爆破获取到Shiro加密密钥。
  这里做一个实验验证下以上的猜想,登录写好的环境http://localhost:8080/login,勾选rememberMe,登录成功后,看到一个key为rememberMecookie:

NMhQ5j+uiYfUA+gQF93wGknW88ru39LFDKiOmaAuphx7h+r/XUhlebml7+KNwfF0gIIOnJg6LA8xVpzPJTYknq/aYPeeDNJEVYX8DSUMNUh0nbCdHW1YNuFDdBNg6chk5nEZwkh7dG9k+uAnZEfpFbRTajQ4vEolbOktGAS+feNmpurL2P/0dpWwzsSGMZubiVs0ICMVt6CS3qvU8rKC22lbPILSqTiD5Ao+6YNCm19qm/6uQ7De2E+gmKmxGA9o/EsaRUE71wdiHdJbaDeNOQ5am8rXiejqtfEl5YHzeU2MEdxqo+POVUgaSal7O3FYhLjfn4U1nS97/VUHfY7mlz3iP9rU4KvIYjtB5RhbNwkgoFmtUY6MFyFaJNoOAwKBfkeVY0w7QoF7zo0P1HEA3G1XEBR7GeC4O/XAChMnDx7NYfm5D5RZuWWNkW8qI0U9n5UJXmpVsS1hB3vor0eB/5gO5USMy+ToHAW3bOB6REK1x3/U9IS82sY/aLv7aXBA
  通过一系列的解密,上面的rememberMe解密后的序列化内容base64编码如下:
rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBzcgAXamF2YS51dGlsLkxpbmtlZEhhc2hNYXA0wE5cEGzA+wIAAVoAC2FjY2Vzc09yZGVyeHIAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAF0AAhpbmlSZWFsbXNyABdqYXZhLnV0aWwuTGlua2VkSGFzaFNldNhs11qV3SoeAgAAeHIAEWphdmEudXRpbC5IYXNoU2V0ukSFlZa4tzQDAAB4cHcMAAAAED9AAAAAAAABdAAEcm9vdHh4AHcBAXEAfgAFeBAQEBAQEBAQEBAQEBAQEBA=
```
  可以看到里面包含了shiro的用户凭证组的信息:

图片.png
  得到该内容后即可结合key字典进行AES加密,然后进行base64编码,作为rememberMe的值在请求中进行提交,然后根据respnose是否返回deleteMe判断key是否正确。
  结合实际场景测试,当key正确时,response的header没有相关关键字:

图片.png
  当key不正确时,返回包返回Set-Cookie: rememberMe=deleteMe:

图片.png
  综上,测试环境的shiro key为4AvVhmFLUs0KTA3Kprsdag==。
  相比前两种方式,该方式不受网络限制的影响,并且结合并发,效率上也有一定的保证。那么就可以先枚举出对应的key,然后结合实际情况,结合对应的gadget进行深入的漏洞检测/利用了。

相关推荐: 使用网址计划窃取Bear Notes

译文声明 本文是翻译文章,文章原作者Wojciech Reguła 文章来源:https://wojciechregula.blog 原文地址:https://wojciechregula.blog/post/stealing-bear-notes-with-…