Web application penetration testing is a well researched area with proven tools and methodologies. Still, new techniques and interesting scenarios come up all the time that create new challenges even after a hundred projects.
In this case study we start with a relatively simple blind SQL injection situation and show how this issue could be exploited in a way that made remote code execution possible. The post will also serve as a reference for using Duncan, our simple framework created to facilitate blind exploitation.
Blind SQL injection – Duncan style
The initial vulnerability was present in the user preferences editor of the application. One of the submitted parameters was simply concatenated to an SQL query string without proper sanitization or filtering, but I couldn’t get access to the result of the injected statements via the application response or error messages. So this is a general blind SQL injection scenario that is still good enough to extract interesting data from the database. To make life easier first I tried to use SQLmap, but I couldn’t get the tool recognize the injectable parameter. As usual I turned to Duncan that I created exactly for situations like this. I created a simple module that injected the following expression to the vulnerable query (the backend database was PostgreSQL):
"ascii(substr(cast((%s) as varchar),%d,1))<%d"
Duncan replaces the first formatting sequence (%s) with the query to be executed. The second placeholder indicates the position of the character I want to guess, the third is the guessed value of the character. With this added logic Duncan performs a multi-threaded binary search to retrieve the result of the injected query. After mapping the database structure I quickly went for the passwords hoping that I can log into the administrative interface of the application:
$ wc -l mytarget.py # Including blank lines, imports, and everything...
$ python run_duncan.py --use mytarget.SimpleDuncan --query "select password from users where id=1" --pos-end 33 --threads 5 --charset 0123456789abcdef
For efficiency I defined a custom charset and defined a maximum result length based on previous tests – it seemed like the application stores the MD5 hashes of passwords in hexadecimal form. I could successfully retrieve the password hashes of some interesting users and also fetched my own to test if there’s some twist in the password storage implementation. It seemed like there was: I tried numerous combinations of my password, username and related data, but I couldn’t reproduce the hash I retrieved from the database. I had to go deeper!
It was rather obvious that there was some salt involved in the password hashing process, but I didn’t know anything about it. It could be a prefix salt, a postfix salt, some cascaded hash construction (like MD5(MD5(password)||salt)) and there was no way I could guess what it was. But fortunately I remembered an article I read some time ago: Aaron Devaney of Context described how to use database metadata to SELECT the currently executing query – he called this technique SQL inception. There was a chance that the hash was calculated by the database and not the application, so if I could capture a query that sets hashed passwords I could learn how the hash is generated.
First I had to find out what query I was injecting to. Although I suspected that I’m injecting into an UPDATE modifying the users table, it was possible that some other query (like one used for logging) got corrupted by my injection, proving the approach impractical in this case*. Fortunately I was right, it seemed that I’m indeed injecting into an UPDATE:
$ python run_duncan.py --user mytarget.SimpleDuncan --query "select upper(query) from pg_stat_activity" --pos-end 6 ascii-end 97
I quickly tried to find the part of the query that sets the password, but interestingly I got unreliable results or no results at all. After some tinkering I decided to find out more about the pg_stat_activity table:
$ python run_duncan.py --user mytarget.SimpleDuncan --query "select cast(count(*) as varchar) from pg_stat_activity" --pos-end 4 --charset 0123456789
It looked like some other people are using the database too, and the inception query accesses wrong data. To solve this I used a WHERE clause to limit the results to my own queries:
$ python run_duncan.py --user mytarget.SimpleDuncan --query "select cast(strpos(upper(query),'MD5') as varchar) from pg_stat_activity WHERE query like '%S2S2S2%'" --pos-end 4 --charset 0123456789
Since the WHERE clause will be contained in the query attribute of the ps_stat_activity table, it guaranties that the query will match itself :) After finding the position of the MD5() call I could select its argument:
$ python run_duncan.py --user mytarget.SimpleDuncan --query "select upper(query) from pg_stat_activity WHERE query like '%S2S2S2%'" --pos-end 4 --ascii-end 97 --pos-start 150 --pos-end 180 --threads 10
ORD = MD5('MYPASS#SOMESALT#'
Knowing how the hash is constructed I could load the previously collected administrative hashes to our GPU-based cracker. oclHashcat benchmarks pretty well with MD5, so most passwords didn’t have a chance to “survive” brute-force – especially since the “salt” was constant…
Although the administrative interface of the application didn’t turn out to be that interesting I could use one of the cracked passwords (and the user enumeration feature of WPScan) to access the administrative panel of a WordPress blog that was also in scope. In WordPress administration I placed a backdoor to one of the PHP templates so I could achieve remote code execution with the privileges of the web server.
* Acutally I could exploit parallel execution to retreive pieces of an UPDATE while I was injecting to another query, but this would’ve made exploitation much less effective.
In this blog post we showed how the SQL inception technique described by Aaron Devaney can be used to solve a practical problem during penetration testing, and how one can fine-tune the process for optimal results with Duncan. We’d also emphasize that password management and storage is still a major weakness of simple web applications and complex corporate systems alike.
Here are our advises to developers and administrators:
- Use parameterised queries or ORM for database access. You should never construct SQL queries in your application by hand in 2015.
- Use a modern key stretching algorithm for password storage. Although Bcrypt, PBKDF2, Scrypt and yescrypt have different properties, from a practical standpoint they all raise the bar for attackers significantly. MD5 and SHA-* weren’t designed for password storage but for efficient fingerprinting, so they can be efficiently brute-forced as well.
- Use long, unique salts. Most algorithms mentioned in the previous point take care of salting for you.
- The security of your systems shouldn’t rely on the secrecy of the salts (or the algorithms in use).
- Use an offline password manager. Never use the same password for different systems.
- If you have to remember your credentials by heart, use long passphrases instead of traditional passwords. Teach your users to do the same by enforcing a password policy that favors long passwords instead of complex ones.