--- -- 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 Specification: http://msdn.microsoft.com/en-us/library/dd304523.aspx -- * TDS Protocol Documentation: http://www.freetds.org/tds.html. -- * The JTDS source code: http://jtds.sourceforge.net/index.html. -- -- * SSRP: Class that handles communication over the SQL Server Resolution Protocol, used for identifying instances on a host. -- * 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. -- * PreLoginPacket: Class used to (partially) implement the TDS PreLogin packet -- * 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:Connect( host, port ) -- status, result = helper:Login( username, password, "temdpb", host.ip ) -- status, result = helper:Query( "SELECT name FROM master..syslogins" ) -- helper:Disconnect() -- -- -- The following sample code illustrates how scripts can use the Helper class -- with pre-discovered instances (e.g. by ms-sql-discover or broadcast-ms-sql-discover): -- -- -- local instance = mssql.Helper.GetDiscoveredInstances( host, port ) -- if ( instance ) then -- local helper = mssql.Helper:new() -- status, result = helper:ConnectEx( instance ) -- status, result = helper:LoginEx( instance ) -- status, result = helper:Query( "SELECT name FROM master..syslogins" ) -- helper:Disconnect() -- end -- -- -- 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 , Chris Woodbury" -- -- @args mssql.username The username to use to connect to SQL Server instances. -- This username is used by scripts taking actions that require -- authentication (e.g. ms-sql-query) This username (and its -- associated password) takes precedence over any credentials discovered -- by the ms-sql-brute and ms-sql-empty-password -- scripts. -- -- @args mssql.password The password for mssql.username. If this -- argument is not given but mssql.username, a blank password -- is used. -- -- @args mssql.instance-name The name of the instance to connect to. -- -- @args mssql.instance-port The port of the instance to connect to. -- -- @args mssql.instance-all Targets all SQL server instances discovered -- throught the browser service. -- -- @args mssql.domain The domain against which to perform integrated -- authentication. When set, the scripts assume integrated authentication -- should be performed, rather than the default sql login. -- -- @args mssql.protocol The protocol to use to connect to the instance. The -- protocol may be either NP,Named Pipes or -- TCP. -- -- @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. -- -- @args mssql.scanned-ports-only If set, the script will only connect -- to ports that were included in the Nmap scan. This may result in -- instances not being discovered, particularly if UDP port 1434 is not -- included. Additionally, instances that are found to be running on -- ports that were not scanned (e.g. if 1434/udp is in the scan and the -- SQL Server Browser service on that port reports an instance -- listening on 43210/tcp, which was not scanned) will be reported but -- will not be stored for use by other ms-sql-* scripts. module(... or "mssql", package.seeall) -- 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 -- Revised 02/01/2011 - v0.4 - numerous changes and additions to support new -- functionality in ms-sql- scripts and to be more -- robust in parsing and handling data. (Chris Woodbury) -- Revised 02/19/2011 - v0.5 - numerous changes in script, library behaviour -- * huge improvements in version detection -- * added support for named pipes -- * added support for integrated NTLMv1 authentication -- -- (Patrik Karlsson, Chris Woodbury) require("bit") require("bin") require("stdnse") require("strbuf") require("smb") require("smbauth") HAVE_SSL = (nmap.have_ssl() and pcall(require, "openssl")) do namedpipes = smb.namedpipes local arg = stdnse.get_script_args( "mssql.timeout" ) or "30s" local timeout, err = stdnse.parse_timespec(arg) if not timeout then error(err) end MSSQL_TIMEOUT = timeout SCANNED_PORTS_ONLY = false if ( stdnse.get_script_args( "mssql.scanned-ports-only" ) ) then SCANNED_PORTS_ONLY = true end end -- ************************************* -- Informational Classes -- ************************************* --- SqlServerInstanceInfo class SqlServerInstanceInfo = { instanceName = nil, version = nil, serverName = nil, isClustered = nil, host = nil, port = nil, pipeName = nil, new = function(self,o) o = o or {} setmetatable(o, self) self.__index = self return o end, -- Compares two SqlServerInstanceInfo objects and determines whether they -- refer to the same SQL Server instance, judging by a combination of host, -- port, named pipe information and instance name. __eq = function( self, other ) local areEqual if ( not (self.host and other.host) ) then -- if they don't both have host information, we certainly can't say -- whether they're the same areEqual = false else areEqual = (self.host.ip == other.host.ip) end if (self.port and other.port) then areEqual = areEqual and ( other.port.number == self.port.number and other.port.protocol == self.port.protocol ) elseif (self.pipeName and other.pipeName) then areEqual = areEqual and (self.pipeName == other.pipeName) elseif (self.instanceName and other.instanceName) then areEqual = areEqual and (self.instanceName == other.instanceName) else -- if we have neither port nor named pipe info nor instance names, -- we can't say whether they're the same areEqual = false end return areEqual end, --- Merges the data from one SqlServerInstanceInfo object into another. Each -- field in the first object is populated with the data from that field in -- second object if the first object's field is nil OR if overwrite -- is set to true. A special case is made for the version field, -- which is only overwritten in the second object has more reliable version -- information. The second object is not modified. Merge = function( self, other, overwrite ) local mergeFields = { "host", "port", "instanceName", "version", "isClustered", "pipeName" } for _, fieldname in ipairs( mergeFields ) do -- Add values from other only if self doesn't have a value, or if overwrite is true if ( other[ fieldname ] ~= nil and (overwrite or self[ fieldname ] == nil) ) then self[ fieldname ] = other[ fieldname ] end end if (self.version and self.version.source == "SSRP" and other.version and other.version.Source == "SSNetLib") then self.version = other.version end end, --- Returns a name for the instance, based on the available information. This -- may take one of the following forms: -- * HOST\INSTANCENAME -- * PIPENAME -- * HOST:PORT GetName = function( self ) if (self.instanceName) then return string.format( "%s\\%s", self.host.ip or self.serverName or "[nil]", self.instanceName or "[nil]" ) elseif (self.pipeName) then return string.format( "%s", self.pipeName ) else return string.format( "%s:%s", self.host.ip or self.serverName or "[nil]", (self.port and self.port.number) or "[nil]" ) end end, --- Sets whether the instance is in a cluster -- -- @param self -- @param isClustered Boolean true or the string "Yes" are interpreted as true; -- all other values are interpreted as false. SetIsClustered = function( self, isClustered ) self.isClustered = (isClustered == true) or (isClustered == "Yes") end, --- Indicates whether this instance has networking protocols enabled, such -- that scripts could attempt to connect to it. HasNetworkProtocols = function( self ) return (self.pipeName ~= nil) or (self.port and self.port.number) end, } --- SqlServerVersionInfo class SqlServerVersionInfo = { versionNumber = "", -- The full version string (e.g. "9.00.2047.00") major = nil, -- The major version (e.g. 9) minor = nil, -- The minor version (e.g. 0) build = nil, -- The build number (e.g. 2047) subBuild = nil, -- The sub-build number (e.g. 0) productName = nil, -- The prodcut name (e.g. "SQL Server 2005") brandedVersion = nil, -- The branded version of the product (e.g. "2005") servicePackLevel = nil, -- The service pack leve (e.g. "SP1") patched = nil, -- Whether patches have been applied since SP installation (true/false/nil) source = nil, -- The source of the version info (e.g. "SSRP", "SSNetLib") new = function(self,o) o = o or {} setmetatable(o, self) self.__index = self return o end, --- Sets the version using a version number string. -- -- @param versionNumber a version number string (e.g. "9.00.1399.00") -- @param source a string indicating the source of the version info (e.g. "SSRP", "SSNetLib") SetVersionNumber = function(self, versionNumber, source) local major, minor, revision, subBuild if versionNumber:match( "^%d+%.%d+%.%d+.%d+" ) then major, minor, revision, subBuild = versionNumber:match( "^(%d+)%.(%d+)%.(%d+)" ) elseif versionNumber:match( "^%d+%.%d+%.%d+" ) then major, minor, revision = versionNumber:match( "^(%d+)%.(%d+)%.(%d+)" ) else stdnse.print_debug( 1, "%s: SetVersionNumber: versionNumber is not in correct format: %s", "MSSQL", versionNumber or "nil" ) end self:SetVersion( major, minor, revision, subBuild, source ) end, --- Sets the version using the individual numeric components of the version -- number. -- -- @param source a string indicating the source of the version info (e.g. "SSRP", "SSNetLib") SetVersion = function(self, major, minor, build, subBuild, source) self.source = source -- make sure our version numbers all end up as valid numbers self.major, self.minor, self.build, self.subBuild = tonumber( major or 0 ), tonumber( minor or 0 ), tonumber( build or 0 ), tonumber( subBuild or 0 ) self.versionNumber = string.format( "%u.%02u.%u.%02u", self.major, self.minor, self.build, self.subBuild ) self:_ParseVersionInfo() end, --- Using the version number, determines the product version _InferProductVersion = function(self) local VERSION_LOOKUP_TABLE = { ["^6%.0"] = "6.0", ["^6%.5"] = "6.5", ["^7%.0"] = "7.0", ["^8%.0"] = "2000", ["^9%.0"] = "2005", ["^10%.0"] = "2008", ["^10%.50"] = "2008 R2", ["^11%.0"] = "2011", } local product = "" for m, v in pairs(VERSION_LOOKUP_TABLE) do if ( self.versionNumber:match(m) ) then product = v self.brandedVersion = product break end end self.productName = ("Microsoft SQL Server %s"):format(product) end, --- Returns a lookup table that maps revision numbers to service pack levels for -- the applicable SQL Server version (e.g. { {1600, "RTM"}, {2531, "SP1"} }). _GetSpLookupTable = function(self) -- Service pack lookup tables: -- For instances where a revised service pack was released (e.g. 2000 SP3a), we will include the -- build number for the original SP and the build number for the revision. However, leaving it -- like this would make it appear that subsequent builds were a patched version of the revision -- (e.g. a patch applied to 2000 SP3 that increased the build number to 780 would get displayed -- as "SP3a+", when it was actually SP3+). To avoid this, we will include an additional fake build -- number that combines the two. local SP_LOOKUP_TABLE_6_5 = { {201, "RTM"}, {213, "SP1"}, {240, "SP2"}, {258, "SP3"}, {281, "SP4"}, {415, "SP5"}, {416, "SP5a"}, {417, "SP5/SP5a"}, } local SP_LOOKUP_TABLE_7 = { {623, "RTM"}, {699, "SP1"}, {842, "SP2"}, {961, "SP3"}, {1063, "SP4"}, } local SP_LOOKUP_TABLE_2000 = { {194, "RTM"}, {384, "SP1"}, {532, "SP2"}, {534, "SP2"}, {760, "SP3"}, {766, "SP3a"}, {767, "SP3/SP3a"}, {2039, "SP4"}, } local SP_LOOKUP_TABLE_2005 = { {1399, "RTM"}, {2047, "SP1"}, {3042, "SP2"}, {4035, "SP3"}, } local SP_LOOKUP_TABLE_2008 = { {1600, "RTM"}, {2531, "SP1"}, {4000, "SP2"}, } local SP_LOOKUP_TABLE_2008R2 = { {1660, "RTM"}, } if ( not self.brandedVersion ) then self:_InferProductVersion() end local spLookupTable if self.brandedVersion == "6.5" then spLookupTable = SP_LOOKUP_TABLE_6_5 elseif self.brandedVersion == "7.0" then spLookupTable = SP_LOOKUP_TABLE_7 elseif self.brandedVersion == "2000" then spLookupTable = SP_LOOKUP_TABLE_2000 elseif self.brandedVersion == "2005" then spLookupTable = SP_LOOKUP_TABLE_2005 elseif self.brandedVersion == "2008" then spLookupTable = SP_LOOKUP_TABLE_2008 elseif self.brandedVersion == "2008 R2" then spLookupTable = SP_LOOKUP_TABLE_2008R2 end return spLookupTable end, --- Processes version data to determine (if possible) the product version, -- service pack level and patch status. _ParseVersionInfo = function(self) local spLookupTable = self:_GetSpLookupTable() if spLookupTable then local spLookupItr = 0 -- Loop through the service pack levels until we find one whose revision -- number is the same as or lower than our revision number. while spLookupItr < #spLookupTable do spLookupItr = spLookupItr + 1 if (spLookupTable[ spLookupItr ][1] == self.build ) then spLookupItr = spLookupItr break elseif (spLookupTable[ spLookupItr ][1] > self.build ) then -- The target revision number is lower than the first release if spLookupItr == 1 then self.servicePackLevel = "Pre-RTM" else -- we went too far - it's the previous SP, but with patches applied spLookupItr = spLookupItr - 1 end break end end -- Now that we've identified the proper service pack level: if self.servicePackLevel ~= "Pre-RTM" then self.servicePackLevel = spLookupTable[ spLookupItr ][2] if ( spLookupTable[ spLookupItr ][1] == self.build ) then self.patched = false else self.patched = true end end -- Clean up some of our inferences. If the source of our revision number -- was the SSRP (SQL Server Browser) response, we need to recognize its -- limitations: -- * Versions of SQL Server prior to 2005 are reported with the RTM build -- number, regardless of the actual version (e.g. SQL Server 2000 is -- always 8.00.194). -- * Versions of SQL Server starting with 2005 (and going through at least -- 2008) do better but are still only reported with the build number as -- of the last service pack (e.g. SQL Server 2005 SP3 with patches is -- still reported as 9.00.4035.00). if ( self.source == "SSRP" ) then self.patched = nil if ( self.major <= 8 ) then self.servicePackLevel = nil end end end return true end, --- ToString = function(self) local friendlyVersion = strbuf.new() if self.productName then friendlyVersion:concatbuf( self.productName ) if self.servicePackLevel then friendlyVersion:concatbuf( " " ) friendlyVersion:concatbuf( self.servicePackLevel ) end if self.patched then friendlyVersion:concatbuf( "+" ) end end return friendlyVersion:dump() end, --- Uses the information in this SqlServerVersionInformation object to -- populate the version information in an Nmap port table for a SQL Server -- TCP listener. -- -- @param self A SqlServerVersionInformation object -- @param port An Nmap port table corresponding to the instance PopulateNmapPortVersion = function(self, port) port.service = "ms-sql-s" port.version = port.version or {} port.version.name = "ms-sql-s" port.version.product = self.productName local versionString = strbuf.new() if self.source ~= "SSRP" then versionString:concatbuf( self.versionNumber ) if self.servicePackLevel then versionString:concatbuf( "; " ) versionString:concatbuf( self.servicePackLevel ) end if self.patched then versionString:concatbuf( "+" ) end port.version.version = versionString:dump() end return port end, } -- ************************************* -- SSRP (SQL Server Resolution Protocol) -- ************************************* SSRP = { PORT = { number = 1434, protocol = "udp" }, DEBUG_ID = "MSSQL-SSRP", MESSAGE_TYPE = { ClientBroadcast = 0x02, ClientUnicast = 0x03, ClientUnicastInstance = 0x04, ClientUnicastDAC = 0x0F, ServerResponse = 0x05, }, --- Parses an SSRP string and returns a table containing one or more -- SqlServerInstanceInfo objects created from the parsed string. _ParseSsrpString = function( host, ssrpString ) -- 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 = ssrpString:find( ";ServerName;(.-);InstanceName;(.-);IsClustered;(.-);" ) if firstInstanceEnd then instanceString = ssrpString:sub( 1, firstInstanceEnd ) ssrpString = ssrpString:sub( firstInstanceEnd + 1 ) else instanceString = ssrpString end table.insert( instanceStrings, instanceString ) until (not firstInstanceEnd) stdnse.print_debug( 2, "%s: SSRP Substrings:\n %s", SSRP.DEBUG_ID, stdnse.strjoin( "\n ", instanceStrings ) ) local instances = {} for _, instanceString in ipairs( instanceStrings ) do local instance = SqlServerInstanceInfo:new() local version = SqlServerVersionInfo:new() instance.version = version instance.host = host instance.serverName = instanceString:match( "ServerName;(.-);") instance.instanceName = instanceString:match( "InstanceName;(.-);") instance:SetIsClustered( instanceString:match( "IsClustered;(.-);") ) version:SetVersionNumber( instanceString:match( "Version;(.-);"), "SSRP" ) local tcpPort = tonumber( instanceString:match( ";tcp;(.-);") ) if tcpPort then instance.port = {number = tcpPort, protocol = "tcp"} end local pipeName = instanceString:match( ";np;(.-);") local status, pipeSubPath = namedpipes.get_pipe_subpath( pipeName ) if status then pipeName = namedpipes.make_pipe_name( host.ip, pipeSubPath ) elseif pipeName ~= nil then stdnse.print_debug( 1, "%s: Invalid pipe name:\n%s", SSRP.DEBUG_ID, pipeName ) end instance.pipeName = pipeName table.insert( instances, instance ) end return instances end, --- _ProcessResponse = function( host, responseData ) local instances local pos, messageType, dataLength = 1, nil, nil pos, messageType, dataLength = bin.unpack("CSS", optionType, offset, optionLength ) offset = offset + optionLength optionType = PreLoginPacket.OPTION_TYPE.Encryption optionLength = OPTION_LENGTH_CLIENT[ optionType ] data = data .. bin.pack( ">CSS", optionType, offset, optionLength ) offset = offset + optionLength optionType = PreLoginPacket.OPTION_TYPE.InstOpt optionLength = #self._instanceName + 1 --(string length + null-terminator) data = data .. bin.pack( ">CSS", optionType, offset, optionLength ) offset = offset + optionLength optionType = PreLoginPacket.OPTION_TYPE.ThreadId optionLength = OPTION_LENGTH_CLIENT[ optionType ] data = data .. bin.pack( ">CSS", optionType, offset, optionLength ) offset = offset + optionLength if self.requestMars then optionType = PreLoginPacket.OPTION_TYPE.MARS optionLength = OPTION_LENGTH_CLIENT[ optionType ] data = data .. bin.pack( ">CSS", optionType, offset, optionLength ) offset = offset + optionLength end data = data .. bin.pack( "C", PreLoginPacket.OPTION_TYPE.Terminator ) -- Now that the pre-login headers are done, write the data data = data .. bin.pack( ">CCSS", self.versionInfo.major, self.versionInfo.minor, self.versionInfo.build, self.versionInfo.subBuild ) data = data .. bin.pack( "C", self._requestEncryption ) data = data .. bin.pack( "z", self._instanceName ) data = data .. bin.pack( "SS", bytes, pos) if not (optionPos and optionLength) then stdnse.print_debug( 2, "%s: Could not unpack optionPos and optionLength.", "MSSQL" ) return false, "Invalid pre-login response" end optionPos = optionPos + 1 -- convert from 0-based index to 1-based index if ( (optionPos + optionLength) > (#bytes + 1) ) then stdnse.print_debug( 2, "%s: Pre-login response: pos+len for option type %s is beyond end of data.", "MSSQL", optionType ) stdnse.print_debug( 2, "%s: (optionPos: %s) (optionLength: %s)", "MSSQL", optionPos, optionLength ) return false, "Invalid pre-login response" end if ( optionLength ~= expectedOptionLength and expectedOptionLength ~= -1 ) then stdnse.print_debug( 2, "%s: Option data is incorrect size in pre-login response. ", "MSSQL" ) stdnse.print_debug( 2, "%s: (optionType: %s) (optionLength: %s)", "MSSQL", optionType, optionLength ) return false, "Invalid pre-login response" end optionData = bytes:sub( optionPos, optionPos + optionLength - 1 ) if #optionData ~= optionLength then stdnse.print_debug( 2, "%s: Could not read sufficient bytes from version data.", "MSSQL" ) return false, "Invalid pre-login response" end if ( optionType == PreLoginPacket.OPTION_TYPE.Version ) then local major, minor, build, subBuild, version major = string.byte( optionData:sub( 1, 1 ) ) minor = string.byte( optionData:sub( 2, 2 ) ) build = (string.byte( optionData:sub( 3, 3 ) ) * 256) + string.byte( optionData:sub( 4, 4 ) ) subBuild = (string.byte( optionData:sub( 5, 5 ) ) * 256) + string.byte( optionData:sub( 6, 6 ) ) version = SqlServerVersionInfo:new() version:SetVersion( major, minor, build, subBuild, "SSNetLib" ) preLoginPacket.versionInfo = version elseif ( optionType == PreLoginPacket.OPTION_TYPE.Encryption ) then preLoginPacket:SetRequestEncryption( bin.unpack( "C", optionData ) ) elseif ( optionType == PreLoginPacket.OPTION_TYPE.InstOpt ) then preLoginPacket:SetInstanceName( bin.unpack( "z", optionData ) ) elseif ( optionType == PreLoginPacket.OPTION_TYPE.ThreadId ) then -- Do nothing. According to the TDS spec, this option is empty when sent from the server elseif ( optionType == PreLoginPacket.OPTION_TYPE.MARS ) then preLoginPacket:SetRequestMars( bin.unpack( "C", optionData ) ) end end return status, preLoginPacket 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 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, SetDomain = function(self, domain) self.domain = domain end, --- Returns the authentication packet as string -- -- @return string containing the authentication packet ToString = function(self) local data local offset = 86 local ntlmAuth = not(not(self.domain)) local authLen = 0 self.cli_pid = math.random(100000) self.length = offset + 2 * ( self.client:len() + self.app:len() + self.server:len() + self.library:len() + self.database:len() ) if ( ntlmAuth ) then authLen = 32 + #self.domain self.length = self.length + authLen self.options_2 = self.options_2 + 0x80 else self.length = self.length + 2 * (self.username:len() + self.password:len()) end data = bin.pack("smb -- library (for use with named pipes). ConnectEx = function( self, instanceInfo, connectionPreference, smbOverrides ) if ( self._socket ) then return false, "Already connected via TCP" end if ( self._pipe ) then return false, "Already connected via named pipes" end connectionPreference = connectionPreference or stdnse.get_script_args('mssql.protocol') or { "TCP", "Named Pipes" } if ( connectionPreference and 'string' == type(connectionPreference) ) then connectionPreference = { connectionPreference } end local status, result, connectionType, errorMessage stdnse.print_debug( 3, "%s: Connection preferences for %s: %s", "MSSQL", instanceInfo:GetName(), stdnse.strjoin( ", ", connectionPreference ) ) for _, connectionType in ipairs( connectionPreference ) do if connectionType == "TCP" then if not ( instanceInfo.port ) then stdnse.print_debug( 3, "%s: Cannot connect to %s via TCP because port table is not set.", "MSSQL", instanceInfo:GetName() ) result = "No TCP port for this instance" else status, result = self:Connect( instanceInfo.host, instanceInfo.port ) if status then return true end end elseif connectionType == "Named Pipes" or connectionType == "NP" then if not ( instanceInfo.pipeName ) then stdnse.print_debug( 3, "%s: Cannot connect to %s via named pipes because pipe name is not set.", "MSSQL", instanceInfo:GetName() ) result = "No named pipe for this instance" else status, result = self:ConnectToNamedPipe( instanceInfo.host, instanceInfo.pipeName, smbOverrides ) if status then return true end end else stdnse.print_debug( 1, "%s: Unknown connection preference: %s", "MSSQL", connectionType ) return false, ("ERROR: Unknown connection preference: %s"):format(connectionType) end -- Handle any error messages if not status then if errorMessage then errorMessage = string.format( "%s, %s: %s", errorMessage, connectionType, result or "nil" ) else errorMessage = string.format( "%s: %s", connectionType, result or "nil" ) end end end if not errorMessage then errorMessage = string.format( "%s: None of the preferred connection types are available for %s\\%s", "MSSQL", instanceInfo:GetName() ) end return false, errorMessage end, --- Establishes a connection to the SQL server -- -- @param host A host table for the target host -- @param pipePath The path to the named pipe of the target SQL Server -- (e.g. "\MSSQL$SQLEXPRESS\sql\query"). If nil, "\sql\query\" is used. -- @param smbOverrides (Optional) An overrides table for calls to the smb -- library (for use with named pipes). -- @return status: true on success, false on failure -- @return error_message: an error message, or nil ConnectToNamedPipe = function( self, host, pipePath, overrides ) if ( self._socket ) then return false, "Already connected via TCP" end if ( SCANNED_PORTS_ONLY and smb.get_port( host ) == nil ) then stdnse.print_debug( 2, "%s: Connection disallowed: scanned-ports-only is set and no SMB port is available", "MSSQL" ) return false, "Connection disallowed: scanned-ports-only" end pipePath = pipePath or "\\sql\\query" self._pipe = namedpipes.named_pipe:new() local status, result = self._pipe:connect( host, pipePath, overrides ) if ( status ) then self._name = self._pipe.pipe else self._pipe = nil end return status, result 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 ) if ( self._pipe ) then return false, "Already connected via named pipes" end if ( SCANNED_PORTS_ONLY and nmap.get_port_state( host, port ) == nil ) then stdnse.print_debug( 2, "%s: Connection disallowed: scanned-ports-only is set and port %d was not scanned", "MSSQL", port.number ) return false, "Connection disallowed: scanned-ports-only" end 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 ( status ) then -- 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() end if ( not(status) ) then self._socket = nil stdnse.print_debug( 2, "%s: Socket connection failed on %s:%s", "MSSQL", host.ip, port.number ) return false, "Socket connection failed" end self._name = string.format( "%s:%s", host.ip, port.number ) 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 ) if ( self._socket ) then local status, result = self._socket:close() self._socket = nil return status, result elseif ( self._pipe ) then local status, result = self._pipe:disconnect() self._pipe = nil return status, result else return false, "Not connected" end end, --- Sets the timeout for communication over the socket -- -- @param timeout number containing the new socket timeout in ms SetTimeout = function( self, timeout ) if ( self._socket ) then self._socket:set_timeout(timeout) else return false, "Not connected" end end, --- Gets the name of the name pipe, or nil GetNamedPipeName = function( self ) if ( self._pipe ) then return self._pipe.name else return nil end end, --- Send a TDS request to the server -- -- @param packetType A PacketType, indicating the type of TDS -- packet being sent. -- @param packetData A 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, packetType, packetData ) local packetLength = packetData:len() + 8 -- +8 for TDS header local messageStatus, spid, window = 1, 0, 0 if ( packetType ~= PacketType.NTAuthentication ) then self._packetId = self._packetId + 1 end local assembledPacket = bin.pack(">CCSSCCA", packetType, messageStatus, packetLength, spid, self._packetId, window, packetData ) if ( self._socket ) then return self._socket:send( assembledPacket ) elseif ( self._pipe ) then return self._pipe:send( assembledPacket ) else return false, "Not connected" end 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 -- @return errorDetail nil, or additional information about an error. In -- the case of named pipes, this will be an SMB error name (e.g. NT_STATUS_PIPE_DISCONNECTED) Receive = function( self ) local status, result, errorDetail local combinedData, readBuffer = "", "" -- the buffer is solely for the benefit of TCP connections local tdsPacketAvailable = true if not ( self._socket or self._pipe ) then return false, "Not connected" end -- Large messages (e.g. result sets) can be split across multiple TDS -- packets from the server (which could themselves each be split across -- multiple TCP packets or SMB messages). while ( tdsPacketAvailable ) do local packetType, messageStatus, packetLength, spid, window local pos = 1 if ( self._socket ) then -- If there is existing data in the readBuffer, see if there's -- enough to read the TDS headers for the next packet. If not, -- do another read so we have something to work with. if ( readBuffer:len() < 8 ) then status, result = self._socket:receive_bytes(8 - readBuffer:len()) readBuffer = readBuffer .. result end elseif ( self._pipe ) then -- The named pipe takes care of all of its reassembly. We don't -- have to mess with buffers and repeatedly reading until we get -- the whole packet. We'll still write to readBuffer, though, so -- that the common logic can be reused. status, result, errorDetail = self._pipe:receive() readBuffer = result end if not ( status and readBuffer ) then return false, result, errorDetail end -- TDS packet validity check: packet at least as long as the TDS header if ( readBuffer:len() < 8 ) then stdnse.print_debug( 2, "%s: Receiving (%s): packet is invalid length", "MSSQL", self._name ) return false, "Server returned invalid packet" end -- read in the TDS headers pos, packetType, messageStatus, packetLength = bin.unpack(">CCS", readBuffer, pos ) pos, spid, self._packetId, window = bin.unpack(">SCC", readBuffer, pos ) -- TDS packet validity check: packet type is Response (0x4) if ( packetType ~= mssql.PacketType.Response ) then stdnse.print_debug( 2, "%s: Receiving (%s): Expected type 0x4 (response), but received type 0x%x", "MSSQL", self._name, packetType ) return false, "Server returned invalid packet" end if ( self._socket ) then -- If we didn't previously read in enough data to complete this -- TDS packet, let's do so. while ( packetLength - readBuffer:len() > 0 ) do status, result = self._socket:receive() if not ( status and result ) then return false, result end readBuffer = readBuffer .. result end end -- We've read in an apparently valid TDS packet local thisPacketData = readBuffer:sub( pos, packetLength ) -- Append its data to that of any previous TDS packets combinedData = combinedData .. thisPacketData if ( self._socket ) then -- If we read in data beyond the end of this TDS packet, save it -- so that we can use it in the next loop. readBuffer = readBuffer:sub( packetLength + 1 ) end -- TDS packet validity check: packet length matches length from header if ( packetLength ~= (thisPacketData:len() + 8) ) then stdnse.print_debug( 2, "%s: Receiving (%s): Header reports length %d, actual length is %d", "MSSQL", self._name, packetLength, thisPacketData:len() ) return false, "Server returned invalid packet" end -- Check the status flags in the TDS packet to see if the message is -- continued in another TDS packet. tdsPacketAvailable = (bit.band( messageStatus, TDSStream.MESSAGE_STATUS_FLAGS.EndOfMessage) ~= TDSStream.MESSAGE_STATUS_FLAGS.EndOfMessage) end -- return only the data section ie. without the headers return status, combinedData 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 ConnectEx = function( self, instanceInfo ) local status, result self.stream = TDSStream:new() status, result = self.stream:ConnectEx( instanceInfo ) if ( not(status) ) then return false, result end return true 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, --- Returns true if discovery has been performed to detect -- SQL Server instances on the given host WasDiscoveryPerformed = function( host ) local mutex = nmap.mutex( "discovery_performed for " .. host.ip ) mutex( "lock" ) nmap.registry.mssql = nmap.registry.mssql or {} nmap.registry.mssql.discovery_performed = nmap.registry.mssql.discovery_performed or {} local wasPerformed = nmap.registry.mssql.discovery_performed[ host.ip ] or false mutex( "done" ) return wasPerformed end, --- Adds an instance to the list of instances kept in the Nmap registry for -- shared use by SQL Server scripts. If the registry already contains the -- instance, any new information is merged into the existing instance info. -- This may happen, for example, when an instance is discovered via named -- pipes, but the same instance has already been discovered via SSRP; this -- will prevent duplicates, where possible. AddOrMergeInstance = function( newInstance ) local instanceExists nmap.registry.mssql = nmap.registry.mssql or {} nmap.registry.mssql.instances = nmap.registry.mssql.instances or {} nmap.registry.mssql.instances[ newInstance.host.ip ] = nmap.registry.mssql.instances[ newInstance.host.ip ] or {} for _, existingInstance in ipairs( nmap.registry.mssql.instances[ newInstance.host.ip ] ) do if existingInstance == newInstance then existingInstance:Merge( newInstance ) instanceExists = true break end end if not instanceExists then table.insert( nmap.registry.mssql.instances[ newInstance.host.ip ], newInstance ) end end, --- Gets a table containing SqlServerInstanceInfo objects discovered on -- the specified host (and port, if specified). -- -- @param host A host table for the target host -- @param port (Optional) If omitted, all of the instances for the host -- will be returned. -- @return A table containing SqlServerInstanceInfo objects, or nil GetDiscoveredInstances = function( host, port ) nmap.registry.mssql = nmap.registry.mssql or {} nmap.registry.mssql.instances = nmap.registry.mssql.instances or {} nmap.registry.mssql.instances[ host.ip ] = nmap.registry.mssql.instances[ host.ip ] or {} if ( not port ) then local instances = nmap.registry.mssql.instances[ host.ip ] if ( instances and #instances == 0 ) then instances = nil end return instances else for _, instance in ipairs( nmap.registry.mssql.instances[ host.ip ] ) do if ( instance.port and instance.port.number == port.number and instance.port.protocol == port.protocol ) then return { instance } end end return nil end end, --- Attempts to discover SQL Server instances using SSRP to query one or -- more (if broadcast is used) SQL Server Browser services. -- Any discovered instances are returned, as well as being stored for use -- by other scripts (see mssql.Helper.GetDiscoveredInstances()). -- -- @param host A host table for the target. -- @param port (Optional) A port table for the target port. If this is nil, -- the default SSRP port (UDP 1434) is used. -- @param broadcast If true, this will be done with an SSRP broadcast, and -- host should contain the broadcast specification (e.g. -- ip = "255.255.255.255"). -- @return (status, result) If status is true, result is a table of -- tables containing SqlServerInstanceInfo objects. The top-level table -- is indexed by IP address. If status is false, result is an -- error message. DiscoverBySsrp = function( host, port, broadcast ) if broadcast then local status, result = SSRP.DiscoverInstances_Broadcast( host, port ) if not status then return status, result else for ipAddress, host in pairs( result ) do for _, instance in ipairs( host ) do Helper.AddOrMergeInstance( instance ) -- Give some version info back to Nmap if ( instance.port and instance.version ) then instance.version:PopulateNmapPortVersion( instance.port ) nmap.set_port_version( instance.host, instance.port, "hardmatched" ) end end end return true, result end else local status, result = SSRP.DiscoverInstances( host, port ) if not status then return status, result else for _, instance in ipairs( result ) do Helper.AddOrMergeInstance( instance ) -- Give some version info back to Nmap if ( instance.port and instance.version ) then instance.version:PopulateNmapPortVersion( instance.port ) nmap.set_port_version( host, instance.port, "hardmatched" ) end end local instances_all = {} instances_all[ host.ip ] = result return true, instances_all end end end, --- Attempts to discover a SQL Server instance listening on the specified -- port. If an instance is discovered, it is returned, as well as being -- stored for use by other scripts (see mssql.Helper.GetDiscoveredInstances()). -- -- @param host A host table for the target. -- @param port A port table for the target port. -- @return (status, result) If status is true, result is a table of -- SqlServerInstanceInfo objects. If status is false, result is an -- error message or nil. DiscoverByTcp = function( host, port ) local version, instance, status -- Check to see if we've already discovered an instance on this port instance = mssql.Helper.GetDiscoveredInstances( host, port ) if ( not instance ) then instance = mssql.SqlServerInstanceInfo:new() instance.host = host instance.port = port status, version = mssql.Helper.GetInstanceVersion( instance ) if ( status ) then mssql.Helper.AddOrMergeInstance( instance ) -- The point of this wasn't to get the version, just to use the -- pre-login packet to determine whether there was a SQL Server on -- the port. However, since we have the version now, we'll store it. instance.version = version -- Give some version info back to Nmap if ( instance.port and instance.version ) then instance.version:PopulateNmapPortVersion( instance.port ) nmap.set_port_version( host, instance.port, "hardmatched" ) end end end return (instance ~= nil), { instance } end, --- Attempts to discover SQL Server instances listening on default named -- pipes. Any discovered instances are returned, as well as being stored -- for use by other scripts (see mssql.Helper.GetDiscoveredInstances()). -- -- @param host A host table for the target. -- @param port A port table for the port to connect on for SMB -- @return (status, result) If status is true, result is a table of -- SqlServerInstanceInfo objects. If status is false, result is an -- error message or nil. DiscoverBySmb = function( host, port ) local defaultPipes = { "\\sql\\query", "\\MSSQL$SQLEXPRESS\\sql\\query", "\\MSSQL$SQLSERVER\\sql\\query", } local tdsStream = TDSStream:new() local status, result, instances_host for _, pipeSubPath in ipairs( defaultPipes ) do status, result = tdsStream:ConnectToNamedPipe( host, pipeSubPath, nil ) if status then instances_host = {} local instance = SqlServerInstanceInfo:new() instance.pipeName = tdsStream:GetNamedPipeName() tdsStream:Disconnect() instance.host = host Helper.AddOrMergeInstance( instance ) table.insert( instances_host, instance ) else stdnse.print_debug( 3, "DiscoverBySmb \n pipe: %s\n result: %s", pipeSubPath, tostring( result ) ) end end return (instances_host ~= nil), instances_host end, --- Attempts to discover SQL Server instances by a variety of means. -- This function calls the three DiscoverBy functions, which perform the -- actual discovery. Any discovered instances can be retrieved using -- mssql.Helper.GetDiscoveredInstances(). -- -- @param host Host table as received by the script action function Discover = function( host ) nmap.registry.mssql = nmap.registry.mssql or {} nmap.registry.mssql.discovery_performed = nmap.registry.mssql.discovery_performed or {} nmap.registry.mssql.discovery_performed[ host.ip ] = false local mutex = nmap.mutex( "discovery_performed for " .. host.ip ) mutex( "lock" ) local sqlDefaultPort = nmap.get_port_state( host, {number = 1433, protocol = "tcp"} ) or {number = 1433, protocol = "tcp"} local sqlBrowserPort = nmap.get_port_state( host, {number = 1434, protocol = "udp"} ) or {number = 1434, protocol = "udp"} local smbPort -- smb.get_port() will return nil if no SMB port was scanned OR if SMB ports were scanned but none was open local smbPortNumber = smb.get_port( host ) if ( smbPortNumber ) then smbPort = nmap.get_port_state( host, {number = smbPortNumber, protocol = "tcp"} ) -- There's no use in manually setting an SMB port; if no SMB port was -- scanned and found open, the SMB library won't work end -- if the user has specified ports, we'll check those too local targetInstancePorts = stdnse.get_script_args( "mssql.instance-port" ) if ( sqlBrowserPort and sqlBrowserPort.state ~= "closed" ) then Helper.DiscoverBySsrp( host, sqlBrowserPort ) end if ( sqlDefaultPort and sqlDefaultPort.state ~= "closed" ) then Helper.DiscoverByTcp( host, sqlDefaultPort ) end if ( smbPort ) then Helper.DiscoverBySmb( host, smbPort ) end if ( targetInstancePorts ) then if ( type( targetInstancePorts ) == "string" ) then targetInstancePorts = { targetInstancePorts } end for _, portNumber in ipairs( targetInstancePorts ) do portNumber = tonumber( portNumber ) Helper.DiscoverByTcp( host, {number = portNumber, protocol = "tcp"} ) end end nmap.registry.mssql.discovery_performed[ host.ip ] = true mutex( "done" ) end, --- Returns all of the credentials available for the target instance, -- including any set by the mssql.username and mssql.password -- script arguments. -- -- @param instanceInfo A SqlServerInstanceInfo object for the target instance -- @return A table of usernames mapped to passwords (i.e. creds[ username ] = password) GetLoginCredentials_All = function( instanceInfo ) local credentials = instanceInfo.credentials or {} local credsExist = false for _, _ in pairs( credentials ) do credsExist = true break end if ( not credsExist ) then credentials = nil end if ( stdnse.get_script_args( "mssql.username" ) ) then credentials = credentials or {} local usernameArg = stdnse.get_script_args( "mssql.username" ) local passwordArg = stdnse.get_script_args( "mssql.password" ) or "" credentials[ usernameArg ] = passwordArg end return credentials end, --- Returns a username-password set according to the following rules of -- precedence: -- * If the mssql.username and mssql.password -- script arguments were set, their values are used. (If the username -- argument was specified without the password argument, a blank -- password is used.) -- * If the password for the "sa" account has been discovered (e.g. by the -- ms-sql-empty-password or ms-sql-brute -- scripts), these credentials are used. -- * If other credentials have been discovered, the first of these in the -- table are used. -- * Otherwise, nil is returned. -- -- @param instanceInfo A SqlServerInstanceInfo object for the target instance -- @return (username, password) GetLoginCredentials = function( instanceInfo ) -- First preference goes to any user-specified credentials local username = stdnse.get_script_args( "mssql.username" ) local password = stdnse.get_script_args( "mssql.password" ) or "" -- Otherwise, use any valid credentials that have been discovered (e.g. by ms-sql-brute) if ( not(username) and instanceInfo.credentials ) then -- Second preference goes to the "sa" account if ( instanceInfo.credentials.sa ) then username = "sa" password = instanceInfo.credentials.sa else -- ok were stuck with some n00b account, just get the first one for user, pass in pairs( instanceInfo.credentials ) do username = user password = pass break end end end return username, password 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. If login fails, one of the following error -- messages will be returned: -- * "Password is expired" -- * "Must change password at next logon" -- * "Account is locked out" -- * "Login Failed" -- -- @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 -- @return errorDetail nil or a LoginErrorType value, if available Login = function( self, username, password, database, servername ) local loginPacket = LoginPacket:new() local status, result, data, errorDetail, token local servername = servername or "DUMMY" local pos = 1 local ntlmAuth = false if ( not self.stream ) then return false, "Not connected to server" end loginPacket:SetUsername(username) loginPacket:SetPassword(password) loginPacket:SetDatabase(database) loginPacket:SetServer(servername) local domain = stdnse.get_script_args("mssql.domain") if (domain) then if ( not(HAVE_SSL) ) then return false, "mssql: OpenSSL not present" end ntlmAuth = true -- if the domain was specified without an argument, set a default domain of "." if (domain == 1 or domain == true ) then domain = "." end loginPacket:SetDomain(domain) end status, result = self.stream:Send( loginPacket:ToString() ) if ( not(status) ) then return false, result end status, data, errorDetail = self.stream:Receive() if ( not(status) ) then -- When logging in via named pipes, SQL Server will sometimes -- disconnect the pipe if the login attempt failed (this only seems -- to happen with non-"sa") accounts. At this point, having -- successfully connected and sent a message, we can be reasonably -- comfortable that a disconnected pipe indicates a failed login. if ( errorDetail == "NT_STATUS_PIPE_DISCONNECTED" ) then return false, "Bad username or password", LoginErrorType.InvalidUsernameOrPassword end return false, data end if ( ntlmAuth ) then local pos, nonce = Token.ParseToken( data, pos ) local authpacket = NTAuthenticationPacket:new( username, password, domain, nonce ) status, result = self.stream:Send( authpacket:ToString() ) status, data = self.stream:Receive() if ( not(status) ) then return false, data end end while( pos < data:len() ) do pos, token = Token.ParseToken( data, pos ) if ( -1 == pos ) then return false, token end if ( token.type == TokenType.ErrorMessage ) then local errorMessageLookup = { [LoginErrorType.AccountLockedOut] = "Account is locked out", [LoginErrorType.NotAssociatedWithTrustedConnection] = "User is not associated with a trusted connection (instance may allow Windows authentication only)", [LoginErrorType.InvalidUsernameOrPassword] = "Bad username or password", [LoginErrorType.PasswordExpired] = "Password is expired", [LoginErrorType.PasswordMustChange] = "Must change password at next logon", } local errorMessage = errorMessageLookup[ token.errno ] or string.format( "Login Failed (%s)", tostring(token.errno) ) return false, errorMessage, token.errno elseif ( token.type == TokenType.LoginAcknowledgement ) then return true, "Login Success" end end return false, "Failed to process login response" end, --- Authenticates to SQL Server, using the credentials returned by -- Helper.GetLoginCredentials(). If the login is rejected by the server, -- the error code will be returned, as a number in the form of a -- mssql.LoginErrorType (for which error messages can be -- looked up in mssql.LoginErrorMessage). -- -- @param instanceInfo a SqlServerInstanceInfo object for the instance to log into -- @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 code or error message LoginEx = function( self, instanceInfo, database, servername ) local servername = servername or instanceInfo.host.ip local username, password = Helper.GetLoginCredentials( instanceInfo ) if ( not username ) then return false, "No login credentials" end return self:Login( username, password, database, servername ) 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, --- Attempts to connect to a SQL Server instance listening on a TCP port in -- order to determine the version of the SSNetLib DLL, which is an -- authoritative version number for the SQL Server instance itself. -- -- @param instanceInfo An instance of SqlServerInstanceInfo -- @return status true on success, false on failure -- @return versionInfo an instance of mssql.SqlServerVersionInfo, or nil GetInstanceVersion = function( instanceInfo ) if ( not instanceInfo.host or not (instanceInfo:HasNetworkProtocols()) ) then return false, nil end local status, response, version local tdsStream = TDSStream:new() status, response = tdsStream:ConnectEx( instanceInfo ) if ( not status ) then stdnse.print_debug( 2, "%s: Connection to %s failed: %s", "MSSQL", instanceInfo:GetName(), response or "" ) return false, "Connect failed" end local preLoginRequest = PreLoginPacket:new() preLoginRequest:SetInstanceName( instanceInfo.instanceName ) tdsStream:SetTimeout( 5000 ) tdsStream:Send( preLoginRequest:ToBytes() ) -- read in any response we might get status, response = tdsStream:Receive() tdsStream:Disconnect() if status then local preLoginResponse status, preLoginResponse = PreLoginPacket.FromBytes( response ) if status then version = preLoginResponse.versionInfo else stdnse.print_debug( 2, "%s: Parsing of pre-login packet from %s failed: %s", "MSSQL", instanceInfo:GetName(), preLoginResponse or "" ) return false, "Parsing failed" end else stdnse.print_debug( 2, "%s: Receive for %s failed: %s", "MSSQL", instanceInfo:GetName(), response or "" ) return false, "Receive failed" end return status, version end, --- Gets a table containing SqlServerInstanceInfo objects for the instances -- that should be run against, based on the script-args (e.g. mssql.instance) -- -- @param host Host table as received by the script action function -- @param port (Optional) Port table as received by the script action function -- @return status True on success, false on failure -- @return instances If status is true, this will be a table with one or -- more SqlServerInstanceInfo objects. If status is false, this will be -- an error message. GetTargetInstances = function( host, port ) if ( port ) then local status = true local instance = Helper.GetDiscoveredInstances( host, port ) if ( not instance ) then status, instance = Helper.DiscoverByTcp( host, port ) end if ( instance ) then return true, instance else return false, "No SQL Server instance detected on this port" end else local targetInstanceNames = stdnse.get_script_args( "mssql.instance-name" ) local targetInstancePorts = stdnse.get_script_args( "mssql.instance-port" ) local targetAllInstances = stdnse.get_script_args( "mssql.instance-all" ) if ( targetInstanceNames and targetInstancePorts ) then return false, "Connections can be made either by instance name or port." end if ( targetAllInstances and ( targetInstanceNames or targetInstancePorts ) ) then return false, "All instances cannot be specified together with an instance name or port." end if ( not (targetInstanceNames or targetInstancePorts or targetAllInstances) ) then return false, "No instance(s) specified." end if ( not Helper.WasDiscoveryPerformed( host ) ) then stdnse.print_debug( 2, "%s: Discovery has not been performed prior to GetTargetInstances() call. Performing discovery now.", "MSSQL" ) Helper.Discover( host ) end local instanceList = Helper.GetDiscoveredInstances( host ) if ( not instanceList ) then return false, "No instances found on target host" end local targetInstances = {} if ( targetAllInstances ) then targetInstances = instanceList else -- We want an easy way to look up whether an instance's name was -- in our target list. So, we'll make a table of { instanceName = true, ... } local temp = {} if ( targetInstanceNames ) then if ( type( targetInstanceNames ) == "string" ) then targetInstanceNames = { targetInstanceNames } end for _, instanceName in ipairs( targetInstanceNames ) do temp[ string.upper( instanceName ) ] = true end end targetInstanceNames = temp -- Do the same for the target ports temp = {} if ( targetInstancePorts ) then if ( type( targetInstancePorts ) == "string" ) then targetInstancePorts = { targetInstancePorts } end for _, portNumber in ipairs( targetInstancePorts ) do portNumber = tonumber( portNumber ) temp[portNumber] = true end end targetInstancePorts = temp for _, instance in ipairs( instanceList ) do if ( instance.instanceName and targetInstanceNames[ string.upper( instance.instanceName ) ] ) then table.insert( targetInstances, instance ) elseif ( instance.port and targetInstancePorts[ tonumber( instance.port.number ) ] ) then table.insert( targetInstances, instance ) end end end if ( #targetInstances > 0 ) then return true, targetInstances else return false, "Specified instance(s) not found on target host" end end end, --- Returns a hostrule for standard SQL Server scripts, which will return -- true if one or more instances have been targeted with the mssql.instance -- script argument. However, if a previous script has failed to find any -- SQL Server instances on the host, the hostrule function will return -- false to keep further scripts from running unnecessarily on that host. -- -- @return A hostrule function (use as hostrule = mssql.GetHostrule_Standard()) GetHostrule_Standard = function() return function( host ) if ( stdnse.get_script_args( {"mssql.instance-all", "mssql.instance-name", "mssql.instance-port"} ) ~= nil ) then if ( Helper.WasDiscoveryPerformed( host ) ) then return Helper.GetDiscoveredInstances( host ) ~= nil else return true end else return false end end end, --- Returns a portrule for standard SQL Server scripts, which will run return -- true if BOTH of the following conditions are met: -- * The port has been identified as "ms-sql-s" -- * The mssql.instance script argument has NOT been used -- -- @return A portrule function (use as portrule = mssql.GetPortrule_Standard()) GetPortrule_Standard = function() return function( host, port ) return ( shortport.service( "ms-sql-s" )(host, port) and stdnse.get_script_args( {"mssql.instance-all", "mssql.instance-name", "mssql.instance-port"} ) == nil) end end, } Auth = { --- 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, LmResponse = function( password, nonce ) if ( not(HAVE_SSL) ) then stdnse.print_debug("ERROR: Nmap is missing OpenSSL") return end if(#password < 14) then password = password .. string.rep(string.char(0), 14 - #password) end password = password:upper() -- Take the first and second half of the password (note that if it's longer than 14 characters, it's truncated) local str1 = string.sub(password, 1, 7) local str2 = string.sub(password, 8, 14) -- Generate the keys local key1 = openssl.DES_string_to_key(str1) local key2 = openssl.DES_string_to_key(str2) local result = openssl.encrypt("DES", key1, nil, nonce) .. openssl.encrypt("DES", key2, nil, nonce) if(#result < 21) then result = result .. string.rep(string.char(0), 21 - #result) end str1 = string.sub(result, 1, 7) str2 = string.sub(result, 8, 14) local str3 = string.sub(result, 15, 21) key1 = openssl.DES_string_to_key(str1) key2 = openssl.DES_string_to_key(str2) local key3 = openssl.DES_string_to_key(str3) result = openssl.encrypt("DES", key1, nil, nonce) .. openssl.encrypt("DES", key2, nil, nonce) .. openssl.encrypt("DES", key3, nil, nonce) return result end, NtlmResponse = function( password, nonce ) local lm_response, ntlm_response, mac_key = smbauth.get_password_response(nil, nil, nil, password, nil, "v1", nonce, false ) return ntlm_response 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, }