CVE-2008-3905: Sequential Transaction IDs and DNS Spoofing in resolv.rb
An in-depth look at CVE-2008-3905, where predictable transaction IDs and source ports in Ruby’s resolv.rb allowed remote attackers to spoof DNS responses, potentially directing applications to malicious servers.
The Predictability Problem in DNS
If you were to mail a letter to a friend and include a sequential return number on the envelope — say, 001 — you might expect the reply to reference that same number. If someone intercepted your mail flow, though, they wouldn’t need to read your letter to send a fake reply; they could guess that your next letter would be numbered 002 and send a pre-emptive response matching that number.
This, broadly speaking, is the core of the problem identified in CVE-2008-3905. When our Ruby applications need to resolve domain names to IP addresses, they often rely on the standard library’s resolv.rb. Prior to the patches for this vulnerability, resolv.rb used sequential transaction IDs and constant source ports for its DNS requests. This predictability made it possible for remote attackers to spoof DNS responses, directing our applications to malicious servers.
Of course, this vulnerability is often mentioned alongside CVE-2008-1447 — the infamous Kaminsky bug. While both involve DNS spoofing, they are fundamentally different. The Kaminsky bug exploited the caching behavior of recursive resolvers. CVE-2008-3905, on the other hand, was a flaw in the stub resolver itself: the way Ruby generated the requests made the responses trivial to forge.
The Mechanics of a DNS Request
To understand why this predictability was dangerous, we must first understand how a DNS request works over the wire. DNS typically uses the User Datagram Protocol (UDP). Unlike TCP, UDP is connectionless; there is no handshake to establish a session, and there are no sequence numbers built into the protocol to guarantee that a response belongs to a specific request.
Instead, a DNS client relies on two pieces of information to verify that an incoming response matches its outgoing request:
- The source port of the request (which becomes the destination port of the response).
- A 16-bit Transaction ID (TXID) embedded in the DNS header.
For example, if we use Ruby’s Resolv class to look up a domain:
require 'resolv'
ip = Resolv.getaddress("api.example.com")
puts ip
Under the hood, resolv.rb constructs a DNS query packet, assigns it a TXID, and sends it from a local port to the DNS server (typically on port 53). When the server replies, it sends the packet back to that local port and includes the same TXID. If the port and TXID match what the client expects, the client accepts the response.
The Vulnerability: Predictability
Strictly speaking, if an attacker wants to spoof a DNS response, they must send a forged packet to the client before the legitimate DNS server can reply. To do this successfully, the attacker must guess both the local port the client is listening on and the TXID of the request.
If a client uses a random local port and a random 16-bit TXID, the attacker must guess one combination out of roughly 4 billion possibilities (65,535 ports * 65,535 TXIDs).
However, prior to the fix for CVE-2008-3905, resolv.rb exhibited two critical flaws:
- It used a constant source port for its requests.
- It generated the TXID sequentially (e.g., 1, 2, 3…).
Because the source port was static and the TXID was predictable, the entropy of the request was effectively reduced to zero. An attacker who could observe a single DNS request from the Ruby application could perfectly predict the port and TXID of the next request. Even without observing traffic, an attacker could flood the client with forged responses covering a small window of sequential TXIDs, practically guaranteeing a successful spoof.
The Implications of Spoofed DNS
If an attacker successfully spoofs a DNS response, the implications for our application are severe. The application will cache the malicious IP address and use it for subsequent connections.
If our application is fetching an API endpoint, it will now connect to the attacker’s server. If we are downloading a file or an update, we might download a malicious payload instead. While TLS/SSL provides a layer of defense by verifying the identity of the server via certificates, an attacker could still cause a denial of service or exploit applications that do not strictly validate certificates.
The Fix: Introducing Entropy
To mitigate this vulnerability, the Ruby core team patched resolv.rb in versions 1.8.6-p287, 1.8.7-p72, and 1.9. The fix involved introducing randomness into both the source port selection and the TXID generation.
By randomizing the source port (a technique known as Source Port Randomization, or SPR) and using a cryptographically secure random number generator for the TXID, the client drastically increases the difficulty of a spoofing attack. The attacker must now flood the client with millions of packets to have a reasonable chance of guessing the correct combination before the legitimate server responds.
This highlights a fundamental principle in network programming: security often relies on entropy. When we build or use protocols that lack built-in session verification, we must ensure that our request identifiers are sufficiently unpredictable.
Upgrading and Verification
For modern Ruby applications, this specific vulnerability is a relic of the past; if you are running a supported version of Ruby, you are protected. Nevertheless, it serves as a valuable case study in how implementation details — such as how we generate IDs — can undermine the security of an entire protocol.
If you are maintaining a legacy system, though, you should verify your Ruby version. Running ruby -v will confirm if you are running a vulnerable version (1.8.5 and earlier, 1.8.6 before p287, or 1.8.7 before p72).
$ ruby -v
ruby 1.8.7 (2008-08-11 patchlevel 72) [x86_64-linux]
If you find yourself responsible for such an outdated environment, the most straightforward path forward is to plan an upgrade to a modern, supported Ruby version. Until that is possible, you may consider relying on the operating system’s native resolver (via the socket library) rather than the pure-Ruby resolv.rb, assuming the OS resolver has been patched against similar predictability flaws.
Sponsored by Durable Programming
Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.
Hire Durable Programming