With software, popularity comes at a cost. While a thriving community of users (and contributors for open source) is the dream outcome of every project, the downside to this attention is the target it creates for those looking to do bad - after all, the more people who use a piece of software, the more potential victims to target with the inevitable vulnerabilities waiting to be uncovered.
Few situations can better showcase this downside to popularity than the catastrophic SQL injection vulnerability discovered in Drupal 7 in October 2014, otherwise known as CVE-2014-3704. Drupal is of course a hugely popular open source framework/CMS, seeing increasing use in super serious situations like official government websites, and this particular vulnerability - a SQL injection, perhaps the most damaging type of vulnerability possible in a database driven web application - made every Drupal 7 website in the world completely open to hackers with no mitigation, while unpatched.
Or did it? While this vulnerability rendered Drupal helpless within its own boundaries, what if there was a way to intercept nefarious requests seeking to exploit CVE-2014-3704, or other vulnerabilities, before they even reached a byte of web code?
Enter the Web Application Firewall, of WAF for short. Like a typical network firewall, a WAF is designed to inspect traffic and apply rules based on what it sees. What makes A WAF special is it works at the application layer, parsing aspects of a HTTP request and ruling on whether a request shall pass through or not. How effective can this be? Well, spoiler alert (in case the page title didn't give it away): if you were running a WAF, chances are you would have been completely protected from CVE-2014-3704. But don't take my word for it, let me show you!
To demonstrate the value of a WAF, even a minimally configured one, I will exploit Drupal 7.31, on a CentOS 7 VM, using Apache as the web server. Assuming the VM is ready to go (and has a DB server configured to install Drupal):
wget http://files.drush.org/drush.phar chmod +x drush.phar mv drush.phar /usr/local/sbin/drush cd /var/www/html drush dl drupal-7.31 mv drupal-7.31 drupal drush site-install standard --account-name=admin --account-pass=admin --db-url=mysql://root@localhost/drupal -y chown -R apache.apache drupal
I won't go into a detailed breakdown of the vulnerability here - for that, you should check out this post by securitysift.com. As a high level overview however, when building prepared SQL statements, Drupal was creating parameter names dynamically in prepared SQL statements using untrusted data (like field names in HTTP requests). The fix was actually pretty light code wise.
To perform the exploit, I'll use this proof-of-concept script (the only change I had to make to the script was add an "Accept" header to the HTTP request, to prevent the WAF from rejecting the request for reasons other than SQLi).
Before we run the attack, here is the Drupal users table:
MariaDB [drupal]> use drupal; MariaDB [drupal]> select name from users; name ----- admin
And now the attack:
This is an interactive script, asking for the name of the user to create using the exploit - I chose "test". Did it work?
MariaDB [drupal]> select name from users; name ----- admin test
Yep, it sure did.
The beauty of this attack is the fact the HTTP request method and the resource being targeted are not an unusual combo, so it wouldn't look overly suspicious in the logs (outside of the basic user-agent used by this particular exploit script, but that can obviously be changed easily enough), and Apache (or any other web server) is not going to log the actual POST request data by default. Here is what the request looked like to our standard Apache log:
127.0.0.1 - - [12/Jan/2016:19:05:49 +1100] "POST /drupal/?q=node&destination=node HTTP/1.1" 200 9083 "-" "Mozilla"`
It isn't until the user table in the DB is compared before and after the attack that we can see the backdoor user account, however this is just one of effectively infinite possibilities with this vulnerability - just about any SQL command could have been executed instead of creating a user, so there is no sure way to tell a Drupal site has been compromised with this attack, meaning the likely course of action would be to do a thorough audit of the server for any obvious backdoors and restore from a known good backup.
So, I've shown how catastrophic the exploit can be, but what happens if we have a WAF? Given I'm using Apache, my WAF of choice will be Mod Security - an Apache module that extends the web server to include WAF capabilities.
yum install mod_security mod_security_crs -y systemctl restart httpd
The second package - mod_security_crs - is the "Core Rule Set", that is, a collection of Mod Security rules designed to block patterns in requests that are commonly associated with exploit attempts.
Is this enough to protect against CVE-2014-3704? Lets try to add another user now Mod Security is enabled:
This time I tried to add the user "test2". Did it work?
MariaDB [drupal]> select name from users; name ----- admin test
It did not! But why? Mod Security creates an audit log that keeps a record of when it steps in to block a request, located at
/var/log/httpd/modsec_audit.log in a default CentOS 7 install, and here is what it says for this request:
Message: Access denied with code 403 (phase 2). Pattern match "(/\\*!?|\\*/|[';]--|--[\\s\\r\\n\\v\\f]|(?:--[^-]*?-)|([^\\-&])#.*?[\\s\\r\\n\\v\\f]|;?\\x00)" at ARGS_NAMES:name[0 ;insert into users (uid, name, pass, mail, status) select max(uid)+1, 'test2', '$S$S$CTo9G7Lx27gCe3dTBYhLhZOTqtJrlc7n31BjHl/aWgfK82GIACiTExGY3A9yrK1l3DdUONFFv8xV8SH9wr4r23HJauz47c/', 'firstname.lastname@example.org', 1 from users; insert into users_roles (uid, rid) VALUES ((select uid from users where name='test2'), (select rid from role where name = 'administrator'));; # ]. [file "/etc/httpd/modsecurity.d/activated_rules/modsecurity_crs_41_sql_injection_attacks.conf"] [line "49"] [id "981231"] [rev "2"] [msg "SQL Comment Sequence Detected."] [data "Matched Data: # found within ARGS_NAMES:name[0 ;insert into users (uid, name, pass, mail, status) select max(uid)+1, 'test2', '$S$S$CTo9G7Lx27gCe3dTBYhLhZOTqtJrlc7n31BjHl/aWgfK82GIACiTExGY3A9yrK1l3DdUONFFv8xV8SH9wr4r23HJauz47c/', 'email@example.com', 1 from users; insert into users_roles (uid, rid) VALUES ((sAction: Intercepted (phase 2)Stopwatch: 1452588038059746 1179 (- - -)Stopwatch2: 1452588038059746 1179; combined=326, p1=121, p2=187, p3=0, p4=0, p5=18, sr=10, sw=0, l=0, gc=0Response-Body-Transformed: DechunkedProducer: ModSecurity for Apache/2.7.3 (http://www.modsecurity.org/); OWASP_CRS/2.2.6.Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.1e-fips PHP/5.4.16Engine-Mode: "ENABLED"
What this healthy chunk of text shows is Mod Security rejecting the request (with a HTTP 403 Forbidden response) because the request data matches a pattern recorded as being a likely SQL injection attack, which is exactly what this request was. With Mod Security, CVE-2014-3704 (in this form at least) has been neutered before it can cause any harm, and we even have a log to show the attack attempt in full detail.
The point of this write up is, of course, not to criticise Drupal for a lack of security, as there are few major web software packages out there which haven't suffered from a similar vulnerability as the 2014 SQL injection described here. Rather, the point is to show that, if you are responsible for a web service and suspect you wouldn't enjoy a nasty day changing surprise one sleepy Sunday morning, then perhaps a Web Application Firewall is worth your time and effort (as the default config is unlikely to work perfectly) to include in your web server builds. It won't stop every attack, and using a WAF does not mean you should get complacent on applying security fixes where available, but it may just save your bacon in the time between a vulnerability being disclosed and the moment you apply the fix - it most likely would have in regards to CVE-2014-3704, and probably countless other vulnerabilities out there for all types of back-end web software.