Skip to content
Merged
34 changes: 34 additions & 0 deletions documentation/modules/auxiliary/scanner/http/redoc_exposed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## ReDoc API Docs UI Exposed

Detects publicly exposed ReDoc API documentation pages by looking for known DOM elements, script names, and titles. The module is read-only and makes safe GET requests.

### Module Options

* **RHOSTS** (required): Target address range or CIDR identifier.
* **RPORT**: Default `80` (overridable via `DefaultOptions` or at runtime).
* **SSL**: HTTPS support is registered by default (set if needed).
* **REDOC_PATHS**: Comma-separated custom paths to probe. If unset, defaults to:
`/redoc,/redoc/,/docs,/api/docs,/openapi`.

### Verification Steps

1. Start `msfconsole`.
2. `use auxiliary/scanner/http/redoc_exposed`
3. `set RHOSTS <target-or-range>`
4. (Optional) `set REDOC_PATHS /redoc,/docs`
5. (Optional) `set SSL true`
6. `run`

### Scenarios
```text
msf6 > use auxiliary/scanner/http/redoc_exposed
msf6 auxiliary(scanner/http/redoc_exposed) > set RHOSTS 192.0.2.0/24
msf6 auxiliary(scanner/http/redoc_exposed) > run
[+] 192.0.2.15 - ReDoc likely exposed at /docs
[*] 192.0.2.23 - no ReDoc found
```
### Notes

* **Stability**: `CRASH_SAFE` (GET requests only).
* **Reliability**: No session creation.
* **SideEffects**: Requests may appear in server logs (`IOC_IN_LOGS` if applicable).
90 changes: 90 additions & 0 deletions modules/auxiliary/scanner/http/redoc_exposed.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
##
# 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' => [] # Add IOC_IN_LOGS if server logs may record these requests
},
'DefaultOptions' => {
'RPORT' => 80
# SSL is registered by default; set here only if you want a non-default value
# 'SSL' => false
}
)
)

register_options(
[
OptString.new('REDOC_PATHS', [
false,
'Comma-separated list of paths to probe (overrides defaults)',
nil
])
]
)
end

# returns true if the response looks like a ReDoc page
def redoc_like?(res)
return false unless res && res.code.between?(200, 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")

paths =
if datastore['REDOC_PATHS'].to_s.empty?
['/redoc', '/redoc/', '/docs', '/api/docs', '/openapi']
else
datastore['REDOC_PATHS'].split(',').map(&:strip)
end

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