mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-25 12:41:53 +08:00
Harden scan robustness and tests
This commit is contained in:
+255
-23
@@ -4,12 +4,17 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -17,6 +22,7 @@ import (
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// MongoDBPlugin MongoDB扫描插件(纯 raw TCP 实现,无重型依赖)
|
||||
@@ -108,12 +114,13 @@ func (p *MongoDBPlugin) doMongoDBAuth(ctx context.Context, info *common.HostInfo
|
||||
|
||||
// Step 2: saslStart SCRAM-SHA-1
|
||||
nonce := randomString(24)
|
||||
saslPayload := "n=" + cred.Username + ",r=" + nonce
|
||||
clientFirstBare := "n=" + cred.Username + ",r=" + nonce
|
||||
saslPayload := "n,," + clientFirstBare
|
||||
|
||||
saslStartBody := mongoDoc{
|
||||
"saslStart": 1,
|
||||
"mechanism": "SCRAM-SHA-1",
|
||||
"payload": base64EncodeStr(saslPayload),
|
||||
"payload": []byte(saslPayload),
|
||||
"autoAuthorize": 1,
|
||||
}
|
||||
saslStartCmd := buildMongoCommand("admin", saslStartBody)
|
||||
@@ -127,21 +134,48 @@ func (p *MongoDBPlugin) doMongoDBAuth(ctx context.Context, info *common.HostInfo
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
|
||||
// saslStart 响应检查:
|
||||
// - ok:0 + code:18 → 认证失败
|
||||
// - ok:1 + conversationId + payload → 认证有效
|
||||
respStr := string(resp)
|
||||
if strings.Contains(respStr, "\"ok\":0") || strings.Contains(respStr, "Authentication failed") {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("authentication failed")}
|
||||
startReply, err := parseMongoCommandReply(resp)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
if !startReply.ok {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("authentication failed: %s", startReply.errmsg)}
|
||||
}
|
||||
if !startReply.conversationSet || len(startReply.payload) == 0 {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("invalid saslStart response")}
|
||||
}
|
||||
|
||||
// 如果在响应中找到 conversationId,说明凭据有效
|
||||
if strings.Contains(respStr, "conversationId") {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return &AuthResult{Success: true, ErrorType: ErrorTypeUnknown, Error: nil}
|
||||
serverFirst := string(startReply.payload)
|
||||
clientFinal, err := buildMongoSCRAMClientFinal(cred.Username, cred.Password, clientFirstBare, serverFirst)
|
||||
if err != nil {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: err}
|
||||
}
|
||||
|
||||
saslContinueBody := mongoDoc{
|
||||
"saslContinue": 1,
|
||||
"conversationId": int(startReply.conversationID),
|
||||
"payload": []byte(clientFinal),
|
||||
}
|
||||
saslContinueCmd := buildMongoCommand("admin", saslContinueBody)
|
||||
if _, err := sendMongoMsg(ctx, conn, saslContinueCmd, timeout); err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
resp, err = readMongoMsg(conn, timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
finalReply, err := parseMongoCommandReply(resp)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
if !finalReply.ok {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("authentication failed: %s", finalReply.errmsg)}
|
||||
}
|
||||
|
||||
// 无认证失败的明确信号 = 尝试成功
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return &AuthResult{Success: true, ErrorType: ErrorTypeUnknown, Error: nil}
|
||||
}
|
||||
@@ -149,9 +183,10 @@ func (p *MongoDBPlugin) doMongoDBAuth(ctx context.Context, info *common.HostInfo
|
||||
// ── MongoDB wire protocol 工具 ──────────────────────────────────
|
||||
|
||||
const (
|
||||
opMsg uint32 = 2013
|
||||
opQuery uint32 = 2004
|
||||
opReply uint32 = 1
|
||||
opMsg uint32 = 2013
|
||||
opQuery uint32 = 2004
|
||||
opReply uint32 = 1
|
||||
maxMongoMessageBody = 1024 * 1024
|
||||
)
|
||||
|
||||
var mongoRequestID uint32
|
||||
@@ -225,7 +260,7 @@ func buildBSON(doc mongoDoc) []byte {
|
||||
buf = append(buf, []byte(k)...)
|
||||
buf = append(buf, 0x00)
|
||||
b := []byte(val)
|
||||
buf = append(buf, byte(len(b)+1), 0, 0, 0)
|
||||
buf = binary.LittleEndian.AppendUint32(buf, uint32(len(b)+1))
|
||||
buf = append(buf, b...)
|
||||
buf = append(buf, 0x00)
|
||||
case int:
|
||||
@@ -235,13 +270,16 @@ func buildBSON(doc mongoDoc) []byte {
|
||||
i32 := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(i32, uint32(val))
|
||||
buf = append(buf, i32...)
|
||||
case int64:
|
||||
buf = append(buf, 0x12) // type int64
|
||||
buf = append(buf, []byte(k)...)
|
||||
buf = append(buf, 0x00)
|
||||
buf = binary.LittleEndian.AppendUint64(buf, uint64(val))
|
||||
case float64:
|
||||
buf = append(buf, 0x01) // type double
|
||||
buf = append(buf, []byte(k)...)
|
||||
buf = append(buf, 0x00)
|
||||
f64 := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(f64, uint64(val))
|
||||
buf = append(buf, f64...)
|
||||
buf = binary.LittleEndian.AppendUint64(buf, math.Float64bits(val))
|
||||
case mongoDoc:
|
||||
buf = append(buf, 0x03) // type document
|
||||
buf = append(buf, []byte(k)...)
|
||||
@@ -252,7 +290,7 @@ func buildBSON(doc mongoDoc) []byte {
|
||||
buf = append(buf, 0x05) // type binary
|
||||
buf = append(buf, []byte(k)...)
|
||||
buf = append(buf, 0x00)
|
||||
buf = append(buf, byte(len(val)), 0, 0, 0)
|
||||
buf = binary.LittleEndian.AppendUint32(buf, uint32(len(val)))
|
||||
buf = append(buf, 0x00) // subtype 0
|
||||
buf = append(buf, val...)
|
||||
case bool:
|
||||
@@ -301,8 +339,11 @@ func readMongoMsg(conn io.Reader, timeout time.Duration) ([]byte, error) {
|
||||
}
|
||||
// 读取剩余 body
|
||||
bodyLen := int(msgLen) - 16
|
||||
if bodyLen <= 0 || bodyLen > 1024*1024 {
|
||||
return nil, nil
|
||||
if bodyLen == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
if bodyLen > maxMongoMessageBody {
|
||||
return nil, fmt.Errorf("mongodb response too large: %d", msgLen)
|
||||
}
|
||||
body := make([]byte, bodyLen)
|
||||
if _, err := io.ReadFull(conn, body); err != nil {
|
||||
@@ -316,6 +357,197 @@ func readMongoMsg(conn io.Reader, timeout time.Duration) ([]byte, error) {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
type mongoCommandReply struct {
|
||||
ok bool
|
||||
conversationID int32
|
||||
conversationSet bool
|
||||
payload []byte
|
||||
done bool
|
||||
errmsg string
|
||||
}
|
||||
|
||||
func parseMongoCommandReply(doc []byte) (mongoCommandReply, error) {
|
||||
var reply mongoCommandReply
|
||||
if len(doc) < 5 {
|
||||
return reply, fmt.Errorf("short bson document")
|
||||
}
|
||||
docLen := int(binary.LittleEndian.Uint32(doc[:4]))
|
||||
if docLen < 5 || docLen > len(doc) {
|
||||
return reply, fmt.Errorf("invalid bson document length: %d", docLen)
|
||||
}
|
||||
pos := 4
|
||||
for pos < docLen-1 {
|
||||
typ := doc[pos]
|
||||
pos++
|
||||
keyStart := pos
|
||||
for pos < docLen && doc[pos] != 0 {
|
||||
pos++
|
||||
}
|
||||
if pos >= docLen {
|
||||
return reply, fmt.Errorf("unterminated bson key")
|
||||
}
|
||||
key := string(doc[keyStart:pos])
|
||||
pos++
|
||||
|
||||
switch typ {
|
||||
case 0x01: // double
|
||||
if pos+8 > docLen {
|
||||
return reply, fmt.Errorf("short bson double")
|
||||
}
|
||||
if key == "ok" {
|
||||
reply.ok = binary.LittleEndian.Uint64(doc[pos:pos+8]) != 0
|
||||
}
|
||||
pos += 8
|
||||
case 0x02: // string
|
||||
if pos+4 > docLen {
|
||||
return reply, fmt.Errorf("short bson string length")
|
||||
}
|
||||
n := int(binary.LittleEndian.Uint32(doc[pos : pos+4]))
|
||||
pos += 4
|
||||
if n <= 0 || pos+n > docLen {
|
||||
return reply, fmt.Errorf("invalid bson string length: %d", n)
|
||||
}
|
||||
value := string(doc[pos : pos+n-1])
|
||||
pos += n
|
||||
switch key {
|
||||
case "errmsg":
|
||||
reply.errmsg = value
|
||||
case "payload":
|
||||
reply.payload = []byte(value)
|
||||
}
|
||||
case 0x05: // binary
|
||||
if pos+5 > docLen {
|
||||
return reply, fmt.Errorf("short bson binary")
|
||||
}
|
||||
n := int(binary.LittleEndian.Uint32(doc[pos : pos+4]))
|
||||
pos += 5 // length + subtype
|
||||
if n < 0 || pos+n > docLen {
|
||||
return reply, fmt.Errorf("invalid bson binary length: %d", n)
|
||||
}
|
||||
if key == "payload" {
|
||||
reply.payload = append([]byte(nil), doc[pos:pos+n]...)
|
||||
}
|
||||
pos += n
|
||||
case 0x03, 0x04: // document, array
|
||||
if pos+4 > docLen {
|
||||
return reply, fmt.Errorf("short bson embedded document")
|
||||
}
|
||||
n := int(binary.LittleEndian.Uint32(doc[pos : pos+4]))
|
||||
if n < 5 || pos+n > docLen {
|
||||
return reply, fmt.Errorf("invalid bson embedded document length: %d", n)
|
||||
}
|
||||
pos += n
|
||||
case 0x07: // objectId
|
||||
if pos+12 > docLen {
|
||||
return reply, fmt.Errorf("short bson objectId")
|
||||
}
|
||||
pos += 12
|
||||
case 0x08: // bool
|
||||
if pos+1 > docLen {
|
||||
return reply, fmt.Errorf("short bson bool")
|
||||
}
|
||||
if key == "done" {
|
||||
reply.done = doc[pos] != 0
|
||||
}
|
||||
if key == "ok" {
|
||||
reply.ok = doc[pos] != 0
|
||||
}
|
||||
pos++
|
||||
case 0x10: // int32
|
||||
if pos+4 > docLen {
|
||||
return reply, fmt.Errorf("short bson int32")
|
||||
}
|
||||
value := int32(binary.LittleEndian.Uint32(doc[pos : pos+4]))
|
||||
if key == "conversationId" {
|
||||
reply.conversationID = value
|
||||
reply.conversationSet = true
|
||||
}
|
||||
if key == "ok" {
|
||||
reply.ok = value != 0
|
||||
}
|
||||
pos += 4
|
||||
case 0x09, 0x11: // datetime, timestamp
|
||||
if pos+8 > docLen {
|
||||
return reply, fmt.Errorf("short bson fixed64")
|
||||
}
|
||||
pos += 8
|
||||
case 0x0a, 0x7f, 0xff: // null, maxKey, minKey
|
||||
case 0x12: // int64
|
||||
if pos+8 > docLen {
|
||||
return reply, fmt.Errorf("short bson int64")
|
||||
}
|
||||
if key == "ok" {
|
||||
reply.ok = binary.LittleEndian.Uint64(doc[pos:pos+8]) != 0
|
||||
}
|
||||
pos += 8
|
||||
case 0x13: // decimal128
|
||||
if pos+16 > docLen {
|
||||
return reply, fmt.Errorf("short bson decimal128")
|
||||
}
|
||||
pos += 16
|
||||
default:
|
||||
return reply, fmt.Errorf("unsupported bson type 0x%02x for key %s", typ, key)
|
||||
}
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
func buildMongoSCRAMClientFinal(username, password, clientFirstBare, serverFirst string) (string, error) {
|
||||
attrs := parseSCRAMAttributes(serverFirst)
|
||||
serverNonce := attrs["r"]
|
||||
saltB64 := attrs["s"]
|
||||
iterText := attrs["i"]
|
||||
if serverNonce == "" || saltB64 == "" || iterText == "" {
|
||||
return "", fmt.Errorf("invalid SCRAM server-first payload")
|
||||
}
|
||||
clientNonce := scramAttr(clientFirstBare, "r")
|
||||
if clientNonce == "" || !strings.HasPrefix(serverNonce, clientNonce) {
|
||||
return "", fmt.Errorf("invalid SCRAM nonce")
|
||||
}
|
||||
salt, err := base64.StdEncoding.DecodeString(saltB64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid SCRAM salt: %w", err)
|
||||
}
|
||||
iterations, err := strconv.Atoi(iterText)
|
||||
if err != nil || iterations <= 0 {
|
||||
return "", fmt.Errorf("invalid SCRAM iteration count")
|
||||
}
|
||||
|
||||
clientFinalWithoutProof := "c=biws,r=" + serverNonce
|
||||
authMessage := clientFirstBare + "," + serverFirst + "," + clientFinalWithoutProof
|
||||
digest := md5.Sum([]byte(username + ":mongo:" + password))
|
||||
saltedPassword := pbkdf2.Key([]byte(fmt.Sprintf("%x", digest)), salt, iterations, sha1.Size, sha1.New)
|
||||
clientKey := mongoHMAC(saltedPassword, []byte("Client Key"))
|
||||
storedKey := sha1.Sum(clientKey)
|
||||
clientSignature := mongoHMAC(storedKey[:], []byte(authMessage))
|
||||
proof := make([]byte, len(clientKey))
|
||||
for i := range clientKey {
|
||||
proof[i] = clientKey[i] ^ clientSignature[i]
|
||||
}
|
||||
return clientFinalWithoutProof + ",p=" + base64.StdEncoding.EncodeToString(proof), nil
|
||||
}
|
||||
|
||||
func parseSCRAMAttributes(payload string) map[string]string {
|
||||
attrs := make(map[string]string)
|
||||
for _, part := range strings.Split(payload, ",") {
|
||||
if len(part) < 3 || part[1] != '=' {
|
||||
continue
|
||||
}
|
||||
attrs[part[:1]] = part[2:]
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
func scramAttr(payload, key string) string {
|
||||
return parseSCRAMAttributes(payload)[key]
|
||||
}
|
||||
|
||||
func mongoHMAC(key, data []byte) []byte {
|
||||
mac := hmac.New(sha1.New, key)
|
||||
_, _ = mac.Write(data)
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
// dialTCP 带超时的 TCP 连接
|
||||
func dialTCP(ctx context.Context, addr string, timeout time.Duration) (net.Conn, error) {
|
||||
dialer := net.Dialer{Timeout: timeout}
|
||||
|
||||
Reference in New Issue
Block a user