← all walkthroughs

RainyDay

Linux· Hard
owned
2026-10-14
time to own
20m54s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I logged into a Flask-based 'RainyCloud' container-management web app using a weak, crackable password for a low-privileged user, then used a leaked application signing key to forge a session cookie for a second user, 'jack'. Jack's container shared the host's process namespace, which exposed his home directory — including his SSH private key — through /proc.

SSH access as jack led to a sudo rule running a 'safe' Python wrapper that could be escaped to become the 'jack_adm' user. Jack_adm could run a root-owned password-hashing script whose secret salt could be recovered and combined with a weak, cracked password, and that password was reused for the root account — 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 PASSWORD3="<the-password-you-recovered, for gary>"
export KEY="<the-key-you-recovered>"

Some commands also carry a value in angle brackets — <base64_payload>, <container_id>, <forged_jack_cookie> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1Initial AccessWeak credentials / password cracking (T1110)
Logged into the web application with a weak, guessable password
The target exposed only SSH and an nginx-fronted Flask application on rainycloud.htb. A user-enumeration weakness in the API exposed account password hashes, and the account 'gary' used the weak password. Logging in granted a valid session and access to the container-management features (create and execute containers).
POST /login with gary's password returned HTTP 302 to / and issued a Flask session cookie.
Exact commands 3
shell

Add the application vhosts to local resolution.

$ echo "$TARGET rainycloud.htb dev.rainycloud.htb" | sudo tee -a /etc/hosts
shell

Confirms only 22/tcp and 80/tcp are open.

$ nmap -p- --min-rate=1000 $TARGET
shell

Authenticate as gary and capture the session cookie.

$ curl --resolve rainycloud.htb:80:$TARGET -s -i -m 10 -X POST http://rainycloud.htb/login --data-urlencode username=gary --data-urlencode password=$PASSWORD3
FixEnforce strong passwords and stop exposing password hashes through the APIHigh
WeaknessA user account used a trivially guessable password, and the API's user lookup returned password hashes, letting an unauthorised user crack credentials offline and sign in.
FixRequire long, unique passwords with a banned-password list, never return password hashes from any API response, enforce authorization on /api/user/<id> lookups, and add rate limiting and lockout on login.
2Credential AccessFlask session forgery with a leaked secret key (T1606)
Forged a session cookie for the user 'jack' using the leaked signing key
The application's Flask SECRET_KEY was readable through an internal development service, which let me sign arbitrary session cookies. A locally recomputed signature matched gary's real cookie exactly, proving the key was the production secret. A cookie asserting the username 'jack' was then accepted by the app and revealed jack's containers, including one named 'secrets'.
Recomputed HMAC-SHA1 reproduced gary's cookie signature; a forged jack cookie returned HTTP 200 with jack's container list.
Exact commands 2
shell

Sign a session cookie for jack using the recovered SECRET_KEY.

$ flask-unsign -s --secret "$KEY" --cookie '{"username":"jack"}'
shell

Confirm the forged cookie is accepted and list jack's containers.

$ curl --resolve rainycloud.htb:80:$TARGET -s -m 30 http://rainycloud.htb/containers -H 'Cookie: session=<forged_jack_cookie>'
FixProtect the Flask SECRET_KEY and rotate itCritical
WeaknessThe application's session-signing SECRET_KEY was readable through an internal development service, allowing anyone with it to forge a session for any user.
FixStore the key outside the web root in a secrets manager or environment variable, remove file-read capabilities from the dev service, rotate the key now, and move to server-side sessions with an additional per-user check on sensitive actions.
3ExecutionAuthenticated command execution in a container runner (T1609)
Ran commands inside jack's container through the application's exec feature
The /containers endpoint let an authenticated user execute arbitrary shell commands inside any container they owned. Using the forged jack session, I ran commands in jack's 'secrets' container, which ran as uid 1000. Shell payloads were base64-encoded to avoid quoting problems.
Command output showed uid=1000 gid=1000 inside container c0fe876cc336.
Exact commands 1
shell

Run a base64-wrapped shell payload in the container; replace <container_id> with the 'secrets' container ID.

$ curl --resolve rainycloud.htb:80:$TARGET -s -m 30 -X POST http://rainycloud.htb/containers -H 'Cookie: session=<forged_jack_cookie>' --data-urlencode "action=exec sh -c 'echo <base64_payload> | base64 -d | sh'" --data-urlencode id=<container_id>"
FixRestrict who and what the container exec feature can reachHigh
WeaknessThe application let any authenticated session run arbitrary shell commands in containers with no re-authentication or auditing, so a forged session immediately became code execution.
FixRequire re-authentication or step-up approval for exec actions, limit exec to explicitly allow-listed commands, and log and alert on all exec calls.
4Container Escape / FootholdShared host PID namespace abuse via /proc/<pid>/cwd (T1611) and SSH key theft (T1552.004)
Read jack's home directory and SSH key through the shared host process namespace
The container was started with the host PID namespace, so host processes were visible inside it. A host process owned by jack had its working directory set to /home/jack, and following the /proc/<pid>/cwd symlink exposed jack's home — including user.txt and the private key .ssh/id_rsa. The key was used to log in over SSH as jack.
CWD /proc/1185 -> /home/jack; id_rsa (2602 bytes) read; ssh as jack returned uid=1000(jack) on host 'rainyday'.
Exact commands 3
shell

Run inside the container to find host processes whose cwd is a home directory.

$ for p in /proc/[0-9]*; do c=$(readlink $p/cwd 2>/dev/null); case "$c" in /home/*|/root*) echo "$p -> $c";; esac; done
shell

Read the user flag (<user.txt>) and jack's private key via the cwd symlink.

$ cat /proc/1185/cwd/user.txt; cat /proc/1185/cwd/.ssh/id_rsa
shell

Log in over the public SSH port as jack.

$ chmod 600 jack_id_rsa && ssh -i jack_id_rsa jack@$TARGET 'id; hostname; sudo -l'
uid=1000(jack)
FixDo not run user containers in the host PID namespaceCritical
WeaknessContainers were started with --pid=host, so processes inside could read host processes' working directories and files through /proc, exposing a user's home directory and SSH private key.
FixRemove the host PID namespace setting, keep each container in its own PID namespace, drop unneeded capabilities, and protect SSH keys with passphrases.
5Lateral MovementPython sandbox escape via __subclasses__ gadget traversal (T1548.003)
Escaped the restricted Python sandbox to become jack_adm
Jack could run /usr/bin/safe_python as jack_adm without a password. The wrapper removed __import__ and other dangerous builtins, but still exposed enough of the Python object model to walk object.__subclasses__(), find a class whose __init__.__globals__ held an unrestricted __builtins__, and recover __import__. This gave full Python code execution as jack_adm, which was used to obtain a shell and SSH access as that user.
Sudo -l showed (jack_adm) NOPASSWD: /usr/bin/safe_python *; the builtins dump listed only open, print, bytearray and similar; subsequent session ran as uid=1002(jack_adm).
Exact commands 4
shell

Reveals (jack_adm) NOPASSWD: /usr/bin/safe_python *.

$ sudo -l
shell

Inspect which builtins the sandbox leaves available.

$ printf 'print(__builtins__)\n' > /tmp/t.py; sudo -u jack_adm /usr/bin/safe_python /tmp/t.py
shell

Run a sandbox-escape script that walks ().__class__.__base__.__subclasses__() to recover __import__, then installs jack's public key in /home/jack_adm/.ssh/authorized_keys.

$ sudo -u jack_adm /usr/bin/safe_python /tmp/escape.py
shell

Log in as jack_adm; sudo -l shows (root) NOPASSWD: /opt/hash_system/hash_password.py.

$ ssh -i jack_id_rsa jack_adm@$TARGET 'id; hostname; sudo -l'
uid=1002(jack_adm)
FixRemove the sudo rule for the Python sandbox wrapperHigh
Weaknessjack was allowed to run a Python 'sandbox' as another user without a password, and the sandbox only removed a few builtins, which is not a security boundary and could be bypassed.
FixRemove the NOPASSWD sudo rule for safe_python; if sandboxing is required, use OS-level isolation (separate unprivileged user, seccomp, containers) rather than in-language restrictions.
6Privilege Escalationbcrypt 72-byte truncation oracle / secret salt disclosure (CWE-208, CWE-257)
Leaked the secret salt from the root-run password-hashing helper
Jack_adm could run /opt/hash_system/hash_password.py as root. It bcrypt-hashed supplied input after appending a secret salt server-side. Because bcrypt only uses the first 72 bytes and the script limited length by characters rather than bytes, multi-byte characters could be used to push the salt past the cutoff one byte at a time, allowing it to be recovered by comparing local hashes against the returned ones. The recovered salt was then applied to cracking root's password hash.
Echo htb | sudo -n /opt/hash_system/hash_password.py returned the hash, a bcrypt hash that incorporated the hidden salt.
Exact commands 2
shell

Run the helper as root via sudo and obtain a bcrypt hash that includes the secret salt.

$ echo htb | sudo -n /opt/hash_system/hash_password.py
shell

Pad input with 4-byte emoji characters to exactly 71 bytes so one salt byte lands inside the 72-byte window; test each printable byte locally with bcrypt.checkpw and repeat until the full salt is recovered.

$ python3 salt_leak.py
FixFix the password-hashing helper and remove its root sudo ruleHigh
WeaknessA helper run as root appended a secret salt after user input and measured length in characters, so bcrypt's 72-byte truncation let an unauthorised user recover the secret one byte at a time.
FixRemove the sudo rule or run the helper unprivileged; never append secrets to user input before hashing, use a per-hash random salt handled by the bcrypt library, and enforce limits in bytes.
7Privilege EscalationOffline bcrypt cracking with a custom rule (T1110.002) and password reuse (T1078)
Cracked root's password using the leaked salt and logged in as root
With the salt known, root's bcrypt hash (obtained earlier through the application's user API) was attacked with a salt-aware hashcat rule against rockyou.txt, recovering a weak password. The same password was reused for the operating-system root account, so su - root succeeded and gave full control of the host.
Hashcat job recovered the password; su - root returned uid=0(root) gid=0(root) on host 'rainyday' and /root/root.txt was readable.
Exact commands 3
shell

Hashcat/John rule that appends the leaked salt to each candidate.

$ printf 'Az"'"$KEY"'"\n' > /tmp/work/salt.rule
shell

Crack root's bcrypt hash.

$ hashcat -m 3200 -a 0 -r /tmp/work/salt.rule --potfile-disable -o /tmp/work/root.cracked /tmp/work/root.hash /usr/share/wordlists/rockyou.txt
shell

Enter the cracked password; the flag is <root.txt>.

$ su - root -c 'id; hostname; cat /root/root.txt'
uid=0(root) gid=0(root)
FixUse unique, strong passwords for the root accountCritical
WeaknessThe root OS password was weak, matched the application's stored password hash, and was crackable offline once the salt was known.
FixSet a long random root password that is not shared with any application account, disable password login for root where possible (SSH keys, sudo with MFA), and rotate it now.

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

Exposed services

22/tcp
80/tcp