CloudPanel Server Hack — How an Attacker Created an Admin Account
· 7 min read
I got a message from a client on a Monday morning: "There's a user in CloudPanel I didn't create." The user was called brian71, had full admin privileges, and had been created over the weekend. Nobody on the client's team had done it.
This is the kind of message that makes you put the coffee down and start pulling logs immediately.
Discovery and Initial Assessment
I logged into the CloudPanel server and confirmed the rogue account. The user brian71 had been created via clpctl user:add — CloudPanel's command-line management tool. This wasn't someone who'd brute-forced a WordPress login. They had shell-level access to the server and had used CloudPanel's own tooling to create a persistent admin account.
First priority: understand how they got in. Second priority: understand what else they did. Third priority: make sure they can't get back in.
Tracing the Attack Through Logs
I started with auth.log to find the session that created the account:
# Find when the brian71 user was created
grep -i "brian71" /var/log/auth.log
# Look for sudo commands around that timestamp
grep "sudo" /var/log/auth.log | grep "clpctl"
# Check for any SSH sessions during the window
grep "Accepted" /var/log/auth.log | grep "Aug 17"
The logs showed a sudo command executing clpctl user:add from a session that had authenticated via an existing service account. The interesting part was a base64-encoded command in the sudo log:
echo "Y2xwY3RsIHVzZXI6YWRkIC0tdXNlck5hbWU9YnJpYW43MSAtLXBhc3N3b3JkPSdSZWRhY3RlZCc=" | base64 -d
This decoded to the clpctl user:add command with credentials. Base64 encoding is a common technique to avoid simple log pattern matching — it won't fool anyone doing a real investigation, but it can slip past automated alerting that's only looking for plaintext command patterns.
I then checked the web server access logs to see if the initial entry was through a web vulnerability:
# Check for suspicious POST requests around the compromise window
grep "POST" /var/log/nginx/access.log | grep "Aug 17" | grep -v "wp-admin\|wp-cron\|wc-api"
# Look for requests to unusual paths
grep -E "(\.php\?|eval|base64|shell|cmd=)" /var/log/nginx/access.log | tail -50
Checking for Backdoors
With the entry point identified, I needed to determine the full extent of the compromise. Attackers who create admin accounts typically leave other persistence mechanisms too.
Webshell scan — I searched all web-accessible directories for common PHP backdoor patterns:
# Search for common webshell indicators
find /home/*/htdocs -name "*.php" -exec grep -l "eval\s*(" {} \;
find /home/*/htdocs -name "*.php" -exec grep -l "base64_decode\s*(" {} \;
find /home/*/htdocs -name "*.php" -exec grep -l "shell_exec\|passthru\|system\s*(" {} \;
# Check for recently modified PHP files (last 7 days)
find /home/*/htdocs -name "*.php" -mtime -7 -ls
# Look for PHP files in upload directories (should only be images)
find /home/*/htdocs/wp-content/uploads -name "*.php" -ls
SSH key check — I audited all authorized_keys files on the server:
# Find all authorized_keys files
find / -name "authorized_keys" -ls 2>/dev/null
# Check for unexpected SSH keys
for user_dir in /home/*; do
if [ -f "$user_dir/.ssh/authorized_keys" ]; then
echo "=== $user_dir ==="
cat "$user_dir/.ssh/authorized_keys"
fi
done
# Also check root
cat /root/.ssh/authorized_keys
Cron jobs — Attackers love cron for persistence:
# Check all user crontabs
for user in $(cut -f1 -d: /etc/passwd); do
crontab -u "$user" -l 2>/dev/null && echo "^^^ $user ^^^"
done
# Check system cron directories
ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/
Running processes — Any unfamiliar processes or outbound connections:
# Check for suspicious outbound connections
ss -tnp | grep ESTABLISHED
# Look for unusual processes
ps auxf | grep -v "\[" | sort -k3 -rn | head -20
Immediate Response
Once I had a clear picture, I moved fast:
- Removed the rogue account — deleted
brian71viaclpctl user:deleteand confirmed removal - Killed active sessions — terminated all SSH sessions and forced re-authentication
- Rotated all credentials — CloudPanel admin password, SSH keys for all legitimate users, database passwords, and WordPress admin credentials for every site on the server
- Removed an unauthorized SSH key I found in one of the site user directories
- Patched the entry point — the service account that had been leveraged had an unnecessarily broad sudo configuration, which I tightened to only the specific commands it needed
Hardening CloudPanel After the Incident
This incident exposed several weaknesses in the default CloudPanel configuration that I now address on every server I manage as part of my server management service:
Restrict CloudPanel access by IP. The CloudPanel admin interface was accessible from any IP address. I configured nginx to restrict access to specific IP ranges and added Cloudflare Access as an additional authentication layer.
Limit clpctl sudo access. By default, site users in CloudPanel can have broad sudo permissions. I reviewed and tightened these to the minimum required commands.
Enable SSH key-only authentication. Password-based SSH was still enabled. I disabled it and ensured all legitimate access used key-based authentication with strong key types (Ed25519).
Set up file integrity monitoring. I deployed a simple script that checksums critical system files and CloudPanel configuration, alerting on any changes. This would have caught the user:add command within minutes rather than waiting for a human to notice.
Configure centralised logging. Server logs were only stored locally, meaning an attacker with root access could have tampered with them. I set up remote log forwarding so there's always an off-server copy.
Regular access audits. I now run monthly reviews of all user accounts, SSH keys, and sudo configurations on every managed server. It takes ten minutes and catches configuration drift before it becomes a vulnerability.
The full investigation and hardening took about four hours. The client's sites were never directly compromised — the attacker had created the admin account but hadn't yet used it to access any WordPress installations. We caught it in time, but only because someone happened to log into CloudPanel that Monday morning.
Automated alerting on user creation events would have caught this instantly. That's now in place, along with the rest of the security monitoring stack I deploy on every server I manage.
Need help with something similar? Check out my maintenance plans.
