Detect Public Website Defacement via Compromised CMS Admin Session or Mass Static Asset Tampering in Elastic Security
Adversaries who obtain stolen CMS administrator credentials or exploit an unpatched plugin/theme vulnerability on a public-facing content management system (WordPress, Joomla, Drupal, Umbraco, Sitecore, etc.) frequently pivot straight to defacing the site — replacing the homepage, posting propaganda content, or mass-editing pages — to deliver a political message, claim credit, or intimidate the target organization. This pattern shows up in two complementary ways depending on the compromise vector: (1) a burst of authenticated content-edit HTTP requests (POST/PUT to admin post/page/theme-editor endpoints) from a single source IP or session hitting many distinct URLs in a short window, captured in WAF or reverse-proxy access logs, which is characteristic of a scripted mass-edit rather than a human editor working through the admin UI one page at a time; and (2) direct filesystem tampering where an attacker who has RCE (via a vulnerable plugin, exposed file manager, or stolen SFTP/SSH credentials) bypasses the CMS application layer entirely and mass-modifies static HTML, PHP, CSS, JS, and image assets directly in the web root, captured via endpoint file-monitoring telemetry. Both paths differ sharply from routine content operations: legitimate CMS editors and CI/CD deployment pipelines touch a handful of pages per session or deploy through a recognized service account/process, not dozens of distinct admin endpoints or web-root files from a single caller within minutes. Detection correlates a distinct-URL or distinct-file count threshold within a short burst window against the initiating source IP, session, or account to separate scripted mass defacement from normal editorial and deployment activity.
MITRE ATT&CK
- Tactic
- Impact
Elastic Detection Query
sequence by host.name, user.name with maxspan=15m
[file where event.action == "modification" and file.path : ("*/var/www/*", "*wwwroot*", "*htdocs*", "*public_html*") and file.extension : ("html", "htm", "php", "css", "js", "jpg", "png", "svg")]
[file where event.action == "modification" and file.path : ("*/var/www/*", "*wwwroot*", "*htdocs*", "*public_html*") and file.extension : ("html", "htm", "php", "css", "js", "jpg", "png", "svg")]
[file where event.action == "modification" and file.path : ("*/var/www/*", "*wwwroot*", "*htdocs*", "*public_html*") and file.extension : ("html", "htm", "php", "css", "js", "jpg", "png", "svg")] Elastic EQL sequence matching 3 or more web-root file modification events from the same host and user within a 15-minute window, using the Elastic Agent filesystem integration (logs-endpoint.events.file-*). This illustrative 3-step sequence approximates the mass-tampering pattern; a production rule should replace it with an Elastic detection-rule threshold aggregation (distinct file.path count >= 5 per host.name/user.name in 15m) for full parity with the KQL/SPL Branch 2 logic, since EQL sequence matching alone does not enforce a distinct-file-count threshold. Branch 1 (WAF admin-edit burst) should be implemented as a separate threshold rule over the web proxy/WAF integration dataset (logs-*.access-*), aggregating dcount(url.path) by source.ip.
Data Sources
Required Tables
False Positives & Tuning
- CI/CD deployment agents pushing an updated site build to the web root as part of a normal release
- CMS auto-update processes rewriting many PHP/HTML/CSS files during a scheduled update window
- Site migration or staging-to-production sync jobs copying a large static asset set into the web root
- Backup/restore jobs that touch many web root files during a scheduled maintenance window
Other platforms for THREAT-Impact-PublicWebsiteDefacement
Testing Methodology
Validate this detection against 3 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.
- Test 1Simulate Burst of CMS Admin Content-Edit Requests
Expected signal: WAF/reverse-proxy access logs (CommonSecurityLog) showing 10 POST requests to /wp-admin/post.php from the test source IP within a few minutes, targeting 10 distinct post IDs.
- Test 2Simulate Mass Static Asset Tampering on Web Root (Linux)
Expected signal: File-modification events (Sysmon Event ID 11 / auditd) for 6 distinct files under /tmp/atomictest-webroot within seconds of each other, written by a shell process.
- Test 3Simulate Mass Static Asset Tampering on IIS Web Root (Windows)
Expected signal: MDE DeviceFileEvents / Sysmon Event ID 11 entries for 5 distinct files under C:\atomictest\wwwroot within seconds, with InitiatingProcessFileName = powershell.exe.
References (6)
- https://attack.mitre.org/techniques/T1491/002/
- https://attack.mitre.org/techniques/T1491/
- https://attack.mitre.org/tactics/TA0040/
- https://owasp.org/www-community/attacks/Content_Spoofing
- https://www.cisa.gov/news-events/cybersecurity-advisories
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1491.002/T1491.002.md
Response Playbook
Triage
- Confirm the defacement is real, not a false positive: browse the affected page(s) directly (or check a cached/archived copy) to verify content was actually altered, rather than relying on the alert alone
- Identify the source: for Branch 1, pull the source IP and user session/account tied to the admin-edit burst from the WAF logs; for Branch 2, identify the initiating process and account that wrote the modified files on the host
- Determine the entry vector — check CMS admin login history for the account used (was it a known editor, or a first-time/anomalous login?) and check for recent plugin/theme installs or known CVEs in the CMS version if Branch 2 fired without a matching Branch 1 admin session
- Enumerate the full scope of modified content: list every distinct URL (Branch 1) or file (Branch 2) touched in the burst window, not just the homepage — attackers often modify multiple pages to maximize visibility or plant additional backdoors
- Check for webshells or backdoor files dropped alongside the defacement content (unexpected .php/.aspx/.jsp files with recent creation timestamps in or near the web root)
- Review DNS and CDN configuration for tampering — some defacement campaigns modify DNS records or CDN origin settings instead of (or in addition to) site content directly
Containment
- Take the affected page(s) or the entire site offline (maintenance mode, CDN-level block, or full server isolation) if the defaced content is actively serving to the public, to limit reputational damage and prevent secondary payload delivery
- Disable or reset credentials for the compromised CMS admin account(s) and any account showing anomalous login activity, and force session/token invalidation
- Isolate the host from the network if Branch 2 (direct filesystem tampering) fired, to stop an active RCE session or backdoor from re-deploying defacement content after cleanup
- Block the source IP(s) identified in the admin-edit burst at the WAF/firewall while investigation continues
- Restore the web root and CMS database from a known-good backup taken before the compromise window, rather than manually reverting individual files, since manual reversion can miss planted backdoors
Evidence Collection
- Full WAF/reverse-proxy access logs for the source IP and session covering the incident window and the 48-72 hours preceding it
- CMS admin audit log / revision history for every page or post touched, including before/after content diffs where the CMS retains revisions
- Host-level file modification records (Sysmon Event ID 11, MDE DeviceFileEvents) for the full web root directory tree, plus any newly created files not part of the original site build
- CMS plugin/theme inventory and version list at time of incident, cross-referenced against known CVEs, to establish the exploited vulnerability if the vector was unpatched software rather than stolen credentials
- A forensic copy (disk image or targeted file capture) of the affected web server before remediation begins, to preserve evidence of the exploitation path and any dropped webshells
Escalation Criteria
- !8 or more distinct admin-edit requests from a single source IP within 15 minutes with no matching change ticket or known editorial session
- !5 or more distinct static web assets modified directly on the filesystem within 15 minutes by a process outside the routine servicing exclusion list
- !Both branches fire against the same host in the same window (application-layer edit burst plus filesystem-layer tampering)
- !A webshell or other backdoor artifact is discovered alongside the defacement content
- !The affected site handles payment processing, authentication, or customer PII, escalating this from a reputational incident to a potential data-breach investigation
Investigation Guide
Related Techniques
Forensic Artifacts
- >
WAF/reverse-proxy access log entries (CommonSecurityLog CEF) showing the admin-edit request burst, including full RequestURL, SourceIP, and response codes - >
CMS application audit log / content revision history for every modified page or post - >
Endpoint file-modification telemetry (Sysmon Event ID 11, MDE DeviceFileEvents) for the web root directory tree, including InitiatingProcessFileName and InitiatingProcessAccountName - >
CMS admin authentication logs (login success/failure, source IP, session tokens) for the account used in the edit burst - >
Web server process list and recently created files in or near the web root, to identify webshells or other persistence artifacts dropped during the same incident window
Tuning Guidance
Build and maintain an allowlist of known CI/CD deployment agent IPs and CMS auto-updater service accounts that legitimately touch many pages/files in a short window, and exclude them by identity rather than by request pattern, since the request/file signatures are otherwise identical between legitimate deployment and malicious defacement. Tune MinEditCount and MinFilesModified down for smaller sites with infrequent legitimate bulk edits, and up for large multi-editor sites or sites with frequent scheduled content pushes — validate against your own 7-day baseline using the first hunting query before deploying to production. Treat the login-then-file-modification sequence (second hunting query) as materially higher confidence than either base detection branch alone, since it isolates the pattern with the fewest legitimate explanations. Where the site sits behind a CDN or WAF that does not forward CEF logs to your SIEM, prioritize deploying Branch 2 (endpoint file telemetry) alone as a compensating control.
Hunting Queries
7-day baseline hunt to distinguish routine editorial/automation traffic (steady, low daily volume, usually a recognized office IP range or CI/CD egress IP) from bursty or first-time high-volume callers hitting admin endpoints, which are the higher-suspicion population worth reviewing individually.
// Hunt: source IPs issuing any volume of POST/PUT requests to CMS admin endpoints over the last 7 days, to baseline normal editorial traffic volume vs anomalous callers
CommonSecurityLog
| where TimeGenerated > ago(7d)
| where RequestMethod in ("POST", "PUT")
| where RequestURL has_any (dynamic(["/wp-admin/", "/administrator/", "/umbraco/", "/sitecore/", "/node/add"]))
| summarize RequestCount = count(), DistinctURLs = dcount(RequestURL), ActiveDays = dcount(bin(TimeGenerated, 1d)) by SourceIP
| where DistinctURLs >= 3
| order by DistinctURLs desc index=waf sourcetype=cef (method=POST OR method=PUT)
| eval AdminPath=if(like(request_url,"%/wp-admin/%") OR like(request_url,"%/administrator/%") OR like(request_url,"%/umbraco/%") OR like(request_url,"%/sitecore/%") OR like(request_url,"%/node/add%"), 1, 0)
| where AdminPath=1
| stats count as RequestCount, dc(request_url) as DistinctURLs by src_ip
| where DistinctURLs >= 3
| sort - DistinctURLs Hunts for the specific sequence of a CMS admin login followed quickly by web root file modification from the same source IP — this ordering is a stronger compromise signal than either event alone, since it isolates fresh/anomalous logins that lead directly into content tampering rather than long-lived legitimate editorial sessions.
// Hunt: web root file modifications immediately preceded by a new/anomalous CMS admin login (within 30 minutes), correlating the credential-compromise and defacement stages
let AdminLogins = CommonSecurityLog
| where TimeGenerated > ago(7d)
| where RequestURL has_any (dynamic(["/wp-login.php", "/administrator/index.php", "/user/login"]))
| where RequestMethod == "POST"
| project LoginTime = TimeGenerated, SourceIP;
DeviceFileEvents
| where TimeGenerated > ago(7d)
| where ActionType == "FileModified"
| where FolderPath has_any (dynamic(["\\wwwroot\\", "/var/www/"]))
| join kind=inner (AdminLogins) on $left.RemoteIP == $right.SourceIP
| where TimeGenerated - LoginTime between (0min .. 30min)
| project TimeGenerated, DeviceName, FileName, SourceIP, LoginTime index=waf sourcetype=cef method=POST (like(request_url,"%/wp-login.php%") OR like(request_url,"%/administrator/index.php%") OR like(request_url,"%/user/login%"))
| rename src_ip as SourceIP
| join type=inner SourceIP
[ search index=wineventlog sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=11 (like(TargetFilename,"%wwwroot%") OR like(TargetFilename,"%/var/www/%")) | rename SourceIp as SourceIP ]
| where (_time - _time) <= 1800 Novelty-based companion to the Branch 1 admin-edit burst: rather than thresholding request volume, this flags a CMS content-publish or content-change action from a source IP that has never previously been associated with that admin account across a 90-day baseline — the credential-theft / session-hijack path folded in from the T1491.001 internal-defacement detection. Catches the low-volume defacement case (a single homepage edit from a new location) that the count-based burst branch would miss. Seed the 90-day baseline before enabling alerting, and consider excluding known corporate egress / VPN exit ranges to reduce travel-related noise.
// Hunt: CMS admin content-publish/content-change action from a source IP never previously seen for that admin account
// Novelty-based companion to the volume-based Branch 1 admin-edit burst — catches the low-and-slow defacement case (a single homepage/page edit after credential theft or session hijack) that a request-count threshold would miss.
// Requires a CMS/web-application audit log ingested as a custom table (modeled here as CMSAuditLogs_CL) exposing the admin account, action, and source IP.
let Lookback = 24h;
let KnownIPsPerAccount = CMSAuditLogs_CL
| where TimeGenerated between (ago(90d) .. ago(Lookback))
| where Action_s in ("login_success", "admin_login", "publish_post", "update_page")
| summarize KnownSourceIPs = make_set(SourceIP_s) by AdminAccount_s;
CMSAuditLogs_CL
| where TimeGenerated > ago(Lookback)
| where Action_s in ("publish_post", "update_page", "edit_theme_file", "plugin_install")
| lookup KnownIPsPerAccount on AdminAccount_s
| where isnotempty(SourceIP_s) and KnownSourceIPs !has SourceIP_s
| project TimeGenerated, AdminAccount_s, SourceIP_s, Action_s, KnownSourceIPs
| extend Indicator = "CMSAdminPublishFromNeverSeenSourceIP", RiskScore = 75
| sort by TimeGenerated desc index=cms sourcetype="cms:audit" Action IN ("publish_post","update_page","edit_theme_file","plugin_install")
| rename AdminAccount as AdminAccount_publish, SourceIP as SourceIP_publish
| join type=inner AdminAccount_publish
[ search index=cms sourcetype="cms:audit" Action IN ("login_success","admin_login","publish_post","update_page") earliest=-90d
| stats values(SourceIP) AS KnownSourceIPs by AdminAccount
| rename AdminAccount as AdminAccount_publish ]
| where NOT mvfind(KnownSourceIPs, SourceIP_publish) >= 0
| eval Indicator="CMSAdminPublishFromNeverSeenSourceIP", RiskScore=75
| table _time, AdminAccount_publish, SourceIP_publish, Action, KnownSourceIPs, Indicator, RiskScore
| sort - _time Atomic Red Team Tests
Issues a rapid burst of authenticated POST requests to a test CMS instance's admin content-edit endpoints, simulating the application-layer mass-edit pattern. Run only against a disposable lab/test CMS instance, never a production site.
Command
for i in $(seq 1 10); do curl -s -b "$COOKIE_JAR" -X POST "http://lab-cms.internal/wp-admin/post.php" -d "post_ID=$i&content=defaced-test-content&action=editpost" -o /dev/null; done Cleanup
Restore the original content of each test post/page from the CMS revision history, or delete the disposable test CMS instance entirely if it was created solely for this exercise. Expected Telemetry
WAF/reverse-proxy access logs (CommonSecurityLog) showing 10 POST requests to /wp-admin/post.php from the test source IP within a few minutes, targeting 10 distinct post IDs.
Expected Detection
Fires once EditCount >= 8 and DistinctURLs reflects the distinct post IDs for the same SourceIP within the 15-minute EditWindow.
Directly overwrites several static asset files in a disposable test web root directory in rapid succession, simulating the filesystem-layer defacement path used when an attacker has RCE or file-manager access bypassing the CMS UI.
Command
mkdir -p /tmp/atomictest-webroot && for f in index.html about.html contact.html style.css app.js logo.png; do echo '<html>DEFACED-ATOMIC-TEST</html>' > /tmp/atomictest-webroot/$f; done Cleanup
rm -rf /tmp/atomictest-webroot Expected Telemetry
File-modification events (Sysmon Event ID 11 / auditd) for 6 distinct files under /tmp/atomictest-webroot within seconds of each other, written by a shell process.
Expected Detection
Fires once FilesModified >= 5 for the same DeviceName/InitiatingProcessAccountName within the 15-minute EditWindow. Point the WebRootPaths list at the test directory when validating in a lab environment.
Overwrites several static asset files in a disposable test IIS web root in rapid succession from a non-service account context, simulating a Windows-hosted defacement scenario. Run only against a lab IIS instance.
Command
New-Item -ItemType Directory -Force -Path C:\atomictest\wwwroot | Out-Null; 'index.html','about.html','contact.html','style.css','app.js' | ForEach-Object { Set-Content -Path "C:\atomictest\wwwroot\$_" -Value '<html>DEFACED-ATOMIC-TEST</html>' } Cleanup
Remove-Item -Recurse -Force C:\atomictest\wwwroot Expected Telemetry
MDE DeviceFileEvents / Sysmon Event ID 11 entries for 5 distinct files under C:\atomictest\wwwroot within seconds, with InitiatingProcessFileName = powershell.exe.
Expected Detection
Fires once FilesModified >= 5 for the same DeviceName/InitiatingProcessAccountName within the 15-minute EditWindow. Point the WebRootPaths list at the test directory when validating in a lab environment.