0x01 漏洞简介
Tomcat根据默认配置(conf/server.xml
)启动两个连接器。一个是HTTP Connector
默认监听8080
端口处理HTTP请求,一个AJP connector
默认8009
端口处理AJP请求。Tomcat处理两个协议请求区别并不大,AJP协议相当于HTTP协议的二进制优化版。
本次漏洞出现在通过设置AJP请求属性,可控制AJP连接器封装的request对象的属性,最终导致文件包含可以任意文件读取和代码执行。 下面我们以Tomcat 8.5.47
来具体分析。
0x02 漏洞分析
当我们向Tomcat发送AJP请求时,请求会被org.apache.coyote.ajp.AjpProcessor
,AjpProcessor
调用prepareRequest
方法读取AJP请求中的信息来设置request属性。
由于没有任何过滤,我们可以给request
设置任何属性和值。本次漏洞与如下三个属性有关,为了方便后续描述统一简称为“三个include属性
”。
- javax.servlet.include.request_uri
- javax.servlet.include.path_info
- javax.servlet.include.servlet_path
最终会将封装好的request
丢给Servlet
容器Catalina
处理,之后就和HTTP消息的处理一样,按照Servlet映射走。
2.1 任意文件读取
任意文件读取问题出现在org.apache.catalina.servlets.DefaultServlet
这个Servlet。现在假设我们发出一个请求内容如下的AJP请求
1 | RequestUri:/docs/test.jpg |
通过查看servlet映射规则(conf/web.xml
)知道,请求会走默认的DefaultServlet
。
1 | ... |
会交给org.apache.catalina.servlets.DefaultServlet
的doGet
方法处理。doGet
会调用ServeResource
方法进行具体的资源读取操作。首先它会调用 getRelativePath
方法获取要读取资源的相对路径,这里注意它是本次任意读取漏洞的关键,我们先往下看后续再细说它。通过getResources
方法就可以获取到了对应路径的Web资源对象了。
最后资源对象的内容随着resourceBody
被写入了ostream
流对象中返回给客户端。
接下来我们来看漏洞真正核心,org.apache.catalina.servlets.DefaultServlet
类的getRelativePath()
,它负责获取资源的相对路径。由于我们AJP请求设置javax.servlet.include.request_uri
属性值为/
不为null
。故资源
的相对路径构造如下:
1 | = javax.servlet.include.path_info + javax.servlet.include.path_info |
这就导致我们虽然请求的是/docs/test.jpg
文件内容,而实际上返回了/docs/WEB-INF/web.xml
文件的内容。
至此大家可能有两个疑问
问题1:为何Tomcat处理HTTP协议不存在该问题?
答:因为在HTTP请求中,我们无法控制request对象三个include
属性的值,而在AJP请求中可以。
问题2:为何无法跳出webapps目录读文件呢?
DefaultServlet
在读取资源时
会调用org.apache.tomcat.util.http.RequestUtil
工具类中的normalize
方法来对路径进行校验,如果存在./
或../
则会返回null
,最终会抛出一个非法路径的异常终止文件读取操作。
2.2 任意代码执行
任意代码执行问题出现在org.apache.jasper.servlet.JspServlet
这个servlet,假设我们发出一个请求内容如下的AJP请求,让Tomcat执行/docs/test.jsp
,但实际上它会将code.txt
当成jsp来解析执行.
1 | RequestUri:/docs/test.jsp |
code.txt内容如下:
1 | <% |
按照映射规则,我们的请求会被org.apache.jasper.servlet.JspServlet
进行处理。
1 | <servlet> |
由于javax.servlet.include.servlet_path
值为/
不为null
,所以根据代码逻辑我们jsp文件的路径为:
1 | jspUri = javax.servlet.include.servlet_path + javax.servlet.include.path_info |
可见jspUri
是客户端可控。
由我们控制的jspuri
被封装成了一个JspServletWrapper
添加到了Jsp运行上下文JspRuntimeContext
中.最后wrapper.service()
会编译code.txt
,并执行它的_jspService()
方法来处理当前请求,我们的代码被执行。
综上整个过程就清晰了,简而言之就是我们发送AJP请求,请求的是/docs/test.jsp
这个jsp,但是由于那三个include属性可控,我们可以将test.jsp
对应的服务器脚本文件改为了code.txt
。
导致tomcat把我们的code.txt
当jsp文件编译运行,导致代码执行。
最后给大家提两个问题:
问题1: 请求的/docs/test.jsp需要在web目录下真是存在么?
答: 不需要,我们只是为了让请求路径命中org.apache.catalina.servlets.DefaultServlet
这个servlet的匹配规则。
问题2: 如果tomcat不解析任何jsp,jspx等后缀,或者以它们为view的模板,还能触发漏洞么?如果可以又该如何触发?
PS:这个问题是一个师傅留给我的,觉得很有意思,分享给大家思考,有想法的可以留言讨论。
0x03 漏洞修复
Tomcat在8.5.51版本做了如下修复 :
- 默认不开启AJP
- 默认只监听本地ip
- 强制设置认证secret
- 代码层面主要在
AjpProcessor
类的prepareRequest
方法封装requst
对象时采用了白名单,只添加已知属性。这样三个include属性
不再被客户端控制,漏洞修复。