mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
slim mssql credential check
This commit is contained in:
@@ -4,11 +4,9 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/denisenkom/go-mssqldb" // MSSQL driver
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
@@ -65,29 +63,11 @@ func (p *MSSQLPlugin) createAuthFunc(info *common.HostInfo, config *common.Confi
|
||||
|
||||
// doMSSQLAuth 执行MSSQL认证
|
||||
func (p *MSSQLPlugin) doMSSQLAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
connStr := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=master;encrypt=disable;connection timeout=%d",
|
||||
info.Host, cred.Username, cred.Password, info.Port, int64(config.Timeout.Seconds()))
|
||||
|
||||
db, err := sql.Open("mssql", connStr)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyMSSQLErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
db.SetConnMaxLifetime(config.Timeout)
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
pingCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
authCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
err = db.PingContext(pingCtx)
|
||||
_, err := mssqlRawLogin(authCtx, info.Host, info.Port, cred.Username, cred.Password, config.Timeout)
|
||||
if err != nil {
|
||||
_ = db.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
@@ -100,7 +80,6 @@ func (p *MSSQLPlugin) doMSSQLAuth(ctx context.Context, info *common.HostInfo, cr
|
||||
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &SQLDBWrapper{db},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
@@ -148,23 +127,10 @@ func classifyMSSQLErrorType(err error) ErrorType {
|
||||
func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
connStr := fmt.Sprintf("server=%s;user id=invalid;password=invalid;port=%d;database=master;encrypt=disable;connection timeout=%d",
|
||||
info.Host, info.Port, int64(config.Timeout.Seconds()))
|
||||
|
||||
db, err := sql.Open("mssql", connStr)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mssql",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
pingCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
identifyCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
err = db.PingContext(pingCtx)
|
||||
result, err := mssqlRawLogin(identifyCtx, info.Host, info.Port, "invalid", "invalid", config.Timeout)
|
||||
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
@@ -178,11 +144,10 @@ func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
errLower = strings.ToLower(err.Error())
|
||||
}
|
||||
|
||||
if err != nil && (strings.Contains(errLower, "login failed") ||
|
||||
strings.Contains(errLower, "mssql") ||
|
||||
strings.Contains(errLower, "sql server")) {
|
||||
banner = "MSSQL"
|
||||
} else if err == nil {
|
||||
if err == nil || (result != nil && result.isMSSQL()) ||
|
||||
(strings.Contains(errLower, "login failed") ||
|
||||
strings.Contains(errLower, "mssql") ||
|
||||
strings.Contains(errLower, "sql server")) {
|
||||
banner = "MSSQL"
|
||||
} else {
|
||||
return &ScanResult{
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
//go:build plugin_mssql || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
const (
|
||||
tdsPacketReply = 4
|
||||
tdsPacketLogin7 = 16
|
||||
tdsPacketPrelogin = 18
|
||||
|
||||
tdsStatusEOM = 1
|
||||
|
||||
tdsVersion74 = 0x74000004
|
||||
tdsDefaultPacketLen = 4096
|
||||
|
||||
tdsPreloginVersion = 0
|
||||
tdsPreloginEncryption = 1
|
||||
tdsPreloginInstOpt = 2
|
||||
tdsPreloginThreadID = 3
|
||||
tdsPreloginMARS = 4
|
||||
tdsPreloginTerminator = 0xff
|
||||
|
||||
tdsEncryptNotSupported = 2
|
||||
|
||||
tdsTokenError = 0xaa
|
||||
tdsTokenInfo = 0xab
|
||||
tdsTokenLoginAck = 0xad
|
||||
tdsTokenEnvChange = 0xe3
|
||||
tdsTokenDone = 0xfd
|
||||
tdsTokenDoneProc = 0xfe
|
||||
tdsTokenDoneInProc = 0xff
|
||||
|
||||
tdsDoneError = 0x0002
|
||||
tdsDoneSrvError = 0x0100
|
||||
|
||||
tdsOptionUseDB = 0x20
|
||||
tdsOptionSetLang = 0x80
|
||||
tdsOptionODBC = 0x02
|
||||
|
||||
tdsLoginHeaderLen = 94
|
||||
)
|
||||
|
||||
type mssqlRawResult struct {
|
||||
sawPrelogin bool
|
||||
sawLoginAck bool
|
||||
errors []mssqlRawError
|
||||
}
|
||||
|
||||
func (r *mssqlRawResult) isMSSQL() bool {
|
||||
return r != nil && (r.sawPrelogin || r.sawLoginAck || len(r.errors) > 0)
|
||||
}
|
||||
|
||||
type mssqlRawError struct {
|
||||
number int32
|
||||
message string
|
||||
}
|
||||
|
||||
func (e mssqlRawError) Error() string {
|
||||
if e.message == "" {
|
||||
return fmt.Sprintf("mssql: error %d", e.number)
|
||||
}
|
||||
return "mssql: " + e.message
|
||||
}
|
||||
|
||||
func mssqlRawLogin(ctx context.Context, host string, port int, username, password string, timeout time.Duration) (*mssqlRawResult, error) {
|
||||
target := net.JoinHostPort(host, fmt.Sprint(port))
|
||||
dialer := net.Dialer{Timeout: timeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = conn.SetDeadline(deadline)
|
||||
} else if timeout > 0 {
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
}
|
||||
|
||||
result := &mssqlRawResult{}
|
||||
if err := mssqlSendPrelogin(conn); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if err := mssqlReadPrelogin(conn); err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.sawPrelogin = true
|
||||
|
||||
if err := mssqlSendLogin7(conn, host, username, password); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if err := mssqlReadLoginResponse(conn, result); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if len(result.errors) > 0 {
|
||||
return result, result.errors[len(result.errors)-1]
|
||||
}
|
||||
if !result.sawLoginAck {
|
||||
return result, fmt.Errorf("mssql: login acknowledgement not received")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func mssqlSendPrelogin(w io.Writer) error {
|
||||
fields := map[byte][]byte{
|
||||
tdsPreloginVersion: {0, 0, 0, 0, 0, 0},
|
||||
tdsPreloginEncryption: {tdsEncryptNotSupported},
|
||||
tdsPreloginInstOpt: {0},
|
||||
tdsPreloginThreadID: {0, 0, 0, 0},
|
||||
tdsPreloginMARS: {0},
|
||||
}
|
||||
|
||||
keys := make([]int, 0, len(fields))
|
||||
for k := range fields {
|
||||
keys = append(keys, int(k))
|
||||
}
|
||||
sort.Ints(keys)
|
||||
|
||||
payload := bytes.NewBuffer(nil)
|
||||
offset := uint16(len(fields)*5 + 1)
|
||||
for _, key := range keys {
|
||||
value := fields[byte(key)]
|
||||
payload.WriteByte(byte(key))
|
||||
_ = binary.Write(payload, binary.BigEndian, offset)
|
||||
_ = binary.Write(payload, binary.BigEndian, uint16(len(value)))
|
||||
offset += uint16(len(value))
|
||||
}
|
||||
payload.WriteByte(tdsPreloginTerminator)
|
||||
for _, key := range keys {
|
||||
payload.Write(fields[byte(key)])
|
||||
}
|
||||
|
||||
return mssqlWritePacket(w, tdsPacketPrelogin, payload.Bytes())
|
||||
}
|
||||
|
||||
func mssqlReadPrelogin(r io.Reader) error {
|
||||
packetType, payload, err := mssqlReadMessage(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if packetType != tdsPacketReply {
|
||||
return fmt.Errorf("mssql: invalid prelogin response packet type %d", packetType)
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
return fmt.Errorf("mssql: empty prelogin response")
|
||||
}
|
||||
|
||||
fields, err := mssqlParsePreloginFields(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := fields[tdsPreloginEncryption]; !ok {
|
||||
return fmt.Errorf("mssql: prelogin response missing encryption field")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mssqlParsePreloginFields(payload []byte) (map[byte][]byte, error) {
|
||||
fields := make(map[byte][]byte)
|
||||
for pos := 0; ; pos += 5 {
|
||||
if pos >= len(payload) {
|
||||
return nil, fmt.Errorf("mssql: invalid prelogin option table")
|
||||
}
|
||||
token := payload[pos]
|
||||
if token == tdsPreloginTerminator {
|
||||
return fields, nil
|
||||
}
|
||||
if pos+5 > len(payload) {
|
||||
return nil, fmt.Errorf("mssql: truncated prelogin option")
|
||||
}
|
||||
offset := int(binary.BigEndian.Uint16(payload[pos+1 : pos+3]))
|
||||
length := int(binary.BigEndian.Uint16(payload[pos+3 : pos+5]))
|
||||
if offset < 0 || length < 0 || offset+length > len(payload) {
|
||||
return nil, fmt.Errorf("mssql: invalid prelogin option bounds")
|
||||
}
|
||||
fields[token] = payload[offset : offset+length]
|
||||
}
|
||||
}
|
||||
|
||||
func mssqlSendLogin7(w io.Writer, host, username, password string) error {
|
||||
hostname, _ := os.Hostname()
|
||||
values := []struct {
|
||||
text string
|
||||
password bool
|
||||
}{
|
||||
{hostname, false},
|
||||
{username, false},
|
||||
{password, true},
|
||||
{"fscan", false},
|
||||
{host, false},
|
||||
{"fscan", false},
|
||||
{"", false},
|
||||
{"master", false},
|
||||
{"", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
encoded := make([][]byte, len(values))
|
||||
lengths := make([]uint16, len(values))
|
||||
for i, value := range values {
|
||||
if value.password {
|
||||
encoded[i] = mssqlEncodePassword(value.text)
|
||||
} else {
|
||||
encoded[i] = mssqlUCS2(value.text)
|
||||
}
|
||||
lengths[i] = uint16(len(encoded[i]) / 2)
|
||||
}
|
||||
|
||||
offsets := make([]uint16, len(values))
|
||||
offset := uint16(tdsLoginHeaderLen)
|
||||
for i, value := range encoded {
|
||||
offsets[i] = offset
|
||||
offset += uint16(len(value))
|
||||
}
|
||||
|
||||
body := bytes.NewBuffer(make([]byte, 0, int(offset)))
|
||||
put32 := func(v uint32) { _ = binary.Write(body, binary.LittleEndian, v) }
|
||||
put16 := func(v uint16) { _ = binary.Write(body, binary.LittleEndian, v) }
|
||||
|
||||
put32(uint32(offset))
|
||||
put32(tdsVersion74)
|
||||
put32(tdsDefaultPacketLen)
|
||||
put32(0)
|
||||
put32(uint32(os.Getpid()))
|
||||
put32(0)
|
||||
body.WriteByte(tdsOptionUseDB | tdsOptionSetLang)
|
||||
body.WriteByte(tdsOptionODBC)
|
||||
body.WriteByte(0)
|
||||
body.WriteByte(0)
|
||||
put32(0)
|
||||
put32(0)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
put16(offsets[i])
|
||||
put16(lengths[i])
|
||||
}
|
||||
put16(0)
|
||||
put16(0)
|
||||
for i := 5; i < 8; i++ {
|
||||
put16(offsets[i])
|
||||
put16(lengths[i])
|
||||
}
|
||||
body.Write([]byte{0, 0, 0, 0, 0, 0})
|
||||
put16(offsets[8])
|
||||
put16(0)
|
||||
for i := 8; i < 10; i++ {
|
||||
put16(offsets[i])
|
||||
put16(lengths[i])
|
||||
}
|
||||
put32(0)
|
||||
|
||||
for _, value := range encoded {
|
||||
body.Write(value)
|
||||
}
|
||||
return mssqlWritePacket(w, tdsPacketLogin7, body.Bytes())
|
||||
}
|
||||
|
||||
func mssqlReadLoginResponse(r io.Reader, result *mssqlRawResult) error {
|
||||
for {
|
||||
packetType, payload, err := mssqlReadMessage(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if packetType != tdsPacketReply {
|
||||
return fmt.Errorf("mssql: unexpected login response packet type %d", packetType)
|
||||
}
|
||||
done, err := mssqlParseLoginTokens(payload, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if done || result.sawLoginAck || len(result.errors) > 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mssqlParseLoginTokens(payload []byte, result *mssqlRawResult) (bool, error) {
|
||||
pos := 0
|
||||
for pos < len(payload) {
|
||||
token := payload[pos]
|
||||
pos++
|
||||
switch token {
|
||||
case tdsTokenError:
|
||||
errMsg, next, err := mssqlParseErrorToken(payload, pos)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
result.errors = append(result.errors, errMsg)
|
||||
pos = next
|
||||
case tdsTokenInfo:
|
||||
next, err := mssqlSkipUSVarError(payload, pos)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
pos = next
|
||||
case tdsTokenEnvChange:
|
||||
next, err := mssqlSkipLen16(payload, pos)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
pos = next
|
||||
case tdsTokenLoginAck:
|
||||
next, err := mssqlSkipLen16(payload, pos)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
result.sawLoginAck = true
|
||||
pos = next
|
||||
case tdsTokenDone, tdsTokenDoneProc, tdsTokenDoneInProc:
|
||||
if pos+12 > len(payload) {
|
||||
return false, fmt.Errorf("mssql: truncated done token")
|
||||
}
|
||||
status := binary.LittleEndian.Uint16(payload[pos : pos+2])
|
||||
pos += 12
|
||||
return status&(tdsDoneError|tdsDoneSrvError) == 0, nil
|
||||
default:
|
||||
return false, fmt.Errorf("mssql: unexpected login token 0x%02x", token)
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func mssqlParseErrorToken(payload []byte, pos int) (mssqlRawError, int, error) {
|
||||
if pos+2 > len(payload) {
|
||||
return mssqlRawError{}, pos, fmt.Errorf("mssql: truncated error token")
|
||||
}
|
||||
size := int(binary.LittleEndian.Uint16(payload[pos : pos+2]))
|
||||
end := pos + 2 + size
|
||||
if size < 6 || end > len(payload) || pos+8 > len(payload) {
|
||||
return mssqlRawError{}, pos, fmt.Errorf("mssql: invalid error token size")
|
||||
}
|
||||
pos += 2
|
||||
number := int32(binary.LittleEndian.Uint32(payload[pos : pos+4]))
|
||||
pos += 4
|
||||
pos += 2
|
||||
message, next, err := mssqlReadUSVarChar(payload, pos)
|
||||
if err != nil {
|
||||
return mssqlRawError{}, pos, err
|
||||
}
|
||||
return mssqlRawError{number: number, message: message}, end, mssqlEnsureSkipBVarStrings(payload, next, end)
|
||||
}
|
||||
|
||||
func mssqlSkipUSVarError(payload []byte, pos int) (int, error) {
|
||||
if pos+2 > len(payload) {
|
||||
return pos, fmt.Errorf("mssql: truncated info token")
|
||||
}
|
||||
size := int(binary.LittleEndian.Uint16(payload[pos : pos+2]))
|
||||
end := pos + 2 + size
|
||||
if size < 6 || end > len(payload) || pos+8 > len(payload) {
|
||||
return pos, fmt.Errorf("mssql: invalid info token size")
|
||||
}
|
||||
_, _, err := mssqlReadUSVarChar(payload, pos+8)
|
||||
return end, err
|
||||
}
|
||||
|
||||
func mssqlEnsureSkipBVarStrings(payload []byte, pos, end int) error {
|
||||
for i := 0; i < 2; i++ {
|
||||
if pos >= end {
|
||||
return fmt.Errorf("mssql: truncated string in error token")
|
||||
}
|
||||
length := int(payload[pos]) * 2
|
||||
pos++
|
||||
if pos+length > end {
|
||||
return fmt.Errorf("mssql: invalid string in error token")
|
||||
}
|
||||
pos += length
|
||||
}
|
||||
if pos+4 > end {
|
||||
return fmt.Errorf("mssql: truncated error line number")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mssqlSkipLen16(payload []byte, pos int) (int, error) {
|
||||
if pos+2 > len(payload) {
|
||||
return pos, fmt.Errorf("mssql: truncated token")
|
||||
}
|
||||
size := int(binary.LittleEndian.Uint16(payload[pos : pos+2]))
|
||||
next := pos + 2 + size
|
||||
if next > len(payload) {
|
||||
return pos, fmt.Errorf("mssql: invalid token size")
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func mssqlReadUSVarChar(payload []byte, pos int) (string, int, error) {
|
||||
if pos+2 > len(payload) {
|
||||
return "", pos, fmt.Errorf("mssql: truncated us varchar")
|
||||
}
|
||||
chars := int(binary.LittleEndian.Uint16(payload[pos : pos+2]))
|
||||
pos += 2
|
||||
size := chars * 2
|
||||
if pos+size > len(payload) {
|
||||
return "", pos, fmt.Errorf("mssql: invalid us varchar size")
|
||||
}
|
||||
return mssqlDecodeUCS2(payload[pos : pos+size]), pos + size, nil
|
||||
}
|
||||
|
||||
func mssqlWritePacket(w io.Writer, packetType byte, payload []byte) error {
|
||||
if len(payload)+8 > 0xffff {
|
||||
return fmt.Errorf("mssql: packet too large")
|
||||
}
|
||||
header := []byte{packetType, tdsStatusEOM, 0, 0, 0, 0, 1, 0}
|
||||
binary.BigEndian.PutUint16(header[2:4], uint16(len(payload)+8))
|
||||
if _, err := w.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(payload)
|
||||
return err
|
||||
}
|
||||
|
||||
func mssqlReadMessage(r io.Reader) (byte, []byte, error) {
|
||||
var packetType byte
|
||||
var payload []byte
|
||||
for {
|
||||
header := make([]byte, 8)
|
||||
if _, err := io.ReadFull(r, header); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if packetType == 0 {
|
||||
packetType = header[0]
|
||||
} else if packetType != header[0] {
|
||||
return 0, nil, fmt.Errorf("mssql: packet type changed in message")
|
||||
}
|
||||
size := int(binary.BigEndian.Uint16(header[2:4]))
|
||||
if size < 8 {
|
||||
return 0, nil, fmt.Errorf("mssql: invalid packet size")
|
||||
}
|
||||
chunk := make([]byte, size-8)
|
||||
if _, err := io.ReadFull(r, chunk); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
payload = append(payload, chunk...)
|
||||
if header[1]&tdsStatusEOM != 0 {
|
||||
return packetType, payload, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mssqlUCS2(s string) []byte {
|
||||
runes := utf16.Encode([]rune(s))
|
||||
out := make([]byte, len(runes)*2)
|
||||
for i, r := range runes {
|
||||
binary.LittleEndian.PutUint16(out[i*2:], r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mssqlDecodeUCS2(data []byte) string {
|
||||
if len(data)%2 != 0 {
|
||||
data = data[:len(data)-1]
|
||||
}
|
||||
runes := make([]uint16, len(data)/2)
|
||||
for i := range runes {
|
||||
runes[i] = binary.LittleEndian.Uint16(data[i*2:])
|
||||
}
|
||||
return string(utf16.Decode(runes))
|
||||
}
|
||||
|
||||
func mssqlEncodePassword(password string) []byte {
|
||||
out := mssqlUCS2(password)
|
||||
for i, ch := range out {
|
||||
out[i] = (((ch << 4) & 0xff) | (ch >> 4)) ^ 0xa5
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user