Kürzlich habe Ich einen Tweet vom User @xxByte gefunden, in dem er im folgenden Code-Beispiel nach dem "tödlichen Bug" gefragt hat.
Daran werde Ich versuchen euch zu verdeutlichen, warum so viele Programmierer PHP gegenüber abgeneigt sind.
Tipp: Ich werde den Code Schritt für Schritt durchgehen, ist noch nicht nötig den ganzen Block zu verstehen.
<?php
if(empty($_POST['hmac']) || empty($_POST['host'])) {
header('HTTP/1.0 400 Bad Request');
exit;
}
$secret = getenv("SECRET");
if(isset($_POST['nonce']))
$secret = hash_hmac('sha256', $_POST['nonce'], $secret);
$hmac = hash_hmac('sha256', $_POST['host'], $secret);
if($hmac !== $_POST['hmac']) {
header('HTTP/1.0 403 Forbidden');
exit;
}
echo exec("host ".$_POST['host']);
?>
Grob gesagt wird ein Service angeboten mit dem man sich die IP-Adressen von Domains anzeigen lassen kann. Beispielsweise wenn Ich eine Webseite öffnen will, muss mein Computer ja wissen wo (Adresse) sich die Seite befindet.
Dafür wird hier das kleine Programm host
benutzt.
Das kann wie folgt aussehen:
host google.de
google.de has address 172.217.21.227
google.de has IPv6 address 2a00:1450:4001:806::2003
Auf den ersten Blick sollte einem direkt Zeile 19
echo exec("host ".$_POST['host']);
ins Auge springen.
Wenn man dem Server Daten schicken will, kann man das über einen so genannten POST-Request machen. Beschreibung von Wikipedia:
schickt unbegrenzte, je nach physischer Ausstattung des eingesetzten Servers, Mengen an Daten zur weiteren Verarbeitung zum Server [...]
PHP gibt einem Zugriff auf diese Daten in der Variable $_POST
, das bedeutet wir als User können diese Variable (mehr oder weniger) kontrollieren.
exec()
macht laut PHP-Doku:
exec — Führt ein externes Programm aus
Es wäre im Interesse eines Angreifers z.B. ein Datenbank-Programm auszuführen um Daten zu klauen bzw. zu setzen oder zu löschen.
Der Knackpunkt befindet sich hier:
$hmac = hash_hmac('sha256', $_POST['host'], $secret);
if($hmac !== $_POST['hmac']) {
header('HTTP/1.0 403 Forbidden');
exit;
}
echo exec("host ".$_POST['host']);
Wir vergleichen die Serverseitige Variable $hmac
mit der vom User gesetzten Variable hmac
.
Falls die beiden gleich sind, gelangen wir zu exec()
, unserem Ziel.
Falls die beiden unterschiedlich sind beenden wir das Programm via exit
.
Schauen wir uns an wie die Variable $hmac
generiert wird.
$hmac = hash_hmac('sha256', $_POST['host'], $secret);
Wir weisen $hmac
das Ergebnis der Funktion hash_hmac()
zu. Ein Blick in die PHP-Doku sagt uns:
hash_hmac — Berechnet einen Hash mit Schlüssel unter Verwendung von HMAC
Ein wenig anders ausgedrückt heißt das, dass diese Funktion eine Nachricht und einen Schlüssel dazu benutzt eine einzigartige Zeichenfolge zu erstellen.
Für jemanden der sich damit nie beschäftigt hat, kann das ein wenig schwer zu verstehen sein, deshalb hab ich hier eine kleine vergleichbare Demo vorbereitet.
Du kannst zwei Dinge eingeben, eine Nachricht und einen Schlüssel. Das Ergebnis sollte automatisch aktualisieren.
Nachricht:
Schlüssel:
Zeichenfolge:
Eine bestimmte Eingabe=E_1, liefert immer eine bestimmte Ausgabe=A_1. Diese Ausgabe A_1, kann nur mit der bestimmten Eingabe E_1 generiert werden. Wenn ich eine andere Eingabe E_2 eingebe, kriege ich nicht mehr A_1 sondern eine andere Ausgabe A_2 - aka, 'einzigartige Zeichenfolge'.
Probier es aus, gib irgendetwas ein und sieh zu wie sich die Ausgabe bei jedem Zeichen wieder ändert.
Hier sind ein paar echte Beispielausgaben:
hash_hmac('md5', 'Nachricht', 'Schlüssel');
"eecaaeceed73c85d78c6092feb3fc80b"
hash_hmac('md5', 'kurz', 'abc123');
"bb025ddb83ba74b2340ecec3da11e0d7"
hash_hmac('md5', 'kurz', 'abc1234');
"9e28d712f8f5a71278e41ee904cf06ca"
hash_hmac('md5', 'super mega lange nachricht blabla', 'abc123');
"a1531f330796348872d66400272c9df4"
Siehst du was Ich mit 'Zeichenfolge' meine?
Hier gibts einige interessante Eigenschaften die dir wahrscheinlich aufgefallen sind.
Wir wissen nun wie die Zeichenfolge aussehen wird.
Die Nachricht kontrollieren wir durch Variable host
(Zeile 12) - schauen wir uns an woher der Schlüssel $secret
kommt.
$secret = getenv("SECRET");
if(isset($_POST['nonce']))
$secret = hash_hmac('sha256', $_POST['nonce'], $secret);
$hmac = hash_hmac('sha256', $_POST['host'], $secret);
Als erstes wird $secret
Serverseitig gesetzt, wir wissen nicht was sich drin befindet. Jetzt wirds interessant, falls der Benutzer die Variable nonce
(?) gesetzt hat überschreiben wir $secret
mit einer neuen Zeichenfolge.
Wenn wir rauskriegen könnten was in $secret
steht, würden wir beide Parameter für $hmac
kennen und das würde Code-Ausführung bewirken!
"Ähm, wir kennen
$secret
doch gar nicht, das ist Serverseitig! Daher können wir gar nicht wissen washash_hmac()
erzeugt."
- Du
Am Anfang wurde gesagt dass wir Daten, hier nonce
, via POST-Request an den Server schicken können. Wir haben zwei Möglichkeiten, wir können dem Server sagen dass nonce
entweder eine Zeichenfolge - oder - eine Liste ist.
Listen werden dazu genutzt um Daten zu bündeln.
Beispiel: Ich habe eine Seite wo Benutzer Fotos hochladen können. Wenn also jemand 22 Fotos hochladen möchte, bräuchte der Server 22 Variablen um die hochgeladenen Fotos verarbeiten zu können. Es bietet sich also viel mehr an dem Server direkt mitzuteilen, dass Ich ihm eine Liste von Fotos schicke, dann kann er mit nur einer Variable alle Fotos ansprechen.
Was macht also PHP wenn die Nachricht in der hash_hmac
-Funktion eine Liste ist?
hash_hmac('md5', array(), 'schlüssel');
"PHP Warning: hash_hmac() expects parameter 2 to be string,
array given in php shell code on line 1"
NULL
Um das mal verdeutlichen wie unfassbar das ist, stell dir vor du gehst in eine Bank. Du spazierst zum Safe und sagst dem Sicherheitspersonal was mit Gewehren davor steht:
"Hey, Ich werde das Geld aus dem Safe nehmen."
Das Sicherheitspersonal guckt dich an und das einzige was passiert ist dass die dir sagen:
"Das solltest du eigentlich nicht machen - ahja das Geld befindet sich auf der Rechten Seite."
Jetzt kommt auch noch das aller beste: Es steht in der offiziellen Dokumentation nichts zu diesem Verhalten. Man kriegt das nur über benutzer-erstellte Kommentare/Posts mit. Hier ein Auszug aus der Doku:
Rückgabewerte
Gibt den berechneten Hash als Hexadezimalzahl zurück, außer raw_output ist wahr, in diesem Fall wird die binäre Darstellung des Hashes zurückgegeben. Gibt FALSE zurück, wenn algo nicht bekannt oder eine nicht-kryptographische Hash-Funktion ist.
NULL wird überhaupt nicht erwähnt.
Angenommen wir greifen nun an. Es wäre gut zu wissen unter welchem Benutzer PHP auf dem Server läuft. Es gibt ein kleines Programm id
welches einem den aktuellen Benutzer und seine Gruppen anzeigt.
Unsere Variable host
muss also host=;id;
sein.
Wir generieren via PHP die korrekte hmac
hash_hmac('sha256', ';id;', NULL);
"206a5d01dee603ea7486045355935ff23d878fd0be5104fd4a465618bfa699bb"
Wir sagen dem Server via nonce[]=
dass die nonce
eine Liste sein soll.
curl -X POST -d "host=;id;&hmac=206a5d01dee603ea7486045355935ff23d878fd0be5104fd4a465618bfa699bb&nonce[]=" http://0.0.0.0:8080
uid=1000(phpuser) gid=100(users) groups=100(users),3(sys)
Auf dem Server läuft PHP über den Benutzer phpuser. Unser Angriff war erfolgreich und wir haben Code-Ausführung auf dem Server!
Jetzt ist es nur noch eine Frage der Zeit bis der Angreifer was Interessantes findet und Schaden anrichten kann.
Man muss wirklich dringend aufpassen, man kann sich nicht immer auf die offizielle Doku verlassen. Das hier ist bei weitem nicht die einzige verwundbare Funktion.
Fehler machen ist wirklich sehr leicht in PHP und das kann fatale Folgen haben.
Was denkst du?