diff --git a/CHANGELOG b/CHANGELOG index 39ac8bdad..b4e9f87d9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the mobilme library and the scripts http-icloud-findmyiphone and + http-icloud-sendmsg, that finds the location of iOS devices and provides + functionality to send them messages. [Patrik Karlsson] + o [NSE] Added gps library and the gpsd-info script that collects GPS data from the gpsd daemon. [Patrik Karlsson] diff --git a/nselib/mobileme.lua b/nselib/mobileme.lua new file mode 100644 index 000000000..52986cf05 --- /dev/null +++ b/nselib/mobileme.lua @@ -0,0 +1,223 @@ +--- +-- A MobileMe web service client that allows discovering Apple devices +-- using the "find my iPhone" functionality. +-- +-- @author "Patrik Karlsson " +-- +module(... or "mobileme", package.seeall) + +local http = require('http') +local json = require('json') + +MobileMe = { + + -- headers used in all requests + headers = { + ["Content-Type"] = "application/json; charset=utf-8", + ["X-Apple-Find-Api-Ver"] = "2.0", + ["X-Apple-Authscheme"] = "UserIdGuest", + ["X-Apple-Realm-Support"] = "1.0", + ["User-Agent"] = "Find iPhone/1.3 MeKit (iPad: iPhone OS/4.2.1)", + ["X-Client-Name"] = "iPad", + ["X-Client-UUID"] = "0cf3dc501ff812adb0b202baed4f37274b210853", + ["Accept-Language"] = "en-us", + ["Connection"] = "keep-alive" + }, + + -- Creates a MobileMe instance + -- @param username string containing the Apple ID username + -- @param password string containing the Apple ID password + -- @return o new instance of MobileMe + new = function(self, username, password) + local o = { + host = "fmipmobile.icloud.com", + port = 443, + username = username, + password = password + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Sends a message to an iOS device + -- @param devid string containing the device id to which the message should + -- be sent + -- @param subject string containing the messsage subject + -- @param message string containing the message body + -- @param alarm boolean true if alarm should be sounded, false if not + -- @return status true on success, false on failure + -- @return err string containing the error message (if status is false) + sendMessage = function(self, devid, subject, message, alarm) + local data = '{"clientContext":{"appName":"FindMyiPhone","appVersion":"1.3","buildVersion":"145","deviceUDID":"0000000000000000000000000000000000000000","inactiveTime":5911,"osVersion":"3.2","productType":"iPad1,1","selectedDevice":"%s","shouldLocate":false},"device":"%s","serverContext":{"callbackIntervalInMS":3000,"clientId":"0000000000000000000000000000000000000000","deviceLoadStatus":"203","hasDevices":true,"lastSessionExtensionTime":null,"maxDeviceLoadTime":60000,"maxLocatingTime":90000,"preferredLanguage":"en","prefsUpdateTime":1276872996660,"sessionLifespan":900000,"timezone":{"currentOffset":-25200000,"previousOffset":-28800000,"previousTransition":1268560799999,"tzCurrentName":"Pacific Daylight Time","tzName":"America/Los_Angeles"},"validRegion":true},"sound":%s,"subject":"%s","text":"%s"}' + data = data:format(devid, devid, tostring(alarm), subject, message) + + local url = ("/fmipservice/device/%s/sendMessage"):format(self.username) + local auth = { username = self.username, password = self.password } + + local response = http.post(self.host, self.port, url, { header = self.headers, auth = auth, timeout = 10000 }, nil, data) + + if ( response.status == 200 ) then + local status, resp = json.parse(response.body) + if ( not(status) ) then + stdnse.print_debug(2, "Failed to parse JSON response from server") + return false, "Failed to parse JSON response from server" + end + + if ( resp.statusCode ~= "200" ) then + stdnse.print_debug(2, "Failed to send message to server") + return false, "Failed to send message to server" + end + end + return true + end, + + -- Updates location information for all devices controlled by the Apple ID + -- @return status true on success, false on failure + -- @return json parsed json table or string containing an error message on + -- failure + update = function(self) + + local auth = { + username = self.username, + password = self.password + } + + local url = ("/fmipservice/device/%s/initClient"):format(self.username) + local data= '{"clientContext":{"appName":"FindMyiPhone","appVersion":"1.3","buildVersion":"145","deviceUDID":"0000000000000000000000000000000000000000","inactiveTime":2147483647,"osVersion":"4.2.1","personID":0,"productType":"iPad1,1"}}' + + local retries = 2 + + local response + repeat + response = http.post(self.host, self.port, url, { header = self.headers, auth = auth }, nil, data) + if ( response.header["x-apple-mme-host"] ) then + self.host = response.header["x-apple-mme-host"] + end + + if ( response.status == 401 ) then + return false, "Authentication failed" + elseif ( response.status ~= 200 and response.status ~= 330 ) then + return false, "An unexpected error occured" + end + + retries = retries - 1 + until ( 200 == response.status or 0 == retries) + + if ( response.status ~= 200 ) then + return false, "Received unexpected response from server" + end + + local status, parsed_json = json.parse(response.body) + + if ( not(status) or parsed_json.statusCode ~= "200" ) then + return false, "Failed to parse JSON response from server" + end + + -- cache the parsed_json.content as devices + self.devices = parsed_json.content + + return true, parsed_json + end, + + -- Get's a list of devices + -- @return devices table containing a list of devices + getDevices = function(self) + if ( not(self.devices) ) then + self:update() + end + return self.devices + end +} + + +Helper = { + + + -- Creates a Helper instance + -- @param username string containing the Apple ID username + -- @param password string containing the Apple ID password + -- @return o new instance of Helper + new = function(self, username, password) + local o = { + mm = MobileMe:new(username, password) + } + setmetatable(o, self) + self.__index = self + o.mm:update() + return o + end, + + -- Get's the geolocation from each device + -- + -- @return status true on success, false on failure + -- @return result table containing a table of device locations + -- the table is indexed based on the name of the device and + -- contains a location table with the following fields: + -- * longitude - the GPS longitude + -- * latitude - the GPS latitude + -- * accuracy - the location accuracy + -- * timestamp - the time the location was acquired + -- * postype - the position type (GPS or WiFi) + -- * finished - + -- or string containing an error message on failure + getLocation = function(self) + -- do 3 tries, with a 5 second timeout to allow the location to update + -- there are two attributes, locationFinished and isLocating that seem + -- to be good candidates to monitor, but so far, I haven't had any + -- success with that. + local tries, timeout = 3, 5 + local result = {} + + repeat + local status, response = self.mm:update() + + if ( not(status) or not(response) ) then + return false, "Failed to retrieve response from server" + end + for _, device in ipairs(response.content) do + if ( device.location ) then + result[device.name] = { + longitude = device.location.longitude, + latitude = device.location.latitude, + accuracy = device.location.horizontalAccuracy, + timestamp = device.location.timeStamp, + postype = device.location.positionType, + finished = device.location.locationFinished, + } + end + end + tries = tries - 1 + if ( tries > 0 ) then + stdnse.sleep(timeout) + end + until( tries == 0 ) + return true, result + end, + + -- Gets a list of names and ids of devices associated with the Apple ID + -- @return status true on success, false on failure + -- @return table of devices containing the following fields: + -- name and id + getDevices = function(self) + local devices = {} + for _, dev in ipairs(self.mm:getDevices()) do + table.insert(devices, { name = dev.name, id = dev.id }) + end + return true, devices + end, + + -- Send a message to an iOS Device + -- + -- @param devid string containing the device id to which the message should + -- be sent + -- @param subject string containing the messsage subject + -- @param message string containing the message body + -- @param alarm boolean true if alarm should be sounded, false if not + -- @return status true on success, false on failure + -- @return err string containing the error message (if status is false) + sendMessage = function(self, ...) + return self.mm:sendMessage(...) + end + +} \ No newline at end of file diff --git a/scripts/http-icloud-findmyiphone.nse b/scripts/http-icloud-findmyiphone.nse new file mode 100644 index 000000000..e48195fdd --- /dev/null +++ b/scripts/http-icloud-findmyiphone.nse @@ -0,0 +1,84 @@ +description = [[ +Retrieves the locations of all "Find my iPhone" enabled iOS devices by querying +the MobileMe web service. +]] + +--- +-- @usage +-- nmap -sn -Pn --script http-icloud-findmyiphone --script-args='username=,password=' +-- +-- @output +-- Pre-scan script results: +-- | http-icloud-findmyiphone: +-- | name location accuracy date type +-- | Patrik Karlsson's MacBook Air -,- - - - +-- | Patrik Karlsson's iPhone 40.690,-74.045 65 04/10/12 16:56:37 Wifi +-- |_ Mac mini 40.690,-74.045 65 04/10/12 16:56:36 Wifi +-- +-- @args http-icloud-findmyiphone.username the Apple Id username +-- @args http-icloud-findmyiphone.password the Apple Id password +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +local mobileme = require('mobileme') +local tab = require('tab') + +local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username") +local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password") + +prerule = function() return true end + +-- decode basic UTF8 encoded strings +-- iOS devices are commonly named after the user eg: +-- * Patrik Karlsson's Macbook Air +-- * Patrik Karlsson's iPhone +-- +-- This function decodes the single quote as a start and should really +-- be replaced with a proper UTF-8 decoder in the future +local function decodeString(str) + return str:gsub("\226\128\153", "'") +end + +local function fail(err) return ("\n ERROR: %s"):format(err or "") end + +action = function() + + if ( not(arg_username) or not(arg_password) ) then + return fail("No username or password was supplied") + end + + local mobileme = mobileme.Helper:new(arg_username, arg_password) + local status, response = mobileme:getLocation() + + if ( not(status) ) then + stdnse.print_debug(2, "%s: %s", SCRIPT_NAME, response) + return fail("Failed to retrieve location information") + end + + local output = tab.new(4) + tab.addrow(output, "name", "location", "accuracy", "date", "type") + for name, info in pairs(response) do + local loc + if ( info.latitude and info.longitude ) then + loc = ("%.3f,%.3f"):format( + tonumber(info.latitude) or "-", + tonumber(info.longitude) or "-") + else + loc = "-,-" + end + local ts + if ( info.timestamp and 1000 < info.timestamp ) then + ts = os.date("%x %X", info.timestamp/1000) + else + ts = "-" + end + tab.addrow(output, decodeString(name), loc, info.accuracy or "-", ts, info.postype or "-") + end + + if ( 1 < #output ) then + return stdnse.format_output(true, tab.dump(output)) + end +end \ No newline at end of file diff --git a/scripts/http-icloud-sendmsg.nse b/scripts/http-icloud-sendmsg.nse new file mode 100644 index 000000000..3ab258364 --- /dev/null +++ b/scripts/http-icloud-sendmsg.nse @@ -0,0 +1,106 @@ +description = [[ +]] +--- +-- @usage +-- nmap -sn -Pn --script http-icloud-sendmsg --script-args="username=,password=,http-icloud-sendmsg.listdevices" +-- nmap -sn -Pn --script http-icloud-sendmsg --script-args="username=,password=,deviceindex=1,subject='subject',message='hello world.',sound=false" +-- +-- @output +-- Pre-scan script results: +-- | http-icloud-sendmsg: +-- |_ Message was successfully sent to "Patrik Karlsson's iPhone" +-- +-- @args http-icloud-sendmsg.username the Apple ID username +-- @args http-icloud-sendmsg.password the Apple ID password +-- @args http-icloud-sendmsg.listdevices list the devices managed by the +-- specified Apple ID. +-- @args http-icloud-sendmsg.deviceindex the device index to which the message +-- should be sent (@see http-icloud-sendmsg.listdevices) +-- @args http-icloud-sendmsg.subject the subject of the message to send to the +-- device. +-- @args http-icloud-sendmsg.message the body of the message to send to the +-- device. +-- @args http-icloud-sendmsg.sound boolean specifying if a loud sound should be +-- played while displaying the message. (default: true) + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +local mobileme = require('mobileme') +local tab = require('tab') + +local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username") +local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password") +local arg_listdevices = stdnse.get_script_args(SCRIPT_NAME .. ".listdevices") +local arg_deviceindex = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".deviceindex")) +local arg_subject = stdnse.get_script_args(SCRIPT_NAME .. ".subject") +local arg_message = stdnse.get_script_args(SCRIPT_NAME .. ".message") +local arg_sound = stdnse.get_script_args(SCRIPT_NAME .. ".sound") or true + + +prerule = function() return true end + +-- decode basic UTF8 encoded strings +-- iOS devices are commonly named after the user eg: +-- * Patrik Karlsson's Macbook Air +-- * Patrik Karlsson's iPhone +-- +-- This function decodes the single quote as a start and should really +-- be replaced with a proper UTF-8 decoder in the future +local function decodeString(str) + return str:gsub("\226\128\153", "'") +end + +local function listDevices(mm) + local status, devices = mm:getDevices() + if ( not(status) ) then + return fail("Failed to get devices") + end + + local output = tab.new(2) + tab.addrow(output, "id", "name") + for i=1, #devices do + local name = decodeString(devices[i].name or "") + tab.addrow(output, i, name) + end + + if ( 1 < #output ) then + return stdnse.format_output(true, tab.dump(output)) + end +end + +local function fail(err) return ("\n ERROR: %s"):format(err or "") end + +action = function() + if ( not(arg_username) or not(arg_password) ) then + return fail("No username or password was supplied") + end + + if ( not(arg_deviceindex) and not(arg_listdevices) ) then + return fail("No device ID was specificed") + end + + if ( 1 == tonumber(arg_listdevices) or "true" == arg_listdevices ) then + local mm = mobileme.Helper:new(arg_username, arg_password) + return listDevices(mm) + elseif ( not(arg_subject) or not(arg_message) ) then + return fail("Missing subject or message") + else + local mm = mobileme.Helper:new(arg_username, arg_password) + local status, devices = mm:getDevices() + + if ( not(status) ) then + return fail("Failed to get devices") + end + + if ( status and arg_deviceindex <= #devices ) then + local status = mm:sendMessage( devices[arg_deviceindex].id, arg_subject, arg_message, arg_sound) + if ( status ) then + return ("\n Message was successfully sent to \"%s\""):format(decodeString(devices[arg_deviceindex].name or "")) + else + return "\n Failed to send message" + end + end + end +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 8f8a02fa4..50654ac3e 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -130,6 +130,8 @@ Entry { filename = "http-generator.nse", categories = { "default", "discovery", Entry { filename = "http-google-malware.nse", categories = { "discovery", "external", "malware", "safe", } } Entry { filename = "http-grep.nse", categories = { "discovery", "safe", } } Entry { filename = "http-headers.nse", categories = { "discovery", "safe", } } +Entry { filename = "http-icloud-findmyiphone.nse", categories = { "discovery", "safe", } } +Entry { filename = "http-icloud-sendmsg.nse", categories = { "discovery", "safe", } } Entry { filename = "http-iis-webdav-vuln.nse", categories = { "intrusive", "vuln", } } Entry { filename = "http-joomla-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "http-litespeed-sourcecode-download.nse", categories = { "exploit", "intrusive", "vuln", } }