How to Find Hidden WordPress Backdoors Using WP-CLI and SQL
Clicking the core update button on an outsourced WordPress site can give anyone pause. Delaying updates out of fear that a broken theme or custom plugin will turn the screen white eventually leaves the site riddled with vulnerabilities. When anxiety strikes that you might have already been compromised, here is a practical inspection workflow to audit your database and server as quickly as possible.
Hunting for Hidden Admin Accounts and Files
Attackers usually enter quietly. A common trick is directly modifying the database's wp_usermeta table to stealthily grant administrator privileges to an existing account. These accounts often stay hidden from the standard login list, but querying the DB directly exposes them immediately.
`sql
SELECT
u.ID,
u.user_login,
u.user_email,
u.user_registered,
m.meta_value AS capabilities
FROM
wp_users u
INNER JOIN
wp_usermeta m ON u.ID = m.user_id
WHERE
m.meta_key = 'wp_capabilities'
AND (
m.meta_value LIKE '%"administrator"%'
OR m.meta_value LIKE '%"administrator":true%'
)
ORDER BY
u.user_registered DESC;
`
Next is the file system. If there are PHP files sitting in the wp-content/uploads directory—a place reserved strictly for images and documents—nine times out of ten, it's a backdoor. Open an SSH terminal and scan for suspicious files.
`bash
1. Search for PHP files inside the uploads directory
find wp-content/uploads/ -type f -name "*.php" -ls
2. Detect files containing obfuscation functions (eval, base64_decode, etc.)
find . -type f -name "*.php" -exec grep -HnE "(eval(|base64_decode(|gzinflate(|passthru(|shell_exec()" {} ;
3. Verify checksums to check if core files have been modified
wp core verify-checksums --include-root
`
You also need to check the logs. Attacks targeting the REST API typically flood the server with POST requests in a short time frame, aiming to get 200 or 201 HTTP responses. Extract suspicious requests from your Nginx access.log.
`bash
grep -E "POST|PUT" /var/log/nginx/access.log | grep -E "/wp-json/|rest_route=" | awk '9 /(200∣201∣207)/ {print $1, $4, $6, $7, $9}' | sort | uniq -c | sort -nr | head -n 30
`
Safely Verifying in a Staging Environment
Applying patches directly to a live production site isn't bravery—it's reckless. Always create a staging environment first by cloning both the database and the file system.
`bash
Export DB backup and duplicate files
wp db export production_backup.sql --add-drop-table
rsync -avz --exclude='wp-content/cache' /var/www/html/ staging:/var/www/staging/
Connect to staging server, import DB, and replace domain
wp db import production_backup.sql
wp search-replace 'https://example.com' 'https://staging.example.com' --skip-columns=guid
Check for incompatible plugins
wp plugin list --update=available --fields=name,version,update_version,requires_php --format=table
`
Once replication is complete, run the updates and spend just five minutes testing core features. Manually testing user registration, login, adding items to the cart, opening the checkout page, and submitting contact forms will give you true peace of mind.
Setting Up Firewall Rules When Core Updates Must Be Delayed
There are frustrating situations where a plugin developer hasn't issued an update, preventing you from bumping the core version right away. In those cases, buy time by blocking access to specific paths at the server level or WAF. If you're using Nginx, block REST API user creation and batch processing endpoints in nginx.conf, excluding your own IP address.
`nginx
location ~* ^/wp-json/(batch/v1|wp/v2/users) {
limit_except GET {
allow 192.0.2.1;
deny all;
}
try_files $uri uri//index.php?args;
}
`
As an extra safety net, set up a backup script to upload the DB and uploads folder to S3 in case the unexpected happens.
`bash
#!/bin/bash
WP_PATH="/var/www/html"
BACKUP_DIR="/tmp/wp_backups"
DATE=$(date +%Y%m%d_%H%M%S)
S3_BUCKET="s3://my-wordpress-secure-backups"
mkdir -p $BACKUP_DIR
wp db export BACKUP_DIR/db_DATE.sql --path=$WP_PATH --quiet
tar -czf BACKUP_DIR/files_DATE.tar.gz -C $WP_PATH wp-content/uploads/
aws s3 cp BACKUP_DIR/db_DATE.sql S3_BUCKET/db_DATE.sql
aws s3 cp BACKUP_DIR/files_DATE.tar.gz S3_BUCKET/files_DATE.tar.gz
find $BACKUP_DIR -type f -mtime +7 -delete
`
Terminating Sessions and Blocking In-Dashboard Script Editing
Clearing malicious code and applying security patches isn't the end of the story. An attacker could still regain access using stolen session cookies. Flush out all active sessions and completely regenerate the salt keys in wp-config.php.
`bash
Destroy active sessions across all accounts at once
wp user list --field=ID | xargs -n 1 wp user session destroy --all
Regenerate salt keys
wp config shuffle-salts
`
Disable the built-in capability to edit plugin or theme files directly from the admin dashboard. This serves as a minimal safeguard to prevent attackers from injecting code even if they manage to gain administrative privileges. Add these two lines to wp-config.php:
`php
define( 'DISALLOW_FILE_EDIT', true );
define( 'DISALLOW_FILE_MODS', true );
`
Finally, drop a custom filter into the mu-plugins directory to prevent unauthenticated users from scraping the user list via the REST API.
`php
<?php
/**
- Plugin Name: REST API Security Hardening
*/
add_filter( 'rest_authentication_errors', function( $result ) {
if ( ! empty( $result ) ) return $result;
if ( ! is_user_logged_in() ) {
if ( strpos( $_SERVER['REQUEST_URI'], '/wp/v2/users' ) !== false ) {
return new WP_Error( 'rest_cannot_access', '접근이 거부되었습니다.', array( 'status' => 401 ) );
}
}
return $result;
});
add_filter( 'xmlrpc_enabled', '__return_false' );
`