projectdiscovery/nuclei · warning

expected NTLM challenge message, got type %d

Error message

expected NTLM challenge message, got type %d

What it means

ParseNTLMResponse reads the NTLM message type at offset 8 and requires 2 (NEGOTIATE-to-CHALLENGE exchange: the server's Challenge message is what carries target name, timestamps, and NetBIOS info this parser extracts). Type 1 (Negotiate), type 3 (Authenticate), or garbage yields 'expected NTLM challenge message, got type %d'.

Source

Thrown at pkg/utils/telnetmini/ntlm.go:52

	// Find the end of NTLM data (Sub-option End: 0xFF 0xF0)
	ntlmEnd := bytes.Index(data[ntlmStart:], []byte{0xFF, 0xF0})
	if ntlmEnd == -1 {
		return nil, fmt.Errorf("NTLM response not properly terminated with Sub-option End")
	}

	// Extract NTLM data (NTLMSSP.*\xff\xf0)
	ntlmData := data[ntlmStart : ntlmStart+ntlmEnd]

	// 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])
	if messageType != 2 {
		return nil, fmt.Errorf("expected NTLM challenge message, got type %d", messageType)
	}

	// Parse target name fields
	targetNameLen := binary.LittleEndian.Uint16(ntlmData[12:14])
	targetNameOffset := binary.LittleEndian.Uint32(ntlmData[16:20])

	// Parse target info fields
	targetInfoLen := binary.LittleEndian.Uint16(ntlmData[40:42])
	targetInfoOffset := binary.LittleEndian.Uint32(ntlmData[44:48])

	// Extract target name (Target Name will always be returned under any implementation)
	var targetName string
	if targetNameLen > 0 && int(targetNameOffset) < len(ntlmData) {
		end := int(targetNameOffset) + int(targetNameLen)
		if end <= len(ntlmData) {
			targetName = string(ntlmData[targetNameOffset:end])
		}
	}

View on GitHub (pinned to 265b3a3dec)

Solutions

  1. Treat as unsupported — the parser only exists to harvest Challenge-message info, other types carry none of it
  2. If the reported type is 1, the server likely echoed your negotiation — one retry occasionally gets the real Challenge
  3. Verify the target really is Windows telnet (banner/other probes) before investing further
  4. Hexdump the blob when integrating telnetmini yourself, to confirm which message type the device emits
Defensive patterns

Strategy: validation

Validate before calling

// Peek at the message type (offset 8, LE uint32) before full parsing:
start := bytes.Index(raw, []byte("NTLMSSP"))
if start >= 0 && len(raw) >= start+12 {
    if mt := binary.LittleEndian.Uint32(raw[start+8 : start+12]); mt != 2 {
        return nil, fmt.Errorf("not a challenge message (type %d) — nothing to extract", mt)
    }
}

Type guard

func isNTLMChallenge(data []byte) bool {
    i := bytes.Index(data, []byte("NTLMSSP"))
    return i >= 0 && len(data) >= i+12 && binary.LittleEndian.Uint32(data[i+8:i+12]) == 2
}

Try / catch

if err != nil && strings.Contains(err.Error(), "expected NTLM challenge") {
    return nil, nil // wrong message type — no info to harvest, skip host
}

Prevention

When it happens

Trigger: The captured sub-option contains a different NTLM message than the challenge: e.g. a server that echoes the client's type-1 Negotiate back, a capture that picked up a type-3 exchange, or 4+ bytes of non-message data that happens to follow the signature. Type values other than 2 (commonly 1 or 3) appear in the message.

Common situations: Non-compliant telnet implementations that mirror negotiation; security appliances that respond with odd NTLM blobs; off-by-one conditions where the signature match lands on embedded NTLM bytes inside other data (e.g. SMB-over-telnet tunnels).

Related errors


AI-assisted analysis of projectdiscovery/nuclei@265b3a3dec (2026-08-15). Data as JSON: /api/errors/4026dcb4bc70c4dd. Report an issue: GitHub.