【CVE-2020-1938】Tomcat AJP 任意文件读取和包含漏洞分析记录

0x00 前言
2020年2月20日傍晚,在某群看到有群友问CNVD的tomcat文件包含漏洞有什么消息没

 

 

接着看到安恒信息应急响应中心公众号发了个漏洞公告
随后chy师傅也在群里发了个阿里云的公告链接
根据安恒和阿里云公告给出的信息我们知道是tomcat ajp服务出了问题,但具体是哪里出了问题却不知道。(这个时候我就在想阿里云是怎么知道ajp服务出了问题的呢?

 

 

 
以tomcat7为例给出的修复版本是100,遂去github看commit,看100版本release之前的commit记录。
 
0x01 初见端倪
找到15天前相关ajp的commit

 

从最下面往上看:
1,先是默认不开启AJP connector,然后又是修改默认绑定地址,绑定在本地

 

2,修改一个属性设置

https://github.com/apache/tomcat/commit/40d5d93bd284033cf4a1f77f5492444f83d803e2

 

 强制设置认证secret,否则不启动AJP Connector

 

3,添加一个新AJP属性

https://github.com/apache/tomcat/commit/b99fba5bd796d876ea536e83299603443842feba

 

 

 应该就是新属性这里。

因为原本的代码里面会把不识别的属性添加进去,从而导致操纵内部数据。(但怎么操纵呢?)

https://github.com/apache/tomcat/commit/b99fba5bd796d876ea536e83299603443842feba#diff-e5bf250e10dab446db3ee424bc5c9ba8L871

 

 

接下来重点是分析ajp协议交互,如何发送属性
后面翻资料,看到cnvd的公告(https://www.cnvd.org.cn/webinfo/show/5415

 

注意公告里面的这句话

相关参数可控,构造特定参数”

 

加上前面对commit的分析,也从侧面证实了自己的想法。
课间休息,吃个饭回来
安恒研究院公众号直接发了分析文章(https://mp.weixin.qq.com/s/GzqLkwlIQi_i3AVIXn59FQ)
直接就抛出了要控制的三个属性

 

 

与及两种利用方式

1、利用DefaultServlet实现任意文件下载 (不带后缀)

2、通过jspservlet实现任意后缀文件包含 (带jsp后缀)

 这下子就全明白了,接下来就是下载tomcat源码调试了。

 

0x02 环境搭建

环境搭建这块其实是在吃饭之前就弄了,因为一直被吹去吃饭,导致一直配置不成功源码导入idea。
根据这个文章:https://blog.csdn.net/u013268035/article/details/81349341,我用的是tomcat 7.0.99搭建环境
有几个注意点:
1,我们需要下载两个tomcat,一个是tomcat源码压缩包(https://archive.apache.org/dist/tomcat/tomcat-7/v7.0.99/src/apache-tomcat-7.0.99-src.zip),另一个是tomcat可执行的压缩包。(https://archive.apache.org/dist/tomcat/tomcat-7/v7.0.99/bin/apache-tomcat-7.0.99-windows-x64.zip)
2,根据文章,注意是将tomcat可执行文件压缩包里面的webapps和conf拷贝到源码的home目录
3,新建一个pom.xml,复制一下即可
<?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>Tomcat7.0.99</artifactId>
    <name>Tomcat7.0.99</name>
    <version>7.0</version>
 
    <properties>
        <java.version>1.7</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.apache.ant</groupId>
            <artifactId>ant</artifactId>
            <version>1.7.1</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.5.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <build>
        <finalName>Tomcat7.0</finalName>
        <sourceDirectory>java</sourceDirectory>
        <resources>
            <resource>
                <directory>java</directory>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.0</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

成功跑起来(后补的图)

 

接下来的问题就在于如何用ajp协议与tomcat交互,然后动态调试了
交互这块,两个方法:
1,直接根据官方文档手撸
2,找人家写好的

 

在之前长亭科技(漏洞发现者)发了漏洞公告,还起了个名字叫“幽灵猫”,搭配发了poc检测到xray,拉下xray用wireshark抓包分析了一下
发现只是检测一下版本而已,但是有一个标头让我觉得奇怪,“AJP_REMOTE_PORT”,这个东西应该就是属性了。

 

 

回到正题,代码太水,手撸时间花费太多了,直接找别人写好的,github大法好,直接搜索ajp

 

 

找到两个库,看一下READEME,选择了python版本
拉下来研究一下代码(代码很熟悉就是chy师傅推特gif图上面的),又看到了我们熟悉的"AJP_REMOTE_PORT"

 

接着研究一下代码,改写一下就可以发送我们自己的属性了。

 

 wireshark抓包,跟xray的poc一样了

 

 

 0x03 源码调试

根据前面,我们可以知道关键点在org.apache.coyote.ajp.AbstractAjpProcessor prepareRequest方法
在org.apache.coyote.ajp.AbstractAjpProcessor process方法中 在调用prepareRequest方法的地方下一个断点
 

 

 跟进,直接跟进到Decode extra attributes,也就是获取解析属性设置属性的地方

 

循环获取,switch判断,看到如果case是属性类型,在最后的一个else里面把没有判断到的属性直接设置到request里面

 

代码接着往下走,在预处理完了request headers之后,在adapter里面处理request

 

 接着调用容器来处理

 

 

 根据请求的url是否带JSP后缀,tomcat会将request交由不同的servlet来处理

不带jsp后缀的,直接用DefaultServlet来处理的情况(文件读取)

在HttpServlet中根据请求方法调用不同的方法处理

 

这里方法是GET,一路跟进去

 

 跟进,最后看到处理路径的方法getRelativePath

 

 也就是在这里对安恒说的那三个值进行判断

 

 当javax.servlet.include.request_uri不为空的时候,取javax.servlet.include.path_info和javax.servlet.include.servlet_path的值进行拼接,然后返回path,之后进入lookupCache方法

 

 

 这里面的流程先是在缓存里面找,找不到了,然后在本地找,最终来到 org.apache.naming.resources.FileDirContext 的file方法,然后new一个File类对象。

 

 在File构造函数中会对path进行净化,限制了跨目录

 

 调用栈

file:811, FileDirContext (org.apache.naming.resources)
doLookup:208, FileDirContext (org.apache.naming.resources)
doLookupWithoutNNFE:494, BaseDirContext (org.apache.naming.resources)
lookup:475, BaseDirContext (org.apache.naming.resources)
lookupCache:1463, ProxyDirContext (org.apache.naming.resources)
serveResource:831, DefaultServlet (org.apache.catalina.servlets)
doGet:435, DefaultServlet (org.apache.catalina.servlets)
service:621, HttpServlet (javax.servlet.http)
service:415, DefaultServlet (org.apache.catalina.servlets)
service:728, HttpServlet (javax.servlet.http)
internalDoFilter:303, ApplicationFilterChain (org.apache.catalina.core)
doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
invoke:219, StandardWrapperValve (org.apache.catalina.core)
invoke:110, StandardContextValve (org.apache.catalina.core)
invoke:492, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:165, StandardHostValve (org.apache.catalina.core)
invoke:104, ErrorReportValve (org.apache.catalina.valves)
invoke:1025, AccessLogValve (org.apache.catalina.valves)
invoke:116, StandardEngineValve (org.apache.catalina.core)
service:452, CoyoteAdapter (org.apache.catalina.connector)
process:190, AjpProcessor (org.apache.coyote.ajp)
process:654, AbstractProtocol$AbstractConnectionHandler (org.apache.coyote)
run:317, JIoEndpoint$SocketProcessor (org.apache.tomcat.util.net)
runWorker:1145, ThreadPoolExecutor (java.util.concurrent)
run:615, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

最后就是将读取到的资源输出回来

 

带JSP后缀jspservlet处理情况(文件包含)

在jspservlet的service方法断点,从request中属性中取出org.apache.catalina.jsp_file的值放到jspFile中,之后传入到serviceJspFile中处理。

 

 继续跟进,会先判断jsp 文件是否存在,如果存在,随后才会初始化wrapper,最后调用JspServletWrapper的service方法来解析。

 

 这里继续跟进getResource,当System.getSecurityManager()=true的时候,可从远程加载文件

 

 继续走,先是对path进行规范化处理

 

 继续跟进,到最后同样也是在org.apache.naming.resources.FileDirContext file方法中新创建一个File对象,判断文件是否存在。

 

而其中base变量的值为访问的容器的web根目录

 

题外:tomcat内部是如何判断使用哪个servlet的呢?

在org.apache.tomcat.util.http.mapper internalMapExtensionWrapper方法进行一系列判断,设置wrapper,其中有判断,根据后缀设置warpper,当后缀为jsp或jspx的时候都会用jspServlet来处理

 

 

 

 调用栈如下:

internalMapExtensionWrapper:1170, Mapper (org.apache.tomcat.util.http.mapper)
internalMapWrapper:945, Mapper (org.apache.tomcat.util.http.mapper)
internalMap:874, Mapper (org.apache.tomcat.util.http.mapper)
map:742, Mapper (org.apache.tomcat.util.http.mapper)
postParseRequest:782, CoyoteAdapter (org.apache.catalina.connector)
service:446, CoyoteAdapter (org.apache.catalina.connector)
process:190, AjpProcessor (org.apache.coyote.ajp)
process:654, AbstractProtocol$AbstractConnectionHandler (org.apache.coyote)
run:317, JIoEndpoint$SocketProcessor (org.apache.tomcat.util.net)
runWorker:1145, ThreadPoolExecutor (java.util.concurrent)
run:615, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

具体的处理逻辑就不说了

 

0x04 总结
0,思路一定要冷静清晰,分析清楚最重要的点,明白自己应该做什么
这里总结一下自己的思路:看公告,寻蛛丝马迹,然后到github找commit记录,大概理解漏洞的原理,然后根据需要的东西一步步进行
(需要AJP交互,如何解决?需要源码调试,环境搭建?)
1,漏洞公告以官方为准,细心留意公告的用词
2,开源的代码首先想到到github找commit记录
3,多方资料辅助验证
 
不足的地方:
0,tomcat源码运行流程,安恒是怎么知道DefaultServlet和jspservlet的呢?
1,代码能力提升,如果没有人家的轮子,你能自己快速造出来吗?
posted @ 2020-02-21 21:20  水泡泡  阅读(6070)  评论(0编辑  收藏  举报