|
| 1 | +require 'msf/core' |
| 2 | + |
| 3 | +class MetasploitModule < Msf::Auxiliary |
| 4 | + include Msf::Exploit::Remote::HttpClient |
| 5 | + include Msf::Auxiliary::Scanner |
| 6 | + include Msf::Auxiliary::Report |
| 7 | + |
| 8 | + def initialize(info = {}) |
| 9 | + super( |
| 10 | + update_info( |
| 11 | + info, |
| 12 | + 'Name' => 'ReDoc API Docs UI Exposed', |
| 13 | + 'Description' => %q{ |
| 14 | + Detects publicly exposed ReDoc API documentation pages which may reveal API surface, |
| 15 | + endpoints, request/response models, and other implementation details useful to attackers. |
| 16 | + }, |
| 17 | + 'Author' => [ |
| 18 | + |
| 19 | + ], |
| 20 | + 'License' => MSF_LICENSE, |
| 21 | + 'References' => [ |
| 22 | + [ 'URL', 'https://redocly.com/docs/redoc/' ] |
| 23 | + ], |
| 24 | + 'Actions' => [ [ 'Scan', { 'Description' => 'Scan for exposed ReDoc UI' } ] ], |
| 25 | + 'DefaultAction' => 'Scan' |
| 26 | + ) |
| 27 | + ) |
| 28 | + |
| 29 | + register_advanced_options([ |
| 30 | + OptString.new('REDOC_PATHS', [ true, 'Comma-separated paths to probe', '/redoc,/docs,/api/docs,/openapi,/redoc/' ]) |
| 31 | + ]) |
| 32 | + end |
| 33 | + |
| 34 | + # Returns :yes if the path looks like a ReDoc page, else :no |
| 35 | + def check_path(path) |
| 36 | + res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(path) }, 10) |
| 37 | + return :no if res.nil? |
| 38 | + |
| 39 | + code_ok = res.code && res.code.between?(200, 403) |
| 40 | + body = res.body.to_s |
| 41 | + |
| 42 | + title_hit = body =~ /<\s*title[^>]*>[^<]*redoc[^<]*<\/\s*title\s*>/i |
| 43 | + redoc_hit = body =~ /(redoc(?:\.standalone)?\.js|<\s*redoc-?)/i |
| 44 | + |
| 45 | + return :yes if code_ok && (title_hit || redoc_hit) |
| 46 | + :no |
| 47 | + end |
| 48 | + |
| 49 | + def run_host(ip) |
| 50 | + vprint_status("#{ip} - scanning for ReDoc") |
| 51 | + |
| 52 | + paths = (datastore['REDOC_PATHS'] || '').split(',').map(&:strip) |
| 53 | + paths = ['/redoc', '/docs', '/api/docs', '/openapi', '/redoc/'] if paths.empty? |
| 54 | + |
| 55 | + hit = paths.find { |p| check_path(p) == :yes } |
| 56 | + |
| 57 | + if hit |
| 58 | + print_good("#{ip} - ReDoc likely exposed at #{hit}") |
| 59 | + report_service( |
| 60 | + host: ip, |
| 61 | + port: rport, |
| 62 | + proto: 'tcp', |
| 63 | + name: (ssl ? 'https' : 'http') |
| 64 | + ) |
| 65 | + report_note( |
| 66 | + host: ip, |
| 67 | + port: rport, |
| 68 | + proto: 'tcp', |
| 69 | + type: 'http.redoc.exposed', |
| 70 | + data: { path: hit } |
| 71 | + ) |
| 72 | + else |
| 73 | + vprint_status("#{ip} - no ReDoc found") |
| 74 | + end |
| 75 | + end |
| 76 | +end |
0 commit comments