agent-bypass 2.11.4 → 2.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/SKILL.md +5 -3
- package/bin/install.js +6 -3
- package/package.json +1 -1
- package/references/mobile-fast-recon.md +102 -0
- package/scripts/__pycache__/basicauth_brute.cpython-312.pyc +0 -0
- package/scripts/__pycache__/cvefetch.cpython-312.pyc +0 -0
- package/scripts/__pycache__/deck.cpython-312.pyc +0 -0
- package/scripts/__pycache__/depscan.cpython-312.pyc +0 -0
- package/scripts/__pycache__/dirbrute.cpython-312.pyc +0 -0
- package/scripts/__pycache__/fastrecon.cpython-312.pyc +0 -0
- package/scripts/__pycache__/jsrip.cpython-312.pyc +0 -0
- package/scripts/__pycache__/mutate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/portscan.cpython-312.pyc +0 -0
- package/scripts/__pycache__/webprobe.cpython-312.pyc +0 -0
- package/scripts/argsmini.py +158 -0
- package/scripts/basicauth_brute.py +82 -7
- package/scripts/dirbrute.py +505 -71
- package/scripts/fastrecon.py +982 -0
- package/scripts/platf.py +44 -0
- package/scripts/portscan.py +2 -0
package/scripts/dirbrute.py
CHANGED
|
@@ -1,25 +1,92 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
并发目录/文件爆破(双引擎:curl -Z 批量 / py 线程池)
|
|
5
5
|
|
|
6
6
|
用法:
|
|
7
7
|
python3 dirbrute.py https://target.com
|
|
8
8
|
python3 dirbrute.py https://target.com --wordlist my.txt
|
|
9
9
|
python3 dirbrute.py https://target.com --ext .php,.bak,.zip --mc 200,301,302,403
|
|
10
10
|
python3 dirbrute.py https://target.com --conc 30 --delay 0.1
|
|
11
|
+
python3 dirbrute.py https://target.com --engine py --no-soft404 --retries 3
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
引擎:
|
|
14
|
+
curl curl -Z 批量并行(默认,最快);并发由 --batch 控制
|
|
15
|
+
py 线程池(--conc 精确控制;--delay 为全局总速率限制)
|
|
16
|
+
auto --delay>0 或无 curl 时走 py,否则走 curl
|
|
17
|
+
|
|
18
|
+
两引擎行为一致,jsonl 字段一致: {"url","status","len","location","server","engine"}
|
|
19
|
+
软404过滤(默认开启,两引擎共用同一套基线):
|
|
20
|
+
先用若干随机路径取基线 (code, size),命中项若 code 与基线相同且
|
|
21
|
+
|len - 基线len| <= SOFT404_TOL(64) 字节则判定为误报丢弃;--no-soft404 关闭。
|
|
22
|
+
瞬态失败(连接被拒/超时)默认重试 --retries 次:指数退避 + 逐轮降低并发,
|
|
23
|
+
仍失败则计入 error 并在 stderr 汇总(不再静默丢结果)。
|
|
13
24
|
"""
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
import
|
|
25
|
+
import argsmini as argparse
|
|
26
|
+
import json
|
|
27
|
+
import random
|
|
28
|
+
import re
|
|
29
|
+
import os
|
|
30
|
+
import shutil
|
|
31
|
+
import string
|
|
32
|
+
import sys
|
|
33
|
+
import threading
|
|
34
|
+
import time
|
|
35
|
+
from collections import Counter
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---- 懒加载/popen 优化:ssl/urllib/concurrent import 各耗 1-5s(iSH),仅首次用时载入 ----
|
|
39
|
+
_ssl_m = None
|
|
40
|
+
def _get_ssl():
|
|
41
|
+
global _ssl_m
|
|
42
|
+
if _ssl_m is None: import ssl as _s; _ssl_m = _s
|
|
43
|
+
return _ssl_m
|
|
44
|
+
_url_m = None
|
|
45
|
+
def _get_urllib():
|
|
46
|
+
global _url_m
|
|
47
|
+
if _url_m is None: import urllib.request as _r; import urllib.error as _e; _url_m = (_r, _e)
|
|
48
|
+
return _url_m
|
|
49
|
+
_cf_m = None
|
|
50
|
+
def _get_cf():
|
|
51
|
+
global _cf_m
|
|
52
|
+
if _cf_m is None: import concurrent.futures as _c; _cf_m = _c
|
|
53
|
+
return _cf_m
|
|
54
|
+
def _urljoin(base, url):
|
|
55
|
+
import urllib.parse as _p
|
|
56
|
+
return _p.urljoin(base, url)
|
|
57
|
+
_SAFE = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@%_+=:,./-")
|
|
58
|
+
def _shq(s):
|
|
59
|
+
if s and all(c in _SAFE for c in s): return s
|
|
60
|
+
return "'%s'" % s.replace("'", "'" + chr(92) + "'" + "'")
|
|
61
|
+
def _run_curl(cargs):
|
|
62
|
+
p = os.popen(" ".join(_shq(a) for a in cargs) + " 2>/dev/null")
|
|
63
|
+
out = p.read(); p.close(); return out
|
|
64
|
+
|
|
65
|
+
def _new_ctx():
|
|
66
|
+
"""不用 create_default_context():本脚本已关闭证书校验,加载 CA 库纯属浪费时间
|
|
67
|
+
(实测多花 ~2s 启动,iSH 上尤其明显)。"""
|
|
68
|
+
ssl = _get_ssl()
|
|
69
|
+
c = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
70
|
+
c.check_hostname = False
|
|
71
|
+
c.verify_mode = ssl.CERT_NONE
|
|
72
|
+
return c
|
|
73
|
+
|
|
17
74
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
75
|
+
_CTX = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _ctx():
|
|
79
|
+
global _CTX
|
|
80
|
+
if _CTX is None:
|
|
81
|
+
_CTX = _new_ctx()
|
|
82
|
+
return _CTX
|
|
21
83
|
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36"
|
|
22
84
|
|
|
85
|
+
SOFT404_TOL = 64 # 软404 尺寸容差(字节)
|
|
86
|
+
SOFT404_PROBES = 3 # 软404 基线随机路径探测次数
|
|
87
|
+
MAX_BODY = 4 * 1024 * 1024
|
|
88
|
+
BACKOFF = (0.3, 0.8, 1.5, 3.0)
|
|
89
|
+
|
|
23
90
|
# 内置字典(常用路径/文件)
|
|
24
91
|
DEFAULT = """admin
|
|
25
92
|
login
|
|
@@ -207,92 +274,459 @@ assets.html
|
|
|
207
274
|
""".strip().split("\n")
|
|
208
275
|
|
|
209
276
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
277
|
+
# ---------------------------------------------------------------- 通用工具
|
|
278
|
+
def _hget(headers, name, default=""):
|
|
279
|
+
"""大小写不敏感取响应头,headers 可能为 None。"""
|
|
280
|
+
try:
|
|
281
|
+
v = headers.get(name)
|
|
282
|
+
return v if v is not None else default
|
|
283
|
+
except Exception:
|
|
284
|
+
return default
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _abs_location(url, loc):
|
|
288
|
+
"""把相对 Location 补成绝对 URL,与 curl 的 %{redirect_url} 对齐。"""
|
|
289
|
+
if not loc:
|
|
290
|
+
return ""
|
|
291
|
+
try:
|
|
292
|
+
return _urljoin(url, loc)
|
|
293
|
+
except Exception:
|
|
294
|
+
return loc
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _size_from(fp, headers, cap=MAX_BODY):
|
|
298
|
+
"""响应体字节数。优先用 Content-Length(与 curl 的 %{size_download} 对齐),
|
|
299
|
+
缺失时流式读取计数(最多 cap 字节)。返回 (size, truncated)。"""
|
|
300
|
+
cl = _hget(headers, "Content-Length", None)
|
|
301
|
+
if cl is not None:
|
|
302
|
+
try:
|
|
303
|
+
n = int(cl)
|
|
304
|
+
if n >= 0:
|
|
305
|
+
return n, n > cap
|
|
306
|
+
except (TypeError, ValueError):
|
|
307
|
+
pass
|
|
308
|
+
total = 0
|
|
309
|
+
try:
|
|
310
|
+
while total < cap:
|
|
311
|
+
buf = fp.read(min(65536, cap - total))
|
|
312
|
+
if not buf:
|
|
313
|
+
break
|
|
314
|
+
total += len(buf)
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
return total, total >= cap
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
_NOREDIR = None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _noredir_cls():
|
|
324
|
+
"""显式抛 HTTPError 的 no-redirect handler(首次用时构造,懒加载 urllib)。"""
|
|
325
|
+
global _NOREDIR
|
|
326
|
+
if _NOREDIR is None:
|
|
327
|
+
_r, _e = _get_urllib()
|
|
328
|
+
|
|
329
|
+
class _NoRedir(_r.HTTPRedirectHandler):
|
|
330
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
215
331
|
return None
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
332
|
+
|
|
333
|
+
def http_error_302(self, req, fp, code, msg, headers):
|
|
334
|
+
raise _e.HTTPError(req.full_url, code, msg, headers, fp)
|
|
335
|
+
|
|
336
|
+
http_error_301 = http_error_303 = http_error_307 = http_error_308 = http_error_302
|
|
337
|
+
|
|
338
|
+
_NOREDIR = _NoRedir
|
|
339
|
+
return _NOREDIR
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
_OPENERS = {}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _get_opener(no_redirect=True):
|
|
346
|
+
"""opener 全局缓存(SSL context 与 handler 只装一次)。"""
|
|
347
|
+
key = bool(no_redirect)
|
|
348
|
+
if key not in _OPENERS:
|
|
349
|
+
_r, _e = _get_urllib()
|
|
350
|
+
handlers = [_r.HTTPSHandler(context=_ctx())]
|
|
351
|
+
if key:
|
|
352
|
+
handlers.insert(0, _noredir_cls()())
|
|
353
|
+
_OPENERS[key] = _r.build_opener(*handlers)
|
|
354
|
+
return _OPENERS[key]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class RateLimiter:
|
|
358
|
+
"""全局速率限制:--delay 表示所有 worker 合计的每请求最小间隔
|
|
359
|
+
(原实现是每个 worker 各自 sleep,20 并发下 0.1s 实际会打出 ~200 req/s)。"""
|
|
360
|
+
|
|
361
|
+
def __init__(self, delay):
|
|
362
|
+
self.delay = float(delay or 0)
|
|
363
|
+
self._lock = threading.Lock()
|
|
364
|
+
self._next = 0.0
|
|
365
|
+
|
|
366
|
+
def wait(self):
|
|
367
|
+
if self.delay <= 0:
|
|
368
|
+
return
|
|
369
|
+
with self._lock:
|
|
370
|
+
now = time.monotonic()
|
|
371
|
+
if self._next < now:
|
|
372
|
+
self._next = now
|
|
373
|
+
self._next += self.delay
|
|
374
|
+
sleep_for = self._next - now
|
|
375
|
+
if sleep_for > 0:
|
|
376
|
+
time.sleep(sleep_for)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ---------------------------------------------------------------- py 引擎
|
|
380
|
+
def _probe_py(url, timeout):
|
|
381
|
+
"""单请求探测,返回 (code|None, size, location, server, err)。
|
|
382
|
+
|
|
383
|
+
code=None 表示传输层失败(连接被拒/DNS/超时等)。
|
|
384
|
+
3xx 不跟随重定向,状态码照常返回(见 _NoRedir)。
|
|
385
|
+
"""
|
|
222
386
|
try:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
387
|
+
_r, _e = _get_urllib()
|
|
388
|
+
req = _r.Request(url, headers={"User-Agent": UA, "Accept": "*/*"})
|
|
389
|
+
except ValueError as e:
|
|
390
|
+
return None, 0, "", "", "URL 非法: %s" % e
|
|
391
|
+
try:
|
|
392
|
+
with _get_opener(True).open(req, timeout=timeout) as x:
|
|
393
|
+
size, _ = _size_from(x, x.headers)
|
|
394
|
+
return (x.status, size,
|
|
395
|
+
_abs_location(url, _hget(x.headers, "Location")),
|
|
396
|
+
_hget(x.headers, "Server"), None)
|
|
397
|
+
except _e.HTTPError as e:
|
|
398
|
+
try:
|
|
399
|
+
size, _ = _size_from(e, e.headers)
|
|
400
|
+
except Exception:
|
|
401
|
+
size = 0
|
|
402
|
+
loc = _abs_location(url, _hget(e.headers, "Location"))
|
|
403
|
+
srv = _hget(e.headers, "Server")
|
|
404
|
+
try:
|
|
405
|
+
e.close()
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
return e.code, size, loc, srv, None
|
|
228
409
|
except Exception as e:
|
|
229
|
-
|
|
230
|
-
if not req._warned:
|
|
231
|
-
print(f"[req 错误] {url}: {type(e).__name__}: {str(e)[:80]}",
|
|
232
|
-
file=sys.stderr)
|
|
233
|
-
req._warned = True
|
|
234
|
-
return None, 0, {}
|
|
410
|
+
return None, 0, "", "", "%s: %s" % (type(e).__name__, str(e)[:90])
|
|
235
411
|
|
|
236
412
|
|
|
237
|
-
|
|
413
|
+
def _probe_py_retry(url, timeout, retries):
|
|
414
|
+
"""带退避重试的 py 探测。"""
|
|
415
|
+
last_err = ""
|
|
416
|
+
for attempt in range(retries + 1):
|
|
417
|
+
code, size, loc, srv, err = _probe_py(url, timeout)
|
|
418
|
+
if code is not None:
|
|
419
|
+
return code, size, loc, srv, None
|
|
420
|
+
last_err = err or "未知错误"
|
|
421
|
+
if attempt < retries:
|
|
422
|
+
time.sleep(BACKOFF[min(attempt, len(BACKOFF) - 1)])
|
|
423
|
+
return None, 0, "", "", last_err
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# ---------------------------------------------------------------- curl 引擎
|
|
427
|
+
def curl_available():
|
|
428
|
+
return shutil.which("curl") is not None
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
_CURL_HDR_OK = None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _curl_supports_header():
|
|
435
|
+
"""%header{name} 需要 curl >= 7.84。"""
|
|
436
|
+
global _CURL_HDR_OK
|
|
437
|
+
if _CURL_HDR_OK is None:
|
|
438
|
+
_CURL_HDR_OK = False
|
|
439
|
+
try:
|
|
440
|
+
out = _run_curl(["curl", "--version"])
|
|
441
|
+
m = re.search(r"curl\s+(\d+)\.(\d+)", out)
|
|
442
|
+
if m:
|
|
443
|
+
_CURL_HDR_OK = (int(m.group(1)), int(m.group(2))) >= (7, 84)
|
|
444
|
+
except Exception:
|
|
445
|
+
pass
|
|
446
|
+
return _CURL_HDR_OK
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def curl_batch(urls, timeout, parallel, with_server=None):
|
|
450
|
+
"""curl -Z 批量探测。
|
|
451
|
+
|
|
452
|
+
返回 {url: (code, size, location, server)};code=0 表示该 URL 传输失败。
|
|
453
|
+
with_server=False 时不追加 %header{server}(老版本 curl),此时 server 恒为 ""。
|
|
454
|
+
"""
|
|
455
|
+
if not urls:
|
|
456
|
+
return {}
|
|
457
|
+
if with_server is None:
|
|
458
|
+
with_server = _curl_supports_header()
|
|
459
|
+
par = max(1, min(int(parallel), len(urls)))
|
|
460
|
+
fmt = "%{http_code} %{size_download} %{redirect_url} %{url_effective}"
|
|
461
|
+
if with_server:
|
|
462
|
+
fmt += " %header{server}"
|
|
463
|
+
fmt += "\n"
|
|
464
|
+
cargs = ["curl", "-sZ", "--parallel-max", str(par),
|
|
465
|
+
"--connect-timeout", "%g" % min(timeout, 5.0),
|
|
466
|
+
"-m", "%g" % timeout, "-w", fmt]
|
|
467
|
+
for u in urls:
|
|
468
|
+
cargs += ["-o", "/dev/null", u]
|
|
469
|
+
try:
|
|
470
|
+
out = _run_curl(cargs)
|
|
471
|
+
except OSError:
|
|
472
|
+
return {u: (0, 0, "", "") for u in urls}
|
|
473
|
+
res = {}
|
|
474
|
+
nfields = 5 if with_server else 4
|
|
475
|
+
for line in out.splitlines():
|
|
476
|
+
parts = line.split(" ", nfields - 1)
|
|
477
|
+
if len(parts) < 4:
|
|
478
|
+
continue
|
|
479
|
+
code = int(parts[0]) if parts[0].isdigit() else 0
|
|
480
|
+
size = int(parts[1]) if parts[1].isdigit() else 0
|
|
481
|
+
server = parts[4].strip() if (with_server and len(parts) > 4) else ""
|
|
482
|
+
# parts[3] 是 url_effective;极少数情况下 curl 会把它做归一化,做一次容错匹配
|
|
483
|
+
res[parts[3]] = (code, size, parts[2], server)
|
|
484
|
+
return res
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def curl_run(urls, timeout, batch, retries, on_result, on_progress=None):
|
|
488
|
+
"""分批发车 + 逐轮降并发重试。返回最终仍失败的 URL 列表。
|
|
489
|
+
|
|
490
|
+
每轮按 chunk 分期调用 curl(避免超长命令行撞 ARG_MAX),
|
|
491
|
+
chunk 内的并发由 --parallel-max 控制。
|
|
492
|
+
"""
|
|
493
|
+
pending = list(urls)
|
|
494
|
+
total = len(pending)
|
|
495
|
+
decided = 0
|
|
496
|
+
par = batch
|
|
497
|
+
chunk = max(batch, 200)
|
|
498
|
+
for attempt in range(retries + 1):
|
|
499
|
+
failed = []
|
|
500
|
+
for i in range(0, len(pending), chunk):
|
|
501
|
+
part = pending[i:i + chunk]
|
|
502
|
+
res = curl_batch(part, timeout, par)
|
|
503
|
+
for u in part:
|
|
504
|
+
got = res.get(u)
|
|
505
|
+
if got is None or got[0] == 0:
|
|
506
|
+
failed.append(u)
|
|
507
|
+
else:
|
|
508
|
+
decided += 1
|
|
509
|
+
on_result(u, got)
|
|
510
|
+
if on_progress:
|
|
511
|
+
on_progress(decided, total, len(failed), attempt)
|
|
512
|
+
if not failed or attempt >= retries:
|
|
513
|
+
return failed
|
|
514
|
+
time.sleep(BACKOFF[min(attempt, len(BACKOFF) - 1)])
|
|
515
|
+
par = max(2, min(8, par // 2))
|
|
516
|
+
print("# 重试第 %d 轮: %d 个失败 (并发降到 %d)"
|
|
517
|
+
% (attempt + 1, len(failed), min(par, len(failed))), file=sys.stderr)
|
|
518
|
+
pending = failed
|
|
519
|
+
return pending
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
# ---------------------------------------------------------------- 主程序
|
|
523
|
+
def _rand_path(n=12):
|
|
524
|
+
return "".join(random.choices(string.ascii_lowercase + string.digits, k=n))
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _parse_mc(ap, txt):
|
|
528
|
+
mc = set()
|
|
529
|
+
for x in txt.split(","):
|
|
530
|
+
x = x.strip()
|
|
531
|
+
if not x:
|
|
532
|
+
continue
|
|
533
|
+
if not x.isdigit():
|
|
534
|
+
ap.error("--mc 含非法状态码: %r" % x)
|
|
535
|
+
mc.add(int(x))
|
|
536
|
+
if not mc:
|
|
537
|
+
ap.error("--mc 不能为空")
|
|
538
|
+
return mc
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _load_words(ap, path):
|
|
542
|
+
if not path:
|
|
543
|
+
return list(DEFAULT)
|
|
544
|
+
try:
|
|
545
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
546
|
+
words = [l.strip() for l in f if l.strip()]
|
|
547
|
+
except OSError as e:
|
|
548
|
+
ap.error("读取字典失败 %s: %s" % (path, e))
|
|
549
|
+
return words
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def soft404_baseline(engine, base, timeout, retries):
|
|
553
|
+
"""随机路径软404基线探测,返回 (code, size);全部失败返回 None。"""
|
|
554
|
+
probes = ["%s/%s" % (base, _rand_path()) for _ in range(SOFT404_PROBES)]
|
|
555
|
+
cand = []
|
|
556
|
+
if engine == "curl":
|
|
557
|
+
res = curl_batch(probes, timeout, len(probes))
|
|
558
|
+
for u in probes:
|
|
559
|
+
got = res.get(u)
|
|
560
|
+
if got and got[0] != 0:
|
|
561
|
+
cand.append((got[0], got[1]))
|
|
562
|
+
else:
|
|
563
|
+
for u in probes:
|
|
564
|
+
code, size, _loc, _srv, err = _probe_py_retry(u, timeout, retries)
|
|
565
|
+
if code is not None:
|
|
566
|
+
cand.append((code, size))
|
|
567
|
+
if not cand:
|
|
568
|
+
return None
|
|
569
|
+
return Counter(cand).most_common(1)[0][0]
|
|
238
570
|
|
|
239
571
|
|
|
240
572
|
def main():
|
|
241
|
-
ap = argparse.ArgumentParser(description="
|
|
242
|
-
ap.add_argument("base")
|
|
573
|
+
ap = argparse.ArgumentParser(description="并发目录/文件批量爆破")
|
|
574
|
+
ap.add_argument("base", help="目标,如 https://target.com(缺协议按 http:// 处理)")
|
|
243
575
|
ap.add_argument("--wordlist", help="字典文件(一行一条)")
|
|
244
576
|
ap.add_argument("--ext", default="", help="附加扩展名,如 .php,.bak")
|
|
245
577
|
ap.add_argument("--mc", default="200,204,301,302,307,401,403,405,500",
|
|
246
578
|
help="关注的状态码")
|
|
247
|
-
ap.add_argument("--conc", type=int, default=20)
|
|
248
|
-
ap.add_argument("--delay", type=float, default=0.0, help="
|
|
579
|
+
ap.add_argument("--conc", type=int, default=20, help="py 引擎并发数(1-512)")
|
|
580
|
+
ap.add_argument("--delay", type=float, default=0.0, help="全局每请求最小间隔(秒)")
|
|
249
581
|
ap.add_argument("--timeout", type=float, default=8)
|
|
582
|
+
ap.add_argument("--engine", choices=["auto", "curl", "py"], default="auto",
|
|
583
|
+
help="curl=批量并行(快) py=线程池(精细/delay) auto=自动")
|
|
584
|
+
ap.add_argument("--batch", type=int, default=48, help="curl 引擎每批并行数(1-512)")
|
|
585
|
+
ap.add_argument("--retries", type=int, default=2,
|
|
586
|
+
help="瞬态失败(连接被拒/超时)重试次数,默认 2")
|
|
587
|
+
ap.add_argument("--no-soft404", action="store_true",
|
|
588
|
+
help="关闭软404基线过滤(默认开启,两引擎共用)")
|
|
589
|
+
ap.add_argument("--force", action="store_true", help="跳过预检(连接失败仍全量跑)")
|
|
250
590
|
a = ap.parse_args()
|
|
251
591
|
|
|
252
|
-
|
|
253
|
-
if a.
|
|
254
|
-
|
|
592
|
+
# ---- 参数校验 ----
|
|
593
|
+
if a.conc < 1:
|
|
594
|
+
ap.error("--conc 必须 >= 1")
|
|
595
|
+
if a.batch < 1:
|
|
596
|
+
ap.error("--batch 必须 >= 1")
|
|
597
|
+
if a.retries < 0:
|
|
598
|
+
ap.error("--retries 必须 >= 0")
|
|
599
|
+
if a.timeout <= 0:
|
|
600
|
+
ap.error("--timeout 必须 > 0")
|
|
601
|
+
if a.delay < 0:
|
|
602
|
+
ap.error("--delay 必须 >= 0")
|
|
603
|
+
if a.conc > 512:
|
|
604
|
+
a.conc = 512
|
|
605
|
+
if a.batch > 512:
|
|
606
|
+
a.batch = 512
|
|
255
607
|
|
|
256
|
-
|
|
257
|
-
|
|
608
|
+
base = a.base.strip()
|
|
609
|
+
if not base:
|
|
610
|
+
ap.error("目标不能为空")
|
|
611
|
+
if not re.match(r"^[A-Za-z][A-Za-z0-9+.\-]*://", base):
|
|
612
|
+
base = "http://" + base
|
|
613
|
+
print("# 目标未带协议,按 %s 处理" % base, file=sys.stderr)
|
|
614
|
+
base = base.rstrip("/")
|
|
615
|
+
|
|
616
|
+
mc = _parse_mc(ap, a.mc)
|
|
617
|
+
words = _load_words(ap, a.wordlist)
|
|
618
|
+
exts = ([""] + [e.strip() for e in a.ext.split(",") if e.strip()]) if a.ext else [""]
|
|
619
|
+
paths, seen = [], set()
|
|
258
620
|
for w in words:
|
|
259
621
|
for e in exts:
|
|
260
|
-
|
|
622
|
+
p = (w + e).lstrip("/")
|
|
623
|
+
if p and p not in seen:
|
|
624
|
+
seen.add(p)
|
|
625
|
+
paths.append(p)
|
|
626
|
+
if not paths:
|
|
627
|
+
ap.error("字典为空,无探测路径")
|
|
628
|
+
urls = ["%s/%s" % (base, p) for p in paths]
|
|
261
629
|
|
|
262
|
-
|
|
263
|
-
|
|
630
|
+
engine = a.engine
|
|
631
|
+
if engine == "auto":
|
|
632
|
+
engine = "py" if (a.delay and a.delay > 0) or not curl_available() else "curl"
|
|
633
|
+
if engine == "curl" and a.delay and a.delay > 0:
|
|
634
|
+
print("# --delay>0 时 curl 批量引擎无法逐请求限速,自动改用 py 引擎", file=sys.stderr)
|
|
635
|
+
engine = "py"
|
|
636
|
+
if engine == "curl" and not curl_available():
|
|
637
|
+
print("# 未找到 curl,自动改用 py 引擎", file=sys.stderr)
|
|
638
|
+
engine = "py"
|
|
639
|
+
if a.conc > 200:
|
|
640
|
+
print("# 警告: --conc %d 过高,本地/目标可能大量连接被拒(会触发重试)" % a.conc,
|
|
641
|
+
file=sys.stderr)
|
|
264
642
|
|
|
265
|
-
|
|
643
|
+
soft = not a.no_soft404
|
|
644
|
+
_purl = "%s/%s" % (base, _rand_path())
|
|
645
|
+
if engine == "curl":
|
|
646
|
+
_pg = curl_batch([_purl], min(a.timeout, 6.0), 1).get(_purl)
|
|
647
|
+
_pcode = _pg[0] if _pg else 0
|
|
648
|
+
else:
|
|
649
|
+
_pc = _probe_py(_purl, min(a.timeout, 6.0))[0]
|
|
650
|
+
_pcode = _pc if _pc is not None else 0
|
|
651
|
+
if _pcode == 0 and not a.force:
|
|
652
|
+
print("# [预检失败] %s 不可达,已跳过全量。可达请加 --force。" % base, file=sys.stderr)
|
|
653
|
+
sys.exit(3)
|
|
654
|
+
baseline = None
|
|
655
|
+
if soft:
|
|
656
|
+
baseline = soft404_baseline(engine, base, a.timeout, a.retries)
|
|
657
|
+
if baseline is None:
|
|
658
|
+
soft = False
|
|
659
|
+
print("# 软404基线探测全部失败,已自动关闭软404过滤", file=sys.stderr)
|
|
660
|
+
else:
|
|
661
|
+
print("# 软404基线: %d / %dB (随机路径 x%d)" % (baseline[0], baseline[1], SOFT404_PROBES),
|
|
662
|
+
file=sys.stderr)
|
|
266
663
|
|
|
267
|
-
|
|
664
|
+
def is_soft(code, size):
|
|
665
|
+
return bool(soft and baseline and code == baseline[0]
|
|
666
|
+
and abs(size - baseline[1]) <= SOFT404_TOL)
|
|
268
667
|
|
|
269
|
-
def
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
668
|
+
def emit(url, code, size, loc, srv):
|
|
669
|
+
r = {"url": url, "status": code, "len": size,
|
|
670
|
+
"location": loc or "", "server": srv or "", "engine": engine}
|
|
671
|
+
print(json.dumps(r, ensure_ascii=False))
|
|
672
|
+
return r
|
|
673
|
+
|
|
674
|
+
hits, errors = [], []
|
|
675
|
+
t0 = time.time()
|
|
676
|
+
|
|
677
|
+
if engine == "curl":
|
|
678
|
+
print("# %s 共 %d 条 (curl 引擎, 批 %d, 软404%s)"
|
|
679
|
+
% (base, len(paths), min(a.batch, len(paths)),
|
|
680
|
+
"开启" if soft else "关闭"), file=sys.stderr)
|
|
681
|
+
|
|
682
|
+
def on_result(url, got):
|
|
683
|
+
code, size, loc, srv = got
|
|
684
|
+
if code in mc and not is_soft(code, size):
|
|
685
|
+
hits.append(emit(url, code, size, loc, srv))
|
|
686
|
+
|
|
687
|
+
def on_progress(done_n, total_n, waiting, attempt):
|
|
688
|
+
print(" ... 已探测 %d/%d 命中 %d 待重试 %d%s"
|
|
689
|
+
% (done_n, total_n, len(hits), waiting,
|
|
690
|
+
"" if attempt == 0 else " [第 %d 轮]" % (attempt + 1)),
|
|
691
|
+
file=sys.stderr)
|
|
692
|
+
|
|
693
|
+
failed = curl_run(urls, a.timeout, a.batch, a.retries, on_result, on_progress)
|
|
694
|
+
errors.extend([(u, "传输失败(重试后仍失败)") for u in failed])
|
|
695
|
+
for u, e in errors[:5]:
|
|
696
|
+
print("# 失败: %s (%s)" % (u, e), file=sys.stderr)
|
|
697
|
+
else:
|
|
698
|
+
print("# %s 共 %d 条 (py 引擎, 并发 %d, delay %gs, 软404%s)"
|
|
699
|
+
% (base, len(paths), a.conc, a.delay, "开启" if soft else "关闭"),
|
|
700
|
+
file=sys.stderr)
|
|
701
|
+
limiter = RateLimiter(a.delay)
|
|
702
|
+
done = [0]
|
|
703
|
+
|
|
704
|
+
def one(url):
|
|
705
|
+
limiter.wait()
|
|
706
|
+
return url, _probe_py_retry(url, a.timeout, a.retries)
|
|
707
|
+
|
|
708
|
+
with _get_cf().ThreadPoolExecutor(max_workers=a.conc) as ex:
|
|
709
|
+
futs = {ex.submit(one, u): u for u in urls}
|
|
710
|
+
for fu in _get_cf().as_completed(futs):
|
|
711
|
+
done[0] += 1
|
|
712
|
+
try:
|
|
713
|
+
url, (code, size, loc, srv, err) = fu.result()
|
|
714
|
+
except Exception as e:
|
|
715
|
+
url, code, size, loc, srv = futs[fu], None, 0, "", ""
|
|
716
|
+
err = "%s: %s" % (type(e).__name__, e)
|
|
717
|
+
if code is None:
|
|
718
|
+
errors.append((url, err or "未知错误"))
|
|
719
|
+
elif code in mc and not is_soft(code, size):
|
|
720
|
+
hits.append(emit(url, code, size, loc, srv))
|
|
721
|
+
if done[0] % 200 == 0 or done[0] == len(urls):
|
|
722
|
+
print(" ... %d/%d 命中 %d 错误 %d"
|
|
723
|
+
% (done[0], len(urls), len(hits), len(errors)), file=sys.stderr)
|
|
724
|
+
for u, e in errors[:5]:
|
|
725
|
+
print("# 失败: %s (%s)" % (u, e), file=sys.stderr)
|
|
279
726
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
for fu in as_completed(futs):
|
|
284
|
-
done += 1
|
|
285
|
-
try:
|
|
286
|
-
r = fu.result()
|
|
287
|
-
except Exception:
|
|
288
|
-
r = None
|
|
289
|
-
if r:
|
|
290
|
-
hits.append(r)
|
|
291
|
-
print(json.dumps(r, ensure_ascii=False))
|
|
292
|
-
if done % 200 == 0:
|
|
293
|
-
print(f" ... {done}/{len(paths)} 命中 {len(hits)}", file=sys.stderr)
|
|
294
|
-
|
|
295
|
-
print(f"# 完成: 命中 {len(hits)}", file=sys.stderr)
|
|
727
|
+
dt = time.time() - t0
|
|
728
|
+
print("# 完成: 命中 %d, 错误 %d, 共 %d 条, 耗时 %.2fs (%s 引擎)"
|
|
729
|
+
% (len(hits), len(errors), len(urls), dt, engine), file=sys.stderr)
|
|
296
730
|
|
|
297
731
|
|
|
298
732
|
if __name__ == "__main__":
|