Skip to content
Merged
37 changes: 37 additions & 0 deletions documentation/modules/auxiliary/scanner/http/redoc_exposed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Vulnerable Application

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

### How It Works
- Prefers DOM checks (`<redoc>`, `#redoc`, or scripts containing `redoc` / `redoc.standalone`).
- Falls back to title/body heuristics for “redoc”.
- Considers only **2xx** and **403** responses (avoids noisy redirects).

## Verification Steps

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

## Options
### REDOC_PATHS
Comma-separated custom paths to probe. If unset, defaults to `/redoc,/redoc/,/docs,/api/docs,/openapi`

## 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`).
86 changes: 86 additions & 0 deletions modules/auxiliary/scanner/http/redoc_exposed.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
##
# 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