????
Current Path : /usr/share/nmap/nselib/ |
Current File : //usr/share/nmap/nselib/mssql.lua |
--- -- MSSQL Library supporting a very limited subset of operations. -- -- The library was designed and tested against Microsoft SQL Server 2005. -- However, it should work with versions 7.0, 2000, 2005 and 2008. -- Only a minimal amount of parsers have been added for tokens, column types -- and column data in order to support the first scripts. -- -- The code has been implemented based on traffic analysis and the following -- documentation: -- * SSRP Protocol Specification: http://msdn.microsoft.com/en-us/library/cc219703.aspx -- * TDS Protocol Documentation: http://www.freetds.org/tds.html. -- * The JTDS source code: http://jtds.sourceforge.net/index.html. -- -- * ColumnInfo: Class containing parsers for column types which are present before the row data in all query response packets. The column information contains information relevant to the data type used to hold the data eg. precision, character sets, size etc. -- * ColumnData: Class containing parsers for the actual column information. -- * Token: Class containing parsers for tokens returned in all TDS responses. A server response may hold one or more tokens with information from the server. Each token has a type which has a number of type specific fields. -- * QueryPacket: Class used to hold a query and convert it to a string suitable for transmission over a socket. -- * LoginPacket: Class used to hold login specific data which can easily be converted to a string suitable for transmission over a socket. -- * TDSStream: Class that handles communication over the Tabular Data Stream protocol used by SQL serve. It is used to transmit the the Query- and Login-packets to the server. -- * Helper: Class which facilitates the use of the library by through action oriented functions with descriptive names. -- * Util: A "static" class containing mostly character and type conversion functions. -- -- The following sample code illustrates how scripts can use the Helper class -- to interface the library: -- -- <code> -- local helper = mssql.Helper:new() -- status, result = helper:Login( username, password, "temdpb", host.ip ) -- status, result = helper:Query( "SELECT name FROM master..syslogins") -- helper:Disconnect() -- <code> -- -- Known limitations: -- * The library does not support SSL. The foremost reason being the akward choice of implementation where the SSL handshake is performed within the TDS data block. By default, servers support connections over non SSL connections though. -- * Version 7 and ONLY version 7 of the protocol is supported. This should cover Microsoft SQL Server 7.0 and later. -- * TDS Responses contain one or more response tokens which are parsed based on their type. The supported tokens are listed in the <code>TokenType</code> table and their respective parsers can be found in the <code>Token</code> class. Note that some token parsers are not fully implemented and simply move the offset the right number of bytes to continue processing of the response. -- * The library only supports a limited subsets of datatypes and will abort execution and return an error if it detects an unsupported type. The supported data types are listed in the <code>DataTypes</code> table. In order to add additional data types a parser function has to be added to both the <code>ColumnInfo</code> and <code>ColumnData</code> class. -- * No functionality for languages, localization or characted codepages has been considered or implemented. -- * The library does database authentication only. No OS authentication or use of the integrated security model is supported. -- * Queries using SELECT, INSERT, DELETE and EXEC of procedures have been tested while developing scripts. -- -- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html -- -- @author "Patrik Karlsson <patrik@cqure.net>" -- -- @args mssql.timeout How long to wait for SQL responses. This is a number -- followed by <code>ms</code> for milliseconds, <code>s</code> for seconds, -- <code>m</code> for minutes, or <code>h</code> for hours. Default: -- <code>30s</code>. module(... or "mssql", package.seeall) -- Version 0.2 -- Created 01/17/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net> -- Revised 03/28/2010 - v0.2 - fixed incorrect token types. added 30 seconds timeout -- Revised 01/23/2011 - v0.3 - fixed parsing error in discovery code with patch -- from Chris Woodbury require("bit") require("bin") require("stdnse") do local arg = nmap.registry.args and nmap.registry.args["mssql.timeout"] or "30s" local timeout, err timeout, err = stdnse.parse_timespec(arg) if not timeout then error(err) end MSSQL_TIMEOUT = timeout end -- TDS packet types PacketType = { Query = 0x01, Response = 0x04, Login = 0x10, } -- TDS response token types TokenType = { TDS7Results = 0x81, ErrorMessage = 0xAA, InformationMessage = 0xAB, LoginAcknowledgement = 0xAD, Row = 0xD1, OrderBy = 0xA9, EnvironmentChange = 0xE3, Done = 0xFD, DoneInProc = 0xFF, } -- SQL Server/Sybase data types DataTypes = { SYBINTN = 0x26, SYBINT2 = 0x34, SYBINT4 = 0x38, SYBDATETIME = 0x3D, SYBDATETIMN = 0x6F, XSYBVARBINARY = 0xA5, XSYBVARCHAR = 0xA7, XSYBNVARCHAR = 0xE7, } -- "static" ColumInfo parser class ColumnInfo = { Parse = { [DataTypes.XSYBNVARCHAR] = function( data, pos ) local colinfo = {} local tmp pos, colinfo.lts, colinfo.codepage, colinfo.flags, colinfo.charset, colinfo.msglen = bin.unpack("<SSSCC", data, pos ) pos, tmp = bin.unpack("A" .. (colinfo.msglen * 2), data, pos) colinfo.text = Util.FromWideChar(tmp) return pos, colinfo end, [DataTypes.SYBINT2] = function( data, pos ) return ColumnInfo.Parse[DataTypes.SYBDATETIME](data, pos) end, [DataTypes.SYBINTN] = function( data, pos ) local colinfo = {} local tmp pos, colinfo.unknown, colinfo.msglen = bin.unpack("<CC", data, pos) pos, tmp = bin.unpack("A" .. (colinfo.msglen * 2), data, pos ) colinfo.text = Util.FromWideChar(tmp) return pos, colinfo end, [DataTypes.SYBINT4] = function( data, pos ) return ColumnInfo.Parse[DataTypes.SYBDATETIME](data, pos) end, [DataTypes.XSYBVARBINARY] = function( data, pos ) local colinfo = {} local tmp pos, colinfo.lts, colinfo.msglen = bin.unpack("<SC", data, pos) pos, tmp = bin.unpack("A" .. (colinfo.msglen * 2), data, pos ) colinfo.text = Util.FromWideChar(tmp) return pos, colinfo end, [DataTypes.SYBDATETIME] = function( data, pos ) local colinfo = {} local tmp pos, colinfo.msglen = bin.unpack("C", data, pos) pos, tmp = bin.unpack("A" .. (colinfo.msglen * 2), data, pos ) colinfo.text = Util.FromWideChar(tmp) return pos, colinfo end, [DataTypes.SYBDATETIMN] = function( data, pos ) return ColumnInfo.Parse[DataTypes.SYBINTN](data, pos) end, [DataTypes.XSYBVARCHAR] = function( data, pos ) return ColumnInfo.Parse[DataTypes.XSYBNVARCHAR](data, pos) end, } } -- "static" ColumData parser class ColumnData = { Parse = { [DataTypes.XSYBNVARCHAR] = function( data, pos ) local size, coldata pos, size = bin.unpack( "<S", data, pos ) pos, coldata = bin.unpack( "A"..size, data, pos ) return pos, Util.FromWideChar(coldata) end, [DataTypes.XSYBVARCHAR] = function( data, pos ) local size, coldata pos, size = bin.unpack( "<S", data, pos ) pos, coldata = bin.unpack( "A"..size, data, pos ) return pos, coldata end, [DataTypes.XSYBVARBINARY] = function( data, pos ) local coldata, size pos, size = bin.unpack( "<S", data, pos ) pos, coldata = bin.unpack( "A"..size, data, pos ) return pos, "0x" .. select(2, bin.unpack("H"..coldata:len(), coldata ) ) end, [DataTypes.SYBINT4] = function( data, pos ) local num pos, num = bin.unpack("<I", data, pos) return pos, num end, [DataTypes.SYBINT2] = function( data, pos ) local num pos, num = bin.unpack("<S", data, pos) return pos, num end, [DataTypes.SYBINTN] = function( data, pos ) local len, num pos, len = bin.unpack("C", data, pos) if ( len == 1 ) then return bin.unpack("C", data, pos) elseif ( len == 2 ) then return bin.unpack("<S", data, pos) elseif ( len == 4 ) then return bin.unpack("<I", data, pos) elseif ( len == 8 ) then return bin.unpack("<L", data, pos) else return -1, ("Unhandled length (%d) for SYBINTN"):format(len) end return -1, "Error" end, [DataTypes.SYBDATETIME] = function( data, pos ) local hi, lo, dt, result pos, hi, lo = bin.unpack("<II", data, pos) -- CET 01/01/1900 dt = -2208996000 result = os.date("%x %X", dt + (hi*24*60*60) + (lo/300) ) return pos, result end, [DataTypes.SYBDATETIMN] = function( data, pos ) return ColumnData.Parse[DataTypes.SYBINTN]( data, pos ) end, } } -- "static" Token parser class Token = { Parse = { --- Parse error message tokens -- -- @param data string containing "raw" data -- @param pos number containing offset into data -- @return pos number containing new offset after parse -- @return token table containing token specific fields [TokenType.ErrorMessage] = function( data, pos ) local token = {} local tmp token.type = TokenType.ErrorMessage pos, token.size, token.errno, token.state, token.severity, token.errlen = bin.unpack( "<SICCS", data, pos ) pos, tmp = bin.unpack("A" .. (token.errlen * 2), data, pos ) token.error = Util.FromWideChar(tmp) pos, token.srvlen = bin.unpack("C", data, pos) pos, tmp = bin.unpack("A" .. (token.srvlen * 2), data, pos ) token.server = Util.FromWideChar(tmp) pos, token.proclen = bin.unpack("C", data, pos) pos, tmp = bin.unpack("A" .. (token.proclen * 2), data, pos ) token.proc = Util.FromWideChar(tmp) pos, token.lineno = bin.unpack("<S", data, pos) return pos, token end, --- Parse environment change tokens -- (This function is not implemented and simply moves the pos offset) -- -- @param data string containing "raw" data -- @param pos number containing offset into data -- @return pos number containing new offset after parse -- @return token table containing token specific fields [TokenType.EnvironmentChange] = function( data, pos ) local token = {} local tmp token.type = TokenType.EnvironmentChange pos, token.size = bin.unpack("<S", data, pos) return pos + token.size, token end, --- Parse information message tokens -- -- @param data string containing "raw" data -- @param pos number containing offset into data -- @return pos number containing new offset after parse -- @return token table containing token specific fields [TokenType.InformationMessage] = function( data, pos ) local pos, token = Token.Parse[TokenType.ErrorMessage]( data, pos ) token.type = TokenType.InformationMessage return pos, token end, --- Parse login acknowledgment tokens -- -- @param data string containing "raw" data -- @param pos number containing offset into data -- @return pos number containing new offset after parse -- @return token table containing token specific fields [TokenType.LoginAcknowledgement] = function( data, pos ) local token = {} local _ -- don't do much, just increase the pos offset to next token token.type = TokenType.LoginAcknowledgement pos, token.size, _, _, _, _, token.textlen = bin.unpack( "<SCCCSC", data, pos ) pos, token.text = bin.unpack("A" .. token.textlen * 2, data, pos) pos, token.version = bin.unpack("<I", data, pos ) return pos, token end, --- Parse done tokens -- -- @param data string containing "raw" data -- @param pos number containing offset into data -- @return pos number containing new offset after parse -- @return token table containing token specific fields [TokenType.Done] = function( data, pos ) local token = {} local _ -- don't do much, just increase the pos offset to next token token.type = TokenType.Done pos, token.flags, token.operation, token.rowcount = bin.unpack( "<SSI", data, pos ) return pos, token end, --- Parses a DoneInProc token recieved after executing a SP -- -- @param data string containing "raw" data -- @param pos number containing offset into data -- @return pos number containing new offset after parse -- @return token table containing token specific fields [TokenType.DoneInProc] = function( data, pos ) local token pos, token = Token.Parse[TokenType.Done]( data, pos ) token.type = TokenType.DoneInProc return pos, token end, --- Parses a OrderBy token -- -- @param data string containing "raw" data -- @param pos number containing offset into data -- @return pos number containing new offset after parse -- @return token table containing token specific fields [TokenType.OrderBy] = function( data, pos ) local token = {} pos, token.size = bin.unpack("<S", data, pos) token.type = TokenType.OrderBy return pos + token.size, token end, --- Parse TDS result tokens -- -- @param data string containing "raw" data -- @param pos number containing offset into data -- @return pos number containing new offset after parse -- @return token table containing token specific fields [TokenType.TDS7Results] = function( data, pos ) local token = {} local _ token.type = TokenType.TDS7Results pos, token.count = bin.unpack( "<S", data, pos ) token.colinfo = {} for i=1, token.count do local colinfo = {} local usertype, flags, ttype pos, usertype, flags, ttype = bin.unpack("<SSC", data, pos ) if ( not(ColumnInfo.Parse[ttype]) ) then return -1, ("Unhandled data type: 0x%X"):format(ttype) end pos, colinfo = ColumnInfo.Parse[ttype]( data, pos ) colinfo.usertype = usertype colinfo.flags = flags colinfo.type = ttype table.insert( token.colinfo, colinfo ) end return pos, token end, }, --- Parses the first token at positions pos -- -- @param data string containing "raw" data -- @param pos number containing offset into data -- @return pos number containing new offset after parse or -1 on error -- @return token table containing token specific fields or error message on error ParseToken = function( data, pos ) local ttype pos, ttype = bin.unpack("C", data, pos) if ( not(Token.Parse[ttype]) ) then return -1, ("No parser for token type: 0x%X"):format( ttype ) end return Token.Parse[ttype](data, pos) end, } --- QueryPacket class QueryPacket = { new = function(self,o) o = o or {} setmetatable(o, self) self.__index = self return o end, SetQuery = function( self, query ) self.query = query end, --- Returns the query packet as string -- -- @return string containing the authentication packet ToString = function( self ) return PacketType.Query, Util.ToWideChar( self.query ) end, } --- LoginPacket class LoginPacket = { -- options_1 possible values -- 0x80 enable warning messages if SET LANGUAGE issued -- 0x40 change to initial database must succeed -- 0x20 enable warning messages if USE <database> issued -- 0x10 enable BCP -- options_2 possible values -- 0x80 enable domain login security -- 0x40 "USER_SERVER - reserved" -- 0x20 user type is "DQ login" -- 0x10 user type is "replication login" -- 0x08 "fCacheConnect" -- 0x04 "fTranBoundary" -- 0x02 client is an ODBC driver -- 0x01 change to initial language must succeed length = 0, version = 0x71000001, -- Version 7.1 size = 0, cli_version = 7, -- From jTDS JDBC driver cli_pid = 0, -- Dummy value conn_id = 0, options_1 = 0xa0, options_2 = 0x03, sqltype_flag = 0, reserved_flag= 0, time_zone = 0, collation = 0, -- Strings client = "Nmap", username = nil, password = nil, app = "Nmap NSE", server = nil, library = "mssql.lua", locale = "", database = "master", --nil, MAC = string.char(0x00,0x00,0x00,0x00,0x00,0x00), -- should contain client MAC, jTDS uses all zeroes new = function(self,o) o = o or {} setmetatable(o, self) self.__index = self return o end, --- Sets the username used for authentication -- -- @param username string containing the username to user for authentication SetUsername = function(self, username) self.username = username end, --- Sets the password used for authentication -- -- @param password string containing the password to user for authentication SetPassword = function(self, password) self.password = password end, --- Sets the database used in authentication -- -- @param database string containing the database name SetDatabase = function(self, database) self.database = database end, --- Sets the server's name used in authentication -- -- @param server string containing the name or ip of the server SetServer = function(self, server) self.server = server end, --- Returns the authentication packet as string -- -- @return string containing the authentication packet ToString = function(self) local data local offset = 86 self.cli_pid = math.random(100000) self.length = offset + 2 * ( self.client:len() + self.username:len() + self.password:len() + self.app:len() + self.server:len() + self.library:len() + self.database:len() ) data = bin.pack("<IIIIII", self.length, self.version, self.size, self.cli_version, self.cli_pid, self.conn_id ) data = data .. bin.pack("CCCC", self.options_1, self.options_2, self.sqltype_flag, self.reserved_flag ) data = data .. bin.pack("<II", self.time_zone, self.collation ) -- offsets begin data = data .. bin.pack("<SS", offset, self.client:len() ) offset = offset + self.client:len() * 2 data = data .. bin.pack("<SS", offset, self.username:len() ) offset = offset + self.username:len() * 2 data = data .. bin.pack("<SS", offset, self.password:len() ) offset = offset + self.password:len() * 2 data = data .. bin.pack("<SS", offset, self.app:len() ) offset = offset + self.app:len() * 2 data = data .. bin.pack("<SS", offset, self.server:len() ) offset = offset + self.server:len() * 2 -- unknown1 offset data = data .. bin.pack("<SS", 0, 0 ) data = data .. bin.pack("<SS", offset, self.library:len() ) offset = offset + self.library:len() * 2 data = data .. bin.pack("<SS", offset, self.locale:len() ) offset = offset + self.locale:len() * 2 data = data .. bin.pack("<SS", offset, self.database:len() ) offset = offset + self.database:len() * 2 -- client MAC address, hardcoded to 00:00:00:00:00:00 data = data .. bin.pack("A", self.MAC) -- offset to auth info data = data .. bin.pack("<S", offset) -- lenght of nt auth (should be 0 for sql auth) data = data .. bin.pack("<S", 0) -- next position (same as total packet length) data = data .. bin.pack("<S", self.length) -- zero pad data = data .. bin.pack("<S", 0) -- Auth info wide strings data = data .. bin.pack("A", Util.ToWideChar(self.client) ) data = data .. bin.pack("A", Util.ToWideChar(self.username) ) data = data .. bin.pack("A", self.TDS7CryptPass(self.password) ) data = data .. bin.pack("A", Util.ToWideChar(self.app) ) data = data .. bin.pack("A", Util.ToWideChar(self.server) ) data = data .. bin.pack("A", Util.ToWideChar(self.library) ) data = data .. bin.pack("A", Util.ToWideChar(self.locale) ) data = data .. bin.pack("A", Util.ToWideChar(self.database) ) return PacketType.Login, data end, --- Encrypts a password using the TDS7 *ultra secure* XOR encryption -- -- @param password string containing the password to encrypt -- @return string containing the encrypted password TDS7CryptPass = function(password) local xormask = 0x5a5a local result = "" for i=1, password:len() do local c = bit.bxor( string.byte( password:sub( i, i ) ), xormask ) local m1= bit.band( bit.rshift( c, 4 ), 0x0F0F ) local m2= bit.band( bit.lshift( c, 4 ), 0xF0F0 ) result = result .. bin.pack("S", bit.bor( m1, m2 ) ) end return result end, } -- Handles communication with SQL Server TDSStream = { packetno = 0, new = function(self,o) o = o or {} setmetatable(o, self) self.__index = self return o end, --- Establishes a connection to the SQL server -- -- @param host table containing host information -- @param port table containing port information -- @return status true on success, false on failure -- @return result containing error message on failure Connect = function( self, host, port ) local status, result, lport, _ self.socket = nmap.new_socket() -- Set the timeout to something realistic for connects self.socket:set_timeout( 5000 ) status, result = self.socket:connect(host, port) if ( not(status) ) then return false, "Connect failed" end -- Sometimes a Query can take a long time to respond, so we set -- the timeout to 30 seconds. This shouldn't be a problem as the -- library attempt to decode the protocol and avoid reading past -- the end of the input buffer. So the only time the timeout is -- triggered is when waiting for a response to a query. self.socket:set_timeout( MSSQL_TIMEOUT * 1000 ) status, _, lport, _, _ = self.socket:get_info() if ( status ) then math.randomseed(os.time() * lport ) else math.randomseed(os.time() ) end if ( not(status) ) then return false, "Socket connection failed" end return status, result end, --- Disconnects from the SQL Server -- -- @return status true on success, false on failure -- @return result containing error message on failure Disconnect = function( self ) local status, result = self.socket:close() self.socket = nil return status, result end, --- Sets the timeout for communication over the socket -- -- @param timeout number containing the new socket timeout in ms SetTimeout = function( self, timeout ) self.socket:set_timeout(timeout) end, --- Send a TDS request to the server -- -- @param pkt_type number containing the type of packet to send -- @param data string containing the raw data to send to the server -- @return status true on success, false on failure -- @return result containing error message on failure Send = function( self, pkt_type, data ) local len = data:len() + 8 local last, channel, window = 1, 0, 0 local packet self.packetno = self.packetno + 1 packet = bin.pack(">CCSSCCA", pkt_type, last, len, channel, self.packetno, window, data ) return self.socket:send( packet ) end, --- Recieves responses from SQL Server -- The function continues to read and assemble a response until the server -- responds with the last response flag set -- -- @return status true on success, false on failure -- @return result containing raw data contents or error message on failure Receive = function( self ) local status local pkt_type, last, size, channel, packet_no, window, tmp, needed local data, response = "", "" local pos = 1 repeat if( response:len() - pos < 4 ) then status, tmp = self.socket:receive_bytes(4) response = response .. tmp end if ( not(status) ) then return false, "Failed to receive packet from MSSQL server" end pos, pkt_type, last, size = bin.unpack(">CCS", response, pos ) if ( pkt_type ~= PacketType.Response ) then return false, "Server returned invalid packet" end needed = size - ( response:len() - pos + 5 ) if ( needed > 0 ) then status, tmp = self.socket:receive_bytes(needed) if ( not(status) ) then return false, "Failed to receive packet from MSSQL server" end response = response .. tmp end pos, channel, packet_no, window, tmp = bin.unpack(">SccA" .. ( size - 8 ), response, pos) data = data .. tmp until last == 1 -- return only the data section ie. without the headers return status, data end, } --- Helper class Helper = { new = function(self,o) o = o or {} setmetatable(o, self) self.__index = self return o end, --- Establishes a connection to the SQL server -- -- @param host table containing host information -- @param port table containing port information -- @return status true on success, false on failure -- @return result containing error message on failure Connect = function( self, host, port ) local status, result self.stream = TDSStream:new() status, result = self.stream:Connect(host, port) if ( not(status) ) then return false, result end return true end, --- Sends a broadcast message to the SQL Browser Agent and parses the -- results. The response is returned as an array of tables representing -- each database instance. The tables have the following fields: -- <code>servername</code> - the server name -- <code>name</code> - the name of the instance -- <code>clustered</code> - is the server clustered? -- <code>version</code> - the db version, WILL MOST LIKELY BE INCORRECT -- <code>port</code> - the TCP port of the server -- <code>pipe</code> - the location of the listening named pipe -- <code>ip</code> - the IP of the server -- -- @param host table as received by the script action function -- @param port table as received by the script action function -- @param broadcast boolean true if the discovery should be performed -- against the broadcast address or not. -- @return status boolean, true on success false on failure -- @return instances array of instance tables Discover = function( host, port, broadcast ) local socket = nmap.new_socket("udp") local instances = {} -- set a reasonable timeout socket:set_timeout(5000) local status, err if ( not(broadcast) ) then status, err = socket:connect( host, port ) if ( not(status) ) then return false, err end status, err = socket:send("\002") if ( not(status) ) then return status, err end else status, err = socket:sendto(host, port, "\002") end local data repeat status, data = socket:receive() if ( not(status) ) then break end -- strip of first 3 bytes as they contain thing we don't want data = data:sub(4) local _, ip status, _, _, ip, _ = socket:get_info() -- It would seem easier to just capture (.-;;) repeateadly, since -- each instance ends with ";;", but ";;" can also occur within the -- data, signifying an empty field (e.g. "...bv;;@COMPNAME;;tcp;1433;;..."). -- So, instead, we'll split up the string ahead of time. -- See the SSRP specification for more details. local instanceStrings = {} local firstInstanceEnd, instanceString repeat firstInstanceEnd = data:find( ";ServerName;(.-);InstanceName;(.-);IsClustered;(.-);" ) if firstInstanceEnd then instanceString = data:sub( 1, firstInstanceEnd ) data = data:sub( firstInstanceEnd + 1 ) else instanceString = data end table.insert( instanceStrings, instanceString ) until (not firstInstanceEnd) for _, instance in ipairs( instanceStrings ) do instances[ip] = instances[ip] or {} local info = {} info.servername = string.match(instance, "ServerName;(.-);") info.name = string.match(instance, "InstanceName;(.-);") info.clustered = string.match(instance, "IsClustered;(.-);") info.version = string.match(instance, "Version;(.-);") info.port = string.match(instance, ";tcp;(.-);") info.pipe = string.match(instance, ";np;(.-);") info.ip = ip if ( not(instances[ip][info.name]) ) then instances[ip][info.name] = info end end until( not(broadcast) ) socket:close() return true, instances end, --- Disconnects from the SQL Server -- -- @return status true on success, false on failure -- @return result containing error message on failure Disconnect = function( self ) if ( not(self.stream) ) then return false, "Not connected to server" end self.stream:Disconnect() self.stream = nil return true end, --- Authenticates to SQL Server -- -- @param username string containing the username for authentication -- @param password string containing the password for authentication -- @param database string containing the database to access -- @param servername string containing the name or ip of the remote server -- @return status true on success, false on failure -- @return result containing error message on failure Login = function( self, username, password, database, servername ) local loginPacket = LoginPacket:new() local status, result, data, token local servername = servername or "DUMMY" local pos = 1 if ( nil == self.stream ) then return false, "Not connected to server" end loginPacket:SetUsername(username) loginPacket:SetPassword(password) loginPacket:SetDatabase(database) loginPacket:SetServer(servername) status, result = self.stream:Send( loginPacket:ToString() ) if ( not(status) ) then return false, result end status, data = self.stream:Receive() if ( not(status) ) then return false, data end while( pos < data:len() ) do pos, token = Token.ParseToken( data, pos ) if ( -1 == pos ) then return false, token end -- Let's check for user must change password, it appears as if this is -- reported as ERROR 18488 if ( token.type == TokenType.ErrorMessage and token.errno == 18488 ) then return false, "Must change password at next logon" elseif ( token.type == TokenType.LoginAcknowledgement ) then return true, "Login Success" end end return false, "Login Failed" end, --- Performs a SQL query and parses the response -- -- @param query string containing the SQL query -- @return status true on success, false on failure -- @return table containing a table of columns for each row -- or error message on failure Query = function( self, query ) local queryPacket = QueryPacket:new() local status, result, data, token, colinfo, rows local pos = 1 if ( nil == self.stream ) then return false, "Not connected to server" end queryPacket:SetQuery( query ) status, result = self.stream:Send( queryPacket:ToString() ) if ( not(status) ) then return false, result end status, data = self.stream:Receive() if ( not(status) ) then return false, data end -- Iterate over tokens until we get to a rowtag while( pos < data:len() ) do local rowtag = select(2, bin.unpack("C", data, pos)) if ( rowtag == TokenType.Row ) then break end pos, token = Token.ParseToken( data, pos ) if ( -1 == pos ) then return false, token end if ( token.type == TokenType.ErrorMessage ) then return false, token.error elseif ( token.type == TokenType.TDS7Results ) then colinfo = token.colinfo end end rows = {} while(true) do local rowtag pos, rowtag = bin.unpack("C", data, pos ) if ( rowtag ~= TokenType.Row ) then break end if ( rowtag == TokenType.Row and colinfo and #colinfo > 0 ) then local columns = {} for i=1, #colinfo do local val if ( ColumnData.Parse[colinfo[i].type] ) then pos, val = ColumnData.Parse[colinfo[i].type](data, pos) if ( -1 == pos ) then return false, val end table.insert(columns, val) else return false, ("unknown datatype=0x%X"):format(colinfo[i].type) end end table.insert(rows, columns) end end result = {} result.rows = rows result.colinfo = colinfo return true, result end, } --- "static" Utility class containing mostly conversion functions Util = { --- Converts a string to a wide string -- -- @param str string to be converted -- @return string containing a two byte representation of str where a zero -- byte character has been tagged on to each character. ToWideChar = function( str ) return str:gsub("(.)", "%1" .. string.char(0x00) ) end, --- Concerts a wide string to string -- -- @param wstr containing the wide string to convert -- @return string with every other character removed FromWideChar = function( wstr ) local str = "" if ( nil == wstr ) then return nil end for i=1, wstr:len(), 2 do str = str .. wstr:sub(i, i) end return str end, --- Takes a table as returned by Query and does some fancy formatting -- better suitable for <code>stdnse.output_result</code> -- -- @param tbl as recieved by <code>Helper.Query</code> -- @param with_headers boolean true if output should contain column headers -- @return table suitable for <code>stdnse.output_result</code> FormatOutputTable = function ( tbl, with_headers ) local new_tbl = {} local col_names = {} if ( not(tbl) ) then return end if ( with_headers and tbl.rows and #tbl.rows > 0 ) then local headers table.foreach( tbl.colinfo, function( k, v ) table.insert( col_names, v.text) end) headers = stdnse.strjoin("\t", col_names) table.insert( new_tbl, headers) headers = headers:gsub("[^%s]", "=") table.insert( new_tbl, headers ) end for _, v in ipairs( tbl.rows ) do table.insert( new_tbl, stdnse.strjoin("\t", v) ) end return new_tbl end, --- Decodes the version based on information from the SQL browser service. -- -- @param info table with instance information as received by -- <code>Helper.Discover</code> -- @return status true on successm false on failure -- @return version table containing the following fields -- <code>product</code>, <code>version</code>, -- <code>level</code> DecodeBrowserInfoVersion = function(info) local VER_INFO = { ["^6%.0"] = "6.0", ["^6%.5"] = "6.5", ["^7%.0"] = "7.0", ["^8%.0"] = "2000", ["^9%.0"] = "2005", ["^10%.0"]= "2008", } local VER_LEVEL = { ["9.00.3042"] = "SP2", ["9.00.3043"] = "SP2", ["9.00.2047"] = "SP1", ["9.00.1399"] = "RTM", ["10.0.1075"] = "CTP", ["10.0.1600"] = "CTP", ["10.0.2531"] = "SP1" } local product = "" local version = {} for m, v in pairs(VER_INFO) do if ( info.version:match(m) ) then product=v break end end if ( info.name == "SQLEXPRESS" ) then product = product .. " Express Edition" end version.product = ("Microsoft SQL Server %s"):format(product) version.version = info.version for ver, level in pairs( VER_LEVEL ) do -- make sure we're comparing the same length local len = ( #info.version > #ver ) and #ver or #info.version if ( ver == info.version:sub(1, len) ) then version.level = level break end end if ( version.level ) then version.version = version.version .. (" (%s)"):format(version.level) end version.version = version.version .. " - UNVERIFIED" return true, version end }