修复凭据追加和模块选择异常

This commit is contained in:
ZacharyZcR
2026-10-01 12:46:29 +08:00
parent b73c707a3b
commit 4f169206d7
13 changed files with 367 additions and 13 deletions
+5
View File
@@ -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`
![](image/1.png)
+5
View File
@@ -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`
![](image/1.png)
+9 -2
View File
@@ -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
}
+100
View File
@@ -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",
+7
View File
@@ -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
+3 -1
View File
@@ -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
}
// =============================================================================
+6 -2
View File
@@ -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:
+6 -2
View File
@@ -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
View File
@@ -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)
+74
View File
@@ -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()
}
+127
View File
@@ -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{}
}
+2 -6
View File
@@ -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
+13
View File
@@ -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{}