diff --git a/CHANGELOG b/CHANGELOG index a339431ab..f12ec2d58 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a SIP library and two new scripts sip-brute.nse and + sip-user-enum.nse providing brute and user enumeration support for the SIP + protocol. [Patrik] + o [NSE] Added xmpp.nse, which collects XMPP server information [Vasiliy Kulikov] o [NSE] Added broadcast-avahi-dos.nse, which tries to detect if the diff --git a/nselib/sip.lua b/nselib/sip.lua new file mode 100755 index 000000000..340847436 --- /dev/null +++ b/nselib/sip.lua @@ -0,0 +1,833 @@ +--- A SIP library supporting a limited subset of SIP commands and methods +-- +-- The library currently supports the following methods: +-- * REGISTER, INVITE & OPTIONS +-- +-- Overview +-- -------- +-- The library consists of the following classes: +-- +-- o SessionData +-- - Holds session data for the SIP session +-- +-- o Session +-- - Contains application functionality related to the implemented +-- SIP methods. +-- +-- o Connection +-- - A class containing code related to socket communication. +-- +-- o Response +-- - A class containing code for handling SIP responses +-- +-- o Request +-- - A class containing code for handling SIP requests +-- +-- o Util +-- - A class containing static utility functions +-- +-- o SIPAuth +-- - A class containing code related to SIP Authentication +-- +-- o Helper +-- - A class containing code used as a primary interface by scripts +-- +-- +-- @author "Patrik Karlsson " +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- +-- @args sip.timeout - specifies the session (socket) timeout in seconds + +-- Version 0.1 +-- Created 2011/03/30 - v0.1 - created by Patrik Karlsson + +module(... or "sip", package.seeall) + +-- Method constants +Method = { + ACK = "ACK", + INVITE = "INVITE", + OPTIONS = "OPTIONS", + REGISTER = "REGISTER", +} + +-- Error constants +Error = { + TRYING = 100, + RING = 180, + OK = 200, + UNAUTHORIZED = 401, + FORBIDDEN = 403, + PROXY_AUTH_REQUIRED = 407, +} + +-- The SessionData class +SessionData = { + + --- Creates a new instance of sessiondata + -- @return o an instance of SessionData + new = function(self, o) + local o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, + + --- Sets the session username + -- @param user string containing the username + setUsername = function(self, user) self.user = user end, + + --- Sets the session password + -- @param pass string containing the password + setPassword = function(self, pass) self.pass = pass end, + + --- Sets the SIP domain + -- @param domain string containing the SIP domain + setDomain = function(self, domain) self.domain = domain end, + + --- Sets the ip and port of the remote server + -- @param host string containing the ip of the remote server + -- @param port number containing the port of the remote server + setServer = function(self, host, port) self.server = { host = host, port = port } end, + + --- Sets the ip and port of the client + -- @param host string containing the ip of the client + -- @param port number containing the port of the client + setClient = function(self, host, port) self.client = { host = host, port = port } end, + + --- Sets the SIP users Full Name + -- @param name string containing the full name of the user + setName = function(self, name) self.name = name end, + + --- Retrieves the username + -- @return user string containing the sessions username + getUsername = function(self) return self.user end, + + --- Retrieves the session password + -- @return pass string containing the session password + getPassword = function(self) return self.pass end, + + --- Retrieves the SIP domain + -- @return domain string containing the SIP domain + getDomain = function(self) return self.domain end, + + --- Retrieves the client IP and port + -- @return host string containing the client IP + -- @return port number containing the client port + getClient = function(self) return self.client.host, self.client.port end, + + --- Retrieves the server IP and port + -- @return host string containing the server IP + -- @return port number containing the server port + getServer = function(self) return self.server.host, self.server.port end, + + --- Retrieves the SIP users full name + -- @return name string containing the users full name + getName = function(self) return self.name or "Nmap NSE" end, +} + +-- The session class holds the code necessary to register a SIP session +Session = { + + --- Creates a new session instance + -- @param host table containing the remote host to connect to + -- @param port table containing the remote port to connect to + -- @param sessdata instance of SessionData + -- @param options table containing zero or more of the following options + -- expires - the expire value in seconds + -- timeout - the socket timeout in seconds + -- @return o containing a new instance of the Session class + new = function(self, host, port, sessdata, options) + local o = {} + setmetatable(o, self) + self.__index = self + o.expires = (options and options.expires) or 300 + o.conn = Connection:new(host,port) + o.cseq = (options and options.cseq) or 1234 + local timeout = ( ( options and options.timeout ) and + options.timeout * 1000 ) or 5000 + o.conn.socket:set_timeout( timeout ) + o.sessdata = sessdata or sip.SessionData:new() + return o + end, + + --- Connect the session + -- @return true on success, false on failure + -- @return err string containing error message + connect = function(self) + local status, err = self.conn:connect() + if (not(status)) then + return false, "ERROR: Failed to connect to server" + end + local status, lhost, lport, rhost, rport = self.conn.socket:get_info() + if ( not(status) ) then + return false, "Failed to retreive socket information" + end + self.sessdata:setClient(lhost, lport) + self.sessdata:setServer(rhost, rport) + return true + end, + + --- Closes the session + -- TODO: We should probably send some "closing" packets here + -- @return true on success, false on failure + close = function(self) return self.conn:close() end, + + --- Sends and SIP invite + -- @param uri + invite = function(self, uri) + local request = Request:new(Method.INVITE) + local callid = Util.get_random_string(20) + + local lhost, _ = self.sessdata:getClient() + local tm = os.time() + + local uri = (uri and uri:match("^sip:.*@.*")) or + ("sip:%s@%s"):format(uri, self.sessdata:getDomain()) + + request:setUri(uri) + request:setSessionData(self.sessdata) + request:setCallId(callid) + + local data = {} + table.insert(data, "v=0") + table.insert(data, ("o=- %s %s IN IP4 %s"):format(tm, tm, lhost)) + table.insert(data, "s=-") + table.insert(data, ("c=IN IP4 %s"):format(lhost)) + table.insert(data, "t=0 0") + table.insert(data, "m=audio 49174 RTP/AVP 0") + table.insert(data, "a=rtpmap:0 PCMU/8000") + + request:setContent(stdnse.strjoin("\r\n", data)) + request:setContentType("application/sdp") + + local status, response = self:exch(request) + if ( not(status) ) then return false, response end + + local errcode = response:getErrorCode() + + if ( Error.PROXY_AUTH_REQUIRED == errcode or + Error.UNAUTHORIZED == errcode ) then + + -- Send an ACK to the server + request:setMethod(Method.ACK) + local status, err = self.conn:send( tostring(request) ) + if ( not(status) ) then return status, "ERROR: Failed to send request" end + + -- Send an authenticated INVITE to the server + request:setMethod(Method.INVITE) + self.cseq = self.cseq + 1 + status, data = self:authenticate(request, response) + if ( not(status) ) then return false, "SIP Authentication failed" end + response = Response:new(data) + + -- read a bunch of 180 Ringing and 100 Trying requests, until we get a 200 OK + while ( response:getErrorCode() ~= Error.OK ) do + status, data = self.conn:recv() + if ( not(status) ) then return status, "ERROR: Failed to receive response" end + response = Response:new(data) + end + + end + + return true + end, + + --- Prepares and sends the challenge response authentication to the server + -- @param request instance of the request object requiring authentication + -- @param authdata string containing authentication data + -- @return status true on success false on failure + -- @return err string containing an error message if status is false + authenticate = function(self, request, response) + local rhost, _ = self.sessdata:getServer() + local auth_header, auth_data = response:getAuthData() + local auth = SipAuth:new(auth_data) + auth:setUsername(self.sessdata:getUsername()) + auth:setPassword(self.sessdata:getPassword()) + auth:setMethod(request.method) + auth:setUri(("sip:%s"):format(rhost)) + + if ( auth_header == "WWW-Authenticate" ) then + request:setWWWAuth(auth:createResponse()) + else + request:setProxyAuth(auth:createResponse()) + end + request:setCseq(self.cseq) + + local status, err = self.conn:send( tostring(request) ) + if ( not(status) ) then return status, "ERROR: Failed to send request" end + + local data + status, data = self.conn:recv() + if ( not(status) and data ~= "TIMEOUT" ) then + return status, "ERROR: Failed to receive response" + end + return status, data + end, + + --- Sends a SIP Request and receives the Response + -- @param request instance of Request + -- @return status true on success, false on failure + -- @return resp containing a new Response instance + -- err containing error message if status is false + exch = function(self, request) + request:setCseq(self.cseq) + + local status, err = self.conn:send( tostring(request) ) + if ( not(status) ) then return status, "ERROR: Failed to send request" end + + local status, data = self.conn:recv() + if ( not(status) ) then return status, "ERROR: Failed to receive response" end + + return true, Response:new(data) + end, + + --- Sends a register request to the server + -- @return status true on success, false on failure + -- @return msg string containing the error message (if status is false) + register = function(self) + local request = Request:new(Method.REGISTER) + local callid = Util.get_random_string(20) + + request:setUri("sip:" .. self.sessdata:getServer()) + request:setSessionData(self.sessdata) + request:setCallId(callid) + request:setExpires(self.expires) + request:setAllow({"PRACK","INVITE","ACK","BYE","CANCEL","UPDATE", + "SUBSCRIBE","NOTIFY","REFER","MESSAGE","OPTIONS"}) + request:setContentLength(0) + + local status, response = self:exch(request) + if (not(status)) then return false, response end + + local errcode = response:getErrorCode() + + if ( status and errcode == Error.OK ) then + return true, response + elseif ( Error.PROXY_AUTH_REQUIRED == errcode or Error.UNAUTHORIZED == errcode ) then + local data + self.cseq = self.cseq + 1 + status, data = self:authenticate(request, response) + response = Response:new(data) + errcode = response:getErrorCode() + if ( not(status) or ( errcode and errcode ~= Error.OK ) ) then + return false, "ERROR: Failed to authenticate" + end + elseif ( Error.FORBIDDEN == errcode ) then + return false, "Authentication forbidden" + else + return false, ("Unhandled error: %d"):format(errcode) + end + return true + end, + + --- Sends an option request to the server and handles the response + -- @return status true on success, false on failure + -- @return msg string containing the error message (if status is false) + options = function(self) + local req = Request:new(Method.OPTIONS) + local callid = Util.get_random_string(20) + req:setUri("sip:" .. self.sessdata:getServer()) + req:setSessionData(self.sessdata) + req:setCallId(callid) + req:setExpires(self.expires) + req:setAllow({"PRACK","INVITE","ACK","BYE","CANCEL","UPDATE", + "SUBSCRIBE","NOTIFY","REFER","MESSAGE","OPTIONS"}) + req:addHeader("Accept", "application/sdp") + req:setContentLength(0) + + local status, response = self:exch(req) + if (not(status)) then return false, response end + + local errcode = response:getErrorCode() + local errmsg = response:getErrorMessage() + local msg = ( errcode and ( errcode .. " " .. errmsg ) ) + return ( not(errcode) or errcode == 200), msg + end, + +} + +-- The connection class contains basic communication code +Connection = { + + --- Creates a new SIP Connection + -- @param host table containing the host to connect to + -- @param port table containing the port to connect to + -- @return o containing a new Connection instance + new = function(self, host, port) + local o = {} + setmetatable(o, self) + self.__index = self + o.host = host + o.port = port + o.socket = nmap.new_socket() + return o + end, + + --- Connects to the server + -- @return status containing true on success and false on failure + -- @return err containing the error message (if status is false) + connect = function(self) + local status, err = self.socket:connect(self.host, self.port) + if ( status ) then + local status, lhost, lport, _, _ = self.socket:get_info() + if ( status ) then + self.lhost = lhost + self.lport = lport + end + end + return status, err + end, + + --- Sends the data over the socket + -- @return status true on success, false on failure + send = function(self, data) + return self.socket:send(data) + end, + + --- Receives data from the socket + -- @return status true on success, false on failure + recv = function(self) + return self.socket:receive() + end, + + --- Closes the communication channel (socket) + -- @return true on success false on failure + close = function(self) + return self.socket:close() + end, + + --- Retrieves the client ip and port + -- @return lhost string containing the local ip + -- @return lport number containing the local port + getClient = function(self) return self.lhost, self.lport end, + + --- Retrieves the server ip and port + -- @return rhost string containing the server ip + -- @return rport number containing the server port + getServer = function(self) return ( self.host.ip or self.host ), ( self.port.number or self.port ) end, + + +} + +-- The response class holds the necessary methods and parameters to parse a response +Response = { + + --- Creates a new Response instance + -- @param str containing the data as received over the socket + -- @return o table containing a new Response instance + new = function(self, str) + local o = {} + setmetatable(o, self) + self.__index = self + o.tbl = stdnse.strsplit("\r\n", str) + return o + end, + + --- Retrieves a given header value from the response + -- @param name string containing the name of the header + -- @return value string containing the header value + getHeader = function(self,name) + for _, line in ipairs(self.tbl) do + local header, value = line:match("^(.-): (.*)$") + if ( header == name ) then return value end + end + end, + + --- Returns the error code from the SIP response + -- @return err number containing the error code + getErrorCode = function(self) + return tonumber(self.tbl[1]:match("SIP/%d%.%d (%d+)")) + end, + + --- Returns the error message returned by the server + -- @return errmsg string containing the error message + getErrorMessage = function(self) + return self.tbl[1]:match("^SIP/%d%.%d %d+ (.+)$") + end, + + --- Returns the message method + -- @return method string containing the method + getMethod = function(self) + return self.tbl[1]:match("^(.-)%s.*SIP/2%.0$") + end, + + --- Returns the authentication data from the SIP response + -- @return auth string containing the raw authentication data + getAuthData = function(self) + local auth = self:getHeader("WWW-Authenticate") or self:getHeader("Proxy-Authenticate") + if ( auth ) then + return ( self:getHeader("WWW-Authenticate") and + "WWW-Authenticate" or + "Proxy-Authenticate"), auth + end + end, + + --- Retrieves the current sequence number + -- @return cseq number containing the current sequence number + getCSeq = function(self) + local cseq = self:getHeader("CSeq") + cseq = (cseq and cseq:match("^(%d+)")) + return (cseq and tonumber(cseq)) + end, + +} + +-- The request class holds the necessary functions and parameters for a basic SIP request +Request = { + + --- Creates a new Request instance + -- @param method string containing the request method to use + -- @return o containing a new Request instance + new = function(self, method) + local o = {} + setmetatable(o, self) + self.__index = self + + o.ua = "Nmap NSE" + o.protocol = "UDP" + o.maxfwd = 70 + o.method = method + return o + end, + + --- Sets the sessiondata so that session information may be fetched + -- @param data instance of SessionData + setSessionData = function(self, data) self.sessdata = data end, + + --- Adds a custom header to the request + -- @param name string containing the header name + -- @param value string containing the header value + addHeader = function(self, name, value) + self.headers = self.headers or {} + table.insert(self.headers, ("%s: %s"):format(name, value)) + end, + + --- Sets the SIP uri + -- @param uri string containing the SIP uri + setUri = function(self, uri) self.uri = uri end, + + --- Sets an error + -- @param code number containing the error code + -- @param msg string containing the error message + setError = function(self, code, msg) self.error = { code = code, msg = msg } end, + + --- Sets the request method + -- @param method string containing a valid SIP method (@see Method constant) + setMethod = function(self, method) self.method = method end, + + --- Sets the sequence number + -- @param seq number containing the sequence number to set + setCseq = function(self, seq) self.cseq = seq end, + + --- Sets the allow header + -- @param allow string containing all of the allowed SIP methods + setAllow = function(self, allow) self.allow = stdnse.strjoin(", ", allow) end, + + --- Sets the request content data + -- @param string containing the content data + setContent = function(self, content) self.content = content end, + + --- Sets the requests' content type + -- @param t string containing the content type + setContentType = function(self, t) self.content_type = t end, + + --- Sets the supported SIP methods + -- @param supported string containing the supported methods + setSupported = function(self, supported) self.supported = supported end, + + --- Sets the content-length of the SIP request + -- @param len number containing the length of the actual request + setContentLength = function(self, len) self.length = len end, + + --- Sets the expires header of the SIP request + -- @param expires number containing the expire value + setExpires = function(self, expires) self.expires = expires end, + + --- Sets the User Agent being used to connect to the SIP server + -- @param ua string containing the User-Agent name (defaults to Nmap NSE) + setUA = function(self, ua) self.ua = ua end, + + --- Sets the caller ID information of the SIP request + -- @param cid string containing the callers id + setCallId = function(self, cid) self.cid = cid end, + + --- Sets the maximum forwards allowed of this request + -- @param maxfwd number containing the maximum allowed forwards + setForwards = function(self, maxfwd) self.maxfwd = maxfwd end, + + --- Sets the proxy authentication data + -- @param auth string containing properly formatted proxy authentication data + setProxyAuth = function(self, auth) self.proxyauth = auth end, + + --- Sets the www authentication data + -- @param auth string containing properly formatted proxy authentication data + setWWWAuth = function(self, auth) self.wwwauth = auth end, + + --- Specifies the network protocol being used + -- @param proto should be either "UDP" or "TCP" + setProtocol = function(self, proto) + assert( proto == "UDP" or proto == "TCP", ("Unsupported protocol %s"):format(proto)) + self.protocol = proto + end, + + + --- Converts the request to a String suitable to be sent over the socket + -- @return ret string containing the complete request for sending over the socket + __tostring = function(self) + local data = {} + local branch = "z9hG4bK" .. Util.get_random_string(25) + -- must be at least 32-bit unique + self.from_tag = self.from_tag or Util.get_random_string(20) + local sessdata = self.sessdata + local lhost, lport = sessdata:getClient() + local rhost, rport = sessdata:getServer() + + local name, user, domain = sessdata:getName(), sessdata:getUsername(), sessdata:getDomain() + + assert(self.method, "No method specified") + assert(self.maxfwd, "Max forward not set") + + -- if no domain was specified use the remote host instead + domain = domain or rhost + + if ( self.error ) then + table.insert(data, ("SIP/2.0 %s %d"):format(self.error.msg, self.error.code)) + else + if ( self.method == Method.ACK ) then + table.insert(data, ("%s %s:%d SIP/2.0"):format(self.method, self.uri, rport)) + else + table.insert(data, ("%s %s SIP/2.0"):format(self.method, self.uri)) + end + end + table.insert(data, ("Via: SIP/2.0/%s %s:%d;rport;branch=%s"):format(self.protocol, lhost, lport, branch)) + table.insert(data, ("Max-Forwards: %d"):format(self.maxfwd)) + table.insert(data, ("From: \"%s\" ;tag=%s"):format(name, user, domain, self.from_tag)) + + if ( self.method == Method.INVITE ) then + table.insert(data, ("To: "):format(user, domain)) + else + table.insert(data, ("To: \"%s\" "):format(name, user, domain)) + end + + table.insert(data, ("Call-ID: %s"):format(self.cid)) + + if ( self.error and self.error.code == Error.OK ) then + table.insert(data, ("CSeq: %d OPTIONS"):format(self.cseq)) + else + table.insert(data, ("CSeq: %d %s"):format(self.cseq, self.method)) + end + + if ( self.method ~= Method.ACK ) then + table.insert(data, ("User-Agent: %s"):format(self.ua)) + table.insert(data, ("Contact: \"%s\" "):format(name, user, lhost, lport)) + if ( self.expires ) then + table.insert(data, ("Expires: %d"):format(self.expires)) + end + if ( self.allow ) then + table.insert(data, ("Allow: %s"):format(self.allow)) + end + if ( self.supported ) then + table.insert(data, ("Supported: %s"):format(self.supported)) + end + + if ( not(self.error) ) then + if ( self.proxyauth ) then + table.insert(data, ("Proxy-Authorization: %s"):format(self.proxyauth)) + end + if ( self.wwwauth ) then + table.insert(data, ("Authorization: %s"):format(self.wwwauth)) + end + end + + self.length = (self.content and #self.content +2 or 0) + if ( self.headers ) then + for _, val in ipairs(self.headers) do + table.insert(data, val) + end + end + if ( self.content_type ) then + table.insert(data, ("Content-Type: %s"):format(self.content_type)) + end + table.insert(data, ("Content-Length: %d"):format(self.length)) + table.insert(data, "") + + if ( self.content ) then table.insert(data, self.content) end + table.insert(data, "") + else + self.length = (self.content and #self.content +2 or 0) + + table.insert(data, ("Content-Length: %d"):format(self.length)) + table.insert(data, "") + end + return stdnse.strjoin("\r\n", data) + end, + +} + +-- A minimal Util class with supporting functions +Util = { + + --- Generates a random string of the requested length. + -- @param length (optional) The length of the string to return. Default: 8. + -- @param set (optional) The set of letters to choose from. Default: upper, lower, numbers, and underscore. + -- @return The random string. + get_random_string = function(length, set) + if(length == nil) then + length = 8 + end + + if(set == nil) then + set = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_" + end + + local str = "" + + -- Seed the random number, if we haven't already + if (not(nmap.registry.sip) or not(nmap.registry.sip.seeded)) then + math.randomseed(os.time()) + nmap.registry.sip = {} + nmap.registry.sip.seeded = true + end + + for i = 1, length, 1 do + local random = math.random(#set) + str = str .. string.sub(set, random, random) + end + + return str + end + +} + +-- The SIP authentication class, supporting MD5 digest authentication +SipAuth = { + + --- Creates a new SipAuth instance + -- @param auth string containing the auth data as received from the server + new = function(self, auth) + local o = {} + setmetatable(o, self) + self.__index = self + o.auth = auth + return o + end, + + --- Sets the username used for authentication + -- @param username string containing the name of the user + setUsername = function(self, username) self.username = username end, + + --- Sets the password used for authentication + -- @param password string containing the password of the user + setPassword = function(self, password) self.password = password end, + + --- Sets the method used for authentication + -- @param method string containing the method (Usually REGISTER) + setMethod = function(self, method) self.method = method end, + + --- Sets the uri used for authentication + -- @param uri string containing the uri (Usually sip:) + setUri = function(self, uri) self.uri = uri end, + + --- Processes and parses a challenge as received from the server + parseChallenge = function(self) + if ( not(self.auth) ) then return end + self.nonce = self.auth:match("nonce=[\"]([^,]-)[\"]") + self.algorithm = self.auth:match("algorithm=[\"]*(.-)[\"]*,") + self.realm = self.auth:match("realm=[\"]([^,]-)[\"]") + assert(self.algorithm:upper() == "MD5", + ("Unsupported algorithm detected in authentication challenge (%s)"):format(self.algorithm:upper())) + end, + + --- Calculates the authentication response + -- @return reponse string containing the authentication response + calculateResponse = function(self) + + if ( not(self.nonce) or not(self.algorithm) or not(self.realm) ) then + self:parseChallenge() + end + + assert(self.username, "SipAuth: No username specified") + assert(self.password, "SipAuth: No password specified") + assert(self.method, "SipAuth: No method specified") + assert(self.uri, "SipAuth: No uri specified") + + local result + if ( self.algorithm == "MD5" ) then + local HA1 = select(2, bin.unpack("H16", openssl.md5(self.username .. ":" .. self.realm .. ":" .. self.password))) + local HA2 = select(2, bin.unpack("H16", openssl.md5(self.method .. ":" .. self.uri))) + result = openssl.md5(HA1:lower() .. ":" .. self.nonce ..":" .. HA2:lower()) + end + return select(2, bin.unpack("H16", result)):lower() + end, + + --- Creates the complete authentication response + -- @return auth string containing the complete authentication digest + createResponse = function(self) + local response = self:calculateResponse() + return ("Digest username=\"%s\", realm=\"%s\", nonce=\"%s\"," .. + " uri=\"%s\", response=\"%s\", algorithm=%s"):format(self.username, self.realm, + self.nonce, self.uri, response, self.algorithm) + end, + +} + +-- The Helper class used as main script interface +Helper = { + + --- Creates a new instance of the Helper class + -- @param host table containing the remote host + -- @param port table containing the remote port + -- @param options table containing any options to pass along to the + -- session (@see Session:new for more details) + -- @return o containing a new instance of the Helper class + new = function(self, host, port, options) + local o = {} + setmetatable(o, self) + self.__index = self + local timeout = stdnse.get_script_args("sip.timeout") + if ( timeout ) then options.timeout = timeout end + o.sessdata = SessionData:new() + o.session = Session:new(host, port, o.sessdata, options) + return o + end, + + --- Connects the helper instance + connect = function(self) return self.session:connect() end, + + --- Disconnects and closes the helper instance + close = function(self) return self.session:close() end, + + --- Sets the credentials used when performing authentication + -- @param username string containing the username to use for authentication + -- @param password string containing the password to use for authentication + setCredentials = function(self, username, password) + self.sessdata:setUsername(username) + self.sessdata:setPassword(password) + end, + + --- Sets the SIP domain + -- @param domain string containing the domain name + setDomain = function(self, domain) self.sessdata:setDomain(domain) end, + + --- Register the UAC with the server + -- @param options table containing zero or more options + -- (@see Session:register for more details) + -- @return status true on success, false on failure + -- @return msg containing the error message if status is false + register = function(self, options) + local status, response = self.session:register(options) + if ( not(status) ) then return false, response end + return true + end, + + options = function(self) return self.session:options() end, + + --- Attempts to INVITE the user at uri to a call + -- @param uri string containing the sip uri + -- @return status true on success, false on failure + invite = function(self, uri) + return self.session:invite(uri) + end, + +} \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 64939d89e..08d32aa52 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -141,6 +141,8 @@ 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 = "servicetags.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "sip-brute.nse", categories = { "auth", "intrusive", } } +Entry { filename = "sip-enum-users.nse", categories = { "auth", "intrusive", } } Entry { filename = "skypev2-version.nse", categories = { "version", } } Entry { filename = "smb-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "smb-check-vulns.nse", categories = { "dos", "exploit", "intrusive", "vuln", } } @@ -193,3 +195,4 @@ Entry { filename = "wdb-version.nse", categories = { "default", "discovery", "ve Entry { filename = "whois.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "wsdd-discover.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "x11-access.nse", categories = { "auth", "default", "safe", } } +Entry { filename = "xmpp.nse", categories = { "default", "discovery", "safe", } } diff --git a/scripts/sip-brute.nse b/scripts/sip-brute.nse new file mode 100755 index 000000000..bcb25f79b --- /dev/null +++ b/scripts/sip-brute.nse @@ -0,0 +1,106 @@ +description = [[ +Attempts to brute-force SIP accounts +]] + +--- +-- @usage +-- nmap -sU -p 5060 --script=sip-brute +-- +-- PORT STATE SERVICE +-- 5060/udp open|filtered sip +-- | sip-brute: +-- | Accounts +-- | 1000:password123 => Login correct +-- | Statistics +-- |_ Performed 5010 guesses in 3 seconds, average tps: 1670 + +-- Version 0.1 +-- Created 04/03/2011 - v0.1 - created by Patrik Karlsson + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} + +require "shortport" +require "sip" +require "brute" + +portrule = shortport.port_or_service(5060, "sip", "udp") + +Driver = { + + new = function(self, host, port) + local o = {} + setmetatable(o, self) + self.__index = self + o.host = host + o.port = port + return o + end, + + connect = function( self ) + self.helper = sip.Helper:new(self.host, self.port, { expires = 0 }) + local status, err = self.helper:connect() + if ( not(status) ) then + return "ERROR: Failed to connect to SIP server" + end + return true + end, + + login = function( self, username, password ) + self.helper:setCredentials(username, password) + status, err = self.helper:register() + if ( not(status) ) then + -- The 3CX System has an anti-hacking option that triggers after + -- a certain amount of guesses. This protection basically prevents + -- any connection from the offending IP at an application level. + if ( err:match("^403 Forbidden") ) then + local err = brute.Error:new("The systems seems to have blocked our IP") + err:setAbort( true ) + return false, err + end + return false, brute.Error:new( "Incorrect password" ) + end + return true, brute.Account:new(username, password, "OPEN") + end, + + disconnect = function(self) return self.helper:close() end, +} + +-- Function used to check if we can distinguish existing from non-existing +-- accounts. In order to do so we send a semi-random username and password +-- and interpret the response. Some servers will respond as if the login +-- was successful which makes it impossible to tell successfull logins +-- from non-existing accounts apart. +local function checkBadUser(host, port) + math.randomseed( os.time() ) + local user = "baduser-" .. math.random(10000) + local pass = "badpass-" .. math.random(10000) + local helper = sip.Helper:new(host, port, { expires = 0 }) + + print(user, pass) + local status, err = helper:connect() + if ( not(status) ) then return false, "ERROR: Failed to connect" end + + helper:setCredentials(user, pass) + local status, err = helper:register() + helper:close() + return status, err +end + +action = function(host, port) + local force = stdnse.get_script_args("sip-brute.force") + + if ( not(force) ) then + local status = checkBadUser(host, port) + if ( status ) then + return "\nERROR: Cannot detect non-existing user accounts, this will result in:\n" .. + " * Non-exisiting accounts being detected as found\n" .. + " * Passwords for existing accounts being correctly detected\n\n" .. + "Supply the sip-brute.force argument to override" + end + end + local engine = brute.Engine:new(Driver, host, port) + local status, result = engine:start() + return result +end \ No newline at end of file diff --git a/scripts/sip-enum-users.nse b/scripts/sip-enum-users.nse new file mode 100644 index 000000000..c10fb3d38 --- /dev/null +++ b/scripts/sip-enum-users.nse @@ -0,0 +1,143 @@ +description = [[ +Attempts to enumerate valid user account using SIP. Currently only the SIP +server Asterisk is supported. + +* Asterisk + - The script enumerates valid accounts by checking the SIP servers response + to the REGISTER request. If TRYING is returned, the account does not + exist. If REGISTER is returned the account is valid. +]] + +--- +-- @usage +-- nmap -sU -p 5060 --script=sip-brute +-- +-- PORT STATE SERVICE +-- 5060/udp open|filtered sip +-- | sip-enum-users: +-- | Valid SIP accounts +-- | 1000 +-- |_ 1001 + +-- Version 0.1 +-- Created 04/03/2011 - v0.1 - created by Patrik Karlsson + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} + +require "shortport" +require "sip" +require "unpwdb" + +portrule = shortport.port_or_service(5060, "sip", "udp") + +-- Send a register request to the server and returned the unparsed response +-- @param session instance of Session class +-- @param username string containing the name of the user +-- @return status true on success false on failure +-- @return response instance of sip.Response (on success) +-- @return err string containing the error message (on failure) +local function register(session, username) + local request = sip.Request:new(sip.Method.REGISTER) + local callid = sip.Util.get_random_string(20) + + session.sessdata:setUsername(username) + request:setUri("sip:" .. session.sessdata:getServer() ) + request:setSessionData(session.sessdata) + request:setCallId(callid) + request:setExpires(0) + request:setAllow({"PRACK","INVITE","ACK","BYE","CANCEL","UPDATE", + "SUBSCRIBE","NOTIFY","REFER","MESSAGE","OPTIONS"}) + request:setContentLength(0) + + local status, response = session:exch(request) + if (not(status)) then return false, response end + + return true, response +end + + +-- Confirm the server is a valid and supported one +-- @param host table as passed to the action method +-- @param port table as passed to the action method +-- @return status true on success, false on failure +-- @return header string containing the server name +local function confirmServer(host, port) + local user = "nmap_banner_check" + local session = sip.Session:new( host, port ) + + local status = session:connect() + if ( not(status) ) then + return "ERROR: Failed to connect to the SIP server" + end + + local response + status, response = register(session, user) + if ( status ) then + return true, ( + response:getHeader("User-Agent") or + response:getHeader("Server") + ) + end + + return false +end + +-- Asterisk specific function used to check for valid usernames +-- @param session instance of SIP Session +-- @param username string containing the SIP username +-- @return status true on success, false on failure +-- @return err on failure +local function checkAsteriskUsername(session, username) + local status, response = register(session, username) + if ( status and response:getErrorCode() == 401 ) then + return true, "SUCCESS" + end + return false, "FAILURE" +end + +-- Table containing a server match and corresponding check function +local detectiontbl = { + { name="^Asterisk PBX", func=checkAsteriskUsername } +} + +action = function(host, port) + local accounts = {} + local status, usernames = unpwdb.usernames() + if ( not(status) ) then return false, "Failed to load usernames" end + + local server + status, server = confirmServer( host, port ) + if ( not(status) ) then + return "ERROR: Failed to determine server version" + end + + local checkUsername + for _, item in ipairs( detectiontbl ) do + if ( server and server:match( item.name ) ) then + checkUsername = item.func + break + end + end + + if ( not(checkUsername) ) then return ("ERROR: Unsupported server (%s)"):format((server or "")) end + + for username in usernames do + local session = sip.Session:new( host, port ) + + local status = session:connect() + if ( not(status) ) then + return "ERROR: Failed to connect to the SIP server" + end + + local status, err = checkUsername( session, username ) + if ( status ) then table.insert( accounts, username ) end + + session:close() + end + + accounts.name = "Valid SIP accounts" + return stdnse.format_output(true, { accounts } ) + +end \ No newline at end of file