summaryrefslogtreecommitdiffstats
path: root/scripts/rtsp-url-brute.nse
blob: 7a96c9aa2643f24578ea4c90143580dd5a230b49 (plain)
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