Wordfence v5.2.3 suffers from multiple vulnerabilities including 2 stored XSS, insufficient logging of requests, being able to bypass the throttling feature (designed to limit scraping) and being able to bypass the exploit detection feature. All of these appear to be the result of a lack of understanding of PHP superglobals.
Let’s start with a little blurb that will be relevant to some of the vulnerabilities. In PHP, $_SERVER[‘REQUEST_URI’] contains the request uri (shocking, I know), which is usually everything after the host and port. So for ‘http://example.com/a/b.php?one=two’, the request uri would be ‘/a/b.php?one=two’. Any special characters in the url should be encoded properly by the browser before they are sent to the server. The server (or apache, at least) will not encode or alter the request uri if the client failed to encode it properly. Enough background, on to the vulnerabilities.
XSS #1
The request uri is displayed exactly as received (i.e. unescaped) on the IPTraf.php page. An attacker can make a request with javascript in the url that hasn’t been url encoded to trigger the XSS.
For example, this:
curl -v 'http://wptestbox1.dev/<script>alert(0)</script>'
results in this:
Note: curl doesn’t url encode the url for you. Trying this in a regular web browser (or with many other tools) won’t work because they automatically encode stuff.
XSS #2
OK, this one is so close to being super fun, but it falls a bit short. It’s only exploitable if the target site is the default vhost, basic caching is turned on, and debugging data is being appended to cached pages. Yeah, so not too many sites. The HTTP Host header is placed in the debug data unescaped.
This:
curl -v -H "Host: y--><img src=f onerror=alert(0); >y" 'http://wptestbox1.dev/2014/09/15/hello-world/'
will produce a cached file located:
http://wptestbox1.dev/wp-content/wfcache/y--imgsrcfonerroralert0y_2014/09~15~hello-world~~_wfcache.html
which when viewed will execute the javascript that was in the host header. Cached pages are kept per host name, so this page is only accessible with a direct link. I say this one falls a bit short of being really fun because wordpress doesn’t like $_SERVER[‘REQUEST_URI’] being an absolute URI. By specifying an absolute URI in your HTTP request, you can include an arbitrary Host header which will be ignored by the server. It would pick a vhost by the absolute URI, while wordfence would still use the value of the host header. So this would have gotten around the default vhost requirement, but no. HTTP is fun.
It’s also worth mentioning that the ‘/wp-content/wfcache/’ directory has no .htaccess or index file, so you can freely browse through the cached pages. There shouldn’t be anything too sensitive in there, but undoubtedly on some sites there will be.
Insufficient Logging
Now the insufficient logging. When wordfence is about to log a request, it calls this function to filter out ajax requests, whitelisted IPs, logged in admins, etc.
public function logHitOK(){ if(stristr($_SERVER['REQUEST_URI'], 'wp-admin/admin-ajax.php')){ return false; } //Don't log wordpress ajax requests. if(is_admin()){ return false; } //Don't log admin pageviews if(isset($_SERVER['HTTP_USER_AGENT'])){ if(preg_match('/WordPress\/' . $this->wp_version . '/i', $_SERVER['HTTP_USER_AGENT'])){ return false; } //Ignore requests generated by WP UA. }
If this function returns false, the request isn’t logged. Do you see some really obvious problems with this code? $SERVER[‘REQUEST_URI’] includes the query args (that crap after the ‘?’). They are checking if ‘wp-admin/admin-ajax.php’ is present somewhere in the request URI.
This gets logged:
http://wptestbox1.dev/2014/09/15/hello-world/
This does not:
http://wptestbox1.dev/2014/09/15/hello-world/?this-doesnt-do-anything=wp-admin/admin-ajax.php
You could also set your user agent appropriately to avoid having your requests logged.
Throttle Bypass
Wordfence has a cool feature to rate limit/block IPs making too many requests too quickly. This is great when someone is scraping your site. Unfortunately, it depends on requests being logged correctly to function. Simply set your user agent or append ‘wp-admin/admin-ajax.php’ to your requests and scrape away. You’ll never be throttled or blocked (automatically). This doesn’t extend to repeated log in attempts. Fail your log in attempts too many times and your IP will be blocked regardless of the above stuff.
Exploit Protection Bypass
Another cool thing that wordfence tries to do, is block exploit attempts of Revolution Slider.
An attempt to exploit the revslider vulnerability looks something like this:
http://victim/wp-admin/admin-ajax.php?action=revslider_show_image&img=../wp-config.php
You might notice that in the above POC, all of the parameters are GET, not POST. This is important. The vulnerable revslider code only uses GET parameters supplied to it, not POST or REQUEST.
And the wordfence code that protects against revslider exploits:
public static function initProtection(){ if(preg_match('/\/wp\-admin\/admin\-ajax\.php/', $_SERVER['REQUEST_URI'])){ if(isset($_REQUEST['action']) && $_REQUEST['action'] == 'revslider_show_image' && isset($_REQUEST['img']) && preg_match('/\.php$/i', $_REQUEST['img']) ){ self::getLog()->do503(86400, "URL not allowed. Slider Revolution Hack attempt detected. #2"); exit(); //function above exits anyway } } }
In general, $_REQUEST = $_GET + $_POST + $_COOKIE.
Wordpress has this code though:
// Force REQUEST to be GET + POST. $_REQUEST = array_merge( $_GET, $_POST );
The array_merge function merges $_GET and $_POST together. If a GET and POST parameter both have the same name, the POST parameter takes precedence. If you supply malicious GET parameters and a benign ‘img’ POST parameter, the wordfence code will allow the request through and the revslider code will only use the malicious GET parameters.
Umm, make the admin think you’re google?
One last thing. By default, the plugin won’t let you manually ban IPs that belong to google (that’s not the best SEO strategy). When the admin tries to manually ban an IP, this bit of code runs:
if(wfConfig::get('neverBlockBG') != 'treatAsOtherCrawlers'){ //Either neverBlockVerified or neverBlockUA is selected which means the user doesn't want to block google if(wfCrawl::verifyCrawlerPTR('/googlebot\.com$/i', $IP)){ return array('err' => 1, 'errorMsg' => "The IP address you're trying to block belongs to Google. Your options are currently set to not block these crawlers. Change this in Wordfence options if you want to manually block Google."); } }
This code calls verifyCrawlerPTR, which does a reverse lookup on the IP, makes sure it matches the provided regex, then does a forward lookup on the domain to verify that it matches the IP. The missing period at the start of the regex means that the domain simply has to end in ‘googlebot.com’. One could register ‘this-is-definitely-not-googlebot.com’, set the A record to the IP you’re using and set the reverse dns of the IP. Someone trying to manually ban this address would be told that they can’t because it belongs to google.
Wait, this is a security plugin, right?
Did you notify the Wordfence author before presenting this on your website?
He’s good about closing these sorts of issues and responding responsively, so wondering…
No I did not. Code that sloppy shouldn’t appear in a security plugin.
He’s fast though. ~12 hours to release an updated version that fixes some of problems.
I applaud your efforts in getting the word out. Though honestly, there are repercussions to your actions.
Notification to the author and a reasonable period of time to respond would be the appropriate course of action before publishing– no matter how badly you feel the developer coded the app respectively.
I do not agree, I informed a plugin provider of an exploit and they did not deal with it for over a year. Publishing it adds the sense of urgency required. Also it is important that Wordfence users are not lulled into a false sense of security. A lot of people pay for this plugin and even the free users give their data to the Wordfence which are then sold a version that uses that data. That is fair enought but it means code has to be top notch. Thanks to author, hope you continue to keep wordfence secure by pointing out these errors..
I agree with Jim. You really should let the author know about security holes before you release a blog post. Short term it is a win to release bugs right away. You get to smack the author around a little and look smart doing it but long term when someone finds something that can’t be fixed quickly and releases it. There really just causing problems for the 2,986,426 people that have downloaded it not the author. At least give him or her a week (or even 24 hours) for the sake of the users that run it. No i don’t think you should wait very long. In this case the author was quick to fix it but i feel sorry for all the users that don’t understand php when there is a problem and there is no quick fix. It use to be that security was about protecting the users not slamming “sloppy” plugins. That sloppy plugin was still helping all the users that can’t code.
David just because one plugin or even hundreds of them doesn’t fix security bugs doesn’t mean that everyone of them doesn’t. For some reason I don’t think this author got 1,717 5 star reviews by not fixing any bugs. Why don’t you contact this author of this post I’m sure he would be happy to post the security bug you found. I would even post the security bug. I think 30 days with no response or fix would be more then reasonable but please don’t wait a year. The users need someone to speak up and to say something if the author won’t fix the security hole so they can move away from that plugin.
This is not some free open source plug-in, they want money for it, they sell it as being a professionally developed product. If the code is indeed this sloppy then the OP had every write to post this and I more than applaud him, I shame you for not applauding him!!!
WordFence is currently at 5.3.4 I assume that you have not discovered anything new
I haven’t looked in several months.
You seem to have a very good handle on this stuff, so here’s hoping you get a chance to have another look.
[WORDFENCE 5.3.4 –> lib/wfLog.php]
public function logHitOK(){
if(stristr($_SERVER[‘REQUEST_URI’], ‘wp-admin/admin-ajax.php’)){ return false; } //Don’t log wordpress ajax requests.
if(is_admin()){ return false; } //Don’t log admin pageviews
if(isset($_SERVER[‘HTTP_USER_AGENT’])){
if(preg_match(‘/WordPress\/’ . $this->wp_version . ‘/i’, $_SERVER[‘HTTP_USER_AGENT’])){ return false; } //Ignore requests generated by WP UA.
}
Our sites have been hammered by requests appended with admin-ajax, and after a month of fruitless searching I know why.
In general, I notify WP plugin developers of potential security breaches privately, but in this case, Voxel is correct. A security plugin should be written with security in mind. Certainly some holes are just hard to think of, but you would have thought that pentesting would have revealed them.
Also, one other major problem with Wordfence is that ‘live traffic logging’ is enabled by default.
This creates huge loads on servers running multiple sites with multiple Wordfence plugins. We had a high-load issue for months and months. We never thought it would have come from a security plugin. It was literally the last thing we looked at.
But, seriously, amazing how you deconstructed all this so easily.