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?