shell bypass 403
<?php
/**
* @package admintools
* @copyright Copyright (c)2010-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Plugin\System\AdminTools\Utility;
defined('_JEXEC') or die;
use Joomla\Utilities\IpHelper;
/**
* IP Filtering utility class
*/
abstract class Filter
{
/**
* The valid subnet masks for IPv4 networks.
*
* Remember, we cannot have any arbitrary value as a subnet mask. The subnet mask is an unsigned long integer which,
* in binary representation, is a block of 1s followed by a block of 0s. This unsigned long integer converted to
* octets expressed in decimal and separated by dots is the subnet mask (‘netmask’). Therefore, it can have only
* one of 31 possible values, listed below.
*
* @var string[]
* @link https://en.wikipedia.org/wiki/Subnet#Determining_the_network_prefix
*/
private static $validNetmasks = [
'255.255.255.255',
'255.255.255.254',
'255.255.255.252',
'255.255.255.248',
'255.255.255.240',
'255.255.255.224',
'255.255.255.192',
'255.255.255.128',
'255.255.255.0',
'255.255.254.0',
'255.255.252.0',
'255.255.248.0',
'255.255.240.0',
'255.255.224.0',
'255.255.192.0',
'255.255.128.0',
'255.255.0.0',
'255.254.0.0',
'255.252.0.0',
'255.248.0.0',
'255.240.0.0',
'255.224.0.0',
'255.192.0.0',
'255.128.0.0',
'255.0.0.0',
'254.0.0.0',
'252.0.0.0',
'248.0.0.0',
'240.0.0.0',
'224.0.0.0',
'192.0.0.0',
'128.0.0.0',
];
/**
* Caches the IP arrays which were converted to IP ranges for faster reprocessing.
*
* @var array<string[]>
*/
private static $cachedRanges = [];
/** @var string The IP address of the current visitor */
protected static $ip = null;
/**
* Get the current visitor's IP address
*
* @return string
*/
public static function getIp()
{
if (is_null(static::$ip))
{
$ip = IpHelper::getIp();
static::setIp($ip);
}
return static::$ip;
}
/**
* Set the IP address of the current visitor (to be used in testing)
*
* @param string $ip
*
* @return void
*/
public static function setIp($ip)
{
static::$ip = $ip;
}
/**
* Convert an IPv4 or IPv6 expression into a normalised IPv6 binary string representation.
*
* @param string|null $ip The IP expression to normalise.
*
* @return string|null The normalised binary string; NULL if normalisation failed.
*/
public static function ipToNormalisedIPv6(?string $ip): ?string
{
$ip = trim($ip ?? '');
$binString = inet_pton($ip);
if (empty($ip) || empty($binString))
{
return null;
}
// Already an IPv6?
if (strlen($binString) === 16)
{
// PHP evaluates ::1.2.3.4 as ::0102:0304 when it should be ::ffff:0102:0304. Catch and fix that.
$mightBeIPv4 = substr($ip, 0, 2) === '::' && strpos($ip, '.', 2) !== false;
$isIPv4In6 = $mightBeIPv4
&& array_reduce(
array_slice(unpack('C*', $binString), 0, 12),
function (bool $carry, int $byte) {
return $carry && ($byte === 0);
},
true
);
if ($isIPv4In6)
{
return str_repeat(chr(0x00), 10) . str_repeat(chr(0xFF), 2) .
substr($binString, -4);
}
return $binString;
}
/**
* IPv4-in-IPv6 is ::ffff:0123:4567 where 0123:4567 is the IPv4 address.
*
* In practical terms it means that I can prefix the IPv4 expression with ten bytes 0x00 and two bytes 0xFF.
*/
return str_repeat(chr(0x00), 10) . str_repeat(chr(0xFF), 2) . $binString;
}
/**
* Resolve an Admin Tools IPv4 domain expression (e.g. `@example.com`) to its IPv4 address.
*
* If the domain has multiple A records only the contents of one record will be returned (OS choice!).
*
* @param string|null $expression The expression to resolve.
*
* @return string|null NULL if it's not an expression, or resolution failed.
*/
public static function resolveIPv4Domain(?string $expression): ?string
{
$expression = trim($expression ?? '');
if (empty($expression) || substr($expression, 0, 1) != '@')
{
return null;
}
/** @see https://secure.php.net/manual/en/function.gethostbyname.php */
putenv('RES_OPTIONS=retrans:1 retry:1 timeout:3 attempts:1');
$domain = substr($expression, 1);
$domain = rtrim($domain, '.') . '.';
$ip = gethostbyname($domain);
if ($ip == $domain)
{
return null;
}
return $ip;
}
/**
* Resolve an Admin Tools IPv6 domain expression (e.g. `#example.com`) to its IPv6 address.
*
* If the domain has multiple AAAA records only the contents of the first record communicated by the DNS server will
* be returned.
*
* @param string|null $expression The expression to resolve.
*
* @return string|null NULL if it's not an expression, or resolution failed.
*/
public static function resolveIPv6Domain(?string $expression): ?string
{
$expression = trim($expression ?? '');
if (empty($expression) || substr($expression, 0, 1) != '#')
{
return null;
}
$domain = substr($expression, 1);
$dns = dns_get_record($domain, DNS_AAAA);
foreach ($dns as $record)
{
if ($record['type'] === 'AAAA')
{
return $record['ipv6'];
}
}
return null;
}
/**
* Checks if the user's IP is contained in a list of IPs or IP expressions
*
* This code has been copied from FOF to lower the amount of dependencies required
*
* @param array|string $ipTable The list of IP expressions
* @param string $ip The user's IP address, leave empty / null to get the current IP address
*
* @return null|bool True if it's in the list, null if the filtering can't proceed
*/
public static function IPinList($ipTable = [], $ip = null)
{
// Sanity check
if (!function_exists('inet_pton'))
{
return false;
}
// No point proceeding with an empty IP list. DO NOT REMOVE. This checks the raw input.
if (empty($ipTable))
{
return false;
}
// Get our IP address
if (empty($ip))
{
$ip = static::getIp();
}
// If no IP address can be found, return false
if ($ip == '0.0.0.0' || empty($ip))
{
return false;
}
// Normalise the IP
$ip = self::ipToNormalisedIPv6($ip);
// Do not continue with an invalid IP address.
if ($ip === null)
{
return null;
}
// If the IP list is not an array, convert it to an array.
if (!is_array($ipTable))
{
if (strpos($ipTable, ',') !== false)
{
$ipTable = explode(',', $ipTable);
$ipTable = array_map(function ($x) {
return trim($x);
}, $ipTable);
}
else
{
$ipTable = trim($ipTable);
$ipTable = [$ipTable];
}
}
// Process the IP table and cache it.
$key = md5(serialize($ipTable));
self::$cachedRanges[$key] ??= array_filter(
array_map([self::class, 'expressionToRange'], $ipTable)
);
// No point proceeding with a now-empty IP list. DO NOT REMOVE. This checks the result of the conversion.
if (empty($ipTable))
{
return false;
}
foreach (self::$cachedRanges[$key] as $range)
{
[$from, $to] = $range;
if ($from > $to)
{
[$from, $to] = [$to, $from];
}
if (($ip >= $from) && ($ip <= $to))
{
return true;
}
}
return false;
}
/**
* Converts an IP or IP range expression into an array of starting and ending addresses in in_addr format.
*
* The expressions supported are:
* * `@example.com` Resolve a domain into a singular IPv4 address for exact IP matching.
* * `#example.com` Resolve a domain into a singular IPv6 address for exact IP matching.
* * `1.2.3.4` A single IPv4 address for exact IP matching.
* * `1.2.3.4/255.255.255.0` An IPv4 with a subnet mask (netmask).
* * `1.2.3.4/24` An IPv4 with a CIDR.
* * `1.2.3.` A partial IPv4. Existing octets become 255 and missing octets become 0 in the netmask.
* * `1.2.3.4-2.3.4.5` Any pair of IPv4 addresses separated by a dash is parsed as a range.
* * `::1` Any validly formatted IPv6 address for exact IP matching.
* * `2001::cafe:1-2001::fefe:ffff` Any pair of IPv6 addresses separated by a dash is parsed as a range.
* * `2001::cafe:1/64` Any validly formatted IPv6 address with a CIDR.
*
* Expressions are normalised to IPv6. Matching takes place in the IPv6 address space. This allows IPv4-in-IPv6
* encapsulation (i.e. ::1.2.3.4 or ::ffff:1.2.3.4 or ::ffff:0102:0304) to be processed correctly, e.g. on IPv6-only
* servers receiving tunneled IPv4 traffic.
*
* @param string|null $expression The expression to convert.
*
* @return string[]|null The [$from, $to] array, NULL if the expression is invalid.
*/
private static function expressionToRange(?string $expression): ?array
{
$expression = trim($expression ?? '');
if (empty($expression))
{
return null;
}
// Resolve IPv4 / IPv6 domain lookup expressions, if necessary.
$expression = self::resolveIPv4Domain($expression) ?? self::resolveIPv6Domain($expression) ?? $expression;
// IP Range
if (strpos($expression, '-') !== false)
{
[$from, $to] = explode('-', $expression, 2);
$from = self::ipToNormalisedIPv6($from);
$to = self::ipToNormalisedIPv6($to);
if ($from === null || $to === null)
{
return null;
}
return [$from, $to];
}
// Dangling dot (IPv4 only) – converted to a netmask, so it can be processed in the next block.
if (!self::isIPv6($expression) && substr($expression, -1) === '.')
{
$countChars = count_chars($expression, 1);
$dots = $countChars[ord('.')];
switch ($dots)
{
case 1:
$expression = "{$expression}0.0.0/255.0.0.0";
break;
case 2:
$expression = "{$expression}0.0/255.255.0.0";
break;
case 3:
$expression = "{$expression}0/255.255.255.0";
break;
default:
$expression .= '/255.255.255.255';
}
}
// If there's no slash, it's a single IP address.
if (strpos($expression, '/') === false)
{
$binaryIP = self::ipToNormalisedIPv6($expression);
if ($binaryIP === null)
{
return null;
}
return [$binaryIP, $binaryIP];
}
[$ip, $netmaskOrCidr] = explode('/', $expression, 2);
$notNormalisedIp = inet_pton($ip);
$ip = self::ipToNormalisedIPv6($ip);
if ($ip === null)
{
return null;
}
// IPv4 addresses are exactly 32 bit (4 bytes) long.
$isIPv4 = strlen($notNormalisedIp) === 4;
// Netmask expressions are non-integers. We will try to convert that into a CIDR.
if (!is_numeric($netmaskOrCidr))
{
// Netmasks are only defined for IPv4 addresses, which are 4 bytes long. Do we actually have one?
if (!$isIPv4)
{
return null;
}
// Is the string we have an actual netmask?
// -- normalise the netmask e.g. 255.00.000.0 => 255.0.0.0
$temp = self::ipToNormalisedIPv6($netmaskOrCidr);
if ($temp === null)
{
return null;
}
$normalisedNetmask = inet_ntop(substr($temp, -4));
// -- Check if the netmask is a valid one. Remember, we cannot have arbitrary values there.
/** @link https://en.wikipedia.org/wiki/Subnet#Subnet_host_count */
if (!in_array($normalisedNetmask, self::$validNetmasks))
{
return null;
}
// Convert the netmask into a long integer
$long = ip2long($netmaskOrCidr);
if ($long === false)
{
// Well, the netmask was invalid. Sorry!
return null;
}
// Convert the netmask to a CIDR and fall through.
$base = ip2long('255.255.255.255');
$netmaskOrCidr = 32 - (int) log(($long ^ $base) + 1, 2);
}
/**
* We have a CIDR.
*
* CIDR is an integer which effectively tells us how many bits to keep from the IP address.
*
* The way we handle it is to convert the IP to bits, keep the bits specified by the CIDR and then stuff
* the rest of the bits with zeroes to get the starting address, and then with ones to get the ending
* address in the address range.
*/
$bits = @intval($netmaskOrCidr);
// CIDR ranges are between 1 and 128 bits.
if ($bits < 1 || $bits > 128)
{
return null;
}
// We can't have CIDR wider than 32 bits on an IPv4 address!
if ($isIPv4 && $bits > 32)
{
return null;
}
// IPv4 needs to be padded by the 96 constant bits (The 0000:0000:0000:0000:0000:ffff: IPv4-in-Ipv6 prefix)
if ($isIPv4)
{
$bits += 96;
}
// Do all the fun bit math.
$keepBits = substr(self::inet_to_bits($ip), 0, $bits);
$fromBin = str_pad($keepBits, 128, '0', STR_PAD_RIGHT);
$toBin = str_pad($keepBits, 128, '1', STR_PAD_RIGHT);
return [self::bits_to_inet($fromBin), self::bits_to_inet($toBin)];
}
/**
* Is it an IPv6 IP address?
*
* @param string $ip An IPv4 or IPv6 address
*
* @return boolean True if it's IPv6
* @link https://ihateregex.io/expr/ipv6/
*/
private static function isIPv6($ip)
{
return preg_match('/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(::)?([0-9a-fA-F]{1,4}:){1,4}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/', $ip);
}
/**
* Converts an in_addr-format string representing an IPv4 or IPv6 address to a bits string.
*
* @param string $inet The in_addr representation of an IPv4 or IPv6 address (4 or 16 characters long).
*
* @return string The bits string (32 or 128 characters long).
*/
private static function inet_to_bits($inet)
{
if (strlen($inet) == 4)
{
$unpacked = unpack('C4', $inet);
}
else
{
$unpacked = unpack('C16', $inet);
}
$binaryip = '';
foreach ($unpacked as $byte)
{
$binaryip .= str_pad(decbin($byte), 8, '0', STR_PAD_LEFT);
}
return $binaryip;
}
/**
* Converts a bits string into an in_addr-format string which represents an IPv4 or IPv6 address.
*
* @param string $bits The bits string (32 or 128 characters long).
*
* @return string The in_addr representation of an IPv4 or IPv6 address (4 or 16 characters long).
*/
private static function bits_to_inet(string $bits): string
{
$ret = '';
$bytes = str_split($bits, 8);
foreach ($bytes as $byte)
{
$ret .= pack('C', bindec($byte));
}
return $ret;
}
}