Detecting web shells with file integrity monitoring
How to baseline web roots, detect new or modified executable files, hunt with find, PowerShell and osquery, and tune file integrity monitoring so web shells stand out.
Most web shells are files. That makes the file system the single most reliable place to catch them — if you know what "normal" looks like. This guide covers building a baseline, hunting for deviations on Linux and Windows, and turning that into continuous file integrity monitoring (FIM) that does not drown you in noise.
The core idea: web roots should be boring
On a well-run server, executable content in a web root changes only during deployments. Everything else — a new .php file on a Tuesday afternoon, an .aspx file in aspnet_client, a modified web.config with no change ticket — is an anomaly worth a look.
That gives three detection strategies, from strongest to weakest:
| Strategy | Detects | Weakness |
|---|---|---|
| Compare to known-good release (hash manifest from CI) | Any added, removed or modified file | Requires a disciplined deployment process |
| Compare to previous snapshot (FIM baseline) | Any change since baseline | Baseline may already include a shell |
| Heuristic hunting (location, extension, content, timing) | Suspicious files without a baseline | Noisier; needs analyst review |
Know what to cover
Scope matters more than tooling. Cover every path from which the server will execute code, not just the obvious document root.
| Platform | Paths to monitor |
|---|---|
| IIS / ASP.NET | Site physical paths (commonly under C:\inetpub\wwwroot), virtual directories, aspnet_client, C:\Windows\Microsoft.NET\Framework*\v4.0.30319\Temporary ASP.NET Files, all web.config files, applicationHost.config |
| Exchange (on-prem) | %ExchangeInstallPath%FrontEnd\HttpProxy\owa\auth, ...\ecp\auth, C:\inetpub\wwwroot\aspnet_client |
| SharePoint | ...\Web Server Extensions\<version>\TEMPLATE\LAYOUTS |
| Apache / PHP | DocumentRoot and Alias targets, .htaccess, /etc/apache2 or /etc/httpd, PHP include_path, auto_prepend_file targets |
| nginx / PHP-FPM | root directives, PHP-FPM pool configs, php.ini |
| Tomcat / Java | webapps/, work/ (compiled JSPs), conf/, lib/ |
| Node / Python | Application directory, template directories, installed package directories if writable |
Configuration files deserve special attention: a changed handler mapping, auto_prepend_file, or an .htaccess that makes image extensions execute as scripts is a web shell without an obvious "shell file".
Hunting on Linux
Start with recently created or changed files in executable extensions. The inode change time (ctime) is updated whenever content or metadata changes and cannot be set with ordinary tools like touch, making it more trustworthy than mtime.
# Script files changed (ctime) in the last 14 days under the web root
find /var/www -type f \
\( -name '*.php' -o -name '*.phtml' -o -name '*.php[0-9]' -o -name '*.phar' \
-o -name '*.jsp' -o -name '*.jspx' -o -name '.htaccess' \) \
-ctime -14 -printf '%TY-%Tm-%Td %TH:%TM ctime:%CY-%Cm-%Cd %CH:%CM %u %p\n' | sort
Look at who owns each file. If your deployment user is deploy and a file is owned by www-data, the web server wrote it — which is exactly what happens when a shell is uploaded or dropped via an exploit.
# Executable-type files owned by the web server identity
find /var/www -type f -user www-data \( -name '*.php' -o -name '*.jsp' \) -ls
Script files in places that should only contain static assets are another strong signal:
find /var/www -type f \( -path '*/uploads/*' -o -path '*/images/*' -o -path '*/cache/*' \) \
\( -name '*.php*' -o -name '*.phtml' -o -name '*.jsp' \) -ls
If you package the application, the package manager can verify files against their shipped hashes (rpm -Va, debsums -c), and for WordPress wp core verify-checksums and wp plugin verify-checksums --all compare installed files to the official releases.
Hunting on Windows / IIS
PowerShell gives you the same view. Include CreationTime and LastWriteTime, and remember both can be timestomped.
$roots = 'C:\inetpub\wwwroot', "$env:ExchangeInstallPath\FrontEnd\HttpProxy"
$ext = '.aspx','.ashx','.asmx','.asp','.cshtml','.config'
Get-ChildItem -Path $roots -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $ext -contains $_.Extension.ToLower() -and $_.LastWriteTime -gt (Get-Date).AddDays(-14) } |
Select-Object LastWriteTime, CreationTime, Length, FullName |
Sort-Object LastWriteTime
Pay attention to files in aspnet_client — a directory that historically contains only static client scripts and was a favourite drop location during the 2021 Exchange campaigns — and to .aspx files in authentication folders of Exchange and SharePoint that are not part of the product.
Content heuristics
When you have no baseline, file content can prioritise review. Useful signals, none conclusive alone:
| Signal | Why it matters |
|---|---|
| Dynamic evaluation of request data | Direct path from HTTP input to code execution |
| Encoding/decoding chains around eval-like calls | Common obfuscation to evade string matching |
| Process execution APIs in a page that has no business running commands | Command-runner capability |
| Very long single lines, high entropy | Packed or encrypted payloads |
| Tiny file with only a few statements in an otherwise large app | "One-liner" loader style |
Mismatch between extension and content (e.g. script code in a .jpg) | Polyglot or handler-abuse technique |
These heuristics translate directly into YARA rules, covered in YARA and Sigma rules for web shells.
Continuous monitoring
One-off hunts find existing shells; continuous FIM finds the next one within minutes.
Linux: auditd and AIDE
auditd watches record who changed a file and which process did it — invaluable for attribution:
# /etc/audit/rules.d/50-webroot.rules
-w /var/www/ -p wa -k webroot_write
-w /etc/nginx/ -p wa -k webserver_conf
-w /etc/php/ -p wa -k php_conf
Query with ausearch -k webroot_write -i. An event whose exe is /usr/sbin/php-fpm* or /usr/sbin/apache2 writing a .php file is a high-priority alert.
AIDE (or Tripwire, Wazuh syscheck, OSSEC) provides periodic hash-based comparison against a stored database. Keep the database off the monitored host or read-only so an intruder cannot update it.
Windows: Sysmon file events
Sysmon Event ID 11 (FileCreate) captures the creating process. A focused configuration fragment:
<Sysmon schemaversion="4.90">
<EventFiltering>
<RuleGroup name="WebShellFileCreate" groupRelation="or">
<FileCreate onmatch="include">
<TargetFilename condition="begin with">C:\inetpub\</TargetFilename>
<TargetFilename condition="contains">\FrontEnd\HttpProxy\</TargetFilename>
<TargetFilename condition="contains">\TEMPLATE\LAYOUTS\</TargetFilename>
</FileCreate>
</RuleGroup>
</EventFiltering>
</Sysmon>
Alert when the Image is w3wp.exe (or UMWorkerProcess.exe on Exchange) and the target extension is executable. Legitimate IIS worker processes rarely write .aspx files.
Cross-platform: osquery
osquery's file and hash tables make fleet-wide hunts easy:
SELECT f.path, f.mtime, f.ctime, f.size, h.sha256
FROM file f JOIN hash h USING (path)
WHERE f.path LIKE '/var/www/%%'
AND (f.filename LIKE '%.php' OR f.filename LIKE '%.jsp')
AND f.ctime > (strftime('%s','now') - 86400*7);
On Windows, file_events (with FIM configured) or the ntfs_journal_events table on supported builds give near-real-time visibility.
Reducing noise
FIM on a busy CMS will alert on caches, sessions and uploaded media. Tune deliberately:
- Exclude non-executable churn — cache and session directories — but keep alerting on executable extensions within them.
- Correlate with deployments — suppress changes made by the deployment identity during change windows; alert on everything else.
- Separate writable from executable — if the architecture guarantees uploads can never execute, FIM there can be low priority.
- Watch config harder than content — handler and include changes are rare and high-impact.
Timestamps and timestomping
Attackers commonly copy timestamps from neighbouring files so a new shell appears years old. Two counter-measures:
- NTFS: compare
$STANDARD_INFORMATION(easily modified by user-mode APIs) with$FILE_NAMEtimestamps in the MFT. A creation time in$SIfar earlier than$FNis a classic timestomping indicator. The USN journal also records the real creation event. - Linux:
ctimechanges whenevermtimeis reset, so a file with an oldmtimebut a recentctimeis suspicious. Filesystemcrtime(birth time) on ext4, XFS and Btrfs, visible viastaton modern coreutils, is another anchor.
These techniques are covered in depth in the forensic investigation guide.
Checklist
- Inventory every path the web server executes from, including config files.
- Produce a hash manifest per release and compare production against it.
- Enable creator-aware file events (auditd, Sysmon EID 11, EDR).
- Alert on executable files written by the web server process.
- Protect the FIM baseline from tampering.
- Review ownership and
ctime/$FNtimestamps, not justmtime.