DOS through IP spoofing – (Jetpack <= 11.3.1)

| in


Affected pluginJetpack and Jetpack Protect
Active installs5+ million
Vulnerable version<= 11.3.1
Audited version11.3.1
Fully patched version
Recommended remediationEnsure nobody can directly connect to your server if behind a reverse proxy. Use something like Cloudflares authenticated origin pull or disable Jetpack protect.

Description


Jetpack is susceptible to IP spoofing during login rate limiting, which an attacker can abuse to prevent legitimate users or a site’s reverse proxy from making requests to the wp-login.php endpoint.

Proof of concept


Jetpack’s protect module uses the “Jetpack_Protect_Module::jetpack_protect_get_ip” to access the current IP address in multiple security-related contexts like login throttling and banning of IPs.

Jetpack handles IP detection better than most WordPress plugins, but it does not cover all edge cases. Furthermore, implementing IP detection at the plugin level 100% correctly is conceptually impossible.

function jetpack_protect_get_ip() {
   $trusted_header_data = get_site_option( 'trusted_ip_header' );
   if ( isset( $trusted_header_data->trusted_header ) && isset( $_SERVER[ $trusted_header_data->trusted_header ] ) ) {
      $ip            = wp_unslash( $_SERVER[ $trusted_header_data->trusted_header ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- jetpack_clean_ip does it below.
      $segments      = $trusted_header_data->segments;
      $reverse_order = $trusted_header_data->reverse;
   } else {
      $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- jetpack_clean_ip does it below.
   }
   if ( ! $ip ) {
      return false;
   }
   $ips = explode( ',', $ip );
   if ( ! isset( $segments ) || ! $segments ) {
      $segments = 1;
   }
   if ( isset( $reverse_order ) && $reverse_order ) {
      $ips = array_reverse( $ips );
   }
   $ip_count = count( $ips );
   if ( 1 === $ip_count ) {
      return jetpack_clean_ip( $ips[0] );
   } elseif ( $ip_count >= $segments ) {
      $the_one = $ip_count - $segments;
      return jetpack_clean_ip( $ips[ $the_one ] );
   } else {
      return jetpack_clean_ip( isset( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- jetpack_clean_ip does it.
   }
}

The big question here is:

What is the value of $trusted_header_data?

The only place the plugin updates the “trusted_ip_header” option is inside the “maybe_update_headers” function, which retrieves this value by making an HTTP request to one of Jetpack’s APIs.

The response looks something like this:

{
  "trusted-header": "HTTP_X_FORWARDED_FOR",
  "segments": 1,
  "reverse": true
}

The protection against IP spoofing relies on the premise that Jetpack’s API will always find the optimal strategy for determining the IP address.

Likewise, the determined strategy must always be up to date and valid for every request.

There are at least two scenarios where this premise does not hold.

Given that Jetpack’s user base is in the millions, there will be a sizeable number of sites affected by either one.

Scenario #1: A network architecture change

The entire protection breaks down when a site owner changes his hosting provider or if the current hosting provider makes a change to his network architecture.

Suppose the current host of the site owner uses a stack consisting of:

AWS LB ==> Varnish ==> NGINX ==> PHP-fpm.

Jetpack will configure itself to get the “real” IP from the third rightmost IP value in the “X-Forwarded-For” (XXF) header. If an attacker sends a request with a spoofed IP address to the AWS LB, the final header at the application level will look like this:

X-Forwarded-For: <spoofed-ip> <real-ip> <varnish-ip> <aws-load-balancer-ip>

==> “Jetpack_Protect_Module::jetpack_protect_get_ip” will correctly use the value of <real-ip>, since it’s the third right-most IP address in the XXF header.

The current host now changes his (internal) network architecture and removes the Varnish reverse proxy.

AWS LB ==> NGINX ==> PHP-fpm.

Jetpack does not know about this, so it’s still configured to use the third rightmost IP address in the XFF header. If an attacker now sends a request with a spoofed IP to the AWS LB, the XFF header will now look like this at the application level:

X-Forwarded-For: <spoofed-ip> <real-ip> <aws-load-balancer-ip>

==> “Jetpack_Protect_Module::jetpack_protect_get_ipwill now use an attacker-controlled IP since the <spoofed-ip> is the third right-most.

The same issue occurs if the site owner moves to a hosting provider that uses a different header to determine IPs altogether.

Scenario #2: Server behind a reverse proxy and directly connectable

This attack scenario is not covered by the current implementation.
It is described in great detail here.

Generally speaking, if your server is behind one or more reverse proxies, there are one or more rightmost IPs in the XFF header that you can trust. The “rightmost-ish” algorithm is predicated on that. But if your server can also be connected to directly from the internet, that is no longer true.

With some experimentation, an attacker can craft an XFF header to look exactly like the one you expect from your reverse proxy:

1. Attacker gets her IP limited/blocked by your server.

2. Attacker crafts XFF header so that the rightmost of it has different IPs in the private space, and different counts of those IPs.

3. Continue until the limit/block unexpectedly disappears

Consider the following scenario. Almost all WordPress hosts with Cloudflare integrations do not use authenticated original pulls.

Consequently, the only thing stopping an attacker from making direct requests to the target server is discovering its IP address:

If Jetpack detects a site using Cloudflare, it will read the IP address from the “CF-Connecting-IP” header, which is 100% safe since Cloudflare will remove this header if an attacker already sets it. Therefore, it cannot be spoofed.

However, this only holds for requests that originate from Cloudflare.

Once an attacker finds out the direct IP address of the target website, he can set the “CF-Connecting-IP” header to arbitrary values.

==> “Jetpack_Protect_Module::jetpack_protect_get_ipwill again return an attacker-controlled IP.

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 13, 2022
First ResponseSeptember 16, 2022
Fully patched at
Publicly disclosedApril 24, 2023

Miscellaneous


  • The vendor did not consider this a security issue that required attention.

Leave a Reply

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