1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-06 04:31:29 +00:00
Files
nmap/nselib/iscsi.lua
dmiller 2692746c42 NSEdoc cleanup
Mostly splitting function summaries (the first paragraph of NSEdoc) from
the body of the description to make the summary indexes shorter and
easier to scan.

Also fixed some unbalanced code tags like <code>foo</table>
2014-09-02 18:23:06 +00:00

729 lines
21 KiB
Lua

--- An iSCSI library implementing written by Patrik Karlsson <patrik@cqure.net>
-- The library currently supports target discovery and login.
--
-- The implementation is based on packetdumps and the iSCSI RFC
-- * http://tools.ietf.org/html/rfc3720
--
-- The library contains the protocol message pairs in <code>Packet</code>
-- E.g. <code>LoginRequest</code> and <code>LoginResponse</code>
--
-- Each request can be "serialized" to a string using:
-- <code>tostring(request)</code>.
-- All responses can be read and instantiated from the socket by calling:
-- <code>local status,resp = Response.fromSocket(sock)</code>
--
-- In addition the library has the following classes:
-- * <code>Packet</code>
-- ** A class containing the request and response packets
-- * <code>Comm</code>
-- ** A class used to send and receive packet between the library and server
-- ** The class handles some of the packet "counting" and value updating
-- * <code>KVP</code>
-- ** A key/value pair class that holds key value pairs
-- * <code>Helper</code>
-- ** A class that wraps the <code>Comm</code> and <code>Packet</code> classes
-- ** The purpose of the class is to provide easy access to common iSCSI task
--
--
-- @author "Patrik Karlsson <patrik@cqure.net>"
-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
-- Version 0.2
-- Created 2010/11/18 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 2010/11/28 - v0.2 - improved error handling, fixed discovery issues
-- with multiple addresses <patrik@cqure.net>
local bin = require "bin"
local bit = require "bit"
local ipOps = require "ipOps"
local match = require "match"
local nmap = require "nmap"
local stdnse = require "stdnse"
local openssl = stdnse.silent_require "openssl"
local string = require "string"
local table = require "table"
_ENV = stdnse.module("iscsi", stdnse.seeall)
Packet = {
Opcode = {
LOGIN = 0x03,
TEXT = 0x04,
LOGOUT = 0x06,
},
LoginRequest = {
CSG = {
SecurityNegotiation = 0,
LoginOperationalNegotiation = 1,
FullFeaturePhase = 3,
},
NSG = {
SecurityNegotiation = 0,
LoginOperationalNegotiation = 1,
FullFeaturePhase = 3,
},
--- Creates a new instance of LoginRequest
--
-- @return instance of LoginRequest
new = function( self )
local o = {}
setmetatable(o, self)
self.__index = self
o.immediate = 0
o.opcode = Packet.Opcode.LOGIN
o.flags = {}
o.ver_max = 0
o.ver_min = 0
o.total_ahs_len = 0
o.data_seg_len = 0
o.isid = { t=0x01, a=0x00, b=0x0001, c=0x37, d=0 }
o.tsih = 0
o.initiator_task_tag = 1
o.cid = 1
o.cmdsn = 0
o.expstatsn = 1
o.kvp = KVP:new()
return o
end,
setImmediate = function(self, b) self.immediate = ( b and 1 or 0 ) end,
--- Sets the transit bit
--
-- @param b boolean containing the new transit value
setTransit = function(self, b) self.flags.transit = ( b and 1 or 0 ) end,
--- Sets the continue bit
--
-- @param b boolean containing the new continue value
setContinue = function(self, b) self.flags.continue = ( b and 1 or 0 ) end,
--- Sets the CSG values
--
-- @param csg number containing the new NSG value
setCSG = function(self, csg) self.flags.csg = csg end,
--- Sets the NSG values
--
-- @param nsg number containing the new NSG value
setNSG = function(self, nsg) self.flags.nsg = nsg end,
--- Converts the class instance to string
--
-- @return string containing the converted instance
__tostring = function( self )
local reserved = 0
local kvps = tostring(self.kvp)
self.data_seg_len = #kvps
local pad = 4 - ((#kvps + 48) % 4)
pad = ( pad == 4 ) and 0 or pad
for i=1, pad do kvps = kvps .. "\0" end
local len = bit.lshift( self.total_ahs_len, 24 ) + self.data_seg_len
local flags = bit.lshift( ( self.flags.transit or 0 ), 7 )
flags = flags + bit.lshift( ( self.flags.continue or 0 ), 6)
flags = flags + ( self.flags.nsg or 0 )
flags = flags + bit.lshift( ( self.flags.csg or 0 ), 2 )
local opcode = self.opcode + bit.lshift((self.immediate or 0), 6)
local data = bin.pack(">CCCCICSCSSISSIILLA", opcode,
flags, self.ver_max, self.ver_min, len,
bit.lshift( self.isid.t, 6 ) + bit.band( self.isid.a, 0x3f),
self.isid.b, self.isid.c, self.isid.d, self.tsih,
self.initiator_task_tag, self.cid, reserved, self.cmdsn,
self.expstatsn, reserved, reserved, kvps )
return data
end
},
LoginResponse = {
-- Error messages
ErrorMsgs = {
[0x0000] = "Success",
[0x0101] = "Target moved temporarily",
[0x0102] = "Target moved permanently",
[0x0200] = "Initiator error",
[0x0201] = "Authentication failure",
[0x0202] = "Authorization failure",
[0x0203] = "Target not found",
[0x0204] = "Target removed",
[0x0205] = "Unsupported version",
[0x0206] = "Too many connections",
[0x0207] = "Missing parameter",
[0x0208] = "Can't include in session",
[0x0209] = "Session type not supported",
[0x020a] = "Session does not exist",
[0x020b] = "Invalid request during login",
[0x0300] = "Target error",
[0x0301] = "Service unavailable",
[0x0302] = "Out of resources",
},
-- Error constants
Errors = {
SUCCESS = 0,
AUTH_FAILED = 0x0201,
},
--- Creates a new instance of LoginResponse
--
-- @return instance of LoginResponse
new = function( self )
local o = {}
setmetatable(o, self)
self.__index = self
return o
end,
--- Returns the error message
getErrorMessage = function( self )
return Packet.LoginResponse.ErrorMsgs[self.status_code] or "Unknown error"
end,
--- Returns the error code
getErrorCode = function( self ) return self.status_code or 0 end,
--- Creates a LoginResponse with data read from the socket
--
-- @return status true on success, false on failure
-- @return resp instance of LoginResponse
fromSocket = function( s )
local status, header = s:receive_buf(match.numbytes(48), true)
if ( not(status) ) then
return false, "Failed to read header from socket"
end
local resp = Packet.LoginResponse:new()
local pos, len = bin.unpack(">I", header, 5)
resp.total_ahs_len = bit.rshift(len, 24)
resp.data_seg_len = bit.band(len, 0x00ffffff)
pos, resp.status_code = bin.unpack(">S", header, 37)
local pad = ( 4 - ( resp.data_seg_len % 4 ) )
pad = ( pad == 4 ) and 0 or pad
local status, data = s:receive_buf(match.numbytes(resp.data_seg_len + pad), true)
if ( not(status) ) then
return false, "Failed to read data from socket"
end
resp.kvp = KVP:new()
for _, kvp in ipairs(stdnse.strsplit( "\0", data )) do
local k, v = kvp:match("(.*)=(.*)")
if ( v ) then resp.kvp:add( k, v ) end
end
return true, resp
end,
},
TextRequest = {
--- Creates a new instance of TextRequest
--
-- @return instance of TextRequest
new = function( self )
local o = {}
setmetatable(o, self)
self.__index = self
o.opcode = Packet.Opcode.TEXT
o.flags = {}
o.flags.final = 0
o.flags.continue = 0
o.total_ahs_len = 0
o.data_seg_len = 0
o.lun = 0
o.initiator_task_tag = 1
o.target_trans_tag = 0xffffffff
o.cmdsn = 2
o.expstatsn = 1
o.kvp = KVP:new()
return o
end,
--- Sets the final bit of the TextRequest
setFinal = function( self, b ) self.flags.final = ( b and 1 or 0 ) end,
--- Sets the continue bit of the TextRequest
setContinue = function( self, b ) self.flags.continue = ( b and 1 or 0 ) end,
--- Converts the class instance to string
--
-- @return string containing the converted instance
__tostring = function(self)
local flags = bit.lshift( ( self.flags.final or 0 ), 7 )
flags = flags + bit.lshift( (self.flags.continue or 0), 6 )
local kvps = tostring(self.kvp)
for i=1, (#kvps % 2) do kvps = kvps .. "\0" end
self.data_seg_len = #kvps
local len = bit.lshift( self.total_ahs_len, 24 ) + self.data_seg_len
local reserved = 0
local data = bin.pack(">CCSILIIIILLA", self.opcode, flags, reserved,
len, self.lun, self.initiator_task_tag, self.target_trans_tag,
self.cmdsn, self.expstatsn, reserved, reserved, kvps)
return data
end,
},
TextResponse = {
--- Creates a new instance of TextResponse
--
-- @return instance of TextResponse
new = function( self )
local o = {}
setmetatable(o, self)
self.__index = self
return o
end,
--- Creates a TextResponse with data read from the socket
--
-- @return status true on success, false on failure
-- @return instance of TextResponse
-- err string containing error message
fromSocket = function( s )
local resp = Packet.TextResponse:new()
local textdata = ""
repeat
local status, header = s:receive_buf(match.numbytes(48), true)
local pos, _, flags, _, _, len = bin.unpack(">CCCCI", header)
local cont = ( bit.band(flags, 0x40) == 0x40 )
resp.total_ahs_len = bit.rshift(len, 24)
resp.data_seg_len = bit.band(len, 0x00ffffff)
local data
status, data = s:receive_buf(match.numbytes(resp.data_seg_len), true)
textdata = textdata .. data
until( not(cont) )
resp.records = {}
local kvps = stdnse.strsplit( "\0", textdata )
local record
-- Each target record starts with one text key of the form:
-- TargetName=<target-name-goes-here>
-- Followed by zero or more address keys of the form:
-- TargetAddress=<hostname-or-ipaddress>[:<tcp-port>],
-- <portal-group-tag>
for _, kvp in ipairs(kvps) do
local k, v = kvp:match("(.*)%=(.*)")
if ( k == "TargetName" ) then
if ( record ) then
table.insert(resp.records, record)
record = {}
end
if ( #resp.records == 0 ) then record = {} end
record.name = v
elseif ( k == "TargetAddress" ) then
record.addr = record.addr or {}
table.insert( record.addr, v )
elseif ( not(k) ) then
-- this should be the ending empty kvp
table.insert(resp.records, record)
break
else
stdnse.debug1("ERROR: iscsi.TextResponse: Unknown target record (%s)", k)
end
end
return true, resp
end,
},
--- Class handling a login request
LogoutRequest = {
--- Creates a new instance of LogoutRequest
--
-- @return instance of LogoutRequest
new = function( self )
local o = {}
setmetatable(o, self)
self.__index = self
o.opcode = Packet.Opcode.LOGOUT
o.immediate = 1
o.reasoncode = 0
o.total_ahs_len = 0
o.data_seg_len = 0
o.initiator_task_tag = 2
o.cid = 1
o.cmdsn = 0
o.expstatsn = 1
return o
end,
--- Converts the class instance to string
--
-- @return string containing the converted instance
__tostring = function(self)
local opcode = self.opcode + bit.lshift((self.immediate or 0), 6)
local reserved = 0
local len = bit.lshift( self.total_ahs_len, 24 ) + self.data_seg_len
local data = bin.pack(">CCSILISSIILL", opcode, (0x80 + self.reasoncode),
reserved, len, reserved,self.initiator_task_tag, self.cid,
reserved, self.cmdsn, self.expstatsn, reserved, reserved )
return data
end,
},
--- Class handling the Logout response
LogoutResponse = {
--- Creates a new instance of LogoutResponse
--
-- @return instance of LogoutResponse
new = function( self )
local o = {}
setmetatable(o, self)
self.__index = self
return o
end,
--- Creates a LogoutResponse with data read from the socket
--
-- @return status true on success, false on failure
-- @return instance of LogoutResponse
-- err string containing error message
fromSocket = function( s )
local resp = Packet.LogoutResponse:new()
local status, header = s:receive_buf(match.numbytes(48), true)
if ( not(status) ) then return status, header end
return true, resp
end
}
}
--- The communication class handles socket reads and writes
--
-- In addition it keeps track of both immediate packets and the amount of read
-- packets and updates cmdsn and expstatsn accordingly.
Comm = {
--- Creates a new instance of Comm
--
-- @return instance of Comm
new = function(self, socket)
local o = {}
setmetatable(o, self)
self.__index = self
o.expstatsn = 0
o.cmdsn = 1
o.socket = socket
return o
end,
--- Sends a packet and retrieves the response
--
-- @param out_packet instance of a packet to send
-- @param in_class class of the packet to read
-- @return status true on success, false on failure
-- @return r decoded instance of in_class
exchange = function( self, out_packet, in_class )
local expstatsn = ( self.expstatsn == 0 ) and 1 or self.expstatsn
if ( out_packet.immediate and out_packet.immediate == 1 ) then
self.cmdsn = self.cmdsn + 1
end
out_packet.expstatsn = expstatsn
out_packet.cmdsn = self.cmdsn
self.socket:send( tostring( out_packet ) )
local status, r = in_class.fromSocket( self.socket )
self.expstatsn = self.expstatsn + 1
return status, r
end,
}
--- Key/Value pairs class
KVP = {
--- Creates a new instance of KVP
--
-- @return instance of KVP
new = function( self )
local o = {}
setmetatable(o, self)
self.__index = self
o.kvp = {}
return o
end,
--- Adds a key/value pair
--
-- @param key string containing the key name
-- @param value string containing the value
add = function( self, key, value )
table.insert( self.kvp, {[key]=value} )
end,
--- Gets all values for a specific key
--
-- @param key string containing the name of the key to retrieve
-- @return values table containing all values for the specified key
get = function( self, key )
local values = {}
for _, kvp in ipairs(self.kvp) do
for k, v in pairs( kvp ) do
if ( key == k ) then
table.insert( values, v )
end
end
end
return values
end,
--- Returns all key value pairs as string delimited by \0
-- eg. "key1=val1\0key2=val2\0"
--
-- @return string containing all key/value pairs
__tostring = function( self )
local ret = ""
for _, kvp in ipairs(self.kvp) do
for k, v in pairs( kvp ) do
ret = ret .. ("%s=%s\0"):format(k,v)
end
end
return ret
end,
}
--- CHAP authentication class
CHAP = {
--- Calculate a CHAP - response
--
-- @param identifier number containing the CHAP identifier
-- @param challenge string containing the challenge
-- @param secret string containing the users password
-- @return response string containing the CHAP response
calcResponse = function( identifier, challenge, secret )
return openssl.md5( identifier .. secret .. challenge )
end,
}
--- The helper class contains functions with more descriptive names
Helper = {
--- Creates a new instance of the Helper class
--
-- @param host table as received by the script action function
-- @param port table as received by the script action function
-- @return o instance of Helper
new = function( self, host, port )
local o = {}
setmetatable(o, self)
self.__index = self
o.host, o.port = host, port
o.socket = nmap.new_socket()
return o
end,
--- Connects to the iSCSI target
--
-- @return status true on success, false on failure
-- @return err string containing error message is status is false
connect = function( self )
self.socket:set_timeout(10000)
local status, err = self.socket:connect(self.host, self.port, "tcp")
if ( not(status) ) then return false, err end
self.comm = Comm:new( self.socket )
return true
end,
--- Attempts to discover accessible iSCSI targets on the remote server
--
-- @return status true on success, false on failure
-- @return targets table containing discovered targets
-- each table entry is a target table with <code>name</code>
-- and <code>addr</code>.
-- err string containing an error message is status is false
discoverTargets = function( self )
local p = Packet.LoginRequest:new()
p:setTransit(true)
p:setNSG(Packet.LoginRequest.NSG.LoginOperationalNegotiation)
p.kvp:add( "InitiatorName", "iqn.1991-05.com.microsoft:nmap_iscsi_probe" )
p.kvp:add( "SessionType", "Discovery" )
p.kvp:add( "AuthMethod", "None" )
local status, resp = self.comm:exchange( p, Packet.LoginResponse )
if ( not(status) ) then
return false, ("ERROR: iscsi.Helper.discoverTargets: %s"):format(resp)
end
local auth_method = resp.kvp:get("AuthMethod")[1]
if ( auth_method:upper() ~= "NONE" ) then
return false, "ERROR: iscsi.Helper.discoverTargets: Unsupported authentication method"
end
p = Packet.LoginRequest:new()
p:setTransit(true)
p:setNSG(Packet.LoginRequest.NSG.FullFeaturePhase)
p:setCSG(Packet.LoginRequest.CSG.LoginOperationalNegotiation)
p.kvp:add( "HeaderDigest", "None")
p.kvp:add( "DataDigest", "None")
p.kvp:add( "MaxRecvDataSegmentLength", "65536")
p.kvp:add( "DefaultTime2Wait", "0")
p.kvp:add( "DefaultTime2Retain", "60")
status, resp = self.comm:exchange( p, Packet.LoginResponse )
p = Packet.TextRequest:new()
p:setFinal(true)
p.kvp:add( "SendTargets", "All" )
status, resp = self.comm:exchange( p, Packet.TextResponse )
if ( not(resp.records) ) then
return false, "iscsi.discoverTargets: response returned no targets"
end
for _, record in ipairs(resp.records) do
table.sort( record.addr, function(a, b) local c = ipOps.compare_ip(a:match("(.-):"), "le", b:match("(.-):")); return c end )
end
return true, resp.records
end,
--- Logs out from the iSCSI target
--
-- @return status true on success, false on failure
logout = function(self)
local p = Packet.LogoutRequest:new()
local status, resp = self.comm:exchange( p, Packet.LogoutResponse )
return status
end,
--- Authenticate to the iSCSI service
--
-- @param target_name string containing the name of the iSCSI target
-- @param username string containing the username
-- @param password string containing the password
-- @param auth_method string containing either "None" or "Chap"
-- @return status true on success false on failure
-- @return response containing the loginresponse or
-- err string containing an error message if status is false
login = function( self, target_name, username, password, auth_method )
local auth_method = auth_method or "None"
if ( not(target_name) ) then
return false, "No target name specified"
end
if ( auth_method:upper()~= "NONE" and
auth_method:upper()~= "CHAP" ) then
return false, "Unknown authentication method"
end
local p = Packet.LoginRequest:new()
p:setTransit(true)
p:setNSG(Packet.LoginRequest.NSG.LoginOperationalNegotiation)
p.kvp:add( "InitiatorName", "iqn.1991-05.com.microsoft:nmap_iscsi_probe" )
p.kvp:add( "SessionType", "Normal" )
p.kvp:add( "TargetName", target_name )
p.kvp:add( "AuthMethod", auth_method )
if ( not(self.comm) ) then
return false, "ERROR: iscsi.Helper.login: Not connected"
end
local status, resp = self.comm:exchange( p, Packet.LoginResponse )
if ( not(status) ) then
return false, ("ERROR: iscsi.Helper.login: %s"):format(resp)
end
if ( resp.status_code ~= 0 ) then
stdnse.debug3("ERROR: iscsi.Helper.login: Authentication failed (error code: %d)", resp.status_code)
return false, resp
elseif ( auth_method:upper()=="NONE" ) then
return true, resp
end
p = Packet.LoginRequest:new()
p.kvp:add( "CHAP_A", "5" )
status, resp = self.comm:exchange( p, Packet.LoginResponse )
if ( not(status) ) then
return false, ("ERROR: iscsi.Helper.login: %s"):format(resp)
end
local alg = resp.kvp:get("CHAP_A")[1]
if ( alg ~= "5" ) then return false, "Unsupported authentication algorithm" end
local chall = resp.kvp:get("CHAP_C")[1]
if ( not(chall) ) then return false, "Failed to decode challenge" end
chall = bin.pack("H", chall:sub(3))
local ident = resp.kvp:get("CHAP_I")[1]
if (not(ident)) then return false, "Failed to decoded identifier" end
ident = string.char(tonumber(ident))
local resp = CHAP.calcResponse( ident, chall, password )
resp = "0x" .. select(2, bin.unpack("H16", resp))
p = Packet.LoginRequest:new()
p:setImmediate(true)
p:setTransit(true)
p:setNSG(Packet.LoginRequest.NSG.LoginOperationalNegotiation)
p.kvp:add("CHAP_N", username)
p.kvp:add("CHAP_R", resp)
status, resp = self.comm:exchange( p, Packet.LoginResponse )
if ( not(status) ) then
return false, ("ERROR: iscsi.Helper.login: %s"):format(resp)
end
if ( resp:getErrorCode() ~= Packet.LoginResponse.Errors.SUCCESS ) then
return false, "Login failed"
end
return true, resp
end,
--- Disconnects the socket from the server
close = function(self) self.socket:close() end
}
return _ENV;