WAF bypass through IP spoofing – (Jetpack <= 11.3.1)

| in


Affected pluginJetpack
Active installs5+ million
Vulnerable version<= 11.3.1
Audited version11.3.1
Fully patched version
Recommended remediationNever configure Jetpack’s WAF to retrieve the IP from anything but REMOTE_ADRR.

Description


Jetpack contains a currently not exploitable security issue that allows an attacker to bypass all WAF rules.

Proof of concept


The below code runs every time before Jetpack bootstraps its Web Application Firewall (WAF).

if ( require('ABSPATH/wp-content/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/../rules/allow-ip.php') ) { return; }
if ( require('ABSPATH/wp-content/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/../rules/block-ip.php') ) { return $waf->block('block', -1, 'ip block list'); }

The two conditional checks will short-circuit the entire WAF logic if anyone returns “(bool) true.”

The PHP code in the “allow-ip.php” files is the following:

// EDITOR: $waf_allow_list is an array of IPs stored in the database
return $waf->is_ip_in_array( $waf_allow_list );

The PHP code in the “block-ip.php” file is the following:

// EDITOR: $waf_block_list is an array of IPs stored in the database
return $waf->is_ip_in_array( $waf_block_list );

Jetpack generates both of those files automatically once the plugin is activated.

“$waf” is an instance of Automattic\Jetpack\Waf\Waf_Runtime class, which contains the following “is_ip_in_array” method.

/**
 * Verifies is ip from request is in an array.
 *
 * @param array $array Array to verify ip against.
 */
public function is_ip_in_array( $array ) {
   $request = new Waf_Request();
   $real_ip = $request->get_real_user_ip_address();
   return in_array( $real_ip, $array, true );
}

If an attacker can manipulate the return value of “Waf_Request::get_real_user_ip_address,” he will be able to either:

  • First, bypass the entire WAF by spoofing his IP address to one in the allowlist.
  • Bypass the denylist by spoofing his IP address to one that is not banned.
public function get_real_user_ip_address() {
   $remote_addr = ! empty( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
   if ( in_array( $remote_addr, $this->trusted_proxies, true ) ) {
      $ip_by_header = $this->get_ip_by_header( array_merge( $this->trusted_headers, array( 'REMOTE_ADDR' ) ) );
      if ( ! empty( $ip_by_header ) ) {
         return $ip_by_header;
      }
   }
   return $remote_addr;
}

This is possible IF Jetpack calls the “Waf_Request::get_ip_by_header” method.

Jetpack will call this method if the following conditions are met (Lines 3-7 above):

  • $this->trusted_headers is not an empty array.
  • $this->trusted_proxies is not an empty array.

Both of these class properties are empty by default.

/**
 * Trusted proxies.
 *
 * @var array List of trusted proxy IP addresses.
 */
private $trusted_proxies = array();
/**
 * Trusted headers.
 *
 * @var array List of headers to trust from the trusted proxies.
 */
private $trusted_headers = array();
/**
 * Sets the list of IP addresses for the proxies to trust. Trusted headers will only be accepted as the
 * user IP address from these IP adresses.
 *
 * Popular choices include:
 * - 192.168.0.1
 * - 10.0.0.1
 *
 * @param array $proxies List of proxy IP addresses.
 * @return void
 */
public function set_trusted_proxies( $proxies ) {
   $this->trusted_proxies = (array) $proxies;
}
/**
 * Sets the list of headers to be trusted from the proxies. These headers will only be taken into account
 * if the request comes from a trusted proxy as configured with set_trusted_proxies().
 *
 * Popular choices include:
 * - HTTP_CLIENT_IP
 * - HTTP_X_FORWARDED_FOR
 * - HTTP_X_FORWARDED
 * - HTTP_X_CLUSTER_CLIENT_IP
 * - HTTP_FORWARDED_FOR
 * - HTTP_FORWARDED
 *
 * @param array $headers List of HTTP header strings.
 * @return void
 */
public function set_trusted_headers( $headers ) {
   $this->trusted_headers = (array) $headers;
}

Jetpack currently has no way to configure these settings in the UI, which is the only reason this vulnerability is not exploitable.

As Jetpack builds a UI around this already implemented configuration at the code level, this vulnerability becomes exploitable as Jetpack will then call the vulnerable “Waf_Request::get_ip_by_header” method.

private function get_ip_by_header( $headers ) {
   foreach ( $headers as $key ) {
      if ( isset( $_SERVER[ $key ] ) ) {
         foreach ( explode( ',', wp_unslash( $_SERVER[ $key ] ) ) as $ip ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- filter_var is applied below.
            $ip = trim( $ip );
            if ( filter_var( $ip, FILTER_VALIDATE_IP ) !== false ) {
               return $ip;
            }
         }
      }
   }
   return null;
}

This IP detection method makes the common mistake of always reading the LEFTMOST IP address if a header contains multiple IP addresses.

Suppose the target site is behind a reverse proxy that uses the X-Forwarded-For header.

An attacker now sends a request to the reverse proxy that already contains the following header:

X-Forwarded-For: <spoofed-ip>

As per the HTTP spec, the reverse proxy will now send the following header to the application:

X-Forwarded-For: <spoofed-ip> <real-ip>

Thus, Waf_Request::get_ip_by_header will always return an attacker-controlled IP address.

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 needed immediate attention.
  • A plugin with the installation count of Jetpack must aim for zero security vulnerabilities (including the ones that are not currently exploitable).

Leave a Reply

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