Java安全(2)shiro-550(CVE-2016-4437)正向审计

shiro简介

简单概括一下shiro是啥(

Apache Shiro 是Java 的一个安全框架。Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与Web 集成、缓存等。

shiro 反序列化

Apache Shiro 是一个广泛使用的 Java 安全框架,它提供了认证、授权、加密和会话管理等功能。Shiro 反序列化漏洞,特别是 Shiro-550(CVE-2016-4437)和 Shiro-721(CVE-2019-12422),是由于 Shiro 在处理记住我(RememberMe)功能时,使用了可预测的加密密钥进行序列化和反序列化操作,导致攻击者能够执行任意代码。

漏洞原理

Shiro-550 漏洞(CVE-2016-4437)的产生是因为 Shiro 在序列化用户身份时使用了硬编码的 AES 密钥。攻击者可以通过构造恶意的序列化对象,然后将其加密和编码后作为 RememberMe cookie 发送给服务器,服务器在反序列化时执行恶意代码

cookie 加密解密认证过程

漏洞特征

返回包中带有 rememberMe=deleteMe 字段

  • 登录失败时:如果用户登录失败,Shiro会返回一个rememberMe=deleteMe的Cookie值,表示清除之前的RememberMe状态。
  • 登录成功但未勾选RememberMe时:即使用户登录成功,但如果没有勾选RememberMe选项,Shiro也会返回rememberMe=deleteMe,表示不启用RememberMe功能。

在Shiro的RememberMe功能中,Cookie值会经过解密和反序列化处理。如果攻击者能够构造恶意的Cookie值并触发反序列化漏洞,就可能导致任意代码执行。因此,当返回包中出现rememberMe=deleteMe字段时,表明系统使用了Shiro的RememberMe功能,可能存在反序列化漏洞的风险。

审计流程

正向分析

正向就是上图的上方这条路线

首先关注 rememberIdentify 方法,这个方法就是实现的 “记住我” 功能

protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
    byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
    rememberSerializedIdentity(subject, bytes);
}

其中的convertPrincipalsToBytes()实现了对用户信息的序列化+AES,跟进过去(其实就写在下面)

protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
    byte[] bytes = serialize(principals);
    if (getCipherService() != null) {
        bytes = encrypt(bytes);
    }
    return bytes;
}

那么先去看一下序列化这一块

序列化

方法的第二行调用了 serialize 方法,跟进,看看怎么实现的

protected byte[] serialize(PrincipalCollection principals) {
    return getSerializer().serialize(principals);
}

是对 getSerializer().serialize(principals); 的调用,直接再跟进:

public interface Serializer<T> {
    byte[] serialize(T o) throws SerializationException;
    T deserialize(byte[] serialized) throws SerializationException;
}

是个接口,去看一下接口的实现

有两个,一个是DefaultSerializer,一个是 XmlSerializer ,肯定不是xml的,那就去看看默认

package org.apache.shiro.io;

import java.io.*;

public class DefaultSerializer<T> implements Serializer<T> {

    public byte[] serialize(T o) throws SerializationException {
        if (o == null) {
            String msg = "argument cannot be null.";
            throw new IllegalArgumentException(msg);
        }
        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 e) {
            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, e);
        }
    }

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

使用的输出流是 ByteArrayOutputStream 不同于上一篇笔记示例用的文档输出流

try {
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(o);
            oos.close();
            return baos.toByteArray();
    }

oos还是没区别的,一个 writeObject 完成序列化了

AES加密

回到刚刚的 convertPrincipalsToBytes 方法

protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
    byte[] bytes = serialize(principals);
    if (getCipherService() != null) {
        bytes = encrypt(bytes);
    }
    return bytes;
}

看完了序列化去看看 encrypt ,负责实现加密功能的

protected byte[] encrypt(byte[] serialized) {
    byte[] value = serialized;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
        value = byteSource.getBytes();
    }
    return value;
}

getEncryptionCipherKey() 获取到加密的密钥,然后还有刚刚序列化得到的东西,然后一起传参给加密方法 cipherService.encrypt

跟进 cipherService.encrypt

ByteSource encrypt(byte[] raw, byte[] encryptionKey) throws CryptoException;

这里是实现了一个接口,去看一下

public ByteSource encrypt(byte[] plaintext, byte[] key) {
    byte[] ivBytes = null;
    boolean generate = isGenerateInitializationVectors(false);
    if (generate) {
        ivBytes = generateInitializationVector(false);
        if (ivBytes == null || ivBytes.length == 0) {
            throw new IllegalStateException("Initialization vector generation is enabled - generated vector" +
                    "cannot be null or empty.");
        }
    }
    return encrypt(plaintext, key, ivBytes, generate);
}

经典的AES特征,偏移量和密钥

然后之前说过是因为硬编码的密钥,导致了漏洞的产生,那key在哪呢(

接着往上一步步跟进也找不到,直接搜索 DEFAULT_CIPHER_KEY_BYTES 就能看到(IDEA直接双击shift)

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

序列化和加密都完成了,该 base64 了

base64

回到最开始的地方

protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
    byte[] bytes = convertPrincipalsToBytes(accountPrincipals);        //分析终了
    rememberSerializedIdentity(subject, bytes);
}

去跟进 rememberSerializedIdentity 方法

protected abstract void rememberSerializedIdentity(Subject subject, byte[] serialized);

这也是个 interface ,继续(两个,选择跟进 CookieRememberMeManager 那个)

protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {

    if (!WebUtils.isHttp(subject)) {
        if (log.isDebugEnabled()) {
            String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet " +
                    "request and response in order to set the rememberMe cookie. Returning immediately and " +
                    "ignoring rememberMe operation.";
            log.debug(msg);
        }
        return;
    }


    HttpServletRequest request = WebUtils.getHttpRequest(subject);
    HttpServletResponse response = WebUtils.getHttpResponse(subject);

    //base 64 encode it and store as a cookie:
    String base64 = Base64.encodeToString(serialized);

    Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
    Cookie cookie = new SimpleCookie(template);
    cookie.setValue(base64);
    cookie.saveTo(request, response);
}

看后半部分,把序列化并且AES之后的一个结果(byte[])传参进来然后直接 base64

然后其他的代码的功能就是生成好请求包,然后把我们 base64 之后的值 save 进 http 的请求包里面

反向流程的分析下集再说(逃

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇