1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
|
local coroutine = require "coroutine"
local io = require "io"
local nmap = require "nmap"
local rtsp = require "rtsp"
local shortport = require "shortport"
local stdnse = require "stdnse"
local table = require "table"
local rand = require "rand"
description = [[
Attempts to enumerate RTSP media URLS by testing for common paths on devices such as surveillance IP cameras.
The script attempts to discover valid RTSP URLs by sending a DESCRIBE
request for each URL in the dictionary. It then parses the response, based
on which it determines whether the URL is valid or not.
]]
---
-- @usage
-- nmap --script rtsp-url-brute -p 554 <ip>
--
-- @output
-- PORT STATE SERVICE
-- 554/tcp open rtsp
-- | rtsp-url-brute:
-- | discovered:
-- | rtsp://camera.example.com/mpeg4
-- | other responses:
-- | 401:
-- |_ rtsp://camera.example.com/live/mpeg4
-- @xmloutput
-- <table key="discovered">
-- <elem>rtsp://camera.example.com/mpeg4</elem>
-- </table>
-- <table key="other responses">
-- <table key="401">
-- <elem>rtsp://camera.example.com/live/mpeg4</elem>
-- </table>
-- </table>
--
-- @args rtsp-url-brute.urlfile sets an alternate URL dictionary file
-- @args rtsp-url-brute.threads sets the maximum number of parallel threads to run
--
-- Version 0.1
-- Created 23/10/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
--
author = "Patrik Karlsson"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"brute", "intrusive"}
portrule = shortport.port_or_service(554, "rtsp", "tcp", "open")
--- Retrieves the next RTSP relative URL from the datafile
-- @param filename string containing the name of the file to read from
-- @return url string containing the relative RTSP url
urlIterator = function(fd)
local function getNextUrl ()
repeat
local line = fd:read()
if ( line and not(line:match('^#!comment:')) ) then
coroutine.yield(line)
end
until(not(line))
fd:close()
while(true) do coroutine.yield(nil) end
end
return coroutine.wrap( getNextUrl )
end
local function fetch_url(host, port, url)
local helper = rtsp.Helper:new(host, port)
local status = helper:connect()
if not status then
stdnse.debug2("ERROR: Connecting to RTSP server url: %s", url)
return nil
end
local response
status, response = helper:describe(url)
if not status then
stdnse.debug2("ERROR: Sending DESCRIBE request to url: %s", url)
return nil, response
end
helper:close()
return true, response
end
-- Fetches the next url from the iterator, creates an absolute url and tries
-- to fetch it from the RTSP service.
-- @param host table containing the host table as received by action
-- @param port table containing the port table as received by action
-- @param url_iter function containing the url iterator
-- @param result table containing the urls that were successfully retrieved
local function processURL(host, port, url_iter, result)
local condvar = nmap.condvar(result)
local name = stdnse.get_hostname(host)
for u in url_iter do
local url = ("rtsp://%s%s"):format(name, u)
local status, response = fetch_url(host, port, url)
if not status then
table.insert(result, { url = url, status = -1 } )
break
else
table.insert(result, { url = url, status = response.status } )
end
end
condvar "signal"
end
action = function(host, port)
local response
local result = {}
local condvar = nmap.condvar(result)
local threadcount = stdnse.get_script_args('rtsp-url-brute.threads') or 10
local filename = stdnse.get_script_args('rtsp-url-brute.urlfile') or
nmap.fetchfile("nselib/data/rtsp-urls.txt")
threadcount = tonumber(threadcount)
if ( not(filename) ) then
return stdnse.format_output(false, "No dictionary could be loaded")
end
local f = io.open(filename)
if ( not(f) ) then
return stdnse.format_output(false, ("Failed to open dictionary file: %s"):format(filename))
end
local url_iter = urlIterator(f)
if ( not(url_iter) ) then
return stdnse.format_output(false, ("Could not open the URL dictionary: %s"):format(f))
end
-- Try to see what a nonexistent URL looks like
local status, response = fetch_url(
host, port, ("rtsp://%s/%s"):format(
stdnse.get_hostname(host), rand.random_alpha(14))
)
local status_404 = 404
if status then
local status_404 = response.status
end
local threads = {}
for t=1, threadcount do
local co = stdnse.new_thread(processURL, host, port, url_iter, result)
threads[co] = true
end
repeat
for t in pairs(threads) do
if ( coroutine.status(t) == "dead" ) then threads[t] = nil end
end
if ( next(threads) ) then
condvar "wait"
end
until( next(threads) == nil )
-- urls that could not be retrieved due to low level errors, such as
-- failure in socket send or receive
local failure_urls = {}
-- urls that elicited a 200 OK response
local success_urls = {}
-- urls that got some non-404-type response
local urls_by_code = {}
for _, r in ipairs(result) do
if ( r.status == -1 ) then
table.insert(failure_urls, r.url)
elseif ( r.status == 200 ) then
table.insert(success_urls, r.url)
elseif r.status ~= status_404 then
local s = tostring(r.status)
urls_by_code[s] = urls_by_code[s] or {}
table.insert(urls_by_code[s], r.url)
end
end
local output = stdnse.output_table()
if next(failure_urls) then
output.errors = failure_urls
end
if next(success_urls) then
output.discovered = success_urls
end
if next(urls_by_code) then
output["other responses"] = urls_by_code
end
if #output > 0 then
return output
end
end
|