From 1aff867491442092f95b23afee227d1bdaae0561 Mon Sep 17 00:00:00 2001 From: tejgokani Date: Mon, 25 May 2026 14:47:57 +0530 Subject: [PATCH 1/3] fix(telnetmini): replace len<12 guard with len<48 to prevent slice-bounds panic on truncated NTLM challenge ParseNTLMResponse read fixed-header fields at offsets 12-14, 16-20, 40-42, and 44-48 after only checking len>=12, causing a slice-bounds-out-of-range panic for any NTLM section between 12 and 47 bytes. Because this input comes from untrusted network data, a malicious or malformed telnet server could crash the scanner (or the embedding host process when used via the SDK). Raise the guard to len<48 so the full fixed header is present before any field is touched. Add unit tests covering the happy path, existing error cases, and a no-panic regression table for every truncated length in [12,47]. Co-Authored-By: Claude Sonnet 4.6 --- pkg/utils/telnetmini/ntlm.go | 8 +- pkg/utils/telnetmini/ntlm_test.go | 126 ++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 pkg/utils/telnetmini/ntlm_test.go 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..ffedd5344b --- /dev/null +++ b/pkg/utils/telnetmini/ntlm_test.go @@ -0,0 +1,126 @@ +package telnetmini + +import ( + "encoding/binary" + "strings" + "testing" +) + +// buildChallenge constructs a minimal well-formed 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) + + // NTLMSSP\0 signature (8 bytes) + copy(ntlm[0:], "NTLMSSP\x00") + + // Message type 2 (Challenge) at offset 8 + if headerLen >= 12 { + binary.LittleEndian.PutUint32(ntlm[8:12], 2) + } + + // Wrap with telnet framing: prefix + Sub-option End terminator + 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) + + // Target name: len=6, max len=6, offset=48 + 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) + + // Negotiate flags at 20 (4 bytes) – zero is fine for this test + // Server challenge at 24 (8 bytes) – zero + // Reserved at 32 (8 bytes) – zero + + // Target info: len=0, offset=48 (no target info block) + 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") + } +} + +func TestParseNTLMResponse_ErrorCases(t *testing.T) { + 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: append(func() []byte { b := make([]byte, 48); copy(b, "NTLMSSP\x00"); binary.LittleEndian.PutUint32(b[8:12], 1); return b }(), 0xFF, 0xF0), + 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) + } + } +} From 79105261bd497c5b268d76f5bb9a2f03a6b458b9 Mon Sep 17 00:00:00 2001 From: tejgokani Date: Mon, 25 May 2026 15:05:00 +0530 Subject: [PATCH 2/3] =?UTF-8?q?test(telnetmini):=20address=20CodeRabbit=20?= =?UTF-8?q?review=20=E2=80=94=20add=2048-byte=20boundary=20test=20and=20cl?= =?UTF-8?q?ean=20up=20wrong-type=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add TestParseNTLMResponse_Minimal48Bytes to explicitly cover the exact lower boundary of the len<48 guard (minimal valid challenge, no target name or info) - Replace the inline immediately-invoked function for the wrong-message-type case with a pre-built local variable for readability Co-Authored-By: Claude Sonnet 4.6 --- pkg/utils/telnetmini/ntlm_test.go | 32 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/pkg/utils/telnetmini/ntlm_test.go b/pkg/utils/telnetmini/ntlm_test.go index ffedd5344b..dbaac973c9 100644 --- a/pkg/utils/telnetmini/ntlm_test.go +++ b/pkg/utils/telnetmini/ntlm_test.go @@ -6,20 +6,17 @@ import ( "testing" ) -// buildChallenge constructs a minimal well-formed NTLM type-2 challenge message -// of exactly headerLen bytes, wrapped in the telnet framing expected by ParseNTLMResponse. +// 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) - // NTLMSSP\0 signature (8 bytes) copy(ntlm[0:], "NTLMSSP\x00") - // Message type 2 (Challenge) at offset 8 if headerLen >= 12 { binary.LittleEndian.PutUint32(ntlm[8:12], 2) } - // Wrap with telnet framing: prefix + Sub-option End terminator var out []byte out = append(out, ntlm...) out = append(out, 0xFF, 0xF0) @@ -35,16 +32,10 @@ func buildValidChallenge() []byte { copy(ntlm[0:], "NTLMSSP\x00") binary.LittleEndian.PutUint32(ntlm[8:12], 2) - // Target name: len=6, max len=6, offset=48 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) - // Negotiate flags at 20 (4 bytes) – zero is fine for this test - // Server challenge at 24 (8 bytes) – zero - // Reserved at 32 (8 bytes) – zero - - // Target info: len=0, offset=48 (no target info block) binary.LittleEndian.PutUint16(ntlm[40:42], 0) binary.LittleEndian.PutUint32(ntlm[44:48], 48) @@ -67,7 +58,24 @@ func TestParseNTLMResponse_Valid(t *testing.T) { } } +// 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 @@ -95,7 +103,7 @@ func TestParseNTLMResponse_ErrorCases(t *testing.T) { }, { name: "wrong message type", - input: append(func() []byte { b := make([]byte, 48); copy(b, "NTLMSSP\x00"); binary.LittleEndian.PutUint32(b[8:12], 1); return b }(), 0xFF, 0xF0), + input: wrongTypeChallenge, wantErr: "expected NTLM challenge message", }, } From 840c0ffe668f67a9f8512c6ac06e6b0a3dfd8778 Mon Sep 17 00:00:00 2001 From: Mzack9999 Date: Mon, 25 May 2026 14:12:08 +0200 Subject: [PATCH 3/3] downgrading gologger --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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=