I wanted to give enough time for everyone to update before releasing more information about this vulnerability. According to the newly updated WordPress plugin repository, the WP All Import free version has 20,000+ active installs. The code is out there so anyone would be able to figure out what the big deal is. I won’t distribute my exploit code, so please stop asking!
I will be referencing version 3.2.3 of the free plugin in this post, if you are looking at the paid version the line numbers and filenames might be different.
There were two distinct vulnerabilities for getting access to the plugin and they have been exploited time and time again so I won’t spend time covering them.
First off are ajax requests that don’t verify the user has permissions, in this case they are not the ‘ajax_nopriv’ type so you at least need to have an account on the system to use them, but even the lowest account would work.
The second type is assuming that the ‘admin_init’ action only happens for admins. Which isn’t the case, it is called for any action that starts up the admin interface. Which could be the user profile editor or the AJAX interface.
“If this isn’t Nice, What Is?”
So now that we’re behind enemy lines without permission, what can we do?
- Retrieve any file on the system that ends in .txt
- Retrieve any file on the system that ends in .html
- Retrieve any value from the postmeta table
- Upload arbitrary files to system
Once I have the keys to the kingdom I stop looking for unlocked windows. There were likely other things you could do, but once you’ve got full access to the system who cares?
“Trust thyself only, and another shall not betray thee”
The helpful function $input->getpost(‘page’, ”)) here takes the ‘page’ variable from a HTTP POST and sticks it in $page. If page starts with ‘pmxi_’ then we keep on rolling and set $action to HTTP POST ‘action’.
Now we’re cooking. We can arbitrarily call any class that is an instanceof PMXI_Controller_Admin and starts with ‘pmxi_’ (there are 8 of them).
Find your favorite function and work with it, I’m sure you will find something fun. When you find one that lets you transfer a file into uploads you’re starting to see the light at the end of the tunnel.
Of course, someone was trying to keep people from guessing the location (or maybe just trying to keep from overwriting previous uploads). So there is a function to return a ‘secure’ directory/file.
Looks good, right? Just last week Securi wrote an advisory about another plugin that used md5( time() ) to generate a ‘secret’ key. In this case the naming happens at time of upload and the HTTP Protocol happily returns a timestamp, so you have a pretty small window to search for the ‘secret’ directory.
“You keep using that word. I do not think it means what you think it means.”
So we’ve now managed to upload a file to a ‘secret’ directory. In my experience most systems will happily execute php code from the ‘wp-content/uploads’ directory, so now we can run whatever code we want. My example just ran phpinfo() and exited, but I don’t think the next guy will be quite as nice.
Lessons Learned
- “Always… no, no… never… forget to check your references.”
- Set your uploads directory to not execute php, cgi, or any other handlers
- Disable indexes on uploads directory (not an issue here because it creates an index.php, but to be continued in another post)
- Keep your plugins updated!
- If you are a plugin developer, think like an attacker!
I tested the updates to WP All Import and WP All Import Pro and could no longer have fun inside the code base. A few simple lines of code fixed these major vulnerabilities.
Update