The WordPress site opened, then redirected visitors to spam
The website loaded its content. A few seconds later, the browser left for an unrelated address.
That was the starting point for a recent WordPress malware cleanup at Ravensdale Digital. The original redirect affected desktop and mobile visitors. The injected code included a seven-second meta refresh alongside JavaScript that could send mobile visitors away sooner.
The first database repairs restored the homepage address and removed injected content. The site appeared to open normally again. Later mobile testing exposed another redirect, this time in JavaScript loaded by the page.
The investigation covered 677 affected content records, an altered SEO setting, generated cache files, suspicious PHP directories, three infected carousel scripts and eight identical fake maintenance files. The client is anonymised throughout this case study.
Here is what we found and how we worked through it. Client domains, account names and private backup locations are omitted. The spam URLs remain as non-clickable text so readers can recognise matching indicators.
| Finding | What it meant for the repair |
|---|---|
| Desktop and mobile redirects | Check timed redirects as well as device-dependent JavaScript |
| 677 content records | Inspect revisions and custom post types, alongside live pages |
| Three infected carousel scripts | Inspect linked assets after the database repair |
| Eight matching maintenance files | Compare contents and hashes before replacing files |
| Mobile retest passed according to the owner | Report the observed recovery separately from a full security clearance |
The fake maintenance page visitors can mistake for a real outage
A maintenance message can look convincing while concealing an unauthorised change. The supplied screenshot below illustrates the dark maintenance screen associated with this investigation. It belongs to the case material; it is not a Ravensdale service-status notice.

The screenshot reads “We'll be back shortly.” and “Service Maintenance Center”. The eight matching files recovered later defined a different, white WordPress-style page. We cannot identify the screenshot's generating code or prove redirect behaviour from the image alone. The recovered files and their exact wording are documented in the fake maintenance file inspection below.
Recognising the urshort redirect
The original database payload pointed to:
https://urshort.com/GJRiGxMgA0r6A later payload in the carousel scripts used:
https://ushort.dev/rltBnhFeJ0r2Notice the spelling: urshort in the first domain and ushort in the second.
While preparing this article, a separate report was supplied describing a similar delayed redirect involving:
https://urshort.live/LkZRChpuD0r5That third address is a reported example, not an indicator recovered from the repaired site. Similar names and behaviour do not establish a shared attacker or entry point.
If you are searching for urshort.com/GJRiGxMgA0r6, urshort.live/LkZRChpuD0r5, or part of either address, inspect the website's files and database. The matching text may be in settings, revisions, generated output or linked scripts rather than the visible page.
Different spam destinations were reported from different locations during our case. We did not establish the routing mechanism or attribute the destinations to a particular advertising network.
Why desktop and mobile behaved differently
The original injection combined a timed redirect with a device-dependent redirect. Its meta refresh contained:
<meta http-equiv="refresh"
content="7; url=https://urshort.com/GJRiGxMgA0r6" />The accompanying JavaScript checked navigator.userAgent for mobile device strings or used window.matchMedia("(pointer:coarse)").matches, then called location.replace().
The meta refresh explains the original all-device symptom. The JavaScript explains why some mobile visitors could leave sooner. The pointer query detects a type of input device; it does not require someone to tap the screen first.
Later, a separate mobile-dependent payload remained in external JavaScript after the database had been cleaned. That distinction matters: this was an all-device redirect problem with an additional mobile-dependent mechanism, not an incident that affected phones alone.
We confirmed those client-side checks. We did not confirm referrer filtering, cookies that suppressed repeat redirects, or special treatment of logged-in administrators.
First, preserve the evidence
Backups were taken before database edits and targeted file repairs. Suspect files were quarantined outside the public website, with their original paths recorded.
An administrator can run WP-CLI as the site account using a helper like this:
SITE_USER='siteaccount'
SITE_ROOT='/home/siteaccount/public_html'
wp_site() {
runuser -u "$SITE_USER" -- wp --path="$SITE_ROOT" "$@"
}Inspect configuration and loaders before commands that execute website PHP. --skip-plugins still allows must-use plugins to load. The core checksum command avoids loading WordPress; that does not make every other WP-CLI command equivalent. Core checksum documentation
1. Find the database changes
The WordPress home option had been changed to the spam URL. siteurl still contained the legitimate website address. Redirect code also appeared in 677 post_content records, including revisions and custom post types. That figure does not mean 677 live pages were affected.
For an already authorised database session, these read-only queries illustrate the checks. Replace wp_ with the installation's actual table prefix:
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('home', 'siteurl');
SELECT COUNT(*) AS content_records_containing_urshort
FROM wp_posts
WHERE post_content LIKE '%urshort.com%';
SELECT ID, post_type, post_status, post_title,
LEFT(post_content, 350) AS content_start
FROM wp_posts
WHERE post_content LIKE '%urshort.com%';The repair script backed up the database, restored home and removed the exact identified meta-refresh-and-script payload from matching content. It did not strip every script from every post.
The recorded output included:
home_settings_corrected
1
content_records_cleaned
677
content_records_still_containing_urshort
0Both homepage settings then contained the legitimate domain. The database backup was retained outside the public website.
Repair the remaining serialised SEO option
Another match remained inside Yoast's serialised wpseo option, under home_url. The unwanted address appeared in this token:
s:32:"https://urshort.com/GJRiGxMgA0r6";PHP serialisation stores string byte lengths. Replacing the URL without correcting that length can invalidate the serialised value. Our repair corrected the specific key/value token while preserving its structure. The follow-up option check returned no remaining match for that address.
For readers choosing WP-CLI instead, it provides a serialisation-aware search-and-replace operation. This is an alternative dry-run example, not the script used for the original correction:
wp_site --skip-plugins --skip-themes search-replace \
'https://urshort.com/GJRiGxMgA0r6' 'https://example.com' \
wp_options --dry-run --report-changed-onlyUse the actual options table and intended homepage URL. Review the result and preserve a backup before any write. The WP-CLI reference documents how serialised data is handled.
A URL replacement is appropriate only where a setting should contain that URL. Replacing a destination inside an injected script is not equivalent to removing the script: it could leave a self-redirect or timed reload.
2. Clear affected Divi output and rebuild the SEO index
Matching code appeared in generated Divi files under wp-content/et-cache. Affected generated cache output was cleared so it could be rebuilt from the corrected source content.
The canonical URL initially remained wrong because Yoast's stored index data also needed rebuilding. Once the settings had been corrected and the plugin could be loaded, the equivalent command was:
# Write operation: removes and rebuilds Yoast indexables.
wp_site yoast index --reindexThe command prompted for confirmation, then completed the indexing stages. This was a specific SEO repair, not a malware scan. Yoast documents the --reindex behaviour in its WP-CLI guide.
The terminal also produced PHP deprecation notices from cPanel's bundled WP-CLI dependencies. Their paths identified CLI compatibility warnings; the notices themselves did not demonstrate another infection.
3. Inspect cron, processes, MU plugins and uploads
The site's Unix crontab was empty, and a process snapshot listed no processes for the account at that moment:
crontab -u "$SITE_USER" -l
ps -u "$SITE_USER" -o pid,ppid,etime,args
find "$SITE_ROOT/wp-content/mu-plugins" -type f -ls
find "$SITE_ROOT/wp-content/uploads" -type f \
\( -iname '*.php' -o -iname '*.phtml' -o -name '.user.ini' \
-o -name '.htaccess' \) -printThose results had limits. An empty Unix crontab does not inspect WordPress scheduled events or server-level jobs. A process snapshot cannot exclude earlier or intermittent activity.
The file listing revealed two deeply nested, unexpectedly named PHP trees under mu-plugins. Suspect upload files were also present at:
wp-content/uploads/2012/ajax30.php
wp-content/uploads/2014/02/wp-theme.phpThe two MU subdirectories and those upload files were moved into a private quarantine outside the website. This is the anonymised form of those moves, run by the administrator after identifying the files:
# Write operations: move the identified items outside the public website.
QUARANTINE=$(mktemp -d /root/wordpress-quarantine-XXXXXXXX)
chmod 700 "$QUARANTINE"
mv -v -- "$SITE_ROOT/wp-content/mu-plugins/uctlemf" \
"$QUARANTINE/mu-uctlemf"
mv -v -- "$SITE_ROOT/wp-content/mu-plugins/oflwxum" \
"$QUARANTINE/mu-oflwxum"
mv -v -- "$SITE_ROOT/wp-content/uploads/2012/ajax30.php" \
"$QUARANTINE/uploads-2012-ajax30.php"
mv -v -- "$SITE_ROOT/wp-content/uploads/2014/02/wp-theme.php" \
"$QUARANTINE/uploads-2014-02-wp-theme.php"These paths describe this case's findings. They are not a list of filenames that every affected site will contain.
We read mu-plugins/0-worker.php separately. Its captured contents were consistent with a ManageWP Worker loader, which included the main plugin only when active. That did not independently verify the underlying plugin or management account.
WordPress automatically loads top-level PHP files in the MU directory. Nested files need a loader or another execution route; being inside mu-plugins does not prove that every nested file executes on every request. Must-use plugin reference
The MU directory's .htaccess also contained permissive PHP access rules. It was another configuration item to review, rather than proof of a particular exploit.
4. Interpret the checksum warnings
Core and plugin checks helped identify changed and unexpected files:
wp_site core verify-checksums --include-root
wp_site --skip-plugins --skip-themes \
plugin verify-checksums --all --strict
wp_site --skip-plugins --skip-themes \
plugin list --fields=name,status,version --format=csv
wp_site --skip-plugins --skip-themes \
theme list --fields=name,status,version --format=csvWordPress's wp-admin and wp-includes directories had already been replaced earlier in the repair. This did not address files elsewhere in the installation or stored database content.
The core report flagged changed license.txt and readme.html files, plus extra files in a suspicious well-known tree. That directory had no leading dot, an important detail when comparing paths with .well-known.
It also flagged sitemaps, robots.txt, logs and other non-core items. “File should not exist” means outside the expected WordPress distribution, not automatically malicious. The recorded report did not show a core PHP checksum mismatch.
Plugin reports showed widespread JavaScript mismatches and added index.php files. Those required closer inspection. A generic index.php name alone is not grounds for deletion. Plugin checksum documentation
The carousel plugin was identified from its header as Divi Carousel by DiviGear, stored under dg-carousel. It was a premium component, requiring a vendor package rather than an ordinary WordPress.org download.
Finding injected code in a plugin file establishes that this copy was modified. It does not establish that a vulnerability in that plugin provided initial access.
5. Reinstall Divi and resolve the archive layout
The uploaded Divi ZIP passed its compressed-data test, but it stored style.css and the other theme files at the archive root. An initial attempt to read Divi/style.css returned:
caution: filename not matched: Divi/style.cssThese read-only checks illustrate how we confirmed the structure:
DIVI_ZIP='/home/siteaccount/repair-staging/Divi-upload.zip'
unzip -t "$DIVI_ZIP"
unzip -Z -1 "$DIVI_ZIP" | sed -n '1,25p'
unzip -p "$DIVI_ZIP" style.css | sed -n '1,30p'The theme header identified Divi 4.27.9. The archive was checked and repacked with the files under a Divi/ directory. The previous theme and original upload were backed up. The installation used WP-CLI's local-ZIP route, equivalent to:
# Write operation: install the reviewed, correctly repacked theme ZIP.
wp_site --skip-plugins --skip-themes theme install \
'/home/siteaccount/repair-staging/Divi-repacked.zip' --forceInstallation succeeded:
Theme updated successfully.
Success: Installed 1 of 1 themes.
DIVI_REINSTALLEDThat version records what was installed during this repair. It is not a recommendation to use an older release when a newer compatible vendor package is available.
The reinstall addressed the theme. It did not repair separate plugin assets.
6. Follow the JavaScript when mobile redirects persist
Mobile testing still showed redirects after the earlier repairs. Inspecting the scripts actually loaded by the page exposed the remaining payload.
Three files contained an injected prefix:
wp-content/plugins/dg-carousel/scripts/builder-bundle.min.js
wp-content/plugins/dg-carousel/scripts/frontend-bundle.min.js
wp-content/plugins/dg-carousel/scripts/swiper.min.jsThe prefix checked mobile user-agent strings or (pointer:coarse), then used location.replace() to navigate to the ushort.dev URL listed earlier.
The targeted cleanup backed up each original and removed only the exact matched prefix, preserving ownership and permissions. It reported:
CLEANED: scripts/builder-bundle.min.js
CLEANED: scripts/frontend-bundle.min.js
CLEANED: scripts/swiper.min.js
FILES CLEANED: 3This corrected an important gap in our earlier verification: clean-looking HTML did not mean that every linked asset was clean. A full comparison of the remaining plugin against a trusted vendor package was a separate integrity question.
A response check must include the body and linked assets
The following is an anonymised diagnostic example of fetching HTML for inspection. Run it from a working directory outside the webroot:
SITE_URL='https://example.com/'
MOBILE_UA='Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'
curl --silent --show-error --max-time 30 \
-A "$MOBILE_UA" -H 'Cache-Control: no-cache' \
--dump-header response-headers.txt \
--output page.html "$SITE_URL"
grep -nEi -- \
'urshort|ushort|http-equiv|location[.](replace|href)|pointer:coarse' \
page.htmlcurl does not execute JavaScript or follow an HTML meta refresh. A 200 OK response cannot establish that a browser will remain on the site. A headers-only curl -I check would miss a payload embedded in the response body.
Inspect linked scripts as text too. A user-agent header is useful for comparing responses, but it does not reproduce a real phone's JavaScript environment. In our case, actual mobile testing was essential.
7. Search the wider installation for remaining indicators
After cleaning the three scripts, this search located additional matches:
grep -rIlE -- \
'ushort[.]dev|urshort[.]com|rltBnhFeJ0r2|GJRiGxMgA0r6' \
"$SITE_ROOT"The results included a staging plugin directory, an unexpected Akismet3 directory and several index.php files elsewhere under wp-content. The staging and Akismet3 directories were quarantined outside the website.
Their names were not sufficient evidence by themselves. The matching contents were the reason to investigate them. Likewise, a plugin absent from the active inventory can still leave files on disk, and a direct PHP request may not depend on normal plugin activation.
For someone investigating the separately reported urshort.live address, this additional search example broadens the indicators. It is not the search used to produce our final case result:
grep -rIlE -- \
'urshort[.](com|live)|ushort[.]dev|GJRiGxMgA0r6|rltBnhFeJ0r2|LkZRChpuD0r5' \
"$SITE_ROOT"Results are candidates for inspection, not an automatic deletion list. A string search can miss encoded or differently written payloads.
8. Inspect the fake maintenance files
Eight remaining PHP files were identical. They displayed a WordPress-style maintenance page with a white panel, a WordPress heading and a loading animation.
The exact visible wording was:
Briefly unavailable for scheduled maintenance.
Check back in a some hours.
The unusual second sentence is reproduced exactly because someone searching “WordPress Check back in a some hours” may be trying to identify the same sample.
The file excerpt also showed these headers:
http_response_code(503);
header("Retry-After: 3600");
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: 0");They made a request to the file resemble a temporary outage. The captured excerpt continued into JavaScript after the page markup, and the files matched the known redirect indicators. The complete script tail was not in that excerpt, so we do not attribute additional capabilities to it.
Locations and fingerprint
These are the eight paths relative to wp-content:
managewp/index.php
plugins/dg-carousel/functions/index.php
plugins/dg-carousel/includes/index.php
plugins/dg-carousel/languages/index.php
plugins/dg-carousel/styles/index.php
plugins/index.php
themes_backup/index.php
wflogs/index.phpAll eight files were 2,018 bytes, with the same SHA-256 hash:
18720ed37d93a3af4453e5fe029cee8489309ce818cf5eaeb858ff733c041771These read-only examples can help locate candidates and compare the exact sample:
find "$SITE_ROOT/wp-content" -type f \
-name 'index.php' -size 2018c -print
grep -rIlF -- 'Check back in a some hours' \
"$SITE_ROOT/wp-content"
sha256sum "$SITE_ROOT/wp-content/plugins/index.php"Filename, size and wording are clues. The hash identifies this exact byte-for-byte sample; variants may differ.
Our cleanup checked all eight against the expected hash before backing them up and replacing only those matches with minimal no-output placeholders. It reported:
FILES REPLACED: 8It did not replace legitimate application entry points or delete every index.php file. We did not establish that these eight files provided command-execution access or represented the initial entry point.
The final search and retest
After those replacements, the known-indicator filesystem search returned no matches. The site owner then reported that mobile browsing no longer redirected.
| Area | Recorded result |
|---|---|
| Homepage settings | One altered home value corrected; siteurl already legitimate |
| Content | Exact identified payload removed from 677 records |
| Original shortener in checked content | Zero remaining matches |
| SEO | Remaining option corrected; indexables rebuilt |
| Divi | Theme reinstalled successfully after correcting ZIP layout |
| Carousel | Identified redirect prefix removed from three scripts |
| Suspect files | Two MU trees, two upload PHP files and two further plugin directories quarantined |
| Maintenance copies | Eight identical files backed up and replaced |
| Final filesystem search | No matches for the original known redirect indicators |
| Mobile retest | Site owner reported that redirects had stopped |
The symptom was resolved in that retest. This record does not establish a complete clearance of every package, account, scheduled task or server setting. The final search covered the observed urshort.com and ushort.dev indicators, not the later urshort.live report.
Could PHP2Shell have caused this?
It is a possible entry route, but it was not confirmed here. PHP's October 2026 advisory describes CVE-2026-109618 and CVE-2026-109619, commonly called PHP2Shell, as vulnerabilities that can allow unauthenticated remote code execution. Updating WordPress does not replace the PHP runtime patch. Official PHP advisory
Attribution would require evidence about the domain's actual web PHP runtime, patch history, worker restarts and relevant request, access and error logs. File modification dates alone are insufficient because they can be altered.
Compromised credentials, vulnerable components, remote-management access and an existing foothold would also need investigation. Finding the redirect mechanism did not establish how the attacker first obtained write access.
For runtime patching, see our related guide: Critical PHP RCE Vulnerabilities: How to Update Safely.
What remains after a redirect cleanup?
Further account, credential, scheduler and configuration review is distinct from removing the visible payload. Those checks should not be described as completed without their results.
The follow-up questions are practical: who had administrator and hosting access, which credentials or sessions need revoking, whether scheduled jobs can restore files, and whether remaining code matches trusted packages. Preserve logs and quarantine samples to support that investigation. WordPress's hacked-site guidance also covers documenting the incident and reviewing access.
We did not establish whether customer data had been accessed or copied. The redirect confirms unauthorised behaviour; it does not settle that separate question.
Questions this case helps answer
Why does WordPress redirect after a few seconds?
In this case, a meta refresh specified a seven-second delay. Check the response body and loaded assets before attributing a similar symptom elsewhere to the same payload.
Why did mobile redirects remain after the homepage was fixed?
Separate JavaScript files still contained a mobile-dependent redirect. The database repair did not remove those filesystem changes.
Is “Briefly unavailable for scheduled maintenance” always malware?
No. WordPress can legitimately enter maintenance mode during an update. Here, repeated files and matching redirect indicators established the problem. Deleting a .maintenance file would not have removed those copies.
Does a clean checksum report prove the entire website is clean?
No. It covers the files and package versions actually verified. Database content, extra directories, credentials and server configuration require their own assessment.
Need help with a WordPress redirect?
If your website redirects to a short link, an unexpected maintenance page or unrelated spam, Ravensdale Digital can help investigate the behaviour and plan the repair. Our WordPress development and website maintenance services cover technical troubleshooting and ongoing support.
Contact us about a WordPress redirect with the affected page, destination URL, device and timing. In this case, following that evidence beyond the homepage led us to database injections, external scripts and the remaining maintenance files.



