METASPLOIT

ReDoc API Docs UI Exposed_MSF:AUXILIARY-SCANNER-HTTP-REDOC_EXPOSED-

Description

Detects publicly exposed ReDoc API documentation pages. The module performs safe, read-only GET requests and reports likely ...
Visit Original Source

Basic Information

ID MSF:AUXILIARY-SCANNER-HTTP-REDOC_EXPOSED-
Published Oct 24, 2025 at 19:00

Affected Product

Affected Versions ##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
include Msf::Auxiliary::Scanner
include Msf::Exploit::Remote::HttpClient

def initialize(info = {})
super(
update_info(
info,
'Name' => 'ReDoc API Docs UI Exposed',
'Description' => %q{
Detects publicly exposed ReDoc API documentation pages.
The module performs safe, read-only GET requests and reports likely
ReDoc instances based on HTML markers.
},
'Author' => [
'Hamza Sahin (@hamzasahin61)'
],
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE], # GET requests only; should not crash or disrupt the target service
'Reliability' => [], # Does not establish sessions; leaving this empty is acceptable
'SideEffects' => [IOC_IN_LOGS] # Requests may be logged by the target web server
},
'DefaultOptions' => {
'RPORT' => 80
}
)
)

register_options(
[
# Mark as required and surface the built-in defaults here
OptString.new('REDOC_PATHS', [
true,
'Comma-separated list of paths to probe',
'/redoc,/redoc/,/docs,/api/docs,/openapi'
])
]
)
end

# returns true if the response looks like a ReDoc page
def redoc_like?(res)
# Accept only 2xx or 403 (exclude redirects; many 3xx lack HTML to analyze)
return false unless res && (res.code.between?(200, 299) || res.code == 403)

# Prefer DOM checks
doc = res.get_html_document
if doc && (doc.at_css('redoc, redoc-, #redoc') ||
doc.css('script[src*="redoc"]').any? ||
doc.css('script[src*="redoc.standalone"]').any?)
return true
end

# Fallback to body/title heuristics
title = res.get_html_title.to_s
body = res.body.to_s
return true if title =~ /redoc/i || body =~ /<redoc-?/i || body =~ /redoc(\.standalone)?\.js/i

false
end

def check_path(path)
redoc_like?(send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(path) }))
end

def run_host(ip)
vprint_status("#{ip} - scanning for ReDoc")

# REDOC_PATHS is required and has defaults; always use it directly
paths = datastore['REDOC_PATHS'].split(',').map(&:strip)

hit = paths.find { |p| check_path(p) }
if hit
print_good("#{ip} - ReDoc likely exposed at #{hit}")
report_service(host: ip, port: rport, proto: 'tcp', name: 'http')
else
vprint_status("#{ip} - no ReDoc found")
end
end
end

💭 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.