Abstract
Tomcat AJP协议由于存在实现缺陷导致相关参数可控,攻击者利用该漏洞可通过构造特定参数,读取服务器webapp下的任意文件。若服务器端同时存在文件上传功能,攻击者可进一步实现远程代码的执行
——《关于Apache Tomcat存在文件包含漏洞的安全公告》
影响版本:
6.0
7.0 < 7.0.100
8.0 < 8.5.51
9.0 < 9.0.31
环境搭建
下载该版本的源码文件➡传送门 ,然后解压进入该目录。新建一个pom.xml
解决依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 <?xml version="1.0" encoding="UTF-8"?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > org.apache.tomcat</groupId > <artifactId > Tomcat8.0</artifactId > <name > Tomcat8.0</name > <version > 8.0</version > <build > <finalName > Tomcat8.0</finalName > <sourceDirectory > java</sourceDirectory > <testSourceDirectory > test</testSourceDirectory > <resources > <resource > <directory > java</directory > </resource > </resources > <testResources > <testResource > <directory > test</directory > </testResource > </testResources > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 2.0.2</version > <configuration > <encoding > UTF-8</encoding > <source > 1.8</source > <target > 1.8</target > </configuration > </plugin > </plugins > </build > <dependencies > <dependency > <groupId > ant</groupId > <artifactId > ant</artifactId > <version > 1.7.0</version > </dependency > <dependency > <groupId > ant</groupId > <artifactId > ant-apache-log4j</artifactId > <version > 1.6.5</version > </dependency > <dependency > <groupId > ant</groupId > <artifactId > ant-commons-logging</artifactId > <version > 1.6.5</version > </dependency > <dependency > <groupId > wsdl4j</groupId > <artifactId > wsdl4j</artifactId > <version > 1.6.2</version > </dependency > <dependency > <groupId > javax.xml.rpc</groupId > <artifactId > javax.xml.rpc-api</artifactId > <version > 1.1</version > </dependency > <dependency > <groupId > org.eclipse.jdt.core.compiler</groupId > <artifactId > ecj</artifactId > <version > 4.6.1</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency > <dependency > <groupId > org.easymock</groupId > <artifactId > easymock</artifactId > <version > 4.2</version > <scope > test</scope > </dependency > </dependencies > </project >
然后创建一个home
的文件夹,将webapps和conf文件复制过去,再创建三个文件夹,最终的结构如下
IDEA导入pom.xml
新建项目,然后开始下载依赖jar包
导入成功之后开始配置Configurations,新建一个Application,配置项如下:
主类名:org.apache.catalina.startup.Bootstrap
虚拟机选项:-Dcatalina.home=”home文件夹路径”
【注】:如果类名报错,就选中项目中java文件夹标记为src
运行即可。BTW,我这里启动的时候报错:未能找到该CookieFilter
类
需要在test/util
中新建这么一个类,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package util;import java.util.Locale;import java.util.StringTokenizer;public class CookieFilter { private static final String OBFUSCATED = "[obfuscated]" ; private CookieFilter () { } public static String filter (String cookieHeader, String sessionId) { StringBuilder sb = new StringBuilder(cookieHeader.length()); StringTokenizer st = new StringTokenizer(cookieHeader, ";" ); boolean first = true ; while (st.hasMoreTokens()) { if (first) { first = false ; } else { sb.append(';' ); } sb.append(filterNameValuePair(st.nextToken(), sessionId)); } return sb.toString(); } private static String filterNameValuePair (String input, String sessionId) { int i = input.indexOf('=' ); if (i == -1 ) { return input; } String name = input.substring(0 , i); String value = input.substring(i + 1 , input.length()); return name + "=" + filter(name, value, sessionId); } public static String filter (String cookieName, String cookieValue, String sessionId) { if (cookieName.toLowerCase(Locale.ENGLISH).contains("jsessionid" ) && (sessionId == null || !cookieValue.contains(sessionId))) { cookieValue = OBFUSCATED; } return cookieValue; } }
再次运行,访问http://127.0.0.1:8080
成功,表示环境搭建成功
Analysis AJP协议 AJP13协议是定向包协议,出于性能考虑选择了更具可读性的纯文本的二进制格式。Web服务器通过TCP连接与Servlet容器进行通信,为了节省建立套接字的开销,采用长连接的形式,并且在多个请求/响应中重用连接。
我们来看conf\server.xml
Tomcat默认配置了两个connector
HTTP Connector:默认监听8080端口,负责建立HTTP连接,用于接收客户端发来的请求以及Tomcat返回的响应,同时
AJP Connector:默认监听8009端口,可以通过ajp协议与其他web服务器进行数据交互
客户端访问Tomcat服务器的两种方式(图来源 ):
复现 POC1 POC2
读取/ROOT/WEB-INF/2.txt
文件内容
抓包查看过程。具体的协议分析可以参考Tomcat官方文档传送门
这个数据包的重点在于以下三个选项:
1 2 3 javax.servlet.include.request_uri : /javax.servlet.include.path_info : /WEB-INF/2.txtjavax.servlet.include.servlet_path : /
漏洞分析 AJP2HTTP
定位到src\java\org\apache\coyote\ajp\AjpProcessor.java
的prepareRequest方法中
拿到Request请求中的methodCode(2),然后解析成GET请求,紧接着就是解析Req请求拿到协议、URI、Host等信息
下面就进入一个循环然后进入case逻辑用来解析AJP协议未定义的请求头,显然之前协议包中设置的三个属性也会放到request中
然后回到AjpProcessor#service
方法,调用适配器来处理Req和Resp对象
进入CoyoteAdapter#service()
中,对连接器设置一系列属性之后,再利用反射去调用Servlet处理
DefaultServlet
从AJP拿到的是Get请求,所以会调用Servlet的doGet()方法处理
1 2 3 4 5 6 7 8 protected void doGet (HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { serveResource(request, response, true , fileEncoding); }
跟进这个serveResource方法
调用getRelativePath方法从Request中确定请求的资源路径,我们来看下这方法
通过拼接PATH_INFO
+SERVLET_PATH
= /WEB-INF/2.txt
拿到该资源的相对路径,然后回到serveResource根据路径获取资源。其中会调用normalize 方法来验证路径是否合法
这就限制了不能使用../
进行跨目录,只能在Webapps目录下。在后续逻辑里,先生成响应头,然后getContent()拿到资源对象的内容作为响应Body返回给客户端
总调用栈如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 serveResource:1142, DefaultServlet (org.apache.catalina.servlets) doGet:504, DefaultServlet (org.apache.catalina.servlets) service:634, HttpServlet (javax.servlet.http) service:484, DefaultServlet (org.apache.catalina.servlets) service:741, HttpServlet (javax.servlet.http) internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilter:52, WsFilter (org.apache.tomcat.websocket.server) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) invoke:200, StandardWrapperValve (org.apache.catalina.core) invoke:96, StandardContextValve (org.apache.catalina.core) invoke:528, AuthenticatorBase (org.apache.catalina.authenticator) invoke:139, StandardHostValve (org.apache.catalina.core) invoke:81, ErrorReportValve (org.apache.catalina.valves) invoke:678, AbstractAccessLogValve (org.apache.catalina.valves) invoke:87, StandardEngineValve (org.apache.catalina.core) service:343, CoyoteAdapter (org.apache.catalina.connector) service:476, AjpProcessor (org.apache.coyote.ajp) process:66, AbstractProcessorLight (org.apache.coyote) process:810, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1500, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:748, Thread (java.lang)
虽然不能绕过webapps目录,但是通过修改访问的uri就能够读取任意war包目录下文件
JspServlet
如果请求的URL为jsp文件,就会在之前反射的时候调jspservlet处理
同样是用servlet_path
和path_info
进行拼接拿到jspUri
。然后经过一系列逻辑还是会走到getResource传入path去获取文件内容
同样会去判断是否存在../
之类的字符,如果存在就返回path为NULL,最终抛出异常退出。拿到文件内容之后调用jsp引擎去解析,达到了RCE的目的。
总调用栈如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 _jspService:1, _1_txt (org.apache.jsp) service:70, HttpJspBase (org.apache.jasper.runtime) service:741, HttpServlet (javax.servlet.http) service:476, JspServletWrapper (org.apache.jasper.servlet) serviceJspFile:386, JspServlet (org.apache.jasper.servlet) service:330, JspServlet (org.apache.jasper.servlet) service:741, HttpServlet (javax.servlet.http) internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilter:52, WsFilter (org.apache.tomcat.websocket.server) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) invoke:199, StandardWrapperValve (org.apache.catalina.core) invoke:96, StandardContextValve (org.apache.catalina.core) invoke:493, AuthenticatorBase (org.apache.catalina.authenticator) invoke:137, StandardHostValve (org.apache.catalina.core) invoke:81, ErrorReportValve (org.apache.catalina.valves) invoke:660, AbstractAccessLogValve (org.apache.catalina.valves) invoke:87, StandardEngineValve (org.apache.catalina.core) service:343, CoyoteAdapter (org.apache.catalina.connector) service:476, AjpProcessor (org.apache.coyote.ajp) process:66, AbstractProcessorLight (org.apache.coyote) process:808, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1498, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1142, ThreadPoolExecutor (java.util.concurrent) run:617, ThreadPoolExecutor$Worker (java.util.concurrent) run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:745, Thread (java.lang)
RCE的POC(需要上传至服务器,即使是图片也能成功)
1 <%Runtime.getRuntime().exec("calc.exe" );%>
补丁 传送门
白名单限制可以赋值的设置,之前的三个设置不可控。
Defence
升级打补丁
关闭ajp、修改地址为127.0.0.1、设置高强度secret
Reference https://zhuanlan.zhihu.com/p/29665847
https://www.cnblogs.com/softidea/p/5735102.html
http://gv7.me/articles/2020/cve-2020-1938-tomcat-ajp-lfi/
https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
https://www.anquanke.com/post/id/199448