Security Risk: Critical
Exploitation Level: Easy/Remote
DREAD Score: 9
Vulnerability: SQL Injection
Patched Version: 2.1.79
As part of a vulnerability research project for our Sucuri Firewall (WAF), we have been auditing multiple open source projects looking for security issues. While working on the WordPress plugin NextGEN Gallery, we discovered a severe SQL Injection vulnerability. This vulnerability allows an unauthenticated user to grab data from the victim’s website database, including sensitive user information.
Are You at Risk?
This vulnerability can be exploited by attackers in at least two different scenarios:
If you use a NextGEN Basic TagCloud Gallery on your site, or
If you allow your users to submit posts to be reviewed (contributors).
If you fit into any of these two cases, you’re definitely at risk.
This issue existed because NextGEN Gallery allowed improperly sanitized user input in a WordPress prepared SQL query; which is basically the same as adding user input inside a raw SQL query. Using this attack vector, an attacker could leak hashed passwords and WordPress secret keys in certain configurations.
Never trust the input – that is the golden rule. This leads to better security and safe customers. In every scenario we must ask ourselves a few simple questions:
Is this input safe enough?
Is it sanitized?
Do we follow any framework-specific rules and best practices?
WordPress uses the PHP vsprintf function in order to prepare SQL statements in $wpdb->prepare(); which means that the SQL statement uses a format string and the input values as its arguments. This guides us to the conclusion that it is never a good idea to supply user input in the format string because it may not be sanitized against characters that could create valid arbitrary sprintf/printf directives.
This is why this specific method, get_term_ids_for_tags() caught our attention:
This code was found in nextgen-gallery/products/photocrati_nextgen/modules/
From the source code, we notice the $container_ids string is created from tag input and its values are not properly sanitized. They are safe from SQL injection but wouldn’t prevent arbitrary format string directives/input from being inserted, which may cause issues with the WordPress database abstraction prepare() method.
$wpdb->prepare and sprintf
From the prepare method’s code, we notice that few changes are performed on the original SQL code. When %s is found, it will replace it with ‘%s’. Also we see that after changes are performed, it is passes to the vsprintf function, which means any valid format string directives we may have injected will be processed. From PHP’s sprintf function documentation, we know that swapping arguments could take place, and when improperly sanitized inputs are added to the format string, it could lead into some issues like the following:
A malicious user injects the following input into the format string/query:
[any_text1]%1$%s[any_text2]Which will make the query look like this:
querycode1%1$%sany_text2When passed to the prepare method, it will be changed to:
querycode1%1$'%s'any_text2(e.g. %s will become ‘%s’) And then, after the resulting format string passed through the vsprintf function, the resulting SQL query will have the following form:
querycode1[first_argument]'any_text2This means we will have an extra ‘ remaining. This breaks our string’s single-quote sequence and makes our raw [any_text2] input part of the SQL query itself. ###Exploit Scenarios From the plugin’s source code, we found two places where this function would create the $container_ids string (necessary to get the exploit working):
When using the tag gallery shortcode, which requires a privileged authenticated user to perform the attack.
When accessing tags from a NextGEN Basic TagCloud gallery, which malicious visitors can do by modifying the gallery’s URL a bit (given such a gallery exists on the site).
With this knowledge, an unauthenticated attacker could add extra sprintf/printf directives to the SQL query and use $wpdb->prepare’s behavior to add attacker-controlled code to the executed query.
The final attack payloads (using the TagCloud method) would look like the following: