Compare commits
50 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ac68df70f7 | ||
![]() |
76cbdfb5f6 | ||
![]() |
c4378545b9 | ||
![]() |
5aa2fd3599 | ||
![]() |
faa9f319c8 | ||
![]() |
25dc6102ed | ||
![]() |
0dc4a6c360 | ||
![]() |
2b4a4024b8 | ||
![]() |
e58a48ba9b | ||
![]() |
a8bd8ca508 | ||
![]() |
247459a7f7 | ||
![]() |
424c654c43 | ||
![]() |
7865038b22 | ||
![]() |
64588ab28a | ||
![]() |
2d9ea9c1d3 | ||
![]() |
a30cd12249 | ||
![]() |
c074adb3a9 | ||
![]() |
f2475bf97c | ||
![]() |
580b067298 | ||
![]() |
a010fcbb6c | ||
![]() |
1f0d11d93e | ||
![]() |
a3c5092f9b | ||
![]() |
16e40fe7ed | ||
![]() |
f921d81a76 | ||
![]() |
a1452eb635 | ||
![]() |
e4833fd5af | ||
![]() |
9092b09b16 | ||
![]() |
d90deb0201 | ||
![]() |
d1d242e6a8 | ||
![]() |
28a64d60c4 | ||
![]() |
124d29a6b3 | ||
![]() |
4928b4668a | ||
![]() |
5dfd0397d5 | ||
![]() |
805af82a1e | ||
![]() |
875d128e53 | ||
![]() |
36134b7298 | ||
![]() |
be3affcedd | ||
![]() |
165ac8507d | ||
![]() |
7da74ebb52 | ||
![]() |
a8b83f90a0 | ||
![]() |
cb6d67ed7b | ||
![]() |
5c8088ff32 | ||
![]() |
8170515236 | ||
![]() |
f27d9b31aa | ||
![]() |
d05641a7fc | ||
![]() |
4aaa05f6a4 | ||
![]() |
42f8052b96 | ||
![]() |
3ae0f306c1 | ||
![]() |
cc9d292bdd | ||
![]() |
b8a591920b |
6
.github/conf/.goreleaser.yml
vendored
@ -17,9 +17,9 @@ builds:
|
||||
- "386"
|
||||
- arm
|
||||
- arm64
|
||||
- mips
|
||||
- mipsle
|
||||
- mips64
|
||||
# - mips
|
||||
# - mipsle
|
||||
# - mips64
|
||||
goarm:
|
||||
- "6"
|
||||
- "7"
|
||||
|
2
.gitignore
vendored
@ -3,3 +3,5 @@ main
|
||||
.idea
|
||||
fscan.exe
|
||||
fscan
|
||||
makefile
|
||||
fscanapi.csv
|
||||
|
@ -925,8 +925,9 @@ var (
|
||||
// POC与漏洞利用配置
|
||||
// =========================================================
|
||||
// POC配置
|
||||
PocPath string // POC脚本路径
|
||||
Pocinfo PocInfo // POC详细信息结构
|
||||
PocPath string // POC脚本路径
|
||||
Pocinfo PocInfo // POC详细信息结构
|
||||
DisablePocScan bool //nopoc
|
||||
|
||||
// Redis利用
|
||||
RedisFile string // Redis利用目标文件
|
||||
@ -956,6 +957,8 @@ var (
|
||||
ShowScanPlan bool // 是否显示扫描计划详情
|
||||
SlowLogOutput bool // 是否启用慢速日志输出
|
||||
Language string // 界面语言设置
|
||||
ApiAddr string // API地址
|
||||
SecretKey string // 加密密钥
|
||||
)
|
||||
|
||||
var (
|
||||
|
101
Common/Flag.go
@ -3,9 +3,10 @@ package Common
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/fatih/color"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
func Banner() {
|
||||
@ -65,14 +66,15 @@ func Flag(Info *HostInfo) {
|
||||
flag.StringVar(&Info.Host, "h", "", GetText("flag_host"))
|
||||
flag.StringVar(&ExcludeHosts, "eh", "", GetText("flag_exclude_hosts"))
|
||||
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(&ScanMode, "m", "All", GetText("flag_scan_mode"))
|
||||
flag.IntVar(&ThreadNum, "t", 10, GetText("flag_thread_num"))
|
||||
flag.StringVar(&ScanMode, "m", "all", GetText("flag_scan_mode"))
|
||||
flag.IntVar(&ThreadNum, "t", 600, GetText("flag_thread_num"))
|
||||
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"))
|
||||
@ -114,6 +116,7 @@ func Flag(Info *HostInfo) {
|
||||
flag.BoolVar(&PocFull, "full", false, GetText("flag_poc_full"))
|
||||
flag.BoolVar(&DnsLog, "dns", false, GetText("flag_dns_log"))
|
||||
flag.IntVar(&PocNum, "num", 20, GetText("flag_poc_num"))
|
||||
flag.BoolVar(&DisablePocScan, "nopoc", false, GetText("flag_no_poc"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// Redis利用参数
|
||||
@ -149,7 +152,8 @@ func Flag(Info *HostInfo) {
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&Shellcode, "sc", "", GetText("flag_shellcode"))
|
||||
flag.StringVar(&Language, "lang", "zh", GetText("flag_language"))
|
||||
|
||||
flag.StringVar(&ApiAddr, "api", "", GetText("flag_api"))
|
||||
flag.StringVar(&SecretKey, "secret", "", GetText("flag_api_key"))
|
||||
// 解析命令行参数
|
||||
parseCommandLineArgs()
|
||||
|
||||
@ -157,6 +161,95 @@ func Flag(Info *HostInfo) {
|
||||
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() {
|
||||
// 首先检查环境变量中的参数
|
||||
|
183
Common/Log.go
@ -44,6 +44,7 @@ type LogEntry struct {
|
||||
const (
|
||||
LogLevelAll = "ALL" // 显示所有级别日志
|
||||
LogLevelError = "ERROR" // 仅显示错误日志
|
||||
LogLevelBase = "BASE" // 仅显示信息日志
|
||||
LogLevelInfo = "INFO" // 仅显示信息日志
|
||||
LogLevelSuccess = "SUCCESS" // 仅显示成功日志
|
||||
LogLevelDebug = "DEBUG" // 仅显示调试日志
|
||||
@ -51,10 +52,11 @@ const (
|
||||
|
||||
// 日志级别对应的显示颜色映射
|
||||
var logColors = map[string]color.Attribute{
|
||||
LogLevelError: color.FgRed, // 错误日志显示红色
|
||||
LogLevelInfo: color.FgYellow, // 信息日志显示黄色
|
||||
LogLevelSuccess: color.FgGreen, // 成功日志显示绿色
|
||||
LogLevelDebug: color.FgBlue, // 调试日志显示蓝色
|
||||
LogLevelError: color.FgBlue, // 错误日志显示蓝色
|
||||
LogLevelBase: color.FgYellow, // 信息日志显示黄色
|
||||
LogLevelInfo: color.FgGreen, // 信息日志显示绿色
|
||||
LogLevelSuccess: color.FgRed, // 成功日志显示红色
|
||||
LogLevelDebug: color.FgWhite, // 调试日志显示白色
|
||||
}
|
||||
|
||||
// InitLogger 初始化日志系统
|
||||
@ -63,42 +65,50 @@ func InitLogger() {
|
||||
log.SetOutput(io.Discard)
|
||||
}
|
||||
|
||||
var StartTime = time.Now()
|
||||
|
||||
// formatLogMessage 格式化日志消息为标准格式
|
||||
// 返回格式:[时间] [级别] 内容
|
||||
func formatLogMessage(entry *LogEntry) string {
|
||||
timeStr := entry.Time.Format("2006-01-02 15:04:05")
|
||||
return fmt.Sprintf("[%s] [%s] %s", timeStr, entry.Level, entry.Content)
|
||||
elapsed := time.Since(StartTime)
|
||||
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 根据日志级别打印日志
|
||||
func printLog(entry *LogEntry) {
|
||||
// 根据当前设置的日志级别过滤日志
|
||||
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 {
|
||||
if LogLevel != "debug" && (entry.Level == LogLevelDebug || entry.Level == LogLevelError) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -141,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 记录错误日志,自动包含文件名和行号信息
|
||||
func LogError(errMsg string) {
|
||||
// 获取调用者的文件名和行号
|
||||
@ -162,53 +230,6 @@ func LogError(errMsg string) {
|
||||
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 检查是否为需要重试的错误
|
||||
func CheckErrs(err error) error {
|
||||
if err == nil {
|
||||
@ -231,7 +252,7 @@ func CheckErrs(err error) error {
|
||||
errLower := strings.ToLower(err.Error())
|
||||
for _, key := range errs {
|
||||
if strings.Contains(errLower, strings.ToLower(key)) {
|
||||
time.Sleep(3 * time.Second)
|
||||
time.Sleep(1 * time.Second)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,18 @@ func InitOutput() error {
|
||||
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{
|
||||
outputPath: Outputfile,
|
||||
outputFormat: OutputFormat,
|
||||
@ -135,6 +147,17 @@ func SaveResult(result *ScanResult) error {
|
||||
LogDebug(GetText("output_saving_result", result.Type, result.Target))
|
||||
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 {
|
||||
om.mu.Lock()
|
||||
@ -165,6 +188,62 @@ func (om *OutputManager) saveResult(result *ScanResult) error {
|
||||
}
|
||||
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 {
|
||||
// 格式化 Details 为键值对字符串
|
||||
|
@ -43,7 +43,7 @@ func ParseUser() error {
|
||||
// 处理命令行参数指定的用户名列表
|
||||
if Username != "" {
|
||||
usernames = strings.Split(Username, ",")
|
||||
LogInfo(GetText("no_username_specified", len(usernames)))
|
||||
LogBase(GetText("no_username_specified", len(usernames)))
|
||||
}
|
||||
|
||||
// 从文件加载用户名列表
|
||||
@ -59,12 +59,12 @@ func ParseUser() error {
|
||||
usernames = append(usernames, user)
|
||||
}
|
||||
}
|
||||
LogInfo(GetText("load_usernames_from_file", len(fileUsers)))
|
||||
LogBase(GetText("load_usernames_from_file", len(fileUsers)))
|
||||
}
|
||||
|
||||
// 去重处理
|
||||
usernames = RemoveDuplicate(usernames)
|
||||
LogInfo(GetText("total_usernames", len(usernames)))
|
||||
LogBase(GetText("total_usernames", len(usernames)))
|
||||
|
||||
// 更新所有字典的用户名列表
|
||||
for name := range Userdict {
|
||||
@ -113,7 +113,7 @@ func parsePasswords() {
|
||||
}
|
||||
}
|
||||
Passwords = pwdList
|
||||
LogInfo(GetText("load_passwords", len(pwdList)))
|
||||
LogBase(GetText("load_passwords", len(pwdList)))
|
||||
}
|
||||
|
||||
// 从文件加载密码列表
|
||||
@ -130,7 +130,7 @@ func parsePasswords() {
|
||||
}
|
||||
}
|
||||
Passwords = pwdList
|
||||
LogInfo(GetText("load_passwords_from_file", len(passes)))
|
||||
LogBase(GetText("load_passwords_from_file", len(passes)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,7 +161,7 @@ func parseHashes() {
|
||||
LogError(GetText("invalid_hash", line))
|
||||
}
|
||||
}
|
||||
LogInfo(GetText("load_valid_hashes", validCount))
|
||||
LogBase(GetText("load_valid_hashes", validCount))
|
||||
}
|
||||
|
||||
// parseURLs 解析URL目标配置
|
||||
@ -201,7 +201,7 @@ func parseURLs() {
|
||||
}
|
||||
|
||||
if len(URLs) > 0 {
|
||||
LogInfo(GetText("load_urls", len(URLs)))
|
||||
LogBase(GetText("load_urls", len(URLs)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,7 +240,7 @@ func parseHosts(Info *HostInfo) error {
|
||||
Info.Host += "," + hostStr
|
||||
}
|
||||
|
||||
LogInfo(GetText("load_hosts_from_file", len(hosts)))
|
||||
LogBase(GetText("load_hosts_from_file", len(hosts)))
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -270,11 +270,21 @@ func parsePorts() error {
|
||||
|
||||
// 更新全局端口配置
|
||||
Ports = portBuilder.String()
|
||||
LogInfo(GetText("load_ports_from_file"))
|
||||
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) {
|
||||
@ -306,7 +316,7 @@ func ReadFileLines(filename string) ([]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
LogInfo(GetText("read_file_success", filename, lineCount))
|
||||
LogBase(GetText("read_file_success", filename, lineCount))
|
||||
return content, nil
|
||||
}
|
||||
|
||||
@ -321,6 +331,9 @@ func ParseInput(Info *HostInfo) error {
|
||||
// 处理端口配置组合
|
||||
processPortsConfig()
|
||||
|
||||
// 处理排除端口配置
|
||||
parseExcludePorts()
|
||||
|
||||
// 处理额外用户名和密码
|
||||
processExtraCredentials()
|
||||
|
||||
@ -379,7 +392,12 @@ func processPortsConfig() {
|
||||
} else {
|
||||
Ports += "," + AddPorts
|
||||
}
|
||||
LogInfo(GetText("extra_ports", AddPorts))
|
||||
LogBase(GetText("extra_ports", AddPorts))
|
||||
}
|
||||
|
||||
// 确保排除端口配置被记录
|
||||
if ExcludePorts != "" {
|
||||
LogBase(GetText("exclude_ports_applied", ExcludePorts))
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,7 +411,7 @@ func processExtraCredentials() {
|
||||
Userdict[dict] = append(Userdict[dict], users...)
|
||||
Userdict[dict] = RemoveDuplicate(Userdict[dict])
|
||||
}
|
||||
LogInfo(GetText("extra_usernames", AddUsers))
|
||||
LogBase(GetText("extra_usernames", AddUsers))
|
||||
}
|
||||
|
||||
// 处理额外密码
|
||||
@ -401,7 +419,7 @@ func processExtraCredentials() {
|
||||
passes := strings.Split(AddPasswords, ",")
|
||||
Passwords = append(Passwords, passes...)
|
||||
Passwords = RemoveDuplicate(Passwords)
|
||||
LogInfo(GetText("extra_passwords", AddPasswords))
|
||||
LogBase(GetText("extra_passwords", AddPasswords))
|
||||
}
|
||||
}
|
||||
|
||||
@ -447,7 +465,7 @@ func setupSocks5Proxy() error {
|
||||
|
||||
// 使用Socks5代理时禁用Ping(无法通过代理进行ICMP)
|
||||
DisablePing = true
|
||||
LogInfo(GetText("socks5_proxy", Socks5Proxy))
|
||||
LogBase(GetText("socks5_proxy", Socks5Proxy))
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -481,7 +499,7 @@ func setupHttpProxy() error {
|
||||
return fmt.Errorf(GetText("proxy_format_error", err))
|
||||
}
|
||||
|
||||
LogInfo(GetText("http_proxy", HttpProxy))
|
||||
LogBase(GetText("http_proxy", HttpProxy))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ func ParseIP(host string, filename string, nohosts ...string) (hosts []string, e
|
||||
host = hostport[0]
|
||||
hosts = parseIPList(host)
|
||||
Ports = hostport[1]
|
||||
LogInfo(GetText("host_port_parsed", Ports))
|
||||
LogBase(GetText("host_port_parsed", Ports))
|
||||
}
|
||||
} else {
|
||||
// 解析主机地址
|
||||
@ -48,7 +48,7 @@ func ParseIP(host string, filename string, nohosts ...string) (hosts []string, e
|
||||
LogError(GetText("read_host_file_failed", err))
|
||||
} else {
|
||||
hosts = append(hosts, fileHosts...)
|
||||
LogInfo(GetText("extra_hosts_loaded", len(fileHosts)))
|
||||
LogBase(GetText("extra_hosts_loaded", len(fileHosts)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -58,7 +58,7 @@ func ParseIP(host string, filename string, nohosts ...string) (hosts []string, e
|
||||
|
||||
// 去重并排序
|
||||
hosts = removeDuplicateIPs(hosts)
|
||||
LogInfo(GetText("final_valid_hosts", len(hosts)))
|
||||
LogBase(GetText("final_valid_hosts", len(hosts)))
|
||||
|
||||
// 检查解析结果
|
||||
if len(hosts) == 0 && len(HostPort) == 0 && (host != "" || filename != "") {
|
||||
@ -160,7 +160,7 @@ func parseCIDR(cidr string) []string {
|
||||
// 转换为IP范围
|
||||
ipRange := calculateIPRange(ipNet)
|
||||
hosts := parseIPRange(ipRange)
|
||||
LogInfo(GetText("parse_cidr_to_range", cidr, ipRange))
|
||||
LogBase(GetText("parse_cidr_to_range", cidr, ipRange))
|
||||
return hosts
|
||||
}
|
||||
|
||||
@ -188,7 +188,7 @@ func calculateIPRange(cidr *net.IPNet) string {
|
||||
end := bcst.String()
|
||||
|
||||
result := fmt.Sprintf("%s-%s", start, end)
|
||||
LogInfo(GetText("cidr_range", result))
|
||||
LogBase(GetText("cidr_range", result))
|
||||
return result
|
||||
}
|
||||
|
||||
@ -264,7 +264,7 @@ func parseShortIPRange(startIP, endSuffix string) []string {
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d", prefixIP, i))
|
||||
}
|
||||
|
||||
LogInfo(GetText("generate_ip_range", prefixIP, startNum, prefixIP, endNum))
|
||||
LogBase(GetText("generate_ip_range", prefixIP, startNum, prefixIP, endNum))
|
||||
return allIP
|
||||
}
|
||||
|
||||
@ -333,7 +333,7 @@ func parseFullIPRange(startIP, endIP string) []string {
|
||||
allIP = append(allIP, ip)
|
||||
}
|
||||
|
||||
LogInfo(GetText("generate_ip_range_full", startIP, endIP, len(allIP)))
|
||||
LogBase(GetText("generate_ip_range_full", startIP, endIP, len(allIP)))
|
||||
return allIP
|
||||
}
|
||||
|
||||
@ -356,7 +356,7 @@ func parseSubnet8(subnet string) []string {
|
||||
firstOctet := strings.Split(baseIP, ".")[0]
|
||||
var sampleIPs []string
|
||||
|
||||
LogInfo(GetText("parse_subnet", firstOctet))
|
||||
LogBase(GetText("parse_subnet", firstOctet))
|
||||
|
||||
// 预分配足够的容量以提高性能
|
||||
// 每个二级网段10个IP,共256*256个二级网段
|
||||
@ -388,7 +388,7 @@ func parseSubnet8(subnet string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
LogInfo(GetText("sample_ip_generated", len(sampleIPs)))
|
||||
LogBase(GetText("sample_ip_generated", len(sampleIPs)))
|
||||
return sampleIPs
|
||||
}
|
||||
|
||||
@ -445,7 +445,7 @@ func readIPFile(filename string) ([]string, error) {
|
||||
for _, host := range hosts {
|
||||
HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, portPart))
|
||||
}
|
||||
LogInfo(GetText("parse_ip_port", line))
|
||||
LogBase(GetText("parse_ip_port", line))
|
||||
} else {
|
||||
LogError(GetText("invalid_ip_port_format", line))
|
||||
}
|
||||
@ -453,7 +453,7 @@ func readIPFile(filename string) ([]string, error) {
|
||||
// 处理纯IP格式
|
||||
hosts := parseIPList(line)
|
||||
ipList = append(ipList, hosts...)
|
||||
LogInfo(GetText("parse_ip_address", line))
|
||||
LogBase(GetText("parse_ip_address", line))
|
||||
}
|
||||
}
|
||||
|
||||
@ -463,7 +463,7 @@ func readIPFile(filename string) ([]string, error) {
|
||||
return ipList, err
|
||||
}
|
||||
|
||||
LogInfo(GetText("file_parse_complete", len(ipList)))
|
||||
LogBase(GetText("file_parse_complete", len(ipList)))
|
||||
return ipList, nil
|
||||
}
|
||||
|
||||
@ -505,7 +505,7 @@ func excludeHosts(hosts []string, nohosts []string) []string {
|
||||
|
||||
// 排序以保持结果的稳定性
|
||||
sort.Strings(result)
|
||||
LogInfo(GetText("hosts_excluded", len(excludeList)))
|
||||
LogBase(GetText("hosts_excluded", len(excludeList)))
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// ParsePort 解析端口配置字符串为端口号列表
|
||||
@ -73,7 +73,7 @@ func ParsePort(ports string) []int {
|
||||
scanPorts = removeDuplicate(scanPorts)
|
||||
sort.Ints(scanPorts)
|
||||
|
||||
LogInfo(GetText("valid_port_count", len(scanPorts)))
|
||||
LogBase(GetText("valid_port_count", len(scanPorts)))
|
||||
return scanPorts
|
||||
}
|
||||
|
||||
|
@ -8,13 +8,32 @@ type HostInfo struct {
|
||||
Infostr []string
|
||||
}
|
||||
|
||||
// 在 Common/const.go 中添加
|
||||
// 插件类型常量
|
||||
const (
|
||||
PluginTypeService = "service" // 服务类型插件
|
||||
PluginTypeWeb = "web" // Web类型插件
|
||||
PluginTypeLocal = "local" // 本地类型插件
|
||||
)
|
||||
|
||||
// ScanPlugin 定义扫描插件的结构
|
||||
type ScanPlugin struct {
|
||||
Name string // 插件名称
|
||||
Ports []int // 关联的端口列表,空切片表示特殊扫描类型
|
||||
Ports []int // 适用端口
|
||||
Types []string // 插件类型标签,一个插件可以有多个类型
|
||||
ScanFunc func(*HostInfo) error // 扫描函数
|
||||
}
|
||||
|
||||
// 添加一个用于检查插件类型的辅助方法
|
||||
func (p ScanPlugin) HasType(typeName string) bool {
|
||||
for _, t := range p.Types {
|
||||
if t == typeName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasPort 检查插件是否支持指定端口
|
||||
func (p *ScanPlugin) HasPort(port int) bool {
|
||||
// 如果没有指定端口列表,表示支持所有端口
|
||||
|
@ -419,6 +419,13 @@ var i18nMap = map[string]map[string]string{
|
||||
LangRU: "Чтение списка портов из файла",
|
||||
},
|
||||
|
||||
"flag_exclude_ports": {
|
||||
LangZH: "排除指定端口",
|
||||
LangEN: "Exclude specified ports",
|
||||
LangJA: "指定されたポートを除外する",
|
||||
LangRU: "Исключить указанные порты",
|
||||
},
|
||||
|
||||
"flag_target_url": {
|
||||
LangZH: "指定目标URL",
|
||||
LangEN: "Specify target URL",
|
||||
@ -503,6 +510,13 @@ var i18nMap = map[string]map[string]string{
|
||||
LangRU: "Установить параллельность POC-сканирования",
|
||||
},
|
||||
|
||||
"flag_no_poc": {
|
||||
LangZH: "禁用POC扫描",
|
||||
LangEN: "Disable POC scanning",
|
||||
LangJA: "POCスキャンを無効にする",
|
||||
LangRU: "Отключить POC-сканирование",
|
||||
},
|
||||
|
||||
// Redis配置相关
|
||||
"flag_redis_file": {
|
||||
LangZH: "指定Redis写入的SSH公钥文件",
|
||||
|
14
Core/ICMP.go
@ -80,7 +80,7 @@ func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool) {
|
||||
|
||||
// 保留原有的控制台输出
|
||||
if !Common.Silent {
|
||||
Common.LogSuccess(Common.GetText("target_alive", ip, protocol))
|
||||
Common.LogInfo(Common.GetText("target_alive", ip, protocol))
|
||||
}
|
||||
}
|
||||
livewg.Done()
|
||||
@ -97,7 +97,7 @@ func probeWithICMP(hostslist []string, chanHosts chan string) {
|
||||
}
|
||||
|
||||
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探测
|
||||
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
|
||||
}
|
||||
|
||||
Common.LogError(Common.GetText("icmp_connect_failed", err))
|
||||
Common.LogInfo(Common.GetText("insufficient_privileges"))
|
||||
Common.LogInfo(Common.GetText("switching_to_ping"))
|
||||
Common.LogBase(Common.GetText("icmp_connect_failed", err))
|
||||
Common.LogBase(Common.GetText("insufficient_privileges"))
|
||||
Common.LogBase(Common.GetText("switching_to_ping"))
|
||||
|
||||
// 降级使用ping探测
|
||||
RunPing(hostslist, chanHosts)
|
||||
@ -121,7 +121,7 @@ func printAliveStats(hostslist []string) {
|
||||
if len(hostslist) > 1000 {
|
||||
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, true)
|
||||
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 {
|
||||
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, false)
|
||||
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
@ -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
@ -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
|
||||
}
|
363
Core/PortScan.go
@ -1,262 +1,151 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Addr 表示待扫描的地址
|
||||
type Addr struct {
|
||||
ip string // IP地址
|
||||
port int // 端口号
|
||||
}
|
||||
|
||||
// ScanResult 扫描结果
|
||||
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
|
||||
// EnhancedPortScan 高性能端口扫描函数
|
||||
func EnhancedPortScan(hosts []string, ports string, timeout int64) []string {
|
||||
// 解析端口和排除端口
|
||||
portList := Common.ParsePort(ports)
|
||||
if len(portList) == 0 {
|
||||
Common.LogError("无效端口: " + ports)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 排除指定端口
|
||||
probePorts = excludeNoPorts(probePorts)
|
||||
exclude := make(map[int]struct{})
|
||||
for _, p := range Common.ParsePort(Common.ExcludePorts) {
|
||||
exclude[p] = struct{}{}
|
||||
}
|
||||
|
||||
// 初始化并发控制
|
||||
workers := Common.ThreadNum
|
||||
addrs := make(chan Addr, 100) // 待扫描地址通道
|
||||
scanResults := make(chan ScanResult, 100) // 扫描结果通道
|
||||
var wg sync.WaitGroup
|
||||
var workerWg sync.WaitGroup
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
to := time.Duration(timeout) * time.Second
|
||||
sem := semaphore.NewWeighted(int64(Common.ThreadNum))
|
||||
var count int64
|
||||
var aliveMap sync.Map
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
// 启动扫描工作协程
|
||||
for i := 0; i < workers; i++ {
|
||||
workerWg.Add(1)
|
||||
go func() {
|
||||
defer workerWg.Done()
|
||||
for addr := range addrs {
|
||||
PortConnect(addr, scanResults, timeout, &wg)
|
||||
// 并发扫描所有目标
|
||||
for _, host := range hosts {
|
||||
for _, port := range portList {
|
||||
if _, excluded := exclude[port]; excluded {
|
||||
continue
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 启动结果处理协程
|
||||
var resultWg sync.WaitGroup
|
||||
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()
|
||||
}
|
||||
}()
|
||||
host, port := host, port // 捕获循环变量
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
// 分发扫描任务
|
||||
for _, port := range probePorts {
|
||||
for _, host := range hostslist {
|
||||
wg.Add(1)
|
||||
addrs <- Addr{host, port}
|
||||
if err := sem.Acquire(ctx, 1); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 等待所有任务完成
|
||||
close(addrs)
|
||||
workerWg.Wait()
|
||||
wg.Wait()
|
||||
close(scanResults)
|
||||
resultWg.Wait()
|
||||
_ = g.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
|
||||
}
|
||||
|
||||
// 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.EnableFingerprint && 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
|
||||
}
|
||||
|
@ -15,18 +15,21 @@ func init() {
|
||||
Name: "FTP",
|
||||
Ports: []int{21},
|
||||
ScanFunc: Plugins.FtpScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("ssh", Common.ScanPlugin{
|
||||
Name: "SSH",
|
||||
Ports: []int{22, 2222},
|
||||
ScanFunc: Plugins.SshScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("telnet", Common.ScanPlugin{
|
||||
Name: "Telnet",
|
||||
Ports: []int{23},
|
||||
ScanFunc: Plugins.TelnetScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// Windows网络服务
|
||||
@ -34,18 +37,21 @@ func init() {
|
||||
Name: "FindNet",
|
||||
Ports: []int{135},
|
||||
ScanFunc: Plugins.Findnet,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("netbios", Common.ScanPlugin{
|
||||
Name: "NetBIOS",
|
||||
Ports: []int{139},
|
||||
ScanFunc: Plugins.NetBIOS,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("smb", Common.ScanPlugin{
|
||||
Name: "SMB",
|
||||
Ports: []int{445},
|
||||
ScanFunc: Plugins.SmbScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 数据库服务
|
||||
@ -53,18 +59,21 @@ func init() {
|
||||
Name: "MSSQL",
|
||||
Ports: []int{1433, 1434},
|
||||
ScanFunc: Plugins.MssqlScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("oracle", Common.ScanPlugin{
|
||||
Name: "Oracle",
|
||||
Ports: []int{1521, 1522, 1526},
|
||||
ScanFunc: Plugins.OracleScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("mysql", Common.ScanPlugin{
|
||||
Name: "MySQL",
|
||||
Ports: []int{3306, 3307, 13306, 33306},
|
||||
ScanFunc: Plugins.MysqlScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 中间件和消息队列服务
|
||||
@ -72,24 +81,28 @@ func init() {
|
||||
Name: "Elasticsearch",
|
||||
Ports: []int{9200, 9300},
|
||||
ScanFunc: Plugins.ElasticScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("rabbitmq", Common.ScanPlugin{
|
||||
Name: "RabbitMQ",
|
||||
Ports: []int{5672, 5671, 15672, 15671},
|
||||
ScanFunc: Plugins.RabbitMQScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("kafka", Common.ScanPlugin{
|
||||
Name: "Kafka",
|
||||
Ports: []int{9092, 9093},
|
||||
ScanFunc: Plugins.KafkaScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("activemq", Common.ScanPlugin{
|
||||
Name: "ActiveMQ",
|
||||
Ports: []int{61613},
|
||||
ScanFunc: Plugins.ActiveMQScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 目录和认证服务
|
||||
@ -97,6 +110,7 @@ func init() {
|
||||
Name: "LDAP",
|
||||
Ports: []int{389, 636},
|
||||
ScanFunc: Plugins.LDAPScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 邮件服务
|
||||
@ -104,18 +118,21 @@ func init() {
|
||||
Name: "SMTP",
|
||||
Ports: []int{25, 465, 587},
|
||||
ScanFunc: Plugins.SmtpScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("imap", Common.ScanPlugin{
|
||||
Name: "IMAP",
|
||||
Ports: []int{143, 993},
|
||||
ScanFunc: Plugins.IMAPScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("pop3", Common.ScanPlugin{
|
||||
Name: "POP3",
|
||||
Ports: []int{110, 995},
|
||||
ScanFunc: Plugins.POP3Scan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 网络管理和监控服务
|
||||
@ -123,12 +140,14 @@ func init() {
|
||||
Name: "SNMP",
|
||||
Ports: []int{161, 162},
|
||||
ScanFunc: Plugins.SNMPScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("modbus", Common.ScanPlugin{
|
||||
Name: "Modbus",
|
||||
Ports: []int{502, 5020},
|
||||
ScanFunc: Plugins.ModbusScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 数据同步和备份服务
|
||||
@ -136,6 +155,7 @@ func init() {
|
||||
Name: "Rsync",
|
||||
Ports: []int{873},
|
||||
ScanFunc: Plugins.RsyncScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// NoSQL数据库
|
||||
@ -143,12 +163,14 @@ func init() {
|
||||
Name: "Cassandra",
|
||||
Ports: []int{9042},
|
||||
ScanFunc: Plugins.CassandraScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("neo4j", Common.ScanPlugin{
|
||||
Name: "Neo4j",
|
||||
Ports: []int{7687},
|
||||
ScanFunc: Plugins.Neo4jScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 远程桌面和显示服务
|
||||
@ -156,18 +178,21 @@ func init() {
|
||||
Name: "RDP",
|
||||
Ports: []int{3389, 13389, 33389},
|
||||
ScanFunc: Plugins.RdpScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("postgres", Common.ScanPlugin{
|
||||
Name: "PostgreSQL",
|
||||
Ports: []int{5432, 5433},
|
||||
ScanFunc: Plugins.PostgresScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("vnc", Common.ScanPlugin{
|
||||
Name: "VNC",
|
||||
Ports: []int{5900, 5901, 5902},
|
||||
ScanFunc: Plugins.VncScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 缓存和键值存储服务
|
||||
@ -175,18 +200,21 @@ func init() {
|
||||
Name: "Redis",
|
||||
Ports: []int{6379, 6380, 16379},
|
||||
ScanFunc: Plugins.RedisScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("memcached", Common.ScanPlugin{
|
||||
Name: "Memcached",
|
||||
Ports: []int{11211},
|
||||
ScanFunc: Plugins.MemcachedScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("mongodb", Common.ScanPlugin{
|
||||
Name: "MongoDB",
|
||||
Ports: []int{27017, 27018},
|
||||
ScanFunc: Plugins.MongodbScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 2. 特殊漏洞扫描插件
|
||||
@ -194,12 +222,14 @@ func init() {
|
||||
Name: "MS17010",
|
||||
Ports: []int{445},
|
||||
ScanFunc: Plugins.MS17010,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("smbghost", Common.ScanPlugin{
|
||||
Name: "SMBGhost",
|
||||
Ports: []int{445},
|
||||
ScanFunc: Plugins.SmbGhost,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 3. Web应用扫描插件
|
||||
@ -207,12 +237,14 @@ func init() {
|
||||
Name: "WebTitle",
|
||||
Ports: Common.ParsePortsFromString(Common.WebPorts),
|
||||
ScanFunc: Plugins.WebTitle,
|
||||
Types: []string{Common.PluginTypeWeb},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("webpoc", Common.ScanPlugin{
|
||||
Name: "WebPoc",
|
||||
Ports: Common.ParsePortsFromString(Common.WebPorts),
|
||||
ScanFunc: Plugins.WebPoc,
|
||||
Types: []string{Common.PluginTypeWeb},
|
||||
})
|
||||
|
||||
// 4. Windows系统专用插件
|
||||
@ -220,6 +252,7 @@ func init() {
|
||||
Name: "SMBScan2",
|
||||
Ports: []int{445},
|
||||
ScanFunc: Plugins.SmbScan2,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 5. 本地信息收集插件
|
||||
@ -227,33 +260,30 @@ func init() {
|
||||
Name: "LocalInfo",
|
||||
Ports: []int{},
|
||||
ScanFunc: Plugins.LocalInfoScan,
|
||||
Types: []string{Common.PluginTypeLocal},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("dcinfo", Common.ScanPlugin{
|
||||
Name: "DCInfo",
|
||||
Ports: []int{},
|
||||
ScanFunc: Plugins.DCInfoScan,
|
||||
Types: []string{Common.PluginTypeLocal},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("minidump", Common.ScanPlugin{
|
||||
Name: "MiniDump",
|
||||
Ports: []int{},
|
||||
ScanFunc: Plugins.MiniDump,
|
||||
Types: []string{Common.PluginTypeLocal},
|
||||
})
|
||||
}
|
||||
|
||||
// GetAllPlugins 返回所有已注册插件的名称列表
|
||||
// 当用户未指定特定插件或使用"All"模式时使用
|
||||
func GetAllPlugins() []string {
|
||||
pluginNames := make([]string, 0, len(Common.PluginManager))
|
||||
|
||||
// 遍历插件管理器,获取所有插件名称
|
||||
for name := range Common.PluginManager {
|
||||
pluginNames = append(pluginNames, name)
|
||||
}
|
||||
|
||||
// 对插件名称进行排序,使输出更加一致
|
||||
sort.Strings(pluginNames)
|
||||
|
||||
return pluginNames
|
||||
}
|
||||
|
566
Core/Scanner.go
@ -5,7 +5,6 @@ import (
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -13,455 +12,90 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// 全局状态
|
||||
var (
|
||||
LocalScan bool // 本地扫描模式标识
|
||||
WebScan bool // Web扫描模式标识
|
||||
Mutex = &sync.Mutex{} // 用于保护共享资源
|
||||
)
|
||||
|
||||
// ScanTask 表示单个扫描任务
|
||||
type ScanTask struct {
|
||||
pluginName string // 插件名称
|
||||
target Common.HostInfo // 目标信息
|
||||
}
|
||||
|
||||
// 添加一个本地插件集合,用于识别哪些插件是本地信息收集插件
|
||||
var localPlugins = map[string]bool{
|
||||
"localinfo": true,
|
||||
"dcinfo": true,
|
||||
"minidump": true,
|
||||
// ScanStrategy 定义扫描策略接口
|
||||
type ScanStrategy interface {
|
||||
// 名称和描述
|
||||
Name() string
|
||||
Description() string
|
||||
|
||||
// 执行扫描的主要方法
|
||||
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
|
||||
}
|
||||
|
||||
// Scan 执行整体扫描流程的入口函数
|
||||
func Scan(info Common.HostInfo) {
|
||||
Common.LogInfo("开始信息扫描")
|
||||
// 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{}
|
||||
|
||||
// 选择并执行扫描模式
|
||||
selectScanMode(info, &ch, &wg)
|
||||
// 执行策略
|
||||
s.strategy.Execute(info, &ch, &wg)
|
||||
|
||||
// 等待所有扫描完成
|
||||
wg.Wait()
|
||||
finishScan()
|
||||
s.finishScan()
|
||||
}
|
||||
|
||||
// 根据配置选择扫描模式
|
||||
func selectScanMode(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
switch {
|
||||
case Common.LocalMode:
|
||||
LocalScan = true
|
||||
executeLocalScan(info, ch, wg)
|
||||
case len(Common.URLs) > 0:
|
||||
WebScan = true
|
||||
executeWebScan(info, ch, wg)
|
||||
default:
|
||||
executeHostScan(info, ch, wg)
|
||||
}
|
||||
}
|
||||
|
||||
// 完成扫描并输出结果
|
||||
func finishScan() {
|
||||
// finishScan 完成扫描并输出结果
|
||||
func (s *Scanner) finishScan() {
|
||||
if Common.ProgressBar != nil {
|
||||
Common.ProgressBar.Finish()
|
||||
fmt.Println()
|
||||
}
|
||||
Common.LogSuccess(fmt.Sprintf("扫描已完成: %v/%v", Common.End, Common.Num))
|
||||
Common.LogBase(fmt.Sprintf("扫描已完成: %v/%v", Common.End, Common.Num))
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 三种扫描模式实现
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// 执行本地信息收集
|
||||
func executeLocalScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
Common.LogInfo("执行本地信息收集")
|
||||
|
||||
// 验证插件配置
|
||||
if err := validateScanPlugins(); err != nil {
|
||||
Common.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 输出插件信息
|
||||
logPluginInfo()
|
||||
|
||||
// 执行扫描任务
|
||||
executeScanTasks([]Common.HostInfo{info}, ch, wg)
|
||||
}
|
||||
|
||||
// 执行Web扫描
|
||||
func executeWebScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
Common.LogInfo("开始Web扫描")
|
||||
|
||||
// 验证插件配置
|
||||
if err := validateScanPlugins(); err != nil {
|
||||
Common.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 准备URL目标
|
||||
targetInfos := prepareURLTargets(info)
|
||||
|
||||
// 输出插件信息
|
||||
logPluginInfo()
|
||||
|
||||
// 执行扫描任务
|
||||
executeScanTasks(targetInfos, ch, wg)
|
||||
}
|
||||
|
||||
// 执行主机扫描
|
||||
func executeHostScan(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.LogInfo("开始主机扫描")
|
||||
|
||||
// 输出插件信息
|
||||
logPluginInfo()
|
||||
|
||||
// 执行主机扫描
|
||||
performHostScan(hosts, info, ch, wg)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 主机扫描流程详细实现
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// 执行主机扫描的完整流程
|
||||
func 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 shouldPerformLivenessCheck(hosts) {
|
||||
hosts = CheckLive(hosts, Common.UsePing)
|
||||
Common.LogInfo(fmt.Sprintf("存活主机数量: %d", len(hosts)))
|
||||
}
|
||||
|
||||
// 端口扫描
|
||||
targetInfos = scanPortsAndPrepareTargets(hosts, info)
|
||||
}
|
||||
|
||||
// 添加URL目标
|
||||
targetInfos = appendURLTargets(targetInfos, info)
|
||||
|
||||
// 执行漏洞扫描
|
||||
if len(targetInfos) > 0 {
|
||||
Common.LogInfo("开始漏洞扫描")
|
||||
executeScanTasks(targetInfos, ch, wg)
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否需要执行存活性检测
|
||||
func shouldPerformLivenessCheck(hosts []string) bool {
|
||||
return Common.DisablePing == false && len(hosts) > 1
|
||||
}
|
||||
|
||||
// 扫描端口并准备目标信息
|
||||
func scanPortsAndPrepareTargets(hosts []string, info Common.HostInfo) []Common.HostInfo {
|
||||
// 扫描存活端口
|
||||
alivePorts := discoverAlivePorts(hosts)
|
||||
if len(alivePorts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换为目标信息
|
||||
return convertToTargetInfos(alivePorts, info)
|
||||
}
|
||||
|
||||
// 发现存活的端口
|
||||
func discoverAlivePorts(hosts []string) []string {
|
||||
var alivePorts []string
|
||||
|
||||
// 根据扫描模式选择端口扫描方式
|
||||
if WebScan || len(Common.URLs) > 0 {
|
||||
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 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
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 插件管理和解析
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// getAllLocalPlugins 返回所有本地插件的名称列表
|
||||
func getAllLocalPlugins() []string {
|
||||
var localPluginList []string
|
||||
for plugin := range localPlugins {
|
||||
localPluginList = append(localPluginList, plugin)
|
||||
}
|
||||
sort.Strings(localPluginList)
|
||||
return localPluginList
|
||||
}
|
||||
|
||||
// parsePluginList 解析逗号分隔的插件列表
|
||||
// pluginStr: 逗号分隔的插件字符串,如 "ssh,ftp,telnet"
|
||||
// 返回: 插件名称的字符串切片
|
||||
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
|
||||
}
|
||||
|
||||
// validateScanPlugins 验证扫描插件的有效性
|
||||
// 返回: 错误信息
|
||||
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, ", "))
|
||||
}
|
||||
|
||||
// 如果是本地模式,验证是否包含非本地插件
|
||||
if Common.LocalMode {
|
||||
var nonLocalPlugins []string
|
||||
for _, plugin := range plugins {
|
||||
if !isLocalPlugin(plugin) {
|
||||
nonLocalPlugins = append(nonLocalPlugins, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
if len(nonLocalPlugins) > 0 {
|
||||
Common.LogInfo(fmt.Sprintf("本地模式下,以下非本地插件将被忽略: %s", strings.Join(nonLocalPlugins, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isLocalPlugin 判断插件是否为本地信息收集插件
|
||||
func isLocalPlugin(pluginName string) bool {
|
||||
return localPlugins[pluginName]
|
||||
}
|
||||
|
||||
// getPluginsToRun 获取要执行的插件列表
|
||||
// 返回: 插件列表和是否为自定义插件模式
|
||||
func getPluginsToRun() ([]string, bool) {
|
||||
// 本地模式处理
|
||||
if Common.LocalMode {
|
||||
// 在本地模式下只执行本地插件
|
||||
|
||||
// 如果指定了特定插件(单个或多个)
|
||||
if Common.ScanMode != "" && Common.ScanMode != "All" {
|
||||
requestedPlugins := parsePluginList(Common.ScanMode)
|
||||
if len(requestedPlugins) == 0 {
|
||||
requestedPlugins = []string{Common.ScanMode}
|
||||
}
|
||||
|
||||
// 过滤出本地插件
|
||||
var localPluginsToRun []string
|
||||
for _, plugin := range requestedPlugins {
|
||||
if isLocalPlugin(plugin) {
|
||||
localPluginsToRun = append(localPluginsToRun, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
return localPluginsToRun, true
|
||||
}
|
||||
|
||||
// 如果是All模式或未指定,则返回所有本地插件
|
||||
return getAllLocalPlugins(), true
|
||||
}
|
||||
|
||||
// 非本地模式处理(保持原有行为)
|
||||
// 如果指定了插件列表(逗号分隔)
|
||||
if Common.ScanMode != "" && Common.ScanMode != "All" {
|
||||
plugins := parsePluginList(Common.ScanMode)
|
||||
if len(plugins) > 0 {
|
||||
return plugins, true
|
||||
}
|
||||
return []string{Common.ScanMode}, true
|
||||
}
|
||||
|
||||
// 默认情况:使用所有非本地插件
|
||||
allPlugins := GetAllPlugins()
|
||||
filteredPlugins := make([]string, 0, len(allPlugins))
|
||||
|
||||
for _, plugin := range allPlugins {
|
||||
if !isLocalPlugin(plugin) {
|
||||
filteredPlugins = append(filteredPlugins, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredPlugins, false
|
||||
}
|
||||
|
||||
// logPluginInfo 输出插件信息
|
||||
func logPluginInfo() {
|
||||
if Common.LocalMode {
|
||||
if Common.ScanMode == "" || Common.ScanMode == "All" {
|
||||
Common.LogInfo("本地模式: 使用所有本地信息收集插件")
|
||||
} else {
|
||||
plugins := parsePluginList(Common.ScanMode)
|
||||
if len(plugins) == 0 {
|
||||
plugins = []string{Common.ScanMode}
|
||||
}
|
||||
|
||||
// 过滤出本地插件
|
||||
var localPluginsToRun []string
|
||||
for _, plugin := range plugins {
|
||||
if isLocalPlugin(plugin) {
|
||||
localPluginsToRun = append(localPluginsToRun, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
if len(localPluginsToRun) > 1 {
|
||||
Common.LogInfo(fmt.Sprintf("本地模式: 使用本地插件: %s", strings.Join(localPluginsToRun, ", ")))
|
||||
} else if len(localPluginsToRun) == 1 {
|
||||
Common.LogInfo(fmt.Sprintf("本地模式: 使用本地插件: %s", localPluginsToRun[0]))
|
||||
} else {
|
||||
Common.LogInfo("本地模式: 未指定有效的本地插件,将不执行任何扫描")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 非本地模式的原有逻辑
|
||||
if Common.ScanMode == "" || Common.ScanMode == "All" {
|
||||
Common.LogInfo("使用所有可用插件(已排除本地敏感插件)")
|
||||
} else {
|
||||
plugins := parsePluginList(Common.ScanMode)
|
||||
if len(plugins) > 1 {
|
||||
Common.LogInfo(fmt.Sprintf("使用插件: %s", strings.Join(plugins, ", ")))
|
||||
} else {
|
||||
Common.LogInfo(fmt.Sprintf("使用插件: %s", Common.ScanMode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 目标准备
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// 准备URL目标列表
|
||||
func prepareURLTargets(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
|
||||
}
|
||||
|
||||
// 将端口列表转换为目标信息
|
||||
func 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
|
||||
}
|
||||
|
||||
// 添加URL扫描目标
|
||||
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
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 任务执行
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// 执行扫描任务集合
|
||||
func executeScanTasks(targets []Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
// 任务执行通用框架
|
||||
func ExecuteScanTasks(targets []Common.HostInfo, strategy ScanStrategy, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
// 获取要执行的插件
|
||||
pluginsToRun, isCustomMode := getPluginsToRun()
|
||||
pluginsToRun, isCustomMode := strategy.GetPlugins()
|
||||
|
||||
// 准备扫描任务
|
||||
tasks := prepareScanTasks(targets, pluginsToRun, isCustomMode)
|
||||
tasks := prepareScanTasks(targets, pluginsToRun, isCustomMode, strategy)
|
||||
|
||||
// 输出扫描计划
|
||||
if Common.ShowScanPlan && len(tasks) > 0 {
|
||||
@ -479,6 +113,35 @@ func executeScanTasks(targets []Common.HostInfo, ch *chan struct{}, wg *sync.Wai
|
||||
}
|
||||
}
|
||||
|
||||
// 准备扫描任务列表
|
||||
func prepareScanTasks(targets []Common.HostInfo, pluginsToRun []string, isCustomMode bool, strategy ScanStrategy) []ScanTask {
|
||||
var tasks []ScanTask
|
||||
|
||||
for _, target := range targets {
|
||||
targetPort := 0
|
||||
if target.Ports != "" {
|
||||
targetPort, _ = strconv.Atoi(target.Ports)
|
||||
}
|
||||
|
||||
for _, pluginName := range pluginsToRun {
|
||||
plugin, exists := Common.PluginManager[pluginName]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查插件是否适用于当前目标 (通过策略判断)
|
||||
if strategy.IsPluginApplicable(plugin, targetPort, isCustomMode) {
|
||||
tasks = append(tasks, ScanTask{
|
||||
pluginName: pluginName,
|
||||
target: target,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
// logScanPlan 输出扫描计划信息
|
||||
func logScanPlan(tasks []ScanTask) {
|
||||
// 统计每个插件的目标数量
|
||||
@ -495,56 +158,7 @@ func logScanPlan(tasks []ScanTask) {
|
||||
planInfo.WriteString(fmt.Sprintf(" - %s: %d 个目标\n", plugin, count))
|
||||
}
|
||||
|
||||
Common.LogInfo(planInfo.String())
|
||||
}
|
||||
|
||||
// 准备扫描任务列表
|
||||
func prepareScanTasks(targets []Common.HostInfo, pluginsToRun []string, isCustomMode bool) []ScanTask {
|
||||
var tasks []ScanTask
|
||||
|
||||
for _, target := range targets {
|
||||
targetPort, _ := strconv.Atoi(target.Ports)
|
||||
|
||||
for _, pluginName := range pluginsToRun {
|
||||
plugin, exists := Common.PluginManager[pluginName]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查插件是否适用于当前目标
|
||||
if isPluginApplicable(plugin, targetPort, isCustomMode, pluginName) {
|
||||
tasks = append(tasks, ScanTask{
|
||||
pluginName: pluginName,
|
||||
target: target,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
// isPluginApplicable 判断插件是否适用于目标
|
||||
func isPluginApplicable(plugin Common.ScanPlugin, targetPort int, isCustomMode bool, pluginName string) bool {
|
||||
// 本地模式下,只执行本地插件
|
||||
if LocalScan {
|
||||
return isLocalPlugin(pluginName)
|
||||
}
|
||||
|
||||
// 非本地模式下,本地插件特殊处理
|
||||
if isLocalPlugin(pluginName) {
|
||||
// 只有在自定义模式下明确指定时才执行本地插件
|
||||
return isCustomMode
|
||||
}
|
||||
|
||||
// 特殊扫描模式下的处理
|
||||
if WebScan || isCustomMode {
|
||||
return true
|
||||
}
|
||||
|
||||
// 端口匹配检查
|
||||
// 无端口限制的插件或端口匹配的插件
|
||||
return len(plugin.Ports) == 0 || plugin.HasPort(targetPort)
|
||||
Common.LogBase(planInfo.String())
|
||||
}
|
||||
|
||||
// 初始化进度条
|
||||
@ -585,7 +199,7 @@ func scheduleScanTask(pluginName string, target Common.HostInfo, ch *chan struct
|
||||
// 完成任务,释放资源
|
||||
duration := time.Since(startTime)
|
||||
if Common.ShowScanPlan {
|
||||
Common.LogInfo(fmt.Sprintf("完成 %s 扫描 %s:%s (耗时: %.2fs)",
|
||||
Common.LogBase(fmt.Sprintf("完成 %s 扫描 %s:%s (耗时: %.2fs)",
|
||||
pluginName, target.Host, target.Ports, duration.Seconds()))
|
||||
}
|
||||
|
||||
@ -603,7 +217,7 @@ func scheduleScanTask(pluginName string, target Common.HostInfo, ch *chan struct
|
||||
func executeSingleScan(pluginName string, info Common.HostInfo) {
|
||||
plugin, exists := Common.PluginManager[pluginName]
|
||||
if !exists {
|
||||
Common.LogInfo(fmt.Sprintf("扫描类型 %v 无对应插件,已跳过", pluginName))
|
||||
Common.LogBase(fmt.Sprintf("扫描类型 %v 无对应插件,已跳过", pluginName))
|
||||
return
|
||||
}
|
||||
|
||||
@ -624,3 +238,9 @@ func updateProgress() {
|
||||
Common.ProgressBar.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 入口函数,向后兼容旧的调用方式
|
||||
func Scan(info Common.HostInfo) {
|
||||
scanner := NewScanner(info)
|
||||
scanner.Scan(info)
|
||||
}
|
||||
|
218
Core/ServiceScanner.go
Normal 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
@ -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)
|
||||
}
|
@ -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\.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\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\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\.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\.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=\"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=\"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\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/
|
||||
|
@ -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:数据库相关端口
|
||||
- WebPorts:Web服务端口
|
||||
- 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
|
@ -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 编码规范
|
||||
- 保证代码可读性和可维护性
|
||||
- 禁止提交恶意代码
|
||||
- 做好异常处理和超时控制
|
||||
- 避免过度消耗系统资源
|
||||
- 注意信息安全,不要泄露敏感数据
|
@ -224,6 +224,6 @@ func read(text []byte, host string) error {
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogSuccess(output.String())
|
||||
Common.LogInfo(output.String())
|
||||
return nil
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ var (
|
||||
|
||||
// LocalInfoScan 本地信息收集主函数
|
||||
func LocalInfoScan(info *Common.HostInfo) (err error) {
|
||||
Common.LogInfo("开始本地信息收集...")
|
||||
Common.LogBase("开始本地信息收集...")
|
||||
|
||||
// 获取用户主目录
|
||||
home, err := os.UserHomeDir()
|
||||
@ -107,7 +107,7 @@ func LocalInfoScan(info *Common.HostInfo) (err error) {
|
||||
// 根据规则搜索敏感文件
|
||||
searchSensitiveFiles()
|
||||
|
||||
Common.LogInfo("本地信息收集完成")
|
||||
Common.LogBase("本地信息收集完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -267,7 +267,7 @@ func MS17010Scan(info *Common.HostInfo) error {
|
||||
defer MS17010EXP(info)
|
||||
}
|
||||
} else if os != "" {
|
||||
Common.LogInfo(fmt.Sprintf("系统信息 %s [%s]", ip, os))
|
||||
Common.LogBase(fmt.Sprintf("系统信息 %s [%s]", ip, os))
|
||||
|
||||
// 保存系统信息
|
||||
sysResult := &Common.ScanResult{
|
||||
|
@ -488,5 +488,5 @@ func logShareInfo(info *Common.HostInfo, user string, pass string, hash []byte,
|
||||
msg += fmt.Sprintf(" Pass:%s", pass)
|
||||
}
|
||||
msg += fmt.Sprintf(" 共享:%v", shares)
|
||||
Common.LogInfo(msg)
|
||||
Common.LogBase(msg)
|
||||
}
|
||||
|
@ -537,9 +537,9 @@ func (c *TelnetClient) Login() error {
|
||||
case UnauthorizedAccess:
|
||||
return nil
|
||||
case OnlyPassword:
|
||||
return c.loginForOnlyPassword()
|
||||
return c.LogBaserOnlyPassword()
|
||||
case UsernameAndPassword:
|
||||
return c.loginForUsernameAndPassword()
|
||||
return c.LogBaserUsernameAndPassword()
|
||||
default:
|
||||
return errors.New("unknown server type")
|
||||
}
|
||||
@ -605,8 +605,8 @@ func isNoAuthRequired(line string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// loginForOnlyPassword 处理只需密码的登录
|
||||
func (c *TelnetClient) loginForOnlyPassword() error {
|
||||
// LogBaserOnlyPassword 处理只需密码的登录
|
||||
func (c *TelnetClient) LogBaserOnlyPassword() error {
|
||||
c.Clear() // 清空之前的响应
|
||||
|
||||
// 发送密码并等待响应
|
||||
@ -625,8 +625,8 @@ func (c *TelnetClient) loginForOnlyPassword() error {
|
||||
return errors.New("login failed")
|
||||
}
|
||||
|
||||
// loginForUsernameAndPassword 处理需要用户名和密码的登录
|
||||
func (c *TelnetClient) loginForUsernameAndPassword() error {
|
||||
// LogBaserUsernameAndPassword 处理需要用户名和密码的登录
|
||||
func (c *TelnetClient) LogBaserUsernameAndPassword() error {
|
||||
// 发送用户名
|
||||
c.WriteContext(c.UserName)
|
||||
time.Sleep(time.Second * 2)
|
||||
|
@ -7,6 +7,9 @@ import (
|
||||
|
||||
// WebPoc 直接执行Web漏洞扫描
|
||||
func WebPoc(info *Common.HostInfo) error {
|
||||
if Common.DisablePocScan {
|
||||
return nil
|
||||
}
|
||||
WebScan.WebScan(info)
|
||||
return nil
|
||||
}
|
||||
|
@ -8,9 +8,9 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
@ -20,175 +20,203 @@ import (
|
||||
"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标题和指纹信息
|
||||
func WebTitle(info *Common.HostInfo) error {
|
||||
Common.LogDebug(fmt.Sprintf("开始获取Web标题,初始信息: %+v", info))
|
||||
|
||||
// 获取网站标题信息
|
||||
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
|
||||
}
|
||||
if info == nil {
|
||||
return fmt.Errorf("主机信息为空")
|
||||
}
|
||||
|
||||
// 输出错误信息(如果有)
|
||||
// 初始化Url
|
||||
if err := initializeUrl(info); err != nil {
|
||||
Common.LogError(fmt.Sprintf("初始化Url失败: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取网站标题信息
|
||||
checkData, err := fetchWebInfo(info)
|
||||
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
|
||||
}
|
||||
|
||||
// GOWebTitle 获取网站标题并处理URL,增强错误处理和协议切换
|
||||
func GOWebTitle(info *Common.HostInfo) (err error, CheckData []WebScan.CheckDatas) {
|
||||
Common.LogDebug(fmt.Sprintf("开始处理URL: %s", info.Url))
|
||||
|
||||
// 如果URL未指定,根据端口生成URL
|
||||
// 初始化Url:根据主机和端口生成完整Url
|
||||
func initializeUrl(info *Common.HostInfo) error {
|
||||
if info.Url == "" {
|
||||
Common.LogDebug("URL为空,根据端口生成URL")
|
||||
// 根据端口推断Url
|
||||
switch info.Ports {
|
||||
case "80":
|
||||
info.Url = fmt.Sprintf("http://%s", info.Host)
|
||||
case "443":
|
||||
info.Url = fmt.Sprintf("https://%s", info.Host)
|
||||
case httpPort:
|
||||
info.Url = fmt.Sprintf("%s://%s", httpProtocol, info.Host)
|
||||
case httpsPort:
|
||||
info.Url = fmt.Sprintf("%s://%s", httpsProtocol, info.Host)
|
||||
default:
|
||||
host := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("正在检测主机协议: %s", host))
|
||||
protocol := GetProtocol(host, Common.Timeout)
|
||||
Common.LogDebug(fmt.Sprintf("检测到协议: %s", protocol))
|
||||
protocol, err := detectProtocol(host, Common.Timeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("协议检测失败: %w", err)
|
||||
}
|
||||
info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
|
||||
}
|
||||
} else {
|
||||
// 处理未指定协议的URL
|
||||
if !strings.Contains(info.Url, "://") {
|
||||
Common.LogDebug("URL未包含协议,开始检测")
|
||||
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协议
|
||||
originalProtocol := "http"
|
||||
if strings.HasPrefix(info.Url, "https://") {
|
||||
originalProtocol = "https"
|
||||
}
|
||||
|
||||
// 第一次获取URL
|
||||
Common.LogDebug("第一次尝试访问URL")
|
||||
err, result, CheckData := geturl(info, 1, CheckData)
|
||||
Common.LogDebug(fmt.Sprintf("第一次访问结果 - 错误: %v, 返回信息: %s", err, result))
|
||||
|
||||
// 如果访问失败并且使用的是HTTPS,尝试降级到HTTP
|
||||
if err != nil && !strings.Contains(err.Error(), "EOF") {
|
||||
if originalProtocol == "https" {
|
||||
Common.LogDebug("HTTPS访问失败,尝试降级到HTTP")
|
||||
// 替换协议部分
|
||||
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
|
||||
Common.LogDebug(fmt.Sprintf("降级后的URL: %s", info.Url))
|
||||
err, result, CheckData = geturl(info, 1, CheckData)
|
||||
Common.LogDebug(fmt.Sprintf("HTTP降级访问结果 - 错误: %v, 返回信息: %s", err, result))
|
||||
|
||||
// 如果仍然失败,返回错误
|
||||
if err != nil && !strings.Contains(err.Error(), "EOF") {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 如果本来就是HTTP并且失败了,直接返回错误
|
||||
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))
|
||||
} else if !strings.Contains(info.Url, "://") {
|
||||
// 处理未指定协议的Url
|
||||
host := strings.Split(info.Url, "/")[0]
|
||||
protocol, err := detectProtocol(host, Common.Timeout)
|
||||
if err != nil {
|
||||
// 如果重定向跟踪失败,尝试降级协议
|
||||
if strings.HasPrefix(info.Url, "https://") {
|
||||
Common.LogDebug("重定向HTTPS访问失败,尝试降级到HTTP")
|
||||
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
|
||||
err, result, CheckData = geturl(info, 3, CheckData)
|
||||
Common.LogDebug(fmt.Sprintf("重定向降级访问结果 - 错误: %v, 返回信息: %s", err, result))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return fmt.Errorf("协议检测失败: %w", err)
|
||||
}
|
||||
info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
|
||||
}
|
||||
|
||||
// 处理HTTP到HTTPS的升级提示
|
||||
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)
|
||||
Common.LogDebug(fmt.Sprintf("HTTPS升级访问结果 - 错误: %v, 返回信息: %s", err, result))
|
||||
|
||||
// 如果HTTPS升级后访问失败,回退到HTTP
|
||||
if err != nil && !strings.Contains(err.Error(), "EOF") {
|
||||
Common.LogDebug("HTTPS升级访问失败,回退到HTTP")
|
||||
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
|
||||
err, result, CheckData = geturl(info, 1, CheckData)
|
||||
Common.LogDebug(fmt.Sprintf("回退到HTTP访问结果 - 错误: %v, 返回信息: %s", err, result))
|
||||
}
|
||||
|
||||
// 处理升级后的跳转
|
||||
if strings.Contains(result, "://") {
|
||||
Common.LogDebug(fmt.Sprintf("协议升级后发现重定向到: %s", result))
|
||||
info.Url = result
|
||||
err, _, CheckData = geturl(info, 3, CheckData)
|
||||
if err != nil {
|
||||
// 如果重定向跟踪失败,再次尝试降级
|
||||
if strings.HasPrefix(info.Url, "https://") {
|
||||
Common.LogDebug("升级后重定向HTTPS访问失败,尝试降级到HTTP")
|
||||
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
|
||||
err, _, CheckData = geturl(info, 3, CheckData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("GOWebTitle执行完成 - 错误: %v", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
func geturl(info *Common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (error, string, []WebScan.CheckDatas) {
|
||||
Common.LogDebug(fmt.Sprintf("geturl开始执行 - URL: %s, 标志位: %d", info.Url, flag))
|
||||
// 获取Web信息:标题、指纹等
|
||||
func fetchWebInfo(info *Common.HostInfo) ([]WebScan.CheckDatas, error) {
|
||||
var checkData []WebScan.CheckDatas
|
||||
|
||||
// 处理目标URL
|
||||
Url := info.Url
|
||||
if flag == 2 {
|
||||
Common.LogDebug("处理favicon.ico URL")
|
||||
URL, err := url.Parse(Url)
|
||||
if err == nil {
|
||||
Url = fmt.Sprintf("%s://%s/favicon.ico", URL.Scheme, URL.Host)
|
||||
// 记录原始Url协议
|
||||
originalUrl := info.Url
|
||||
isHTTPS := strings.HasPrefix(info.Url, "https://")
|
||||
|
||||
// 第一次尝试访问Url
|
||||
resp, err := fetchUrlWithRetry(info, false, &checkData)
|
||||
|
||||
// 处理不同的错误情况
|
||||
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 {
|
||||
Url += "/favicon.ico"
|
||||
return checkData, err
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("favicon URL: %s", Url))
|
||||
}
|
||||
|
||||
// 创建HTTP请求
|
||||
Common.LogDebug("开始创建HTTP请求")
|
||||
req, err := http.NewRequest("GET", Url, nil)
|
||||
// 处理重定向
|
||||
if resp != nil && resp.RedirectUrl != "" {
|
||||
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 {
|
||||
Common.LogDebug(fmt.Sprintf("创建HTTP请求失败: %v", err))
|
||||
return err, "", CheckData
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存检查数据
|
||||
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)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
@ -199,378 +227,327 @@ func geturl(info *Common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (er
|
||||
req.Header.Set("Cookie", Common.Cookie)
|
||||
}
|
||||
req.Header.Set("Connection", "close")
|
||||
Common.LogDebug("已设置请求头")
|
||||
|
||||
// 选择HTTP客户端
|
||||
var client *http.Client
|
||||
if flag == 1 {
|
||||
client = lib.ClientNoRedirect
|
||||
Common.LogDebug("使用不跟随重定向的客户端")
|
||||
} else {
|
||||
if followRedirect {
|
||||
client = lib.Client
|
||||
Common.LogDebug("使用普通客户端")
|
||||
} else {
|
||||
client = lib.ClientNoRedirect
|
||||
}
|
||||
|
||||
// 检查客户端是否为空
|
||||
if client == nil {
|
||||
Common.LogDebug("错误: HTTP客户端为空")
|
||||
return fmt.Errorf("HTTP客户端未初始化"), "", CheckData
|
||||
return nil, ErrHTTPClientInit
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
Common.LogDebug("开始发送HTTP请求")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("HTTP请求失败: %v", err))
|
||||
return err, "https", CheckData
|
||||
// 特殊处理SSL/TLS相关错误
|
||||
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()
|
||||
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 {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应内容失败: %v", err))
|
||||
return err, "https", CheckData
|
||||
return result, fmt.Errorf("读取响应内容失败: %w", err)
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("成功读取响应内容,长度: %d", len(body)))
|
||||
result.Body = body
|
||||
|
||||
// 保存检查数据
|
||||
CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)})
|
||||
Common.LogDebug("已保存检查数据")
|
||||
// 提取标题
|
||||
if !utf8.Valid(body) {
|
||||
body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body)
|
||||
}
|
||||
result.Title = extractTitle(body)
|
||||
|
||||
// 处理非favicon请求
|
||||
var reurl string
|
||||
if flag != 2 {
|
||||
// 处理编码
|
||||
if !utf8.Valid(body) {
|
||||
body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body)
|
||||
}
|
||||
|
||||
// 获取页面信息
|
||||
title := gettitle(body)
|
||||
length := resp.Header.Get("Content-Length")
|
||||
if length == "" {
|
||||
length = fmt.Sprintf("%v", len(body))
|
||||
}
|
||||
|
||||
// 收集服务器信息
|
||||
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
|
||||
}
|
||||
|
||||
// 处理指纹信息 - 添加调试日志
|
||||
Common.LogDebug(fmt.Sprintf("保存结果前的指纹信息: %v", info.Infostr))
|
||||
|
||||
// 处理空指纹情况
|
||||
fingerprints := info.Infostr
|
||||
if len(fingerprints) == 1 && fingerprints[0] == "" {
|
||||
// 如果是只包含空字符串的数组,替换为空数组
|
||||
fingerprints = []string{}
|
||||
Common.LogDebug("检测到空指纹,已转换为空数组")
|
||||
}
|
||||
|
||||
// 保存扫描结果
|
||||
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": fingerprints, // 使用处理过的指纹信息
|
||||
},
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
Common.LogDebug(fmt.Sprintf("已保存结果,指纹信息: %v", fingerprints))
|
||||
|
||||
// 输出控制台日志
|
||||
logMsg := fmt.Sprintf("网站标题 %-25v 状态码:%-3v 长度:%-6v 标题:%v",
|
||||
resp.Request.URL, resp.StatusCode, length, title)
|
||||
if reurl != "" {
|
||||
logMsg += fmt.Sprintf(" 重定向地址: %s", reurl)
|
||||
}
|
||||
// 添加指纹信息到控制台日志
|
||||
if len(fingerprints) > 0 {
|
||||
logMsg += fmt.Sprintf(" 指纹:%v", fingerprints)
|
||||
}
|
||||
Common.LogSuccess(logMsg)
|
||||
if result.Length == "" {
|
||||
result.Length = fmt.Sprintf("%d", len(body))
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
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
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getRespBody 读取HTTP响应体内容
|
||||
func getRespBody(oResp *http.Response) ([]byte, error) {
|
||||
Common.LogDebug("开始读取响应体内容")
|
||||
// 读取HTTP响应体内容
|
||||
func readResponseBody(resp *http.Response) ([]byte, error) {
|
||||
var body []byte
|
||||
var reader io.Reader = resp.Body
|
||||
|
||||
// 处理gzip压缩的响应
|
||||
if oResp.Header.Get("Content-Encoding") == "gzip" {
|
||||
Common.LogDebug("检测到gzip压缩,开始解压")
|
||||
gr, err := gzip.NewReader(oResp.Body)
|
||||
if resp.Header.Get(contentEncoding) == gzipEncoding {
|
||||
gr, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("创建gzip解压器失败: %v", err))
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("创建gzip解压器失败: %w", err)
|
||||
}
|
||||
defer gr.Close()
|
||||
|
||||
// 循环读取解压内容
|
||||
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)))
|
||||
reader = gr
|
||||
}
|
||||
|
||||
// 读取内容
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应内容失败: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// gettitle 从HTML内容中提取网页标题
|
||||
func gettitle(body []byte) (title string) {
|
||||
Common.LogDebug("开始提取网页标题")
|
||||
|
||||
// 提取网页标题
|
||||
func extractTitle(body []byte) string {
|
||||
// 使用正则表达式匹配title标签内容
|
||||
re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>")
|
||||
find := re.FindSubmatch(body)
|
||||
|
||||
if len(find) > 1 {
|
||||
title = string(find[1])
|
||||
Common.LogDebug(fmt.Sprintf("找到原始标题: %s", title))
|
||||
title := string(find[1])
|
||||
|
||||
// 清理标题内容
|
||||
title = strings.TrimSpace(title) // 去除首尾空格
|
||||
title = strings.Replace(title, "\n", "", -1) // 去除换行
|
||||
title = strings.Replace(title, "\r", "", -1) // 去除回车
|
||||
title = strings.Replace(title, " ", " ", -1) // 替换HTML空格
|
||||
title = strings.TrimSpace(title)
|
||||
title = strings.Replace(title, "\n", "", -1)
|
||||
title = strings.Replace(title, "\r", "", -1)
|
||||
title = strings.Replace(title, " ", " ", -1)
|
||||
|
||||
// 截断过长的标题
|
||||
if len(title) > 100 {
|
||||
Common.LogDebug("标题超过100字符,进行截断")
|
||||
title = title[:100]
|
||||
if len(title) > maxTitleLength {
|
||||
title = title[:maxTitleLength]
|
||||
}
|
||||
|
||||
// 处理空标题
|
||||
if title == "" {
|
||||
Common.LogDebug("标题为空,使用双引号代替")
|
||||
title = "\"\""
|
||||
return emptyTitle
|
||||
}
|
||||
} else {
|
||||
Common.LogDebug("未找到标题标签")
|
||||
title = "无标题"
|
||||
|
||||
return title
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("最终标题: %s", title))
|
||||
return
|
||||
|
||||
return noTitleText
|
||||
}
|
||||
|
||||
// GetProtocol 检测目标主机的协议类型(HTTP/HTTPS),优先返回可用的协议
|
||||
func GetProtocol(host string, Timeout int64) (protocol string) {
|
||||
Common.LogDebug(fmt.Sprintf("开始检测主机协议 - 主机: %s, 超时: %d秒", host, Timeout))
|
||||
// 保存Web扫描结果
|
||||
func saveWebResult(info *Common.HostInfo, resp *WebResponse) {
|
||||
// 处理指纹信息
|
||||
fingerprints := info.Infostr
|
||||
if len(fingerprints) == 1 && fingerprints[0] == "" {
|
||||
fingerprints = []string{}
|
||||
}
|
||||
|
||||
// 默认使用http协议
|
||||
protocol = "http"
|
||||
// 准备服务器信息
|
||||
serverInfo := make(map[string]interface{})
|
||||
serverInfo["title"] = resp.Title
|
||||
serverInfo["length"] = resp.Length
|
||||
serverInfo["status_code"] = resp.StatusCode
|
||||
|
||||
timeoutDuration := time.Duration(Timeout) * time.Second
|
||||
// 添加响应头信息
|
||||
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, ":"+httpPort) {
|
||||
return httpProtocol, nil
|
||||
} else if strings.HasSuffix(host, ":"+httpsPort) {
|
||||
return httpsProtocol, nil
|
||||
}
|
||||
|
||||
timeoutDuration := time.Duration(timeout) * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
|
||||
defer cancel()
|
||||
|
||||
// 1. 根据标准端口快速判断协议
|
||||
if strings.HasSuffix(host, ":80") {
|
||||
Common.LogDebug("检测到标准HTTP端口,使用HTTP协议")
|
||||
return "http"
|
||||
} else if strings.HasSuffix(host, ":443") {
|
||||
Common.LogDebug("检测到标准HTTPS端口,使用HTTPS协议")
|
||||
return "https"
|
||||
}
|
||||
|
||||
// 2. 并发检测HTTP和HTTPS
|
||||
type protocolResult struct {
|
||||
name string
|
||||
success bool
|
||||
}
|
||||
|
||||
resultChan := make(chan protocolResult, 2)
|
||||
singleTimeout := timeoutDuration / 2 // 每个协议检测的超时时间减半
|
||||
// 并发检测HTTP和HTTPS
|
||||
resultChan := make(chan ProtocolResult, 2)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
// 检测HTTPS
|
||||
go func() {
|
||||
Common.LogDebug("开始检测HTTPS协议")
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS10,
|
||||
defer wg.Done()
|
||||
success := checkHTTPS(host, timeoutDuration/2)
|
||||
select {
|
||||
case resultChan <- ProtocolResult{httpsProtocol, success}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: singleTimeout,
|
||||
}
|
||||
|
||||
conn, err := tls.DialWithDialer(dialer, "tcp", host, tlsConfig)
|
||||
if err == nil {
|
||||
Common.LogDebug("HTTPS连接成功")
|
||||
conn.Close()
|
||||
resultChan <- protocolResult{"https", true}
|
||||
return
|
||||
}
|
||||
|
||||
// 分析TLS错误
|
||||
if err != nil {
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
// 这些错误可能表明服务器确实支持TLS,但有其他问题
|
||||
if strings.Contains(errMsg, "handshake failure") ||
|
||||
strings.Contains(errMsg, "certificate") ||
|
||||
strings.Contains(errMsg, "tls") ||
|
||||
strings.Contains(errMsg, "x509") ||
|
||||
strings.Contains(errMsg, "secure") {
|
||||
Common.LogDebug(fmt.Sprintf("TLS握手有错误但可能是HTTPS协议: %v", err))
|
||||
resultChan <- protocolResult{"https", true}
|
||||
return
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("HTTPS连接失败: %v", err))
|
||||
}
|
||||
resultChan <- protocolResult{"https", false}
|
||||
}()
|
||||
|
||||
// 检测HTTP
|
||||
go func() {
|
||||
Common.LogDebug("开始检测HTTP协议")
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", fmt.Sprintf("http://%s", host), nil)
|
||||
if err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("创建HTTP请求失败: %v", err))
|
||||
resultChan <- protocolResult{"http", false}
|
||||
return
|
||||
defer wg.Done()
|
||||
success := checkHTTP(ctx, host, timeoutDuration/2)
|
||||
select {
|
||||
case resultChan <- ProtocolResult{httpProtocol, success}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: singleTimeout,
|
||||
}).DialContext,
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse // 不跟随重定向
|
||||
},
|
||||
Timeout: singleTimeout,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
Common.LogDebug(fmt.Sprintf("HTTP连接成功,状态码: %d", resp.StatusCode))
|
||||
resultChan <- protocolResult{"http", true}
|
||||
return
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("标准HTTP请求失败: %v,尝试原始TCP连接", err))
|
||||
|
||||
// 尝试原始TCP连接和简单HTTP请求
|
||||
netConn, err := net.DialTimeout("tcp", host, singleTimeout)
|
||||
if err == nil {
|
||||
defer netConn.Close()
|
||||
netConn.SetDeadline(time.Now().Add(singleTimeout))
|
||||
|
||||
// 发送简单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(singleTimeout))
|
||||
n, err := netConn.Read(buf)
|
||||
if err == nil && n > 0 {
|
||||
response := string(buf[:n])
|
||||
if strings.Contains(response, "HTTP/") {
|
||||
Common.LogDebug("通过原始TCP连接确认HTTP协议")
|
||||
resultChan <- protocolResult{"http", true}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Common.LogDebug("原始TCP连接成功但HTTP响应无效")
|
||||
} else {
|
||||
Common.LogDebug(fmt.Sprintf("原始TCP连接失败: %v", err))
|
||||
}
|
||||
|
||||
resultChan <- protocolResult{"http", false}
|
||||
}()
|
||||
|
||||
// 3. 收集结果并决定使用哪种协议
|
||||
var httpsSuccess, httpSuccess bool
|
||||
// 确保所有goroutine正常退出
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 等待两个goroutine返回结果或超时
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.name == "https" {
|
||||
httpsSuccess = result.success
|
||||
Common.LogDebug(fmt.Sprintf("HTTPS检测结果: %v", httpsSuccess))
|
||||
} else if result.name == "http" {
|
||||
httpSuccess = result.success
|
||||
Common.LogDebug(fmt.Sprintf("HTTP检测结果: %v", httpSuccess))
|
||||
}
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("协议检测超时")
|
||||
break
|
||||
// 收集结果
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 决定使用哪种协议 - 优先使用HTTPS,如果HTTPS不可用则使用HTTP
|
||||
if httpsSuccess {
|
||||
Common.LogDebug("选择使用HTTPS协议")
|
||||
return "https"
|
||||
} else if httpSuccess {
|
||||
Common.LogDebug("选择使用HTTP协议")
|
||||
return "http"
|
||||
// 决定使用哪种协议 - 优先使用HTTPS
|
||||
if httpsResult != nil && httpsResult.Success {
|
||||
return httpsProtocol, nil
|
||||
} else if httpResult != nil && httpResult.Success {
|
||||
return httpProtocol, nil
|
||||
}
|
||||
|
||||
// 5. 如果两种协议都无法确认,保持默认值
|
||||
Common.LogDebug(fmt.Sprintf("无法确定协议,使用默认协议: %s", protocol))
|
||||
return
|
||||
// 默认使用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
@ -1,49 +1,9 @@
|
||||
# Fscan 2.0.0
|
||||
# Fscan
|
||||
[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参数,支持INFO,SUCCESS、ERROR、DEBUG参数,用于调试具体信息。
|
||||
|
||||
8、优化线程,现在会以更好的多线程运行
|
||||
|
||||
|
||||
|
||||
**新版由于对旧版代码进行了全面的重构,难免会有Bug,请在遇到Bug时提交Issue,会尽快修复处理,感谢。**
|
||||
|
||||
**欢迎提交新的插件模块,目前插件为快速热插拔形式,适用于简易开发。**
|
||||
|
||||
# 0x01 简介
|
||||
|
||||
一款功能丰富的内网综合扫描工具,提供一键自动化、全方位的漏洞扫描能力。
|
||||
|
||||
## 主要功能
|
||||
|
||||
- 主机存活探测:快速识别内网中的活跃主机
|
||||
- 端口扫描:全面检测目标主机开放端口
|
||||
- 服务爆破:支持对常见服务进行密码爆破测试
|
||||
- 漏洞利用:集成MS17-010等高危漏洞检测
|
||||
- Redis利用:支持批量写入公钥进行权限获取
|
||||
- 系统信息收集:可读取Windows网卡信息
|
||||
- Web应用检测:
|
||||
- Web指纹识别
|
||||
- Web漏洞扫描
|
||||
- 域环境探测:
|
||||
- NetBIOS信息获取
|
||||
- 域控制器识别
|
||||
- 后渗透功能:支持通过计划任务实现反弹shell
|
||||
一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。
|
||||
|
||||
# 0x02 主要功能
|
||||
## 1. 信息搜集
|
||||
@ -73,119 +33,19 @@
|
||||
- 扫描结果存储:将所有检测结果保存至文件,便于后续分析
|
||||
|
||||
# 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 捐赠
|
||||
如果你觉得这个项目对你有帮助,你可以请作者喝饮料🍹 [点我](image/sponsor.png)
|
||||
|
||||
# 0x09 参考链接
|
||||
# 0x09 安全培训
|
||||

|
||||
学网络安全,就选玲珑安全!专业漏洞挖掘,精准定位风险;助力技能提升,塑造安全精英;玲珑安全,为您的数字世界保驾护航!
|
||||
在线免费学习网络安全,涵盖src漏洞挖掘,0基础安全入门。适用于小白,进阶,高手: https://space.bilibili.com/602205041
|
||||
玲珑安全往期学员报喜🎉: https://www.ifhsec.com/list.html
|
||||
玲珑安全漏洞挖掘培训学习联系微信: linglongsec
|
||||
|
||||
# 0x10 参考链接
|
||||
https://github.com/Adminisme/ServerScan
|
||||
https://github.com/netxfly/x-crack
|
||||
https://github.com/hack2fun/Gscan
|
||||
https://github.com/k8gege/LadonGo
|
||||
https://github.com/jjf012/gopoc
|
||||
|
||||
|
||||
# 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
|
||||
|
@ -59,7 +59,7 @@ func InfoCheck(Url string, CheckData *[]CheckDatas) []string {
|
||||
// 输出结果
|
||||
if len(matchedInfos) > 0 {
|
||||
result := fmt.Sprintf("发现指纹 目标: %-25v 指纹: %s", Url, matchedInfos)
|
||||
Common.LogSuccess(result)
|
||||
Common.LogInfo(result)
|
||||
return matchedInfos
|
||||
}
|
||||
|
||||
|
@ -1,90 +1,171 @@
|
||||
package WebScan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"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
|
||||
var Pocs embed.FS
|
||||
var once sync.Once
|
||||
var AllPocs []*lib.Poc
|
||||
var pocsFS embed.FS
|
||||
var (
|
||||
once sync.Once
|
||||
allPocs []*lib.Poc
|
||||
)
|
||||
|
||||
// WebScan 执行Web漏洞扫描
|
||||
func WebScan(info *Common.HostInfo) {
|
||||
once.Do(initpoc)
|
||||
// 初始化POC
|
||||
once.Do(initPocs)
|
||||
|
||||
var pocinfo = Common.Pocinfo
|
||||
|
||||
// 自动构建URL
|
||||
if info.Url == "" {
|
||||
info.Url = fmt.Sprintf("http://%s:%s", info.Host, info.Ports)
|
||||
// 验证输入
|
||||
if info == nil {
|
||||
Common.LogError("无效的扫描目标")
|
||||
return
|
||||
}
|
||||
|
||||
urlParts := strings.Split(info.Url, "/")
|
||||
|
||||
// 检查切片长度并构建目标URL
|
||||
if len(urlParts) >= 3 {
|
||||
pocinfo.Target = strings.Join(urlParts[:3], "/")
|
||||
} else {
|
||||
pocinfo.Target = info.Url
|
||||
if len(allPocs) == 0 {
|
||||
Common.LogError("POC加载失败,无法执行扫描")
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
Common.LogDebug("直接调用WebPoc,执行所有POC")
|
||||
Execute(pocinfo)
|
||||
} else {
|
||||
// 根据指纹信息选择性执行POC
|
||||
if len(info.Infostr) > 0 {
|
||||
for _, infostr := range info.Infostr {
|
||||
pocinfo.PocName = lib.CheckInfoPoc(infostr)
|
||||
if pocinfo.PocName != "" {
|
||||
Common.LogDebug(fmt.Sprintf("根据指纹 %s 执行对应POC", infostr))
|
||||
Execute(pocinfo)
|
||||
}
|
||||
}
|
||||
} else if pocinfo.PocName != "" {
|
||||
// 指定了特定的POC
|
||||
Common.LogDebug(fmt.Sprintf("执行指定POC: %s", pocinfo.PocName))
|
||||
Execute(pocinfo)
|
||||
}
|
||||
// 使用带超时的上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// 根据扫描策略执行POC
|
||||
if Common.Pocinfo.PocName == "" && len(info.Infostr) == 0 {
|
||||
// 执行所有POC
|
||||
executePOCs(ctx, Common.PocInfo{Target: target})
|
||||
} else if len(info.Infostr) > 0 {
|
||||
// 基于指纹信息执行POC
|
||||
scanByFingerprints(ctx, target, info.Infostr)
|
||||
} else if Common.Pocinfo.PocName != "" {
|
||||
// 基于指定POC名称执行
|
||||
executePOCs(ctx, Common.PocInfo{Target: target, PocName: Common.Pocinfo.PocName})
|
||||
}
|
||||
}
|
||||
|
||||
// Execute 执行具体的POC检测
|
||||
func Execute(PocInfo Common.PocInfo) {
|
||||
Common.LogDebug(fmt.Sprintf("开始执行POC检测,目标: %s", PocInfo.Target))
|
||||
// buildTargetURL 构建规范的目标URL
|
||||
func buildTargetURL(info *Common.HostInfo) (string, error) {
|
||||
// 自动构建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格式正确
|
||||
if !strings.HasPrefix(PocInfo.Target, "http://") && !strings.HasPrefix(PocInfo.Target, "https://") {
|
||||
PocInfo.Target = "http://" + PocInfo.Target
|
||||
if !hasProtocolPrefix(pocInfo.Target) {
|
||||
pocInfo.Target = protocolHTTP + pocInfo.Target
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
_, err := url.Parse(PocInfo.Target)
|
||||
// 验证URL
|
||||
_, err := url.Parse(pocInfo.Target)
|
||||
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
|
||||
}
|
||||
|
||||
// 创建基础HTTP请求
|
||||
req, err := http.NewRequest("GET", PocInfo.Target, nil)
|
||||
// 创建基础请求
|
||||
req, err := createBaseRequest(ctx, pocInfo.Target)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("初始化请求失败 %v: %v", PocInfo.Target, err))
|
||||
Common.LogError(fmt.Sprintf("创建HTTP请求失败: %v", err))
|
||||
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("Accept", Common.Accept)
|
||||
@ -93,75 +174,150 @@ func Execute(PocInfo Common.PocInfo) {
|
||||
req.Header.Set("Cookie", Common.Cookie)
|
||||
}
|
||||
|
||||
// 根据名称筛选POC并执行
|
||||
pocs := filterPoc(PocInfo.PocName)
|
||||
Common.LogDebug(fmt.Sprintf("筛选到的POC数量: %d", len(pocs)))
|
||||
lib.CheckMultiPoc(req, pocs, Common.PocNum)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// initpoc 初始化POC加载
|
||||
func initpoc() {
|
||||
Common.LogDebug("开始初始化POC")
|
||||
// initPocs 初始化并加载POC
|
||||
func initPocs() {
|
||||
allPocs = make([]*lib.Poc, 0)
|
||||
|
||||
if Common.PocPath == "" {
|
||||
Common.LogDebug("从内置目录加载POC")
|
||||
// 从嵌入的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)))
|
||||
loadEmbeddedPocs()
|
||||
} else {
|
||||
// 从指定目录加载POC
|
||||
Common.LogSuccess(fmt.Sprintf("从目录加载POC: %s", Common.PocPath))
|
||||
err := filepath.Walk(Common.PocPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() && (strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")) {
|
||||
if poc, err := lib.LoadPocbyPath(path); err == nil && poc != nil {
|
||||
AllPocs = append(AllPocs, poc)
|
||||
} else if err != nil {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("加载外部POC失败: %v", err))
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("外部POC加载完成,共加载 %d 个", len(AllPocs)))
|
||||
loadExternalPocs(Common.PocPath)
|
||||
}
|
||||
}
|
||||
|
||||
// filterPoc 根据POC名称筛选
|
||||
func filterPoc(pocname string) []*lib.Poc {
|
||||
Common.LogDebug(fmt.Sprintf("开始筛选POC,筛选条件: %s", pocname))
|
||||
|
||||
if pocname == "" {
|
||||
Common.LogDebug(fmt.Sprintf("未指定POC名称,返回所有POC: %d 个", len(AllPocs)))
|
||||
return AllPocs
|
||||
// loadEmbeddedPocs 加载内置POC
|
||||
func loadEmbeddedPocs() {
|
||||
entries, err := pocsFS.ReadDir("pocs")
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("加载内置POC目录失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 收集所有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
|
||||
for _, poc := range AllPocs {
|
||||
if strings.Contains(poc.Name, pocname) {
|
||||
for _, poc := range allPocs {
|
||||
if poc != nil && strings.Contains(strings.ToLower(poc.Name), searchName) {
|
||||
matchedPocs = append(matchedPocs, poc)
|
||||
}
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("POC筛选完成,匹配到 %d 个", len(matchedPocs)))
|
||||
|
||||
return matchedPocs
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
name: poc-yaml-etcd-v3-unauth
|
||||
name: ETCD V3未授权
|
||||
rules:
|
||||
- method: GET
|
||||
path: /version
|
||||
- method: POST
|
||||
path: /v3/kv/range
|
||||
follow_redirects: false
|
||||
Content-Type: application/json;charset=utf-8
|
||||
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:
|
||||
author: rj45(https://github.com/INT2ECALL)
|
||||
links:
|
||||
|
@ -3,19 +3,19 @@ groups:
|
||||
spring1:
|
||||
- method: GET
|
||||
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:
|
||||
- method: GET
|
||||
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:
|
||||
- method: GET
|
||||
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:
|
||||
- method: GET
|
||||
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:
|
||||
author: iak3ec(https://github.com/nu0l)
|
||||
links:
|
||||
|
2
go.mod
@ -7,7 +7,6 @@ require (
|
||||
github.com/denisenkom/go-mssqldb v0.12.3
|
||||
github.com/fatih/color v1.18.0
|
||||
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/gocql/gocql v1.7.0
|
||||
github.com/google/cel-go v0.13.0
|
||||
@ -25,6 +24,7 @@ require (
|
||||
github.com/tomatome/grdp v0.0.0-20211231062539-be8adab7eaf3
|
||||
golang.org/x/crypto v0.31.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/text v0.21.0
|
||||
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c
|
||||
|
13
go.sum
@ -23,7 +23,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
||||
github.com/IBM/sarama v1.43.3 h1:Yj6L2IaNvb2mRBop39N7mmJAHBVY3dTPncr3qGVkxPA=
|
||||
github.com/IBM/sarama v1.43.3/go.mod h1:FVIRaLrhK3Cla/9FfRF5X9Zua2KpS3SYIXxhac1H+FQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/Ullaakut/nmap v2.0.2+incompatible/go.mod h1:fkC066hwfcoKwlI7DS2ARTggSVtBTZYCjVH1TzuTMaQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
@ -42,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/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
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/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
@ -86,9 +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-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-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-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
@ -134,7 +129,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4/go.mod h1:Pw1H1OjSNHiqeuxAduB1BKYXIwFtsyrY47nEqSgEiCM=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
@ -241,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-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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
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/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
@ -310,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/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.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/shadow1ng/grdp v1.0.3 h1:d29xgHDK4aa3ljm/e/yThdJxygf26zJyRPBunrWT65k=
|
||||
github.com/shadow1ng/grdp v1.0.3/go.mod h1:3ZMSLWUvPOwoRr6IwpAQCzKbLEZqT80sbyxxe6YgcTg=
|
||||
@ -401,13 +392,11 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20181026062114-a27dd33d354d/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
@ -493,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-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.1.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -546,7 +534,6 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
BIN
image/5.png
Normal file
After Width: | Height: | Size: 147 KiB |
BIN
image/gpt-4o/4o-1.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
image/gpt-4o/4o-2.png
Normal file
After Width: | Height: | Size: 1.4 MiB |
BIN
image/gpt-4o/4o-3.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
image/gpt-4o/4o-4.png
Normal file
After Width: | Height: | Size: 210 KiB |
BIN
image/gpt-4o/4o-5.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
image/gpt-4o/4o-6.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
image/gpt-4o/4o-7.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
image/gpt-4o/4o-8.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
image/gpt-4o/final.png
Normal file
After Width: | Height: | Size: 198 KiB |
10
main.go
@ -2,9 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/Core"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -12,14 +13,19 @@ func main() {
|
||||
|
||||
var Info Common.HostInfo
|
||||
Common.Flag(&Info)
|
||||
|
||||
// 解析 CLI 参数
|
||||
if err := Common.Parse(&Info); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 初始化输出系统,如果失败则直接退出
|
||||
if err := Common.InitOutput(); err != nil {
|
||||
Common.LogError(fmt.Sprintf("初始化输出系统失败: %v", err))
|
||||
os.Exit(1) // 关键修改:初始化失败时直接退出
|
||||
os.Exit(1)
|
||||
}
|
||||
defer Common.CloseOutput()
|
||||
|
||||
// 执行 CLI 扫描逻辑
|
||||
Core.Scan(Info)
|
||||
}
|
||||
|