A remote stack smash in a pocket router's file upload daemon
CVE-2026-120189 is a pre-auth stack-based buffer overflow in the TFTP upload filename handler (save_upload_file/mp_upload_file) of the /usr/bin/tftpd binary on D-Link DIR-505 firmware 1.10B02. A remote attacker sends a TFTP WRQ with an oversized filename, smashes the stack, and takes control of the daemon.
D-Link labelled it CRITICAL/10, and mechanically the vector is unauthenticated RCE with public exploit code. But the DIR-505 is a low-cost travel router that has been out of production for years; TFTP is not enabled by default, and the device rarely has a public internet footprint. For an enterprise patching team, real-world exposure is far lower than the CVSS score suggests, so this lands at HIGH.
4 steps from start to impact.
Find a DIR-505 with TFTP listening
- TFTP service enabled
- UDP/69 reachable from attacker
- DIR-505 is discontinued and uncommon in enterprise inventories
- TFTP is not enabled by default
- Internet exposure of a travel router is rare
Send an oversized TFTP WRQ filename
save_upload_file/mp_upload_file copies the filename without bounds checking, overwriting the return address on the stack. The overflow is reachable without authentication because TFTP has no auth handshake.- Firmware 1.10B02 or earlier
- Attacker can send UDP packets to tftpd
- Exact offset may require tuning for this firmware; public exploit may need adaptation
- TFTP packet size limits can constrain payload size
Hijack control flow and execute code as root
- Reliable payload for MIPS/ARM target
- No stack canary/ASLR on the device
- Payload staging may be needed due to small packet sizes
- Post-exploitation persistence requires additional steps in limited flash
Pivot to connected clients or upstream network
- Active clients connected to the router
- Uplink to a more valuable network
- Travel routers are transient by design; dwell time is limited
- Enterprise NAC may flag the rogue device or its traffic
- Vendor patching: DIR-505 is discontinued; no firmware fix will be issued.
- Web-application or TLS inspection: this path uses raw UDP/69 TFTP, so HTTP/TLS controls cannot see it.
- Endpoint EDR: the vulnerable component is an embedded router; enterprise EDR agents are not present.
The supporting signals.
| In-the-wild status | Not in CISA KEV; no named campaigns; vendor states exploit is publicly available |
|---|---|
| Proof-of-concept availability | Public exploit referenced in the CVE record; no specific GitHub repo confirmed in supplied data |
| EPSS score | Unknown from supplied data; public exploit likely raises probability but EPSS not available |
| KEV status | Not listed in CISA KEV as of 2026-11-01 |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — 10.0; assumes internet-exposed TFTP and full compromise |
| Affected versions | D-Link DIR-505 firmware 1.10B02 and earlier |
| Fixed versions | None; DIR-505 is end-of-life and no patch is expected |
| Scanning/exposure | No GreyNoise/Shodan data for this CVE; general TFTP UDP/69 internet exposure is low for SOHO routers |
| Disclosure date | 2026-11-01 |
| Reporting researcher/org | Not specified in CVE record |
Sources.
Why this verdict
- Baseline: vendor CRITICAL/10 is technically accurate for unauthenticated pre-auth RCE with public exploit.
- Friction audit: TFTP is off by default, internet exposure is rare, and the DIR-505 is a discontinued SOHO travel router; each step narrows the reachable population.
- Role multiplier: the DIR-505 is not a canonical enterprise high-value component. Worst plausible outcome is compromise of a small router and its directly connected clients—not domain/fleet/supply-chain/OT impact. This caps the practical severity below CRITICAL.
- Public exploit: keeps it above MEDIUM because any exposed device can be taken over with low effort.
Why not higher?
Not CRITICAL because the device is a discontinued consumer travel router with a very small enterprise install base, TFTP is disabled by default, and internet-facing exposure is uncommon for this model. The vendor's 10.0 assumes worst-case reachability that most defenders won't encounter.
Why not lower?
Not MEDIUM because the bug is unauthenticated, root-level, and has publicly available exploit code. Where a DIR-505 is actually present and TFTP is on, the impact is immediate full device compromise and a pivot to connected clients, so it deserves a HIGH response.
Crowdsourced verification payload.
Run from an auditor workstation with Python 3 and network access to the target segment. Invoke python3 check_dir505_tftp.py 192.168.1.0/24 to scan a CIDR, or python3 check_dir505_tftp.py 192.168.1.1 for a single host. No special privileges required, but ICMP filtering may cause UNKNOWN results for filtered ports.
#!/usr/bin/env python3
"""Check for exposed TFTP (UDP/69) on D-Link DIR-505 or any TFTP server."""
import argparse
import ipaddress
import socket
import sys
def check_host(host, port=69, timeout=3):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout)
pkt = b'\x00\x01' + b'noisgate-probe' + b'\x00' + b'octet' + b'\x00'
sock.connect((host, port))
sock.send(pkt)
try:
data, _ = sock.recvfrom(1024)
# TFTP error opcode 5 = any response, listener is present
if len(data) >= 2 and data[1] == 5:
return 'VULNERABLE'
return 'VULNERABLE'
except socket.timeout:
return 'UNKNOWN'
except ConnectionRefusedError:
return 'PATCHED'
finally:
sock.close()
except OSError as e:
if e.errno == 111:
return 'PATCHED'
return 'UNKNOWN'
def main():
ap = argparse.ArgumentParser()
ap.add_argument('target', help='IP or CIDR to check')
ap.add_argument('--port', type=int, default=69)
ap.add_argument('--timeout', type=float, default=3.0)
args = ap.parse_args()
targets = []
try:
net = ipaddress.ip_network(args.target, strict=False)
targets = [str(ip) for ip in net.hosts()] if net.num_addresses > 2 else [args.target]
except ValueError:
targets = [args.target]
for host in targets:
status = check_host(host, args.port, args.timeout)
print(f'{host}: {status}')
if status == 'VULNERABLE':
sys.exit_code = 1
sys.exit(0)
if __name__ == '__main__':
main()