fix: MongoDB SCRAM 认证因 BSON 键序随机而失败

Go map 遍历顺序不确定,导致 buildBSON 输出的命令文档中
saslStart/saslContinue 不一定是第一个键,MongoDB 拒绝执行。

引入有序 []mongoKV 类型,SASL 命令改用 orderedDoc() 构造。
同时新增 6 协议集成测试框架(Docker Compose + go test -tags integration)。
This commit is contained in:
ZacharyZcR
2026-06-17 12:51:42 +08:00
parent 0612255893
commit 9b8e4f3f3b
3 changed files with 374 additions and 61 deletions
+89
View File
@@ -0,0 +1,89 @@
services:
redis:
image: redis:7-alpine
command: redis-server --requirepass test123
ports:
- "16379:6379"
healthcheck:
test: ["CMD", "redis-cli", "-a", "test123", "ping"]
interval: 3s
retries: 10
redis-noauth:
image: redis:7-alpine
ports:
- "16380:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
retries: 10
mysql:
image: mysql:8.0
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_ROOT_HOST: "%"
MYSQL_DATABASE: testdb
ports:
- "13307:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-proot123"]
interval: 5s
retries: 20
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres123
POSTGRES_DB: testdb
ports:
- "15432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
retries: 10
ftp:
image: fauria/vsftpd
environment:
FTP_USER: ftpuser
FTP_PASS: ftp123
PASV_MIN_PORT: 21100
PASV_MAX_PORT: 21110
PASV_ADDRESS: 127.0.0.1
ports:
- "10021:21"
- "21100-21110:21100-21110"
healthcheck:
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/21' || exit 1"]
interval: 5s
retries: 10
ssh:
image: lscr.io/linuxserver/openssh-server:latest
environment:
PUID: 1000
PGID: 1000
USER_NAME: sshuser
USER_PASSWORD: ssh123
PASSWORD_ACCESS: "true"
ports:
- "10022:2222"
healthcheck:
test: ["CMD-SHELL", "nc -z localhost 2222 || exit 1"]
interval: 3s
retries: 10
mongodb:
image: mongo:4.4
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: mongo123
ports:
- "17017:27017"
healthcheck:
test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')", "-u", "admin", "-p", "mongo123"]
interval: 5s
retries: 20
+222
View File
@@ -0,0 +1,222 @@
//go:build integration
package integration
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/config"
"github.com/shadow1ng/fscan/plugins/services"
)
const (
testHost = "127.0.0.1"
)
func testSession() *common.ScanSession {
cfg := common.NewConfig()
cfg.Timeout = 10 * time.Second
cfg.ModuleThreadNum = 5
cfg.MaxRetries = 2
cfg.Credentials.Userdict = nil
cfg.Credentials.Passwords = nil
state := common.NewState()
return common.NewScanSession(cfg, state, &common.FlagVars{})
}
func hostInfo(host string, port int) *common.HostInfo {
return &common.HostInfo{Host: host, Port: port}
}
func TestMain(m *testing.M) {
fmt.Println("integration tests: ensure docker-compose services are running")
os.Exit(m.Run())
}
// ── Redis ──────────────────────────────────────────────────────
func TestRedisUnauthorized(t *testing.T) {
session := testSession()
info := hostInfo(testHost, 16380)
plugin := services.NewRedisPlugin()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result := plugin.Scan(ctx, info, session)
if result == nil {
t.Fatal("result is nil")
}
if !result.Success {
t.Fatalf("expected unauthorized redis to succeed, got error: %v", result.Error)
}
t.Logf("redis noauth: %+v", result)
}
func TestRedisBrute(t *testing.T) {
session := testSession()
session.Config.Credentials.UserPassPairs = []config.CredentialPair{
{Username: "", Password: "wrong1"},
{Username: "", Password: "test123"},
}
info := hostInfo(testHost, 16379)
plugin := services.NewRedisPlugin()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result := plugin.Scan(ctx, info, session)
if result == nil {
t.Fatal("result is nil")
}
if !result.Success {
t.Fatalf("expected redis brute to succeed with test123, got error: %v", result.Error)
}
if result.Password != "test123" {
t.Errorf("expected password test123, got %q", result.Password)
}
t.Logf("redis brute: %+v", result)
}
// ── MySQL ──────────────────────────────────────────────────────
func TestMySQLBrute(t *testing.T) {
session := testSession()
session.Config.Credentials.UserPassPairs = []config.CredentialPair{
{Username: "root", Password: "wrong"},
{Username: "root", Password: "root123"},
}
info := hostInfo(testHost, 13307)
plugin := services.NewMySQLPlugin()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result := plugin.Scan(ctx, info, session)
if result == nil {
t.Fatal("result is nil")
}
if !result.Success {
t.Fatalf("expected mysql brute to succeed, got error: %v", result.Error)
}
t.Logf("mysql brute: user=%s pass=%s", result.Username, result.Password)
}
// ── PostgreSQL ─────────────────────────────────────────────────
func TestPostgreSQLBrute(t *testing.T) {
session := testSession()
session.Config.Credentials.UserPassPairs = []config.CredentialPair{
{Username: "postgres", Password: "wrong"},
{Username: "postgres", Password: "postgres123"},
}
info := hostInfo(testHost, 15432)
plugin := services.NewPostgreSQLPlugin()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result := plugin.Scan(ctx, info, session)
if result == nil {
t.Fatal("result is nil")
}
if !result.Success {
t.Fatalf("expected postgresql brute to succeed, got error: %v", result.Error)
}
t.Logf("postgresql brute: user=%s pass=%s", result.Username, result.Password)
}
// ── FTP ────────────────────────────────────────────────────────
func TestFTPBrute(t *testing.T) {
session := testSession()
session.Config.Credentials.UserPassPairs = []config.CredentialPair{
{Username: "ftpuser", Password: "wrong"},
{Username: "ftpuser", Password: "ftp123"},
}
info := hostInfo(testHost, 10021)
plugin := services.NewFTPPlugin()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
result := plugin.Scan(ctx, info, session)
if result == nil {
t.Fatal("result is nil")
}
if !result.Success {
t.Fatalf("expected ftp brute to succeed, got error: %v", result.Error)
}
t.Logf("ftp brute: user=%s pass=%s", result.Username, result.Password)
}
// ── SSH ────────────────────────────────────────────────────────
func TestSSHBrute(t *testing.T) {
session := testSession()
session.Config.Credentials.UserPassPairs = []config.CredentialPair{
{Username: "sshuser", Password: "wrong"},
{Username: "sshuser", Password: "ssh123"},
}
info := hostInfo(testHost, 10022)
plugin := services.NewSSHPlugin()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
result := plugin.Scan(ctx, info, session)
if result == nil {
t.Fatal("result is nil")
}
if !result.Success {
t.Fatalf("expected ssh brute to succeed, got error: %v", result.Error)
}
t.Logf("ssh brute: user=%s pass=%s", result.Username, result.Password)
}
// ── MongoDB ────────────────────────────────────────────────────
func TestMongoDBBrute(t *testing.T) {
// Fixed: BSON key ordering was non-deterministic (Go map), MongoDB requires command name first
session := testSession()
session.Config.Credentials.UserPassPairs = []config.CredentialPair{
{Username: "admin", Password: "wrong"},
{Username: "admin", Password: "mongo123"},
}
info := hostInfo(testHost, 17017)
plugin := services.NewMongoDBPlugin()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result := plugin.Scan(ctx, info, session)
if result == nil {
t.Fatal("result is nil")
}
if !result.Success {
t.Fatalf("expected mongodb brute to succeed, got error: %v", result.Error)
}
t.Logf("mongodb brute: user=%s pass=%s", result.Username, result.Password)
}
// ── 连接失败场景 ──────────────────────────────────────────────
func TestRedisConnectionRefused(t *testing.T) {
session := testSession()
session.Config.Timeout = 3 * time.Second
info := hostInfo(testHost, 19999)
plugin := services.NewRedisPlugin()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result := plugin.Scan(ctx, info, session)
if result != nil && result.Success {
t.Fatal("expected failure on closed port")
}
}