Skip to content

Fix DNS forward/cache path for relay-driven poisoning - #21853

Open
Pushpenderrathore wants to merge 4 commits into
rapid7:masterfrom
Pushpenderrathore:fix/dns-forward-cache-relay-poisoning
Open

Fix DNS forward/cache path for relay-driven poisoning#21853
Pushpenderrathore wants to merge 4 commits into
rapid7:masterfrom
Pushpenderrathore:fix/dns-forward-cache-relay-poisoning

Conversation

@Pushpenderrathore

@Pushpenderrathore Pushpenderrathore commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Description

Three fixes to the core Rex::Proto::DNS forward/cache path that surface once the DNS server is used as a selective poisoner in front of a real upstream resolver (the Kerberos relay coercion workflow in #21693). Identified during @jheysel-r7's ESC8 relay testing.

Each is small and independent:

  1. Cache#cache_record crashed on non-cacheable forwarded records. Real upstream responses carry records whose names do not match MATCH_HOSTNAME; caching them raised and killed the dispatch thread. Skip the record instead of raising.

  2. Shallow dup shared the question array. Dnsruby::Message#dup is shallow, so req.question and forward.question referenced the same Array. Deleting a cache-served question from the forwarded packet also emptied the original request's question list, so the echoed response lost its questions. forward now gets its own copy.

  3. Mutating a decoded request's @answer produced a malformed packet. Building the reply by instance_variable_set(:@answer, ...) on the decoded request and re-encoding appended the answer bytes after the packet end, so clients decoded zero answers (surfaces as "Bad DNS packet" on Windows). The response is now built as a fresh Dnsruby::Message.

Also handles udp_sock.recvfrom returning an explicit source port so the reply is addressed to the right host/port across socket variants.

Verification

Both non-obvious bugs (2 and 3) were confirmed empirically against the pinned dnsruby before and after the fix, and are covered by new regression specs.

$ bundle exec rspec spec/lib/rex/proto/dns/server_spec.rb spec/lib/rex/proto/dns/cache_spec.rb
18 examples, 0 failures

The existing coverage from #21784 (empty-forward response) still passes.

Related

Three issues surfaced while running the DNS server under the Kerberos relay
coercion workflow (identified during jheysel's ESC8 testing):

- Rex::Proto::DNS::Cache#cache_record raised on any forwarded record whose
  name did not match MATCH_HOSTNAME, killing the dispatch thread. Skip the
  non-cacheable record instead.

- Server#default_dispatch_request duplicated the request with Dnsruby::Message#dup,
  which is shallow, so req.question and forward.question shared one Array.
  Deleting a cache-served question from the forwarded packet also emptied the
  original request's question list. Give forward its own copy.

- The response was built by mutating the decoded request's @answer via
  instance_variable_set and re-encoding it, which appended the answer bytes
  past the packet end so clients decoded zero answers ("bad DNS packet" on
  Windows). Build a fresh Dnsruby::Message for the response instead.

Also handle the udp recvfrom returning an explicit source port so the reply
goes back to the right host/port across socket variants.

Adds regression specs for the shallow-dup question handling and the
answer-encoding fix.
@Pushpenderrathore
Pushpenderrathore force-pushed the fix/dns-forward-cache-relay-poisoning branch from 40fbf90 to b0d920f Compare August 31, 2026 19:33
@jheysel-r7 jheysel-r7 self-assigned this Aug 31, 2026
@jheysel-r7
jheysel-r7 requested a balanced review from Copilot August 31, 2026 20:36
@jheysel-r7 jheysel-r7 added rn-fix release notes fix library labels Aug 31, 2026

Copilot AI 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.

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes DNS forwarding, caching, response encoding, and UDP reply addressing for relay-driven poisoning.

Changes:

  • Skips invalid-hostname cache records.
  • Safely separates forwarded questions and rebuilds DNS responses.
  • Supports alternate UDP recvfrom return shapes.

Impact Analysis:

  • Blast radius: Medium; affects default DNS forwarding and UDP listeners.
  • Data and contract effects: Changes DNS response construction and cache behavior; RD is not currently preserved.
  • Rollback and test focus: Reversible; validate DNS flags, cache rejection, and both UDP address formats.
File summaries
File Description
lib/rex/proto/dns/cache.rb Skips non-cacheable hostnames.
lib/rex/proto/dns/server.rb Fixes forwarding, response encoding, and UDP source handling.
spec/lib/rex/proto/dns/server_spec.rb Adds forwarding and encoding regressions.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 59 to +60
unless record.name.to_s.match(MATCH_HOSTNAME)
raise "Invalid record for cache entry (invalid hostname) - #{record.inspect}"
return # skip non-cacheable record: " - #{record.inspect}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added coverage in the new spec/lib/rex/proto/dns/cache_spec.rb: with the monitor active it feeds cache_record a record whose name fails MATCH_HOSTNAME (an underscore SRV-style label) and asserts it neither raises nor gets added, alongside a control that a valid-hostname record is cached. Resolved in 6ad348f.

Comment on lines +226 to +230
buf, addr, source_port = self.udp_sock.recvfrom(65535)
if source_port
host, port = addr, source_port
else
host, port = addr[3], addr[1]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added two #monitor_listener specs covering both recvfrom return shapes: the three-value (data, host, port) form used by Rex sockets and the two-value (data, sockaddr array) form from a raw UDPSocket, each asserting the mock client is addressed to the extracted host and port. Resolved in 52e6d90.

Comment thread lib/rex/proto/dns/server.rb Outdated
Comment on lines +188 to +191
resp = Dnsruby::Message.new
resp.header.id = req.header.id
resp.header.qr = true
resp.header.ra = req.header.rd

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed. The fresh response now echoes RD via resp.header.rd = req.header.rd. One note: header.recursive= is not defined in the pinned dnsruby 1.73.1 (Header exposes rd/ra accessors only), so I used header.rd=. I also derive RA from whether a forwarder is configured (ra = !fwd_res.nil?) rather than from the client RD, since RA advertises server capability. Specs assert RD is echoed and RA reflects the forwarder. Resolved in 76fd57a (fix) plus 52e6d90 (specs).

The fresh Dnsruby::Message dropped the request RD bit and set RA from the
client RD. Echo RD back and derive RA from whether a forwarder is
configured, so recursive queries get a spec-correct header.
@Pushpenderrathore

Copy link
Copy Markdown
Contributor Author

Follow-up verification.

Specs are now at 18 examples, 0 failures (spec/lib/rex/proto/dns/server_spec.rb plus the new spec/lib/rex/proto/dns/cache_spec.rb).

I also lab-tested the fix end to end by running Rex::Proto::DNS::Server over real UDP in front of a public resolver and driving it with live queries:

  • poisoned name served from cache, forwarded example.com decodes with its A records, question echoed, RD echoed and RA set from the forwarder
  • a _dmarc.google.com TXT query (the non-hostname record path) returns a reply and the dispatch thread stays alive

Before/after on the crash itself: with the old raise, that same TXT query terminated the listener thread (Invalid record for cache entry (invalid hostname) on the real DMARC record at server.rb:183) and the client timed out. With the skip it returns normally. That is the thread-death this PR fixes.

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

Labels

library rn-fix release notes fix

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants