diff --git a/go.mod b/go.mod index dde64a62a4..42631fbc23 100644 --- a/go.mod +++ b/go.mod @@ -102,7 +102,7 @@ require ( github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb github.com/projectdiscovery/goflags v0.1.74 - github.com/projectdiscovery/gologger v1.1.69 + github.com/projectdiscovery/gologger v1.1.68 github.com/projectdiscovery/gostruct v0.0.2 github.com/projectdiscovery/govaluate v0.0.0-20260504230327-80320480bb6e github.com/projectdiscovery/gozero v0.1.1-0.20251027191944-a4ea43320b81 diff --git a/go.sum b/go.sum index a6766d177e..e60bf251aa 100644 --- a/go.sum +++ b/go.sum @@ -879,8 +879,8 @@ github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb h1:rutG90 github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb/go.mod h1:FLjF1DmZ+POoGEiIQdWuYVwS++C/GwpX8YaCsTSm1RY= github.com/projectdiscovery/goflags v0.1.74 h1:n85uTRj5qMosm0PFBfsvOL24I7TdWRcWq/1GynhXS7c= github.com/projectdiscovery/goflags v0.1.74/go.mod h1:UMc9/7dFz2oln+10tv6cy+7WZKTHf9UGhaNkF95emh4= -github.com/projectdiscovery/gologger v1.1.69 h1:lj839gk8x0RhS5tOi9aqYQ+LGU0v7JwEL1Kitrqzt/k= -github.com/projectdiscovery/gologger v1.1.69/go.mod h1:kpLKNafZWRN9P7WpJYtIOY/XvY/v41GDdU9NzICdKmo= +github.com/projectdiscovery/gologger v1.1.68 h1:KfdIO/3X7BtHssWZuqhxPZ+A946epCCx2cz+3NnRAnU= +github.com/projectdiscovery/gologger v1.1.68/go.mod h1:Xae0t4SeqJVa0RQGK9iECx/+HfXhvq70nqOQp2BuW+o= github.com/projectdiscovery/gostruct v0.0.2 h1:s8gP8ApugGM4go1pA+sVlPDXaWqNP5BBDDSv7VEdG1M= github.com/projectdiscovery/gostruct v0.0.2/go.mod h1:H86peL4HKwMXcQQtEa6lmC8FuD9XFt6gkNR0B/Mu5PE= github.com/projectdiscovery/govaluate v0.0.0-20260504230327-80320480bb6e h1:o+ulEIaC2+9V2Ezr6mI5xEhKWsf0V/+FUQIS723Aj6U= diff --git a/pkg/utils/telnetmini/ntlm.go b/pkg/utils/telnetmini/ntlm.go index b4105b52bc..3939235a20 100644 --- a/pkg/utils/telnetmini/ntlm.go +++ b/pkg/utils/telnetmini/ntlm.go @@ -40,9 +40,11 @@ func ParseNTLMResponse(data []byte) (*NTLMInfoResponse, error) { // Extract NTLM data (NTLMSSP.*\xff\xf0) ntlmData := data[ntlmStart : ntlmStart+ntlmEnd] - // Check message type (should be 2 for Challenge) - if len(ntlmData) < 12 { - return nil, fmt.Errorf("NTLM response too short") + // Check message type (should be 2 for Challenge). + // The fixed header runs to offset 48 (target-info offset field ends at byte 48), + // so reject anything shorter before touching any field offsets. + if len(ntlmData) < 48 { + return nil, fmt.Errorf("NTLM response too short: need at least 48 bytes, got %d", len(ntlmData)) } messageType := binary.LittleEndian.Uint32(ntlmData[8:12]) diff --git a/pkg/utils/telnetmini/ntlm_test.go b/pkg/utils/telnetmini/ntlm_test.go new file mode 100644 index 0000000000..dbaac973c9 --- /dev/null +++ b/pkg/utils/telnetmini/ntlm_test.go @@ -0,0 +1,134 @@ +package telnetmini + +import ( + "encoding/binary" + "strings" + "testing" +) + +// buildChallenge constructs a minimal NTLM type-2 challenge message of exactly +// headerLen bytes, wrapped in the telnet framing expected by ParseNTLMResponse. +func buildChallenge(headerLen int) []byte { + ntlm := make([]byte, headerLen) + + copy(ntlm[0:], "NTLMSSP\x00") + + if headerLen >= 12 { + binary.LittleEndian.PutUint32(ntlm[8:12], 2) + } + + var out []byte + out = append(out, ntlm...) + out = append(out, 0xFF, 0xF0) + return out +} + +// buildValidChallenge returns a fully-formed 48-byte NTLM type-2 challenge with +// a small UTF-16LE target name appended after the fixed header. +func buildValidChallenge() []byte { + targetName := []byte("W\x00I\x00N\x00") // "WIN" in UTF-16LE + ntlm := make([]byte, 48+len(targetName)) + + copy(ntlm[0:], "NTLMSSP\x00") + binary.LittleEndian.PutUint32(ntlm[8:12], 2) + + binary.LittleEndian.PutUint16(ntlm[12:14], uint16(len(targetName))) + binary.LittleEndian.PutUint16(ntlm[14:16], uint16(len(targetName))) + binary.LittleEndian.PutUint32(ntlm[16:20], 48) + + binary.LittleEndian.PutUint16(ntlm[40:42], 0) + binary.LittleEndian.PutUint32(ntlm[44:48], 48) + + copy(ntlm[48:], targetName) + + var out []byte + out = append(out, ntlm...) + out = append(out, 0xFF, 0xF0) + return out +} + +func TestParseNTLMResponse_Valid(t *testing.T) { + data := buildValidChallenge() + resp, err := ParseNTLMResponse(data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +// TestParseNTLMResponse_Minimal48Bytes verifies that a challenge with exactly +// 48 bytes (the minimum valid fixed-header size, no target name or info) is +// accepted — confirming the boundary condition of the len<48 guard. +func TestParseNTLMResponse_Minimal48Bytes(t *testing.T) { + data := buildChallenge(48) + resp, err := ParseNTLMResponse(data) + if err != nil { + t.Fatalf("48-byte challenge should be valid, got error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response for 48-byte challenge") + } +} + +func TestParseNTLMResponse_ErrorCases(t *testing.T) { + wrongTypeChallenge := buildChallenge(48) + binary.LittleEndian.PutUint32(wrongTypeChallenge[8:12], 1) + + tests := []struct { + name string + input []byte + wantErr string + }{ + { + name: "nil input", + input: nil, + wantErr: "NTLMSSP signature not found", + }, + { + name: "empty input", + input: []byte{}, + wantErr: "NTLMSSP signature not found", + }, + { + name: "missing NTLMSSP signature", + input: []byte("hello world\xFF\xF0"), + wantErr: "NTLMSSP signature not found", + }, + { + name: "missing Sub-option End terminator", + input: []byte("NTLMSSP\x00" + strings.Repeat("\x00", 40)), + wantErr: "not properly terminated", + }, + { + name: "wrong message type", + input: wrongTypeChallenge, + wantErr: "expected NTLM challenge message", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := ParseNTLMResponse(tc.input) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("error %q does not contain %q", err.Error(), tc.wantErr) + } + }) + } +} + +// TestParseNTLMResponse_TruncatedNoPanic verifies that every NTLM section length +// in [12, 47] returns an error instead of panicking (regression for slice-bounds bug). +func TestParseNTLMResponse_TruncatedNoPanic(t *testing.T) { + for length := 12; length < 48; length++ { + data := buildChallenge(length) + _, err := ParseNTLMResponse(data) + if err == nil { + t.Errorf("length %d: expected error for truncated challenge, got nil", length) + } + } +}