Solved: cPanel’s CPHULK, CloudFlare and X-Forwarded-For

JeffTechnical Articles & Notes

At the time of posting, there is a feature request on the cPanel website which, if approved, would enable the admin of a cPanel server to have CPHULK act on the content of the X-Forwarded-For header in HTTP requests.

https://features.cpanel.net/topic/have-option-for-cphulkd-to-action-on-x-forwarded-for

cPanel feel this is a bad idea because the X-Forwarded-For header is easily spoofed. Which could lead to all sorts of problems.

This is a big problem for server admins because if someone attempts to brute-force a cPanel account via a domain name which is pointed at the server via CloudFlare instead of by direct IP address, their attempts will never be blocked.

I’ve coded up a solution which I think fixes cPanel’s concerns.

I’ve removed all CloudFlare IP ranges from the CPHULK whitelist so that all misbehaving IPs are reported to my script (which I’ve set in the optional ‘Command to Run When an IP Address Triggers Brute Force Protection’ and ‘Command to Run When an IP Address Triggers a One-Day Block’ fields). It then will ONLY act upon the X-Forwarded-For IP address IF AND ONLY IF the originating IP address is in my script’s IP whitelist ranges.

So my solution is to act only on X-Forwarded-For if the originating IP address is one of CloudFlare’s.

(NOTE: Make sure not to check the CPHULK option ‘Block IP addresses at the firewall level if they trigger brute force protection’)

The X-Forwarded-For IP can be found with a regular expression and a scan of the /usr/local/cpanel/logs/access_log file – looking for a matching timestamp and originating IP.

For belt-and-braces, my script blocks the ‘real’ IP address at CloudFlare (using CloudFlare’s API) and on the local firewall.

In the temporary and permanent CPHULK options to run a command I have:

sh /my-file-path/cphulkblock.sh %remote_ip% %user% %logintime%

In /my-file-path/cphulkblock.sh I have:

#!/bin/bash
ip=$(printf "%q" $1)
user=$(printf "%q" $2)
tim=$(printf "%q" $3)
csf -d $ip 0 0 0 0 cphulk 
php -q /my-file-path/cfblock.php $ip $user $tim cphulk

In /my-file-path/cfblock.php I have:

$authemail = "YOUR CLOUDFLARE EMAIL ADDRESS HERE";
$authkey   = "YOUR CLOUDFLARE AUTH KEY HERE";

// Script containing whitelisted IP ranges and function to comapare
// an IP address with them (ip_check())
include('ip_check.php');

$theip = $argv[1];

if (ip_check($theip))
{
	// IP IS WHITELISTED!
	// We need to form up a matching log line and find it
	// if we can, with X-Forwarded-For IP details
	/////////////////////////////////////////////////
	$ip = $theip;
	$un = $argv[2];
	$ts = $argv[3];

	if (($un <> '') and ($ts > 0))
	{
		date_default_timezone_set('UTC');
		$ll = "$ip - $un \[" . date('m/d/Y:H:i:',$ts);

		// Can we find a log line matching this in access_log?
		$pattern = "|^".$ll."|i";
		$result  = preg_grep($pattern, file('/usr/local/cpanel/logs/access_log'));
		if (count($result) > 0)
		{
			$res = $result[array_keys($result)[0]];
			// Can we find the dodgy IP?
			if (preg_match("/X-Forwarded-For:\s(\d+\.\d+\.\d+.\d+)\D/",$res,$matches))
			{
				$badip = $matches[1];
				if ($badip <> $ip)
				{
					// Is THIS also a whitelisted IP?
					if (ip_check($badip))
					{
						// YES DO NOT BLOCK
						$theip = '';
					}
					else
					{
						// Proceed to block this IP then!
						$theip = $badip;

						// Tell someone
						mail($authemail, 'found CF X-Forwarded-For ip '.$theip, "Found above IP in access_log using match $pattern");
					}
				}
			}
		}
	}
}

if (strlen($theip))
{
	$ch = curl_init("https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules");
	curl_setopt($ch, CURLOPT_HTTPHEADER, array(
	    'X-Auth-Email: '.$authemail,
	    'X-Auth-Key: '.$authkey,
	    'Content-Type: application/json'
	    ));
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

	// include the current date and time in the notes...
	$dt = date('r');

	$data_string = '{"mode":"block","configuration":{"target":"ip","value":"'.$theip.'"},"notes":"cphulk - '.$dt.'"}';

	curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
	curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);

	$response = curl_exec($ch);
	curl_close($ch);
}

I’m not at liberty to share the code in ip_check.php but for guidance on creating your own, this is a useful resource:

https://github.com/cloudflare/CloudFlare-Tools/blob/master/cloudflare/ip_in_range.php