Hello U-Boot maintainers,
I found a TFTP client-side source verification issue in U-Boot's pre-boot
network stack.
No CVE is assigned at report time.
Summary
-------
In client mode, U-Boot records the expected TFTP server IP in `tftp_remote_ip`,
but the IPv4 TFTP receive handler accepts the first OACK or DATA packet without
checking that the packet source IP (`sip`) matches that configured server IP.
A network-adjacent attacker who can observe or race traffic on the boot LAN can
send an OACK or first DATA packet to the client's local TFTP port before the
legitimate server response. If accepted, U-Boot binds `tftp_remote_port` from
the attacker packet and can store attacker-controlled DATA into the boot image
buffer.
Affected version
----------------
Observed in current master:
38dbe637c9dfcadbd1bc201bfbb27f96b2ad525a
Affected file:
net/tftp.c
Details
-------
At transfer setup, U-Boot stores the expected TFTP server IP:
net/tftp.c:817-820
tftp_remote_ip6 = net_server_ip6;
tftp_remote_ip = net_server_ip;
The IPv4 receive handler checks the destination port and some source-port/state
conditions:
net/tftp.c:458-470
static void tftp_handler(uchar *pkt, unsigned dest, struct in_addr sip,
unsigned src, unsigned len)
...
if (dest != tftp_our_port)
return;
if (tftp_state != STATE_SEND_RRQ && src != tftp_remote_port &&
tftp_state != STATE_RECV_WRQ && tftp_state != STATE_SEND_WRQ)
return;
There is no equivalent check that `sip` matches `tftp_remote_ip` for
client-side traffic from the expected server.
For OACK, the handler accepts the packet and binds the remote transfer port
from the packet:
net/tftp.c:530-540
case TFTP_OACK:
...
tftp_state = STATE_OACK;
tftp_remote_port = src;
For first DATA in the initial client state, the handler also binds
`tftp_remote_port` from the packet and proceeds to store the received block:
net/tftp.c:636-640
if (tftp_state == STATE_SEND_RRQ || tftp_state == STATE_OACK ||
tftp_state == STATE_RECV_WRQ) {
tftp_state = STATE_DATA;
tftp_remote_port = src;
net/tftp.c:663
if (store_block(tftp_cur_block, pkt + 2, len)) {
Attack scenario
---------------
1. A device starts a TFTP boot from configured server `192.168.1.1`.
2. The attacker observes the RRQ and learns the client's local UDP port.
3. The attacker sends an OACK or DATA block from `192.168.1.102` to that client
port before the legitimate server response.
4. The visible handler checks accept the packet because the destination port
and state match.
5. U-Boot binds the transfer port from the attacker packet and can write
attacker-controlled DATA to the boot buffer.
Minimal host-side PoC
---------------------
The attached/referenced harness includes this reduced check:
configured_server = 0xc0a80101 // 192.168.1.1
packet_source = 0xc0a80166 // 192.168.1.102
state = STATE_SEND_RRQ
dest = tftp_our_port
It shows that the visible accept conditions are true even when the packet
source IP does not match the configured server:
BA2-T6-CAND-002: configured=0xc0a80101 packet=0xc0a80166 accepted=1
PoC source to attach:
poc-tftp-source-ip.c
Build and run:
gcc -std=c11 -Wall -Wextra -O0 poc-tftp-source-ip.c -o poc-tftp-source-ip
./poc-tftp-source-ip
Observed output:
configured_server=0xc0a80101
packet_source=0xc0a80166
accepted_by_visible_gate=1
VULNERABLE: first TFTP response from unexpected source IP passes the
visible receive gate
Impact
------
This affects systems using U-Boot TFTP boot on a network where an attacker can
race or spoof UDP packets on the boot LAN. The impact is boot-data injection or
denial of service in the pre-OS boot path. Existing destination-port and later
transfer-port checks reduce the race window, but they do not verify the server
IP before accepting the first OACK/DATA packet.
Expected invariant
------------------
For TFTP client transfers, before accepting OACK/DATA and before binding the
transfer port, U-Boot should reject packets whose source IP does not match the
configured TFTP server IP. The TFTP server mode initial WRQ path can remain
separate, since it intentionally accepts an incoming client and then records
its address.
I can prepare a separate patch or sandbox test if that would be useful, but I
am sending this first as a vulnerability report so the security impact can be
triaged independently of the fix shape.
Regards,
Zhenyu Liu
#include <stdint.h>
#include <stdio.h>
#define STATE_SEND_RRQ 1u
int main(void)
{
const uint32_t configured_server = 0xc0a80101u; /* 192.168.1.1 */
const uint32_t attacker_source = 0xc0a80166u; /* 192.168.1.102 */
const unsigned tftp_our_port = 1234u;
const unsigned packet_dest = 1234u;
const unsigned tftp_state = STATE_SEND_RRQ;
/*
* This models the visible accept gate in net/tftp.c:
*
* if (dest != tftp_our_port)
* return;
* if (tftp_state != STATE_SEND_RRQ && src != tftp_remote_port &&
* tftp_state != STATE_RECV_WRQ && tftp_state != STATE_SEND_WRQ)
* return;
*
* There is no sip != tftp_remote_ip check in this gate.
*/
const int accepted_by_visible_gate =
(packet_dest == tftp_our_port) && (tftp_state == STATE_SEND_RRQ);
printf("configured_server=0x%08x\n", configured_server);
printf("packet_source=0x%08x\n", attacker_source);
printf("accepted_by_visible_gate=%d\n", accepted_by_visible_gate);
if (attacker_source != configured_server && accepted_by_visible_gate) {
puts("VULNERABLE: first TFTP response from unexpected source IP passes
the visible receive gate");
return 0;
}
puts("not reproduced");
return 1;
}
ÿþc o n f i g u r e d _ s e r v e r = 0 x c 0 a 8 0 1 0 1
p a c k e t _ s o u r c e = 0 x c 0 a 8 0 1 6 6
a c c e p t e d _ b y _ v i s i b l e _ g a t e = 1
V U L N E R A B L E : f i r s t T F T P r e s p o n s e f r o m
u n e x p e c t e d s o u r c e I P p a s s e s t h e v i s i b l e
r e c e i v e g a t e