mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-13 17:56:33 -10:00
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 <[email protected]>
This commit is contained in:
1 parent
3684849ac9
commit
1aff867491
2 files changed
+131
-3
No files matched your search
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user