During a recent penetration test, a client was willing to allow enough time for myself to really throw my best at their apps, the application i’ll be writing about is the Forum CMS (Invision Power Board).
After going at the application blind, I managed to discover a potential vulnerability that I would later confirm to be blind SQL injection with a twist -- There were blacklist filters hardcoded into the CMS in attempt to stop malicious queries being sent via the URL.
In this blog post I will explain the filters I faced and in turn how I managed to bypass them.
Note: Invision Power Board is a widely used Forum CMS used by many large companies to name a couple:
Kaspersky - forum.kaspersky.com
Nvidia - forums.nvidia.com
Analysis
During testing I managed to cause an interesting error by injecting a comment into the (cId) parameter used within the search module of the forum as seen below.
Even though I had at this point assumed these errors were going to be non-verbose, I still attempted to use generic ORDER BY statements followed by UNION in order to verify whether or not I was successfully injecting into the queries.
Ok beautiful so using ORDER BY I was able to identify the current query had 6 columns, naturally I crossed my fingers hoping the database would just give up the goods when I tried to UNION the query -- However it’s never that easy is it :(
After trying close to...if not every obfuscation technique under the sun I was bothered to actually look at the source code and discover that there were hard-coded blacklists that were blocking (SELECT, UNION and ‘ quotes)
Having discovered the blacklisting, I scrapped error based and moved on to boolean-based blind methods as seen below:
AND 3=3-- (True statement)
AND 3=0-- (False statement)
Perfect, I was able to generate both a TRUE and FALSE case in which the TRUE evaluated to my search term being retrieved and FALSE failing to return my search results. (“No results found for ‘tupac’)
With boolean-based blind injection now working I continued to extract information (Keeping in mind the blacklisted keywords - SELECT, UNION, and Apostrophe.
AND substr(version(),1,1)=5-- (MySQL version 5.x)
NOTE: At this stage I could have easily just tried my luck and threw it into SQLmap (More on that later), however the deployment I was working on had implemented various security practices that would have rendered SQLmap no faster than doing it by hand.
Being haunted by the blacklisted words I went back to the drawing board, knowing full-well that without being able to utilize SELECT I will be confined to the current query so I needed to think out of the box for this one.
After a fair amount of headbanging and reading, I had collected enough information in order for me to start trying QUERIES without SELECT or UNION.
Not being able to use quotes (‘) I had to utilize the ASCII representation of the characters.
Getting first letter of system_user()
and ascii(substr(system_user(),1,1))=114-- ( 114 = 'r' in ASCII)
and ascii(substr(system_user(),2,1))=111-- ( 111 = 'o' in ASCII)
So far this was evaluating to system_user() = "ro..." so you can see where this is going.
With SELECT and/or UNION out of the equation, I was getting desperate to break out of this query so I decided to try my luck with load_file().
Note: This Does require that the current user has FILE privileges.
If not being able to use quotes was not enough, I had to think of a way to actually read the output returned by the load_file(), that's if it actually worked in the first place.
Before jumping the gun and trying to read files, I wanted to make sure I could even load them, which I confirmed by combining ISNULL with load_file().
Hence if the result returns as NULL/False then I can read the file.
cId=3 and isnull(load_file(0x2F6574632F686F7374730D0A))--+ (/etc/hosts)
Since this returned as TRUE it meant that I could not load "/etc/hosts".
Keen to have another crack I then tried "/etc/passwd"
cId=3 and isnull(load_file(0x2f6574632f706173737764))--+
Okay, so finally were getting somewhere, I can load certain files but I still need to be able to read the returned value of my load_file() without using UNION, SELECT or Quotes.
I then turned back to faithful substr() and discovered that I was able to enumerate any loaded file char by char using their ASCII values.
Naturally I went straight for the jugular, in this case: (/var/www/html/board/upload/conf_global.php)
Note: You must know the true location of the conf_global.php to be able to load it in the first place. However luckily there is a public full-path disclosure vulnerability for this exact version (3.4.5) www.intelligentexploit.com/view-details.html?id=17288
At this point there are probably countless better ways to optimize the extraction as bruting the conf file char by char is far from desirable.
Note: Coming back to the topic of SQLmap, which I'd like to mention is a great project and hats off to Miroslav and company, however -- In this case SQLmap was failing to read the files due to the dependency of SELECT when using the "--file-read" argument.
So below is the confirmation that my method worked successfully:
AND ascii((substr(load_file(0x2F7661722F7777772F68746D6C2F626F6172642F75706C6F61642F636F6E665F676C6F62616C2E706870),1,1)))=60--
As illustrated, I was able to determine that the first character of conf_global.php was indeed "<" or it's ASCII representation (60) and could have either scripted the rest or sat there for a very long time and pulled out char by char.
Conclusion
Arguably, if someone is game enough to allow the FILE privilege and/or have their MySQL run as root, then they may be inviting such attacks, However I believe the point has been addressed that hard-coded filters are not always impossible to bypass.
Additionally, As I am sure there are other methods that could have produced the same or similar outcome, I am very interested to hear/see any feedback on other bypasses/methods that you would use against these filters.
I guess it also illustrates the importance of manual testing as I was curious to see if any of the popular commercial web-app scanners would detect this, I tried 5 different products and the only tool that didn't fail horribly was SQLmap however I still had to finish off manually when using load_file().
NOTE: This issue has been fixed in the latest version 3.4.6 as the "cId" parameter is sanitized correctly.
I guess it also illustrates the importance of manual testing as I was curious to see if any of the popular commercial web-app scanners would detect this, I tried 5 different products and the only tool that didn't fail horribly was SQLmap however I still had to finish off manually when using load_file().
NOTE: This issue has been fixed in the latest version 3.4.6 as the "cId" parameter is sanitized correctly.