Skip to content

fix(telnetmini): prevent slice-bounds panic in ParseNTLMResponse on truncated NTLM challenge - #7425

Merged
Mzack9999 merged 3 commits into
projectdiscovery:devfrom
tejgokani:test/telnetmini-coverage
May 25, 2026
Merged

fix(telnetmini): prevent slice-bounds panic in ParseNTLMResponse on truncated NTLM challenge#7425
Mzack9999 merged 3 commits into
projectdiscovery:devfrom
tejgokani:test/telnetmini-coverage

Conversation

@tejgokani

@tejgokani tejgokani commented May 25, 2026

Copy link
Copy Markdown
Contributor

fix(telnetmini): prevent slice-bounds panic in ParseNTLMResponse on truncated NTLM challenge

Background

pkg/utils/telnetmini implements the NTLM negotiation handshake used during
telnet fingerprinting, mirroring the logic from Nmap's telnet-ntlm-info.nse
script. ParseNTLMResponse reads the server's type-2 Challenge message off the
wire, extracts system metadata (NetBIOS names, DNS names, OS version), and
returns it as an NTLMInfoResponse. Because the input comes directly from a
remote host, it is fully attacker-controlled.


The Bug

ParseNTLMResponse panics with a runtime error: slice bounds out of range
whenever the NTLMSSP section of the response is between 12 and 47 bytes long.

Root cause

The function checks len(ntlmData) < 12 and then reads the message type at
ntlmData[8:12]. After that single check it reads four more fields from fixed
offsets in the header:

Field Bytes read
Target name length ntlmData[12:14]
Target name offset ntlmData[16:20]
Target info length ntlmData[40:42]
Target info offset ntlmData[44:48]

Per [MS-NLMP], the type-2 Challenge fixed header spans bytes 0–47 (48 bytes
total). The guard only guaranteed enough bytes to read the message type — it
said nothing about the offsets at bytes 40–47. Any ntlmData of length 12–47
therefore passes the guard, passes the type check, and then panics on one of
the later reads.

Why this matters

The input is raw network data. A malformed or deliberately crafted telnet
server can send a response whose NTLMSSP section is between 12 and 47 bytes.
In the standalone scanner this crashes the current scan. When Nuclei is
embedded via the Go SDK an unrecovered panic in this goroutine can propagate
and take down the entire host process.


The Fix

One line change in pkg/utils/telnetmini/ntlm.go.

Replace:

if len(ntlmData) < 12 {
    return nil, fmt.Errorf("NTLM response too short")
}

With:

if len(ntlmData) < 48 {
    return nil, fmt.Errorf("NTLM response too short: need at least 48 bytes, got %d", len(ntlmData))
}

The fixed header must be fully present before any field is read. The existing
per-field bounds checks for the variable-length targetName and targetInfo
blocks that follow the header are already correct and require no changes.


Files Changed

File Change
pkg/utils/telnetmini/ntlm.go Raise the minimum-length guard from 12 to 48
pkg/utils/telnetmini/ntlm_test.go New test file (see below)

Tests

New file: pkg/utils/telnetmini/ntlm_test.go

TestParseNTLMResponse_Valid

Constructs a fully-formed 48-byte type-2 Challenge with a UTF-16LE target name
appended after the header. Asserts a non-nil response and no error.

TestParseNTLMResponse_ErrorCases

Table-driven test covering all existing error paths:

Case Expected error substring
nil input NTLMSSP signature not found
empty input NTLMSSP signature not found
missing NTLMSSP signature NTLMSSP signature not found
missing Sub-option End terminator not properly terminated
wrong message type (type 1) expected NTLM challenge message

TestParseNTLMResponse_TruncatedNoPanic (regression)

Iterates every NTLM section length from 12 to 47 inclusive, calls
ParseNTLMResponse, and asserts the function returns an error rather than
panicking. This test fails on unpatched main and passes after the fix.

go test ./pkg/utils/telnetmini/ -v -run TestParseNTLMResponse

All three test functions pass.


Possible Issues After This Change

1. Non-Microsoft servers sending a 12–47-byte NTLM section

What changes.
Before this fix, any response with a 12–47-byte NTLM section caused a panic —
which is never a useful outcome. After this fix those responses return a
graceful error. For any server sending a fully-formed ≥48-byte header (every
spec-compliant implementation) behaviour is completely unchanged.

Is this a real risk?
Unlikely. [MS-NLMP] mandates a 48-byte fixed header for the type-2 Challenge.
The existing code already filters out non-conforming NTLM implementations by
requiring the 0xFF 0xF0 Sub-option End terminator, mirroring the Nmap script
comment that these implementations "do not return valid data." A response that
passes the terminator check but has a sub-48-byte NTLM section is both
out-of-spec and was already broken before (panic instead of error).

How to resolve if a real target is affected.
If a target is observed emitting a short-but-otherwise-valid NTLM section, the
fix is to restructure the reads into per-field length checks rather than a
single upfront guard. The safer default is to document the target as
unsupported, consistent with the existing filtering logic.


2. Error message string change

What changes.
The error text for an undersized response changed from:

NTLM response too short

to:

NTLM response too short: need at least 48 bytes, got N

Is this a real risk?
No. The one call site — pkg/js/libs/telnet/telnet.go:272 — wraps the error
with %w and returns it upstream without inspecting the message string. No
other caller exists in the codebase.

How to resolve if encountered.
Any consumer doing strings.Contains(err.Error(), "too short") is unaffected
since that substring is preserved. An exact-string match would need updating to
the new format.


3. No partial-parse data on truncated input

What changes.
Previously the function panicked mid-read on a 12–47-byte input, so it never
returned a result regardless. After this fix it returns nil, error. The
observable outcome for all callers is identical — a nil response — except the
process no longer crashes.

Is this a real risk?
No. A panic is not a return value. No caller could have been relying on
receiving data from a path that always crashed.

Summary by CodeRabbit

  • Bug Fixes

    • Tightened authentication response validation to require a larger minimum header before parsing and return clearer, specific errors for undersized or malformed inputs.
  • Tests

    • Added boundary and robustness tests for authentication parsing, including the exact-minimum valid challenge, various malformed inputs, and truncated scenarios to ensure proper error handling and prevent panics.

Review Change Stack

…unds 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 <noreply@anthropic.com>
@auto-assign
auto-assign Bot requested a review from Mzack9999 May 25, 2026 09:22
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ac976ba4-3f23-4674-8272-95b1d138046a

📥 Commits

Reviewing files that changed from the base of the PR and between 7910526 and 840c0ff.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (1)
  • go.mod

Walkthrough

The PR hardens NTLM response parsing by increasing the minimum required byte count from 12 to 48 bytes before reading fixed-header offset/length fields, with a more specific error message. Tests add helpers and cases verifying valid parsing, malformed inputs, and truncation safety to prevent panics.

Changes

NTLM Parsing Guard Hardening

Layer / File(s) Summary
NTLM response parsing guard
pkg/utils/telnetmini/ntlm.go
ParseNTLMResponse now requires at least 48 bytes before accessing fixed-header fields, with an updated error message that reports both the required and actual byte counts.
NTLM parsing validation tests
pkg/utils/telnetmini/ntlm_test.go, go.mod
Helper functions buildChallenge and buildValidChallenge construct minimal and fully-formed NTLM type-2 challenge messages. Test cases verify: valid challenges parse successfully, malformed inputs produce specific error messages (missing signature, missing terminator, wrong message type), and truncated inputs (12–47 bytes) return errors without panicking. go.mod downgrades github.com/projectdiscovery/gologger from v1.1.69 to v1.1.68.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

I nibble bytes with careful hops and flair,
Counting forty-eight before I dare,
No slice-bounds doom, no panic fright,
Tests keep parsing tidy, day and night. 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: preventing a slice-bounds panic in ParseNTLMResponse when NTLM challenges are truncated.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
pkg/utils/telnetmini/ntlm_test.go (2)

59-68: ⚡ Quick win

Consider testing the exact 48-byte boundary.

The current valid test uses a 54-byte challenge (48-byte header + 6-byte target name). Consider adding a test case for exactly 48 bytes (minimal valid challenge with no target name or info) to verify the boundary condition.

📝 Proposed additional test case
func TestParseNTLMResponse_Minimal48Bytes(t *testing.T) {
	// Build exactly 48 bytes: minimal valid type-2 challenge with no target name/info
	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")
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/utils/telnetmini/ntlm_test.go` around lines 59 - 68, Add a new unit test
to cover the 48-byte boundary: create TestParseNTLMResponse_Minimal48Bytes that
calls buildChallenge(48) (or construct a 48-byte challenge similarly to
buildValidChallenge), then invoke ParseNTLMResponse(data) and assert no error
and non-nil resp; place it alongside TestParseNTLMResponse_Valid to ensure the
parser accepts the minimal 48-byte type-2 challenge with no target name/info.

98-98: ⚡ Quick win

Simplify the inline anonymous function for readability.

The "wrong message type" test case constructs the input using an immediately-invoked function expression, which makes the table harder to read.

♻️ Proposed simplification

Option 1: Extract to a local variable before the test table:

func TestParseNTLMResponse_ErrorCases(t *testing.T) {
	wrongTypeChallenge := buildChallenge(48)
	binary.LittleEndian.PutUint32(wrongTypeChallenge[8:12], 1) // Set type to 1 instead of 2
	
	tests := []struct {
		name    string
		input   []byte
		wantErr string
	}{
		// ... other cases ...
		{
			name:    "wrong message type",
			input:   wrongTypeChallenge,
			wantErr: "expected NTLM challenge message",
		},
	}
	// ...
}

Option 2: Add a helper function:

// buildChallengeWithType constructs a challenge with the specified message type
func buildChallengeWithType(headerLen int, msgType uint32) []byte {
	data := buildChallenge(headerLen)
	if headerLen >= 12 {
		binary.LittleEndian.PutUint32(data[8:12], msgType)
	}
	return data
}

Then use: input: buildChallengeWithType(48, 1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/utils/telnetmini/ntlm_test.go` at line 98, Replace the inline
immediately-invoked function used for the "wrong message type" test input with a
clearer construction: either (A) create a local variable (e.g.,
wrongTypeChallenge) inside TestParseNTLMResponse_ErrorCases using
buildChallenge(48) and then call
binary.LittleEndian.PutUint32(wrongTypeChallenge[8:12], 1) before putting it in
the tests table, or (B) add a small helper like
buildChallengeWithType(headerLen, msgType) that calls buildChallenge/header
builder and sets the uint32 at [8:12]; then update the test case to use
wrongTypeChallenge or buildChallengeWithType(48, 1) instead of the anonymous
function. Ensure the surrounding test still expects "expected NTLM challenge
message" and that the data length remains 48.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/utils/telnetmini/ntlm_test.go`:
- Around line 59-68: Add a new unit test to cover the 48-byte boundary: create
TestParseNTLMResponse_Minimal48Bytes that calls buildChallenge(48) (or construct
a 48-byte challenge similarly to buildValidChallenge), then invoke
ParseNTLMResponse(data) and assert no error and non-nil resp; place it alongside
TestParseNTLMResponse_Valid to ensure the parser accepts the minimal 48-byte
type-2 challenge with no target name/info.
- Line 98: Replace the inline immediately-invoked function used for the "wrong
message type" test input with a clearer construction: either (A) create a local
variable (e.g., wrongTypeChallenge) inside TestParseNTLMResponse_ErrorCases
using buildChallenge(48) and then call
binary.LittleEndian.PutUint32(wrongTypeChallenge[8:12], 1) before putting it in
the tests table, or (B) add a small helper like
buildChallengeWithType(headerLen, msgType) that calls buildChallenge/header
builder and sets the uint32 at [8:12]; then update the test case to use
wrongTypeChallenge or buildChallengeWithType(48, 1) instead of the anonymous
function. Ensure the surrounding test still expects "expected NTLM challenge
message" and that the data length remains 48.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cb5877cd-124c-4cae-9d30-f77242e1b1c0

📥 Commits

Reviewing files that changed from the base of the PR and between 3684849 and 1aff867.

📒 Files selected for processing (2)
  • pkg/utils/telnetmini/ntlm.go
  • pkg/utils/telnetmini/ntlm_test.go

…st and clean up wrong-type input

- 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 <noreply@anthropic.com>
@Mzack9999

Copy link
Copy Markdown
Member

@coderabbitai create an issue for this PR

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@Mzack9999 The issue has been created and assigned to you: #7426

@tejgokani

Copy link
Copy Markdown
Contributor Author

@Mzack9999 @coderabbitai. why can't this PR be merged?

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@tejgokani The PR has already been approved by @Mzack9999, has no merge conflicts, and all static checks (Lint, Spell Check) passed. The only reason it can't be merged right now is that the CI test suite is still in progress on all three platforms:

Check Status
Tests (ubuntu-latest) 🔄 In Progress
Tests (windows-latest) 🔄 In Progress
Tests (macOS-latest) 🔄 In Progress

Once those complete successfully, the PR should be ready to merge. You (or a maintainer) can keep an eye on the workflow run for live status.

(ฅ^ω^ฅ) Hang tight — almost there!

@tejgokani

Copy link
Copy Markdown
Contributor Author

so once the tests will be done and green light. will it be merged by @Mzack9999 or @coderabbitai

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@tejgokani Once the tests go green, the merge will be done by a human maintainer — most likely @Mzack9999 or another maintainer with write access to the repo. I'm an AI code review tool and don't have the ability to merge pull requests. 😊

(=^・ω・^=)

@tejgokani

Copy link
Copy Markdown
Contributor Author

@Mzack9999 and @coderabbitai

The two CI failure categories are unrelated to this diff (which only touches pkg/utils/telnetmini/ntlm.go and ntlm_test.go):

Functional tests — every failure is the same release loaded N templates but current loaded N-1 pattern. This is a known drift between the published release binary and HEAD; it appears identically on #7414 and other recent PRs against main before this branch existed.

Integration tests (profile-loader) — all three cases (basic.yml, load-with-id, load-with-filename) call nuclei with -tp cloud / -tp cloud.yml and expect results from an external cloud profile endpoint. All returned 0 results across all 3 retries, which is the signature of a network/auth issue in the CI runner rather than a code regression. The integration suite passed cleanly on main before this branch.

All checks that exercise the changed code paths — unit tests, lint, vet, template-validate, CodeQL, and the release test — are green on all platforms.

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@tejgokani Your analysis checks out against the CI data. Here's a direct comparison with #7414 (the most recent merged PR):

Check #7414 (merged) #7425 (this PR)
Lint
Tests (all 3 platforms)
Integration tests (all 3 platforms) ⚠️ 2 failed, 1 in progress
Functional tests (all 3 platforms) ❌ FAILED ❌ FAILED
Template validate
CodeQL / CodeQL analysis
Release test

Functional tests: Confirmed pre-existing — the same failures appeared on #7414, which was merged anyway. Your template-count-drift explanation is consistent with that.

Integration tests: This is the one area that differs — #7414 passed them, but #7425 shows failures on macOS and Windows (ubuntu is still running). The profile-loader/-tp cloud failures you describe (network/auth to an external cloud endpoint) are a plausible explanation that a maintainer will need to judge.

@Mzack9999 — tagging you since you're the approving maintainer. The functional test failures are the same pre-existing drift seen in #7414. The integration test failures on macOS/Windows for profile-loader are worth a quick look; if they're confirmed as cloud-endpoint flakes rather than regressions, this PR should be good to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(telnetmini): prevent slice-bounds panic in ParseNTLMResponse on truncated NTLM challenge

2 participants