Category: Cpanel
Security Hole Cpanel – Wp-tool-kit: Deeper Look…🤦♂️
I run security audits regularly. I’ve seen misconfigurations, oversights, and the occasional lazy shortcut. What I found in cPanel’s WordPress Toolkit is unbelievable…
This doesn’t appear to be a bug. This is a deliberate architectural decision that gives unauditable code unrestricted root access to your server. By default. Without your consent. 😮🤦♂️
Millions of production servers are running this right now.
Finding #1: Passwordless Root Access — Deployed Automatically
Open this file on any cPanel server running WordPress Toolkit:
cat /etc/sudoers.d/48-wp-toolkit
Here’s what you’ll find:
wp-toolkit ALL=(ALL) NOPASSWD:ALL
Defaults:wp-toolkit secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults:wp-toolkit !requiretty
NOPASSWD:ALL
The wp-toolkit user can execute any command as root without a password. No restrictions. No whitelisting. Complete access to everything.
You didn’t enable this. You weren’t asked. It’s baked into the RPM install script:
rpm -q --scripts wp-toolkit-cpanel 2>/dev/null | grep -A 20 "preinstall scriptlet"
Every time WP Toolkit is installed or updated, this sudoers file gets created. Automatically. Silently.
Finding #2: It’s Actively Executing Root Commands
This isn’t sitting dormant. It’s running. Right now. On your server.
grep wp-toolkit /var/log/secure | tail -20
Here’s what I found in logs that made me dig deeper….
Feb 28 12:11:17 sudo[1911429]: wp-toolkit : USER=root ; COMMAND=/bin/cat /usr/local/cpanel/version
Feb 28 12:11:17 sudo[1911433]: wp-toolkit : USER=root ; COMMAND=/bin/sh -c 'whmapi1 get_domain_info --output=json'
Feb 28 12:11:18 sudo[1911442]: wp-toolkit : USER=root ; COMMAND=/bin/sh -c 'whmapi1 listaccts --output=json'
Look at that pattern: /bin/sh -c '...'
Arbitrary shell commands. As root. Constant execution.
Finding #3: You Cannot Audit What It’s Doing
I wanted to see what these scripts actually do. Here they are:
ls /usr/local/cpanel/3rdparty/wp-toolkit/scripts/
cli-runner.php
execute-background-task.php
read-files.php
write-files.php
transfer-files.php
Read those filenames again:
read-files.php— reads files as rootwrite-files.php— writes files as roottransfer-files.php— moves files as rootexecute-background-task.php— executes tasks as root
So let’s look at the source code:
file /usr/local/cpanel/3rdparty/wp-toolkit/scripts/*.php
cli-runner.php: data
execute-background-task.php: data
read-files.php: data
write-files.php: data
transfer-files.php: data
They’re not identified as PHP files. They’re data.
Because they’re ionCube encoded:
head -5 /usr/local/cpanel/3rdparty/wp-toolkit/scripts/cli-runner.php
<?php
// Copyright 1999-2025. Plesk International GmbH. All rights reserved.
// PLESK://PP.2500101/C4OLIU+C...
@__sw_loader_pragma__('PLESK_18');
Binary encoded. Obfuscated. The source code is hidden.
You cannot read what these scripts do. You cannot audit them for vulnerabilities. You cannot verify they’re secure.
But they have root access to your entire server.
Finding #4: This Is Official Code — Verified and Signed
I wanted to be absolutely sure this wasn’t some compromise or modification. So I verified it:
rpm -qi wp-toolkit-cpanel | grep -E "Signature|Vendor"
Signature : RSA/SHA512, Wed 14 Jan 2026 05:56:56 PM UTC, Key ID ba338aa6d9170f80
Digitally signed by cPanel. Official package.
rpm -V wp-toolkit-cpanel 2>&1 | head -10
All scripts match the official package. No modifications. No tampering.
The script headers explicitly state:
// Copyright 1999-2025. Plesk International GmbH. All rights reserved.
// This is part of Plesk distribution.
@__sw_loader_pragma__('PLESK_18');
This is Plesk’s WordPress Toolkit, distributed through cPanel’s official repository, digitally signed, running on millions of servers worldwide.
Finding #5: It Restores Itself… Every Night 🤦♂️
So I removed the sudoers file. Problem solved, right?
Nope.
There’s a cron job:
cat /etc/cron.d/wp-toolkit-update
This runs daily at 1 AM (with random delay) and executes:
yum -y update wp-toolkit-cpanel
When the package updates, the preinstall script runs. The preinstall script recreates /etc/sudoers.d/48-wp-toolkit.
Your fix gets silently undone. Every night. Automatically.
So removing the sudoers file alone doesn’t work. You have to disable the cron too, or you’ll wake up tomorrow with the same problem.
So….
cPanel ships WordPress Toolkit with:
| What They Ship | What It Means |
|---|---|
NOPASSWD:ALL sudo access |
Unrestricted root access, no authentication |
| Deployed automatically | No consent, no warning, no opt-in |
| ionCube-encoded scripts | Source code hidden, cannot be audited |
| Scripts that read/write/execute | Complete filesystem and command access |
| Digitally signed official package | This is intentional, not a compromise? |
| Nightly auto-update cron | Restores sudo access if you remove it |
| No security scanner detection | Flying under the radar on millions of servers |
This is a “trust us” security model:
- “Trust us with passwordless root access”
- “Trust us with code you can’t read”
- “Trust us that we got it right”
- “Trust us that attackers won’t find a way in”
On production servers. Hosting customer data. Running businesses.
The Attack Path
This is straightforward:
- Any vulnerability in WP Toolkit that allows command injection
- Payload reaches one of the encoded PHP scripts
- Script executes as
wp-toolkituser - User runs
sudo— no password needed - Complete server compromise
And because the scripts are encoded, you will never see the vulnerability coming. You cannot audit code you cannot read.
Check Your Server Right Now
# Check if the sudoers file exists
cat /etc/sudoers.d/48-wp-toolkit
# Check if auto-update cron is enabled
cat /etc/cron.d/wp-toolkit-update
# Verify scripts are encoded
file /usr/local/cpanel/3rdparty/wp-toolkit/scripts/*.php
# See what root commands are being executed
grep wp-toolkit /var/log/secure | grep COMMAND | tail -20
# Verify this is the official signed package (not tampered)
rpm -qi wp-toolkit-cpanel | grep -E "Signature|Vendor"
# Confirm scripts match official package
rpm -V wp-toolkit-cpanel 2>&1 | head -10
How to Fix It
Important: You need to do BOTH steps. Removing the sudoers file alone doesn’t work — the nightly cron will recreate it.
Step 1: Disable the Auto-Update Cron (Do This First)
# Disable the nightly auto-update cron
mv /etc/cron.d/wp-toolkit-update /etc/cron.d/wp-toolkit-update.disabled
# Verify it's disabled
ls -la /etc/cron.d/wp-toolkit-update 2>/dev/null || echo "✓ Auto-update disabled"
Step 2: Remove or Harden the Sudoers File
Option A: Remove it completely (Recommended)
rm /etc/sudoers.d/48-wp-toolkit
Most WordPress management doesn’t require root. If something specific breaks, address it then with a scoped solution. The risk is not worth the convenience.
Option B: Whitelist specific commands (Advanced)
If you need WP Toolkit automation, replace blanket access with specific commands:
cat << EOF > /etc/sudoers.d/48-wp-toolkit
# WP Toolkit - hardened configuration
wp-toolkit ALL=(ALL) NOPASSWD: /usr/local/cpanel/3rdparty/bin/wp
wp-toolkit ALL=(ALL) NOPASSWD: /bin/chown
wp-toolkit ALL=(ALL) NOPASSWD: /bin/chmod
Defaults:wp-toolkit secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults:wp-toolkit !requiretty
EOF
Always validate:
visudo -c -f /etc/sudoers.d/48-wp-toolkit
The Bottom Line
Plesk and cPanel are officially shipping ionCube-encoded PHP scripts that execute as root with NOPASSWD:ALL sudo access. The package is digitally signed. The scripts are verified. This is intentional. You cannot audit what these scripts do. You cannot review the source code. You cannot verify their security. Yet they have root over your server. They could covertly do anything….
It would seem this is deployed by default. On every cPanel server running WordPress Toolkit. No security scanner flags it. Not even a “oh hey, this could be a problem for you but this is how we did it”…
Check yours today.
Security hole: WP Toolkit Deploys Wide Open Sudoers by Default – Here’s How to Fix It
If you’re running cPanel, you’re almost certainly running WP Toolkit. It’s installed by default on cPanel servers and is the standard tool for managing WordPress installations.
Here’s the problem: WP Toolkit deploys with a sudoers configuration that gives it passwordless root access to your entire server. This isn’t something you enabled. It’s there out of the box.
That means every cPanel server running WP Toolkit – and there are millions of them – has this configuration sitting in /etc/sudoers.d/48-wp-toolkit right now.
Don’t Take My Word For It
This isn’t a misconfiguration. It’s baked into the WP Toolkit package itself. You can verify this by checking the RPM preinstall scriptlet:
rpm -q --scripts wp-toolkit-cpanel 2>/dev/null | grep -A 20 "preinstall scriptlet"
Here’s what it shows:
preinstall scriptlet (using /bin/sh):
# Check that "wp-toolkit" user exist and create in case of absence
/usr/bin/getent passwd wp-toolkit >/dev/null 2>&1 || /usr/sbin/useradd -r -s /bin/false -d /usr/local/cpanel/3rdparty/wp-toolkit/var wp-toolkit
# If wp-toolkit/var catalog exists, set its owner. If it doesn't exist — no problem
chown -R wp-toolkit:wp-toolkit /usr/local/cpanel/3rdparty/wp-toolkit/var 2>/dev/null
# Allow sudo without password prompt
cat << EOF > /etc/sudoers.d/48-wp-toolkit
# Rules for wp-toolkit system user.
# WPT needs ability to impersonate other system users to perform WordPress management and maintenance
# tasks under the system users who own the affected WordPress installations.
wp-toolkit ALL=(ALL) NOPASSWD:ALL
Defaults:wp-toolkit secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults:wp-toolkit !requiretty
EOF
# Verify that sudo works, check performed in non-interactive mode to avoid password prompts
su -s /bin/bash wp-toolkit -c 'sudo -n -l'
Every time WP Toolkit is installed or updated, this script runs and creates that sudoers file. It’s intentional. It’s documented in their own comments: “WPT needs ability to impersonate other system users.”
The problem is what they gave themselves to achieve that: NOPASSWD:ALL.
The Default Configuration
WP Toolkit creates this sudoers entry out of the box:
wp-toolkit ALL=(ALL) NOPASSWD:ALL
Defaults:wp-toolkit secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults:wp-toolkit !requiretty
That’s NOPASSWD:ALL. The wp-toolkit user can execute any command as root without a password.
Why This Is Dangerous
This is a classic privilege escalation vector:
- WordPress gets compromised – happens constantly via vulnerable plugins, themes, or weak credentials
- Attacker gains access to the wp-toolkit user or can execute commands through it
- Instant root – no password required, no barriers, game over
Your entire server is one WordPress vulnerability away from full compromise.
Option 1: Just Disable It (Recommended for Most Users)
If you’re not a sysadmin or you don’t rely heavily on WP Toolkit’s advanced features, the safest approach is to remove it entirely:
rm /etc/sudoers.d/48-wp-toolkit
That’s it. Done. Will WP Toolkit break? Probably not. Most day-to-day WordPress management doesn’t need root access. If something specific stops working, you can troubleshoot then. The alternative – leaving a passwordless root backdoor on your server – is not worth the convenience.
Option 2: Harden It (For Advanced Users)
If you’re comfortable with Linux administration and need WP Toolkit’s automation features, you can lock it down to specific commands instead of removing it completely.
Step 1: Audit what WP Toolkit actually needs
Use auditd to track what commands it runs:
# Add audit rule for commands run by wp-toolkit
auditctl -a always,exit -F arch=b64 -F euid=0 -F auid=$(id -u wp-toolkit) -S execve -k wp-toolkit-cmds
Run your normal WP Toolkit operations for a few days, then review:
ausearch -k wp-toolkit-cmds | aureport -x --summary
Step 2: Replace with whitelisted commands
Once you know what it actually runs, create a hardened sudoers file:
cat << EOF > /etc/sudoers.d/48-wp-toolkit
# WP Toolkit - hardened sudoers
# Only allow specific commands required for WordPress management
wp-toolkit ALL=(ALL) NOPASSWD: /usr/local/cpanel/3rdparty/bin/wp
wp-toolkit ALL=(ALL) NOPASSWD: /bin/chown
wp-toolkit ALL=(ALL) NOPASSWD: /bin/chmod
wp-toolkit ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart httpd
wp-toolkit ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart php-fpm
Defaults:wp-toolkit secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults:wp-toolkit !requiretty
EOF
Adjust the command list based on your audit findings. The principle: whitelist only what’s needed.
Step 3: Validate your sudoers
Always validate after editing – a syntax error in sudoers can lock you out of sudo entirely:
visudo -c -f /etc/sudoers.d/48-wp-toolkit
Check Your Server Now
cat /etc/sudoers.d/48-wp-toolkit
If you see NOPASSWD:ALL, take action. Either remove the file or harden it. Don’t leave it as-is.
The Bottom Line
Default configurations prioritise convenience over security. In this case, that convenience is a passwordless root backdoor sitting on your server. Most users: just remove it. Advanced users who need the functionality: audit, whitelist, and lock it down. Either way, don’t ignore it.
How to renable the tempurl in latest Cpanel
As some of you have noticed the new cpanel by default has a bunch of new default settings that nobody likes.
FTPserver is not configured out of the box.
TempURL is disabled for security reasons. Under certain conditions, a user can attack another user’s account if they access a malicious script through a mod_userdir URL.
So they removed it by default.
They did not provide instructions for people who need it. You can easily enable it BUT php wont work on the temp url unless you do the following
remove below: and by remove I mean you need to recompile easyapache 4 the following changes.
mod_ruid2
mod_passenger
mod_mpm_itk
mod_proxy_fcgi
mod_fcgid
Install
Mod_suexec
mod_suphp
Then go into Apache_mode_user dir tweak and enable it and exclude default host only.
It wont save the setting in the portal, but the configuration is updated. If you go back and look it will look like the settings didnt take. Looks like a bug in cpanel they need to fix on their front end.
Then PHP will work again on the tempurl.
How to do a full volume heal with glusterfs
How to fix a split-brain fully
If you nodes get out of sync and you know which node is the correct one.
So if you want node 2 to match Node 1
Follow the following setps:
- gluster volume stop $volumename
- /etc/init.d/glusterfsd stop
- rm -rf /mnt/lv_glusterfs/brick/*
- /etc/init.d/glusterfsd start
- “gluster volume start $volumename force”
- “gluster volume heal $volumename full”
You should see a successful output, and you will start to see the “/mnt/lv_glusterfs/brick/” directory now match node a
Finally you can run.
- gluster volume heal $volumename info split-brain (this will show if there are any splitbrains)
- gluster volume heal $volumename info heal-failed (this will show you files that failed the heal)
Cheers
How to upgrade mysql 5.1 to 5.6 with WHM doing master-slave
How to upgrade MYSQL in a production environment with WHM
Okay, so if you have a master slave database setup with large innodb and myisam, you probably want to upgrade to mysql 5.6. The performance tweaks make a difference especially with utilizing multicores.
Most of the time Cpanel is really good at click upgrade and it works, however with mysql if you’re running a more complex setup, then simply clicking upgrade in cpanel for mysql isn’t going to do the trick. I’ve outlined the process below to help anyone else trying to do this.
- Making a backup of the database using Percona and mysqldump
- The first thing you need to do is make a backup of everything, since we have large innodb and myisam db’s, using mysqldump can be slow.
- Using percona this will backup everything
i. Innobackupex /directory you want everything to backed up to (this will be uncompressed backup. (See my blog on multithreaded backup and restores using percona for more details on how to use Percona Backup)
ii. Next you need to make a mysqldump of all your databases
- Mysqldump –all-databases > alldatabases.sql (old school)
- I do it a bit differently. I have a script that makes full dump of all the databases and creates separate sql files for each db in case I need to import a specific database after that fact.
https://www.nicktailor.com/files/mysqldumpbackup.sh (Here is the script edit according to your needs)
2. Now you need to upgrade mysql, so log into WHM and run the mysql upgrade in the mysql section of whm. If your running a db server and disabled apache, renable it in WHM temporarily, because WHM will be recompiling php and easyapache with the new mysql modules, once its done you can disable it.
- If your mysql upgrade fails check your permissions on mysql or you can run the upgrade from command line forced.
/usr/local/cpanel/scripts/mysqlup –force
And after that run
/usr/local/cpanel/scripts/easyapache
3. Since WHM upgrades /var/lib/mysql regardless if you specified another directory for your data we’re going to have to do a little bit of extra work, while were doing this were going to shrink ibdata1 file to fix any innodb corruption and save you a ton of space.
- Find your mysql data directory if its different from /var/lib/mysql, if it’s the same then you don’t need to do these steps.
i. Delete everything inside the data directory
ii. Copy everything from /var/lib/mysql to mysql datadirectory
cp –ra /var/lib/mysql /datadirectory
iii. Try to start mysql, if you get an error saying myqsl cant create a pid, its probably due to your my.cnf, some setting no longer work with mysql 5.6, easiest way to figure out is just comment stuff out until it works. I will provide a sample one that worked for me. Also its easier to start up in safe mode to avoid all the granty permissions simply uncomment the #skip-grant-tables in the my.cnf file
https://www.nicktailor.com/files/my.cnf.sample (this sample has the performance tweaks enabled it)
iv. Once mysql is started, now ya want to fix up the innodb while you got a chance, if you weren’t using /var/lib/mysql as your data directory then the upgrade will have already created new ibdata1, ib_logfile0 & ib_logfile1 files. If however this is not the case, simply rename those files and restart mysql and mysql will create brand spanking new ones
v. Now we need to restore everything, now I have SSD drives and if you have large DB’s you should only be using SSD’s anyway. You need to do a mysqldump back to mysql using the all-databases.sql file you created earlier.
- Mysql –u root –p<password> < all-databases.sql (best to run this in a screen session on linux as it will take awhile and you don’t want to loose your connection during this)
vi. Once the dump is complete you now need to run mysql_upgrade to upgrade all the databases and tables that didn’t get upgraded to the new version, followed by a mysql-check
- Mysql_upgrade –u root –p<password>
- mysqlcheck –all-databases –check-upgrade –auto-repair
Now you should be able to set grant permissions and things, if you miss the mysql_upgrade step, some of your sites may work and some may not, in addition you will probably be unable to set grant permission in mysql, you’ll get a connection error most likely.
4. If you have a slave db, then you can continue reading. So the next piece is fixing our slave now. Thanks to percona we can do this quick. You will notice that your ibdata1 file is tiny now and clean, so the backup will be super fast.
- You need to back-up full backup using percona
i. Innobackupex /directoryyouwanttobackupto
- Now you need to copy the uncompressed backup to your slave server, you can either scp or rsync, whatever works for you. I have gige switch so I sync over
i. rsync -rva –numeric-ids –progress . root@192.168.0.20:/backupdirectory (this is just a sample)
i. Stop mysql
a. /etc/init.d/mysql stop
ii. Delete the data directory on the slave
b. rm -f /mysqldatadirectory/*
iii. Do a full percona restore
c. Innobackupex –copy-back /backupdirectory
5. Once mysql is restored change your permissions on mysql files to mysql:mysql, edit your my.cnf and startup mysql and you should be good to go. You will need to fix replication, read my mysql failover setup post on how do that if you’re not sure.
chown -R mysql:mysql /mysqldatadirectory
How to fix horde DB connect failed error with cpanel
The easiest way to see whats wrong is see if horde is able to connect to mysql
cat /usr/local/cpanel/base/horde/config/conf.php | grep conf | grep sql
This should show what horde is using for mysql user and pass
Then run
mysql -h <host if remote> or localhost -u horde -p<password>
If you get an error of some kind, then you need log into mysql and run the following below
GRANT ALL ON *.* TO ‘horde’@’whatever host the error said or use localhost’ identified by ‘whatever password it said above’;
Example
GRANT ALL ON *.* TO ‘horde’@’localhost’ identified by ‘password’;
Next run
Mysql> flush privileges; <–at the mysql prompt
test your mysql connect again with the first step if it works try in a browser, it should work now.
Cheers
Nick Tailor
