diff --git a/CHANGELOG b/CHANGELOG index aa7c94838..439704386 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the scripts rtsp-url-brute, rtsp-methods and the supporting rtsp + library. The scripts check the supported RTSP methods and attempt to brute + force valid RTSP urls. [Patrik] + o [NSE] Added the http-robtex-reverse-ip script that uses the Robtex service to perform a reverse lookup in order to discover all names associated with the IP. [riemann] diff --git a/nselib/data/rtsp-urls.txt b/nselib/data/rtsp-urls.txt new file mode 100644 index 000000000..c9d0840bb --- /dev/null +++ b/nselib/data/rtsp-urls.txt @@ -0,0 +1,76 @@ +#!comment: The following dictionary contains a list of well-known RTSP URLs +#!comment: used by common video surveillance equipment. +/ +/1.AMP +/1/stream1 +/CAM_ID.password.mp2 +/CH001.sdp +/GetData.cgi +/MediaInput/h264 +/MediaInput/mpeg4 +/VideoInput/1/h264/1 +/access_code +/access_name_for_stream_1_to_5 +/av0_0 +/av2 +/avn=2 +/axis-media/media.amp +/cam +/cam0_0 +/cam0_1 +/cam1/h264 +/cam1/h264/multicast +/cam1/mjpeg +/cam1/mpeg4 +/camera.stm +/ch0 +/ch001.sdp +/ch0_unicast_firststream +/ch0_unicast_secondstream +/channel1 +/h264 +/h264/media.amp +/image.mpg +/img/media.sav +/img/video.asf +/img/video.sav +/ioImage/1 +/ipcam.sdp +/ipcam_h264.sdp +/live.sdp +/live/h264 +/live/mpeg4 +/live_mpeg4.sdp +/livestream +/livestream/ +/media/media.amp +/media/video1 +/mjpeg/media.smp +/mp4 +/mpeg4 +/mpeg4/1/media.amp +/mpeg4/media.amp +/mpeg4/media.smp +/mpeg4unicast +/mpg4/rtsp.amp +/multicaststream +/now.mp4 +/nph-h264.cgi +/nphMpeg4/g726-640x +/nphMpeg4/g726-640x480 +/nphMpeg4/nil-320x240 +/play1.sdp +/play2.sdp +/rtpvideo1.sdp +/rtsp_tunnel +/rtsph264 +/stream1 +/user.pin.mp2 +/user_defined +/video +/video.3gp +/video.mp4 +/video1 +/video1+audio1 +/vis +/wfov diff --git a/nselib/rtsp.lua b/nselib/rtsp.lua new file mode 100644 index 000000000..f8133793c --- /dev/null +++ b/nselib/rtsp.lua @@ -0,0 +1,286 @@ +--- +-- This Real Time Streaming Protocol (RTSP) library implements only a minimal +-- subset of the protocol needed by the current scripts. +-- +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- @author Patrik Karlsson +-- +-- The library contains the following classes: +-- +-- * Request +-- ** This class contains the functions needed to create the RTSP request +-- +-- * Response +-- ** This class contains the functions needed to parse the RTSP response +-- +-- * Client +-- ** This class contains the RTSP client, a class responsible for sending +-- and receiving requests and responses to/from the server +-- +-- * Helper +-- ** This class serves as the main interface for script writers +-- +-- The following sample code shows how to use the library: +-- +-- local helper = rtsp.Helper:new(host, port) +-- local status = helper:connect() +-- local response +-- status, response = helper:describe(url) +-- helper:close() +-- + +-- +-- Version 0.1 +-- Created 10/23/2011 - v0.1 - Created by Patrik Karlsson +-- + +module(... or "rtsp", package.seeall) + +-- The RTSP Request object +Request = { + + --- Creates a new Request instance + -- @return o instance of Request + new = function(self, url, headers) + local o = { url = url, req = {}, headers = headers or {} } + setmetatable(o, self) + self.__index = self + return o + end, + + --- Sets the RTSP Request method + -- @param method string containing the RTSP method + setMethod = function(self, method) + self.method = method + end, + + --- Sets the RTSP sequence number + -- @param cseq number containing the sequence number + setCSeq = function(self, cseq) + self.cseq = cseq + end, + + --- Adds an optional header to the RTSP request + -- @param header string containing the header name + -- @param value string containing the header value + addHeader = function(self, header, value) + table.insert( self.headers, { header = value } ) + end, + + --- Converts the Request to a string + -- + -- @return req string containing the request as a string + __tostring = function(self) + assert(self.cseq, "Request is missing required header CSeq") + assert(self.url, "Request is missing URL") + + local req = stdnse.strjoin("\r\n", { + ("%s %s RTSP/1.0"):format(self.method, self.url), + ("CSeq: %d"):format(self.cseq) + } ) .. "\r\n" + if ( #self.headers > 0 ) then + req = req .. stdnse.strjoin("\r\n", self.headers) .. "\r\n" + end + + return req .. "\r\n" + end, +} + +-- The RTSP response instance +Response = { + + --- Creates a new Response instance + -- @param data string containing the unparsed data + new = function(self, data) + assert(data, "No data was supplied") + local o = { + raw = data, + status = tonumber(data:match("^RTSP%/1%.0 (%d*) ")) + } + + -- Split the response into a temporary array + local tmp = stdnse.strsplit("\r\n", data) + if ( not(tmp) ) then return nil end + + -- we should have atleas one entry + if ( #tmp > 1 ) then + o.headers = {} + for i=2, #tmp do + -- if we have an empty line, this should be the end of headers + if ( #tmp[i] == 0 ) then break end + local key, val = tmp[i]:match("^(.-): (.*)$") + -- create a key per header name + o.headers[key] = val + end + end + + setmetatable(o, self) + self.__index = self + return o + end, + +} + + +-- RTSP Client class +Client = { + + -- Creates a new Client instance + -- @param host table as received by the action method + -- @param port table as received by the action method + -- @return o instance of Client + new = function(self, host, port) + local o = { + host = host, + port = port, + cseq = 0, + headers = { }, + retries = 3, + timeout = 10 * 1000, + } + setmetatable(o, self) + self.__index = self + return o + end, + + --- Sets the number of retries for socket reads + -- @param retries number containing the number of retries + setRetries = function(self, retries) self.retries = retries end, + + --- Sets the socket connection timeout in ms + -- @param timeout number containing the timeout in ms + setTimeout = function(self, timeout) self.timeout = timeout end, + + --- Adds a RTSP header to the request + -- @param header string containing the header name + -- @param value string containing the header value + addHeader = function(self, header, value) + table.insert(self.headers, { ("%s: %s"):format(header,value) } ) + end, + + --- Connects to the RTSP server + -- @return status true on success, false on failure + -- @return err string containing the error message on failure + connect = function(self) + self.socket = nmap.new_socket() + self.socket:set_timeout(self.timeout) + local status = self.socket:connect(self.host, self.port) + if ( not(status) ) then + stdnse.print_debug(2, "Failed to connect to the server: %s", self.host.ip) + return false, ("Failed to connect to the server: %s"):format(self.host.ip) + end + return true + end, + + --- Sends a DESCRIBE request to the server and receives the response + -- @param url string containing the RTSP URL + -- @return status true on success, false on failure + -- @return response Response instance on success + -- err string containing the error message on failure + describe = function(self, url) + local req = Request:new(url, self.headers) + req:setMethod("DESCRIBE") + return self:exch(req) + end, + + options = function(self, url) + local req = Request:new(url, self.headers) + req:setMethod("OPTIONS") + return self:exch(req) + end, + + --- Sends a request to the server and receives the response and attempts + -- to retry if either send or receive fails. + -- @param request instance of Request + -- @return status true on success, false on failure + -- @return response Response instance on success + -- err string containing the error message on failure + exch = function(self, req) + local retries = self.retries + local status, data + self.cseq = self.cseq + 1 + req:setCSeq( self.cseq ) + + repeat + local err + status, err = self.socket:send( tostring(req) ) + -- check if send was successfull, in case it wasn't AND + -- this is our last retry, ABORT + if ( not(status) and 0 == retries - 1 ) then + stdnse.print_debug(2, "Failed to send request to server (%s)", err) + return false, ("Failed to send request to server (%s)"):format(err) + -- if send was successfull, attempt to receive the response + elseif ( status ) then + status, data = self.socket:receive() + -- if we got the response allright, break out of retry loop + if ( status ) then break end + end + -- if either send or receive fails, re-connect the socket + if ( not(status) ) then + self:close() + local status, err = self:connect() + -- if re-connect fails, BAIL out of here + if ( not(status) ) then + stdnse.print_debug(2, "Failed to reconnect socket to server (%s)", err) + return false, ("Failed to reconnect socket to server (%s)"):format(err) + end + end + retries = retries - 1 + until( status or retries == 0 ) + + if( not(status) ) then + stdnse.print_debug(2, "Failed to receive response from server (%s)", data) + return false, ("Failed to receive response from server (%s)"):format(data) + end + + return true, Response:new(data) + end, + + --- Closes the RTSP socket with the server + close = function(self) + return self.socket:close() + end, + +} + +-- The Helper class is the main script interface +Helper = { + + -- Creates a new Helper instance + -- @param host table as received by the action method + -- @param port table as received by the action method + -- @return o instance of Client + new = function(self, host, port) + local o = { host = host, port = port, client = Client:new(host, port) } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Connects to the RTSP server + -- @return status true on success, false on failure + -- @return err string containing the error message on failure + connect = function(self) + return self.client:connect() + end, + + -- Closes the RTSP socket with the server + close = function(self) + return self.client:close() + end, + + -- Sends a DESCRIBE request to the server and receives the response + -- + -- @param url string containing the RTSP URL + -- @return status true on success, false on failure + -- @return response string containing the unparsed RTSP response on success + -- err string containing the error message on failure + describe = function(self, url) + return self.client:describe(url) + end, + + options = function(self, url) + return self.client:options(url) + end, + +} \ No newline at end of file diff --git a/scripts/rtsp-methods.nse b/scripts/rtsp-methods.nse new file mode 100644 index 000000000..4554d22c8 --- /dev/null +++ b/scripts/rtsp-methods.nse @@ -0,0 +1,47 @@ +description = [[ +Finds out what methods are supported by the RTSP server. +]] + +--- +-- @usage +-- nmap -p 554 --script rtsp-methods +-- +-- @output +-- PORT STATE SERVICE +-- 554/tcp open rtsp +-- | rtsp-methods: +-- |_ DESCRIBE, SETUP, PLAY, TEARDOWN, OPTIONS +-- +-- @args rtsp-methods.path the path to query, defaults to "*" which queries +-- the server itself, rather than a specific url. +-- + +-- +-- Version 0.1 +-- Created 23/10/2011 - v0.1 - created by Patrik Karlsson +-- +author = "Patrik Karlsson " +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"default", "safe"} + +require 'rtsp' +require 'shortport' + +portrule = shortport.port_or_service(554, "rtsp", "tcp", "open") + +action = function(host, port) + local path = stdnse.get_script_args('rtsp-methods.path') or '*' + local helper = rtsp.Helper:new(host, port) + local status = helper:connect() + if ( not(status) ) then + stdnse.print_debug(2, "ERROR: Failed to connect to RTSP server") + return + end + + local response + status, response = helper:options(path) + helper:close() + if ( status ) then + return stdnse.format_output(true, response.headers['Public']) + end +end diff --git a/scripts/rtsp-url-brute.nse b/scripts/rtsp-url-brute.nse new file mode 100644 index 000000000..be1f0a5c5 --- /dev/null +++ b/scripts/rtsp-url-brute.nse @@ -0,0 +1,156 @@ +description = [[ +Attempts to brute common RTSP media URLs for devices such as surveillance IP +cameras. +]] + +--- +-- @usage +-- nmap --script rtsp-url-brute -p 554 +-- +-- @output +-- PORT STATE SERVICE +-- 554/tcp open rtsp +-- | rtsp-url-brute: +-- | Discovered URLs +-- |_ rtsp://camera.example.com/mpeg4 +-- +-- 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. +-- +-- @args rtsp-url-brute.urlfile sets an alternate URL dictionary file +-- @args rtsp-url-brute.threads sets the maximum number of parallell threads to run + +-- +-- Version 0.1 +-- Created 23/10/2011 - v0.1 - created by Patrik Karlsson +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"brute", "intrusive"} + +require 'rtsp' +require 'shortport' + +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 + +-- 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) + for u in url_iter do + local name = ( host.targetname and #host.targetname > 0 ) and host.targetname or + ( host.name and #host.name > 0 ) and host.name or + host.ip + local url = ("rtsp://%s%s"):format(name, u) + local helper = rtsp.Helper:new(host, port) + local status = helper:connect() + + if ( not(status) ) then + stdnse.print_debug(2, "ERROR: Connecting to RTSP server url: %s", url) + table.insert(result, { url = url, status = -1 } ) + break + end + + status, response = helper:describe(url) + if ( not(status) ) then + stdnse.print_debug(2, "ERROR: Sending DESCRIBE request to url: %s", url) + table.insert(result, { url = url, status = -1 } ) + break + end + + table.insert(result, { url = url, status = response.status } ) + helper:close() + 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: "):format(f)) + end + + local threads = {} + for t=1, threadcount do + local co = stdnse.new_thread(processURL, host, port, url_iter, result) + threads[co] = true + end + + repeat + condvar "wait" + for t in pairs(threads) do + if ( coroutine.status(t) == "dead" ) then threads[t] = nil end + 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 = { name='An error occured while testing the following URLs' } + + -- urls that illicited a 200 OK response + local success_urls = { name='Discovered URLs' } + + -- urls requiring authentication + -- local auth_urls = { name='URL requiring authentication' } + + 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 == 401 ) then +-- table.insert(auth_urls, r.url ) + end + end + + local result = { success_urls, failure_urls } + +-- -- insert our URLs requiring auth ONLY if not ALL urls returned auth +-- if (#result > #auth_urls) then +-- table.insert(result, 2, auth_urls) +-- end + + return stdnse.format_output(true, result ) +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 4612985bc..66c375cfe 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -187,6 +187,8 @@ Entry { filename = "realvnc-auth-bypass.nse", categories = { "auth", "default", Entry { filename = "resolveall.nse", categories = { "discovery", "safe", } } Entry { filename = "rmi-dumpregistry.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "rpcinfo.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "rtsp-methods.nse", categories = { "default", "safe", } } +Entry { filename = "rtsp-url-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "servicetags.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "sip-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "sip-enum-users.nse", categories = { "auth", "intrusive", } }