Skip to content

Commit d1c9410

Browse files
authored
Merge pull request #20594 from HamzaSahin61/feat/redoc-exposed-scanner
auxiliary(scanner/http/redoc_exposed): detect exposed ReDoc API docs UI
2 parents 5d73d8a + e17b2a0 commit d1c9410

File tree

2 files changed

+123
-0
lines changed

2 files changed

+123
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
## Vulnerable Application
2+
3+
Detects publicly exposed ReDoc API documentation pages by looking for known DOM elements and script names. The module
4+
is read-only and sends safe `GET` requests.
5+
6+
### How It Works
7+
- Prefers DOM checks (`<redoc>`, `#redoc`, or scripts containing `redoc` / `redoc.standalone`).
8+
- Falls back to title/body heuristics for “redoc”.
9+
- Considers only **2xx** and **403** responses (avoids noisy redirects).
10+
11+
## Verification Steps
12+
13+
1. Start `msfconsole`.
14+
2. `use auxiliary/scanner/http/redoc_exposed`
15+
3. `set RHOSTS <target-or-range>`
16+
4. (Optional) `set SSL true`
17+
5. (Optional) `set REDOC_PATHS /redoc,/docs`
18+
6. `run`
19+
20+
## Options
21+
### REDOC_PATHS
22+
Comma-separated custom paths to probe. If unset, defaults to `/redoc,/redoc/,/docs,/api/docs,/openapi`
23+
24+
## Scenarios
25+
26+
```text
27+
msf6 > use auxiliary/scanner/http/redoc_exposed
28+
msf6 auxiliary(scanner/http/redoc_exposed) > set RHOSTS 192.0.2.0/24
29+
msf6 auxiliary(scanner/http/redoc_exposed) > run
30+
[+] 192.0.2.15 - ReDoc likely exposed at /docs
31+
[*] 192.0.2.23 - no ReDoc found
32+
```
33+
## Notes
34+
35+
* **Stability**: `CRASH_SAFE` (GET requests only).
36+
* **Reliability**: No session creation.
37+
* **SideEffects**: Requests may appear in server logs (`IOC_IN_LOGS`).
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Auxiliary
7+
include Msf::Auxiliary::Scanner
8+
include Msf::Exploit::Remote::HttpClient
9+
10+
def initialize(info = {})
11+
super(
12+
update_info(
13+
info,
14+
'Name' => 'ReDoc API Docs UI Exposed',
15+
'Description' => %q{
16+
Detects publicly exposed ReDoc API documentation pages.
17+
The module performs safe, read-only GET requests and reports likely
18+
ReDoc instances based on HTML markers.
19+
},
20+
'Author' => [
21+
'Hamza Sahin (@hamzasahin61)'
22+
],
23+
'License' => MSF_LICENSE,
24+
'Notes' => {
25+
'Stability' => [CRASH_SAFE], # GET requests only; should not crash or disrupt the target service
26+
'Reliability' => [], # Does not establish sessions; leaving this empty is acceptable
27+
'SideEffects' => [IOC_IN_LOGS] # Requests may be logged by the target web server
28+
},
29+
'DefaultOptions' => {
30+
'RPORT' => 80
31+
}
32+
)
33+
)
34+
35+
register_options(
36+
[
37+
# Mark as required and surface the built-in defaults here
38+
OptString.new('REDOC_PATHS', [
39+
true,
40+
'Comma-separated list of paths to probe',
41+
'/redoc,/redoc/,/docs,/api/docs,/openapi'
42+
])
43+
]
44+
)
45+
end
46+
47+
# returns true if the response looks like a ReDoc page
48+
def redoc_like?(res)
49+
# Accept only 2xx or 403 (exclude redirects; many 3xx lack HTML to analyze)
50+
return false unless res && (res.code.between?(200, 299) || res.code == 403)
51+
52+
# Prefer DOM checks
53+
doc = res.get_html_document
54+
if doc && (doc.at_css('redoc, redoc-, #redoc') ||
55+
doc.css('script[src*="redoc"]').any? ||
56+
doc.css('script[src*="redoc.standalone"]').any?)
57+
return true
58+
end
59+
60+
# Fallback to body/title heuristics
61+
title = res.get_html_title.to_s
62+
body = res.body.to_s
63+
return true if title =~ /redoc/i || body =~ /<redoc-?/i || body =~ /redoc(\.standalone)?\.js/i
64+
65+
false
66+
end
67+
68+
def check_path(path)
69+
redoc_like?(send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(path) }))
70+
end
71+
72+
def run_host(ip)
73+
vprint_status("#{ip} - scanning for ReDoc")
74+
75+
# REDOC_PATHS is required and has defaults; always use it directly
76+
paths = datastore['REDOC_PATHS'].split(',').map(&:strip)
77+
78+
hit = paths.find { |p| check_path(p) }
79+
if hit
80+
print_good("#{ip} - ReDoc likely exposed at #{hit}")
81+
report_service(host: ip, port: rport, proto: 'tcp', name: 'http')
82+
else
83+
vprint_status("#{ip} - no ReDoc found")
84+
end
85+
end
86+
end

0 commit comments

Comments
 (0)