关于Apache Shiro反序列化
在shiro≤1.2.4版本,默认使⽤了CookieRememberMeManager,由于AES使用的key泄露,导致反序列化的cookie可控,从而引发反序列化攻击。(理论上只要AES加密钥泄露,都会导致反序列化漏洞) 利用的两个关键条件是key和可用gadget。1.2.4版本默认key为kPH+bIxk5D2deZiIxcaaaA==,当然也可以通过下面的方式自定义key:
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的获取方式
1
结合Dnslog与URLDNS
在进行漏洞探测的时候,一般会使用ysoserial-URLDNS-gadget结合dnslog进⾏检测,其不受JDK版本和相关的安全策略影响, 除非存在网络限制DNS不能出网。
通过判断dnslog是否收到对应的请求,判断漏洞是否存在。这是获取key比较实用方法,通过在dnslog域名前加⼊对应key的randomNum,结合对应的dnslog记录,即可获取到应用对应的Shiro key了。
例如下图通过结合Dnslog与URLDNS成果枚举出当前应用的key为kPH+bIxk5D2deZiIxcaaaA==:
2利用时间延迟或报错
结合Dnslog与URLDNS方法有一个前提是DNS能出网。那么在不出网的情况下就需要找一个替代的方案了。结合SQL盲注的思路,可以考虑执行如下代码结合时间延迟进行判断,若系统是linux系统,则睡眠10s:
try{ if(!(System.getProperty("os.name").toLowerCase().contains("win"))){ Thread.currentThread().sleep(10000L); } } catch(Exception e){}
同理,可以考虑结合触发Java异常进⾏判断,若系统返回对应的报错系统,或者返回通用的报错提示,说明当前的key和gadget组合是成功的:
String result = "shiro-Vul-Discover";throw new NoClassDefFoundError(new String(result));
上述的思路是通过执行相关的恶意代码来进行判断的,那么就需要在有相关的gadget的前提下才能进行key的枚举了。例如下面的案例,使用CommonsBeanutils1结合时间延迟的方式成功枚举出当前key为4AvVhmFLUs0KTA3Kprsdag==: 同理,也可以使用报错的方式进行key的枚举:
这种方法的话存在一个比较棘手的点:枚举的次数多,耗时长。因为要结合可用gadget执行相关代码进行判断,那么假设字典的key个数为100个,那么枚举的次数就是gadget与key的笛卡尔积(10个gadget就耀枚举1000次),以下是一些常用的gadget:
URLDNSCommonsBeanutils1CommonsCollections*JRMPClientJRMPListenerC3P0Spring1......
自动化不稳定:例如部分场景报错时会统一返回登陆页面,在实际利用中很多情况下也仅仅是在登陆页面的接口进行检测,那么就可能会出现漏报误报的情况。所以在DNS不出网的情况下,这种方式比较繁琐。
3结合CookieRememberMeManaer
shiro提供了记住我(RememberMe)的功能,关闭了浏览器下次再打开时还是能保存身份信息,使得无需再登录即可访问。
在登陆成功时,如果启用了RememberMe功能,shiro会在CookieRememberMeManaer类中将cookie中rememberMe字段内容进行序列化、AES加密、Base64编码操作。然后保存在cookie中。在关闭浏览器后,重新访问对应的业务接口,此时就是反过来的操作,解码,解密,然后序列化。最后获取到当前用户的身份信息。
简单看看具体的代码实现,看看能不能找到相关的思路来解决枚举key的问题。在获取到rememberMe后,会调用getRememberedPrincipals方法解密反序列化,得到用户凭证组信息:
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) { RememberMeManager rmm = getRememberMeManager(); if (rmm != null) { try { return rmm.getRememberedPrincipals(subjectContext); } ......}
getRememberedPrincipals的具体实现:
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方法进行解密操作:
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { if (getCipherService() != null) { bytes = decrypt(bytes); } return deserialize(bytes); }
解密后就是对应的反序列化以及生成对应的用户凭证组的信息了。在调用上述方式时,如果抛出异常,则会调用onRememberedPrincipalFailure方法:
principals = onRememberedPrincipalFailure(re, subjectContext);
查看onRememberedPrincipalFailure的具体实现:
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方法:
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方法,这里具体是设置对应的
responseheader,也就是常见的
rememberMe=deleteMe:
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的用户凭证组的信息:
得到该内容后即可结合key字典进行AES加密,然后进行base64编码,作为rememberMe的值在请求中进行提交,然后根据respnose是否返回deleteMe判断key是否正确。
结合实际场景测试,当key正确时,response的header没有相关关键字:
当key不正确时,返回包返回
Set-Cookie: rememberMe=deleteMe:
综上,测试环境的shiro key为4AvVhmFLUs0KTA3Kprsdag==。
相比前两种方式,该方式不受网络限制的影响,并且结合并发,效率上也有一定的保证。那么就可以先枚举出对应的key,然后结合实际情况,结合对应的gadget进行深入的漏洞检测/利用了。 扫码关注我们在这里,探索技术与热爱 点分享 点点赞 点在看