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
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 2edit 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 3dry run

bash /root/wordpress-replace-core.sh
## Command 4live run

bash /root/wordpress-replace-core.sh live
## Command 5verify 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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *