Project

General

Profile

Bug #8940 » repro_http_stream.py

JIAXIN TAN, 08/24/2026 07:30 AM

 
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
复现「Zeek 有攻击 HTTP 记录但 Suricata 无告警」问题 —— 真实扫描器的攻击请求形态

背景(2026-08-24 通过 ES 日志 + Zeek 离线分析确认):
真实扫描器(如源 1.13.9.36)的攻击请求,是【完整正常的 HTTP 流】:
完整三次握手 + 正常 seq + 服务器回 404 响应(如 /.git/index 等路径)。
之前「半开/seq 错乱状态下发 HTTP 数据」的认知是过拟合的畸形流,已纠正。

本脚本用真实客户端(内核 TCP 栈)发送攻击请求,完整握手、正常 seq、
收到服务器响应 —— 精确复现真实扫描器攻击请求的流量形态。

用法:
# 默认 6 个攻击路径轮流发送
sudo python3 repro_http_stream.py -i 10.10.168.197

# 指定端口 / 单个路径 / 次数
sudo python3 repro_http_stream.py -i 10.10.168.197 -p 8888 -P "/v1/api-docs" -c 3

参数说明:
-i, --ip 目标 IP(受害主机),必填
-p, --port 目标端口,默认 80
-P, --path 攻击 URI 路径,指定后只发该路径;不指定则默认 6 个路径轮流发送
--host 请求 Host 头(默认用 --ip)
-c, --count 每个路径发送次数,默认 1

源 IP 自动探测本机出口 IP。

默认路径列表(不指定 -P 时轮流发送):
/.git/index /.svn/entries /WEB-INF/web.xml
/druid/index.html /v1/api-docs /prod-api/druid/index.html
"""

DEFAULT_PATHS = [
"/.git/index",
"/.svn/entries",
"/WEB-INF/web.xml",
"/druid/index.html",
"/v1/api-docs",
"/prod-api/druid/index.html",
]

import argparse
import socket
import time


def get_local_ip(dst_ip: str) -> str:
"""获取本机到目标的路由出口 IP"""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect((dst_ip, 9))
return s.getsockname()[0]
except Exception:
return "127.0.0.1"
finally:
s.close()


def build_http_request(path: str, host: str, dport: int) -> bytes:
if ":" in host and not host.startswith("["):
host_header = f"{host}:{dport}"
else:
host_header = host
req = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {host_header}\r\n"
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36\r\n"
"Accept: */*\r\n"
"Connection: close\r\n"
"\r\n"
)
return req.encode("latin-1")


def send_http(dst_ip: str, dport: int, path: str, host: str) -> str:
"""发送完整 HTTP 请求(走内核 TCP 栈),返回响应状态码或错误信息。"""
req = build_http_request(path, host, dport)
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3)
s.connect((dst_ip, dport))
s.sendall(req)
status = None
try:
resp = s.recv(4096)
if resp:
first_line = resp.split(b"\r\n", 1)[0].decode("latin-1", "ignore")
parts = first_line.split(" ", 2)
if len(parts) >= 2:
status = parts[1]
except socket.timeout:
pass
s.close()
return status or "(无响应)"
except OSError as e:
return f"ERR:{e}"


def main():
ap = argparse.ArgumentParser(description="复现真实扫描器攻击请求(完整 HTTP 流)")
ap.add_argument("-i", "--ip", required=True, help="目标 IP(受害主机)")
ap.add_argument("-p", "--port", type=int, default=80, help="目标端口,默认 80")
ap.add_argument("-P", "--path", default=None,
help="攻击 URI 路径,指定后只发该路径;不指定则默认 6 个路径轮流发送")
ap.add_argument("--host", default=None, help="Host 头(默认用 --ip)")
ap.add_argument("-c", "--count", type=int, default=1, help="每个路径发送次数,默认 1")
args = ap.parse_args()

dst_ip = args.ip
dport = args.port
src_ip = get_local_ip(dst_ip)
host = args.host or dst_ip

paths = [args.path] if args.path else DEFAULT_PATHS

print(f"[*] 源 IP : {src_ip}")
print(f"[*] 目标 : {dst_ip}:{dport}")
print(f"[*] 攻击URI : {', '.join(paths)}")
print(f"[*] 次数 : x{args.count}")
print()

for i in range(args.count):
for path in paths:
status = send_http(dst_ip, dport, path, host)
print(f" -> {path} (resp={status})")
time.sleep(0.1)


if __name__ == "__main__":
main()
(2-2/3)