|
#!/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()
|