diff --git a/CHANGELOG b/CHANGELOG index bcc29f4a2..4e14084cb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a new library, afp.lua, and a script that uses it, + afp-showmount. The library is for the Apple Filing Protocol and the + script shows shares and their permissions. [Patrik Karlsson] + o Fixed a bug with the decoding of NMAP OID component values greater than 127. [Patrik Karlsson, David] diff --git a/nselib/afp.lua b/nselib/afp.lua new file mode 100644 index 000000000..4ee8f5c11 --- /dev/null +++ b/nselib/afp.lua @@ -0,0 +1,549 @@ +--- +-- This module was written by Patrik Karlsson and facilitates communication +-- with the Apple AFP Service. It is not feature complete and is missing several +-- functions and parameters. +-- +-- The library currently has enough functionality to query share names and access controls. +-- More functionality will be added once more scripts that depend on it are developed. +-- +-- + +-- Version 0.1 +-- Created 01/03/2010 - v0.1 - created by Patrik Karlsson + +module(... or "afp",package.seeall) + +-- Table of valid REQUESTs +local REQUEST = { + OpenSession = 0x04, + Command = 0x02 +} + +-- Table of headers flags to be set accordingly in requests and responses +local FLAGS = { + Request = 0, + Response = 1 +} + +-- Table of possible AFP_COMMANDs +COMMAND = { + FPCloseVol = 0x02, + FPLogin = 0x12, + FPGetUserInfo = 0x25, + FPGetSrvParms = 0x10, + FPOpenVol = 0x18, + FPOpenFork = 0x1a, + FPGetFileDirParams = 0x22, + FPReadExt = 0x3c, + FPEnumerateExt2 = 0x44 +} + +USER_BITMAP = { + UserId = 1, + PrimaryGroupId = 2, + UUID = 4 +} + +VOL_BITMAP = { + Attributes = 1, + Signature = 2, + CreationDate = 4, + ModificationDate = 8, + BackupDate = 16, + ID = 32, + BytesFree = 64, + BytesTotal = 128, + Name = 256, + ExtendedBytesFree = 512, + ExtendedBytesTotal = 1024, + BlockSize = 2048 +} + +FILE_BITMAP = { + Attributes = 1, + DID = 2, + CreationDate = 4, + ModificationDate = 8, + BackupDate = 16, + FinderInfo = 32, + LongName = 64, + ShortName = 128, + FileId = 256, + DataForkSize = 512, + ResourceForkSize = 1024, + ExtendedDataForkSize = 2048, + LaunchLimit = 4096, + UTF8Name = 8192, + ExtendedResourceForkSize = 16384, + UnixPrivileges = 32768 +} + +DIR_BITMAP = { + Attributes = 1, + DID = 2, + CreationDate = 4, + ModificationDate = 8, + BackupDate = 16, + FinderInfo = 32, + LongName = 64, + ShortName = 128, + FileId = 256, + OffspringCount = 512, + OwnerId = 1024, + GroupId = 2048, + AccessRights = 4096, + UTF8Name = 8192, + UnixPrivileges = 32768 +} + +PATH_TYPE = { + LongNames = 2, + UnicodeNames = 3 +} + +ACCESS_MODE = { + Read = 1, + Write = 2, + DenyRead = 16, + DenyWrite = 32 +} + +ACLS = { + OwnerSearch = 1, + OwnerRead = 2, + OwnerWrite = 4, + + GroupSearch = 256, + GroupRead = 512, + GroupWrite = 1024, + + EveryoneSearch = 65536, + EveryoneRead = 131072, + EveryoneWrite = 262144, + + UserSearch = 1048576, + UserRead = 2097152, + UserWrite = 4194304, + + BlankAccess = 268435456, + UserIsOwner = 2147483648 +} + +-- Each packet contains a sequential request id +-- this number is used within create_fp_packet and increased by one in each call +request_id = 1 + + +--- Creates an AFP packet +-- +-- @param command number should be one of the commands in the COMMAND table +-- @param data_offset number holding the offset to the data +-- @param data the actual data of the request +function create_fp_packet( command, data_offset, data ) + + local reserved = 0 + local data = data or "" + local data_len = data:len() + local header = bin.pack("CC>SIII", FLAGS.Request, command, request_id, data_offset, data_len, reserved) + local packet = header .. data + + request_id = request_id + 1 + return packet +end + +--- Parses the FP header (first 16-bytes of packet) +-- +-- @param packet string containing the raw packet +-- @return table with header data containing flags, command, +-- request_id, error_code, length and reserved fields +function parse_fp_header( packet ) + + local header = {} + local pos + + pos, header.flags, header.command, header.request_id = bin.unpack( "CC>S", packet ) + pos, header.error_code, header.length, header.reserved = bin.unpack( "I>II", packet:sub(5) ) + header.raw = packet:sub(1,16) + + return header + +end + +--- Sends an OpenSession AFP request to the server and handles the response +-- +-- @param socket already connected to the server +-- @return status (true or false) +-- @return nil (if status is true) or error string (if status is false) +function open_session( socket ) + + local data_offset = 0 + local option = 0x01 -- Attention Quantum + local option_len = 4 + local quantum = 1024 + + local data = bin.pack( "CCI", option, option_len, quantum ) + local packet = create_fp_packet( REQUEST.OpenSession, data_offset, data ) + + send_fp_packet( socket, packet ) + packet = read_fp_packet( socket ) + + if packet.header.error_code ~= 0 then + return false, string.format("OpenSession error: %d", packet.header.error_code) + end + + return true, nil +end + + + +--- Sends an FPGetUserInfo AFP request to the server and handles the response +-- +-- @param socket already connected to the server +-- @return status (true or false) +-- @return table with user information containing user_bitmap and +-- uid fields (if status is true) or error string (if status is false) +function fp_get_user_info( socket ) + + local packet + local data_offset = 0 + local flags = 1 -- Default User + local uid = 0 + local bitmap = USER_BITMAP.UserId + local response = {} + local pos + + local data = bin.pack( "CCI>S", COMMAND.FPGetUserInfo, flags, uid, bitmap ) + packet = create_fp_packet( REQUEST.Command, data_offset, data ) + + send_fp_packet( socket, packet ) + packet = read_fp_packet( socket ) + + if packet.header.error_code ~= 0 then + return false, string.format("OpenSession error: %d", packet.header.error_code) + end + + pos, response.user_bitmap, response.uid = bin.unpack(">S>I", packet.data) + + return true, response +end + +--- Sends an FPGetSrvrParms AFP request to the server and handles the response +-- +-- @param socket already connected to the server +-- @return status (true or false) +-- @return table with server parameters containing server_time, +-- vol_count, volumes fields (if status is true) or error string (if status is false) +-- +function fp_get_srvr_parms(socket) + + local packet + local data_offset = 0 + local response = {} + local pos = 0 + + local data = bin.pack("CC", COMMAND.FPGetSrvParms, 0) + packet = create_fp_packet( REQUEST.Command, data_offset, data ) + send_fp_packet( socket, packet ) + packet = read_fp_packet( socket ) + + if packet.header.error_code ~= 0 then + return false, string.format("FPGetSrvrParms error: %d", packet.header.error_code) + end + + data = packet.data + pos, response.server_time, response.vol_count = bin.unpack("IC", data) + + -- we should now be at the leading zero preceeding the first volume name + -- next is the length of the volume name, move pos there + pos = pos + 1 + + stdnse.print_debug("Volumes: %d", response.vol_count ) + response.volumes = {} + + for i=1, response.vol_count do + local _, vol_len = bin.unpack("C", data:sub(pos)) + local volume_name = data:sub(pos + 1, pos + 1 + vol_len) + pos = pos + vol_len + 2 + table.insert(response.volumes, string.format("%s", volume_name) ) + stdnse.print_debug("Volume name: %s", volume_name) + end + + return true, response +end + + +--- Sends an FPLogin request to the server and handles the response +-- +-- This function currently only supports the 3.1 through 3.3 protocol versions +-- It does not support authentication so the uam parameter is currently ignored +-- +-- @param socket already connected to the server-- +-- @param afp_version string (AFP3.3|AFP3.2|AFP3.1) +-- @param uam string containing authentication information (currently ignored) +-- @return status (true or false) +-- @return nil (if status is true) or error string (if status is false) +function fp_login( socket, afp_version, uam ) + + local packet + local data_offset = 0 + + -- currently we only support AFP3.3 + if afp_version == nil or ( afp_version ~= "AFP3.3" and afp_version ~= "AFP3.2" and afp_version ~= "AFP3.1" ) then + return + end + + uam = "No User Authent" + + local data = bin.pack( "CCACA", COMMAND.FPLogin, afp_version:len(), afp_version, uam:len(), uam ) + packet = create_fp_packet( REQUEST.Command, data_offset, data ) + send_fp_packet( socket, packet ) + packet = read_fp_packet( socket ) + + if packet.header.error_code ~= 0 then + return false, string.format("FPLogin error: %d", packet.header.error_code) + end + + return true, nil +end + +--- Reads a AFP packet of the socket +-- +-- @param socket socket connected to the server +-- @return table containing data and header fields +function read_fp_packet( socket ) + + local packet = {} + local buf = "" + + local catch = function() + socket:close() + end + + local try = nmap.new_try(catch) + + repeat + buf = buf .. try( socket:receive(16) ) + until buf:len() >= 16 -- make sure we have got atleast the header + + packet.header = parse_fp_header( buf ) + + -- if we didn't get the whole packet when reading the header, try to read the rest + while buf:len() < packet.header.length + packet.header.raw:len() do + buf = buf .. try( socket:receive(packet.header.length) ) + end + + packet.data = buf:len() > 16 and buf:sub( 17 ) or "" + + return packet + +end + +--- Sends an FPOpenVol request to the server and handles the response +-- +-- @param socket already connected to the server +-- @param bitmap number bitmask of volume information to request +-- @param volume_name string containing the volume name to query +-- @return status (true or false) +-- @return table containing bitmap and volume_id fields +-- (if status is true) or error string (if status is false) +function fp_open_vol( socket, bitmap, volume_name ) + + local packet + local data_offset = 0 + local pad = 0 + local response = {} + local pos + + local data = bin.pack("CC>SCA", COMMAND.FPOpenVol, pad, bitmap, volume_name:len(), volume_name ) + packet = create_fp_packet( REQUEST.Command, data_offset, data ) + send_fp_packet( socket, packet ) + packet = read_fp_packet( socket ) + + if packet.header.error_code ~= 0 then + return false, string.format("FPOpenVol error: %d", packet.header.error_code ) + end + + pos, response.bitmap, response.volume_id = bin.unpack(">S>S", packet.data) + + return true, response + +end + +--- Sends an FPGetFileDirParms request to the server and handles the response +-- +-- Currently only handles a request for the Access rights (file_bitmap must be 0 and dir_bitmap must be 0x1000) +-- +-- @param socket already connected to the server +-- @param volume_id number containing the id of the volume to query +-- @param did number containing the id of the directory to query +-- @param file_bitmap number bitmask of file information to query +-- @param dir_bitmap number bitmask of directory information to query +-- @param path string containing the name of the directory to query +-- @return status (true or false) +-- @return table containing file_bitmap, dir_bitmap, +-- file_type and acls fields +-- (if status is true) or error string (if status is false) +function fp_get_file_dir_parms( socket, volume_id, did, file_bitmap, dir_bitmap, path ) + + local packet + local data_offset = 0 + local pad = 0 + local response = {} + local pos + + if file_bitmap ~= 0 or dir_bitmap ~= DIR_BITMAP.AccessRights then + return false, "Only AccessRights querys are supported (file_bitmap=0, dir_bitmap=DIR_BITMAP.AccessRights)" + end + + local data = bin.pack("CC>S>I>S>SCCAC", COMMAND.FPGetFileDirParams, pad, volume_id, did, file_bitmap, dir_bitmap, path.type, path.len, path.name, 0) + packet = create_fp_packet( REQUEST.Command, data_offset, data ) + send_fp_packet( socket, packet ) + packet = read_fp_packet( socket ) + + if packet.header.error_code ~= 0 then + return false, string.format("FPGetFileDirParms error: %d", packet.header.error_code ) + end + + pos, response.file_bitmap, response.dir_bitmap, response.file_type, pad, response.acls = bin.unpack( ">S>SCC>I", packet.data ) + + return true, response +end + +--- Sends an FPEnumerateExt2 request to the server and handles the response +-- +-- @param socket already connected to the server +-- @param volume_id number containing the id of the volume to query +-- @param did number containing the id of the directory to query +-- @param file_bitmap number bitmask of file information to query +-- @param dir_bitmap number bitmask of directory information to query +-- @param req_count number +-- @param start_index number +-- @param reply_size number +-- @param path string containing the name of the directory to query +-- @return status (true or false) +-- @return table containing file_bitmap, dir_bitmap, +-- req_count fields +-- (if status is true) or error string (if status is false) +function fp_enumerate_ext2( socket, volume_id, did, file_bitmap, dir_bitmap, req_count, start_index, reply_size, path ) + + local packet + local data_offset = 0 + local pad = 0 + local response = {} + + local data = bin.pack( "CC>S>I>S>S", COMMAND.FPEnumerateExt2, pad, volume_id, did, file_bitmap, dir_bitmap ) + data = data .. bin.pack( ">S>I>IC>SA", req_count, start_index, reply_size, path.type, path.len, path.name ) + packet = create_fp_packet( REQUEST.Command, data_offset, data ) + + send_fp_packet( socket, packet ) + packet = read_fp_packet( socket ) + + if packet.header.error_code ~= 0 then + return false, string.format("FPEnumerateExt2 error: %d", packet.header.error_code ) + end + + _, response.file_bitmap, response.dir_bitmap, response.req_count = bin.unpack(">S>S>S", packet.data) + + return true, response + +end + +--- Sends an FPOpenFork request to the server and handles the response +-- +-- @param socket already connected to the server +-- @param fork number +-- @param volume_id number containing the id of the volume to query +-- @param did number containing the id of the directory to query +-- @param file_bitmap number bitmask of file information to query +-- @param access_mode number containing bitmask of options from ACCESS_MODE +-- @param path string containing the name of the directory to query +-- @return status (true or false) +-- @return table containing file_bitmap and fork fields (if status is true) or +-- error string (if status is false) +function fp_open_fork( socket, fork, volume_id, did, file_bitmap, access_mode, path ) + + local packet + local data_offset = 0 + local pad = 0 + local response = {} + + local data = bin.pack( "CC>S>I>S>S", COMMAND.FPOpenFork, fork, volume_id, did, file_bitmap, access_mode ) + + if path.type == PATH_TYPE.LongNames then + data = data .. bin.pack( "C>SA", path.type, path.len, path.name ) + end + + if path.type == PATH_TYPE.UnicodeNames then + local unicode_hint = 0x08000103 + data = data .. bin.pack( "C>I>SA", path.type, unicode_hint, path.len, path.name ) + end + + packet = create_fp_packet( REQUEST.Command, data_offset, data ) + send_fp_packet( socket, packet ) + packet = read_fp_packet( socket ) + + if packet.header.error_code ~= 0 then + return false, string.format("FPOpenFork error: %d", packet.header.error_code ) + end + + _, response.file_bitmap, response.fork = bin.unpack(">S>S", packet.data) + + return true, response + +end + +--- Sends an FPCloseVol request to the server and handles the response +-- +-- @param socket already connected to the server +-- @param volume_id number containing the id of the volume to close +-- @return status (true or false) +-- @return nil (if status is true) or error string (if status is false) +function fp_close_vol( socket, volume_id ) + + local packet + local data_offset = 0 + local pad = 0 + local response = {} + + local data = bin.pack( "CC>S>", COMMAND.FPCloseVol, pad, volume_id ) + + packet = create_fp_packet( REQUEST.Command, data_offset, data ) + send_fp_packet( socket, packet ) + packet = read_fp_packet( socket ) + + if packet.header.error_code ~= 0 then + return false, string.format("FPCloseVol error: %d", packet.header.error_code ) + end + + return true, nil + +end + +--- Sends the raw packet over the socket +-- +-- @param socket already connected to the server +-- @param packet containing the raw data +function send_fp_packet( socket, packet ) + + local catch = function() + socket:close() + end + + local try = nmap.new_try(catch) + try( socket:send(packet) ) + +end + + +function fp_read_ext( fork, offset, count ) + + local packet + local data_offset = 0 + local pad = 0 + + local data = bin.pack( "CC>S>L>L", COMMAND.FPReadExt, pad, fork, offset, count ) + packet = create_fp_packet( REQUEST.Command, data_offset, data ) + + return packet + +end diff --git a/scripts/afp-showmount.nse b/scripts/afp-showmount.nse new file mode 100644 index 000000000..a2f5703aa --- /dev/null +++ b/scripts/afp-showmount.nse @@ -0,0 +1,194 @@ +description = [[ Shows AFP shares and ACLs ]] + +--- +--@output +-- PORT STATE SERVICE +-- 548/tcp open afp +-- | afp-showmount: +-- | Yoda's Public Folder +-- | Owner: Search,Read,Write +-- | Group: Search,Read +-- | Everyone: Search,Read +-- | User: Search,Read +-- | Vader's Public Folder +-- | Owner: Search,Read,Write +-- | Group: Search,Read +-- | Everyone: Search,Read +-- | User: Search,Read +-- |_ Options: IsOwner + +-- Version 0.1 +-- Created 01/03/2010 - v0.1 - created by Patrik Karlsson +-- Revised 01/13/2010 - v0.2 - Fixed a bug where a single share wouldn't show due to formatting issues + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require 'shortport' +require 'stdnse' +require 'afp' + +portrule = shortport.portnumber(548, "tcp") + +--- Converts a group bitmask of Search, Read and Write to string +-- eg. WRS, -RS, W-S, etc .. +-- +-- @param acls number containing bitmasked acls +-- @return string of ACLS +function acl_group_to_string( acls ) + + local acl_string = "" + + if bit.band( acls, afp.ACLS.OwnerSearch ) == afp.ACLS.OwnerSearch then + acl_string = "S" + else + acl_string = "-" + end + + if bit.band( acls, afp.ACLS.OwnerRead ) == afp.ACLS.OwnerRead then + acl_string = "R" .. acl_string + else + acl_string = "-" .. acl_string + end + + if bit.band( acls, afp.ACLS.OwnerWrite ) == afp.ACLS.OwnerWrite then + acl_string = "W" .. acl_string + else + acl_string = "-" .. acl_string + end + + return acl_string +end + +--- Converts a group bitmask of Search, Read and Write to table +-- +-- @param acls number containing bitmasked acls +-- @return table of ACLs +function acl_group_to_long_string(acls) + + local acl_table = {} + + if bit.band( acls, afp.ACLS.OwnerSearch ) == afp.ACLS.OwnerSearch then + table.insert( acl_table, "Search") + end + + if bit.band( acls, afp.ACLS.OwnerRead ) == afp.ACLS.OwnerRead then + table.insert( acl_table, "Read") + end + + if bit.band( acls, afp.ACLS.OwnerWrite ) == afp.ACLS.OwnerWrite then + table.insert( acl_table, "Write") + end + + return acl_table +end + +--- Converts a numeric acl to string +-- +-- @param acls number containig acls as recieved from fp_get_file_dir_parms +-- @return string of ACLs +function acls_to_string( acls ) + + local owner = acl_group_to_string( bit.band( acls, 255 ) ) + local group = acl_group_to_string( bit.band( bit.rshift(acls, 8), 255 ) ) + local everyone = acl_group_to_string( bit.band( bit.rshift(acls, 16), 255 ) ) + local user = acl_group_to_string( bit.band( bit.rshift(acls, 24), 255 ) ) + + local blank = bit.band( acls, afp.ACLS.BlankAccess ) == afp.ACLS.BlankAccess and "B" or "-" + local isowner = bit.band( acls, afp.ACLS.UserIsOwner ) == afp.ACLS.UserIsOwner and "O" or "-" + + return string.format("Owner: %s; Group: %s; Everyone: %s; User: %s; Options: %s%s", owner, group, everyone, user, blank, isowner ) + +end + +--- Converts a numeric acl to string +-- +-- @param acls number containig acls as recieved from fp_get_file_dir_parms +-- @return table of long ACLs +function acls_to_long_string( acls ) + + local owner = acl_group_to_long_string( bit.band( acls, 255 ) ) + local group = acl_group_to_long_string( bit.band( bit.rshift(acls, 8), 255 ) ) + local everyone = acl_group_to_long_string( bit.band( bit.rshift(acls, 16), 255 ) ) + local user = acl_group_to_long_string( bit.band( bit.rshift(acls, 24), 255 ) ) + + local blank = bit.band( acls, afp.ACLS.BlankAccess ) == afp.ACLS.BlankAccess and "Blank" or nil + local isowner = bit.band( acls, afp.ACLS.UserIsOwner ) == afp.ACLS.UserIsOwner and "IsOwner" or nil + + local options = {} + + if blank then + table.insert(options, "Blank") + end + + if isowner then + table.insert(options, "IsOwner") + end + + local acls_tbl = {} + + table.insert( acls_tbl, string.format( "Owner: %s", stdnse.strjoin(",", owner) ) ) + table.insert( acls_tbl, string.format( "Group: %s", stdnse.strjoin(",", group) ) ) + table.insert( acls_tbl, string.format( "Everyone: %s", stdnse.strjoin(",", everyone) ) ) + table.insert( acls_tbl, string.format( "User: %s", stdnse.strjoin(",", user) ) ) + + if #options > 0 then + table.insert( acls_tbl, string.format( "Options: %s", stdnse.strjoin(",", options ) ) ) + end + + return acls_tbl + +end + +action = function(host, port) + + local socket = nmap.new_socket() + local status + local result = {} + + -- set a reasonable timeout value + socket:set_timeout(5000) + + -- do some exception handling / cleanup + local catch = function() + socket:close() + end + + local try = nmap.new_try(catch) + + try( socket:connect(host.ip, port.number, "tcp") ) + + response = try( afp.open_session(socket) ) + response = try( afp.fp_login( socket, "AFP3.1", "No User Authent") ) + response = try( afp.fp_get_user_info( socket ) ) + response = try( afp.fp_get_srvr_parms( socket ) ) + + volumes = response.volumes + + for _, vol in pairs(volumes) do + table.insert( result, vol ) + + status, response = afp.fp_open_vol( socket, afp.VOL_BITMAP.ID, vol ) + + if status then + local vol_id = response.volume_id + stdnse.print_debug(string.format("Vol_id: %d", vol_id)) + + local path = {} + path.type = afp.PATH_TYPE.LongNames + path.name = "" + path.len = path.name:len() + + response = try( afp.fp_get_file_dir_parms( socket, vol_id, 2, 0, afp.DIR_BITMAP.AccessRights, path ) ) + local acls = acls_to_long_string(response.acls) + acls.name = nil + try( afp.fp_close_vol( socket, vol_id ) ) + table.insert( result, acls ) + end + + end + + return stdnse.format_output(true, result) + +end \ No newline at end of file