Independently secure, together not so much – a story of 2 WP plugins

Intro

Recently we had to do a security assessment on a WordPress website. Obviously when dealing with a WordPress installation the best option is to always target the plugins. We’ve quickly enumerated the plugins using WPScan and then we recreated this setup in our local environment for easier testing & debugging. We found 2 interesting plugins which support file uploads and that is always interesting functionality to abuse, so we will study them one by one. The objective of this assignment was to check what security issues we can find in the plugins attack surface and see if we can obtain RCE by abusing the existing plugins.

Although the 2 plugins seem secure when you look at them independently we discovered that when used together this can actually result in an RCE, because one plugin can be used to circumvent the security mechanisms created by the other. Hence the reason for this post. Authentication is needed to exploit this. Let’s start.

Contact Form 7

This seems to be a very popular plugin with over 5+ Million installations. It allows you to add a contact form to any article and also optionally attach a file. Let’s try to upload a file and do a bit of dynamic analysis on this to check how it behaves, what sort of sanitization it does on the filename etc.

By tracing the code and adding various breakpoints we reach the following code where the uploaded file is copied to a random dir:

A new random dir gets created inside /wp-content/uploads/wpcf7_uploads and it looks like this:

So, it’s a numeric filename, looks like a number that was perhaps generated with mt_rand and a 0 prefix was added? Let’s inspect the code:

Indeed mt_rand is used together with a 0 prefix. mt_rand is not crypto secure as you probably know, so perhaps we can use that in an attack for later. Let’s have a look at how the filename is sanitized.

A plugin specific function, wpcf7_antiscript_file_name is called to prevent uploading all sort of malicious like php, sh, py, rb anything which could result in a direct shell basically. Let’s see how our filename with double extension looks like after sanitization.

so, basically an underscore has been added to the ‘.php’ extension to neutralize it. Ok, interesting, let’s do more testing with the debugger, to analyze the behavior and get more insight:

If ‘.php’ is the second extension, again it gets neutralized with an underscore and also a ‘.txt’ extension is added. On the other hand if we use the ‘.phar’ extension this gets completely ignored. Nice 🙂

For reference the full function is bellow:

However, this is not everything. Tracing the code further, we found out that wp_unique_filename is called, which is a WordPress specific function, which does a great job at sanitizing the filename again. This implements an excellent whitelist, so even if you found an issue with the plugin function wpcf7_antiscript_file_name, you will have to bypass this one as well.

Let’s quickly study it’s behavior, considering what we found above:

So now the “.phar” extension is also neutralized if it’s the first extension, but it’s ignored if it’s the second extension.

We traced the code further and we realized that there is one more issue we need to overcome, our filename gets quickly deleted from the filesytem after it’s uploaded.

Thus, we would have to exploit a race condition as well. Sounds doable to me. But let’s check one more thing, if we put a breakpoint before deletion, can we actually access this new directory and what’s inside it?

Not really, perhaps a .htaccess file is stopping us? Let’s check.

Indeed, we have a “Deny from all” in the plugin upload directory. So, to summarize we have the following challenges to overcome:

  • a random upload directory name gets created each time to hold our newly uploaded file – we need to leak this because of a .htaccess “Deny from all” rule
  • various sanitization on the filename – the WordPress sanitization is solid
  • the random directory and the uploaded file inside get deleted after upload, so we need to exploit a race condition and access it before it gets deleted

LiteSpeed Cache

Enter LiteSpeed Cache. This plugin is designed to perform various optimizations and make your WordPress install load faster. Without further ado, we’ve found an (authenticated) arbitrary .htaccess write vulnerability:

You control the path and also the file contents. Some strict validation is done on the path(extension), we couldn’t find a bypass for that. However, there’s no restriction on the file content, you can write anything there. It would be pretty hard too, filtering a .htaccess file contents.

We will abuse this to overwrite the .htaccess file from the previous plugin. We’ve decided to make the entire folder publicly accessible with “Allow from All” directive and also make .txt and .png file executable as php code. Of course this is just one option, you could overwrite any .htaccess on the system you like. You could use this plugin vulnerability in many other scenarios, the first plugin(Contact form 7) could be substituted by any other plugin which allows public uploads but gives you a hard time when uploading your shell.

Thus, from the 3 challenges we had to overcome, we can now leak the random directory name, sanitization on the filename/extension is irrelevant and we’re only left with exploiting a race condition. We’ve developed a POC in python for winning this race. It’s very simple, it runs a thread which will continuously try to read the name of the newly created random directory. The POC continuously searches a phpinfo.txt in the created directory, so this is what you’ll have to upload from the frontend. When you win the race and execute phpinfo.txt, this will write another file, shell.txt which will be our permanent shell on the system.

A phpinfo.txt file gets written and then it’s quickly deleted:

Winning the race and executing phpinfo.txt will write shell.txt, a permanent shell.

One thing we noticed is that our newly written permanent shell (shell.txt) will stop the newly random directory from being deleted. In case you need some extra help to win the race condition we found this resource to contain some great tips: https://lab.wallarm.com/race-condition-in-web-applications/. And here is our glorious shell executing commands:

This vulnerability has been fixed in the latest version of LiteSpeed Cache plugin, to clarify there is no security issue with Contact Form 7 plugin. Stay tuned, we have more file uploads/race conditions articles coming, much more difficult to exploit than this one. Thanks for reading.