agent-bypass 2.17.0 → 2.20.0

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.
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """pivot_queries — 资产扩散/同 IP 旁站查询生成器(FOFA / Shodan / Quake / ZoomEye / Hunter / Censys / Netlas)
4
+
5
+ 场景:主站打不动(CDN/WAF/无洞/加固严),用测绘引擎按「同 IP、C 段、同证书、同 favicon、
6
+ 同 body、同 title、同 ASN、同端口/指纹」反查同簇资产,找同系统/同模板的兄弟站,先打最弱一环,
7
+ 再凭共享凭据/共享 DB/共享存储/反代配置回打主站。
8
+
9
+ 本工具只生成查询与扫描命令模板,不联网(favicon 若给 URL 才下载一次);只读。
10
+ 先确认关系再打:同 IP 不等于同资产(CDN 共享 IP 会误伤无关站点),必须多维交叉 + 授权范围内。
11
+
12
+ 用法:
13
+ python3 pivot_queries.py --domain example.com --ip 1.2.3.4
14
+ python3 pivot_queries.py --domain example.com --favicon https://example.com/favicon.ico
15
+ python3 pivot_queries.py --domain example.com --cert example.com --asn AS13335 --json
16
+ python3 pivot_queries.py --domain example.com --title "Example" --body "unique-snippet"
17
+ """
18
+ import base64
19
+ import hashlib
20
+ import json
21
+ import os
22
+ import re
23
+ import sys
24
+ import urllib.request
25
+
26
+ try:
27
+ import argsmini as argparse
28
+ except Exception: # 直接运行/单测环境兜底
29
+ import argparse # type: ignore
30
+
31
+ UA = "Mozilla/5.0 (compatible; pivot-queries/1.0)"
32
+ ENGINES = ("fofa", "shodan", "quake", "zoomeye", "hunter", "censys", "netlas")
33
+
34
+
35
+ # ---------------------------------------------------------------- 哈希
36
+
37
+ def murmur3_32(data, seed=0):
38
+ """MurmurHash3 x86 32-bit(与 mmh3.hash 一致,返回 signed int)。"""
39
+ data = bytes(data)
40
+ c1, c2 = 0xCC9E2D51, 0x1B873593
41
+ length = len(data)
42
+ h1 = seed
43
+ end = length - (length % 4)
44
+ for i in range(0, end, 4):
45
+ k1 = int.from_bytes(data[i:i + 4], "little")
46
+ k1 = (k1 * c1) & 0xFFFFFFFF
47
+ k1 = ((k1 << 15) | (k1 >> 17)) & 0xFFFFFFFF
48
+ k1 = (k1 * c2) & 0xFFFFFFFF
49
+ h1 ^= k1
50
+ h1 = ((h1 << 13) | (h1 >> 19)) & 0xFFFFFFFF
51
+ h1 = (h1 * 5 + 0xE6546B64) & 0xFFFFFFFF
52
+ tail = data[end:]
53
+ k1 = 0
54
+ if len(tail) >= 3:
55
+ k1 ^= tail[2] << 16
56
+ if len(tail) >= 2:
57
+ k1 ^= tail[1] << 8
58
+ if len(tail) >= 1:
59
+ k1 ^= tail[0]
60
+ k1 = (k1 * c1) & 0xFFFFFFFF
61
+ k1 = ((k1 << 15) | (k1 >> 17)) & 0xFFFFFFFF
62
+ k1 = (k1 * c2) & 0xFFFFFFFF
63
+ h1 ^= k1
64
+ h1 ^= length
65
+ h1 ^= h1 >> 16
66
+ h1 = (h1 * 0x85EBCA6B) & 0xFFFFFFFF
67
+ h1 ^= h1 >> 13
68
+ h1 = (h1 * 0xC2B2AE35) & 0xFFFFFFFF
69
+ h1 ^= h1 >> 16
70
+ return h1 - 0x100000000 if h1 >= 0x80000000 else h1
71
+
72
+
73
+ def favicon_hashes(data):
74
+ """favicon → (FOFA icon_hash, 备用 raw mmh3)。
75
+
76
+ FOFA/Shodan 主流口径:mmh3(base64(favicon)) 的 signed 32-bit;
77
+ 部分实现/新版引擎可能用原始字节,故同时给出 raw 值备用。
78
+ """
79
+ b64 = base64.b64encode(bytes(data))
80
+ return murmur3_32(b64), murmur3_32(bytes(data))
81
+
82
+
83
+ def fetch_favicon(url, timeout=8.0):
84
+ req = urllib.request.Request(url, headers={"user-agent": UA})
85
+ with urllib.request.urlopen(req, timeout=timeout) as r:
86
+ return r.read(1 << 20)
87
+
88
+
89
+ def favicon_from_arg(arg):
90
+ if re.match(r"^https?://", arg or "", re.I):
91
+ return fetch_favicon(arg)
92
+ with open(arg, "rb") as f:
93
+ return f.read(1 << 20)
94
+
95
+
96
+ # ---------------------------------------------------------------- 查询生成
97
+
98
+ def build_queries(info):
99
+ """按输入生成各引擎查询(列表),返回 {engine: [query...]}。"""
100
+ ip = (info.get("ip") or "").strip()
101
+ cidr = (info.get("cidr") or "").strip()
102
+ domain = (info.get("domain") or "").strip()
103
+ cert = (info.get("cert") or "").strip()
104
+ title = (info.get("title") or "").strip()
105
+ body = (info.get("body") or "").strip()
106
+ asn = (info.get("asn") or "").strip().upper().replace("AS", "")
107
+ port = str(info.get("port") or "").strip()
108
+ icon = info.get("icon_hash")
109
+ icon_raw = info.get("icon_hash_raw")
110
+ out = {}
111
+
112
+ fofa = []
113
+ if domain:
114
+ fofa.append('domain="%s"' % domain)
115
+ if ip:
116
+ fofa.append('ip="%s"' % ip)
117
+ if cidr:
118
+ fofa.append('ip="%s"' % cidr)
119
+ if cert:
120
+ fofa.append('cert="%s"' % cert)
121
+ if icon is not None:
122
+ fofa.append('icon_hash="%s"' % icon)
123
+ if body:
124
+ fofa.append('body="%s"' % body)
125
+ if title:
126
+ fofa.append('title="%s"' % title)
127
+ if asn:
128
+ fofa.append('asn="%s"' % asn)
129
+ if port:
130
+ fofa.append('port="%s"' % port)
131
+ out["fofa"] = fofa
132
+
133
+ shodan = []
134
+ if ip:
135
+ shodan.append("ip:%s" % ip)
136
+ if cidr:
137
+ shodan.append("net:%s" % cidr)
138
+ if domain:
139
+ shodan.append("hostname:%s" % domain)
140
+ if cert:
141
+ shodan.append('ssl.cert.subject.cn:"%s"' % cert)
142
+ if icon is not None:
143
+ shodan.append("http.favicon.hash:%s" % icon)
144
+ if title:
145
+ shodan.append('http.title:"%s"' % title)
146
+ if body:
147
+ shodan.append('http.html:"%s"' % body)
148
+ if asn:
149
+ shodan.append("asn:AS%s" % asn)
150
+ if port:
151
+ shodan.append("port:%s" % port)
152
+ out["shodan"] = shodan
153
+
154
+ quake = []
155
+ if ip:
156
+ quake.append('ip:"%s"' % ip)
157
+ if cidr:
158
+ quake.append('ip:"%s"' % cidr)
159
+ if domain:
160
+ quake.append('domain:"%s"' % domain)
161
+ if cert:
162
+ quake.append('cert:"%s"' % cert)
163
+ if title:
164
+ quake.append('title:"%s"' % title)
165
+ if body:
166
+ quake.append('body:"%s"' % body)
167
+ if icon is not None:
168
+ quake.append('favicon:"%s"' % icon)
169
+ if port:
170
+ quake.append('port:"%s"' % port)
171
+ out["quake"] = quake
172
+
173
+ zoomeye = []
174
+ if ip:
175
+ zoomeye.append("ip:%s" % ip)
176
+ if cidr:
177
+ zoomeye.append("cidr:%s" % cidr)
178
+ if cert:
179
+ zoomeye.append("ssl.cert:%s" % cert)
180
+ if title:
181
+ zoomeye.append("title:%s" % title)
182
+ if icon is not None:
183
+ zoomeye.append("iconhash:%s" % icon)
184
+ if port:
185
+ zoomeye.append("port:%s" % port)
186
+ out["zoomeye"] = zoomeye
187
+
188
+ hunter = []
189
+ if ip:
190
+ hunter.append('ip="%s"' % ip)
191
+ if cidr:
192
+ hunter.append('ip="%s"' % cidr)
193
+ if domain:
194
+ hunter.append('domain="%s"' % domain)
195
+ if cert:
196
+ hunter.append('cert="%s"' % cert)
197
+ if title:
198
+ hunter.append('web.title="%s"' % title)
199
+ if body:
200
+ hunter.append('web.body="%s"' % body)
201
+ if port:
202
+ hunter.append('port="%s"' % port)
203
+ out["hunter"] = hunter
204
+
205
+ censys = []
206
+ if ip:
207
+ censys.append("ip: %s" % ip)
208
+ if cidr:
209
+ censys.append("ip: %s" % cidr)
210
+ if asn:
211
+ censys.append("autonomous_system.asn: %s" % asn)
212
+ if cert:
213
+ censys.append('services.tls.certificates.leaf_data.subject.common_name: "%s"' % cert)
214
+ if title:
215
+ censys.append('services.http.response.html_title: "%s"' % title)
216
+ out["censys"] = censys
217
+
218
+ netlas = []
219
+ if ip:
220
+ netlas.append("ip:%s" % ip)
221
+ if domain:
222
+ netlas.append("domain:%s" % domain)
223
+ if cert:
224
+ netlas.append("cert:%s" % cert)
225
+ if icon is not None:
226
+ netlas.append("favicon.hash:%s" % icon)
227
+ out["netlas"] = netlas
228
+
229
+ return out
230
+
231
+
232
+ def build_scan_templates(info):
233
+ """后续扫描/验证命令模板(只读,需操作员自行确认授权)。"""
234
+ ip = (info.get("ip") or "<IP>").strip()
235
+ domain = (info.get("domain") or "<DOMAIN>").strip()
236
+ port = str(info.get("port") or "").strip()
237
+ pflag = port if port else "-"
238
+ return [
239
+ "# 端口与服务(-n 禁解析,避免 fake-IP;-Pn 跳主机发现)",
240
+ "nmap -n -Pn -sV -p- --min-rate 2000 %s" % ip,
241
+ "",
242
+ "# 同 IP 多 vhost 枚举(Host 头换绑,对比状态码/长度/标题)",
243
+ "for h in $(cat subs.txt); do "
244
+ "curl -sk -o /dev/null -w \"%%{http_code} %%{size_download} $h\\n\" -H \"Host: $h\" http://%s/; done" % ip,
245
+ "",
246
+ "# 指定端口 + SNI 验证(绕过 CDN/fake-IP 直接打真实 IP)",
247
+ "curl -sk --resolve %s:%s:%s https://%s/" % (domain, pflag, ip, domain),
248
+ "",
249
+ "# 指纹/CVE/面板/默认口令(按指纹选模板;先 -severity 降噪)",
250
+ "nuclei -l urls.txt -tags cve -severity critical,high -stats",
251
+ "nuclei -u https://%s -tags exposed-panels,default-logins,tech" % domain,
252
+ "",
253
+ "# 同簇扩散:把命中的兄弟站加入 urls.txt,凭据/接口交叉复用后回打主站",
254
+ ]
255
+
256
+
257
+ # ---------------------------------------------------------------- 入口
258
+
259
+ def collect_info(a):
260
+ info = {
261
+ "domain": getattr(a, "domain", None),
262
+ "ip": getattr(a, "ip", None),
263
+ "cidr": getattr(a, "cidr", None),
264
+ "cert": getattr(a, "cert", None),
265
+ "title": getattr(a, "title", None),
266
+ "body": getattr(a, "body", None),
267
+ "asn": getattr(a, "asn", None),
268
+ "port": getattr(a, "port", None),
269
+ }
270
+ if getattr(a, "favicon", None):
271
+ data = favicon_from_arg(a.favicon)
272
+ h_b64, h_raw = favicon_hashes(data)
273
+ info["icon_hash"] = h_b64
274
+ info["icon_hash_raw"] = h_raw
275
+ info["icon_md5"] = hashlib.md5(data).hexdigest()
276
+ return info
277
+
278
+
279
+ def main(argv=None):
280
+ ap = argparse.ArgumentParser(description="资产扩散/同 IP 旁站查询生成器(离线,只读)")
281
+ ap.add_argument("--domain")
282
+ ap.add_argument("--ip")
283
+ ap.add_argument("--cidr")
284
+ ap.add_argument("--cert")
285
+ ap.add_argument("--title")
286
+ ap.add_argument("--body")
287
+ ap.add_argument("--asn", help="AS 号,可带或不带 AS 前缀")
288
+ ap.add_argument("--port", type=int)
289
+ ap.add_argument("--favicon", help="favicon URL 或本地文件路径(计算 icon_hash)")
290
+ ap.add_argument("--json", action="store_true")
291
+ a = ap.parse_args(argv)
292
+
293
+ if not any([a.domain, a.ip, a.cidr, a.cert, a.title, a.body, a.asn, a.favicon]):
294
+ print("[错误] 至少给一个输入:--domain/--ip/--cidr/--cert/--title/--body/--asn/--favicon", file=sys.stderr)
295
+ return 2
296
+ info = collect_info(a)
297
+ queries = build_queries(info)
298
+ templates = build_scan_templates(info)
299
+ payload = {"input": info, "queries": queries, "scan_templates": templates}
300
+
301
+ if a.json:
302
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
303
+ return 0
304
+
305
+ print("# 输入: " + ", ".join("%s=%s" % (k, v) for k, v in info.items() if v not in (None, "")))
306
+ for engine in ENGINES:
307
+ qs = [q for q in queries.get(engine, []) if q]
308
+ if not qs:
309
+ continue
310
+ print("\n## %s" % engine.upper())
311
+ for q in qs:
312
+ print(q)
313
+ print("\n## 后续扫描/验证模板")
314
+ for line in templates:
315
+ print(line)
316
+ print("\n# 先多维交叉确认「同簇/同源」再打;CDN 共享 IP 会误伤无关站点;只在授权范围内扩散。")
317
+ return 0
318
+
319
+
320
+ if __name__ == "__main__":
321
+ sys.exit(main())
@@ -355,10 +355,165 @@ class HangProtectionTests(unittest.TestCase):
355
355
  self.assertIn("same-cdn", r_cdn)
356
356
  self.assertLess(s_cdn, s_org)
357
357
 
358
+ def test_passive_ct_parsers(self):
359
+ cs = '[{"dns_names":["www.example.com","*.example.com","x.test"]}]'
360
+ self.assertEqual(self.ot.parse_certspotter(cs, "example.com"), ["www.example.com", "example.com"])
361
+ otx = ('{"passive_dns":[{"hostname":"mail.example.com","address":"1.2.3.4"},'
362
+ '{"hostname":"x.test","address":"9.9.9.9"},'
363
+ '{"hostname":"cdn.example.com","address":"198.18.0.9"}]}')
364
+ hosts, ips = self.ot.parse_otx(otx, "example.com")
365
+ self.assertIn("mail.example.com", hosts)
366
+ self.assertEqual(ips, ["1.2.3.4"], "被动 DNS 的 fake-IP 应被过滤")
367
+ hosts, ips = self.ot.parse_hackertarget("vpn.example.com,5.6.7.8\nAPI count exceeded", "example.com")
368
+ self.assertEqual(hosts, ["vpn.example.com"])
369
+ self.assertEqual(ips, ["5.6.7.8"])
370
+ self.assertEqual(self.ot.parse_anubis('["a.example.com","b.test"]', "example.com"), ["a.example.com"])
371
+ self.assertEqual(self.ot.parse_rapiddns("<td>oa.example.com</td><td>x.test</td>", "example.com"),
372
+ ["oa.example.com"])
373
+ for src in ("crt.sh", "certspotter", "otx", "hackertarget", "anubis", "rapiddns"):
374
+ self.assertIn(src, self.ot.CT_SOURCES)
375
+
376
+ def test_ct_multi_source_fallback(self):
377
+ def fake(name, domain, timeout):
378
+ if name == "crt.sh":
379
+ raise RuntimeError("502 Bad Gateway")
380
+ return (["s1.example.com", "s2.example.com"], ["1.2.3.4"])
381
+ orig = self.ot._ct_fetch
382
+ self.ot._ct_fetch = fake
383
+ try:
384
+ hosts, ips, status = self.ot.fetch_passive_names(
385
+ "example.com", sources=["crt.sh", "otx"], budget=5)
386
+ finally:
387
+ self.ot._ct_fetch = orig
388
+ self.assertEqual(hosts, ["s1.example.com", "s2.example.com"])
389
+ self.assertEqual(ips, ["1.2.3.4"])
390
+ self.assertIn("失败", status["crt.sh"], "单源 502 应被记录并跳过")
391
+
358
392
  def test_origin_bad_flag_rejected(self):
359
393
  r = _tmp_run([sys.executable, str(SCRIPTS / "origin_trace.py"), "--bogus"])
360
394
  self.assertEqual(r.returncode, 2)
361
395
 
362
396
 
397
+ class PivotQueryTests(unittest.TestCase):
398
+ """搜索引警扩面:murmur3/favicon hash、7 引擎查询生成、CLI JSON(离线)。"""
399
+
400
+ @classmethod
401
+ def setUpClass(cls):
402
+ sys.path.insert(0, str(SCRIPTS))
403
+ import pivot_queries
404
+ cls.pq = pivot_queries
405
+
406
+ def test_murmur3_known_vectors(self):
407
+ self.assertEqual(self.pq.murmur3_32(b""), 0)
408
+ self.assertEqual(self.pq.murmur3_32(b"foo"), -156908512)
409
+ self.assertEqual(self.pq.murmur3_32(b"hello"), 613153351)
410
+
411
+ def test_favicon_hash_wiring(self):
412
+ import base64
413
+ data = b"\x89PNG\r\n\x1a\nfake-favicon-bytes"
414
+ hb, hr = self.pq.favicon_hashes(data)
415
+ self.assertEqual(hb, self.pq.murmur3_32(base64.b64encode(data)))
416
+ self.assertEqual(hr, self.pq.murmur3_32(data))
417
+
418
+ def test_build_queries_across_engines(self):
419
+ info = {"domain": "t.com", "ip": "1.2.3.4", "cidr": "1.2.3.0/24", "cert": "t.com",
420
+ "title": "Admin", "body": "uniq", "asn": "AS13335", "port": 8443, "icon_hash": -123}
421
+ q = self.pq.build_queries(info)
422
+ for eng in ("fofa", "shodan", "quake", "zoomeye", "hunter", "censys", "netlas"):
423
+ self.assertIn(eng, q)
424
+ self.assertIn('ip="1.2.3.4"', q["fofa"])
425
+ self.assertIn('icon_hash="-123"', q["fofa"])
426
+ self.assertIn("http.favicon.hash:-123", q["shodan"])
427
+ self.assertIn("net:1.2.3.0/24", q["shodan"])
428
+ self.assertIn('cert:"t.com"', q["quake"])
429
+ self.assertIn("cidr:1.2.3.0/24", q["zoomeye"])
430
+ self.assertIn('web.title="Admin"', q["hunter"])
431
+
432
+ def test_cli_json_and_favicon_file(self):
433
+ with tempfile.TemporaryDirectory() as d:
434
+ fav = Path(d) / "favicon.ico"
435
+ data = b"\x00\x01\x02fake"
436
+ fav.write_bytes(data)
437
+ r = _tmp_run([sys.executable, str(SCRIPTS / "pivot_queries.py"),
438
+ "--domain", "t.com", "--ip", "1.2.3.4",
439
+ "--favicon", str(fav), "--json"])
440
+ self.assertEqual(r.returncode, 0, r.stderr)
441
+ payload = json.loads(r.stdout)
442
+ self.assertEqual(payload["input"]["icon_hash"], self.pq.favicon_hashes(data)[0])
443
+ self.assertTrue(payload["queries"]["fofa"])
444
+ self.assertTrue(payload["scan_templates"])
445
+
446
+ def test_cli_rejects_empty_input(self):
447
+ r = _tmp_run([sys.executable, str(SCRIPTS / "pivot_queries.py")])
448
+ self.assertEqual(r.returncode, 2)
449
+
450
+
451
+ class EnvChainTests(unittest.TestCase):
452
+ """多路径攻击:.env 解析/分类/脱敏/链生成(离线)。"""
453
+
454
+ SAMPLE = "\n".join([
455
+ "APP_ENV=production",
456
+ "DB_HOST=10.0.0.5",
457
+ "DB_PORT=3306",
458
+ "DB_USER=appuser",
459
+ "DB_PASSWORD=SuperSecret1",
460
+ "REDIS_HOST=10.0.0.6",
461
+ "REDIS_PASSWORD=redispass",
462
+ "AWS_ACCESS_KEY_ID=AKIAFAKE123",
463
+ "AWS_SECRET_ACCESS_KEY=fakesecret",
464
+ "JWT_SECRET=jwtsecret123",
465
+ "SMTP_HOST=smtp.example.com",
466
+ "SSH_USER=deploy",
467
+ ]) + "\n"
468
+
469
+ @classmethod
470
+ def setUpClass(cls):
471
+ sys.path.insert(0, str(SCRIPTS))
472
+ import env_chain
473
+ cls.ec = env_chain
474
+
475
+ def test_parse_env_and_classify(self):
476
+ env = self.ec.parse_env('export DB_HOST=1.2.3.4\nDB_USER="u"\n# comment\nBADLINE\n')
477
+ self.assertEqual(env["DB_HOST"], "1.2.3.4")
478
+ self.assertEqual(env["DB_USER"], "u")
479
+ self.assertEqual(self.ec.classify("DB_PASSWORD"), "mysql")
480
+ self.assertEqual(self.ec.classify("REDIS_URL"), "db_url")
481
+ self.assertEqual(self.ec.classify("AWS_SECRET_ACCESS_KEY"), "aws")
482
+ self.assertEqual(self.ec.classify("JWT_SECRET"), "secret")
483
+ self.assertEqual(self.ec.classify("SMTP_PASSWORD"), "smtp")
484
+ self.assertEqual(self.ec.classify("SSH_PRIVATE_KEY"), "ssh")
485
+
486
+ def test_extract_and_chains_masking(self):
487
+ env = self.ec.parse_env(self.SAMPLE)
488
+ data = self.ec.extract(env)
489
+ services = {s["service"] for s in data["services"]}
490
+ self.assertIn("mysql", services)
491
+ self.assertIn("redis", services)
492
+ chains = self.ec.build_chains(data)
493
+ text = json.dumps(chains, ensure_ascii=False)
494
+ self.assertNotIn("SuperSecret1", text, "默认必须脱敏")
495
+ self.assertNotIn("redispass", text)
496
+ revealed = json.dumps(self.ec.build_chains(data, reveal=True), ensure_ascii=False)
497
+ self.assertIn("SuperSecret1", revealed)
498
+ self.assertIn(".env → MySQL", revealed)
499
+
500
+ def test_cli_json_roundtrip(self):
501
+ with tempfile.TemporaryDirectory() as d:
502
+ f = Path(d) / ".env"
503
+ f.write_text(self.SAMPLE, encoding="utf-8")
504
+ r = _tmp_run([sys.executable, str(SCRIPTS / "env_chain.py"), str(f), "--json"])
505
+ self.assertEqual(r.returncode, 0, r.stderr)
506
+ payload = json.loads(r.stdout)
507
+ self.assertIn("chains", payload)
508
+ self.assertNotIn("SuperSecret1", r.stdout)
509
+ r2 = _tmp_run([sys.executable, str(SCRIPTS / "env_chain.py"), str(f), "--json", "--reveal"])
510
+ self.assertEqual(r2.returncode, 0, r2.stderr)
511
+ self.assertIn("SuperSecret1", r2.stdout)
512
+ empty = Path(d) / "empty.txt"
513
+ empty.write_text("# nothing\n", encoding="utf-8")
514
+ r3 = _tmp_run([sys.executable, str(SCRIPTS / "env_chain.py"), str(empty)])
515
+ self.assertEqual(r3.returncode, 2)
516
+
517
+
363
518
  if __name__ == "__main__":
364
519
  unittest.main()