---
-- 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.2
-- Created 01/03/2010 - v0.1 - created by Patrik Karlsson
-- Revised 01/20/2010 - v0.2 - updated all bitmaps to hex for better readability
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 = 0x01,
PrimaryGroupId = 0x2,
UUID = 0x4
}
VOL_BITMAP = {
Attributes = 0x1,
Signature = 0x2,
CreationDate = 0x4,
ModificationDate = 0x8,
BackupDate = 0x10,
ID = 0x20,
BytesFree = 0x40,
BytesTotal = 0x80,
Name = 0x100,
ExtendedBytesFree = 0x200,
ExtendedBytesTotal = 0x400,
BlockSize = 0x800
}
FILE_BITMAP = {
Attributes = 0x1,
DID = 0x2,
CreationDate = 0x4,
ModificationDate = 0x8,
BackupDate = 0x10,
FinderInfo = 0x20,
LongName = 0x40,
ShortName = 0x80,
FileId = 0x100,
DataForkSize = 0x200,
ResourceForkSize = 0x400,
ExtendedDataForkSize = 0x800,
LaunchLimit = 0x1000,
UTF8Name = 0x2000,
ExtendedResourceForkSize = 0x4000,
UnixPrivileges = 0x8000
}
DIR_BITMAP = {
Attributes = 0x1,
DID = 0x2,
CreationDate = 0x4,
ModificationDate = 0x8,
BackupDate = 0x10,
FinderInfo = 0x20,
LongName = 0x40,
ShortName = 0x80,
FileId = 0x100,
OffspringCount = 0x200,
OwnerId = 0x400,
GroupId = 0x800,
AccessRights = 0x1000,
UTF8Name = 0x2000,
UnixPrivileges = 0x8000
}
PATH_TYPE = {
LongNames = 2,
UnicodeNames = 3
}
ACCESS_MODE = {
Read = 0x1,
Write = 0x2,
DenyRead = 0x10,
DenyWrite = 0x20
}
ACLS = {
OwnerSearch = 0x1,
OwnerRead = 0x2,
OwnerWrite = 0x4,
GroupSearch = 0x100,
GroupRead = 0x200,
GroupWrite = 0x400,
EveryoneSearch = 0x10000,
EveryoneRead = 0x20000,
EveryoneWrite = 0x40000,
UserSearch = 0x100000,
UserRead = 0x200000,
UserWrite = 0x400000,
BlankAccess = 0x10000000,
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
--- 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 _
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 _
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