--- -- 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: -- -- -- 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() -- -- -- 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 TokenType table and their respective parsers can be found in the Token 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 DataTypes table. In order to add additional data types a parser function has to be added to both the ColumnInfo and ColumnData 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 " -- -- @args mssql.timeout How long to wait for SQL responses. This is a number -- followed by ms for milliseconds, s for seconds, -- m for minutes, or h for hours. Default: -- 30s. module(... or "mssql", package.seeall) -- Version 0.2 -- Created 01/17/2010 - v0.1 - created by Patrik Karlsson -- 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(" 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("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: -- servername - the server name -- name - the name of the instance -- clustered - is the server clustered? -- version - the db version, WILL MOST LIKELY BE INCORRECT -- port - the TCP port of the server -- pipe - the location of the listening named pipe -- ip - 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 stdnse.output_result -- -- @param tbl as recieved by Helper.Query -- @param with_headers boolean true if output should contain column headers -- @return table suitable for stdnse.output_result 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 -- Helper.Discover -- @return status true on successm false on failure -- @return version table containing the following fields -- product, version, -- level 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 }