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.
- package/AGENT_ADDITIVE_CHANGELOG.md +21 -1
- package/README.md +5 -3
- package/SKILL.md +13 -8
- package/dsh/persona.md +1 -1
- package/package.json +1 -1
- package/references/fake-ip-tun.md +1 -0
- package/references/multi-path-attack.md +100 -0
- package/references/search-engine-pivot.md +109 -0
- package/references/tool-selection.md +4 -0
- package/scripts/env_chain.py +337 -0
- package/scripts/origin_trace.py +191 -25
- package/scripts/pivot_queries.py +321 -0
- package/tests/test_regressions.py +155 -0
|
@@ -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()
|