From 46cdf28fce6d13a78a04e8b7945c1ab84eedb120 Mon Sep 17 00:00:00 2001 From: patrik Date: Fri, 10 Dec 2010 23:20:59 +0000 Subject: [PATCH] o [NSE] Added a new iSCSI library and the two scripts iscsi-info and iscsi-brute. [Patrik] --- CHANGELOG | 3 + nselib/iscsi.lua | 790 ++++++++++++++++++++++++++++++++++++++++ scripts/iscsi-brute.nse | 84 +++++ scripts/iscsi-info.nse | 93 +++++ scripts/script.db | 2 + 5 files changed, 972 insertions(+) create mode 100644 nselib/iscsi.lua create mode 100644 scripts/iscsi-brute.nse create mode 100644 scripts/iscsi-info.nse diff --git a/CHANGELOG b/CHANGELOG index 2befd9204..408f0b425 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a new iSCSI library and the two scripts iscsi-info and + iscsi-brute. [Patrik] + o [NSE] Add new script broadcast-ms-sql-discover and removed broadcast support from ms-sql-info. [Patrik] diff --git a/nselib/iscsi.lua b/nselib/iscsi.lua new file mode 100644 index 000000000..0f7265cd9 --- /dev/null +++ b/nselib/iscsi.lua @@ -0,0 +1,790 @@ +--- An iSCSI library implementing written by Patrik Karlsson +-- 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 Packet +-- E.g. LoginRequest and LoginResponse +-- +-- Each request can be "serialized" to a string using: +-- tostring(request). +-- All responses can be read and instantiated from the socket by calling: +-- local status,resp = Response.fromSocket(sock) +-- +-- In addition the library has the following classes: +-- * Packet +-- ** A class containing the request and response packets +-- * Comm +-- ** A class used to send and receive packet between the library and server +-- ** The class handles some of the packet "counting" and value updating +-- * Socket +-- ** A buffered socket class that allows reading of exakt number of bytes +-- * KVP +-- ** A key/value pair class that holds key value pairs +-- * Helper +-- ** A class that wraps the Comm and Packet classes +-- ** The purpose of the class is to provide easy access to common iSCSI task +-- +-- +-- @author "Patrik Karlsson " +-- @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 +-- Revised 2010/11/28 - v0.2 - improved error handling, fixed discovery issues +-- with multiple addresses + +module(... or "iscsi",package.seeall) + +require("bin") +require("ipOps") + +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:recv(48) + + 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:recv( resp.data_seg_len + pad ) + 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:recv(48) + 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:recv( resp.data_seg_len ) + + 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= + -- Followed by zero or more address keys of the form: + -- TargetAddress=[:], + -- + 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.print_debug("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:recv(48) + 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, + + +} + +--- A buffered socket implementation +Socket = +{ + + --- Creates a new instance of Socket + -- + -- @return instance of Socket + new = function(self) + local o = {} + setmetatable(o, self) + self.__index = self + o.Socket = nmap.new_socket() + o.Buffer = nil + return o + end, + + + --- Establishes a connection. + -- + -- @param hostid Hostname or IP address. + -- @param port Port number. + -- @param protocol "tcp", "udp", or + -- @return Status (true or false). + -- @return Error code (if status is false). + connect = function( self, hostid, port, protocol ) + self.Socket:set_timeout(10000) + return self.Socket:connect( hostid, port, protocol ) + end, + + --- Closes an open connection. + -- + -- @return Status (true or false). + -- @return Error code (if status is false). + close = function( self ) + return self.Socket:close() + end, + + --- Opposed to the socket:receive_bytes function, that returns + -- at least x bytes, this function returns the amount of bytes requested. + -- + -- @param count of bytes to read + -- @return true on success, false on failure + -- @return data containing bytes read from the socket + -- err containing error message if status is false + recv = function( self, count ) + local status, data + + self.Buffer = self.Buffer or "" + + if ( #self.Buffer < count ) then + status, data = self.Socket:receive_bytes( count - #self.Buffer ) + if ( not(status) ) then + return false, data + end + self.Buffer = self.Buffer .. data + end + + data = self.Buffer:sub( 1, count ) + self.Buffer = self.Buffer:sub( count + 1) + + return true, data + end, + + --- Sends data over the socket + -- + -- @return Status (true or false). + -- @return Error code (if status is false). + send = function( self, data ) + return self.Socket:send( data ) + 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 = Socket:new() + 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 ) + 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 name + -- and addr. + -- 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.print_debug(3, "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 + +} + + + + diff --git a/scripts/iscsi-brute.nse b/scripts/iscsi-brute.nse new file mode 100644 index 000000000..15514359b --- /dev/null +++ b/scripts/iscsi-brute.nse @@ -0,0 +1,84 @@ +description = [[ +Performs password guessing against iSCSI targets +]] + +--- +-- @output +-- PORT STATE SERVICE +-- 3260/tcp open iscsi syn-ack +-- | iscsi-brute: +-- | Accounts +-- | user:password123456 => Login correct +-- | Statistics +-- |_ Perfomed 5000 guesses in 7 seconds, average tps: 714 + +-- Version 0.1 +-- Created 2010/11/18 - v0.1 - created by Patrik Karlsson +-- Revised 2010/11/27 - v0.2 - detect if no password is needed + +require 'shortport' +require 'brute' +require 'iscsi' + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} + +portrule = shortport.portnumber(3260, "tcp", {"open", "open|filtered"}) + +Driver = { + + new = function(self, host, port) + local o = {} + setmetatable(o, self) + self.__index = self + o.host = host + o.port = port + o.target = stdnse.get_script_args('iscsi-brute.target') + return o + end, + + connect = function( self ) + self.helper = iscsi.Helper:new( self.host, self.port ) + return self.helper:connect() + end, + + login = function( self, username, password ) + local status = self.helper:login( self.target, username, password, "CHAP") + + if ( status ) then + return true, brute.Account:new(username, password, "OPEN") + end + + return false, brute.Error:new( "Incorrect password" ) + end, + + disconnect = function( self ) + self.helper:close() + end, +} + + +action = function( host, port ) + + local target = stdnse.get_script_args('iscsi-brute.target') + if ( not(target) ) then + return "ERROR: No target specified (see iscsi-brute.target)" + end + + local helper = iscsi.Helper:new( host, port ) + local status, err = helper:connect() + if ( not(status) ) then return false, "Failed to connect" end + + local response + status, response = helper:login( target ) + helper:logout() + helper:close() + + if ( status ) then return "No authentication required" end + + local accounts + status, accounts = brute.Engine:new(Driver, host, port):start() + + if ( status ) then return accounts end +end \ No newline at end of file diff --git a/scripts/iscsi-info.nse b/scripts/iscsi-info.nse new file mode 100644 index 000000000..9df76e742 --- /dev/null +++ b/scripts/iscsi-info.nse @@ -0,0 +1,93 @@ +description = [[ +Retrieves information from the remote iSCSI target. +]] + +--- +-- @output +-- PORT STATE SERVICE +-- 3260/tcp open iscsi +-- | iscsi-info: +-- | iqn.2006-01.com.openfiler:tsn.c8c08cad469d +-- | Target address: 192.168.56.5:3260,1 +-- | Authentication: NOT required +-- | iqn.2006-01.com.openfiler:tsn.6aea7e052952 +-- | Target address: 192.168.56.5:3260,1 +-- |_ Authentication: required +-- + +-- Version 0.2 +-- Created 2010/11/18 - v0.1 - created by Patrik Karlsson +-- Revised 2010/11/28 - v0.2 - improved error handling + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery"} + +require("shortport") +require("iscsi") + +portrule = shortport.portnumber(3260, "tcp", {"open", "open|filtered"}) + +-- Attempts to determine whether authentication is required or not +-- +-- @return status true on success false on failure +-- @return result true if auth is required false if not +-- err string containing error message +local function requiresAuth( host, port, target ) + local helper = iscsi.Helper:new( host, port ) + local errors = iscsi.Packet.LoginResponse.Errors + + local status, err = helper:connect() + if ( not(status) ) then return false, "Failed to connect" end + + local response + status, response = helper:login( target ) + if ( not(status) ) then return false, response:getErrorMessage() end + + if ( status and response:getErrorCode() == errors.SUCCESS) then + -- try to logout + status = helper:logout() + end + + status = helper:close() + + return true, "Authentication successful" +end + +action = function( host, port ) + + local helper = iscsi.Helper:new( host, port ) + + local status = helper:connect() + if ( not(status) ) then + stdnse.print_debug("%s: failed to connect to server", SCRIPT_NAME ) + return + end + + local records + status, records = helper:discoverTargets() + if ( not(status) ) then + stdnse.print_debug("%s: failed to discover targets", SCRIPT_NAME ) + return + end + status = helper:logout() + status = helper:close() + + local result = {} + for _, record in ipairs(records) do + local result_part = {} + result_part.name = ("Target: %s"):format(record.name) + for _, addr in ipairs( record.addr ) do + table.insert(result_part, ("Address: %s"):format(addr) ) + end + + local status, err = requiresAuth( host, port, record.name ) + if ( not(status) ) then + table.insert(result_part, "Authentication: " .. err ) + else + table.insert(result_part, "Authentication: No authentication required") + end + table.insert(result, result_part) + end + return stdnse.format_output( true, result ) +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index d64313247..306ca10fc 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -72,6 +72,8 @@ Entry { filename = "informix-tables.nse", categories = { "auth", "intrusive", } Entry { filename = "ipidseq.nse", categories = { "discovery", "safe", } } Entry { filename = "irc-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "irc-unrealircd-backdoor.nse", categories = { "safe", "vuln", } } +Entry { filename = "iscsi-brute.nse", categories = { "auth", "intrusive", } } +Entry { filename = "iscsi-info.nse", categories = { "discovery", } } Entry { filename = "jdwp-version.nse", categories = { "version", } } Entry { filename = "ldap-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "ldap-rootdse.nse", categories = { "discovery", "safe", } }