前言
重新开始,征战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> |
启动程序
public class Main { |
使用注解注册Servlet、Listener、Filter。
@WebListener |
目录结构
webapp下不用放其他东西。
tomcat.8083是自动生成的。
直接启动
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 { |
Listener马内容
public class MyListener implements ServletRequestListener { |
filter 内存马分析
要找到context是如何记住一个filter的,然后手动加入恶意filter。
调试第一处:手写一个正常的Filter,然后将断点打在doFilter处,发送请求,到达断点,向下查看调用栈。
调用栈中,StandardWrapperValue的invoke方法有这个操作,跟进去。
ApplicationFilterChain filterChain = |
在里面遍历context的FilterMaps,如果请求的路径匹配到了FilterMap的urlPattern,则将这个FilterMap加入filterChain中。遍历过程中,如果这个Filter没有对应的filterConfig,则不加入,直接continue。
调试第二处:手写一个正常的Filter,然后将断点打在init处,启动调试,无需发送请求即可到达断点, 向下查看调用栈。
调用栈中,查看StandardContext的filterStart代码,有这一个操作:
ApplicationFilterConfig filterConfig = |
其中,this就是StandardContext,entry.getValue()是一个FilterDef。
总结:
1、FilterMap保存filterName和urlPattern。相当于web.xml里的<filter-mapping>
2、FilterDef保存filterName和实际Filter对象。相当于web.xml里的<filter>
3、FilterConfig保存filterName和filterConfig。filterConfig中有FilterDef。
内存马的构造即完成上述3个对象。
主要代码
Servlet内存马分析
调试方式:断点打在ContextConfig的configureStart()方法,里面有一个操作webConfig()。webConfig是解析web.xml文件和注解。
然后将解析得到的webxml传入configureContext,开始创建wrapper。
此时的调用栈
在configureContext中,创建wrapper,然后context.addChild(wrapper);
。
然后context.addServletMappingDecoded(entry.getKey(), entry.getValue());
,第一个参数是路由,第二个是wrapper的名字。
然后回到fireLifecycleEvent
,往下翻,可以看到loadOnStartup
这里面的主要作用是,检查哪些Servlet有loadOnStartup参数(web.xml或注解配置的),有的话立刻加载,即执行wrapper.load()
,没有的话等到被访问的时候才加载。
所以内存马编写主要参考configureContext
里创建wrapper和添加路由的写法,完成这些后即可访问。至于有没有loadOnStartup
不重要。
value内存马分析
一个请求进入Engine后,Engine调用自己的pipline的第一个value的invoke,value的invoke又调用下一个value的invoke,像链表一样逐级向下调用,最终调用到Servlet。
所以value内存马注入思路:在Context的pipline里加入一个恶意value。恶意value里也要调用下一个value的invoke,保证下面正常调用。
理论上在Engine和Host里注入也可以,但是还没研究如何获取Host或Engine。
内存马代码
为什么getNext().invoke
放在else里:Context的后面是wrapper。假如在if里最底下也invoke,则:访问不存在路由时,wrapper找不到,res原本的命令执行结果会被404页面信息覆盖(自己猜测的)。上述写法,访问所有路由,只要带cmd参数即可回显结果。
注入代码