Add files via upload

This commit is contained in:
公明
2026-07-16 10:50:35 +08:00
committed by GitHub
parent c5c6b1cb4a
commit d244943019
+124 -12
View File
@@ -127,6 +127,80 @@ args:
return urllib.parse.urlunsplit((parts.scheme, parts.netloc, path, query, fragment))
def path_has_dot_segments(path: str) -> bool:
"""True when path contains '.' / '..' segments that httpx would RFC-normalize away."""
if not path:
return False
for segment in path.split("/"):
if segment in {".", ".."}:
return True
return False
def split_origin_and_target(url: str) -> Tuple[str, bytes]:
"""Split URL into origin (scheme://netloc) and raw request-target bytes."""
parts = urllib.parse.urlsplit(url)
origin = urllib.parse.urlunsplit((parts.scheme, parts.netloc, "", "", ""))
if not origin.endswith("/"):
# httpx needs a valid URL string; path is supplied separately via raw target
pass
target = parts.path or "/"
if parts.query:
target = f"{target}?{parts.query}"
return origin.rstrip("/") + "/", target.encode("utf-8", errors="surrogateescape")
class RawPathURL:
"""Wrap httpx.URL but keep literal request-target (preserves ../ for LFI tests)."""
def __init__(self, base: "httpx.URL", raw_path: bytes):
self._base = base
self.raw_path = raw_path
def __getattr__(self, name):
return getattr(self._base, name)
def __str__(self):
host = self._base.host
scheme = self._base.scheme
port = self._base.port
default_port = 443 if scheme == "https" else 80
netloc = host if port in (None, default_port) else f"{host}:{port}"
return f"{scheme}://{netloc}{self.raw_path.decode('utf-8', errors='replace')}"
def prepare_request_url(url: str):
"""
Return (display_url, request_url, wire_target).
When path has '.'/'..' segments, request_url is a RawPathURL so httpx/httpcore
send the literal path instead of collapsing to e.g. /etc/passwd.
"""
try:
parts = urllib.parse.urlsplit(url)
except ValueError:
return url, url, None
if not path_has_dot_segments(parts.path or ""):
return url, url, None
origin, raw_target = split_origin_and_target(url)
base = httpx.URL(origin)
wrapped = RawPathURL(base, raw_target)
return str(wrapped), wrapped, raw_target
def explain_request_error(exc: BaseException) -> str:
text = str(exc)
lowered = text.lower()
hints = []
if "unexpected_eof" in lowered or "eof occurred in violation of protocol" in lowered:
hints.append(
"服务器在 TLS 层直接断开(常见于 WAF/反代拒绝异常路径、URI 过长或敏感路径探测)。"
"可尝试:缩短 ../ 层数、改用 %2e%2e 编码、加 --allow-insecure、或经代理观察原始响应。"
)
if hints:
return text + "\nHint: " + " ".join(hints)
return text
def quote_form_component_preserving_pct(value: str) -> str:
normalized = urllib.parse.unquote(value)
return urllib.parse.quote_plus(normalized, safe="")
@@ -431,11 +505,23 @@ args:
return min(values), total / count, max(values)
def render_request_overview(method: str, url: str, headers: httpx.Headers, body_meta: Dict[str, str]):
def render_request_overview(
method: str,
url: str,
headers: httpx.Headers,
body_meta: Dict[str, str],
wire_target: bytes = None,
input_url: str = None,
):
items = list(headers.items())
print("\n===== Prepared Request =====")
print(f"Method: {method}")
print(f"URL: {url}")
if input_url and input_url != url:
print(f"Input URL: {input_url}")
if wire_target is not None:
print(f"Wire request-target: {wire_target.decode('utf-8', errors='replace')}")
print("Note: preserved '.'/'..' path segments (httpx would otherwise normalize them away).")
print(f"Headers ({len(items)} total):")
for key, value in items:
print(f" {key}: {value}")
@@ -739,7 +825,8 @@ args:
except ValueError:
delay_between = 0.0
prepared_url = smart_encode_url(args.url) if args.auto_encode_url else args.url
encoded_url = smart_encode_url(args.url) if args.auto_encode_url else args.url
prepared_url, request_url, wire_target = prepare_request_url(encoded_url)
method = (args.method or "GET").upper()
# 处理 headers:支持字典(JSON字符串)和字符串格式
@@ -823,7 +910,14 @@ args:
client_kwargs["trust_env"] = str_to_bool(additional_options["trust_env"])
if args.show_command:
render_request_overview(method, prepared_url, headers, body_meta)
render_request_overview(
method,
prepared_url,
headers,
body_meta,
wire_target=wire_target,
input_url=args.url if args.url != prepared_url else None,
)
aggregate = None
if args.show_summary:
@@ -842,7 +936,7 @@ args:
time.sleep(delay_between)
metrics = {key: None for key in METRIC_KEYS}
probe_metrics = probe_connection(prepared_url, timeout_value or 60.0, verify_tls, skip_probe)
probe_metrics = probe_connection(encoded_url, timeout_value or 60.0, verify_tls, skip_probe)
metrics.update(probe_metrics)
start = time.perf_counter()
@@ -850,15 +944,28 @@ args:
body_buffer = bytearray()
try:
stream_kwargs = {
"method": method,
"url": prepared_url,
"headers": headers,
}
if wire_target is not None:
origin, _ = split_origin_and_target(encoded_url)
build_kwargs = {
"method": method,
"url": origin,
"headers": headers,
}
else:
build_kwargs = {
"method": method,
"url": request_url,
"headers": headers,
}
if body_bytes is not None:
stream_kwargs["content"] = body_bytes
build_kwargs["content"] = body_bytes
with client.stream(**stream_kwargs) as response:
request = client.build_request(**build_kwargs)
if wire_target is not None:
request.url = RawPathURL(request.url, wire_target)
response = client.send(request, stream=True)
try:
status_code = response.status_code
metrics["http_code"] = status_code
metrics["redirects"] = len(response.history)
@@ -960,12 +1067,16 @@ args:
for key, value in probe_metrics.items():
print(f" Probe {key}: {value:.6f}s")
print(f" History length: {len(response.history)}")
if wire_target is not None:
print(f" Wire request-target: {wire_target.decode('utf-8', errors='replace')}")
finally:
response.close()
except httpx.HTTPError as exc:
exit_code = 1
elapsed = time.perf_counter() - start
print(f"\n===== Response #{run_index + 1} =====")
print(f"Request failed after {elapsed:.6f}s: {exc}")
print(f"Request failed after {elapsed:.6f}s: {explain_request_error(exc)}")
continue
if aggregate and repeat > 1:
@@ -1005,6 +1116,7 @@ description: |
**亮点:**
- 纯 Python 实现:httpx 会话重用、HTTP/2/代理/certs 直接在脚本内配置,无外部二进制依赖
- 路径穿越保真:自动保留 URL 中的 `.`/`..` 段(避免 httpx RFC 规范化把 LFI payload 折叠成 `/etc/passwd`
- 智能 Body 编码:支持 application/x-www-form-urlencoded 规范化编码、JSON/文本 charset 推断、
`@file`/`@-` 注入二进制、可视化调试
- 连接探针:在无代理场景下额外进行 DNS/TCP/TLS 探测,粗粒度复刻 curl -w 指标