Skip to content

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.

Published on 6 min read

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:

StrategyDetectsWeakness
Compare to known-good release (hash manifest from CI)Any added, removed or modified fileRequires a disciplined deployment process
Compare to previous snapshot (FIM baseline)Any change since baselineBaseline may already include a shell
Heuristic hunting (location, extension, content, timing)Suspicious files without a baselineNoisier; 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.

PlatformPaths to monitor
IIS / ASP.NETSite 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 / PHPDocumentRoot and Alias targets, .htaccess, /etc/apache2 or /etc/httpd, PHP include_path, auto_prepend_file targets
nginx / PHP-FPMroot directives, PHP-FPM pool configs, php.ini
Tomcat / Javawebapps/, work/ (compiled JSPs), conf/, lib/
Node / PythonApplication 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:

SignalWhy it matters
Dynamic evaluation of request dataDirect path from HTTP input to code execution
Encoding/decoding chains around eval-like callsCommon obfuscation to evade string matching
Process execution APIs in a page that has no business running commandsCommand-runner capability
Very long single lines, high entropyPacked 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:

  1. Exclude non-executable churn — cache and session directories — but keep alerting on executable extensions within them.
  2. Correlate with deployments — suppress changes made by the deployment identity during change windows; alert on everything else.
  3. Separate writable from executable — if the architecture guarantees uploads can never execute, FIM there can be low priority.
  4. 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_NAME timestamps in the MFT. A creation time in $SI far earlier than $FN is a classic timestomping indicator. The USN journal also records the real creation event.
  • Linux: ctime changes whenever mtime is reset, so a file with an old mtime but a recent ctime is suspicious. Filesystem crtime (birth time) on ext4, XFS and Btrfs, visible via stat on 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/$FN timestamps, not just mtime.

Related guides

Indicators & detection

YARA and Sigma rules for web shell detection

Write, test and tune YARA rules for suspicious server-side scripts and Sigma rules for web shell behaviour in logs and process telemetry — with annotated examples and a false-positive strategy.