Skip to content

Latest commit

 

History

History
157 lines (109 loc) · 10.6 KB

playbook.md

File metadata and controls

157 lines (109 loc) · 10.6 KB

Playbook: what to do for each verdict

This page tells you what to do after a scan. It describes what to look for and what to remove. It does not describe how attacks are built, and it contains no real indicator values.

The scanner only reports. Everything below is done by you, by hand, on a copy first.

Before you act

  1. Read the scope. The report lists what was scanned and what was not. A result for a dump alone, or for files alone, or for a --fast scan, says nothing about the rest. Scan again with both inputs before you rely on it.
  2. Check the exit code and the notes. An empty or compressed dump, or a dump with no WordPress tables when no files were given, is refused with exit code 66. With files given, a dump that could not be read as a WordPress database is named in the verdict reason and gives "no WordPress tables recognised" in the scope notes. See the limitations.
  3. Keep the original untouched. Do not clean or edit the live site until you have a copy and a plan.

How to take a safe copy to scan

The aim is a copy that you can read without running any of it.

  1. If you can, put the site behind a maintenance page or take it offline, so that the copy matches one moment in time.

  2. Export the database with a tool that does not load WordPress. Use mysqldump, or the export in your hosting panel or a database client. Do not use WP-CLI or a WordPress export plugin on a site you think is infected: they load the site's own code, which may be the problem.

    mysqldump --single-transaction --no-tablespaces --routines --events --triggers -u DB_USER -p DB_NAME > site-copy.sql
    

    --routines --events --triggers put stored procedures, functions, events and triggers in the copy. WordPress creates none of them, and a trigger is a place to hide code that comes back after a clean-up, so the scanner needs to see them.

    The -p flag with no value asks for the password. Do not type the password into the command, because it would stay in your shell history. Replace DB_USER and DB_NAME with the values from the hosting panel.

  3. Copy the files with your host's file manager, rsync, scp or an archive, into a folder that is not served by a web server. Include hidden files such as .htaccess and .user.ini.

  4. Never open the copy in a browser and never run it. Do not start a local web server on it. Scanning only reads the files.

  5. If the dump is compressed, decompress it first. The scanner refuses .gz, .zip, bzip2, xz and zstandard files with exit code 66 and does not decompress them.

  6. Keep an extra, untouched copy somewhere off the server as evidence. Work on the other copy.

Then run the scan:

wp-malware-scan scan --sql site-copy.sql --files site-files --site-name shop.example

CLEAN (exit code 0)

No rule matched in what was scanned.

  • Check the scope. If the headline says "CLEAN (in what was scanned)", run a full scan with both inputs.
  • A clean result is not proof. If you have a reason to suspect an infection, such as a search engine warning, unexplained redirects, or admin users you do not know, keep investigating. Look at server access logs, the hosting account and other sites on it, which this tool does not read.
  • Medium findings do not change a CLEAN verdict, but the report asks for a manual check. Do the check.
  • Keep WordPress, plugins and themes updated, remove what you do not use, and keep tested backups.

SPAM-ONLY (exit code 0)

Spam content was found and nothing that runs code. It is usually form spam in a table that stores form submissions (DB-SPAM-041), or junk text in another table (DB-SPAM-042).

  1. Take a backup of the database.
  2. Review the rows the report points to. In a form table they are usually safe to delete.
  3. Add a spam filter or a CAPTCHA to the form that collected them, or the spam will come back.
  4. Look at the external domains list (DB-DOM-050) for links you do not recognise.
  5. Scan again after the clean-up.

Spam in published posts (DB-SPAM-040) is different. It is rated HIGH and gives SUSPICIOUS, because it often means someone with access edited your content.

SUSPICIOUS (exit code 1)

Something may be harmful and a person has to check it. The scanner cannot tell a hosting provider's loader from an attacker's, so this is your decision.

For each finding:

  • Must-use plugin (FS-MU-110). These load on every request and do not appear in the normal plugin list. Find out what the file is and who put it there. Hosting providers and some caching or security products add them. Compare the file with the provider's documentation.
  • Drop-in (FS-DROP-111). Files such as an object cache or a database drop-in replace parts of WordPress. Check that you use the matching service.
  • Prepend or append setting (FS-HTA-151). The setting runs a file before or after every PHP request. Open the file it names. If you cannot explain it, treat it as hostile.
  • Statements after the settings include (FS-CFG-142). Open wp-config.php and read what follows the line that loads wp-settings.php. A define line or a snippet from your host is fine, so record why and allowlist the exact location. Code you cannot explain is hostile. Dangerous code in the same place is reported as FS-CFG-140 instead, and that gives FULLY-COMPROMISED.
  • Option names and values (DB-OPT-013, DB-OPT-014). Look at the option and at the plugin or theme that should own it. An option no installed plugin uses is a reason to dig further.
  • Script markers (DB-IOC-002). Find the page or option that contains it and what loads from there.
  • Spam in posts (DB-SPAM-040). See who edited the post and when.

Then decide:

  • Legitimate: record why and add it to an allowlist with a reason. Be exact. Scan again.
  • Not legitimate, or you cannot tell: treat the site as INFECTED and follow that section. Do not clean in place until you have a backup and have reviewed every item.

INFECTED (exit code 2)

Malicious code or content was found. Assume the visible items are not the only ones.

  1. Contain. Put the site behind a maintenance page or offline. Keep an untouched copy as evidence.
  2. Rotate secrets. Change every password and key that the site could reach, from a clean computer:
    • every WordPress user, starting with administrators
    • the database user and password
    • hosting panel, FTP, SFTP and SSH accounts, and any deployment keys
    • API keys and tokens stored in options or in wp-config.php
    • the authentication keys and salts in wp-config.php. Replacing them signs every user out.
  3. Review the administrators. The scanner lists counts, not who is legitimate. List the administrators yourself (see the queries below) and remove any you cannot explain.
  4. Rebuild instead of cleaning. Install fresh WordPress, plugin and theme files from their official sources and restore only content you have reviewed. See "Why cleaning in place fails" below.
  5. Review the content you keep. Posts, pages, menus and widgets can carry injected scripts. Options and post meta can hold encoded payloads.
  6. Scan the rebuilt site. Take a new dump and a copy of the new files and run the scan again. Expect CLEAN, and read the scope.
  7. Watch it. Check logs and rescan on a schedule for the next few weeks.

FULLY-COMPROMISED (exit code 3)

A rule matched that on its own means an attacker has deep and lasting access. Examples are a known backdoor option, dangerous code planted after the settings include in wp-config.php, or a file that checks for scanners.

Do not clean this site in place. Do the steps for INFECTED, with these additions:

  1. Rebuild on clean hosting if you can. The account itself may be compromised.
  2. Restore only posts and media from a backup made before the infection, and review them. Do not restore the old plugins, themes, wp-config.php or any PHP file.
  3. Check every other site on the same account or server. The attacker may have reached them. Sweep them all (wp-malware-scan sweep).
  4. Check what lives outside the copy. Scheduled jobs on the server, other users on the account, mail forwarding rules and anything that can write files.
  5. Treat credentials stored on the site as stolen. If the report has a credential finding (DB-OPT-015), assume the logins in it were collected. Reset those users' passwords, and tell the people affected if their data may have been exposed.
  6. Tell your host. They can check things you cannot see.

Why cleaning in place fails

Some malware keeps a second copy of itself, in a different file, in a database option or in a scheduled task, and puts the first copy back when you remove it. Others add an administrator account or a must-use plugin that you do not see in the normal plugin list. If you delete what the scanner found, you may remove the visible part and leave the part that restores it.

You also cannot be sure that you found every copy. The scanner is based on patterns and can miss one. A rebuild from known good files does not depend on that. That is why the next steps for INFECTED and FULLY-COMPROMISED say to rebuild.

Queries for the database copy

These are templates. The scanner does not produce them and they have no real values in them. Replace <prefix> with the table prefix shown in the report, and run the read-only queries on a copy of the database first.

List the administrators:

SELECT u.ID, u.user_login, u.user_email, u.user_registered
FROM `<prefix>users` u
JOIN `<prefix>usermeta` m ON m.user_id = u.ID
WHERE m.meta_key = '<prefix>capabilities' AND m.meta_value LIKE '%administrator%';

Look at an option the report named, without reading its whole value:

SELECT option_id, option_name, LENGTH(option_value) AS size
FROM `<prefix>options`
WHERE option_name = '<option name from the report>';

Look at a post the report named:

SELECT ID, post_title, post_status, post_modified
FROM `<prefix>posts`
WHERE ID = <post id from the report>;

Verify first. A DELETE cannot be undone. Only after you have checked the row, taken a backup and confirmed that nothing depends on it:

DELETE FROM `<prefix>options` WHERE option_id = <option id you checked>;

If you are rebuilding the site, you do not need to delete anything from the old database. Move over only the content you reviewed.

After the clean-up

  • Scan the new copy with both inputs and read the scope.
  • Keep WordPress, plugins and themes updated, and remove unused ones.
  • Use unique passwords and two-factor authentication for administrators, and give users only the access they need.
  • Keep backups off the server and test that you can restore them.
  • Scan on a schedule. For many sites, put each site in its own folder under one root and run wp-malware-scan sweep ROOT.