curl · Report #4103651
TFTP accepts a first response from an IP other than the requested server
How it was found
Summary:
When curl sends a TFTP request to the IP in the URL, it accepts the first valid response from any source IP and pins that sender as the transfer peer. A host that can get UDP packets to the client's ephemeral port and win this first-response race can receive bytes from a TFTP upload or provide the contents of a download
Affected version
Tested with curl 8.22.0 and master commit 098d3a0d4044d8a3f0a8617a5a5a30cde90fba26, on Linux in isolated network namespaces, with TFTP enabled. The release test recorded 5/5 synthetic uploads reaching the competing responder and 3/5 downloads returning its data, these are results from this isolated environment setup, not a general success rate.
Steps To Reproduce:
- Build curl 8.22.0 with TFTP support on Linux
- In an isolated Linux environment that supports unprivileged user and network namespaces, place
tftp-test.pyandtftp-test-spray.cfrom the attachments in the same directory. - Run
python3 tftp-test.py /path/to/curl-8.22.0/curl --seconds 2 --trials 5 --start-delays-ms 0,250,500,750,1000. - Review the harness output and
ftft-testing.log - For the control, repeat the run with
--drop-attacker, the competing responder should no longer win.
Root cause
The initial RRQ/WRQ is sent to the URL-selected address obtained from Curl_conn_get_remote_addr() (lib/tftp.c:754-761). On receive, recvfrom() supplies a sender address. The current logic checks it only after a peer has already been pinned; the first sender is copied directly into state->remote_addr. This simplified excerpt is from tftp_receive_packet() (lib/tftp.c:1057-1071):
if(state->remote_pinned) {
if((state->remote_addrlen != fromlen) ||
memcmp(&remote_addr, &state->remote_addr, fromlen))
return CURLE_RECV_ERROR;
}
else {
state->remote_pinned = TRUE;
state->remote_addrlen = fromlen;
memcpy(&state->remote_addr, &remote_addr, fromlen);
}
This pins the first packet's complete endpoint, including its source IP and UDP port, but does not compare that IP with the address used for the request. The later comparison only ensures subsequent packets match the first responder; it cannot reject a different host that wins the first-response race. TFTP servers use a new UDP transfer port, so curl should allow that port change while requiring the first response's address family and IP (and IPv6 scope, where applicable) to match the URL selected endpoint. It can then pin the accepted transfer port and validate later packets against that transfer endpoint.
Limitations
The attack requires unsolicited UDP packets to reach curl's ephemeral TFTP port and the competing response to arrive before the selected server's response. Stateful firewalls and NAT can prevent that reachability.
Impact
If an application relies on the TFTP URL's IP to select a trusted endpoint, an attacker able to reach the client's ephemeral UDP port and win the race can read the file being uploaded or make the application consume attacker-chosen download data as a successful transfer.