Skip to content

PersistenceExecutionNetwork

Web Shells in the Web Root: Finding Them on Linux Servers

Where Linux web roots live per distro, how PHP, JSP and CGI web shells and .htaccess or module backdoors look on disk, and how to date and hunt them.

Location
/var/www/ (Debian, RHEL Apache), /usr/share/nginx/html (RHEL nginx), /srv/www/htdocs (SUSE), /srv/http (Arch), Tomcat webapps
Proves
That a server-side script able to run attacker commands was planted in a served directory, when it was written, by which service account, and what it can do
Timestamps
File system times (birth time on ext4/XFS v5 dates the drop); first request time in access logs
Access
Read as root or the web server account; files usually owned by www-data, apache, nginx, wwwrun or http
Retention
Until deleted; deployments and CMS updates may overwrite or remove files
Collection
UAC, Velociraptor, cp -a, tar

What it is

A web shell is a script placed where the web server will execute it: a PHP file in an upload directory, a JSP in a Tomcat application, a CGI script, or a configuration change that makes the server execute something it should only serve. It is both the first persistence after exploiting a web application and the attacker's remote console afterwards (MITRE ATT&CK T1505.003). The access log shows it being used; this page is about the file on disk and the configuration around it.

Where it lives

ItemDebian / UbuntuRHEL familySUSEArch
Apache document root/var/www/html//var/www/html/, CGI in /var/www/cgi-bin//srv/www/htdocs/, CGI in /srv/www/cgi-bin//srv/http/
nginx default root/var/www/html//usr/share/nginx/html//srv/www/htdocs//usr/share/nginx/html/
Web server config/etc/apache2/, /etc/nginx//etc/httpd/, /etc/nginx//etc/apache2/, /etc/nginx//etc/httpd/, /etc/nginx/
Server modules/usr/lib/apache2/modules//usr/lib64/httpd/modules//usr/lib64/apache2*//usr/lib/httpd/modules/
PHP settings/etc/php/<ver>/*/php.ini, conf.d//etc/php.ini, /etc/php.d/, /etc/php-fpm.d//etc/php<ver>//etc/php/
Java apps/var/lib/tomcat<N>/webapps//var/lib/tomcat/webapps/ or /opt/tomcat/webapps//srv/tomcat/webapps//var/lib/tomcat<N>/webapps/ (varies by package)

Virtual hosts often point elsewhere (/srv/, /opt/, /home/<user>/public_html/, container volumes). Read DocumentRoot, Alias, root and ScriptAlias directives before searching.

What it proves

  • A file capable of running commands, reading files or proxying traffic existed in a location the web server executes.
  • When it was created (birth time) and last changed, and by which account (owner is usually the web server or PHP-FPM user when dropped through an exploit; a different owner suggests upload through SSH, FTP or a deploy pipeline).
  • Configuration-level backdoors: .htaccess that maps .jpg or .png to PHP, auto_prepend_file in .user.ini, .htaccess or php.ini that injects code into every page, and extra Apache or nginx modules.
  • It does not prove the shell was used; pair it with requests in the access log and process or network evidence.

Key fields

Typical content patterns (plain or obfuscated):

<?php @eval($_POST['x']); ?>
<?php if(isset($_REQUEST['c'])){system($_REQUEST['c']);} ?>
<?php $f=base64_decode(strrev('...')); @eval(gzinflate($f)); ?>
# .htaccess in an upload folder
AddType application/x-httpd-php .jpg
php_value auto_prepend_file /var/www/html/wp-content/uploads/2026/09/cache.jpg
IndicatorMeaning
eval, assert, create_function, preg_replace with /eDynamic code execution in PHP
system, exec, shell_exec, passthru, popen, proc_open, backticksOS command execution
base64_decode, gzinflate, str_rot13, strrev, long single-line blobsObfuscation
$_POST, $_REQUEST, $_COOKIE, getallheaders() feeding the aboveAttacker-controlled input
Runtime.getRuntime().exec, ProcessBuilder in .jspJava command execution
PHP file in uploads/, images/, cache/, tmp/Code where only data should be
.user.ini, .htaccess with auto_prepend_file, AddHandler, SetHandlerExecution via configuration

Timestamps

Birth time on ext4 and XFS v5 is the most reliable drop time because web shells are often timestomped to match neighbouring files (touch -r index.php shell.php changes mtime and atime, not ctime or crtime); see ext4 timestamps. The first request to the file in the access log should come seconds after its birth time; a first request long after suggests the file was dropped by another route.

Retention

Web shells stay until someone deletes them, but CMS auto-updates, deployment pipelines (rsync --delete, git clean) and container redeploys can remove them along with the evidence. In container setups, the shell may only exist in the container's writable layer; see container artifacts.

Collection

# Preserve the full web root with ownership, times and xattrs
tar --xattrs -C /mnt/evidence -cpf /cases/2026-017/webroot.tar var/www srv/www srv/http usr/share/nginx/html \
  etc/apache2 etc/httpd etc/nginx etc/php* var/lib/tomcat* 2>/dev/null
# Inventory: birth, modify, change (epoch; %W is 0 when birth time is unavailable), owner, size, path
find /mnt/evidence/var/www -xdev -type f -exec stat -c '%W\t%Y\t%Z\t%U\t%s\t%n' {} + > webroot_times.tsv

UAC collects Apache, nginx and Tomcat logs and /etc, but not web roots by default; add a file collector for your document roots. Velociraptor's Linux.Search.FileFinder and its YARA artifacts can sweep web roots across a fleet.

Parsing

W=/mnt/evidence/var/www
# Scripts in upload and media directories
find $W -xdev -type f \( -iname '*.php*' -o -iname '*.phtml' -o -iname '*.jsp*' -o -iname '*.cgi' \) \
  \( -path '*upload*' -o -path '*images*' -o -path '*cache*' \) 2>/dev/null
# Common primitives, obfuscation and very long lines
grep -rlE '(eval|assert|system|shell_exec|passthru|proc_open)\s*\(\s*(\$_(POST|GET|REQUEST|COOKIE)|base64_decode|gzinflate)' $W
find $W -type f -name '*.php' -exec awk 'length > 5000 {print FILENAME; exit}' {} +
grep -rnE 'auto_prepend_file|auto_append_file|AddType .*php|SetHandler|AddHandler' $W /mnt/evidence/etc/{apache2,httpd,php*} 2>/dev/null
# Signature scans
yara -r webshells.yar $W        # php-malware-finder ships PHP-tuned YARA rules usable here
# Files not owned by the CMS: WordPress example, run on a copy
wp core verify-checksums --path=/cases/copy/wordpress

Investigator tips

  • Sort the web root by birth time and read the files created around the first suspicious request in the access log; attackers usually drop more than one shell.
  • Check files owned by the web server account outside the web root too, especially /tmp, /var/tmp and /dev/shm, which are where shells stage tools.
  • On SELinux hosts, a web shell that tried to run a binary from a writable directory often left httpd_t AVC denials.
  • Files written by the database (INTO OUTFILE) are owned by mysql, not the web user; see database logs.
  • Verify server modules against the package database (rpm -V httpd, dpkg -V apache2-bin nginx-core) and list loaded ones (apachectl -M); a malicious module is a web shell that no file scan of the document root will find.

See also