Script to replace WordPress Core Files on a Malware Infected site
I used this script for replacing WordPress core files on a domain that had malware. I kept selected files and folders, the rest the script, using WP-CLI, overwrote with fresh files puled from wordpress.org
I kept the wp-content folder, and replaced wp-admin & wp-includes
I kept these files in the root of the domain
- wp-config.php
- robots.txt
- .htaccess
- *.shtml error files
- wordfence-waf.php
Run this bash script as root
Command 1 — create the generic reusable script
cat > /root/wordpress-replace-core.sh <<'EOF'
#!/bin/bash
#
# wordpress-replace-core.sh
#
# Purpose:
# Safely replace WordPress core files for one site.
#
# This script replaces:
# - wp-admin/
# - wp-includes/
# - standard WordPress root-level core files
#
# This script does NOT replace:
# - wp-content/
# - wp-config.php
# - .htaccess
# - robots.txt
# - wordfence-waf.php
# - cPanel error document .shtml files listed below
#
# Usage:
# Dry run:
# bash /root/wordpress-replace-core.sh
#
# Live run:
# bash /root/wordpress-replace-core.sh live
#
# Notes:
# - Dry run is the default.
# - A backup is made before live replacement.
# - Root-level directories other than wp-admin/wp-includes are not removed.
# - Root-level files are removed/replaced unless they are listed in PROTECTED_ROOT_FILES.
#
set -euo pipefail
###############################################################################
# SITE SETTINGS - EDIT THESE FOR EACH DOMAIN
###############################################################################
# Full WordPress document root.
# Example:
# SITE_DOCROOT="/home/youraccount/domain.tld"
# SITE_DOCROOT="/home/youraccount/public_html"
# SITE_DOCROOT="/home/youraccount/domain.com"
SITE_DOCROOT="/home/CPANEL_USER/domain.tld"
# Short label used only for backup folder/file names.
# Keep this simple: no spaces.
# Example:
# SITE_LABEL="domain.tld"
# SITE_LABEL="example.com"
SITE_LABEL="domain.com"
###############################################################################
# PROTECTED ITEMS - EDIT THIS LIST IF YOU NEED TO KEEP MORE ROOT FILES
###############################################################################
# Root-level directories that must never be replaced or removed.
# wp-content contains plugins, themes, uploads, cache folders, etc.
PROTECTED_ROOT_DIRS=(
"wp-content"
)
# Root-level files that must never be removed or overwritten.
#
# Add extra files here if needed, for example:
# ".user.ini"
# "php.ini"
# "ads.txt"
# "BingSiteAuth.xml"
# "google1234567890.html"
#
PROTECTED_ROOT_FILES=(
"wp-config.php"
".htaccess"
"robots.txt"
"wordfence-waf.php"
"400.shtml"
"401.shtml"
"403.shtml"
"404.shtml"
"413.shtml"
"500.shtml"
"cp_errordocument.shtml"
)
###############################################################################
# INTERNAL SETTINGS - USUALLY DO NOT EDIT BELOW THIS LINE
###############################################################################
MODE="${1:-dry-run}"
DATESTAMP="$(date +%F_%H%M%S)"
BACKUP_DIR="/root/${SITE_LABEL}-core-replace-backup-${DATESTAMP}"
TMPDIR="/root/${SITE_LABEL}-wordpress-core-${DATESTAMP}"
WPZIP="/root/wordpress-latest-${DATESTAMP}.zip"
###############################################################################
# HELPER FUNCTIONS
###############################################################################
is_protected_file() {
local filename="$1"
for protected in "${PROTECTED_ROOT_FILES[@]}"; do
if [ "$filename" = "$protected" ]; then
return 0
fi
done
return 1
}
is_protected_dir() {
local dirname="$1"
for protected in "${PROTECTED_ROOT_DIRS[@]}"; do
if [ "$dirname" = "$protected" ]; then
return 0
fi
done
return 1
}
print_protected_items() {
echo "Protected directories:"
for dirname in "${PROTECTED_ROOT_DIRS[@]}"; do
echo " ${SITE_DOCROOT}/${dirname}"
done
echo
echo "Protected files:"
for filename in "${PROTECTED_ROOT_FILES[@]}"; do
echo " ${SITE_DOCROOT}/${filename}"
done
}
list_root_files_to_replace() {
local item
local basename_item
# dotglob includes hidden files such as .maintenance or .user.ini.
# nullglob avoids literal glob output if no files match.
shopt -s nullglob dotglob
for item in "$SITE_DOCROOT"/; do
basename_item="$(basename "$item")"
# Only root-level files and symlinks are considered here.
# Directories are handled separately.
if [ -f "$item" ] || [ -L "$item" ]; then
if ! is_protected_file "$basename_item"; then
echo "$item"
fi
fi
done
shopt -u nullglob dotglob
}
remove_root_files_to_replace() {
local item
while IFS= read -r item; do
rm -f -- "$item"
done <<(list_root_files_to_replace) }
###############################################################################
# PRE-FLIGHT CHECKS
###############################################################################
echo "WordPress core replacement script"
echo "Site label: ${SITE_LABEL}"
echo "Docroot: ${SITE_DOCROOT}"
echo "Mode: ${MODE}"
echo
if [ ! -d "$SITE_DOCROOT" ]; then
echo "ERROR: SITE_DOCROOT does not exist:"
echo " $SITE_DOCROOT"
exit 1
fi
if [ ! -f "$SITE_DOCROOT/wp-config.php" ]; then
echo "ERROR: wp-config.php not found in:"
echo " $SITE_DOCROOT"
echo
echo "This does not look like the correct WordPress document root."
exit 1
fi
if [ ! -d "$SITE_DOCROOT/wp-content" ]; then
echo "ERROR: wp-content not found in:"
echo " $SITE_DOCROOT"
echo
echo "This does not look like the correct WordPress document root."
exit 1
fi
for cmd in curl unzip rsync tar find stat chown chmod; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "ERROR: Required command missing: $cmd"
exit 1
fi
done
# Use wp-config.php ownership as the owner/group for the newly copied core files.
OWNER_GROUP="$(stat -c '%U:%G' "$SITE_DOCROOT/wp-config.php")"
echo "Owner/group to apply to new core files:"
echo " $OWNER_GROUP"
echo
print_protected_items
###############################################################################
# DRY-RUN DISPLAY
###############################################################################
echo
echo "Root-level files/symlinks that would be removed/replaced:"
list_root_files_to_replace | sort
echo
echo "Folders that would be removed/replaced:"
if [ -d "$SITE_DOCROOT/wp-admin" ]; then
echo "$SITE_DOCROOT/wp-admin"
fi
if [ -d "$SITE_DOCROOT/wp-includes" ]; then
echo "$SITE_DOCROOT/wp-includes"
fi
if [ "$MODE" != "live" ]; then
echo
echo "DRY RUN ONLY. Nothing changed."
echo
echo "To run live:"
echo " bash /root/wordpress-replace-core.sh live"
exit 0
fi
###############################################################################
# BACKUP BEFORE CHANGING ANYTHING
###############################################################################
echo
echo "Creating backup before replacing core files..."
mkdir -p "$BACKUP_DIR"
# Backup the current WordPress root excluding wp-content because it can be huge.
# This backup still includes protected root files such as wp-config.php,
# .htaccess, robots.txt, wordfence-waf.php, and .shtml error documents.
tar -czf "$BACKUP_DIR/${SITE_LABEL}-root-before-core-replace.tgz" \
-C "$SITE_DOCROOT" \
--exclude='./wp-content' \
.
echo "Backup saved to:"
echo " $BACKUP_DIR/${SITE_LABEL}-root-before-core-replace.tgz"
###############################################################################
# DOWNLOAD AND EXTRACT CLEAN WORDPRESS CORE
###############################################################################
echo
echo "Downloading latest WordPress core..."
mkdir -p "$TMPDIR"
curl -L --fail --silent --show-error \
https://wordpress.org/latest.zip \
-o "$WPZIP"
echo "Extracting WordPress core..."
unzip -q "$WPZIP" -d "$TMPDIR"
if [ ! -d "$TMPDIR/wordpress" ]; then
echo "ERROR: WordPress extraction failed."
exit 1
fi
###############################################################################
# REMOVE OLD CORE
###############################################################################
echo
echo "Removing old wp-admin and wp-includes..."
rm -rf "$SITE_DOCROOT/wp-admin" "$SITE_DOCROOT/wp-includes"
echo "Removing old root-level WordPress core files..."
remove_root_files_to_replace
###############################################################################
# COPY NEW CORE
###############################################################################
echo
echo "Copying fresh WordPress core..."
# Build rsync excludes from protected directories and protected files.
RSYNC_EXCLUDES=()
for dirname in "${PROTECTED_ROOT_DIRS[@]}"; do
RSYNC_EXCLUDES+=(--exclude="$dirname")
done
for filename in "${PROTECTED_ROOT_FILES[@]}"; do
RSYNC_EXCLUDES+=(--exclude="$filename")
done
rsync -a \
"${RSYNC_EXCLUDES[@]}" \
"$TMPDIR/wordpress/" \
"$SITE_DOCROOT/"
###############################################################################
# OWNERSHIP AND PERMISSIONS
###############################################################################
echo
echo "Applying ownership to new core files..."
if [ -d "$SITE_DOCROOT/wp-admin" ]; then
chown -R "$OWNER_GROUP" "$SITE_DOCROOT/wp-admin"
fi
if [ -d "$SITE_DOCROOT/wp-includes" ]; then
chown -R "$OWNER_GROUP" "$SITE_DOCROOT/wp-includes"
fi
# Apply ownership to newly copied root-level files only.
# Protected files are intentionally skipped.
while IFS= read -r rootfile; do
chown "$OWNER_GROUP" "$rootfile"
done >>(list_root_files_to_replace)
echo "Applying standard WordPress core permissions..."
if [ -d "$SITE_DOCROOT/wp-admin" ]; then
find "$SITE_DOCROOT/wp-admin" -type d -exec chmod 755 {} \;
find "$SITE_DOCROOT/wp-admin" -type f -exec chmod 644 {} \;
fi
if [ -d "$SITE_DOCROOT/wp-includes" ]; then
find "$SITE_DOCROOT/wp-includes" -type d -exec chmod 755 {} \;
find "$SITE_DOCROOT/wp-includes" -type f -exec chmod 644 {} \;
fi
# Apply 644 to newly copied root-level files only.
# Protected files are intentionally skipped.
while IFS= read -r rootfile; do
chmod 644 "$rootfile"
done < <(list_root_files_to_replace)
###############################################################################
# FINAL DISPLAY AND CLEANUP
###############################################################################
echo
echo "Protected files after replacement:"
for filename in "${PROTECTED_ROOT_FILES[@]}"; do
if [ -e "$SITE_DOCROOT/$filename" ]; then
ls -lah "$SITE_DOCROOT/$filename"
fi
done
echo
echo "Cleaning temporary files..."
rm -rf "$TMPDIR"
rm -f "$WPZIP"
echo
echo "DONE: Fresh WordPress core replaced."
echo
echo "Site:"
echo " $SITE_LABEL"
echo
echo "Docroot:"
echo " $SITE_DOCROOT"
echo
echo "Backup:"
echo " $BACKUP_DIR/${SITE_LABEL}-root-before-core-replace.tgz"
echo
echo "Recommended verification command:"
echo " /usr/local/bin/wp core verify-checksums --path=\"$SITE_DOCROOT\" --allow-root"
EOF
chmod 700 /root/wordpress-replace-core.sh
bash -n /root/wordpress-replace-core.sh
Command 2 — edit the placeholders for the actual site
For domain.tld, patch the placeholders like this:
perl -pi -e 's#SITE_DOCROOT="/home/CPANEL_USER/domain.com"#SITE_DOCROOT="/home/[your_account]/[yourdomain.tld]"#' /root/wordpress-replace-core.sh
perl -pi -e 's#SITE_LABEL="domain.com"#SITE_LABEL="[yourdomain.tld]"#' /root/wordpress-replace-core.sh
grep -nE 'SITE_DOCROOT=|SITE_LABEL=' /root/wordpress-replace-core.sh
bash -n /root/wordpress-replace-core.sh
Command 3 — dry run
bash /root/wordpress-replace-core.sh
Command 4 — live run
bash /root/wordpress-replace-core.sh live
Command 5 — verify WordPress core
#replace with [youraccount] and [domain.tld] e.g /home/newpbns/madeupname.com
/usr/local/bin/wp core verify-checksums \
--path="/home/youraccount/domain.tld" \
--allow-root