'Finde den tödlichen Bug' - oder: Warum niemand PHP mag

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.

phpmeme
"Du bist ohne Zweifel die schlechteste Programmiersprache von der Ich je gehört habe."
"ABER - du hast von mir gehört!"


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']);
?>

Was macht der Code?

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

Los geht's

Auf den ersten Blick sollte einem direkt Zeile 19 ins Auge springen.

echo exec("host ".$_POST['host']);

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.

Unser Ziel ist also schon mal klar, aber wie kommen wir da hin?

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 .

Um kurz allgemein zusammenzufassen:

  1. Wir schicken dem Server Daten
  2. Der Server vergleicht Daten
  3. Falls Sicherheitschecks fehlschlagen, beenden wir
  4. Falls diese erfolgreich sind, führen wir Code aus

OK, soweit so gut. -

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.

  • Jede Ausgabe hat die gleiche Größe, egal wie lang die Eingaben sind. (hier 32 Zeichen)
  • Beim kleinsten Unterschied, ob bei Nachricht oder Schlüssel, sieht die Ausgabe drastisch anders aus.
  • Es gibt folgende Zeichen in den Folgen: abcdef0123456789

Wir wissen nun wie die Zeichenfolge aussehen wird.

Was brauchen wir zur Erstellung?

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.

Das bedeutet:

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 was hash_hmac() erzeugt."
- Du

Ja, das wäre eigentlich so korrekt, aber jetzt kommt unser geliebtes PHP ins Spiel.

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
Schockiertes Gesicht

Nur eine Warnung. Unglaublich.

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.

php witz
"Willkommen bei PHP, wo die Syntax frei erfunden ist und die Regeln keine Rolle spielen!"

Der Angriff

1. Parameter:

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.

2. Parameter:

Wir generieren via PHP die korrekte hmac

hash_hmac('sha256', ';id;', NULL);
"206a5d01dee603ea7486045355935ff23d878fd0be5104fd4a465618bfa699bb"

3. Parameter:

Wir sagen dem Server via nonce[]= dass die nonce eine Liste sein soll.

Voilà:

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.

Fazit

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 wir heute gelernt haben 

  • Immer davon ausgehen, dass eingehende Daten von Benutzern böse sind, deswegen Inhalt UND den Typ der eingehenden Daten überprüfen.
  • Andere Quellen neben der offiziellen Doku aufsuchen.

What did you like about this post? Got questions? Get in touch!

( no registration required! )

Noch keine Kommentare vorhanden

Was denkst du?

Side note: Comments will appear after being spam filtered, this might take a moment.

Contact me

LinkedIn
© Daniel Biegler
Design & implementation by
Daniel Biegler