Aaron's Blog



This post is about a little-known type of vulnerability in which the attacker manipulates the parameters bound to prepared statements with malicious goals such as bypassing access control.

Introduction

While doing a penetration test on a web application this week, I stumbled upon a niche vulnerability involving prepared statements that very few people, if any, are aware of. In fact, I couldn't find any documentation on it online.

Much like SQL injection, it is based on improper implementation of existing libraries.

One of the main purposes of prepared statements is to prevent SQL injection by standardizing how input parameters are passed. So, when developers are working with them, they assume that whatever they write is secure so long as any user input is bound to the query object instead of injected in the query.

Scenario

Take, for example, the following Python-like pseudocode for the backend of a webpage:

db = connect('127.0.0.1', 'db', 'user', 'pass')

...

# get 'username' from POST body, store it in parameters for now
parameters = post('username')

# turn it into a list
if not isinstance(parameters, list):
    parameters = [parameters]

# add second parameter to bind
parameters.append('admin')

# execute query, make sure the user is an administrator before accessing the page
query = db.query('SELECT * FROM users WHERE username = ? AND auth_level = ? LIMIT 1')
query.bind(parameters)

result = query.execute()

# check if user is authorized to access this area
if result.count() != 1:
    # user is not authorized, exit now!
    ...
else:
    # hooray, user is allowed!
    ...

This code models actual code that I saw in the application that I am currently testing.

Assume that this is the data that the table users stores, where the two possible values for auth_level are guest and admin:

+----------+------------+
| username | auth_level |
+----------+------------+
| timothy  | guest      |
+----------+------------+

The expected way that the user timothy (who is a guest) would access this page is:

POST /admin
...

username=timothy

Of course, he would be rejected because he is not admin. In fact, nobody in the users table is an admin, so no matter what username someone uses, they cannot access this page.

Stop right here and read the sample code from before. What do you see that's wrong with the implementation?

Hint: Is the POST request body parameter username being type-checked?

The Problem

First, again, here's the query that it executes:

SELECT * FROM users WHERE username = ? AND auth_level = ? LIMIT 1

At the line where the parameters are bound (query.bind(parameters)), given that the body parameters are username=timothy, the value of the parameters array is ['timothy', 'admin'].

This is the query that would be executed:

SELECT * FROM users WHERE username = 'timothy' AND auth_level = 'admin' LIMIT 1

Of course, this would have no results, because timothy is not an admin—he is a guest. And, the web application rejects the user if the query has no resultant rows.

What if timothy wanted to bypass this security check? He'd have to find a way to trick the query into returning a row, but there's no SQL injection vulnerability, so he can't just UNION SELECT a fake row.

So, the web application builds the parameters array off of the POST body parameter username. Because of this, timothy can easily bypass access control by using a request body payload like this:

username[]=timothy&username[]=guest

This would cause post('username') to return the array ['timothy', 'guest'] for most parameter parsers (source), so at the line that binds the parameters (again, query.bind(parameters)), the value of the parameters array would be ['timothy', 'guest', 'admin'].

So now, this is the resultant query (assuming the library drops the extra bind parameter):

SELECT * FROM users WHERE username = 'timothy' AND auth_level = 'guest' LIMIT 1

pwned! That query returns a row, so timothy now has access to the /admin page that nobody is supposed to have access to.

Impact

Because this vulnerability type is implementation-dependent, the impact could range from (not a vulnerability) to high, where the worst-case scenario is that an attacker would be able to entirely bypass access control and compromise a sensitive portion of a web application.

Mitigation

  • Be sure to check the type of variables that are used in prepared statements
  • Do not parse the arrays in the first place (e.g. in Express, you'd parse it with bodyParser.urlencoded({ extended: false })) (thanks kmh11)
  • Ensure that the array of parameters that are being bound to the query is of the correct length

This vulnerability type completely depends on the library and the implementation of it. Thus, the mitigations listed above are not all applicable to all instances of this vulnerability type.



Article licensed under CC-by-nc-sa-4.0

Edit