mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
Merge pull request #582 from shadow1ng/feature/sdk-library
Add embeddable scanner SDK
This commit is contained in:
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
result.txt
|
result.txt
|
||||||
result.json
|
result.json
|
||||||
main
|
/main
|
||||||
.idea
|
.idea
|
||||||
fscan.exe
|
fscan.exe
|
||||||
fscan
|
/fscan
|
||||||
fscanapi.csv
|
fscanapi.csv
|
||||||
|
|
||||||
# IDE files / IDE 文件
|
# IDE files / IDE 文件
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ func SetLanguage(l string) {
|
|||||||
localizer = i18n.NewLocalizer(bundle, lang, FallbackLanguage)
|
localizer = i18n.NewLocalizer(bundle, lang, FallbackLanguage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLanguage returns the currently configured language.
|
||||||
|
func GetLanguage() string {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
|
||||||
// GetText 获取国际化文本(无参数)
|
// GetText 获取国际化文本(无参数)
|
||||||
func GetText(key string) string {
|
func GetText(key string) string {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
|
|||||||
+40
-1
@@ -16,9 +16,14 @@ import (
|
|||||||
var (
|
var (
|
||||||
globalLogger *logging.Logger
|
globalLogger *logging.Logger
|
||||||
loggerOnce sync.Once
|
loggerOnce sync.Once
|
||||||
|
loggerMu sync.Mutex
|
||||||
|
silentLoggerRefs int
|
||||||
)
|
)
|
||||||
|
|
||||||
func getGlobalLogger() *logging.Logger {
|
func getGlobalLogger() *logging.Logger {
|
||||||
|
loggerMu.Lock()
|
||||||
|
defer loggerMu.Unlock()
|
||||||
|
|
||||||
loggerOnce.Do(func() {
|
loggerOnce.Do(func() {
|
||||||
fv := GetFlagVars()
|
fv := GetFlagVars()
|
||||||
level := getLogLevelFromString(fv.LogLevel)
|
level := getLogLevelFromString(fv.LogLevel)
|
||||||
@@ -27,7 +32,7 @@ func getGlobalLogger() *logging.Logger {
|
|||||||
EnableColor: !fv.NoColor,
|
EnableColor: !fv.NoColor,
|
||||||
SlowOutput: false,
|
SlowOutput: false,
|
||||||
ShowProgress: !fv.DisableProgress,
|
ShowProgress: !fv.DisableProgress,
|
||||||
Silent: fv.Silent,
|
Silent: fv.Silent || silentLoggerRefs > 0,
|
||||||
StartTime: GetGlobalState().GetStartTime(),
|
StartTime: GetGlobalState().GetStartTime(),
|
||||||
}
|
}
|
||||||
if fv.Debug {
|
if fv.Debug {
|
||||||
@@ -84,6 +89,40 @@ func LogError(errMsg string) { getGlobalLogger().Error(errMsg) }
|
|||||||
|
|
||||||
// CloseLogger 关闭日志系统,释放文件资源
|
// CloseLogger 关闭日志系统,释放文件资源
|
||||||
func CloseLogger() {
|
func CloseLogger() {
|
||||||
|
loggerMu.Lock()
|
||||||
|
defer loggerMu.Unlock()
|
||||||
|
closeLoggerLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushSilentLogger suppresses process-wide legacy log output until the returned
|
||||||
|
// restore function is called. It is reference counted so concurrent embedded
|
||||||
|
// scans can overlap safely.
|
||||||
|
func PushSilentLogger() func() {
|
||||||
|
loggerMu.Lock()
|
||||||
|
silentLoggerRefs++
|
||||||
|
resetLoggerLocked()
|
||||||
|
loggerMu.Unlock()
|
||||||
|
|
||||||
|
var once sync.Once
|
||||||
|
return func() {
|
||||||
|
once.Do(func() {
|
||||||
|
loggerMu.Lock()
|
||||||
|
if silentLoggerRefs > 0 {
|
||||||
|
silentLoggerRefs--
|
||||||
|
}
|
||||||
|
resetLoggerLocked()
|
||||||
|
loggerMu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetLoggerLocked() {
|
||||||
|
closeLoggerLocked()
|
||||||
|
globalLogger = nil
|
||||||
|
loggerOnce = sync.Once{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeLoggerLocked() {
|
||||||
if globalLogger != nil {
|
if globalLogger != nil {
|
||||||
globalLogger.Close()
|
globalLogger.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
+124
-18
@@ -4,25 +4,31 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common/proxy"
|
|
||||||
"github.com/shadow1ng/fscan/common/i18n"
|
"github.com/shadow1ng/fscan/common/i18n"
|
||||||
|
"github.com/shadow1ng/fscan/common/output"
|
||||||
|
"github.com/shadow1ng/fscan/common/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ResultSink receives structured scan results for one scan session.
|
||||||
|
type ResultSink func(result *output.ScanResult) error
|
||||||
|
|
||||||
// ScanSession 封装单次扫描的全部上下文
|
// ScanSession 封装单次扫描的全部上下文
|
||||||
// 一次扫描一个 session,并发扫描各自独立
|
// 一次扫描一个 session,并发扫描各自独立
|
||||||
type ScanSession struct {
|
type ScanSession struct {
|
||||||
Config *Config // 不可变,创建后只读
|
Config *Config // 不可变,创建后只读
|
||||||
State *State // 可变,原子操作,每会话独立
|
State *State // 可变,原子操作,每会话独立
|
||||||
Params *FlagVars // 原始参数,只读
|
Params *FlagVars // 原始参数,只读
|
||||||
|
ResultSink ResultSink // 可选,覆盖全局输出
|
||||||
|
|
||||||
// 每会话 dialer(懒初始化,取决于代理配置)
|
// 每会话 dialer(按 timeout 懒初始化,取决于代理配置)
|
||||||
dialerOnce sync.Once
|
dialerMu sync.Mutex
|
||||||
dialer proxy.Dialer
|
dialers map[time.Duration]proxy.Dialer
|
||||||
dialerErr error
|
dialerErrs map[time.Duration]error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScanSession 从已构建的 Config、State 和 FlagVars 创建会话
|
// NewScanSession 从已构建的 Config、State 和 FlagVars 创建会话
|
||||||
@@ -34,18 +40,66 @@ func NewScanSession(config *Config, state *State, params *FlagVars) *ScanSession
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveResult saves a scan result through the session sink if present, otherwise
|
||||||
|
// falls back to the process-wide output pipeline used by the CLI.
|
||||||
|
func (s *ScanSession) SaveResult(result *output.ScanResult) error {
|
||||||
|
if s != nil && s.ResultSink != nil {
|
||||||
|
return s.ResultSink(result)
|
||||||
|
}
|
||||||
|
return SaveResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScanSession) loggingEnabled() bool {
|
||||||
|
return s == nil || s.Config == nil || !s.Config.Output.Silent
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogDebug writes through the session's logging policy.
|
||||||
|
func (s *ScanSession) LogDebug(msg string) {
|
||||||
|
if s.loggingEnabled() {
|
||||||
|
LogDebug(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogInfo writes through the session's logging policy.
|
||||||
|
func (s *ScanSession) LogInfo(msg string) {
|
||||||
|
if s.loggingEnabled() {
|
||||||
|
LogInfo(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogSuccess writes through the session's logging policy.
|
||||||
|
func (s *ScanSession) LogSuccess(result string) {
|
||||||
|
if s.loggingEnabled() {
|
||||||
|
LogSuccess(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogVuln writes through the session's logging policy.
|
||||||
|
func (s *ScanSession) LogVuln(result string) {
|
||||||
|
if s.loggingEnabled() {
|
||||||
|
LogVuln(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogError writes through the session's logging policy.
|
||||||
|
func (s *ScanSession) LogError(errMsg string) {
|
||||||
|
if s.loggingEnabled() {
|
||||||
|
LogError(errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DialTCP 创建 TCP 连接,内含限速检查、代理、计数
|
// DialTCP 创建 TCP 连接,内含限速检查、代理、计数
|
||||||
func (s *ScanSession) DialTCP(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error) {
|
func (s *ScanSession) DialTCP(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error) {
|
||||||
// 检查发包限制
|
// 检查发包限制
|
||||||
if ok, err := CanSendPacketWith(s.Config, s.State); !ok {
|
if ok, err := CanSendPacketWith(s.Config, s.State); !ok {
|
||||||
LogError(fmt.Sprintf("TCP连接 %s 受限: %s", address, err.Error()))
|
s.LogError(fmt.Sprintf("TCP连接 %s 受限: %s", address, err.Error()))
|
||||||
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", err.Error()))
|
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 dialer
|
// 获取 dialer
|
||||||
dialer, err := s.getDialer()
|
dialer, err := s.getDialer(timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogError(fmt.Sprintf("获取代理拨号器失败: %v", err))
|
s.LogError(fmt.Sprintf("获取代理拨号器失败: %v", err))
|
||||||
s.State.IncrementTCPFailedPacketCount()
|
s.State.IncrementTCPFailedPacketCount()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -53,7 +107,7 @@ func (s *ScanSession) DialTCP(ctx context.Context, network, address string, time
|
|||||||
conn, err := dialer.DialContext(ctx, network, address)
|
conn, err := dialer.DialContext(ctx, network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.State.IncrementTCPFailedPacketCount()
|
s.State.IncrementTCPFailedPacketCount()
|
||||||
LogDebug(fmt.Sprintf("连接 %s 失败: %v", address, err))
|
s.LogDebug(fmt.Sprintf("连接 %s 失败: %v", address, err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,18 +120,70 @@ func (s *ScanSession) DialTCP(ctx context.Context, network, address string, time
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ScanSession) getDialer() (proxy.Dialer, error) {
|
// HTTPDo executes an HTTP request with the session's packet limits and counters.
|
||||||
s.dialerOnce.Do(func() {
|
func (s *ScanSession) HTTPDo(client *http.Client, req *http.Request) (*http.Response, error) {
|
||||||
cfg := s.createProxyConfig()
|
if ok, err := CanSendPacketWith(s.Config, s.State); !ok {
|
||||||
manager := proxy.NewProxyManager(cfg)
|
s.LogError(fmt.Sprintf("HTTP请求 %s 受限: %s", req.URL.String(), err.Error()))
|
||||||
s.dialer, s.dialerErr = manager.GetDialer()
|
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", err.Error()))
|
||||||
})
|
}
|
||||||
return s.dialer, s.dialerErr
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
s.State.IncrementTCPFailedPacketCount()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.State.IncrementTCPSuccessPacketCount()
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ScanSession) createProxyConfig() *proxy.ProxyConfig {
|
// ProxyEnabled reports whether this scan session uses a network proxy.
|
||||||
|
func (s *ScanSession) ProxyEnabled() bool {
|
||||||
|
if s == nil || s.Config == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.Config.Network.Socks5Proxy != "" || s.Config.Network.HTTPProxy != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSOCKS5Proxy reports whether this scan session uses SOCKS5.
|
||||||
|
func (s *ScanSession) IsSOCKS5Proxy() bool {
|
||||||
|
return s != nil && s.Config != nil && s.Config.Network.Socks5Proxy != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyReliable reports whether the session proxy should be treated as reliable.
|
||||||
|
func (s *ScanSession) ProxyReliable() bool {
|
||||||
|
if !s.ProxyEnabled() || !s.IsSOCKS5Proxy() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return proxy.IsProxyReliable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScanSession) getDialer(timeout time.Duration) (proxy.Dialer, error) {
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = s.Config.Timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
s.dialerMu.Lock()
|
||||||
|
defer s.dialerMu.Unlock()
|
||||||
|
|
||||||
|
if s.dialers == nil {
|
||||||
|
s.dialers = make(map[time.Duration]proxy.Dialer)
|
||||||
|
s.dialerErrs = make(map[time.Duration]error)
|
||||||
|
}
|
||||||
|
if dialer, ok := s.dialers[timeout]; ok {
|
||||||
|
return dialer, s.dialerErrs[timeout]
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := s.createProxyConfig(timeout)
|
||||||
|
manager := proxy.NewProxyManager(cfg)
|
||||||
|
dialer, err := manager.GetDialer()
|
||||||
|
s.dialers[timeout] = dialer
|
||||||
|
s.dialerErrs[timeout] = err
|
||||||
|
return dialer, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScanSession) createProxyConfig(timeout time.Duration) *proxy.ProxyConfig {
|
||||||
cfg := proxy.DefaultProxyConfig()
|
cfg := proxy.DefaultProxyConfig()
|
||||||
cfg.Timeout = s.Config.Timeout
|
cfg.Timeout = timeout
|
||||||
cfg.LocalAddr = s.Config.Network.Iface
|
cfg.LocalAddr = s.Config.Network.Iface
|
||||||
|
|
||||||
// 优先 SOCKS5
|
// 优先 SOCKS5
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestScanSessionLogMethodsHonorSilentConfig(t *testing.T) {
|
||||||
|
loggerMu.Lock()
|
||||||
|
silentLoggerRefs = 0
|
||||||
|
resetLoggerLocked()
|
||||||
|
loggerMu.Unlock()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
loggerMu.Lock()
|
||||||
|
silentLoggerRefs = 0
|
||||||
|
resetLoggerLocked()
|
||||||
|
loggerMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg := NewConfig()
|
||||||
|
cfg.Output.Silent = true
|
||||||
|
session := NewScanSession(cfg, NewState(), &FlagVars{})
|
||||||
|
|
||||||
|
session.LogDebug("debug")
|
||||||
|
session.LogInfo("info")
|
||||||
|
session.LogSuccess("success")
|
||||||
|
session.LogVuln("vuln")
|
||||||
|
session.LogError("error")
|
||||||
|
|
||||||
|
loggerMu.Lock()
|
||||||
|
defer loggerMu.Unlock()
|
||||||
|
if globalLogger != nil {
|
||||||
|
t.Fatal("silent session log methods initialized global logger")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanSessionDialerCacheIsTimeoutAware(t *testing.T) {
|
||||||
|
cfg := NewConfig()
|
||||||
|
cfg.Timeout = 5 * time.Second
|
||||||
|
session := NewScanSession(cfg, NewState(), &FlagVars{})
|
||||||
|
|
||||||
|
shortTimeout := 100 * time.Millisecond
|
||||||
|
longTimeout := 2 * time.Second
|
||||||
|
|
||||||
|
shortDialer, err := session.getDialer(shortTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
shortDialerAgain, err := session.getDialer(shortTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
longDialer, err := session.getDialer(longTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if shortDialer != shortDialerAgain {
|
||||||
|
t.Fatal("same timeout should reuse the session dialer")
|
||||||
|
}
|
||||||
|
if shortDialer == longDialer {
|
||||||
|
t.Fatal("different timeouts should not share one session dialer")
|
||||||
|
}
|
||||||
|
if got := session.createProxyConfig(shortTimeout).Timeout; got != shortTimeout {
|
||||||
|
t.Fatalf("proxy timeout = %v, want %v", got, shortTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanSessionHTTPDoUsesSessionState(t *testing.T) {
|
||||||
|
previousState := GetGlobalState()
|
||||||
|
globalState := NewState()
|
||||||
|
SetGlobalState(globalState)
|
||||||
|
t.Cleanup(func() { SetGlobalState(previousState) })
|
||||||
|
|
||||||
|
sessionState := NewState()
|
||||||
|
session := NewScanSession(NewConfig(), sessionState, &FlagVars{})
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusNoContent,
|
||||||
|
Body: io.NopCloser(strings.NewReader("")),
|
||||||
|
Header: make(http.Header),
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodHead, "http://example.com", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := session.HTTPDo(client, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
|
if got := sessionState.GetTCPSuccessPacketCount(); got != 1 {
|
||||||
|
t.Fatalf("session TCP success count = %d, want 1", got)
|
||||||
|
}
|
||||||
|
if got := globalState.GetTCPSuccessPacketCount(); got != 0 {
|
||||||
|
t.Fatalf("global TCP success count = %d, want 0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanSessionProxyStateComesFromConfig(t *testing.T) {
|
||||||
|
direct := NewScanSession(NewConfig(), NewState(), &FlagVars{})
|
||||||
|
if direct.ProxyEnabled() {
|
||||||
|
t.Fatal("direct session should not report proxy enabled")
|
||||||
|
}
|
||||||
|
if direct.IsSOCKS5Proxy() {
|
||||||
|
t.Fatal("direct session should not report SOCKS5")
|
||||||
|
}
|
||||||
|
if !direct.ProxyReliable() {
|
||||||
|
t.Fatal("direct session should be reliable")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpCfg := NewConfig()
|
||||||
|
httpCfg.Network.HTTPProxy = "http://127.0.0.1:8080"
|
||||||
|
httpSession := NewScanSession(httpCfg, NewState(), &FlagVars{})
|
||||||
|
if !httpSession.ProxyEnabled() {
|
||||||
|
t.Fatal("HTTP proxy session should report proxy enabled")
|
||||||
|
}
|
||||||
|
if httpSession.IsSOCKS5Proxy() {
|
||||||
|
t.Fatal("HTTP proxy session should not report SOCKS5")
|
||||||
|
}
|
||||||
|
if !httpSession.ProxyReliable() {
|
||||||
|
t.Fatal("HTTP proxy session should be reliable")
|
||||||
|
}
|
||||||
|
|
||||||
|
socksCfg := NewConfig()
|
||||||
|
socksCfg.Network.Socks5Proxy = "127.0.0.1:1080"
|
||||||
|
socksSession := NewScanSession(socksCfg, NewState(), &FlagVars{})
|
||||||
|
if !socksSession.ProxyEnabled() {
|
||||||
|
t.Fatal("SOCKS5 proxy session should report proxy enabled")
|
||||||
|
}
|
||||||
|
if !socksSession.IsSOCKS5Proxy() {
|
||||||
|
t.Fatal("SOCKS5 proxy session should report SOCKS5")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
return f(req)
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ func (s *AliveScanStrategy) Description() string {
|
|||||||
func (s *AliveScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
func (s *AliveScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||||
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
|
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
|
||||||
if info.Host == "" && session.Params.HostsFile == "" {
|
if info.Host == "" && session.Params.HostsFile == "" {
|
||||||
common.LogError(i18n.GetText("parse_error_target_empty"))
|
session.LogError(i18n.GetText("parse_error_target_empty"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ func (s *AliveScanStrategy) Execute(ctx context.Context, session *common.ScanSes
|
|||||||
s.performAliveScan(ctx, info, session)
|
s.performAliveScan(ctx, info, session)
|
||||||
|
|
||||||
// 输出统计信息
|
// 输出统计信息
|
||||||
s.outputStats()
|
s.outputStats(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// performAliveScan 执行存活探测
|
// performAliveScan 执行存活探测
|
||||||
@@ -73,12 +73,12 @@ func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.Ho
|
|||||||
// 解析目标主机
|
// 解析目标主机
|
||||||
hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
|
hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(i18n.Tr("parse_target_failed", err))
|
session.LogError(i18n.Tr("parse_target_failed", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(hosts) == 0 {
|
if len(hosts) == 0 {
|
||||||
common.LogError(i18n.GetText("parse_error_no_hosts"))
|
session.LogError(i18n.GetText("parse_error_no_hosts"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +87,6 @@ func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.Ho
|
|||||||
s.stats.AliveHosts = 0
|
s.stats.AliveHosts = 0
|
||||||
s.stats.DeadHosts = 0
|
s.stats.DeadHosts = 0
|
||||||
|
|
||||||
|
|
||||||
// 执行存活检测
|
// 执行存活检测
|
||||||
aliveList := CheckLive(ctx, hosts, false, session) // 使用ICMP探测
|
aliveList := CheckLive(ctx, hosts, false, session) // 使用ICMP探测
|
||||||
|
|
||||||
@@ -103,10 +102,10 @@ func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.Ho
|
|||||||
}
|
}
|
||||||
|
|
||||||
// outputStats 输出统计信息(精简版)
|
// outputStats 输出统计信息(精简版)
|
||||||
func (s *AliveScanStrategy) outputStats() {
|
func (s *AliveScanStrategy) outputStats(session *common.ScanSession) {
|
||||||
// 只输出存活主机列表,不输出冗余统计
|
// 只输出存活主机列表,不输出冗余统计
|
||||||
for _, host := range s.stats.AliveHostList {
|
for _, host := range s.stats.AliveHostList {
|
||||||
common.LogSuccess(fmt.Sprintf("alive %s", host))
|
session.LogSuccess(fmt.Sprintf("alive %s", host))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHos
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显式指定插件时,尊重调用方选择,不再强制使用插件默认端口过滤。
|
||||||
|
if isCustomMode {
|
||||||
|
return b.isPluginPassesFilterType(pluginName, isCustomMode, config)
|
||||||
|
}
|
||||||
|
|
||||||
// 检查端口匹配和过滤器类型
|
// 检查端口匹配和过滤器类型
|
||||||
return b.isPluginApplicableToPortWithHost(pluginName, targetHost, targetPort) && b.isPluginPassesFilterType(pluginName, isCustomMode, config)
|
return b.isPluginApplicableToPortWithHost(pluginName, targetHost, targetPort) && b.isPluginPassesFilterType(pluginName, isCustomMode, config)
|
||||||
}
|
}
|
||||||
@@ -165,7 +170,7 @@ func (b *BaseScanStrategy) isPluginPassesFilterType(pluginName string, isCustomM
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LogPluginInfo 输出插件信息
|
// LogPluginInfo 输出插件信息
|
||||||
func (b *BaseScanStrategy) LogPluginInfo(config *common.Config) {
|
func (b *BaseScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) {
|
||||||
allPlugins, isCustomMode := b.GetPlugins(config)
|
allPlugins, isCustomMode := b.GetPlugins(config)
|
||||||
|
|
||||||
var prefix string
|
var prefix string
|
||||||
@@ -184,6 +189,7 @@ func (b *BaseScanStrategy) LogPluginInfo(config *common.Config) {
|
|||||||
_ = allPlugins
|
_ = allPlugins
|
||||||
_ = isCustomMode
|
_ = isCustomMode
|
||||||
_ = prefix
|
_ = prefix
|
||||||
|
_ = session
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatPluginList 格式化插件列表(超过5个时精简显示)
|
// formatPluginList 格式化插件列表(超过5个时精简显示)
|
||||||
@@ -200,14 +206,14 @@ func (b *BaseScanStrategy) ValidateConfiguration() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LogScanStart 输出扫描开始信息(已精简,仅在非服务扫描模式下显示)
|
// LogScanStart 输出扫描开始信息(已精简,仅在非服务扫描模式下显示)
|
||||||
func (b *BaseScanStrategy) LogScanStart() {
|
func (b *BaseScanStrategy) LogScanStart(session *common.ScanSession) {
|
||||||
// 服务扫描模式下不显示(插件信息已足够说明)
|
// 服务扫描模式下不显示(插件信息已足够说明)
|
||||||
// 仅在本地/Web等特殊模式下显示
|
// 仅在本地/Web等特殊模式下显示
|
||||||
switch b.filterType {
|
switch b.filterType {
|
||||||
case FilterLocal:
|
case FilterLocal:
|
||||||
common.LogInfo(i18n.GetText("start_local_scan"))
|
session.LogInfo(i18n.GetText("start_local_scan"))
|
||||||
case FilterWeb:
|
case FilterWeb:
|
||||||
common.LogInfo(i18n.GetText("start_web_scan"))
|
session.LogInfo(i18n.GetText("start_web_scan"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-14
@@ -55,7 +55,7 @@ func CheckLive(ctx context.Context, hostslist []string, Ping bool, session *comm
|
|||||||
chanHosts := make(chan string, len(hostslist))
|
chanHosts := make(chan string, len(hostslist))
|
||||||
|
|
||||||
// 处理存活主机
|
// 处理存活主机
|
||||||
go handleAliveHosts(chanHosts, hostslist, Ping, &aliveHosts, &aliveHostsMu, existHosts, config, &livewg)
|
go handleAliveHosts(chanHosts, hostslist, Ping, &aliveHosts, &aliveHostsMu, existHosts, config, session, &livewg)
|
||||||
|
|
||||||
// 根据Ping参数选择检测方式
|
// 根据Ping参数选择检测方式
|
||||||
if Ping {
|
if Ping {
|
||||||
@@ -106,7 +106,7 @@ func tcpSupplementaryProbe(ctx context.Context, allHosts []string, aliveHosts []
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 提示用户正在进行 TCP 补充探测
|
// 提示用户正在进行 TCP 补充探测
|
||||||
common.LogInfo(i18n.Tr("tcp_probe_low_icmp_rate", fmt.Sprintf("%.1f%%", responseRate*100), len(unrespondedHosts)))
|
session.LogInfo(i18n.Tr("tcp_probe_low_icmp_rate", fmt.Sprintf("%.1f%%", responseRate*100), len(unrespondedHosts)))
|
||||||
|
|
||||||
// 执行 TCP 补充探测
|
// 执行 TCP 补充探测
|
||||||
tcpAliveHosts := runTcpProbeForHosts(ctx, unrespondedHosts, session)
|
tcpAliveHosts := runTcpProbeForHosts(ctx, unrespondedHosts, session)
|
||||||
@@ -114,7 +114,7 @@ func tcpSupplementaryProbe(ctx context.Context, allHosts []string, aliveHosts []
|
|||||||
// 合并结果
|
// 合并结果
|
||||||
if len(tcpAliveHosts) > 0 {
|
if len(tcpAliveHosts) > 0 {
|
||||||
aliveHosts = append(aliveHosts, tcpAliveHosts...)
|
aliveHosts = append(aliveHosts, tcpAliveHosts...)
|
||||||
common.LogInfo(i18n.Tr("tcp_probe_found", len(tcpAliveHosts)))
|
session.LogInfo(i18n.Tr("tcp_probe_found", len(tcpAliveHosts)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return aliveHosts
|
return aliveHosts
|
||||||
@@ -130,7 +130,7 @@ func IsContain(items []string, item string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool, aliveHosts *[]string, aliveHostsMu *sync.Mutex, existHosts map[string]struct{}, config *common.Config, livewg *sync.WaitGroup) {
|
func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool, aliveHosts *[]string, aliveHostsMu *sync.Mutex, existHosts map[string]struct{}, config *common.Config, session *common.ScanSession, livewg *sync.WaitGroup) {
|
||||||
for ip := range chanHosts {
|
for ip := range chanHosts {
|
||||||
if _, ok := existHosts[ip]; !ok && IsContain(hostslist, ip) {
|
if _, ok := existHosts[ip]; !ok && IsContain(hostslist, ip) {
|
||||||
existHosts[ip] = struct{}{}
|
existHosts[ip] = struct{}{}
|
||||||
@@ -155,12 +155,9 @@ func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool, al
|
|||||||
"protocol": protocol,
|
"protocol": protocol,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_ = common.SaveResult(result)
|
_ = session.SaveResult(result)
|
||||||
|
|
||||||
// 保留原有的控制台输出
|
session.LogInfo(i18n.Tr("host_alive", ip, protocol))
|
||||||
if !config.Output.Silent {
|
|
||||||
common.LogInfo(i18n.Tr("host_alive", ip, protocol))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
livewg.Done()
|
livewg.Done()
|
||||||
}
|
}
|
||||||
@@ -730,7 +727,6 @@ func tcpProbeAlive(ctx context.Context, session *common.ScanSession, host string
|
|||||||
// runTcpProbeForHosts 对指定主机列表进行 TCP 补充探测
|
// runTcpProbeForHosts 对指定主机列表进行 TCP 补充探测
|
||||||
// 返回存活的主机列表
|
// 返回存活的主机列表
|
||||||
func runTcpProbeForHosts(ctx context.Context, hosts []string, session *common.ScanSession) []string {
|
func runTcpProbeForHosts(ctx context.Context, hosts []string, session *common.ScanSession) []string {
|
||||||
config := session.Config
|
|
||||||
if len(hosts) == 0 {
|
if len(hosts) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -771,11 +767,9 @@ func runTcpProbeForHosts(ctx context.Context, hosts []string, session *common.Sc
|
|||||||
"protocol": "TCP",
|
"protocol": "TCP",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_ = common.SaveResult(result)
|
_ = session.SaveResult(result)
|
||||||
|
|
||||||
if !config.Output.Silent {
|
session.LogInfo(i18n.Tr("host_alive", h, "TCP"))
|
||||||
common.LogInfo(i18n.Tr("host_alive", h, "TCP"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}(host)
|
}(host)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ func NewLocalScanStrategy() *LocalScanStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LogPluginInfo 重写以只显示通过-local指定的插件
|
// LogPluginInfo 重写以只显示通过-local指定的插件
|
||||||
func (s *LocalScanStrategy) LogPluginInfo(config *common.Config) {
|
func (s *LocalScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) {
|
||||||
localPlugin := config.LocalPlugin
|
localPlugin := config.LocalPlugin
|
||||||
if localPlugin != "" {
|
if localPlugin != "" {
|
||||||
common.LogInfo(i18n.Tr("local_plugin_info", localPlugin))
|
session.LogInfo(i18n.Tr("local_plugin_info", localPlugin))
|
||||||
} else {
|
} else {
|
||||||
common.LogError(i18n.GetText("local_plugin_not_specified"))
|
session.LogError(i18n.GetText("local_plugin_not_specified"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,24 +46,24 @@ func (s *LocalScanStrategy) Execute(ctx context.Context, session *common.ScanSes
|
|||||||
config := session.Config
|
config := session.Config
|
||||||
|
|
||||||
// 输出扫描开始信息
|
// 输出扫描开始信息
|
||||||
s.LogScanStart()
|
s.LogScanStart(session)
|
||||||
|
|
||||||
// 验证插件配置
|
// 验证插件配置
|
||||||
if err := s.ValidateConfiguration(); err != nil {
|
if err := s.ValidateConfiguration(); err != nil {
|
||||||
common.LogError(err.Error())
|
session.LogError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证本地插件是否存在
|
// 验证本地插件是否存在
|
||||||
if config.LocalPlugin != "" {
|
if config.LocalPlugin != "" {
|
||||||
if !plugins.Exists(config.LocalPlugin) {
|
if !plugins.Exists(config.LocalPlugin) {
|
||||||
common.LogError(i18n.Tr("local_plugin_not_found", config.LocalPlugin))
|
session.LogError(i18n.Tr("local_plugin_not_found", config.LocalPlugin))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输出插件信息
|
// 输出插件信息
|
||||||
s.LogPluginInfo(config)
|
s.LogPluginInfo(config, session)
|
||||||
|
|
||||||
// 准备目标(本地扫描通常只有一个目标,即本机)
|
// 准备目标(本地扫描通常只有一个目标,即本机)
|
||||||
targets := s.PrepareTargets(info)
|
targets := s.PrepareTargets(info)
|
||||||
|
|||||||
+39
-39
@@ -143,13 +143,13 @@ func (f *failedPortCollector) Count() int {
|
|||||||
func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout int64, session *common.ScanSession, stream chan<- string) []string {
|
func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout int64, session *common.ScanSession, stream chan<- string) []string {
|
||||||
config := session.Config
|
config := session.Config
|
||||||
state := session.State
|
state := session.State
|
||||||
common.LogDebug(fmt.Sprintf("[PortScan] 开始: %d个主机, 线程数=%d", len(hosts), config.ThreadNum))
|
session.LogDebug(fmt.Sprintf("[PortScan] 开始: %d个主机, 线程数=%d", len(hosts), config.ThreadNum))
|
||||||
|
|
||||||
// 大规模扫描预筛:跨多个 /24 时先做网段探活,跳过空网段
|
// 大规模扫描预筛:跨多个 /24 时先做网段探活,跳过空网段
|
||||||
if len(hosts) > subnetProbeThreshold {
|
if len(hosts) > subnetProbeThreshold {
|
||||||
hosts = probeSubnets(ctx, hosts, time.Duration(timeout)*time.Second, session)
|
hosts = probeSubnets(ctx, hosts, time.Duration(timeout)*time.Second, session)
|
||||||
if len(hosts) == 0 {
|
if len(hosts) == 0 {
|
||||||
common.LogInfo(i18n.GetText("port_scan_no_alive_subnet"))
|
session.LogInfo(i18n.GetText("port_scan_no_alive_subnet"))
|
||||||
if stream != nil {
|
if stream != nil {
|
||||||
close(stream)
|
close(stream)
|
||||||
}
|
}
|
||||||
@@ -160,13 +160,13 @@ func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout
|
|||||||
// 解析端口和排除端口
|
// 解析端口和排除端口
|
||||||
portList := parsers.ParsePort(ports)
|
portList := parsers.ParsePort(ports)
|
||||||
if len(portList) == 0 {
|
if len(portList) == 0 {
|
||||||
common.LogError(i18n.Tr("invalid_port", ports))
|
session.LogError(i18n.Tr("invalid_port", ports))
|
||||||
if stream != nil {
|
if stream != nil {
|
||||||
close(stream)
|
close(stream)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
common.LogDebug(fmt.Sprintf("[PortScan] 端口解析完成: %d个端口", len(portList)))
|
session.LogDebug(fmt.Sprintf("[PortScan] 端口解析完成: %d个端口", len(portList)))
|
||||||
|
|
||||||
// 使用config中的排除端口配置
|
// 使用config中的排除端口配置
|
||||||
excludePorts := parsers.ParsePort(config.Target.ExcludePorts)
|
excludePorts := parsers.ParsePort(config.Target.ExcludePorts)
|
||||||
@@ -176,26 +176,26 @@ func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查代理可靠性,如果存在全回显问题则警告
|
// 检查代理可靠性,如果存在全回显问题则警告
|
||||||
if common.IsProxyEnabled() && !common.IsProxyReliable() {
|
if session.ProxyEnabled() && !session.ProxyReliable() {
|
||||||
common.LogError("检测到代理存在全回显问题,端口扫描结果可能不准确")
|
session.LogError("检测到代理存在全回显问题,端口扫描结果可能不准确")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建流式迭代器(O(1) 内存,端口喷洒策略)
|
// 创建流式迭代器(O(1) 内存,端口喷洒策略)
|
||||||
iter := NewSocketIterator(hosts, portList, exclude)
|
iter := NewSocketIterator(hosts, portList, exclude)
|
||||||
totalTasks := iter.Total()
|
totalTasks := iter.Total()
|
||||||
common.LogDebug(fmt.Sprintf("[PortScan] 总任务数: %d", totalTasks))
|
session.LogDebug(fmt.Sprintf("[PortScan] 总任务数: %d", totalTasks))
|
||||||
|
|
||||||
// 使用传入的配置
|
// 使用传入的配置
|
||||||
threadNum := config.ThreadNum
|
threadNum := config.ThreadNum
|
||||||
|
|
||||||
// 大规模扫描警告和线程数自动调整
|
// 大规模扫描警告和线程数自动调整
|
||||||
if totalTasks > 100000 {
|
if totalTasks > 100000 {
|
||||||
common.LogInfo(fmt.Sprintf("大规模扫描: %d 个目标 (%d主机 × %d端口)", totalTasks, len(hosts), len(portList)))
|
session.LogInfo(fmt.Sprintf("大规模扫描: %d 个目标 (%d主机 × %d端口)", totalTasks, len(hosts), len(portList)))
|
||||||
// 如果任务数超过100万且线程数大于300,自动降低线程数
|
// 如果任务数超过100万且线程数大于300,自动降低线程数
|
||||||
if totalTasks > 1000000 && threadNum > 300 {
|
if totalTasks > 1000000 && threadNum > 300 {
|
||||||
oldThreadNum := threadNum
|
oldThreadNum := threadNum
|
||||||
threadNum = 300
|
threadNum = 300
|
||||||
common.LogInfo(fmt.Sprintf("自动调整线程数: %d -> %d (大规模扫描优化)", oldThreadNum, threadNum))
|
session.LogInfo(fmt.Sprintf("自动调整线程数: %d -> %d (大规模扫描优化)", oldThreadNum, threadNum))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout
|
|||||||
description := fmt.Sprintf("端口扫描中(%d线程)", threadNum)
|
description := fmt.Sprintf("端口扫描中(%d线程)", threadNum)
|
||||||
common.InitProgressBar(int64(totalTasks), description)
|
common.InitProgressBar(int64(totalTasks), description)
|
||||||
}
|
}
|
||||||
common.LogDebug("[PortScan] 进度条初始化完成")
|
session.LogDebug("[PortScan] 进度条初始化完成")
|
||||||
|
|
||||||
// 初始化并发控制
|
// 初始化并发控制
|
||||||
to := time.Duration(timeout) * time.Second
|
to := time.Duration(timeout) * time.Second
|
||||||
@@ -214,7 +214,7 @@ func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout
|
|||||||
failedCollector := &failedPortCollector{}
|
failedCollector := &failedPortCollector{}
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("[PortScan] 开始创建线程池, size=%d", threadNum))
|
session.LogDebug(fmt.Sprintf("[PortScan] 开始创建线程池, size=%d", threadNum))
|
||||||
// 创建自适应线程池(支持动态调整)
|
// 创建自适应线程池(支持动态调整)
|
||||||
pool, err := NewAdaptivePool(threadNum, func(task interface{}) {
|
pool, err := NewAdaptivePool(threadNum, func(task interface{}) {
|
||||||
taskInfo, ok := task.(portScanTask)
|
taskInfo, ok := task.(portScanTask)
|
||||||
@@ -230,19 +230,19 @@ func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout
|
|||||||
common.UpdateProgressBar(1)
|
common.UpdateProgressBar(1)
|
||||||
}, state)
|
}, state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(i18n.Tr("thread_pool_create_failed", err))
|
session.LogError(i18n.Tr("thread_pool_create_failed", err))
|
||||||
if stream != nil {
|
if stream != nil {
|
||||||
close(stream)
|
close(stream)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
common.LogDebug("[PortScan] 线程池创建成功")
|
session.LogDebug("[PortScan] 线程池创建成功")
|
||||||
defer pool.Release()
|
defer pool.Release()
|
||||||
|
|
||||||
common.LogDebug("[PortScan] 开始滑动窗口调度")
|
session.LogDebug("[PortScan] 开始滑动窗口调度")
|
||||||
// 滑动窗口调度:维护固定数量的"飞行中"任务
|
// 滑动窗口调度:维护固定数量的"飞行中"任务
|
||||||
slidingWindowSchedule(iter, pool, &wg, threadNum)
|
slidingWindowSchedule(iter, pool, &wg, threadNum)
|
||||||
common.LogDebug("[PortScan] 滑动窗口调度完成")
|
session.LogDebug("[PortScan] 滑动窗口调度完成")
|
||||||
|
|
||||||
// 收集结果
|
// 收集结果
|
||||||
aliveAddrs := collector.GetAll()
|
aliveAddrs := collector.GetAll()
|
||||||
@@ -257,7 +257,7 @@ func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout
|
|||||||
common.FinishProgressBar()
|
common.FinishProgressBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
common.LogInfo(i18n.Tr("port_scan_complete", count))
|
session.LogInfo(i18n.Tr("port_scan_complete", count))
|
||||||
|
|
||||||
// 检查扫描失败率,如果过高则警告用户
|
// 检查扫描失败率,如果过高则警告用户
|
||||||
resourceErrors := state.GetResourceExhaustedCount()
|
resourceErrors := state.GetResourceExhaustedCount()
|
||||||
@@ -268,18 +268,18 @@ func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout
|
|||||||
|
|
||||||
if failureRate > 20 {
|
if failureRate > 20 {
|
||||||
// 失败率超过20%,严重警告
|
// 失败率超过20%,严重警告
|
||||||
common.LogError(i18n.Tr("scan_failure_rate_high", fmt.Sprintf("%.1f%%", failureRate), failedCount, totalTasks))
|
session.LogError(i18n.Tr("scan_failure_rate_high", fmt.Sprintf("%.1f%%", failureRate), failedCount, totalTasks))
|
||||||
common.LogError(i18n.GetText("scan_failure_reason"))
|
session.LogError(i18n.GetText("scan_failure_reason"))
|
||||||
common.LogError(i18n.Tr("scan_reduce_threads_suggestion", threadNum))
|
session.LogError(i18n.Tr("scan_reduce_threads_suggestion", threadNum))
|
||||||
} else if failureRate > 5 {
|
} else if failureRate > 5 {
|
||||||
// 失败率5-20%,一般警告
|
// 失败率5-20%,一般警告
|
||||||
common.LogInfo(i18n.Tr("scan_partial_failure", fmt.Sprintf("%.1f%%", failureRate), failedCount, totalTasks))
|
session.LogInfo(i18n.Tr("scan_partial_failure", fmt.Sprintf("%.1f%%", failureRate), failedCount, totalTasks))
|
||||||
common.LogInfo(i18n.Tr("scan_reduce_threads_accuracy", threadNum))
|
session.LogInfo(i18n.Tr("scan_reduce_threads_accuracy", threadNum))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if resourceErrors > 0 {
|
if resourceErrors > 0 {
|
||||||
common.LogError(i18n.Tr("resource_exhausted_warning", resourceErrors))
|
session.LogError(i18n.Tr("resource_exhausted_warning", resourceErrors))
|
||||||
}
|
}
|
||||||
|
|
||||||
return aliveAddrs
|
return aliveAddrs
|
||||||
@@ -421,10 +421,10 @@ func matchFold(a, b string) bool {
|
|||||||
// 格式: addr service [Product:xxx ||Version:xxx] Banner:(xxx)
|
// 格式: addr service [Product:xxx ||Version:xxx] Banner:(xxx)
|
||||||
func buildServiceLogMessage(addr string, serviceInfo *ServiceInfo, isWeb bool) string {
|
func buildServiceLogMessage(addr string, serviceInfo *ServiceInfo, isWeb bool) string {
|
||||||
var msg strings.Builder
|
var msg strings.Builder
|
||||||
msg.WriteString(fmt.Sprintf("%-21s", addr))
|
fmt.Fprintf(&msg, "%-21s", addr)
|
||||||
|
|
||||||
if serviceInfo.Name != "unknown" {
|
if serviceInfo.Name != "unknown" {
|
||||||
msg.WriteString(fmt.Sprintf(" %-8s", serviceInfo.Name))
|
fmt.Fprintf(&msg, " %-8s", serviceInfo.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建 [Product:xxx ||Version:xxx] 格式
|
// 构建 [Product:xxx ||Version:xxx] 格式
|
||||||
@@ -465,16 +465,16 @@ func scanSinglePort(ctx context.Context, host string, port int, addr string, ada
|
|||||||
adaptiveTO.Record(time.Since(start))
|
adaptiveTO.Record(time.Since(start))
|
||||||
|
|
||||||
// 步骤1.5:代理连接深度验证(防止透明代理/全回显代理的假连接问题)
|
// 步骤1.5:代理连接深度验证(防止透明代理/全回显代理的假连接问题)
|
||||||
valid, verifyMethod := verifyProxyConnectionDeep(conn, addr)
|
valid, verifyMethod := verifyProxyConnectionDeep(conn, addr, session)
|
||||||
if !valid {
|
if !valid {
|
||||||
common.LogDebug(fmt.Sprintf("代理验证失败 %s: %s", addr, verifyMethod))
|
session.LogDebug(fmt.Sprintf("代理验证失败 %s: %s", addr, verifyMethod))
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 步骤1.6:如果使用了代理且进行了数据交互,需要重建连接
|
// 步骤1.6:如果使用了代理且进行了数据交互,需要重建连接
|
||||||
// 因为验证阶段可能读取了Banner或发送了HTTP GET探测,污染了连接状态
|
// 因为验证阶段可能读取了Banner或发送了HTTP GET探测,污染了连接状态
|
||||||
if common.IsProxyEnabled() && verifyMethod != "direct" {
|
if session.ProxyEnabled() && verifyMethod != "direct" {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
// 重新建立干净的连接用于服务识别
|
// 重新建立干净的连接用于服务识别
|
||||||
conn, err = connectWithRetry(ctx, session, addr, timeout, 2)
|
conn, err = connectWithRetry(ctx, session, addr, timeout, 2)
|
||||||
@@ -487,7 +487,7 @@ func scanSinglePort(ctx context.Context, host string, port int, addr string, ada
|
|||||||
// 步骤2:记录开放端口
|
// 步骤2:记录开放端口
|
||||||
atomic.AddInt64(count, 1)
|
atomic.AddInt64(count, 1)
|
||||||
collector.Add(addr)
|
collector.Add(addr)
|
||||||
saveOpenPort(host, port)
|
saveOpenPort(session, host, port)
|
||||||
|
|
||||||
// 步骤3:服务识别(Scanner负责关闭连接,包括探测中可能创建的新连接)
|
// 步骤3:服务识别(Scanner负责关闭连接,包括探测中可能创建的新连接)
|
||||||
scanner := NewSmartPortInfoScanner(ctx, host, port, conn, timeout, config, session)
|
scanner := NewSmartPortInfoScanner(ctx, host, port, conn, timeout, config, session)
|
||||||
@@ -523,10 +523,10 @@ func handleConnectionFailure(err error, host string, port int, addr string, fail
|
|||||||
// 1. 快速 Banner 检测 (100ms) - 大部分服务会主动发送数据
|
// 1. 快速 Banner 检测 (100ms) - 大部分服务会主动发送数据
|
||||||
// 2. 轻量探测 (发送 \r\n) - 触发某些服务响应,同时不污染协议状态
|
// 2. 轻量探测 (发送 \r\n) - 触发某些服务响应,同时不污染协议状态
|
||||||
// 3. 短超时等待 (500ms) - 平衡准确性和性能
|
// 3. 短超时等待 (500ms) - 平衡准确性和性能
|
||||||
func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
func verifyProxyConnectionDeep(conn net.Conn, addr string, session *common.ScanSession) (bool, string) {
|
||||||
// 无代理或SOCKS5代理:跳过深度验证
|
// 无代理或SOCKS5代理:跳过深度验证
|
||||||
// SOCKS5协议层已验证连接可达性,连接成功即端口开放
|
// SOCKS5协议层已验证连接可达性,连接成功即端口开放
|
||||||
if !common.IsProxyEnabled() || common.IsSOCKS5Proxy() {
|
if !session.ProxyEnabled() || session.IsSOCKS5Proxy() {
|
||||||
return true, "direct"
|
return true, "direct"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,8 +640,8 @@ func isConnectionClosed(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// saveOpenPort 保存开放端口结果
|
// saveOpenPort 保存开放端口结果
|
||||||
func saveOpenPort(host string, port int) {
|
func saveOpenPort(session *common.ScanSession, host string, port int) {
|
||||||
_ = common.SaveResult(&output.ScanResult{
|
_ = session.SaveResult(&output.ScanResult{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Type: output.TypePort,
|
Type: output.TypePort,
|
||||||
Target: host,
|
Target: host,
|
||||||
@@ -655,7 +655,7 @@ func processServiceResult(host string, port int, addr string, serviceInfo *Servi
|
|||||||
if serviceInfo == nil {
|
if serviceInfo == nil {
|
||||||
// 服务识别失败,尝试 HTTP 回退探测
|
// 服务识别失败,尝试 HTTP 回退探测
|
||||||
if !tryHTTPFallbackDetection(host, port, addr, config, session) {
|
if !tryHTTPFallbackDetection(host, port, addr, config, session) {
|
||||||
common.LogInfo(i18n.Tr("port_open", addr))
|
session.LogInfo(i18n.Tr("port_open", addr))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -669,7 +669,7 @@ func processServiceResult(host string, port int, addr string, serviceInfo *Servi
|
|||||||
MarkAsWebService(host, port, serviceInfo)
|
MarkAsWebService(host, port, serviceInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = common.SaveResult(&output.ScanResult{
|
_ = session.SaveResult(&output.ScanResult{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Type: output.TypeService,
|
Type: output.TypeService,
|
||||||
Target: fmt.Sprintf("%s:%d", host, port),
|
Target: fmt.Sprintf("%s:%d", host, port),
|
||||||
@@ -677,7 +677,7 @@ func processServiceResult(host string, port int, addr string, serviceInfo *Servi
|
|||||||
Details: details,
|
Details: details,
|
||||||
})
|
})
|
||||||
|
|
||||||
common.LogInfo(buildServiceLogMessage(addr, serviceInfo, isWeb))
|
session.LogInfo(buildServiceLogMessage(addr, serviceInfo, isWeb))
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildServiceDetails 构建服务详情 map
|
// buildServiceDetails 构建服务详情 map
|
||||||
@@ -737,7 +737,7 @@ func tryHTTPFallbackDetection(host string, port int, addr string, config *common
|
|||||||
"is_web": true,
|
"is_web": true,
|
||||||
"detected_by": "http_probe",
|
"detected_by": "http_probe",
|
||||||
}
|
}
|
||||||
_ = common.SaveResult(&output.ScanResult{
|
_ = session.SaveResult(&output.ScanResult{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Type: output.TypeService,
|
Type: output.TypeService,
|
||||||
Target: fmt.Sprintf("%s:%d", host, port),
|
Target: fmt.Sprintf("%s:%d", host, port),
|
||||||
@@ -745,7 +745,7 @@ func tryHTTPFallbackDetection(host string, port int, addr string, config *common
|
|||||||
Details: details,
|
Details: details,
|
||||||
})
|
})
|
||||||
|
|
||||||
common.LogInfo(i18n.Tr("port_open_http", addr))
|
session.LogInfo(i18n.Tr("port_open_http", addr))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -790,7 +790,7 @@ func probeSubnets(ctx context.Context, hosts []string, timeout time.Duration, se
|
|||||||
return hosts
|
return hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
common.LogInfo(fmt.Sprintf("网段预筛: %d 个 /24 子网, %d 个主机", len(subnets), len(hosts)))
|
session.LogInfo(fmt.Sprintf("网段预筛: %d 个 /24 子网, %d 个主机", len(subnets), len(hosts)))
|
||||||
|
|
||||||
aliveSubnets := sync.Map{}
|
aliveSubnets := sync.Map{}
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -872,7 +872,7 @@ done:
|
|||||||
}
|
}
|
||||||
|
|
||||||
skipped := len(subnets) - aliveCount
|
skipped := len(subnets) - aliveCount
|
||||||
common.LogInfo(fmt.Sprintf("网段预筛完成: %d 个存活 (网关命中 %d), %d 个跳过, 剩余 %d 主机",
|
session.LogInfo(fmt.Sprintf("网段预筛完成: %d 个存活 (网关命中 %d), %d 个跳过, 剩余 %d 主机",
|
||||||
aliveCount, gwHits, skipped, len(result)))
|
aliveCount, gwHits, skipped, len(result)))
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-15
@@ -87,7 +87,7 @@ func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSess
|
|||||||
|
|
||||||
// 初始化HTTP客户端(静默,无需日志)
|
// 初始化HTTP客户端(静默,无需日志)
|
||||||
if err := lib.Inithttp(config); err != nil {
|
if err := lib.Inithttp(config); err != nil {
|
||||||
common.LogError(i18n.Tr("http_client_init_failed", err))
|
session.LogError(i18n.Tr("http_client_init_failed", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,22 +107,22 @@ func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSess
|
|||||||
// 检查是否有活跃的连接需要维持
|
// 检查是否有活跃的连接需要维持
|
||||||
if state.IsReverseShellActive() || state.IsSocks5ProxyActive() || state.IsForwardShellActive() {
|
if state.IsReverseShellActive() || state.IsSocks5ProxyActive() || state.IsForwardShellActive() {
|
||||||
if state.IsReverseShellActive() {
|
if state.IsReverseShellActive() {
|
||||||
common.LogInfo(i18n.GetText("active_reverse_shell"))
|
session.LogInfo(i18n.GetText("active_reverse_shell"))
|
||||||
}
|
}
|
||||||
if state.IsSocks5ProxyActive() {
|
if state.IsSocks5ProxyActive() {
|
||||||
common.LogInfo(i18n.GetText("active_socks5_proxy"))
|
session.LogInfo(i18n.GetText("active_socks5_proxy"))
|
||||||
}
|
}
|
||||||
if state.IsForwardShellActive() {
|
if state.IsForwardShellActive() {
|
||||||
common.LogInfo(i18n.GetText("active_forward_shell"))
|
session.LogInfo(i18n.GetText("active_forward_shell"))
|
||||||
}
|
}
|
||||||
common.LogInfo(i18n.GetText("press_ctrl_c_exit"))
|
session.LogInfo(i18n.GetText("press_ctrl_c_exit"))
|
||||||
|
|
||||||
// 优雅等待信号或 context 取消(Web Stop)
|
// 优雅等待信号或 context 取消(Web Stop)
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
select {
|
select {
|
||||||
case <-sigChan:
|
case <-sigChan:
|
||||||
common.LogInfo(i18n.GetText("received_exit_signal"))
|
session.LogInfo(i18n.GetText("received_exit_signal"))
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
}
|
}
|
||||||
cancel()
|
cancel()
|
||||||
@@ -130,18 +130,21 @@ func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSess
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 完成扫描
|
// 完成扫描
|
||||||
finishScan(config, state)
|
finishScan(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// finishScan 完成扫描并输出结果
|
// finishScan 完成扫描并输出结果
|
||||||
func finishScan(config *common.Config, state *common.State) {
|
func finishScan(session *common.ScanSession) {
|
||||||
|
config := session.Config
|
||||||
|
state := session.State
|
||||||
|
|
||||||
// 确保进度条正确完成
|
// 确保进度条正确完成
|
||||||
if common.IsProgressActive() {
|
if common.IsProgressActive() {
|
||||||
common.FinishProgressBar()
|
common.FinishProgressBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输出扫描完成信息
|
// 输出扫描完成信息
|
||||||
common.LogInfo(i18n.Tr("scan_task_complete", time.Since(state.GetStartTime()).Round(time.Millisecond), state.GetNum()))
|
session.LogInfo(i18n.Tr("scan_task_complete", time.Since(state.GetStartTime()).Round(time.Millisecond), state.GetNum()))
|
||||||
|
|
||||||
// 输出性能统计 JSON(如果启用)
|
// 输出性能统计 JSON(如果启用)
|
||||||
if config.Output.PerfStats {
|
if config.Output.PerfStats {
|
||||||
@@ -262,7 +265,7 @@ func executeScanTask(ctx context.Context, session *common.ScanSession, pluginNam
|
|||||||
defer func() {
|
defer func() {
|
||||||
// 捕获并记录任何可能的panic
|
// 捕获并记录任何可能的panic
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
common.LogError(i18n.Tr("plugin_panic", pluginName, target.Host, target.Port, r))
|
session.LogError(i18n.Tr("plugin_panic", pluginName, target.Host, target.Port, r))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新统计和进度(任务真正完成时才更新)
|
// 更新统计和进度(任务真正完成时才更新)
|
||||||
@@ -281,13 +284,13 @@ func executeScanTask(ctx context.Context, session *common.ScanSession, pluginNam
|
|||||||
if result != nil {
|
if result != nil {
|
||||||
if result.Success {
|
if result.Success {
|
||||||
// 保存成功的扫描结果到文件
|
// 保存成功的扫描结果到文件
|
||||||
savePluginResult(&target, pluginName, result)
|
savePluginResult(session, &target, pluginName, result)
|
||||||
} else if result.Type == plugins.ResultTypeCredential {
|
} else if result.Type == plugins.ResultTypeCredential {
|
||||||
// 凭据测试完成但未发现弱密码,在error级别输出提示
|
// 凭据测试完成但未发现弱密码,在error级别输出提示
|
||||||
common.LogError(i18n.Tr("brute_no_weak_pass", target.Host, target.Port, pluginName))
|
session.LogError(i18n.Tr("brute_no_weak_pass", target.Host, target.Port, pluginName))
|
||||||
} else if result.Error != nil {
|
} else if result.Error != nil {
|
||||||
// 其他类型的错误
|
// 其他类型的错误
|
||||||
common.LogError(i18n.Tr("plugin_scan_error", target.Host, target.Port, result.Error))
|
session.LogError(i18n.Tr("plugin_scan_error", target.Host, target.Port, result.Error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,7 +385,7 @@ var defaultSerializer = resultSerializer{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// savePluginResult 保存插件扫描结果
|
// savePluginResult 保存插件扫描结果
|
||||||
func savePluginResult(info *common.HostInfo, pluginName string, result *plugins.Result) {
|
func savePluginResult(session *common.ScanSession, info *common.HostInfo, pluginName string, result *plugins.Result) {
|
||||||
if result == nil || !result.Success || result.Skipped {
|
if result == nil || !result.Success || result.Skipped {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -402,7 +405,7 @@ func savePluginResult(info *common.HostInfo, pluginName string, result *plugins.
|
|||||||
|
|
||||||
// 保存结果
|
// 保存结果
|
||||||
target := info.Target()
|
target := info.Target()
|
||||||
_ = common.SaveResult(&output.ScanResult{
|
_ = session.SaveResult(&output.ScanResult{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Type: serializer.outputType,
|
Type: serializer.outputType,
|
||||||
Target: target,
|
Target: target,
|
||||||
|
|||||||
+17
-18
@@ -25,27 +25,27 @@ func NewServiceScanStrategy() *ServiceScanStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LogPluginInfo 重写以提供基于端口的插件过滤
|
// LogPluginInfo 重写以提供基于端口的插件过滤
|
||||||
func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config) {
|
func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) {
|
||||||
// 需要从命令行参数获取端口信息来进行过滤
|
// 需要从命令行参数获取端口信息来进行过滤
|
||||||
// 如果没有指定端口,使用默认端口进行过滤显示
|
// 如果没有指定端口,使用默认端口进行过滤显示
|
||||||
ports := config.Target.Ports
|
ports := config.Target.Ports
|
||||||
if ports == "" || ports == "all" {
|
if ports == "" || ports == "all" {
|
||||||
// 默认端口扫描:显示所有插件
|
// 默认端口扫描:显示所有插件
|
||||||
s.BaseScanStrategy.LogPluginInfo(config)
|
s.BaseScanStrategy.LogPluginInfo(config, session)
|
||||||
} else {
|
} else {
|
||||||
// 指定端口扫描:只显示匹配的插件
|
// 指定端口扫描:只显示匹配的插件
|
||||||
s.showPluginsForSpecifiedPorts(config)
|
s.showPluginsForSpecifiedPorts(config, session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// showPluginsForSpecifiedPorts 显示指定端口的匹配插件
|
// showPluginsForSpecifiedPorts 显示指定端口的匹配插件
|
||||||
func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config) {
|
func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config, session *common.ScanSession) {
|
||||||
allPlugins, isCustomMode := s.GetPlugins(config)
|
allPlugins, isCustomMode := s.GetPlugins(config)
|
||||||
|
|
||||||
// 解析端口
|
// 解析端口
|
||||||
ports := s.parsePortList(config.Target.Ports)
|
ports := s.parsePortList(config.Target.Ports)
|
||||||
if len(ports) == 0 {
|
if len(ports) == 0 {
|
||||||
s.BaseScanStrategy.LogPluginInfo(config)
|
s.BaseScanStrategy.LogPluginInfo(config, session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,12 +71,12 @@ func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config
|
|||||||
if len(applicablePlugins) > 0 {
|
if len(applicablePlugins) > 0 {
|
||||||
pluginStr := formatPluginList(applicablePlugins)
|
pluginStr := formatPluginList(applicablePlugins)
|
||||||
if isCustomMode {
|
if isCustomMode {
|
||||||
common.LogInfo(i18n.Tr("service_plugin_custom", pluginStr))
|
session.LogInfo(i18n.Tr("service_plugin_custom", pluginStr))
|
||||||
} else {
|
} else {
|
||||||
common.LogInfo(i18n.Tr("service_plugin_info", pluginStr))
|
session.LogInfo(i18n.Tr("service_plugin_info", pluginStr))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
common.LogInfo(i18n.GetText("service_plugin_none"))
|
session.LogInfo(i18n.GetText("service_plugin_none"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,21 +118,21 @@ func (s *ServiceScanStrategy) Execute(ctx context.Context, session *common.ScanS
|
|||||||
|
|
||||||
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
|
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
|
||||||
if info.Host == "" && session.Params.HostsFile == "" {
|
if info.Host == "" && session.Params.HostsFile == "" {
|
||||||
common.LogError(i18n.GetText("parse_error_target_empty"))
|
session.LogError(i18n.GetText("parse_error_target_empty"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输出扫描开始信息
|
// 输出扫描开始信息
|
||||||
s.LogScanStart()
|
s.LogScanStart(session)
|
||||||
|
|
||||||
// 验证插件配置
|
// 验证插件配置
|
||||||
if err := s.ValidateConfiguration(); err != nil {
|
if err := s.ValidateConfiguration(); err != nil {
|
||||||
common.LogError(err.Error())
|
session.LogError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输出插件信息(重写以提供端口过滤)
|
// 输出插件信息(重写以提供端口过滤)
|
||||||
s.LogPluginInfo(config)
|
s.LogPluginInfo(config, session)
|
||||||
|
|
||||||
// 执行主机扫描流程
|
// 执行主机扫描流程
|
||||||
s.performHostScan(ctx, session, info, ch, wg)
|
s.performHostScan(ctx, session, info, ch, wg)
|
||||||
@@ -147,14 +147,14 @@ func (s *ServiceScanStrategy) performHostScan(ctx context.Context, session *comm
|
|||||||
// 解析目标主机
|
// 解析目标主机
|
||||||
hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
|
hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
|
session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主机存活检测
|
// 主机存活检测
|
||||||
if s.shouldPerformLivenessCheck(hosts, config) {
|
if s.shouldPerformLivenessCheck(hosts, config) {
|
||||||
hosts = CheckLive(ctx, hosts, false, session)
|
hosts = CheckLive(ctx, hosts, false, session)
|
||||||
common.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts)))
|
session.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(hosts) == 0 && len(state.GetHostPorts()) == 0 {
|
if len(hosts) == 0 && len(state.GetHostPorts()) == 0 {
|
||||||
@@ -218,7 +218,7 @@ func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, session *comm
|
|||||||
// 发现目标主机和端口
|
// 发现目标主机和端口
|
||||||
targetInfos, err := s.discoverTargets(context.Background(), info.Host, info, session)
|
targetInfos, err := s.discoverTargets(context.Background(), info.Host, info, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(err.Error())
|
session.LogError(err.Error())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return targetInfos
|
return targetInfos
|
||||||
@@ -291,7 +291,7 @@ func (s *ServiceScanStrategy) discoverTargets(ctx context.Context, hostInput str
|
|||||||
// 主机存活检测
|
// 主机存活检测
|
||||||
if s.shouldPerformLivenessCheck(hosts, config) {
|
if s.shouldPerformLivenessCheck(hosts, config) {
|
||||||
hosts = CheckLive(ctx, hosts, false, session)
|
hosts = CheckLive(ctx, hosts, false, session)
|
||||||
common.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts)))
|
session.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 端口扫描
|
// 端口扫描
|
||||||
@@ -325,7 +325,7 @@ func (s *ServiceScanStrategy) discoverAlivePorts(ctx context.Context, hosts []st
|
|||||||
hostPorts := state.GetHostPorts()
|
hostPorts := state.GetHostPorts()
|
||||||
if len(hostPorts) > 0 {
|
if len(hostPorts) > 0 {
|
||||||
alivePorts = mergeHostPorts(alivePorts, hostPorts)
|
alivePorts = mergeHostPorts(alivePorts, hostPorts)
|
||||||
common.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts)))
|
session.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts)))
|
||||||
state.ClearHostPorts()
|
state.ClearHostPorts()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,4 +390,3 @@ func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo comm
|
|||||||
|
|
||||||
return infos
|
return infos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-40
@@ -16,29 +16,6 @@ import (
|
|||||||
gmtls "github.com/tjfoc/gmsm/gmtls"
|
gmtls "github.com/tjfoc/gmsm/gmtls"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ===============================
|
|
||||||
// Web服务检测
|
|
||||||
// ===============================
|
|
||||||
|
|
||||||
// 全局共享 HTTP Client,复用连接池减少 TLS 握手和 TCP 建连开销
|
|
||||||
var (
|
|
||||||
sharedHTTPClientOnce sync.Once
|
|
||||||
sharedHTTPClient *http.Client
|
|
||||||
)
|
|
||||||
|
|
||||||
func getSharedHTTPClient(config *common.Config) *http.Client {
|
|
||||||
sharedHTTPClientOnce.Do(func() {
|
|
||||||
sharedHTTPClient = createHTTPClient(config)
|
|
||||||
// 启用 keep-alive 复用连接
|
|
||||||
if t, ok := sharedHTTPClient.Transport.(*http.Transport); ok {
|
|
||||||
t.DisableKeepAlives = false
|
|
||||||
t.MaxIdleConns = 100
|
|
||||||
t.MaxIdleConnsPerHost = 2
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return sharedHTTPClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebPortDetector 简化的Web检测器 - 保持API兼容
|
// WebPortDetector 简化的Web检测器 - 保持API兼容
|
||||||
type WebPortDetector struct{}
|
type WebPortDetector struct{}
|
||||||
|
|
||||||
@@ -91,7 +68,7 @@ func DetectHTTPScheme(host string, port int, config *common.Config, session *com
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TLS和GM TLS都失败,尝试HTTP
|
// TLS和GM TLS都失败,尝试HTTP
|
||||||
client := getSharedHTTPClient(config)
|
client := createHTTPClient(config, session)
|
||||||
|
|
||||||
// 使用HEAD请求(更轻量)
|
// 使用HEAD请求(更轻量)
|
||||||
httpURL := fmt.Sprintf("http://%s", addr)
|
httpURL := fmt.Sprintf("http://%s", addr)
|
||||||
@@ -106,7 +83,7 @@ func DetectHTTPScheme(host string, port int, config *common.Config, session *com
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createHTTPClient 创建统一的HTTP客户端 - 支持HTTP/HTTPS和代理
|
// createHTTPClient 创建统一的HTTP客户端 - 支持HTTP/HTTPS和代理
|
||||||
func createHTTPClient(config *common.Config) *http.Client {
|
func createHTTPClient(config *common.Config, session *common.ScanSession) *http.Client {
|
||||||
timeout := config.Network.WebTimeout
|
timeout := config.Network.WebTimeout
|
||||||
|
|
||||||
// 创建基础Transport,配置连接和 TLS 超时
|
// 创建基础Transport,配置连接和 TLS 超时
|
||||||
@@ -128,14 +105,14 @@ func createHTTPClient(config *common.Config) *http.Client {
|
|||||||
if proxyURL, err := url.Parse(networkConfig.HTTPProxy); err == nil {
|
if proxyURL, err := url.Parse(networkConfig.HTTPProxy); err == nil {
|
||||||
transport.Proxy = http.ProxyURL(proxyURL)
|
transport.Proxy = http.ProxyURL(proxyURL)
|
||||||
} else {
|
} else {
|
||||||
common.LogError(i18n.Tr("http_proxy_config_error", err))
|
session.LogError(i18n.Tr("http_proxy_config_error", err))
|
||||||
}
|
}
|
||||||
} else if networkConfig.Socks5Proxy != "" {
|
} else if networkConfig.Socks5Proxy != "" {
|
||||||
// 使用SOCKS5代理 - 需要特殊处理
|
// 使用SOCKS5代理 - 需要特殊处理
|
||||||
if _, err := url.Parse(networkConfig.Socks5Proxy); err == nil {
|
if _, err := url.Parse(networkConfig.Socks5Proxy); err == nil {
|
||||||
// SOCKS5代理需要使用代理管理器
|
// SOCKS5代理需要使用代理管理器
|
||||||
// 这里先记录警告,建议使用HTTP代理进行Web检测
|
// 这里先记录警告,建议使用HTTP代理进行Web检测
|
||||||
common.LogError(i18n.GetText("socks5_not_supported_web"))
|
session.LogError(i18n.GetText("socks5_not_supported_web"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,15 +133,15 @@ func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *c
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
client := getSharedHTTPClient(config)
|
client := createHTTPClient(config, session)
|
||||||
|
|
||||||
// 尝试HTTP
|
// 尝试HTTP
|
||||||
if w.tryHTTP(client, host, port, "http") {
|
if w.tryHTTP(client, session, host, port, "http") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试HTTPS
|
// 尝试HTTPS
|
||||||
if w.tryHTTP(client, host, port, "https") {
|
if w.tryHTTP(client, session, host, port, "https") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +163,7 @@ func isPortReachable(host string, port int, config *common.Config, session *comm
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tryHTTP 尝试HTTP请求 - 简化的核心逻辑
|
// tryHTTP 尝试HTTP请求 - 简化的核心逻辑
|
||||||
func (w *WebPortDetector) tryHTTP(client *http.Client, host string, port int, protocol string) bool {
|
func (w *WebPortDetector) tryHTTP(client *http.Client, session *common.ScanSession, host string, port int, protocol string) bool {
|
||||||
// 构造URL
|
// 构造URL
|
||||||
var url string
|
var url string
|
||||||
if (port == 80 && protocol == "http") || (port == 443 && protocol == "https") {
|
if (port == 80 && protocol == "http") || (port == 443 && protocol == "https") {
|
||||||
@@ -204,8 +181,7 @@ func (w *WebPortDetector) tryHTTP(client *http.Client, host string, port int, pr
|
|||||||
req.Header.Set("User-Agent", "fscan-web-detector/2.1")
|
req.Header.Set("User-Agent", "fscan-web-detector/2.1")
|
||||||
req.Header.Set("Accept", "*/*")
|
req.Header.Set("Accept", "*/*")
|
||||||
|
|
||||||
// 使用统一的SafeHTTPDo以确保遵循限速策略和代理设置
|
resp, err := session.HTTPDo(client, req)
|
||||||
resp, err := common.SafeHTTPDo(client, req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -330,19 +306,19 @@ func (s *WebScanStrategy) Description() string {
|
|||||||
// Execute 执行Web扫描策略
|
// Execute 执行Web扫描策略
|
||||||
func (s *WebScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
func (s *WebScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||||
// 输出扫描开始信息
|
// 输出扫描开始信息
|
||||||
s.LogScanStart()
|
s.LogScanStart(session)
|
||||||
|
|
||||||
// 验证插件配置
|
// 验证插件配置
|
||||||
if err := s.ValidateConfiguration(); err != nil {
|
if err := s.ValidateConfiguration(); err != nil {
|
||||||
common.LogError(err.Error())
|
session.LogError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 准备URL目标
|
// 准备URL目标
|
||||||
targets := s.PrepareTargets(info, session.State)
|
targets := s.prepareTargets(info, session.State, session)
|
||||||
|
|
||||||
// 输出插件信息
|
// 输出插件信息
|
||||||
s.LogPluginInfo(session.Config)
|
s.LogPluginInfo(session.Config, session)
|
||||||
|
|
||||||
// 执行扫描任务
|
// 执行扫描任务
|
||||||
ExecuteScanTasks(ctx, session, targets, s, ch, wg)
|
ExecuteScanTasks(ctx, session, targets, s, ch, wg)
|
||||||
@@ -350,12 +326,16 @@ func (s *WebScanStrategy) Execute(ctx context.Context, session *common.ScanSessi
|
|||||||
|
|
||||||
// PrepareTargets 准备URL目标列表
|
// PrepareTargets 准备URL目标列表
|
||||||
func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common.State) []common.HostInfo {
|
func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common.State) []common.HostInfo {
|
||||||
|
return s.prepareTargets(baseInfo, state, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebScanStrategy) prepareTargets(baseInfo common.HostInfo, state *common.State, session *common.ScanSession) []common.HostInfo {
|
||||||
var targetInfos []common.HostInfo
|
var targetInfos []common.HostInfo
|
||||||
|
|
||||||
// 首先从State获取URL目标
|
// 首先从State获取URL目标
|
||||||
urls := state.GetURLs()
|
urls := state.GetURLs()
|
||||||
for _, urlStr := range urls {
|
for _, urlStr := range urls {
|
||||||
urlInfo := s.createTargetFromURL(baseInfo, urlStr)
|
urlInfo := s.createTargetFromURLWithSession(baseInfo, urlStr, session)
|
||||||
if urlInfo != nil {
|
if urlInfo != nil {
|
||||||
targetInfos = append(targetInfos, *urlInfo)
|
targetInfos = append(targetInfos, *urlInfo)
|
||||||
}
|
}
|
||||||
@@ -363,7 +343,7 @@ func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common
|
|||||||
|
|
||||||
// 如果URLs为空但baseInfo.Url有值,使用baseInfo.URL
|
// 如果URLs为空但baseInfo.Url有值,使用baseInfo.URL
|
||||||
if len(targetInfos) == 0 && baseInfo.URL != "" {
|
if len(targetInfos) == 0 && baseInfo.URL != "" {
|
||||||
urlInfo := s.createTargetFromURL(baseInfo, baseInfo.URL)
|
urlInfo := s.createTargetFromURLWithSession(baseInfo, baseInfo.URL, session)
|
||||||
if urlInfo != nil {
|
if urlInfo != nil {
|
||||||
targetInfos = append(targetInfos, *urlInfo)
|
targetInfos = append(targetInfos, *urlInfo)
|
||||||
}
|
}
|
||||||
@@ -374,6 +354,10 @@ func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common
|
|||||||
|
|
||||||
// createTargetFromURL 从URL创建目标信息
|
// createTargetFromURL 从URL创建目标信息
|
||||||
func (s *WebScanStrategy) createTargetFromURL(baseInfo common.HostInfo, urlStr string) *common.HostInfo {
|
func (s *WebScanStrategy) createTargetFromURL(baseInfo common.HostInfo, urlStr string) *common.HostInfo {
|
||||||
|
return s.createTargetFromURLWithSession(baseInfo, urlStr, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebScanStrategy) createTargetFromURLWithSession(baseInfo common.HostInfo, urlStr string, session *common.ScanSession) *common.HostInfo {
|
||||||
// 确保URL包含协议头
|
// 确保URL包含协议头
|
||||||
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
|
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
|
||||||
urlStr = "http://" + urlStr
|
urlStr = "http://" + urlStr
|
||||||
@@ -382,7 +366,7 @@ func (s *WebScanStrategy) createTargetFromURL(baseInfo common.HostInfo, urlStr s
|
|||||||
// 解析URL获取Host和Port信息
|
// 解析URL获取Host和Port信息
|
||||||
parsedURL, err := url.Parse(urlStr)
|
parsedURL, err := url.Parse(urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(i18n.Tr("url_parse_failed", urlStr, err))
|
session.LogError(i18n.Tr("url_parse_failed", urlStr, err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ web_scanner_test.go - WebScanner核心逻辑测试
|
|||||||
4. 指纹缓存 - SetFingerprints, GetFingerprints
|
4. 指纹缓存 - SetFingerprints, GetFingerprints
|
||||||
|
|
||||||
不测试的部分(需要集成测试):
|
不测试的部分(需要集成测试):
|
||||||
- createHTTPClient - 依赖全局配置
|
|
||||||
- tryHTTP, DetectHTTPServiceOnly - 网络IO
|
- tryHTTP, DetectHTTPServiceOnly - 网络IO
|
||||||
- Execute - 完整流程
|
- Execute - 完整流程
|
||||||
|
|
||||||
@@ -765,3 +764,57 @@ func TestDetectHTTPScheme(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateHTTPClientUsesPerSessionProxy(t *testing.T) {
|
||||||
|
cfgA := common.NewConfig()
|
||||||
|
cfgA.Network.WebTimeout = time.Second
|
||||||
|
cfgA.Network.HTTPProxy = "http://127.0.0.1:18080"
|
||||||
|
sessionA := common.NewScanSession(cfgA, common.NewState(), &common.FlagVars{})
|
||||||
|
|
||||||
|
cfgB := common.NewConfig()
|
||||||
|
cfgB.Network.WebTimeout = time.Second
|
||||||
|
cfgB.Network.HTTPProxy = "http://127.0.0.1:28080"
|
||||||
|
sessionB := common.NewScanSession(cfgB, common.NewState(), &common.FlagVars{})
|
||||||
|
|
||||||
|
clientA := createHTTPClient(cfgA, sessionA)
|
||||||
|
clientB := createHTTPClient(cfgB, sessionB)
|
||||||
|
if clientA == clientB {
|
||||||
|
t.Fatal("createHTTPClient reused a process-wide client")
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyA := proxyForTest(t, clientA)
|
||||||
|
proxyB := proxyForTest(t, clientB)
|
||||||
|
if proxyA == proxyB {
|
||||||
|
t.Fatalf("proxy URLs should be per config, both were %q", proxyA)
|
||||||
|
}
|
||||||
|
if proxyA != "http://127.0.0.1:18080" {
|
||||||
|
t.Fatalf("proxyA = %q, want http://127.0.0.1:18080", proxyA)
|
||||||
|
}
|
||||||
|
if proxyB != "http://127.0.0.1:28080" {
|
||||||
|
t.Fatalf("proxyB = %q, want http://127.0.0.1:28080", proxyB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxyForTest(t *testing.T, client *http.Client) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
transport, ok := client.Transport.(*http.Transport)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("client transport is not *http.Transport")
|
||||||
|
}
|
||||||
|
if transport.Proxy == nil {
|
||||||
|
t.Fatal("client proxy is nil")
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
proxyURL, err := transport.Proxy(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if proxyURL == nil {
|
||||||
|
t.Fatal("proxy URL is nil")
|
||||||
|
}
|
||||||
|
return proxyURL.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
fscan "github.com/shadow1ng/fscan/pkg/fscan"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := fscan.Config{
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
Threads: 64,
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Plugins: []string{"ssh", "mysql", "redis"},
|
||||||
|
}
|
||||||
|
target := fscan.Target{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Ports: []int{22, 3306, 6379},
|
||||||
|
}
|
||||||
|
if err := fscan.ValidateConfig(config, target); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := fscan.NewScanner(config)
|
||||||
|
results, err := scanner.Scan(context.Background(), target)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := fscan.SummarizeResults(results)
|
||||||
|
fmt.Printf("scan finished: %+v\n", summary)
|
||||||
|
for _, result := range results {
|
||||||
|
if result.IsService() || result.IsVuln() {
|
||||||
|
fmt.Printf("%s %s %s\n", result.Type, result.Target, result.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
fscan "github.com/shadow1ng/fscan/pkg/fscan"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
for _, plugin := range fscan.ListPlugins() {
|
||||||
|
if plugin.Default {
|
||||||
|
fmt.Printf("default plugin: %s ports=%v safe=%v\n", plugin.Name, plugin.Ports, plugin.Safe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config := fscan.Config{
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
Threads: 64,
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Plugins: []string{"ssh", "mysql", "redis"},
|
||||||
|
}
|
||||||
|
target := fscan.Target{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Ports: []int{22, 3306, 6379},
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary fscan.ResultSummary
|
||||||
|
scanner := fscan.NewScanner(config)
|
||||||
|
err := scanner.ScanEach(context.Background(), func(result fscan.Result) error {
|
||||||
|
summary.Add(result)
|
||||||
|
if service, ok := result.Service(); ok {
|
||||||
|
fmt.Printf("service=%s target=%s\n", service, result.Target)
|
||||||
|
}
|
||||||
|
if result.IsCredential() {
|
||||||
|
username, _ := result.Username()
|
||||||
|
password, _ := result.Password()
|
||||||
|
fmt.Printf("credential target=%s username=%s password=%s\n", result.Target, username, password)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, target)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("stream summary: %+v\n", summary)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# fscan SDK
|
||||||
|
|
||||||
|
`pkg/fscan` exposes fscan as an embeddable Go scanner while keeping the CLI unchanged.
|
||||||
|
|
||||||
|
See `examples/embed-basic` for slice-based collection and `examples/embed-stream` for streaming integration.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
fscan "github.com/shadow1ng/fscan/pkg/fscan"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := fscan.Config{
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
Threads: 128,
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Plugins: []string{"ssh", "mysql", "redis"},
|
||||||
|
OnResult: func(result fscan.Result) {
|
||||||
|
if result.Type == fscan.ResultTypeService || result.Type == fscan.ResultTypeVuln {
|
||||||
|
fmt.Printf("%s %s %s\n", result.Type, result.Target, result.Status)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := fscan.ValidateConfig(config, fscan.Target{Host: "192.168.1.10"}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := fscan.NewScanner(config)
|
||||||
|
err := scanner.ScanEach(context.Background(), func(result fscan.Result) error {
|
||||||
|
// Store, forward, or filter the result in the embedding system.
|
||||||
|
return nil
|
||||||
|
}, fscan.Target{
|
||||||
|
Host: "192.168.1.10",
|
||||||
|
Ports: []int{22, 3306, 6379},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("scan finished")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The SDK currently reuses fscan's existing scan core and plugin registry. Embedded scans build per-session runtime state and can run concurrently.
|
||||||
|
|
||||||
|
By default, the SDK runs a conservative service-oriented plugin set and blocks plugins with local side effects or active POC behavior. Set `AllowUnsafePlugins` only when the embedding system explicitly wants those capabilities.
|
||||||
|
|
||||||
|
## API surface
|
||||||
|
|
||||||
|
| Area | API |
|
||||||
|
| --- | --- |
|
||||||
|
| Scanning | `NewScanner`, `Scan`, `ScanEach` |
|
||||||
|
| Configuration | `Config`, `Target`, `CredentialPair`, `ValidateConfig` |
|
||||||
|
| Plugins | `DefaultSafePlugins`, `ListPlugins`, `GetPlugin`, `IsSafePlugin`, `PluginInfo` |
|
||||||
|
| Results | `Result`, `ResultTypeHost`, `ResultTypePort`, `ResultTypeService`, `ResultTypeVuln` |
|
||||||
|
| Result helpers | `Port`, `Service`, `Plugin`, `Username`, `Password`, `Banner`, `Vulnerability`, `URL`, `Protocol`, `IsWeb`, `IsCredential` |
|
||||||
|
| Summary | `SummarizeResults`, `ResultSummary.Add` |
|
||||||
|
|
||||||
|
Use `Scan` when you want all results returned as a slice. Use `ScanEach` when results should be streamed into another system; handler calls are serialized, and returning an error stops the scan and returns that error.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Package fscan exposes fscan as an embeddable scanner library.
|
||||||
|
//
|
||||||
|
// The package is intentionally thin: it reuses the existing scan core and
|
||||||
|
// plugin registry, while hiding CLI flags, stdout output, and result files from
|
||||||
|
// callers. Embedded scans build per-session runtime state and can run
|
||||||
|
// concurrently.
|
||||||
|
//
|
||||||
|
// Embedded callers can use ValidateConfig before starting a scan, IsSafePlugin
|
||||||
|
// or ListPlugins to build plugin allow lists, and ResultType* constants instead
|
||||||
|
// of matching raw result type strings. Result exposes helpers for common detail
|
||||||
|
// fields such as port, service, plugin, credentials, and web metadata. Use
|
||||||
|
// SummarizeResults or ResultSummary for aggregate counts. Use ScanEach for
|
||||||
|
// streaming consumption when callers do not want to retain the full result set
|
||||||
|
// in memory.
|
||||||
|
package fscan
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package fscan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsHost reports whether the result describes a live host.
|
||||||
|
func (r Result) IsHost() bool { return r.Type == ResultTypeHost }
|
||||||
|
|
||||||
|
// IsPort reports whether the result describes an open port.
|
||||||
|
func (r Result) IsPort() bool { return r.Type == ResultTypePort }
|
||||||
|
|
||||||
|
// IsService reports whether the result describes a service.
|
||||||
|
func (r Result) IsService() bool { return r.Type == ResultTypeService }
|
||||||
|
|
||||||
|
// IsVuln reports whether the result describes a vulnerability or credential.
|
||||||
|
func (r Result) IsVuln() bool { return r.Type == ResultTypeVuln }
|
||||||
|
|
||||||
|
// IsCredential reports whether the result describes a weak credential finding.
|
||||||
|
func (r Result) IsCredential() bool {
|
||||||
|
if resultType, ok := r.DetailString("type"); ok && resultType == "weak_credential" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(r.Status, "weak_credential:")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SummarizeResults counts common result categories.
|
||||||
|
func SummarizeResults(results []Result) ResultSummary {
|
||||||
|
var summary ResultSummary
|
||||||
|
for _, result := range results {
|
||||||
|
summary.Add(result)
|
||||||
|
}
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add includes one result in the summary.
|
||||||
|
func (s *ResultSummary) Add(result Result) {
|
||||||
|
s.Total++
|
||||||
|
switch {
|
||||||
|
case result.IsHost():
|
||||||
|
s.Hosts++
|
||||||
|
case result.IsPort():
|
||||||
|
s.Ports++
|
||||||
|
case result.IsService():
|
||||||
|
s.Services++
|
||||||
|
case result.IsVuln():
|
||||||
|
s.Vulns++
|
||||||
|
}
|
||||||
|
if result.IsWeb() {
|
||||||
|
s.Web++
|
||||||
|
}
|
||||||
|
if result.IsCredential() {
|
||||||
|
s.Credentials++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetailString returns a string detail value.
|
||||||
|
func (r Result) DetailString(key string) (string, bool) {
|
||||||
|
value, ok := r.Details[key]
|
||||||
|
if !ok || value == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
return v, true
|
||||||
|
case fmt.Stringer:
|
||||||
|
return v.String(), true
|
||||||
|
default:
|
||||||
|
return fmt.Sprint(v), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetailInt returns an integer detail value.
|
||||||
|
func (r Result) DetailInt(key string) (int, bool) {
|
||||||
|
value, ok := r.Details[key]
|
||||||
|
if !ok || value == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case int:
|
||||||
|
return v, true
|
||||||
|
case int8:
|
||||||
|
return int(v), true
|
||||||
|
case int16:
|
||||||
|
return int(v), true
|
||||||
|
case int32:
|
||||||
|
return int(v), true
|
||||||
|
case int64:
|
||||||
|
return intFromInt64(v)
|
||||||
|
case uint:
|
||||||
|
return intFromUint64(uint64(v))
|
||||||
|
case uint8:
|
||||||
|
return int(v), true
|
||||||
|
case uint16:
|
||||||
|
return int(v), true
|
||||||
|
case uint32:
|
||||||
|
return intFromUint64(uint64(v))
|
||||||
|
case uint64:
|
||||||
|
return intFromUint64(v)
|
||||||
|
case float32:
|
||||||
|
return intFromFloat64(float64(v))
|
||||||
|
case float64:
|
||||||
|
return intFromFloat64(v)
|
||||||
|
case string:
|
||||||
|
n, err := strconv.Atoi(strings.TrimSpace(v))
|
||||||
|
return n, err == nil
|
||||||
|
case fmt.Stringer:
|
||||||
|
n, err := strconv.Atoi(strings.TrimSpace(v.String()))
|
||||||
|
return n, err == nil
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetailBool returns a boolean detail value.
|
||||||
|
func (r Result) DetailBool(key string) (bool, bool) {
|
||||||
|
value, ok := r.Details[key]
|
||||||
|
if !ok || value == nil {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case bool:
|
||||||
|
return v, true
|
||||||
|
case string:
|
||||||
|
b, err := strconv.ParseBool(strings.TrimSpace(v))
|
||||||
|
return b, err == nil
|
||||||
|
default:
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port returns the result port from details, or from a target in host:port form.
|
||||||
|
func (r Result) Port() (int, bool) {
|
||||||
|
if port, ok := r.DetailInt("port"); ok {
|
||||||
|
return port, true
|
||||||
|
}
|
||||||
|
if _, portText, err := net.SplitHostPort(r.Target); err == nil {
|
||||||
|
port, err := strconv.Atoi(portText)
|
||||||
|
return port, err == nil
|
||||||
|
}
|
||||||
|
if strings.Count(r.Target, ":") == 1 {
|
||||||
|
if idx := strings.LastIndex(r.Target, ":"); idx >= 0 && idx+1 < len(r.Target) {
|
||||||
|
port, err := strconv.Atoi(r.Target[idx+1:])
|
||||||
|
return port, err == nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service returns the detected service name when present.
|
||||||
|
func (r Result) Service() (string, bool) { return r.DetailString("service") }
|
||||||
|
|
||||||
|
// Plugin returns the plugin that produced the result when present.
|
||||||
|
func (r Result) Plugin() (string, bool) { return r.DetailString("plugin") }
|
||||||
|
|
||||||
|
// Username returns the credential username when present.
|
||||||
|
func (r Result) Username() (string, bool) { return r.DetailString("username") }
|
||||||
|
|
||||||
|
// Password returns the credential password when present.
|
||||||
|
func (r Result) Password() (string, bool) { return r.DetailString("password") }
|
||||||
|
|
||||||
|
// Banner returns the service banner when present.
|
||||||
|
func (r Result) Banner() (string, bool) { return r.DetailString("banner") }
|
||||||
|
|
||||||
|
// Vulnerability returns the vulnerability description when present.
|
||||||
|
func (r Result) Vulnerability() (string, bool) { return r.DetailString("vulnerability") }
|
||||||
|
|
||||||
|
// URL returns the web result URL when present.
|
||||||
|
func (r Result) URL() (string, bool) { return r.DetailString("url") }
|
||||||
|
|
||||||
|
// Protocol returns the detected protocol when present.
|
||||||
|
func (r Result) Protocol() (string, bool) { return r.DetailString("protocol") }
|
||||||
|
|
||||||
|
// IsWeb reports whether the result is associated with an HTTP(S) service.
|
||||||
|
func (r Result) IsWeb() bool {
|
||||||
|
if ok, found := r.DetailBool("is_web"); found {
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
for _, getter := range []func() (string, bool){r.Service, r.Protocol} {
|
||||||
|
value, ok := getter()
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value = strings.ToLower(value)
|
||||||
|
if value == "http" || value == "https" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func intFromInt64(v int64) (int, bool) {
|
||||||
|
max := int64(^uint(0) >> 1)
|
||||||
|
min := -max - 1
|
||||||
|
if v < min || v > max {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return int(v), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func intFromUint64(v uint64) (int, bool) {
|
||||||
|
max := uint64(^uint(0) >> 1)
|
||||||
|
if v > max {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return int(v), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func intFromFloat64(v float64) (int, bool) {
|
||||||
|
n := int64(v)
|
||||||
|
if float64(n) != v {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return intFromInt64(n)
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package fscan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResultHelpers(t *testing.T) {
|
||||||
|
result := Result{
|
||||||
|
Type: ResultTypeService,
|
||||||
|
Target: "127.0.0.1:8080",
|
||||||
|
Status: "identified",
|
||||||
|
Details: map[string]interface{}{
|
||||||
|
"port": float64(8080),
|
||||||
|
"service": "http",
|
||||||
|
"plugin": "webtitle",
|
||||||
|
"banner": "nginx",
|
||||||
|
"is_web": "true",
|
||||||
|
"protocol": "http",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.IsService() || result.IsPort() {
|
||||||
|
t.Fatalf("unexpected type helpers for %q", result.Type)
|
||||||
|
}
|
||||||
|
if port, ok := result.Port(); !ok || port != 8080 {
|
||||||
|
t.Fatalf("Port = %d/%v, want 8080/true", port, ok)
|
||||||
|
}
|
||||||
|
if service, ok := result.Service(); !ok || service != "http" {
|
||||||
|
t.Fatalf("Service = %q/%v, want http/true", service, ok)
|
||||||
|
}
|
||||||
|
if plugin, ok := result.Plugin(); !ok || plugin != "webtitle" {
|
||||||
|
t.Fatalf("Plugin = %q/%v, want webtitle/true", plugin, ok)
|
||||||
|
}
|
||||||
|
if banner, ok := result.Banner(); !ok || banner != "nginx" {
|
||||||
|
t.Fatalf("Banner = %q/%v, want nginx/true", banner, ok)
|
||||||
|
}
|
||||||
|
if !result.IsWeb() {
|
||||||
|
t.Fatal("expected web result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultPortFallback(t *testing.T) {
|
||||||
|
result := Result{Target: "[::1]:22"}
|
||||||
|
|
||||||
|
port, ok := result.Port()
|
||||||
|
if !ok || port != 22 {
|
||||||
|
t.Fatalf("Port = %d/%v, want 22/true", port, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultPortDoesNotParseBareIPv6(t *testing.T) {
|
||||||
|
result := Result{Target: "2001:db8::1"}
|
||||||
|
|
||||||
|
if port, ok := result.Port(); ok {
|
||||||
|
t.Fatalf("Port = %d/true, want false", port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultCredentialHelpers(t *testing.T) {
|
||||||
|
result := Result{
|
||||||
|
Type: ResultTypeVuln,
|
||||||
|
Details: map[string]interface{}{
|
||||||
|
"type": "weak_credential",
|
||||||
|
"username": "root",
|
||||||
|
"password": "toor",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.IsVuln() {
|
||||||
|
t.Fatal("expected vuln result")
|
||||||
|
}
|
||||||
|
if !result.IsCredential() {
|
||||||
|
t.Fatal("expected credential result")
|
||||||
|
}
|
||||||
|
if username, ok := result.Username(); !ok || username != "root" {
|
||||||
|
t.Fatalf("Username = %q/%v, want root/true", username, ok)
|
||||||
|
}
|
||||||
|
if password, ok := result.Password(); !ok || password != "toor" {
|
||||||
|
t.Fatalf("Password = %q/%v, want toor/true", password, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSummarizeResults(t *testing.T) {
|
||||||
|
results := []Result{
|
||||||
|
{Type: ResultTypeHost, Target: "127.0.0.1"},
|
||||||
|
{Type: ResultTypePort, Target: "127.0.0.1", Details: map[string]interface{}{"port": 80}},
|
||||||
|
{Type: ResultTypeService, Target: "127.0.0.1:80", Details: map[string]interface{}{"service": "http"}},
|
||||||
|
{Type: ResultTypeVuln, Target: "127.0.0.1:22", Status: "weak_credential: root:toor"},
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := SummarizeResults(results)
|
||||||
|
if summary.Total != 4 {
|
||||||
|
t.Fatalf("Total = %d, want 4", summary.Total)
|
||||||
|
}
|
||||||
|
if summary.Hosts != 1 || summary.Ports != 1 || summary.Services != 1 || summary.Vulns != 1 {
|
||||||
|
t.Fatalf("summary categories = %#v, want one each", summary)
|
||||||
|
}
|
||||||
|
if summary.Web != 1 {
|
||||||
|
t.Fatalf("Web = %d, want 1", summary.Web)
|
||||||
|
}
|
||||||
|
if summary.Credentials != 1 {
|
||||||
|
t.Fatalf("Credentials = %d, want 1", summary.Credentials)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultDetailIntRejectsFraction(t *testing.T) {
|
||||||
|
result := Result{Details: map[string]interface{}{"port": 22.5}}
|
||||||
|
|
||||||
|
if port, ok := result.DetailInt("port"); ok {
|
||||||
|
t.Fatalf("DetailInt = %d/true, want false", port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultDetailIntParsesJSONNumber(t *testing.T) {
|
||||||
|
result := Result{Details: map[string]interface{}{"port": json.Number("443")}}
|
||||||
|
|
||||||
|
port, ok := result.DetailInt("port")
|
||||||
|
if !ok || port != 443 {
|
||||||
|
t.Fatalf("DetailInt = %d/%v, want 443/true", port, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
package fscan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
commonconfig "github.com/shadow1ng/fscan/common/config"
|
||||||
|
"github.com/shadow1ng/fscan/common/i18n"
|
||||||
|
"github.com/shadow1ng/fscan/common/output"
|
||||||
|
"github.com/shadow1ng/fscan/core"
|
||||||
|
"github.com/shadow1ng/fscan/plugins"
|
||||||
|
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/local"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/services"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultSafePlugins = []string{
|
||||||
|
"activemq",
|
||||||
|
"cassandra",
|
||||||
|
"elasticsearch",
|
||||||
|
"ftp",
|
||||||
|
"kafka",
|
||||||
|
"ldap",
|
||||||
|
"memcached",
|
||||||
|
"mongodb",
|
||||||
|
"mssql",
|
||||||
|
"mysql",
|
||||||
|
"neo4j",
|
||||||
|
"netbios",
|
||||||
|
"oracle",
|
||||||
|
"postgresql",
|
||||||
|
"rabbitmq",
|
||||||
|
"rdp",
|
||||||
|
"redis",
|
||||||
|
"rsync",
|
||||||
|
"smb",
|
||||||
|
"smtp",
|
||||||
|
"ssh",
|
||||||
|
"telnet",
|
||||||
|
"vnc",
|
||||||
|
"webtitle",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scanner runs fscan from another Go process.
|
||||||
|
type Scanner struct {
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScanner creates an embedded scanner.
|
||||||
|
func NewScanner(config Config) *Scanner {
|
||||||
|
return &Scanner{config: config}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultSafePlugins returns the plugin set used by the SDK when Config.Plugins
|
||||||
|
// is empty. The returned slice can be modified by callers.
|
||||||
|
func DefaultSafePlugins() []string {
|
||||||
|
return append([]string(nil), defaultSafePlugins...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPlugins returns metadata for all registered plugins, sorted by name.
|
||||||
|
func ListPlugins() []PluginInfo {
|
||||||
|
names := plugins.All()
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
items := make([]PluginInfo, 0, len(names))
|
||||||
|
for _, name := range names {
|
||||||
|
if info, ok := GetPlugin(name); ok {
|
||||||
|
items = append(items, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlugin returns metadata for a registered plugin.
|
||||||
|
func GetPlugin(name string) (PluginInfo, bool) {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" || !plugins.Exists(name) {
|
||||||
|
return PluginInfo{}, false
|
||||||
|
}
|
||||||
|
return PluginInfo{
|
||||||
|
Name: name,
|
||||||
|
Types: pluginTypes(name),
|
||||||
|
Ports: pluginPorts(name),
|
||||||
|
Safe: IsSafePlugin(name),
|
||||||
|
Default: isDefaultSafePlugin(name),
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig checks whether a config and target set can be used for an
|
||||||
|
// embedded scan. If no targets are passed, Config.Targets is validated.
|
||||||
|
func ValidateConfig(config Config, targets ...Target) error {
|
||||||
|
if len(targets) == 0 {
|
||||||
|
targets = config.Targets
|
||||||
|
}
|
||||||
|
return validateConfig(config, targets)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSafePlugin reports whether a plugin may be used while AllowUnsafePlugins is
|
||||||
|
// false. Unknown plugin names are not safe.
|
||||||
|
func IsSafePlugin(name string) bool {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" || !plugins.Exists(name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return plugins.IsSafe(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan runs the scanner for the provided targets and returns structured
|
||||||
|
// findings. If no targets are provided, Config.Targets is used.
|
||||||
|
func (s *Scanner) Scan(ctx context.Context, targets ...Target) ([]Result, error) {
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
results []Result
|
||||||
|
)
|
||||||
|
err := s.ScanEach(ctx, func(result Result) error {
|
||||||
|
mu.Lock()
|
||||||
|
results = append(results, result)
|
||||||
|
mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}, targets...)
|
||||||
|
return snapshotResults(&mu, results), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanEach runs the scanner and calls handle serially for each structured
|
||||||
|
// result without retaining all results in memory. If handle returns an error,
|
||||||
|
// the scan context is canceled and that error is returned.
|
||||||
|
func (s *Scanner) ScanEach(ctx context.Context, handle ResultHandler, targets ...Target) error {
|
||||||
|
if handle == nil {
|
||||||
|
return fmt.Errorf("fscan: result handler is required")
|
||||||
|
}
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
if len(targets) == 0 {
|
||||||
|
targets = s.config.Targets
|
||||||
|
}
|
||||||
|
if err := validateConfig(s.config, targets); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
restoreLogger := common.PushSilentLogger()
|
||||||
|
defer restoreLogger()
|
||||||
|
|
||||||
|
var (
|
||||||
|
errMu sync.Mutex
|
||||||
|
handleMu sync.Mutex
|
||||||
|
handlerErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, target := range targets {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
if stored := getHandlerError(&errMu, &handlerErr); stored != nil {
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sink := func(raw *output.ScanResult) error {
|
||||||
|
if result, ok := convertOutputResult(raw); ok {
|
||||||
|
handleMu.Lock()
|
||||||
|
if err := handle(result); err != nil {
|
||||||
|
handleMu.Unlock()
|
||||||
|
setHandlerError(&errMu, &handlerErr, err)
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.config.OnResult != nil {
|
||||||
|
s.config.OnResult(result)
|
||||||
|
}
|
||||||
|
handleMu.Unlock()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := s.scanOne(ctx, target, sink); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if stored := getHandlerError(&errMu, &handlerErr); stored != nil {
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stored := getHandlerError(&errMu, &handlerErr); stored != nil {
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) scanOne(ctx context.Context, target Target, sink common.ResultSink) error {
|
||||||
|
fv := buildFlagVars(s.config, target)
|
||||||
|
info := common.HostInfo{Host: strings.TrimSpace(target.Host), URL: strings.TrimSpace(target.URL)}
|
||||||
|
|
||||||
|
previousLanguage := i18n.GetLanguage()
|
||||||
|
i18n.SetLanguage(fv.Language)
|
||||||
|
defer i18n.SetLanguage(previousLanguage)
|
||||||
|
|
||||||
|
cfg, state, err := common.BuildConfig(fv, &info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(s.config.UserPassPairs) > 0 {
|
||||||
|
cfg.Credentials.UserPassPairs = make([]commonconfig.CredentialPair, 0, len(s.config.UserPassPairs))
|
||||||
|
for _, pair := range s.config.UserPassPairs {
|
||||||
|
cfg.Credentials.UserPassPairs = append(cfg.Credentials.UserPassPairs, commonconfig.CredentialPair{
|
||||||
|
Username: pair.Username,
|
||||||
|
Password: pair.Password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.Output.DisableSave = true
|
||||||
|
cfg.Output.Silent = true
|
||||||
|
cfg.Output.DisableProgress = true
|
||||||
|
cfg.Output.ShowProgress = false
|
||||||
|
|
||||||
|
session := common.NewScanSession(cfg, state, fv)
|
||||||
|
session.ResultSink = sink
|
||||||
|
core.RunScan(ctx, info, session)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfig(config Config, targets []Target) error {
|
||||||
|
if len(targets) == 0 {
|
||||||
|
return fmt.Errorf("fscan: at least one target is required")
|
||||||
|
}
|
||||||
|
for _, name := range normalizePlugins(config.Plugins) {
|
||||||
|
if !plugins.Exists(name) {
|
||||||
|
return fmt.Errorf("fscan: plugin %q not found", name)
|
||||||
|
}
|
||||||
|
if !config.AllowUnsafePlugins && !IsSafePlugin(name) {
|
||||||
|
return fmt.Errorf("fscan: plugin %q is not enabled for embedded safe mode", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, target := range targets {
|
||||||
|
if strings.TrimSpace(target.Host) == "" && strings.TrimSpace(target.URL) == "" {
|
||||||
|
return fmt.Errorf("fscan: target host or URL is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(target.Host) != "" && strings.TrimSpace(target.URL) != "" {
|
||||||
|
return fmt.Errorf("fscan: target cannot set both Host and URL")
|
||||||
|
}
|
||||||
|
for _, port := range target.Ports {
|
||||||
|
if port < 1 || port > 65535 {
|
||||||
|
return fmt.Errorf("fscan: invalid port %d", port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, port := range config.Ports {
|
||||||
|
if port < 1 || port > 65535 {
|
||||||
|
return fmt.Errorf("fscan: invalid port %d", port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFlagVars(config Config, target Target) *common.FlagVars {
|
||||||
|
timeout := secondsOrDefault(config.Timeout, common.DefaultTimeout)
|
||||||
|
webTimeout := secondsOrDefault(config.WebTimeout, 5)
|
||||||
|
|
||||||
|
threadNum := config.Threads
|
||||||
|
if threadNum <= 0 {
|
||||||
|
threadNum = common.DefaultThreadNum
|
||||||
|
}
|
||||||
|
moduleThreads := config.ModuleThreads
|
||||||
|
if moduleThreads <= 0 {
|
||||||
|
moduleThreads = 20
|
||||||
|
}
|
||||||
|
maxRetries := config.MaxRetries
|
||||||
|
if maxRetries <= 0 {
|
||||||
|
maxRetries = 3
|
||||||
|
}
|
||||||
|
maxRedirects := config.MaxRedirects
|
||||||
|
if maxRedirects <= 0 {
|
||||||
|
maxRedirects = 10
|
||||||
|
}
|
||||||
|
pocConcurrency := config.POCConcurrency
|
||||||
|
if pocConcurrency <= 0 {
|
||||||
|
pocConcurrency = 20
|
||||||
|
}
|
||||||
|
icmpRate := config.ICMPRate
|
||||||
|
if icmpRate <= 0 {
|
||||||
|
icmpRate = 0.1
|
||||||
|
}
|
||||||
|
language := config.Language
|
||||||
|
if language == "" {
|
||||||
|
language = common.DefaultLanguage
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := config.Ports
|
||||||
|
if len(target.Ports) > 0 {
|
||||||
|
ports = target.Ports
|
||||||
|
}
|
||||||
|
|
||||||
|
return &common.FlagVars{
|
||||||
|
Host: strings.TrimSpace(target.Host),
|
||||||
|
Ports: formatPorts(ports),
|
||||||
|
ScanMode: formatPlugins(config),
|
||||||
|
ThreadNum: threadNum,
|
||||||
|
ModuleThreadNum: moduleThreads,
|
||||||
|
TimeoutSec: timeout,
|
||||||
|
GlobalTimeout: 180,
|
||||||
|
DisablePing: config.DisablePing,
|
||||||
|
DisableTcpProbe: config.DisableTCPProbe,
|
||||||
|
AliveOnly: false,
|
||||||
|
DisableBrute: config.DisableBrute,
|
||||||
|
MaxRetries: maxRetries,
|
||||||
|
Username: strings.Join(config.Usernames, ","),
|
||||||
|
Password: strings.Join(config.Passwords, ","),
|
||||||
|
Domain: config.Domain,
|
||||||
|
SSHKeyPath: config.SSHKeyPath,
|
||||||
|
TargetURL: strings.TrimSpace(target.URL),
|
||||||
|
WebTimeout: webTimeout,
|
||||||
|
MaxRedirects: maxRedirects,
|
||||||
|
HTTPProxy: config.HTTPProxy,
|
||||||
|
Socks5Proxy: config.Socks5Proxy,
|
||||||
|
Iface: config.Interface,
|
||||||
|
PocPath: config.POCPath,
|
||||||
|
PocName: config.POCName,
|
||||||
|
PocFull: config.POCFull,
|
||||||
|
PocNum: pocConcurrency,
|
||||||
|
DisablePocScan: config.DisablePOCScan,
|
||||||
|
PacketRateLimit: config.PacketRateLimit,
|
||||||
|
MaxPacketCount: config.MaxPacketCount,
|
||||||
|
ICMPRate: icmpRate,
|
||||||
|
Outputfile: "result.txt",
|
||||||
|
OutputFormat: "txt",
|
||||||
|
DisableSave: true,
|
||||||
|
Silent: true,
|
||||||
|
NoColor: true,
|
||||||
|
LogLevel: common.LogLevelError,
|
||||||
|
DisableProgress: true,
|
||||||
|
Language: language,
|
||||||
|
ForwardShellPort: 4444,
|
||||||
|
KeyloggerOutputFile: "keylog.txt",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPlugins(config Config) string {
|
||||||
|
parts := normalizePlugins(config.Plugins)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
if config.AllowUnsafePlugins {
|
||||||
|
return "all"
|
||||||
|
}
|
||||||
|
parts = defaultSafePlugins
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPorts(ports []int) string {
|
||||||
|
if len(ports) == 0 {
|
||||||
|
return commonconfig.MainPorts
|
||||||
|
}
|
||||||
|
ports = append([]int(nil), ports...)
|
||||||
|
sort.Ints(ports)
|
||||||
|
parts := make([]string, 0, len(ports))
|
||||||
|
for _, port := range ports {
|
||||||
|
parts = append(parts, strconv.Itoa(port))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePlugins(pluginNames []string) []string {
|
||||||
|
parts := make([]string, 0, len(pluginNames))
|
||||||
|
for _, plugin := range pluginNames {
|
||||||
|
plugin = strings.TrimSpace(plugin)
|
||||||
|
if plugin != "" {
|
||||||
|
parts = append(parts, plugin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginTypes(name string) []string {
|
||||||
|
types := make([]string, 0, 3)
|
||||||
|
for _, pluginType := range []string{PluginTypeService, PluginTypeWeb, PluginTypeLocal} {
|
||||||
|
if plugins.HasType(name, pluginType) {
|
||||||
|
types = append(types, pluginType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginPorts(name string) []int {
|
||||||
|
ports := plugins.GetPluginPorts(name)
|
||||||
|
ports = append([]int(nil), ports...)
|
||||||
|
sort.Ints(ports)
|
||||||
|
return ports
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDefaultSafePlugin(name string) bool {
|
||||||
|
for _, plugin := range defaultSafePlugins {
|
||||||
|
if plugin == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func secondsOrDefault(value time.Duration, fallback int) int64 {
|
||||||
|
if value <= 0 {
|
||||||
|
return int64(fallback)
|
||||||
|
}
|
||||||
|
seconds := int64(value.Round(time.Second) / time.Second)
|
||||||
|
if seconds < 1 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertOutputResult(raw *output.ScanResult) (Result, bool) {
|
||||||
|
if raw == nil {
|
||||||
|
return Result{}, false
|
||||||
|
}
|
||||||
|
result := Result{
|
||||||
|
Time: raw.Time,
|
||||||
|
Type: string(raw.Type),
|
||||||
|
Target: raw.Target,
|
||||||
|
Status: raw.Status,
|
||||||
|
Details: raw.Details,
|
||||||
|
}
|
||||||
|
return result, result.Target != "" || result.Status != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotResults(mu *sync.Mutex, results []Result) []Result {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
return append([]Result(nil), results...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHandlerError(mu *sync.Mutex, target *error, err error) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if *target == nil {
|
||||||
|
*target = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHandlerError(mu *sync.Mutex, err *error) error {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
return *err
|
||||||
|
}
|
||||||
@@ -0,0 +1,512 @@
|
|||||||
|
package fscan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
commonconfig "github.com/shadow1ng/fscan/common/config"
|
||||||
|
"github.com/shadow1ng/fscan/common/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildFlagVarsDefaults(t *testing.T) {
|
||||||
|
fv := buildFlagVars(Config{}, Target{Host: "127.0.0.1"})
|
||||||
|
|
||||||
|
if fv.Host != "127.0.0.1" {
|
||||||
|
t.Fatalf("Host = %q", fv.Host)
|
||||||
|
}
|
||||||
|
if fv.Ports != commonconfig.MainPorts {
|
||||||
|
t.Fatalf("Ports = %q, want MainPorts", fv.Ports)
|
||||||
|
}
|
||||||
|
if fv.ScanMode != formatPlugins(Config{Plugins: DefaultSafePlugins()}) {
|
||||||
|
t.Fatalf("ScanMode = %q, want safe defaults", fv.ScanMode)
|
||||||
|
}
|
||||||
|
if !fv.DisableSave || !fv.Silent || !fv.DisableProgress {
|
||||||
|
t.Fatalf("embedded defaults should disable output side effects")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildFlagVarsBlankPluginsUseSafeDefaults(t *testing.T) {
|
||||||
|
fv := buildFlagVars(Config{Plugins: []string{" ", "\t"}}, Target{Host: "127.0.0.1"})
|
||||||
|
|
||||||
|
if fv.ScanMode != formatPlugins(Config{Plugins: DefaultSafePlugins()}) {
|
||||||
|
t.Fatalf("ScanMode = %q, want safe defaults", fv.ScanMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildFlagVarsTargetPortsOverride(t *testing.T) {
|
||||||
|
fv := buildFlagVars(Config{Ports: []int{22, 80}}, Target{Host: "127.0.0.1", Ports: []int{3306, 22}})
|
||||||
|
|
||||||
|
if fv.Ports != "22,3306" {
|
||||||
|
t.Fatalf("Ports = %q, want sorted target override", fv.Ports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateConfig(t *testing.T) {
|
||||||
|
if err := validateConfig(Config{}, nil); err == nil {
|
||||||
|
t.Fatal("expected missing target error")
|
||||||
|
}
|
||||||
|
if err := validateConfig(Config{}, []Target{{Host: "127.0.0.1", URL: "http://127.0.0.1"}}); err == nil {
|
||||||
|
t.Fatal("expected host/url conflict")
|
||||||
|
}
|
||||||
|
if err := validateConfig(Config{Plugins: []string{"definitely-missing"}}, []Target{{Host: "127.0.0.1"}}); err == nil {
|
||||||
|
t.Fatal("expected missing plugin error")
|
||||||
|
}
|
||||||
|
if err := validateConfig(Config{Plugins: []string{"webpoc"}}, []Target{{URL: "http://127.0.0.1"}}); err == nil {
|
||||||
|
t.Fatal("expected unsafe plugin error")
|
||||||
|
}
|
||||||
|
if err := validateConfig(Config{Plugins: []string{"webpoc"}, AllowUnsafePlugins: true}, []Target{{URL: "http://127.0.0.1"}}); err != nil {
|
||||||
|
t.Fatalf("unsafe plugin with opt-in failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := ValidateConfig(Config{Targets: []Target{{Host: "127.0.0.1"}}}); err != nil {
|
||||||
|
t.Fatalf("ValidateConfig with config targets failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := validateConfig(Config{}, []Target{{Host: "127.0.0.1", Ports: []int{70000}}}); err == nil {
|
||||||
|
t.Fatal("expected invalid port error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsSafePlugin(t *testing.T) {
|
||||||
|
if !IsSafePlugin("ssh") {
|
||||||
|
t.Fatal("ssh should be safe")
|
||||||
|
}
|
||||||
|
if IsSafePlugin("webpoc") {
|
||||||
|
t.Fatal("webpoc should not be safe")
|
||||||
|
}
|
||||||
|
if IsSafePlugin("definitely-missing") {
|
||||||
|
t.Fatal("unknown plugin should not be safe")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListPlugins(t *testing.T) {
|
||||||
|
items := ListPlugins()
|
||||||
|
if len(items) == 0 {
|
||||||
|
t.Fatal("expected registered plugins")
|
||||||
|
}
|
||||||
|
for i := 1; i < len(items); i++ {
|
||||||
|
if items[i-1].Name > items[i].Name {
|
||||||
|
t.Fatalf("plugins not sorted: %q before %q", items[i-1].Name, items[i].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ssh, ok := GetPlugin("ssh")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("missing ssh plugin")
|
||||||
|
}
|
||||||
|
if ssh.Name != "ssh" {
|
||||||
|
t.Fatalf("plugin name = %q, want ssh", ssh.Name)
|
||||||
|
}
|
||||||
|
if !ssh.Safe || !ssh.Default {
|
||||||
|
t.Fatalf("ssh safe/default = %v/%v, want true/true", ssh.Safe, ssh.Default)
|
||||||
|
}
|
||||||
|
if !containsString(ssh.Types, PluginTypeService) {
|
||||||
|
t.Fatalf("ssh types = %#v, want service", ssh.Types)
|
||||||
|
}
|
||||||
|
if !containsInt(ssh.Ports, 22) {
|
||||||
|
t.Fatalf("ssh ports = %#v, want 22", ssh.Ports)
|
||||||
|
}
|
||||||
|
if _, ok := GetPlugin("definitely-missing"); ok {
|
||||||
|
t.Fatal("unknown plugin should not exist")
|
||||||
|
}
|
||||||
|
webpoc, ok := GetPlugin("webpoc")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("missing webpoc plugin")
|
||||||
|
}
|
||||||
|
if webpoc.Safe {
|
||||||
|
t.Fatal("webpoc should be marked unsafe")
|
||||||
|
}
|
||||||
|
if !containsString(webpoc.Types, PluginTypeWeb) {
|
||||||
|
t.Fatalf("webpoc types = %#v, want web", webpoc.Types)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanHonorsCanceledContext(t *testing.T) {
|
||||||
|
scanner := NewScanner(Config{
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Plugins: []string{"redis"},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
_, err := scanner.Scan(ctx, Target{Host: "127.0.0.1", Ports: []int{6379}})
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("Scan error = %v, want context.Canceled", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanCollectsResultsThroughSessionSink(t *testing.T) {
|
||||||
|
listener := startFTPListener(t)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
var callbackCalls int32
|
||||||
|
common.SetResultCallback(func(interface{}) {
|
||||||
|
atomic.AddInt32(&callbackCalls, 1)
|
||||||
|
})
|
||||||
|
defer common.ClearResultCallback()
|
||||||
|
|
||||||
|
var streamed int32
|
||||||
|
scanner := NewScanner(Config{
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Threads: 16,
|
||||||
|
Plugins: []string{"ftp"},
|
||||||
|
OnResult: func(result Result) {
|
||||||
|
atomic.AddInt32(&streamed, 1)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
results, err := scanner.Scan(ctx, Target{Host: "127.0.0.1", Ports: []int{port}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(results) == 0 {
|
||||||
|
t.Fatal("expected SDK results")
|
||||||
|
}
|
||||||
|
if got := atomic.LoadInt32(&streamed); got != int32(len(results)) {
|
||||||
|
t.Fatalf("streamed length = %d, want %d", got, len(results))
|
||||||
|
}
|
||||||
|
if !hasResult(results, ResultTypePort, "open", "") {
|
||||||
|
t.Fatalf("missing port result: %#v", results)
|
||||||
|
}
|
||||||
|
if !hasResult(results, ResultTypeService, "FTP", "ftp") {
|
||||||
|
t.Fatalf("missing ftp plugin result: %#v", results)
|
||||||
|
}
|
||||||
|
if got := atomic.LoadInt32(&callbackCalls); got != 0 {
|
||||||
|
t.Fatalf("global callback calls = %d, want 0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanEachStreamsResults(t *testing.T) {
|
||||||
|
listener := startFTPListener(t)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
scanner := NewScanner(Config{
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Threads: 16,
|
||||||
|
Plugins: []string{"ftp"},
|
||||||
|
})
|
||||||
|
|
||||||
|
var results []Result
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
err := scanner.ScanEach(ctx, func(result Result) error {
|
||||||
|
results = append(results, result)
|
||||||
|
return nil
|
||||||
|
}, Target{Host: "127.0.0.1", Ports: []int{port}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !hasResult(results, ResultTypePort, "open", "") {
|
||||||
|
t.Fatalf("missing port result: %#v", results)
|
||||||
|
}
|
||||||
|
if !hasResult(results, ResultTypeService, "FTP", "ftp") {
|
||||||
|
t.Fatalf("missing ftp plugin result: %#v", results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanUsesConfigTargets(t *testing.T) {
|
||||||
|
listener := startFTPListener(t)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
scanner := NewScanner(Config{
|
||||||
|
Targets: []Target{{Host: "127.0.0.1", Ports: []int{port}}},
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Threads: 16,
|
||||||
|
Plugins: []string{"ftp"},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
results, err := scanner.Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !hasPortResult(results, port) {
|
||||||
|
t.Fatalf("missing configured target port result: %#v", results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanExplicitTargetsOverrideConfigTargets(t *testing.T) {
|
||||||
|
configured := startFTPListener(t)
|
||||||
|
defer configured.Close()
|
||||||
|
explicit := startFTPListener(t)
|
||||||
|
defer explicit.Close()
|
||||||
|
|
||||||
|
configuredPort := configured.Addr().(*net.TCPAddr).Port
|
||||||
|
explicitPort := explicit.Addr().(*net.TCPAddr).Port
|
||||||
|
scanner := NewScanner(Config{
|
||||||
|
Targets: []Target{{Host: "127.0.0.1", Ports: []int{configuredPort}}},
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Threads: 16,
|
||||||
|
Plugins: []string{"ftp"},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
results, err := scanner.Scan(ctx, Target{Host: "127.0.0.1", Ports: []int{explicitPort}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !hasPortResult(results, explicitPort) {
|
||||||
|
t.Fatalf("missing explicit target port result: %#v", results)
|
||||||
|
}
|
||||||
|
if hasPortResult(results, configuredPort) {
|
||||||
|
t.Fatalf("configured target should not run when explicit targets are passed: %#v", results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanEachReturnsHandlerError(t *testing.T) {
|
||||||
|
listener := startFTPListener(t)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
scanner := NewScanner(Config{
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Threads: 16,
|
||||||
|
Plugins: []string{"ftp"},
|
||||||
|
})
|
||||||
|
|
||||||
|
stopErr := errors.New("stop scan")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
err := scanner.ScanEach(ctx, func(Result) error {
|
||||||
|
return stopErr
|
||||||
|
}, Target{Host: "127.0.0.1", Ports: []int{port}})
|
||||||
|
if !errors.Is(err, stopErr) {
|
||||||
|
t.Fatalf("ScanEach error = %v, want %v", err, stopErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanEachRequiresHandler(t *testing.T) {
|
||||||
|
scanner := NewScanner(Config{Targets: []Target{{Host: "127.0.0.1"}}})
|
||||||
|
|
||||||
|
if err := scanner.ScanEach(context.Background(), nil); err == nil {
|
||||||
|
t.Fatal("expected missing handler error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanEachRunsConcurrent(t *testing.T) {
|
||||||
|
first := startFTPListener(t)
|
||||||
|
defer first.Close()
|
||||||
|
second := startFTPListener(t)
|
||||||
|
defer second.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
blocked := make(chan struct{})
|
||||||
|
release := make(chan struct{})
|
||||||
|
firstErr := make(chan error, 1)
|
||||||
|
var blockOnce sync.Once
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
scanner := NewScanner(Config{
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Threads: 16,
|
||||||
|
Plugins: []string{"ftp"},
|
||||||
|
})
|
||||||
|
port := first.Addr().(*net.TCPAddr).Port
|
||||||
|
firstErr <- scanner.ScanEach(ctx, func(result Result) error {
|
||||||
|
if result.IsPort() {
|
||||||
|
blockOnce.Do(func() { close(blocked) })
|
||||||
|
select {
|
||||||
|
case <-release:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, Target{Host: "127.0.0.1", Ports: []int{port}})
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-blocked:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("first scan did not reach handler")
|
||||||
|
}
|
||||||
|
|
||||||
|
secondErr := make(chan error, 1)
|
||||||
|
var secondResults []Result
|
||||||
|
go func() {
|
||||||
|
scanner := NewScanner(Config{
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Threads: 16,
|
||||||
|
Plugins: []string{"ftp"},
|
||||||
|
})
|
||||||
|
port := second.Addr().(*net.TCPAddr).Port
|
||||||
|
secondErr <- scanner.ScanEach(ctx, func(result Result) error {
|
||||||
|
secondResults = append(secondResults, result)
|
||||||
|
return nil
|
||||||
|
}, Target{Host: "127.0.0.1", Ports: []int{port}})
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-secondErr:
|
||||||
|
if err != nil {
|
||||||
|
close(release)
|
||||||
|
t.Fatalf("second scan failed: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
close(release)
|
||||||
|
t.Fatal("second scan blocked behind first scan")
|
||||||
|
}
|
||||||
|
if !hasResult(secondResults, ResultTypePort, "open", "") {
|
||||||
|
close(release)
|
||||||
|
t.Fatalf("missing second scan result: %#v", secondResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(release)
|
||||||
|
if err := <-firstErr; err != nil {
|
||||||
|
t.Fatalf("first scan failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanDoesNotReplaceGlobalRuntime(t *testing.T) {
|
||||||
|
listener := startFTPListener(t)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
previousConfig := common.GetGlobalConfig()
|
||||||
|
previousState := common.GetGlobalState()
|
||||||
|
previousFlags := *common.GetFlagVars()
|
||||||
|
previousLanguage := i18n.GetLanguage()
|
||||||
|
defer func() {
|
||||||
|
common.SetGlobalConfig(previousConfig)
|
||||||
|
common.SetGlobalState(previousState)
|
||||||
|
*common.GetFlagVars() = previousFlags
|
||||||
|
i18n.SetLanguage(previousLanguage)
|
||||||
|
}()
|
||||||
|
|
||||||
|
sentinelConfig := common.NewConfig()
|
||||||
|
sentinelState := common.NewState()
|
||||||
|
common.SetGlobalConfig(sentinelConfig)
|
||||||
|
common.SetGlobalState(sentinelState)
|
||||||
|
common.GetFlagVars().LogLevel = "sentinel"
|
||||||
|
i18n.SetLanguage(i18n.LangEN)
|
||||||
|
|
||||||
|
scanner := NewScanner(Config{
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Threads: 16,
|
||||||
|
Plugins: []string{"ftp"},
|
||||||
|
Language: i18n.LangZH,
|
||||||
|
})
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
if _, err := scanner.Scan(context.Background(), Target{Host: "127.0.0.1", Ports: []int{port}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if common.GetGlobalConfig() != sentinelConfig {
|
||||||
|
t.Fatal("SDK scan replaced global config")
|
||||||
|
}
|
||||||
|
if common.GetGlobalState() != sentinelState {
|
||||||
|
t.Fatal("SDK scan replaced global state")
|
||||||
|
}
|
||||||
|
if common.GetFlagVars().LogLevel != "sentinel" {
|
||||||
|
t.Fatal("SDK scan replaced global flags")
|
||||||
|
}
|
||||||
|
if got := i18n.GetLanguage(); got != i18n.LangEN {
|
||||||
|
t.Fatalf("SDK scan leaked global language = %q, want %q", got, i18n.LangEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startFTPListener(t *testing.T) net.Listener {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
_ = conn.SetDeadline(time.Now().Add(2 * time.Second))
|
||||||
|
_, _ = conn.Write([]byte("220 test FTP\r\n"))
|
||||||
|
buf := make([]byte, 64)
|
||||||
|
_, _ = conn.Read(buf)
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsString(items []string, value string) bool {
|
||||||
|
for _, item := range items {
|
||||||
|
if item == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsInt(items []int, value int) bool {
|
||||||
|
for _, item := range items {
|
||||||
|
if item == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasResult(results []Result, resultType, statusText, plugin string) bool {
|
||||||
|
for _, result := range results {
|
||||||
|
if result.Type != resultType || !strings.Contains(result.Status, statusText) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if plugin == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if result.Details != nil && result.Details["plugin"] == plugin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasPortResult(results []Result, port int) bool {
|
||||||
|
for _, result := range results {
|
||||||
|
if !result.IsPort() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got, ok := result.Port(); ok && got == port {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package fscan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PluginTypeWeb marks web-facing plugins.
|
||||||
|
PluginTypeWeb = "web"
|
||||||
|
// PluginTypeLocal marks plugins that operate on the local host.
|
||||||
|
PluginTypeLocal = "local"
|
||||||
|
// PluginTypeService marks network service plugins.
|
||||||
|
PluginTypeService = "service"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ResultTypeHost reports a live host.
|
||||||
|
ResultTypeHost = "HOST"
|
||||||
|
// ResultTypePort reports an open port.
|
||||||
|
ResultTypePort = "PORT"
|
||||||
|
// ResultTypeService reports a service fingerprint or service plugin result.
|
||||||
|
ResultTypeService = "SERVICE"
|
||||||
|
// ResultTypeVuln reports a vulnerability or credential finding.
|
||||||
|
ResultTypeVuln = "VULN"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Target describes one scan target. Use Host for host/IP/CIDR/range service
|
||||||
|
// scans, or URL for web scans. Ports applies only to Host scans.
|
||||||
|
type Target struct {
|
||||||
|
Host string
|
||||||
|
URL string
|
||||||
|
Ports []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialPair pins one username/password pair.
|
||||||
|
type CredentialPair struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginInfo describes one registered scanner plugin.
|
||||||
|
type PluginInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Types []string `json:"types,omitempty"`
|
||||||
|
Ports []int `json:"ports,omitempty"`
|
||||||
|
Safe bool `json:"safe"`
|
||||||
|
Default bool `json:"default"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResultSummary counts common result categories.
|
||||||
|
type ResultSummary struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Hosts int `json:"hosts"`
|
||||||
|
Ports int `json:"ports"`
|
||||||
|
Services int `json:"services"`
|
||||||
|
Vulns int `json:"vulns"`
|
||||||
|
Web int `json:"web"`
|
||||||
|
Credentials int `json:"credentials"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResultHandler receives one structured result. Calls are serialized by the
|
||||||
|
// scanner. Returning an error asks the scanner to stop and returns that error
|
||||||
|
// to the caller.
|
||||||
|
type ResultHandler func(Result) error
|
||||||
|
|
||||||
|
// Config controls an embedded scan. Zero values use the same conservative
|
||||||
|
// defaults as the CLI, except output is silent and file saving is disabled.
|
||||||
|
type Config struct {
|
||||||
|
Targets []Target
|
||||||
|
|
||||||
|
Plugins []string
|
||||||
|
Ports []int
|
||||||
|
// AllowUnsafePlugins permits plugins with local side effects or long-lived
|
||||||
|
// behavior. It is false by default for embedded endpoint use.
|
||||||
|
AllowUnsafePlugins bool
|
||||||
|
// OnResult is called for every structured result as it is discovered.
|
||||||
|
OnResult func(Result)
|
||||||
|
|
||||||
|
Timeout time.Duration
|
||||||
|
Threads int
|
||||||
|
ModuleThreads int
|
||||||
|
MaxRetries int
|
||||||
|
|
||||||
|
DisablePing bool
|
||||||
|
DisableTCPProbe bool
|
||||||
|
DisableBrute bool
|
||||||
|
|
||||||
|
Usernames []string
|
||||||
|
Passwords []string
|
||||||
|
UserPassPairs []CredentialPair
|
||||||
|
Domain string
|
||||||
|
SSHKeyPath string
|
||||||
|
HTTPProxy string
|
||||||
|
Socks5Proxy string
|
||||||
|
Interface string
|
||||||
|
WebTimeout time.Duration
|
||||||
|
MaxRedirects int
|
||||||
|
DisablePOCScan bool
|
||||||
|
POCPath string
|
||||||
|
POCName string
|
||||||
|
POCFull bool
|
||||||
|
POCConcurrency int
|
||||||
|
PacketRateLimit int64
|
||||||
|
MaxPacketCount int64
|
||||||
|
ICMPRate float64
|
||||||
|
Language string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result is the structured scan result returned to embedded callers.
|
||||||
|
type Result struct {
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Details map[string]interface{} `json:"details,omitempty"`
|
||||||
|
}
|
||||||
@@ -84,6 +84,7 @@ type PluginInfo struct {
|
|||||||
factory func() Plugin
|
factory func() Plugin
|
||||||
ports []int
|
ports []int
|
||||||
types []string // 插件类型标签
|
types []string // 插件类型标签
|
||||||
|
safe bool // 是否适合默认嵌入式扫描
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插件类型常量
|
// 插件类型常量
|
||||||
@@ -123,12 +124,23 @@ func RegisterWithPorts(name string, factory func() Plugin, ports []int) {
|
|||||||
|
|
||||||
// RegisterWithTypes 注册带类型标签的插件
|
// RegisterWithTypes 注册带类型标签的插件
|
||||||
func RegisterWithTypes(name string, factory func() Plugin, ports []int, types []string) {
|
func RegisterWithTypes(name string, factory func() Plugin, ports []int, types []string) {
|
||||||
|
RegisterWithOptions(name, factory, ports, types, !hasPluginType(types, PluginTypeLocal))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterUnsafeWithTypes 注册不适合默认嵌入式扫描的插件。
|
||||||
|
func RegisterUnsafeWithTypes(name string, factory func() Plugin, ports []int, types []string) {
|
||||||
|
RegisterWithOptions(name, factory, ports, types, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterWithOptions 注册带完整元数据的插件。
|
||||||
|
func RegisterWithOptions(name string, factory func() Plugin, ports []int, types []string, safe bool) {
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
defer mutex.Unlock()
|
defer mutex.Unlock()
|
||||||
plugins[name] = &PluginInfo{
|
plugins[name] = &PluginInfo{
|
||||||
factory: factory,
|
factory: factory,
|
||||||
ports: ports,
|
ports: ports,
|
||||||
types: types,
|
types: types,
|
||||||
|
safe: safe,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +159,17 @@ func HasType(pluginName string, typeName string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSafe 检查插件是否适合默认嵌入式扫描。
|
||||||
|
func IsSafe(pluginName string) bool {
|
||||||
|
mutex.RLock()
|
||||||
|
defer mutex.RUnlock()
|
||||||
|
|
||||||
|
if info, exists := plugins[pluginName]; exists {
|
||||||
|
return info.safe
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Get 获取插件实例
|
// Get 获取插件实例
|
||||||
func Get(name string) Plugin {
|
func Get(name string) Plugin {
|
||||||
mutex.RLock()
|
mutex.RLock()
|
||||||
@@ -190,6 +213,15 @@ func GetPluginPorts(name string) []int {
|
|||||||
return []int{} // 返回空列表表示适用于所有端口
|
return []int{} // 返回空列表表示适用于所有端口
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasPluginType(types []string, typeName string) bool {
|
||||||
|
for _, t := range types {
|
||||||
|
if t == typeName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateCredentials 生成测试凭据
|
// GenerateCredentials 生成测试凭据
|
||||||
func GenerateCredentials(service string, config *common.Config) []Credential {
|
func GenerateCredentials(service string, config *common.Config) []Credential {
|
||||||
var credentials []Credential
|
var credentials []Credential
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func (p *CleanerPlugin) cleanFiles(output *strings.Builder, dir string, names []
|
|||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
path := filepath.Join(dir, name)
|
path := filepath.Join(dir, name)
|
||||||
if err := os.Remove(path); err == nil {
|
if err := os.Remove(path); err == nil {
|
||||||
output.WriteString(fmt.Sprintf("[清理] %s\n", path))
|
fmt.Fprintf(output, "[清理] %s\n", path)
|
||||||
cleaned++
|
cleaned++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ func (p *CleanerPlugin) cleanGlob(output *strings.Builder, dir, pattern string)
|
|||||||
cleaned := 0
|
cleaned := 0
|
||||||
for _, f := range matches {
|
for _, f := range matches {
|
||||||
if err := os.Remove(f); err == nil {
|
if err := os.Remove(f); err == nil {
|
||||||
output.WriteString(fmt.Sprintf("[清理] %s\n", f))
|
fmt.Fprintf(output, "[清理] %s\n", f)
|
||||||
cleaned++
|
cleaned++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ func (p *CleanerPlugin) cleanUnix(output *strings.Builder) int {
|
|||||||
}
|
}
|
||||||
for _, hf := range histFiles {
|
for _, hf := range histFiles {
|
||||||
if p.scrubHistory(hf) {
|
if p.scrubHistory(hf) {
|
||||||
output.WriteString(fmt.Sprintf("[清理] %s 中的 fscan 记录\n", hf))
|
fmt.Fprintf(output, "[清理] %s 中的 fscan 记录\n", hf)
|
||||||
cleaned++
|
cleaned++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI
|
|||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "activemq",
|
Service: "activemq",
|
||||||
Error: fmt.Errorf("Failed to read response: %w", err),
|
Error: fmt.Errorf("failed to read response: %w", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
|
|||||||
@@ -169,9 +169,10 @@ var cqlStreamID int16
|
|||||||
|
|
||||||
func cqlSend(conn net.Conn, opcode byte, body []byte) error {
|
func cqlSend(conn net.Conn, opcode byte, body []byte) error {
|
||||||
id := cqlStreamID
|
id := cqlStreamID
|
||||||
cqlStreamID++
|
if cqlStreamID == 32767 {
|
||||||
if cqlStreamID > 32767 {
|
|
||||||
cqlStreamID = 0
|
cqlStreamID = 0
|
||||||
|
} else {
|
||||||
|
cqlStreamID++
|
||||||
}
|
}
|
||||||
|
|
||||||
// frame: [1B version|flags] [2B stream] [1B opcode] [4B length] [body]
|
// frame: [1B version|flags] [2B stream] [1B opcode] [4B length] [body]
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ type ConcurrentTestConfig struct {
|
|||||||
RetryDelay time.Duration // 重试延迟,默认 1s
|
RetryDelay time.Duration // 重试延迟,默认 1s
|
||||||
MaxConsecutiveNetErrors int // 连续网络错误阈值,超过则认为目标不可达,默认 5
|
MaxConsecutiveNetErrors int // 连续网络错误阈值,超过则认为目标不可达,默认 5
|
||||||
TargetAddr string // 目标地址 host:port,用于 TCP 预检(可选)
|
TargetAddr string // 目标地址 host:port,用于 TCP 预检(可选)
|
||||||
|
UseProxy bool // 代理模式下跳过直连 TCP 预检
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConcurrentTestConfig 默认配置
|
// DefaultConcurrentTestConfig 默认配置
|
||||||
@@ -125,6 +126,7 @@ func DefaultConcurrentTestConfig(config *common.Config) ConcurrentTestConfig {
|
|||||||
MaxRetries: 3,
|
MaxRetries: 3,
|
||||||
RetryDelay: time.Second,
|
RetryDelay: time.Second,
|
||||||
MaxConsecutiveNetErrors: 5,
|
MaxConsecutiveNetErrors: 5,
|
||||||
|
UseProxy: config.Network.Socks5Proxy != "" || config.Network.HTTPProxy != "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +156,7 @@ func TestCredentialsConcurrently(
|
|||||||
|
|
||||||
// TCP 预检:快速验证目标可达,避免对不可达目标浪费全部凭据尝试
|
// TCP 预检:快速验证目标可达,避免对不可达目标浪费全部凭据尝试
|
||||||
// 代理模式下跳过:net.DialTimeout 直连无法到达代理后的内网目标
|
// 代理模式下跳过:net.DialTimeout 直连无法到达代理后的内网目标
|
||||||
if testConfig.TargetAddr != "" && !common.IsProxyEnabled() {
|
if testConfig.TargetAddr != "" && !testConfig.UseProxy {
|
||||||
preConn, err := net.DialTimeout("tcp", testConfig.TargetAddr, 3*time.Second)
|
preConn, err := net.DialTimeout("tcp", testConfig.TargetAddr, 3*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
|
|||||||
@@ -268,6 +268,33 @@ func TestTestCredentialsConcurrently_EmptyCredentials(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTestCredentialsConcurrently_ProxySkipsDirectPrecheck(t *testing.T) {
|
||||||
|
var calls atomic.Int32
|
||||||
|
authFn := func(ctx context.Context, cred Credential) *AuthResult {
|
||||||
|
calls.Add(1)
|
||||||
|
return &AuthResult{
|
||||||
|
Success: true,
|
||||||
|
Conn: &mockConn{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config := ConcurrentTestConfig{
|
||||||
|
Concurrency: 1,
|
||||||
|
MaxRetries: 1,
|
||||||
|
RetryDelay: time.Millisecond,
|
||||||
|
TargetAddr: "127.0.0.1:1",
|
||||||
|
UseProxy: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := TestCredentialsConcurrently(context.Background(), []Credential{{Username: "u", Password: "p"}}, authFn, "test", config)
|
||||||
|
if !result.Success {
|
||||||
|
t.Fatalf("proxy mode should skip direct precheck: %v", result.Error)
|
||||||
|
}
|
||||||
|
if calls.Load() == 0 {
|
||||||
|
t.Fatal("auth function was not called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestTestCredentialsConcurrently_ContextCancel 测试context取消
|
// TestTestCredentialsConcurrently_ContextCancel 测试context取消
|
||||||
func TestTestCredentialsConcurrently_ContextCancel(t *testing.T) {
|
func TestTestCredentialsConcurrently_ContextCancel(t *testing.T) {
|
||||||
credentials := make([]Credential, 100)
|
credentials := make([]Credential, 100)
|
||||||
@@ -451,4 +478,3 @@ func TestRetryLogic_AuthErrorNoRetry(t *testing.T) {
|
|||||||
|
|
||||||
// 确保 mockConn 实现 io.Closer 接口
|
// 确保 mockConn 实现 io.Closer 接口
|
||||||
var _ io.Closer = (*mockConn)(nil)
|
var _ io.Closer = (*mockConn)(nil)
|
||||||
|
|
||||||
|
|||||||
+11
-7
@@ -28,13 +28,13 @@ func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *co
|
|||||||
config := session.Config
|
config := session.Config
|
||||||
state := session.State
|
state := session.State
|
||||||
if config.DisableBrute {
|
if config.DisableBrute {
|
||||||
return p.identifyService(info, config, state)
|
return p.identifyService(info, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
target := info.Target()
|
target := info.Target()
|
||||||
|
|
||||||
// 优先检测匿名访问
|
// 优先检测匿名访问
|
||||||
if result := p.testAnonymousAccess(ctx, info, config, state); result != nil && result.Success {
|
if result := p.testAnonymousAccess(ctx, info, session); result != nil && result.Success {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *co
|
|||||||
output.WriteString(fmt.Sprintf("\n [->] %s", file))
|
output.WriteString(fmt.Sprintf("\n [->] %s", file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
common.LogVuln(output.String())
|
session.LogVuln(output.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -144,7 +144,9 @@ func classifyFTPErrorType(err error) ErrorType {
|
|||||||
return ClassifyError(err, ftpAuthErrors, ftpNetworkErrors)
|
return ClassifyError(err, ftpAuthErrors, ftpNetworkErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FTPPlugin) identifyService(info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
func (p *FTPPlugin) identifyService(info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||||
|
config := session.Config
|
||||||
|
state := session.State
|
||||||
target := info.Target()
|
target := info.Target()
|
||||||
|
|
||||||
conn, err := ftplib.Dial(target, ftplib.DialWithTimeout(config.Timeout))
|
conn, err := ftplib.Dial(target, ftplib.DialWithTimeout(config.Timeout))
|
||||||
@@ -160,7 +162,7 @@ func (p *FTPPlugin) identifyService(info *common.HostInfo, config *common.Config
|
|||||||
defer func() { _ = conn.Quit() }()
|
defer func() { _ = conn.Quit() }()
|
||||||
|
|
||||||
banner := "FTP"
|
banner := "FTP"
|
||||||
common.LogSuccess(i18n.Tr("ftp_service", target, banner))
|
session.LogSuccess(i18n.Tr("ftp_service", target, banner))
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Type: plugins.ResultTypeService,
|
Type: plugins.ResultTypeService,
|
||||||
Success: true,
|
Success: true,
|
||||||
@@ -170,7 +172,9 @@ func (p *FTPPlugin) identifyService(info *common.HostInfo, config *common.Config
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testAnonymousAccess 测试FTP匿名访问
|
// testAnonymousAccess 测试FTP匿名访问
|
||||||
func (p *FTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
func (p *FTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||||
|
config := session.Config
|
||||||
|
state := session.State
|
||||||
target := info.Target()
|
target := info.Target()
|
||||||
|
|
||||||
anonymousCreds := []Credential{
|
anonymousCreds := []Credential{
|
||||||
@@ -204,7 +208,7 @@ func (p *FTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostIn
|
|||||||
output.WriteString(fmt.Sprintf("\n [->] %s", file))
|
output.WriteString(fmt.Sprintf("\n [->] %s", file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
common.LogVuln(output.String())
|
session.LogVuln(output.String())
|
||||||
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Type: plugins.ResultTypeCredential,
|
Type: plugins.ResultTypeCredential,
|
||||||
|
|||||||
@@ -323,7 +323,6 @@ func mssqlParseLoginTokens(payload []byte, result *mssqlRawResult) (bool, error)
|
|||||||
return false, fmt.Errorf("mssql: truncated done token")
|
return false, fmt.Errorf("mssql: truncated done token")
|
||||||
}
|
}
|
||||||
status := binary.LittleEndian.Uint16(payload[pos : pos+2])
|
status := binary.LittleEndian.Uint16(payload[pos : pos+2])
|
||||||
pos += 12
|
|
||||||
return status&(tdsDoneError|tdsDoneSrvError) == 0, nil
|
return status&(tdsDoneError|tdsDoneSrvError) == 0, nil
|
||||||
default:
|
default:
|
||||||
return false, fmt.Errorf("mssql: unexpected login token 0x%02x", token)
|
return false, fmt.Errorf("mssql: unexpected login token 0x%02x", token)
|
||||||
|
|||||||
@@ -341,8 +341,13 @@ func (s *oracleSession) putString(v string) {
|
|||||||
func (s *oracleSession) putInt(v interface{}, size uint8, bigEndian, compress bool) {
|
func (s *oracleSession) putInt(v interface{}, size uint8, bigEndian, compress bool) {
|
||||||
num := toInt64(v)
|
num := toInt64(v)
|
||||||
if compress {
|
if compress {
|
||||||
|
neg := num < 0
|
||||||
|
encoded := uint64(num)
|
||||||
|
if neg {
|
||||||
|
encoded = uint64(-(num + 1)) + 1
|
||||||
|
}
|
||||||
temp := make([]byte, 8)
|
temp := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint64(temp, uint64(num))
|
binary.BigEndian.PutUint64(temp, encoded)
|
||||||
temp = bytes.TrimLeft(temp, "\x00")
|
temp = bytes.TrimLeft(temp, "\x00")
|
||||||
if size > uint8(len(temp)) {
|
if size > uint8(len(temp)) {
|
||||||
size = uint8(len(temp))
|
size = uint8(len(temp))
|
||||||
@@ -351,8 +356,7 @@ func (s *oracleSession) putInt(v interface{}, size uint8, bigEndian, compress bo
|
|||||||
s.out.WriteByte(0)
|
s.out.WriteByte(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if num < 0 {
|
if neg {
|
||||||
num = -num
|
|
||||||
size |= 0x80
|
size |= 0x80
|
||||||
}
|
}
|
||||||
s.out.WriteByte(size)
|
s.out.WriteByte(size)
|
||||||
|
|||||||
@@ -22,3 +22,10 @@ func RegisterWebPlugin(name string, creator func() WebPlugin) {
|
|||||||
return creator()
|
return creator()
|
||||||
}, []int{}, []string{plugins.PluginTypeWeb})
|
}, []int{}, []string{plugins.PluginTypeWeb})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterUnsafeWebPlugin 注册需要显式授权的主动Web插件。
|
||||||
|
func RegisterUnsafeWebPlugin(name string, creator func() WebPlugin) {
|
||||||
|
plugins.RegisterUnsafeWithTypes(name, func() plugins.Plugin {
|
||||||
|
return creator()
|
||||||
|
}, []int{}, []string{plugins.PluginTypeWeb})
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ func matchCDNorWAF(fingerprints []string) string {
|
|||||||
|
|
||||||
// init 自动注册插件
|
// init 自动注册插件
|
||||||
func init() {
|
func init() {
|
||||||
RegisterWebPlugin("webpoc", func() WebPlugin {
|
RegisterUnsafeWebPlugin("webpoc", func() WebPlugin {
|
||||||
return NewWebPocPlugin()
|
return NewWebPocPlugin()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ func (p *WebTitlePlugin) formatHeaders(headers http.Header) string {
|
|||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
for name, values := range headers {
|
for name, values := range headers {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
builder.WriteString(fmt.Sprintf("%s: %s\n", name, value))
|
fmt.Fprintf(&builder, "%s: %s\n", name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return builder.String()
|
return builder.String()
|
||||||
|
|||||||
Reference in New Issue
Block a user