Vulnerability Spotlight: CVE-2023-32692

Analyse einer Remote Code Execution-Schwachstelle im PHP-Framework CodeIgniter

Dies ist eine übersetzte Version. Das englische Original finden Sie hier.

Im Rahmen eines kürzlichen Penetrationstests haben wir eine ältere Webanwendung untersucht, die mit dem PHP-Framework CodeIgniter entwickelt wurde. Um die Angriffsfläche des Frameworks besser zu verstehen, habe ich zunächst die veröffentlichten Sicherheitshinweise durchgesehen. Ein Eintrag fiel sofort auf: CVE-2023-32692, eine Remote Code Execution-Schwachstelle, die laut Advisory auf einem logischen Fehler im Validierungsfeature des Frameworks basiert. Dieser Post beschreibt die Schwachstellenanalyse und die generelle Ausnutzung.

Beschreibung der Schwachstelle

Das Problem betrifft CodeIgniter-Versionen vor 4.3.5. Das folgende Zitat stammt aus dem offiziellen CodeIgniter-Sicherheitshinweis:

This vulnerability allows attackers to execute arbitrary code when you use Validation Placeholders.

The vulnerability exists in the Validation library, and validation methods in the controller and in-model validation are also vulnerable because they use the Validation library internally.

Der Advisory verweist auf die CodeIgniter-Dokumentation zu Validation Placeholders, die selbst einen aufschlussreichen Hinweis enthält, der in der korrigierten Version hinzugefügt wurde:

Since v4.3.5, you must set the validation rules for the placeholder field (the id field in the sample code above) for security reasons. Because attackers can send any data to your application.

Aufsetzen einer Testumgebung

Bei der Schwachstellenanalyse bevorzuge ich grundsätzlich eine Umgebung, die den Einsatz eines Debuggers erlaubt. Für PHP-basierte Anwendungen ist meine präferrierte Lösung Visual Studio Code mit einem PHP-basierten Dev Container (ein unterschätztes Feature, das die lokale VS Code-Instanz sauber hält, indem alle Extensions und Tools in einem containerisierten Linux-System ausgeführt werden). Das vollständige Setup ist auf unserem [GitHub-Account zu finden])https://github.com/mogwailabs/CVE-2023-32692-CodeIgniter4); es werden lediglich VS Code und Docker benötigt.

Das Dockerfile basiert auf dem offiziellen PHP-Docker-Image und verwendet PHP 8.3, da die verwundbare CodeIgniter-Version mit neueren PHP-Releases nicht kompatibel ist. Zusätzlich werden XDebug zum Durchsteppen des Codes sowie Composer für das Dependency-Management installiert.

Der Dev Container enthält die folgenden VS Code-Extensions:

ExtensionZweck
xdebug.php-debugPHP XDebug-Unterstützung zum Debuggen der Anwendung
bmewburn.vscode-intelephense-clientPHP-Support für VS Code
humao.rest-clientSenden von HTTP-Anfragen direkt aus dem Editor

Der vollständige Ablauf zum Starten der Beispielanwendung in einer Debugging-Session sowie die Konfiguration von Breakpoints sind im GitHub-Repository dokumentiert.

Schwachstellenanalyse

Um die Schwachstelle zu verstehen, betrachten wir zunächst, wie CodeIgniter Validierungsregeln als Zeichenketten definiert. Aus der offiziellen Dokumentation:

$validation->setRules([
    'email' => 'required|max_length[254]|is_unique[users.email,id,{id}]',
]);

CodeIgniter verwendet das Pipe-Zeichen (|) zur Trennung der Validierungsregeln für ein Feld; Parameter werden in eckigen Klammern übergeben. Der Platzhalter {id} wird durch den Wert des id-Feldes aus den übermittelten Formulardaten ersetzt, bevor die Validierung ausgeführt wird.

Genau in dieser Ersetzung liegt die Schwachstelle. Innerhalb von CodeIgniters Validation-Klasse ruft die run-Methode folgenden Code auf, bevor ein Validator ausgeführt wird:

// Replace any placeholders (e.g. {id}) in the rules with
// the value found in $data, if any.
$this->rules = $this->fillPlaceholders($this->rules, $data);

Der Docblock der Methode fillPlaceholders beschreibt das beabsichtigte Verhalten ausführlich:

639/**
640 * Replace any placeholders within the rules with the values that
641 * match the 'key' of any properties being set. For example, if
642 * we had the following $data array:
643 *
644 * [ 'id' => 13 ]
645 *
646 * and the following rule:
647 *
648 *  'required|is_unique[users,email,id,{id}]'
649 *
650 * The value of {id} would be replaced with the actual id in the form data:
651 *
652 *  'required|is_unique[users,email,id,13]'
653 */

Das Problem ist offensichtlich: Benutzereingaben werden ohne jegliche Validierung direkt in die Regelzeichenkette interpoliert. Eine schließende eckige Klammer beendet die Parameterliste der aktuellen Regel, und ein Pipe-Zeichen leitet eine neue Regel ein. Dies ist strukturell identisch mit klassischen Injection-Angriffen — das Framework vertraut darauf, dass Benutzereingaben ein fester Bestandteil einer strukturierten Zeichenkette sind, und dieses Vertrauen wird ausgenutzt, um den beabsichtigten Kontrollfluss zu verändern.

Wenn wir die folgenden Parameter in einer HTTP-Anfrage senden:

id=1]|injected_rule[parameters]|second_rule[&email=foo@acme.com

ergibt sich nach dem Aufruf von fillPlaceholders folgende Validierungsregel-Zeichenkette:

required|valid_email|is_unique[users.email,id,1]|injected_rule[parameters]|second_rule[]

Das abschließende second_rule[ wird benötigt, um die verwaiste ] aus dem ursprünglichen Regeltemplate aufzunehmen.

injizierte Validierungsregel

Aufrufbare Validierungsregeln

CodeIgniter stellt eine Vielzahl von allgemeinen Regeln bereit, von denen jedoch keine aus Angreiferperspektive unmittelbar nützlich ist. Allerdings unterstützt CodeIgniter auch Callable Rules — beliebige PHP-Funktionen, die als Validatoren verwendet werden. Der relevante Ausschnitt aus der Methode processRules in CodeIgniters Validation-Klasse:

281foreach ($rules as $i => $rule) {
282    $isCallable = is_callable($rule);
283
284    $passed = false;
285    $param  = false;
286
287    if (! $isCallable && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) {
288            $rule  = $match[1];
289            $param = $match[2];
290    }
291
292    // Placeholder for custom errors from the rules.
293    $error = null;
294
295    // If it's a callable, call and get out of here.
296    if ($this->isClosure($rule)) {
297        $passed = $rule($value, $data, $error, $field);
298    } elseif ($isCallable) {
299        $passed = $param === false ? $rule($value) : $rule($value, $param, $data);
300    } else {
301    $found = false;

Da PHPs is_callable für jede aufrufbare Funktion true zurückgibt, können wir jede PHP-Funktion injizieren, die mindestens einen Parameter akzeptiert — einschließlich gefährlicher Funktionen wie system oder passthru.

Durch das Injizieren eines Aufrufs von system und die Verwendung des email-Feldwerts als Betriebssystembefehl erhalten wir Remote Code Execution:

id=1]|system|[&email=touch+/tmp/pwn

Umgehung vorheriger Validierungsregeln

Eine erschwerende Bedingung ist, dass der injizierte Callable nur dann ausgeführt wird, wenn alle vorherigen Checks in der Kette erfolgreich durchlaufen wurden. Dies kann die Länge des Payloads einschränken oder Formatanforderungen erzwingen.

Schauen wir die folgende Regelkette, in der der Entwickler eine E-Mail-Formatvalidierung hinzugefügt hat:

'email' => 'required|valid_email|is_unique[users.email,id,{id}]',

In diesem Fall muss der an system() übergebene Wert zunächst valid_email bestehen. Ein Blick auf die Implementierung zeigt, dass es sich im Wesentlichen um einen Wrapper um PHPs filter_var handelt:

public function valid_email(?string $str = null): bool
{
    // @see https://regex101.com/r/wlJG1t/1/
    if (function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46') && preg_match('#\A([^@]+)@(.+)\z#', $str ?? '', $matches)) {
        $str = $matches[1] . '@' . idn_to_ascii($matches[2], 0, INTL_IDNA_VARIANT_UTS46);
    }
    return (bool) filter_var($str, FILTER_VALIDATE_EMAIL);
}

Zwei Techniken lassen sich hier kombinieren, um diesen Filter zu umgehen. Erstens expandiert die Bash-Variable ${IFS} — der Internal Field Separator — zu Leerzeichen und erlaubt es, Leerzeichen in Betriebssystembefehlen zu ersetzen: aus touch /tmp/pwned wird touch${IFS}/tmp/pwned. Zweitens empfängt system() die gesamte E-Mail-Zeichenkette inklusive des @domain-Teils; wir verketten daher mit && einen harmlosen Folgebefehl, der das Suffix absorbiert. Das Ergebnis:

touch${IFS}/tmp/pwned&&ls@foobar.com

Diese Zeichenkette besteht die E-Mail-Prüfung von filter_var und erzeugt, wenn sie an system() übergeben wird, die Datei /tmp/pwned auf dem System.

Behebung

Die Lösung ist einfach: Upgrade auf CodeIgniter 4.3.5 oder höher. Wie im Advisory beschrieben, müssen außerdem explizite Validierungsregeln für jedes Feld definiert werden, das als Platzhalter verwendet wird.

Hätte KI das gefunden?

Ja. Dieser Fall ähnelt einer CTF-Aufgabe mittlerer Schwierigkeit, daher sollte die Identifikation der Schwachstelle anhand des Advisorys für ein LLM mit guter Schlussfolgerungsfähigkeit keine große Herausforderung darstellen. Ich habe verschiedene Modelle mit einfachen eigenen Test-Harnesses erprobt; alle identifizierten den Fehler auf Basis der veröffentlichten Sicherheitsmeldung innerhalb weniger Minuten.

Conclusion

CVE-2023-32692 ist ein gutes Beispiel für alle, die sich mit nicht ganz gängigen Schwachstellen in PHP-basierten Webanwendungen beschäftigen möchten. Das betroffene Feature — Validation Placeholders — wird in freier Wildbahn wahrscheinlich nicht allzu häufig eingesetzt, auch wenn ich bislang keine systematische Untersuchung von Open-Source-Projekten auf Basis von CodeIgniter durchgeführt habe, um diese Annahme zu bestätigen


Vielen Dank an Hannes Köttner auf Unsplash für das Titelbild.