Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions e2e/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ require (
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/BobuSumisu/aho-corasick v1.0.3 // indirect
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions e2e/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g=
github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/Infisical/infisical-merge
go 1.25.9

require (
github.com/Azure/go-ntlmssp v0.1.1
github.com/BobuSumisu/aho-corasick v1.0.3
github.com/Masterminds/sprig/v3 v3.3.0
github.com/awnumar/memguard v0.23.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g=
github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
Expand Down
115 changes: 108 additions & 7 deletions packages/pam/handlers/mssql/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sync"
"time"

"github.com/Azure/go-ntlmssp"
"github.com/Infisical/infisical-merge/packages/pam/session"
"github.com/rs/zerolog/log"
)
Expand All @@ -18,6 +19,8 @@ type MssqlProxyConfig struct {
InjectUsername string
InjectPassword string
InjectDatabase string
InjectDomain string
AuthMethod string // "sql-login" or "ntlm"
EnableTLS bool
TLSConfig *tls.Config
SessionID string
Expand Down Expand Up @@ -231,7 +234,19 @@ func (p *MssqlProxy) connectAndAuthenticateToServer() (net.Conn, []*TDSPacket, e
log.Info().Str("sessionID", p.config.SessionID).Msg("TLS established with server")
}

// 4. Send LOGIN7 with injected credentials
if p.config.AuthMethod == "ntlm" {
return p.authenticateNTLM(serverConn)
}
return p.authenticateSQL(serverConn)
}

func (p *MssqlProxy) authenticateSQL(serverConn net.Conn) (_ net.Conn, _ []*TDSPacket, retErr error) {
defer func() {
if retErr != nil {
serverConn.Close()
}
}()

loginMsg := &Login7Message{
Username: p.config.InjectUsername,
Password: p.config.InjectPassword,
Expand All @@ -247,7 +262,6 @@ func (p *MssqlProxy) connectAndAuthenticateToServer() (net.Conn, []*TDSPacket, e
Payload: loginMsg.Encode(),
}
if err := loginPkt.Write(serverConn); err != nil {
serverConn.Close()
return nil, nil, fmt.Errorf("send login to server: %w", err)
}

Expand All @@ -257,11 +271,8 @@ func (p *MssqlProxy) connectAndAuthenticateToServer() (net.Conn, []*TDSPacket, e
Int("loginPktLen", len(loginPkt.Payload)+TDSHeaderSize).
Msg("Sent LOGIN7 to server")

// 5. Read login response - forward to client
log.Info().Str("sessionID", p.config.SessionID).Msg("Waiting for login response...")
response, err := ReadAllPackets(serverConn)
if err != nil {
serverConn.Close()
return nil, nil, fmt.Errorf("read login response: %w", err)
}
log.Info().
Expand All @@ -271,18 +282,108 @@ func (p *MssqlProxy) connectAndAuthenticateToServer() (net.Conn, []*TDSPacket, e

respPayload := CombinePayloads(response)
if ContainsToken(respPayload, TokenError) {
serverConn.Close()
return nil, nil, fmt.Errorf("server authentication failed")
}
if !ContainsToken(respPayload, TokenLoginAck) {
serverConn.Close()
return nil, nil, fmt.Errorf("no login ack from server")
}

log.Info().Str("sessionID", p.config.SessionID).Msg("MSSQL server authentication successful")
return serverConn, response, nil
}

func (p *MssqlProxy) authenticateNTLM(serverConn net.Conn) (_ net.Conn, _ []*TDSPacket, retErr error) {
defer func() {
if retErr != nil {
serverConn.Close()
}
}()

negotiate, err := ntlmssp.NewNegotiateMessage(p.config.InjectDomain, "infisical-proxy")
if err != nil {
return nil, nil, fmt.Errorf("create NTLM negotiate message: %w", err)
}

loginMsg := &Login7Message{
Database: p.config.InjectDatabase,
AppName: "Infisical PAM Proxy",
Hostname: "infisical-proxy",
SSPIData: negotiate,
}

loginPkt := &TDSPacket{
Type: PacketTypeLogin7,
Status: StatusEOM,
PacketID: 1,
Payload: loginMsg.Encode(),
}
if err := loginPkt.Write(serverConn); err != nil {
return nil, nil, fmt.Errorf("send NTLM login to server: %w", err)
}

log.Info().
Str("sessionID", p.config.SessionID).
Str("domain", p.config.InjectDomain).
Str("user", p.config.InjectUsername).
Msg("Sent LOGIN7 with NTLM negotiate to server")

challengeResponse, err := ReadAllPackets(serverConn)
if err != nil {
return nil, nil, fmt.Errorf("read NTLM challenge: %w", err)
}

challengePayload := CombinePayloads(challengeResponse)

challengeToken, err := ExtractSSPIToken(challengePayload)
if err != nil {
if ContainsToken(challengePayload, TokenError) {
return nil, nil, fmt.Errorf("server rejected NTLM negotiate")
}
return nil, nil, fmt.Errorf("extract NTLM challenge: %w", err)
}

log.Info().
Str("sessionID", p.config.SessionID).
Int("challengeLen", len(challengeToken)).
Msg("Received NTLM challenge from server")

ntlmUsername := p.config.InjectDomain + "\\" + p.config.InjectUsername
authenticate, err := ntlmssp.ProcessChallenge(challengeToken, ntlmUsername, p.config.InjectPassword, true)
if err != nil {
return nil, nil, fmt.Errorf("process NTLM challenge: %w", err)
}
Comment thread
saifsmailbox98 marked this conversation as resolved.

sspiPkt := &TDSPacket{
Type: PacketTypeSSPI,
Status: StatusEOM,
PacketID: 1,
Payload: authenticate,
}
if err := sspiPkt.Write(serverConn); err != nil {
return nil, nil, fmt.Errorf("send NTLM authenticate: %w", err)
}

log.Info().
Str("sessionID", p.config.SessionID).
Msg("Sent NTLM authenticate to server")

response, err := ReadAllPackets(serverConn)
if err != nil {
return nil, nil, fmt.Errorf("read NTLM login response: %w", err)
}

respPayload := CombinePayloads(response)
if ContainsToken(respPayload, TokenError) {
return nil, nil, fmt.Errorf("NTLM authentication failed")
}
if !ContainsToken(respPayload, TokenLoginAck) {
return nil, nil, fmt.Errorf("no login ack after NTLM authentication")
}

log.Info().Str("sessionID", p.config.SessionID).Msg("MSSQL NTLM authentication successful")
return serverConn, response, nil
}

func (p *MssqlProxy) proxyToServer(client, server net.Conn, errCh chan error) {
defer func() {
if r := recover(); r != nil {
Expand Down
48 changes: 43 additions & 5 deletions packages/pam/handlers/mssql/tds.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ const (
MaxPackets = 100
)

var ntlmsspSignature = []byte("NTLMSSP\x00")

// ExtractSSPIToken finds the NTLM token in a TDS server response by scanning for the NTLMSSP signature.
func ExtractSSPIToken(payload []byte) ([]byte, error) {
idx := bytes.Index(payload, ntlmsspSignature)
if idx < 0 {
return nil, fmt.Errorf("no NTLMSSP token found in server response")
}
return payload[idx:], nil
}

// TDSPacket represents a TDS packet
type TDSPacket struct {
Type uint8
Expand Down Expand Up @@ -294,6 +305,7 @@ type Login7Message struct {
Password string
AppName string
Database string
SSPIData []byte
}

// ParseLogin7 parses a LOGIN7 message (extracts only what we need)
Expand Down Expand Up @@ -324,7 +336,8 @@ const (
fSetLang = 0x80

// OptionFlags2
fODBC = 0x02
fODBC = 0x02
fIntSecurity = 0x80 // Integrated Security (SSPI/NTLM)
)

// Encode serializes the LOGIN7 message
Expand All @@ -341,9 +354,19 @@ func (m *Login7Message) Encode() []byte {
m.Header.OptionFlags1 = fUseDB | fSetLang
m.Header.OptionFlags2 = fODBC

useSSPI := len(m.SSPIData) > 0

hostname := encodeUTF16(m.Hostname)
username := encodeUTF16(m.Username)
password := manglePassword(m.Password)
var username, password []byte
if useSSPI {
// NTLM: username and password are empty in LOGIN7; auth is via SSPI blob
username = nil
password = nil
m.Header.OptionFlags2 |= fIntSecurity
} else {
username = encodeUTF16(m.Username)
password = manglePassword(m.Password)
}
appname := encodeUTF16(m.AppName)
database := encodeUTF16(m.Database)
cltIntName := encodeUTF16("ODBC") // Client interface name
Expand Down Expand Up @@ -385,12 +408,24 @@ func (m *Login7Message) Encode() []byte {
offset += uint16(len(database))

m.Header.SSPIOffset = offset
m.Header.SSPILength = 0
if useSSPI {
sspiLen := len(m.SSPIData)
if sspiLen < 65535 {
m.Header.SSPILength = uint16(sspiLen)
m.Header.SSPILongLength = 0
} else {
m.Header.SSPILength = 0
m.Header.SSPILongLength = uint32(sspiLen)
}
offset += uint16(sspiLen)
} else {
m.Header.SSPILength = 0
Comment thread
saifsmailbox98 marked this conversation as resolved.
m.Header.SSPILongLength = 0
}
m.Header.AtchDBFileOffset = offset
m.Header.AtchDBFileLength = 0
m.Header.ChangePasswordOff = offset
m.Header.ChangePasswordLen = 0
m.Header.SSPILongLength = 0

m.Header.Length = uint32(offset)

Expand All @@ -404,6 +439,9 @@ func (m *Login7Message) Encode() []byte {
buf.Write(appname)
buf.Write(cltIntName)
buf.Write(database)
if useSSPI {
buf.Write(m.SSPIData)
}

return buf.Bytes()
}
Expand Down
2 changes: 2 additions & 0 deletions packages/pam/pam-proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo
InjectUsername: credentials.Username,
InjectPassword: credentials.Password,
InjectDatabase: credentials.Database,
InjectDomain: credentials.Domain,
AuthMethod: credentials.AuthMethod,
EnableTLS: credentials.SSLEnabled,
TLSConfig: tlsConfig,
SessionID: pamConfig.SessionId,
Expand Down
Loading