--- -- MSSQL Library supporting a very limited subset of operations -- -- Summary -- ------- -- 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: -- o TDS Protocol Documentation -- http://www.freetds.org/tds.html -- -- o The JTDS source code -- http://jtds.sourceforge.net/index.html -- -- Overview -- -------- -- o ColumInfo - 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. -- -- o ColumnData - Class containing parsers for the actual column information -- -- o 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. -- -- o QueryPacket - Class used to hold a query and convert it to a string -- suitable for transmission over a socket. -- -- o LoginPacket - Class used to hold login specific data which can easily -- be converted to a string suitable for transmission over -- a socket. -- -- o 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. -- -- o Helper - Class which facilitates the use of the library by through action -- oriented functions with descriptive names. -- -- o Util - "static" class containing mostly character and type conversion -- functions. -- -- Example -- ------- -- 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 -- ----------------- -- o 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. -- -- o Version 7 and ONLY version 7 of the protocol is supported. This should -- cover Microsoft SQL Server 7.0 and later. -- -- o 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. -- -- o 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. -- -- o No functionality for languages, localization or characted codepages has -- been considered or implemented. -- -- o The library does database authentication only. No OS authentication or -- use of the integrated security model is supported. -- -- o 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 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, --- 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 true, "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, }