The Sleepy User Agent

6 minute read

From time to time a customer writes in and asks about certain requests that have been blocked by the CloudFlare WAF. Recently, a customer couldn’t understand why it appeared that some simple GET requests for their homepage were listed as blocked in WAF analytics.

A sample request looked liked this:

GET / HTTP/1.1
Host: www.example.com
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (compatible; MSIE 11.0; Windows NT 6.1; Win64; x64; Trident/5.0)'+(select*from(select(sleep(20)))a)+' 
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,fr;q=0.6

As I said, a simple request for the homepage of the web site, which at first glance doesn’t look suspicious at all. Unless your take a look at the User-Agent header (its value is the string that identifies the browser being used):

Mozilla/5.0 (compatible; MSIE 11.0; Windows NT 6.1; Win64; x64; Trident/5.0)'+(select*from(select(sleep(20)))a)+

The start looks reasonable (it’s apparently Microsoft Internet Explorer 11) but the agent strings ends with '+(select*from(select(sleep(20)))a)+. The attacker is attempting a SQL injection inside the User-Agent value.

It’s common to see SQL injection in URIs and form parameters, but here the attacker has hidden the SQL query select * from (select(sleep(20))) inside the User-Agent HTTP request header. This technique is commonly used by scanning tools; for example, sqlmap will try SQL injection against specific HTTP request headers with the -p option.

You are getting very sleepy

Many SQL injection attempts try to extract information from a website (such as the names of users, or their passwords, or other private information). This SQL statement is doing something different: it’s asking the database that’s processing the request to sleep for 20 seconds.

CC BY-SA 2.0 image by Dr Braun

This is a form of blind SQL injection. In a common SQL injection the output of the SQL query would be returned to the attacker as part of a web page. But in a blind injection the attacker doesn’t get to see the output of their query and so they need some other way of determining that their injection worked.

Two common methods are to make the web server generate an error or to make it delay so that the response to the HTTP request comes back after a pause. The use of sleep means that the web server will take 20 seconds to respond and the attacker can be sure that a SQL injection is possible. Once they know it’s possible they can move onto a more sophisticated attack.

Example

To illustrate how this might work I created a really insecure application in PHP that records visits by saving the User-Agent to a MySQL database. This sort of code might exist in a real web application to save analytics information such as number of visits.

In this example, I’ve ignored all good security practices because I want to illustrate a working SQL injection.

BAD CODE: DO NOT COPY/PASTE MY CODE!

Here’s the PHP code:

query($query);

?>

Thanks for visiting

It connects to a local MySQL database and selects the analytics database and then inserts the user agent of the visitor (which comes from the User-Agent HTTP header and is stored in $_SERVER["HTTP_USER_AGENT"]) into the database (along with the current date and time) without any sanitization at all!

This is ripe for a SQL injection, but because my code doesn’t report any errors the attacker won’t know they managed an injection without something like the sleep trick.

To exploit this application it’s enough to do the following (where insecure.php is the script above):

curl -A "Mozilla/5.0', (select*from(select(sleep(20)))a)) #" http://example.com/insecure.php

This sets the User-Agent HTTP header to Mozilla/5.0', (select*from(select(sleep(20)))a)) #. The poor PHP code that creates the query just inserts this string into the middle of the SQL query without any sanitization so the query becomes:

INSERT INTO visits (ua, dt) VALUES ('Mozilla/5.0', (select*from(select(sleep(20)))a)) #', '2016-05-17 03:16:06')

The two values to be inserted are now Mozilla/5.0 and the result of the subquery (select*from(select(sleep(20)))a) (which takes 20 seconds). The # means that the rest of the query (which contains the inserted date/time) is turned into a comment and ignored.

In the database an entry like this appears:

+---------------------+---------------+
| dt                  | ua            |
+---------------------+---------------+
| 0                   | Mozilla/5.0   |
+---------------------+---------------+

Notice how the date/time is 0 (the result of the (select*from(select(sleep(20)))a)) and the user agent is just Mozilla/5.0. Entries like that are likely the only indication that an attacker had succeeded with a SQL injection.

Here’s what the request looks like when it runs. I’ve used the time command to see how long the request takes to process.

$ time curl -v -A "Mozilla/5.0', (select*from(select(sleep(20)))a) #" http://example.com/insecure.php
* Connected to example.com port 80 (#0)
> GET /insecure.php HTTP/1.1
> Host: example.com
> User-Agent: Mozilla/5.0', (select*from(select(sleep(20)))a) #
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Mon, 16 May 2016 10:45:05 GMT
< Content-Type: text/html
< Transfer-Encoding: chunked
< Connection: keep-alive
< Server: nginx

Thanks for visiting
* Connection #0 to host example.com left intact

real   0m20.614s
user   0m0.007s
sys    0m0.012s

It took 20 seconds. The SQL injection worked.

Exploitation

At this point you might be thinking “that’s neat, but doesn’t seem to enable an attacker to hack the web site”.

Unfortunately, the richness of SQL means that this chink in the insecure.php code (a mere 3 lines of PHP!) lets an attacker go much further than just making a slow response happen. Even though the INSERT INTO query being attacked only writes to the database it’s possible to turn this around and extract information and gain access.

CC BY 2.0 image by Scott Schiller

As an illustration I created a table in the database called users containing a user called root and a user called john. Here’s how an attacker might discover that there is a john user. They can craft a query that works out the name of a user letter by letter just by looking at the time a request takes to return.

For example,

curl -A "Mozilla/5.0', (select sleep(20) from users where substring(name,1,1)='a')) #" http://example.com/insecure.php

returns immediately because there are no users with a name starting with a. But

curl -A "Mozilla/5.0', (select sleep(20) from users where substring(name,1,1)='j')) #" http://example.com/insecure.php

takes 20 seconds. The attacker can then try two letters, three letters, and so on. The same technique can be used to extract other data from the database.

If my web app was a little more sophisticated, say, for example, it was part of a blogging platform that allowed comments, it would be possible to use this vulnerability to dump the contents of an entire database table into a comment. The attacker could return and display the appropriate comment to read the table's contents. That way large amounts of data can be exfiltrated.

Securing my code

The better way to write the PHP code above is as follows:

prepare("INSERT INTO visits (ua, dt) VALUES (?, ?)");
$stmt->bind_param("ss", $_SERVER["HTTP_USER_AGENT"], date("Y-m-d h:i:s"));
$stmt->execute();

?>



Thanks for visiting

This prepares the SQL query to perform the insertion using prepare and then binds the two parameters (the user agent and the date/time) using bind_param and then runs the query with execute.

bind_param ensures that the special SQL characters like quotes are escaped correctly for insertion in the database. Trying to repeat the injection above results in the following database entry:

+---------------------+----------------------------------------------------+
| dt                  | ua                                                 |
+---------------------+----------------------------------------------------+
| 2016-05-17 04:46:02 | Mozilla/5.0',(select*from(select(sleep(20)))a)) #  |
+---------------------+----------------------------------------------------+

The attacker's SQL statement has not turned into a SQL injection and has simply been stored in the database.

Conclusion

SQL injection is a perennial favorite of attackers and can happen anywhere input controlled by an attacker is processed by a web application. It's easy to imagine how an attacker might manipulate a web form or a URI, but even HTTP request headers are vulnerable. Literally any input the web browser sends to a web application should be considered hostile.

We saw the same attacker use many variants on this theme. Some tried to make the web server respond slowly using SQL, others using Python or Ruby code (to see if the web server could be tricked into running that code).

CloudFlare's WAF helps mitigate attacks like this with rules to block injection of SQL statements and code.

Categories:

Updated:

Spotlight on Women in Cybersecurity

less than 1 minute read

Sucuri is committed to helping women develop their careers in technology. On International Women’s Day, Sucuri team members share their insights into workin...

Hacked Website Trend Report – 2018

less than 1 minute read

We are proud to be releasing our latest Hacked Website Trend Report for 2018. This report is based on data collected and analyzed by the GoDaddy Security / ...