Exploiting Prepared Statements
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.
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.
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?
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.
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.
bodyParser.urlencoded({ extended: false })
) (thanks kmh11)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.