mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-10-16 20:31:46 +08:00
修复凭据追加和模块选择异常
This commit is contained in:
@@ -201,6 +201,11 @@ yay -S fscan-git
|
||||
|
||||
## 运行截图
|
||||
|
||||
使用 `fscan -list-plugins` 查看当前构建可用的模块名称、类型、默认端口和调用方式。
|
||||
通过 `-m` 指定有固定端口的模块且未传 `-p/-pf` 时,扫描范围使用所选模块默认端口的并集;
|
||||
例如 `fscan -hf ip.txt -m netbios` 使用 `137,139`。指定 `-p` 可扫描非标准端口,Web 模块仍使用通用端口范围。
|
||||
`-pwda/-usera` 追加到默认字典;与 `-pwd/-user` 或 `-pwdf/-userf` 同用时,追加到指定字典。
|
||||
|
||||
`fscan.exe -h 192.168.x.x`
|
||||

|
||||
|
||||
|
||||
@@ -200,6 +200,11 @@ yay -S fscan-git
|
||||
|
||||
## Screenshots
|
||||
|
||||
Use `fscan -list-plugins` to list the plugins available in the current build, their types, default ports and usage.
|
||||
When `-m` selects plugins with fixed ports and neither `-p` nor `-pf` is supplied, scanning uses the union of their default ports;
|
||||
for example, `fscan -hf ip.txt -m netbios` uses `137,139`. Use `-p` for nonstandard ports. Web plugins retain the common port range.
|
||||
`-pwda/-usera` append to the default dictionaries, or to the supplied dictionaries when used with `-pwd/-user` or `-pwdf/-userf`.
|
||||
|
||||
`fscan.exe -h 192.168.x.x`
|
||||

|
||||
|
||||
|
||||
@@ -55,8 +55,12 @@ func parseCredentials(fv *FlagVars, cfg *Config) error {
|
||||
return err
|
||||
}
|
||||
if len(usernames) > 0 {
|
||||
for serviceName := range cfg.Credentials.Userdict {
|
||||
cfg.Credentials.Userdict[serviceName] = usernames
|
||||
for serviceName, defaults := range cfg.Credentials.Userdict {
|
||||
if fv.Username == "" && fv.UsersFile == "" {
|
||||
cfg.Credentials.Userdict[serviceName] = removeDuplicate(append(defaults, usernames...))
|
||||
} else {
|
||||
cfg.Credentials.Userdict[serviceName] = usernames
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +70,9 @@ func parseCredentials(fv *FlagVars, cfg *Config) error {
|
||||
return err
|
||||
}
|
||||
if len(passwords) > 0 {
|
||||
if fv.Password == "" && fv.PasswordsFile == "" {
|
||||
passwords = removeDuplicate(append(cfg.Credentials.Passwords, passwords...))
|
||||
}
|
||||
cfg.Credentials.Passwords = passwords
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -8,6 +10,104 @@ import (
|
||||
fscanconfig "github.com/shadow1ng/fscan/common/config"
|
||||
)
|
||||
|
||||
func TestBuildConfigAdditionalCredentials(t *testing.T) {
|
||||
original := NewConfig().Credentials
|
||||
dir := t.TempDir()
|
||||
usersFile := filepath.Join(dir, "users.txt")
|
||||
passwordsFile := filepath.Join(dir, "passwords.txt")
|
||||
if err := os.WriteFile(usersFile, []byte("file-user\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(passwordsFile, []byte("file-password\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
flags FlagVars
|
||||
users []string
|
||||
passwords []string
|
||||
appendUsers bool
|
||||
}{
|
||||
{name: "defaults"},
|
||||
{
|
||||
name: "append to defaults",
|
||||
flags: FlagVars{
|
||||
AddUsers: "extra-user,extra-user",
|
||||
AddPasswords: "extra-password extra-password",
|
||||
},
|
||||
users: []string{"extra-user"},
|
||||
passwords: append(append([]string(nil), fscanconfig.DefaultPasswords...), "extra-password"),
|
||||
appendUsers: true,
|
||||
},
|
||||
{
|
||||
name: "default password is not duplicated",
|
||||
flags: FlagVars{AddPasswords: "123456,123456"},
|
||||
},
|
||||
{
|
||||
name: "append to command line overrides",
|
||||
flags: FlagVars{
|
||||
Username: "custom-user", Password: "primary password",
|
||||
AddUsers: "extra-user,custom-user", AddPasswords: "extra-password extra-password",
|
||||
},
|
||||
users: []string{"custom-user", "extra-user"},
|
||||
passwords: []string{"primary password", "extra-password"},
|
||||
},
|
||||
{
|
||||
name: "append to file overrides",
|
||||
flags: FlagVars{
|
||||
UsersFile: usersFile, PasswordsFile: passwordsFile,
|
||||
AddUsers: "extra-user,file-user", AddPasswords: "extra-password file-password",
|
||||
},
|
||||
users: []string{"file-user", "extra-user"},
|
||||
passwords: []string{"file-password", "extra-password"},
|
||||
},
|
||||
{
|
||||
name: "command line and files still replace defaults",
|
||||
flags: FlagVars{
|
||||
Username: "custom-user", Password: "primary password",
|
||||
UsersFile: usersFile, PasswordsFile: passwordsFile,
|
||||
AddUsers: "extra-user", AddPasswords: "extra-password",
|
||||
},
|
||||
users: []string{"custom-user", "file-user", "extra-user"},
|
||||
passwords: []string{"primary password", "file-password", "extra-password"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg, _, err := BuildConfig(&tt.flags, &HostInfo{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPasswords := tt.passwords
|
||||
if wantPasswords == nil {
|
||||
wantPasswords = append([]string(nil), fscanconfig.DefaultPasswords...)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.Credentials.Passwords, wantPasswords) {
|
||||
t.Errorf("passwords = %v, want %v", cfg.Credentials.Passwords, wantPasswords)
|
||||
}
|
||||
for service, defaults := range fscanconfig.DefaultUserDict {
|
||||
wantUsers := tt.users
|
||||
if wantUsers == nil || tt.appendUsers {
|
||||
wantUsers = append(append([]string{}, defaults...), wantUsers...)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.Credentials.Userdict[service], wantUsers) {
|
||||
t.Errorf("%s users = %v, want %v", service, cfg.Credentials.Userdict[service], wantUsers)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
defaults, _, err := BuildConfig(&FlagVars{}, &HostInfo{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(defaults.Credentials.Passwords, original.Passwords) ||
|
||||
!reflect.DeepEqual(defaults.Credentials.Userdict, original.Userdict) {
|
||||
t.Fatal("additional credentials leaked into a later config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePasswordsKeepsPrimaryPasswordLiteral(t *testing.T) {
|
||||
fv := &FlagVars{
|
||||
Password: "root admin,pass0",
|
||||
|
||||
@@ -209,6 +209,7 @@ func Flag(Info *HostInfo) error {
|
||||
|
||||
// 帮助参数
|
||||
flag.BoolVar(&fv.ShowHelp, "help", false, i18n.GetText("flag_help"))
|
||||
flag.BoolVar(&fv.ListPlugins, "list-plugins", false, i18n.GetText("flag_list_plugins"))
|
||||
|
||||
// 解析命令行参数
|
||||
if err := parseCommandLineArgs(); err != nil {
|
||||
@@ -218,6 +219,8 @@ func Flag(Info *HostInfo) error {
|
||||
// 检测用户是否显式指定了 -t
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
switch f.Name {
|
||||
case "p":
|
||||
fv.PortsExplicit = true
|
||||
case "t":
|
||||
fv.ThreadNumExplicit = true
|
||||
case "time":
|
||||
@@ -348,6 +351,10 @@ func preProcessLanguage() {
|
||||
|
||||
// shouldShowHelp 检查是否应该显示帮助信息
|
||||
func shouldShowHelp(Info *HostInfo, fv *FlagVars) bool {
|
||||
if fv.ListPlugins {
|
||||
return false
|
||||
}
|
||||
|
||||
// Web模式不需要目标参数
|
||||
if WebMode {
|
||||
return false
|
||||
|
||||
@@ -26,6 +26,7 @@ type FlagVars struct {
|
||||
ExcludeHosts string
|
||||
ExcludeHostsFile string
|
||||
Ports string
|
||||
PortsExplicit bool
|
||||
ExcludePorts string
|
||||
HostsFile string
|
||||
PortsFile string
|
||||
@@ -121,7 +122,8 @@ type FlagVars struct {
|
||||
DownloadSavePath string
|
||||
|
||||
// 帮助
|
||||
ShowHelp bool
|
||||
ShowHelp bool
|
||||
ListPlugins bool
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -9,7 +9,7 @@ flag_exclude_hosts:
|
||||
flag_exclude_hosts_file:
|
||||
other: "Exclude hosts file"
|
||||
flag_ports:
|
||||
other: "Ports: default 1000 common ports"
|
||||
other: "Ports: defaults to selected plugins' ports, otherwise common ports"
|
||||
flag_exclude_ports:
|
||||
other: "Exclude ports"
|
||||
flag_hosts_file:
|
||||
@@ -17,7 +17,11 @@ flag_hosts_file:
|
||||
flag_ports_file:
|
||||
other: "Ports file"
|
||||
flag_scan_mode:
|
||||
other: "Scan mode: all(all plugins), icmp(alive detection), or specific plugin names"
|
||||
other: "Scan mode: all(all plugins), icmp(alive detection), or specific plugin names (see -list-plugins)"
|
||||
flag_list_plugins:
|
||||
other: "List available plugins, types, default ports and usage, then exit"
|
||||
plugin_list_header:
|
||||
other: "Plugin\tType\tDefault ports\tUsage"
|
||||
flag_thread_num:
|
||||
other: "Port scan thread count"
|
||||
flag_timeout:
|
||||
|
||||
@@ -9,7 +9,7 @@ flag_exclude_hosts:
|
||||
flag_exclude_hosts_file:
|
||||
other: "排除主机文件"
|
||||
flag_ports:
|
||||
other: "端口: 默认1000个常用端口"
|
||||
other: "端口: 指定模块时默认使用模块端口,否则使用常用端口"
|
||||
flag_exclude_ports:
|
||||
other: "排除端口"
|
||||
flag_hosts_file:
|
||||
@@ -17,7 +17,11 @@ flag_hosts_file:
|
||||
flag_ports_file:
|
||||
other: "端口文件"
|
||||
flag_scan_mode:
|
||||
other: "扫描模式: all(全部), icmp(存活探测), 或指定插件名称"
|
||||
other: "扫描模式: all(全部), icmp(存活探测), 或指定插件名称(-list-plugins 查看)"
|
||||
flag_list_plugins:
|
||||
other: "列出当前构建可用的模块、类型、默认端口和调用方式后退出"
|
||||
plugin_list_header:
|
||||
other: "模块\t类型\t默认端口\t调用方式"
|
||||
flag_thread_num:
|
||||
other: "端口扫描线程数"
|
||||
flag_timeout:
|
||||
|
||||
+10
@@ -38,11 +38,21 @@ func run() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
flags := common.GetFlagVars()
|
||||
if flags.ListPlugins {
|
||||
if err := printPluginList(os.Stdout); err != nil {
|
||||
common.LogError(i18n.Tr("error_generic", err))
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 检查参数互斥性
|
||||
if err := common.ValidateExclusiveParams(&info); err != nil {
|
||||
common.LogError(i18n.Tr("error_generic", err))
|
||||
return 1
|
||||
}
|
||||
setPluginDefaultPorts(flags)
|
||||
|
||||
// 统一初始化:解析 → 配置 → 输出
|
||||
result, err := common.Initialize(&info)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//go:build !web
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
func setPluginDefaultPorts(flags *common.FlagVars) {
|
||||
if flags.PortsExplicit || flags.PortsFile != "" || flags.AliveOnly || flags.LocalPlugin != "" {
|
||||
return
|
||||
}
|
||||
|
||||
var ports []int
|
||||
for _, name := range strings.Split(flags.ScanMode, ",") {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
defaults := plugins.GetPluginPorts(name)
|
||||
// Web、本地插件和特殊模式没有固定端口,保留通用端口范围。
|
||||
if len(defaults) == 0 {
|
||||
return
|
||||
}
|
||||
ports = append(ports, defaults...)
|
||||
}
|
||||
if len(ports) > 0 {
|
||||
flags.Ports = formatPluginPorts(ports)
|
||||
}
|
||||
}
|
||||
|
||||
func formatPluginPorts(ports []int) string {
|
||||
ports = slices.Clone(ports)
|
||||
slices.Sort(ports)
|
||||
var values []string
|
||||
for _, port := range slices.Compact(ports) {
|
||||
values = append(values, strconv.Itoa(port))
|
||||
}
|
||||
return strings.Join(values, ",")
|
||||
}
|
||||
|
||||
func printPluginList(w io.Writer) error {
|
||||
names := plugins.All()
|
||||
slices.Sort(names)
|
||||
table := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(table, i18n.GetText("plugin_list_header"))
|
||||
for _, name := range names {
|
||||
var types []string
|
||||
for _, kind := range []string{plugins.PluginTypeService, plugins.PluginTypeUDP, plugins.PluginTypeWeb, plugins.PluginTypeLocal} {
|
||||
if plugins.HasType(name, kind) {
|
||||
types = append(types, kind)
|
||||
}
|
||||
}
|
||||
ports := formatPluginPorts(plugins.GetPluginPorts(name))
|
||||
if ports == "" {
|
||||
ports = "-"
|
||||
}
|
||||
option := "-m"
|
||||
if plugins.HasType(name, plugins.PluginTypeLocal) {
|
||||
option = "-local"
|
||||
}
|
||||
_, _ = fmt.Fprintf(table, "%s\t%s\t%s\t%s %s\n", name, strings.Join(types, ","), ports, option, name)
|
||||
}
|
||||
return table.Flush()
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
//go:build !web && !plugin_selective
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
func TestCLIPluginDefaultPorts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{name: "netbios", args: []string{"-m", "netbios"}, want: "137,139"},
|
||||
{name: "multiple plugins", args: []string{"-m", "ssh, netbios,ssh,"}, want: "22,137,139,2200,2222,22222"},
|
||||
{name: "nonstandard port", args: []string{"-m", "ssh", "-p", "2222"}, want: "2222"},
|
||||
{name: "explicit common ports", args: []string{"-m", "netbios", "-p", config.MainPorts}, want: config.MainPorts},
|
||||
{name: "ports file", args: []string{"-m", "netbios", "-pf", "ports.txt"}, want: config.MainPorts},
|
||||
{name: "all", args: []string{"-m", "all"}, want: config.MainPorts},
|
||||
{name: "web plugin", args: []string{"-m", "webtitle"}, want: config.MainPorts},
|
||||
{name: "web and service", args: []string{"-m", "ssh,webtitle"}, want: config.MainPorts},
|
||||
{name: "unknown plugin", args: []string{"-m", "ssh,unknown-plugin"}, want: config.MainPorts},
|
||||
{name: "alive only", args: []string{"-m", "netbios", "-ao"}, want: config.MainPorts},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setCLIArgs(t, append([]string{"-h", "127.0.0.1", "-silent"}, tt.args...)...)
|
||||
info := &common.HostInfo{}
|
||||
if err := common.Flag(info); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
setPluginDefaultPorts(common.GetFlagVars())
|
||||
cfg, _, err := common.BuildConfig(common.GetFlagVars(), info)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Target.Ports != tt.want {
|
||||
t.Errorf("ports = %q, want %q", cfg.Target.Ports, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintPluginList(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
if err := printPluginList(&output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(output.String()), "\n")
|
||||
names := plugins.All()
|
||||
slices.Sort(names)
|
||||
if len(lines) != len(names)+1 {
|
||||
t.Fatalf("got %d rows for %d plugins", len(lines)-1, len(names))
|
||||
}
|
||||
for i, name := range names {
|
||||
fields := strings.Fields(lines[i+1])
|
||||
if len(fields) != 5 {
|
||||
t.Fatalf("invalid plugin row: %q", lines[i+1])
|
||||
}
|
||||
if fields[0] != name || fields[len(fields)-1] != name {
|
||||
t.Errorf("plugin row = %q, want %s", lines[i+1], name)
|
||||
}
|
||||
if name == "netbios" && strings.Join(fields, " ") != "netbios service 137,139 -m netbios" {
|
||||
t.Errorf("netbios metadata = %q", lines[i+1])
|
||||
}
|
||||
if plugins.HasType(name, plugins.PluginTypeLocal) && fields[len(fields)-2] != "-local" {
|
||||
t.Errorf("local plugin usage = %q", lines[i+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIListPluginsExitsWithoutTargetOrOutput(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
outputFile := filepath.Join(dir, "result.txt")
|
||||
setCLIArgs(t, "-list-plugins", "-silent", "-o", outputFile)
|
||||
output, err := os.CreateTemp(dir, "stdout-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer output.Close()
|
||||
previousStdout := os.Stdout
|
||||
os.Stdout = output
|
||||
t.Cleanup(func() { os.Stdout = previousStdout })
|
||||
if code := run(); code != 0 {
|
||||
t.Fatalf("exit code = %d", code)
|
||||
}
|
||||
if _, err := os.Stat(outputFile); !os.IsNotExist(err) {
|
||||
t.Fatalf("listing initialized scan output: %v", err)
|
||||
}
|
||||
if _, err := output.Seek(0, io.SeekStart); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := io.ReadAll(output)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(data), "netbios") || !strings.Contains(string(data), "137,139") {
|
||||
t.Fatalf("plugin list missing from output: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func setCLIArgs(t *testing.T, args ...string) {
|
||||
t.Helper()
|
||||
previousArgs := os.Args
|
||||
previousFlagSet := flag.CommandLine
|
||||
previousFlags := *common.GetFlagVars()
|
||||
t.Cleanup(func() {
|
||||
os.Args = previousArgs
|
||||
flag.CommandLine = previousFlagSet
|
||||
*common.GetFlagVars() = previousFlags
|
||||
})
|
||||
os.Args = append([]string{"fscan-test"}, args...)
|
||||
flag.CommandLine = flag.NewFlagSet("fscan-test", flag.ContinueOnError)
|
||||
*common.GetFlagVars() = common.FlagVars{}
|
||||
}
|
||||
@@ -36,13 +36,9 @@ func (p *NetBIOSPlugin) Scan(ctx context.Context, info *common.HostInfo, session
|
||||
state := session.State
|
||||
target := info.Target()
|
||||
|
||||
// 检查端口类型
|
||||
// 多模块扫描会共享端口列表,跳过其他模块的端口。
|
||||
if info.Port != 137 && info.Port != 139 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "netbios",
|
||||
Error: fmt.Errorf("%s", i18n.GetText("netbios_port_only")),
|
||||
}
|
||||
return &ScanResult{Skipped: true, Service: "netbios"}
|
||||
}
|
||||
|
||||
var netbiosInfo *NetBIOSInfo
|
||||
|
||||
@@ -3,11 +3,24 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
func TestNetBIOSSkipsUnrelatedPorts(t *testing.T) {
|
||||
session := common.NewScanSession(common.NewConfig(), common.NewState(), &common.FlagVars{})
|
||||
for _, port := range []int{22, 80, 443} {
|
||||
result := NewNetBIOSPlugin().Scan(context.Background(), &common.HostInfo{Host: "127.0.0.1", Port: port}, session)
|
||||
if result == nil || !result.Skipped || result.Success || result.Error != nil {
|
||||
t.Errorf("port %d: result = %+v, want skipped without error", port, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNTLMInfoUsesFullTargetInfoOffset(t *testing.T) {
|
||||
p := NewNetBIOSPlugin()
|
||||
info := &NetBIOSInfo{}
|
||||
|
||||
Reference in New Issue
Block a user