草庐IT

Tomcat AJP 文件包含漏洞(CVE-2020-1938)

@Camelus 2025-07-26 原文

目录

1.漏洞简介

2、AJP13 协议介绍

Tomcat 主要有两大功能:

3.Tomcat 远程文件包含漏洞分析

4.漏洞复现

 5、漏洞分析

6.RCE 实现的原理

1.漏洞简介


  • 2020 2 20 日,公开CNVD 的漏洞公告中发现 Apache Tomcat 文件包含漏洞(CVE-2020-1938)。
  • Apache Tomcat Apache 开源组织开发的用于处理 HTTP 服务的项目。Apache Tomcat 服务器中被发现存在文件包含漏洞,攻击者可利用该漏洞读取或包含 Tomcat上所有 webapp 目录下的任意文件。
  • 该漏洞是一个单独的文件包含漏洞,依赖于 Tomcat AJP(定向包协议)AJP自身存在一定缺陷,导致存在可控参数,通过可控参数可以导致文件包含漏洞。AJP协议使用率约为 7.8%,鉴于 Tomcat 作为中间件被大范围部署在服务器上,该漏洞危害较大。

2、AJP13 协议介绍


Tomcat 主要有两大功能:

  • 一是充当 Web 服务器,可以对一切静态资源的请求作出回应;常见的 Web 服务器有 ApacheNginxIIS
  • 二是充当 Servlet 容器常见的 Servlet 容器有 TomcatWeblogicJBOSS
        Servlet 容器可以理解为 Web 服务器的升级版。以 Tomcat 为例, Tomcat 本身可以不作为 Servlet 容器使用,仅仅充当 Web 服务器的角色,但是其处理静态资源请求的效率和速度远不及 Apache ,所以很多情况下生产环境会将 Apache 作为 Web 服务器来接收用户的请求。静态资源由 Apache 直接处理,而 Servlet 请求则交由 Tomcat来进行处理。这种方式使两个中间件各司其职,大大加快了响应速度。
        众所周知,用户的请求是以 HTTP 协议的形式传递给 Web 服务器。我们在浏览器中对某个域名或者 ip 进行访问时,头部都会有 http 或者 https 的表示,而 AJP 浏览器是不支持的,我们无法通过 浏览器发送 AJP 的报文。AJP 这个协议并不是提供给用户使用的。
  • Tomcat$ CATALINA_BASE/conf/web.xml 默认配置了两个 Connector,分别监听两个不同的端口,一个是 HTTP Connector 默认监听 8080 端口,另一个是 AJP Connector 默认监听 8009 端口。
  • HTTP Connector 主要负责接收来自用户的请求,包括静态请求和动态请求。有了 HTTP ConnectorTomcat 才能成为一个 Web 服务器,还可以额外处理 Servlet 和JSP。
  • AJP 的使用对象通常是另一个 Web 服务器,例如 Apache,这里以下图进行说明。
Apache 服务器

 

  • AJP 是一个二进制TCP 传输协议。浏览器无法使用 AJP,而是首先由 Apache 与 Tomcat 进行 AJP 的通信,然后由 Apache 通过 proxy_ajp 模块进行反向代理,将其转换成 HTTP 服务器再暴露给用户,允许用户进行访问。
  • 这样做的原因是,相对于 HTTP 纯文本协议来说,效率和性能更高,同时也做了很多优化。在某种程度上,AJP 可以理解为 HTTP 的二进制版,因加快传输效率被广泛应用。实际情况是类似 Apache 这样有 proxy_ajp 模块可以反向代理 AJP 协议的服务器很少,所以 AJP 协议在生产环境中也很少被用到。

3.Tomcat 远程文件包含漏洞分析


        
首先从官网下载对应的 Tomcat 源码文件和可执行文件,如下图 所示
下载 Tomcat 源码文件和可执行文件

 

两个文件夹下载好后,存放入在同一个目录下,然后在源码中新增 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>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.3</version> 
     <configuration> 
             <encoding>UTF-8</encoding>
             <source>1.8</source> 
 <target>1.8</target> 
 </configuration> 
 </plugin> 
 </plugins> 
 </build> 
 
 <dependencies> 
 <dependency> 
 <groupId>junit</groupId> 
 <artifactId>junit</artifactId> 
 <version>4.12</version> 
 <scope>test</scope> 
 </dependency> 
 <dependency> 
 <groupId>org.easymock</groupId> 
 <artifactId>easymock</artifactId> 
 <version>3.4</version> 
 </dependency> 
 <dependency> 
 <groupId>ant</groupId> 
 <artifactId>ant</artifactId> 
 <version>1.7.0</version> 
 </dependency> 
 <dependency> 
 <groupId>wsdl4j</groupId> 
 <artifactId>wsdl4j</artifactId> 
 <version>1.6.2</version> 
 </dependency> 
 <dependency> 
 <groupId>javax.xml</groupId> 
 <artifactId>jaxrpc</artifactId> 
 <version>1.1</version> 
 </dependency> 
 <dependency> 
 <groupId>org.eclipse.jdt.core.compiler</g
roupId> 
 <artifactId>ecj</artifactId> 
 <version>4.5.1</version> 
 </dependency> 
 </dependencies> 
</project>

然后添加一个 Application,如下图所示

  •  新增 Application 的配置信息。
  • Man class:中填入:org.apache.catalina.startup.Bootstrap
  • VMoptions:中填入:-Dcatalina.home="apache-tomcat-8.5.34",并将 catalina.
  • home 替换成 tomcat binary core 的目录。
  • JDK 默认是 1.8,因为我安装的是 jdk1.8 版本。
  • 启动过程中 Test 模块会报错,且为 TestCookieFilter.java,注释里面的测试内容即可。
  • 然后访问 127.0.0.1:8080,如出现以下页面,则表示环境搭建成功,如下图所示。
环境搭建成功

 

4.漏洞复现


任意文件读取漏洞复现,如下图
读取文件

 RCE 如图 4-1 和图 4-2 所示

图 4-1 RCE(一)

 

图 4-2 RCE(二)

 5、漏洞分析


首先定位到类 org.apache.coyote.ajp.AjpProcessor。根据网上透漏的漏洞消息,得知漏洞的产生是由于 Tomcat ajp 传递过来的数据的处理方式存在问题,导致用户可以控制

  1. “javax.servlet.include.request_uri
  2. javax.servlet.include.path_info
  3. javax. servlet.include.servlet_path”

3 个参数,从而读取任意文件,甚至可以进行 RCE

我们先从任意文件读取开始分析。环境使用 Tomcat 8.0.50 版本搭建,产生漏洞的原因并不在于 AjpProcessor.prepareRequest() 方法。 8.0.50 版本的漏洞点存在于AjpProcessor 的父类,即 AbstractAjpProcessor 抽象类的 prepareRequest() 中,如下图 5-1  所示
图 5-1 漏洞点分析

 在这里设置断点,然后运行 exp,查看此时的调用链,如下图所示

图 5-2 设置断点并运行 exp

 由于此次数据传输使用的是 AJP,经过 8009 口,并非我们常见的 HTTP,因此首先由内部类 SocketPeocessore 来进行处理。

处理完成后,经过几次调用交由 AbstractAjpProcessor.prepareRequest() 方法,该方法是漏洞产生的第一个点,如图 5-3  所示
图 5-3  漏洞产生的第一个点

         单步执行 request.setAttribute()方法,如图 5-4 和图 5-5 所示

图 5-4 单步执行 request.setAttribute()方法(一)

 

图 5-5  单步执行 request.setAttribute()方法(二)

 

这里我们可以看到, attributes 是一个 HashMap ,将通过 AJP 传递过来的 3 个参数循环遍历存入这个 HashMap ,如图 5-6  所示。
图 5-6 存储 3 个参数的 HashMap

 可以看到这里是一个 while 循环,直接来看循环完成后的结果,如图 5-7 所示

图 5-7  while 循环完成后的结果

 

先来查看 exp 发出的数据包,如图 5-8  所示
图 5-8 exp 发出的数据包

 通过使用 WireShark 抓包查看 AJP 报文的信息,其中有 4 个比较重要的参数如下。

URI:/asdf 
javax.servlet.include.request_uri:/ 
javax.servlet.include.path_info: WEB-INF/Test.txt 
javax.servlet.include.servlet_path:/
通过 AJP 传来的数据需要交由 Servlet 进行处理,那么应该交由哪个 Servlet 呢?
通过阅读关于 Tomcat 架构的文章和资料得知, Tomcat$ CATALINA_BASE/conf/web.xml 配置文件中默认定义了两个 Servlet :一个是 DefaultServlet ,如图 5-9所示;另一个是 JspServlet ,如图 5-92 所示。
图 5-9 默认定义的 DefaultServlet

 

图 5-10 默认定义的 JspServlet

         由于$ CATALINA_BASE/conf/web.xml 文件是 tomcat 启动时默认加载的,因此二个Servlet 会默认存放在 Servlet 容器中。当用户请求的 URI 不能与任何 Servlet 匹配时,会默认交由 DefaultServlet 来处理。DefaultServlet 主要用于处理静态资源,如 HTML、图片、CSSJS 文件等,而且为了提升服务器性能,Tomcat 将对访问文件进行缓存。按照默认配置,客户端请求路径与资源的物理路径是一致的。

        我们看到请求的 URI 为“ /asdf ”,符合无法匹配后台任何 Servlet 的条件。这里需要注意的是,举例来说,我们请求一个“abc.jsp ”,但是后台没有“ abc.jsp ”,这不属于无法匹配任何 Servlet ,因为 .jsp 的请求会默认由 JspServlet 进行处理,如图 5-11 所示。
图 5-11  无法匹配任何 Servlet

 根据上述内容,结合发送数据包中的“URI:/asdf”这一属性,可以判断该请求是由 DefaultServlet 进行处理的。

        定位到 DefaultServlet doGet 方法,如图 5-12   所示。
图 5-12  定位到 DefaultServlet 的 doGet 方法

 doGet 方法中调用了 serveResource()方法。serveResource()方法调用了 getRelativePath()

方法来进行路径拼接,如图 5-13  所示
图 5-13 路径拼接

这里就是将传入的 path_info servlet_path 进行复制的地方。 request_uri 用来做判断,如果发送的数据包中没有 request_uri,就会执行 else 后面的两行代码进行赋值。这会导致漏洞利用失败,如图 5-14 所示
图 5-14  执行代码进行赋值

 

接下来是对路径的拼接。这里可以看到,如果传递数据时不传递 servlet_path ,则 result 在进行路径拼接时不会将“/”拼接在“ WEB-INF/web.xml ”的头部。最后拼接的结果仍然是“WEB-INF/web.xml ”,如图 5-15  所示
图 5-15 拼接结果仍然是“WEB-INF/web.xml”

返回 DefaultServle.serveResource() 。然后判断 path 变量长度是否为 0 ,为 0 则调
目录重定向方法,如图 5-16  所示。

 

图 5-16  调用目录重定向方法

 

下面的代码开始读取指定的资源文件,如图 5-17  和图 5-18  所示:
图 5-17 读取指定的资源文件
图 5-18 resources 对象

 

 执行 StandardRoot.getResource()方法,如图 5-19 所示

图 5-19 执行 StandardRoot.getResource()方法

 getResource()方法中调用了很重要的 validate()方法,并将 path 作为变量传递进去进行处理。这里会涉及不能通过“/../../”的方式来读取 webapp 目录的上层目录中的文件的原因。首先是正常请求流程,如图 5-20  所示。

图 5-20 正常请求流程

 我们可以看到正常请求后 return result 路径就是文件所在的相对路径。 当我们尝试使用 WEB-INF/../../Test.txt 来读取 webapp 以外的目录中的文件时, 可以看到此时返回的 result null,而且会抛出异常,如图 5-21 所示

图 5-21 尝试目录穿越(一)

 所有原因都在于 RequestUtil.normalize()函数对我们传递进来的路径的处理方式。

关键的点就在下面的截图代码中。我们传入的路径是“ /WEB-INF/../../Test.txt , 首先程序会判断路径中是否存在“/../ ”,答案是包含且索引大于 8 ,所以第一个 if 判断不会成功,也不会跳出 while 循环。此时处理我们的路径,截取“/WEB-INF/..”以后的内容。然后 String,indexOf()函数判断路径中是否包含“/../ ”,答案是包含且索引为零,符合第二个 if 判断的条件,返回 null ,如图 5-21  所示
图 5-21  尝试目录穿越(二)

    ·此处的目标是不允许传递的路径的开头为“/../”,且不允许同时出现两个连在一起的“/../”,所以我们最多只能读取到 webapp 目录,无法读取 webapp 以外的目录中的文件。

        要读取 webapp 目录下的其余目录内的文件,可以通过修改数据包中的“ URI ”参数来实现,如图 5-22  所示
图 5-22 修改 URI

 程序最终会拼接出我们所指定文件的绝对路径,并作为返回值返回,如图 5-23 所示。

图 5-23 成功拼接文件路径

 

接下来回到 getResource() 函数进行文件读取,如图 5-24  所示
图 5-24 文件读取

 

以下是任意文件读取的调用链,如图 5-25  所示
图 5-25 任意文件读取的调用链

 

6.RCE 实现的原理


        前面介绍过 Tomcat$ CATALINA_BASE/conf/web.xml 配置文件中默认定义了两个 Servlet 。上述任意文件读取利用了 DefaultServlet,而 RCE 则需要用到 JspServlet 。 默认情况下,JspServlet url-pattern .jsp .jspx ,因此它负责处理所有 JSP 文件的请求。
JspServlet 主要完成以下工作:
  • 根据 JSP 文件生成对应 Servlet Java 代码(JSP 文件生成类的父类 org.apache.jasper.runtime.HttpJspBase——实现了 Servlet 接口)。
  • Java 代码编译为 Java 类。
  • 构造 Servlet 类实例并且执行请求。
  1. RCE 本质是通过 JspServlet 来执行我们想要访问的.jsp 文件。
  2. RCE 的前提是,首先想办法将包含需要执行的命令的文件(可以是任意文件后缀,甚至没有后缀)上传到 webapp 的目录下,才能访问该文件;然后通过 JSP 模板的解析造成 RCE。
        查看本次发送的 AJP 报文的内容,如图 6 -1  所示
图 6-1 AJP 报文的内容

 

        这里的“URI ”参数必须以“ .jsp ”结尾,但是该 JSP 文件可以不存在。
        其余 3 个参数与之前的没有区别,“ path_info ”参数对应的是我们上传的包含 JSP代码的文件。
        定位到 JspServlet.Service() 方法,如图 6-2  所示。
图 6-2 定位到 JspServlet.Service()方法
        首先,将“servlet_path ”的值取出赋值给变量 jspUri ,如图 6-3  所示

 

图 6-3  赋值给变量 jspUri

 

        然后,将“path_info ”参数对应的值取出并赋值给“ pathInfo ”变量,然后与“ jspUri ” 进行拼接,如图6-4  和图 6-5    所示
图6-4  赋值给变量 pathInfo 并拼接(一)

图 6-5  赋值给变量 pathInfo 并拼接(二)

        

         接下来调用 serviceJspFile()方法,如图 6-6 所示

图6-6  调用 serviceJspFile()方法

        首先生成 JspServletWrapper 对象,如图 6-7   所示
图 6-7 生成 JspServletWrapper 对象

 

        然后调用 JspServletWrapper.service() 方法,如图6-8   所示

图 6-8 调用 JspServletWrapper.service()方法

 

        获取对应的 servlet ,如图 6-9  所示。

图 6-9  获取对应的 servlet

 

调用该 servlet service 方法,如图 6-10  所示

 

图 6-10 调用的 service 方法

 

接下来解析上传文件中的 Java 代码。至此, RCE 漏洞原理分析完毕。调用链如下图所示
RCE 漏洞原理分析完毕

 

有关Tomcat AJP 文件包含漏洞(CVE-2020-1938)的更多相关文章

  1. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

    我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  8. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  9. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  10. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

随机推荐