From 58edddaedbe7aae49c5ee09acbfca3b9a1819567 Mon Sep 17 00:00:00 2001 From: patrik Date: Sat, 26 Feb 2011 22:41:10 +0000 Subject: [PATCH] o [NSE] Merged the ms-sql branch with several improvements and changes to the ms-sql scripts and library: - Improved version detection - Improved server discovery - Add support for named pipes - Add support for integrated authentication - Add support for connecting to instances by name or port - Improved script and library stability - Improved script and library documentation [Patrik Karlsson, Chris Woodbury] --- CHANGELOG | 11 + nselib/mssql.lua | 2088 ++++++++++++++++++++++--- nselib/smb.lua | 184 ++- scripts/broadcast-ms-sql-discover.nse | 122 +- scripts/ms-sql-brute.nse | 336 +++- scripts/ms-sql-config.nse | 176 ++- scripts/ms-sql-discover.nse | 131 ++ scripts/ms-sql-empty-password.nse | 195 ++- scripts/ms-sql-hasdbaccess.nse | 219 +-- scripts/ms-sql-info.nse | 359 +++-- scripts/ms-sql-query.nse | 163 +- scripts/ms-sql-tables.nse | 287 ++-- scripts/ms-sql-xp-cmdshell.nse | 242 +-- 13 files changed, 3534 insertions(+), 979 deletions(-) create mode 100755 scripts/ms-sql-discover.nse diff --git a/CHANGELOG b/CHANGELOG index 446085e6d..a6cd743ff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,16 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Merged the ms-sql branch with several improvements and changes to the + ms-sql scripts and library: + - Improved version detection + - Improved server discovery + - Add support for named pipes + - Add support for integrated authentication + - Add support for connecting to instances by name or port + - Improved script and library stability + - Improved script and library documentation + [Patrik Karlsson, Chris Woodbury] + o [NSE] Added probe for Apple iPhoto (DPAP) and the dpap-brute script that performs password guessing against a shared iPhoto library. [Patrik] diff --git a/nselib/mssql.lua b/nselib/mssql.lua index 7ac1fd22b..fd02f8ffe 100644 --- a/nselib/mssql.lua +++ b/nselib/mssql.lua @@ -1,3 +1,6 @@ +-- -*- mode: lua -*- +-- vim: set filetype=lua : + --- -- MSSQL Library supporting a very limited subset of operations. -- @@ -9,14 +12,17 @@ -- 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. @@ -26,10 +32,25 @@ -- -- -- 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") +-- 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. @@ -42,47 +63,615 @@ -- -- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html -- --- @author "Patrik Karlsson " +-- @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. +-- 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) --- 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 +-- 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 - local arg = nmap.registry.args and nmap.registry.args["mssql.timeout"] or "30s" - local timeout, err - - timeout, err = stdnse.parse_timespec(arg) + 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 = { @@ -541,17 +1392,30 @@ LoginPacket = 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.username:len() + self.password:len() + - self.app:len() + self.server:len() + self.library:len() + self.database:len() ) + 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 -- @@ -648,32 +1670,43 @@ TDSStream = { -- @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() + self._socket = nmap.new_socket() -- Set the timeout to something realistic for connects - self.socket:set_timeout( 5000 ) - status, result = self.socket:connect(host, port) - if ( not(status) ) then return false, "Connect failed" end - - -- Sometimes a Query can take a long time to respond, so we set - -- the timeout to 30 seconds. This shouldn't be a problem as the - -- library attempt to decode the protocol and avoid reading past - -- the end of the input buffer. So the only time the timeout is - -- triggered is when waiting for a response to a query. - self.socket:set_timeout( MSSQL_TIMEOUT * 1000 ) - - status, _, lport, _, _ = self.socket:get_info() + self._socket:set_timeout( 5000 ) + status, result = self._socket:connect(host, port) + if ( status ) then - math.randomseed(os.time() * lport ) - else - math.randomseed(os.time() ) + -- Sometimes a Query can take a long time to respond, so we set + -- the timeout to 30 seconds. This shouldn't be a problem as the + -- library attempt to decode the protocol and avoid reading past + -- the end of the input buffer. So the only time the timeout is + -- triggered is when waiting for a response to a query. + self._socket:set_timeout( MSSQL_TIMEOUT * 1000 ) + + status, _, lport, _, _ = self._socket:get_info() + if ( status ) then + math.randomseed(os.time() * lport ) + else + math.randomseed(os.time() ) + end 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, @@ -683,32 +1716,61 @@ TDSStream = { -- @return status true on success, false on failure -- @return result containing error message on failure Disconnect = function( self ) - local status, result = self.socket:close() - self.socket = nil - return status, result + 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 ) - self.socket:set_timeout(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 pkt_type number containing the type of packet to send - -- @param data string containing the raw data to send 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, pkt_type, data ) - local len = data:len() + 8 - local last, channel, window = 1, 0, 0 - local packet + 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 ) - self.packetno = self.packetno + 1 - packet = bin.pack(">CCSSCCA", pkt_type, last, len, channel, self.packetno, window, data ) - return self.socket:send( packet ) + 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 @@ -717,42 +1779,95 @@ TDSStream = { -- -- @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 - local pkt_type, last, size, channel, packet_no, window, tmp, needed - local data, response = "", "" - local pos = 1 + local status, result, errorDetail + local combinedData, readBuffer = "", "" -- the buffer is solely for the benefit of TCP connections + local tdsPacketAvailable = true - repeat - if( response:len() - pos < 4 ) then - status, tmp = self.socket:receive_bytes(4) - response = response .. tmp + 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) ) then - return false, "Failed to receive packet from MSSQL server" + + 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 - - pos, pkt_type, last, size = bin.unpack(">CCS", response, pos ) - if ( pkt_type ~= PacketType.Response ) then + + -- 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 - - 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 + 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, data + return status, combinedData end, } @@ -767,6 +1882,23 @@ Helper = 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 @@ -784,94 +1916,319 @@ Helper = 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 = {} + --- 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 {} - -- set a reasonable timeout - socket:set_timeout(5000) + local wasPerformed = nmap.registry.mssql.discovery_performed[ host.ip ] or false + mutex( "done" ) - local status, err + 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 - 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 + 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 - status, err = socket:sendto(host, port, "\002") + 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 - - local data - - repeat - status, data = socket:receive() - if ( not(status) ) then break end + end, - -- strip of first 3 bytes as they contain thing we don't want - data = data:sub(4) + --- 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 ) - local _, ip - status, _, _, ip, _ = socket:get_info() + 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 ) - - -- 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 {} + 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, - 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 + --- 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 - until( not(broadcast) ) - socket:close() + end - return true, instances + 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 @@ -889,7 +2246,12 @@ Helper = return true end, - --- Authenticates to SQL Server + --- 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 @@ -897,13 +2259,15 @@ Helper = -- @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, token + local status, result, data, errorDetail, token local servername = servername or "DUMMY" local pos = 1 + local ntlmAuth = false - if ( nil == self.stream ) then + if ( not self.stream ) then return false, "Not connected to server" end @@ -912,31 +2276,89 @@ Helper = 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 = self.stream:Receive() + 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 - -- 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" + + 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, "Login Failed" + 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 @@ -1022,7 +2444,260 @@ Helper = 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 @@ -1082,55 +2757,4 @@ Util = 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 - } diff --git a/nselib/smb.lua b/nselib/smb.lua index f3421c86c..fd965759a 100644 --- a/nselib/smb.lua +++ b/nselib/smb.lua @@ -131,6 +131,8 @@ command_codes = {} command_names = {} status_codes = {} status_names = {} +filetype_codes = {} +filetype_names = {} local TIMEOUT = 10000 @@ -1350,9 +1352,9 @@ local function start_session_extended(smb, log_errors, overrides) -- Check if they were logged in as a guest if(log_errors == nil or log_errors == true) then if(smb['is_guest'] == 1) then - stdnse.print_debug(1, string.format("SMB: Extended login as %s\\%s failed, but was given guest access (username may be wrong, or system may only allow guest)", domain, stdnse.string_or_blank(username))) + stdnse.print_debug(1, string.format("SMB: Extended login to %s as %s\\%s failed, but was given guest access (username may be wrong, or system may only allow guest)", smb['ip'], domain, stdnse.string_or_blank(username))) else - stdnse.print_debug(2, string.format("SMB: Extended login as %s\\%s succeeded", domain, stdnse.string_or_blank(username))) + stdnse.print_debug(2, string.format("SMB: Extended login to %s as %s\\%s succeeded", smb['ip'], domain, stdnse.string_or_blank(username))) end end @@ -1378,7 +1380,7 @@ local function start_session_extended(smb, log_errors, overrides) else -- Display a message to the user, and try the next account if(log_errors == nil or log_errors == true) then - stdnse.print_debug(1, "SMB: Extended login as %s\\%s failed (%s)", domain, stdnse.string_or_blank(username), status_name) + stdnse.print_debug(1, "SMB: Extended login to %s as %s\\%s failed (%s)", smb['ip'], domain, stdnse.string_or_blank(username), status_name) end -- Go to the next account @@ -1763,7 +1765,9 @@ function read_file(smb, offset, count, overrides) if(header1 == nil or mid == nil) then return false, "SMB: ERROR: Server returned less data than it was supposed to (one or more fields are missing); aborting [25]" end - if(status ~= 0) then + if(status ~= 0 and + (status ~= status_codes.NT_STATUS_BUFFER_OVERFLOW and (smb['filetype'] == filetype_codes.FILE_TYPE_BYTE_MODE_PIPE or + smb['filetype'] == filetype_codes.FILE_TYPE_MESSAGE_MODE_PIPE) ) ) then return false, get_status_name(status) end @@ -1775,6 +1779,7 @@ function read_file(smb, offset, count, overrides) response['remaining'] = remaining response['data_length'] = bit.bor(data_length_low, bit.lshift(data_length_high, 16)) + response['status'] = status -- data_start is the offset of the beginning of the data section -- we use this to calculate where the read data lives @@ -3703,3 +3708,174 @@ for i, v in pairs(status_codes) do status_names[v] = i end + +local NP_LIBRARY_NAME = "PIPE" + +namedpipes = +{ + get_pipe_subpath = function( pipeName, writeToDebugLog ) + local status, pipeSubPath + if not pipeName then return false end + + local _, _, match = pipeName:match( "^(\\+)(.-)\\pipe(\\.-)$" ) + if match then + pipeSubPath = match + status = true + if writeToDebugLog then + stdnse.print_debug( 2, "%s: Converting %s to subpath %s", NP_LIBRARY_NAME, pipeName, match ) + end + else + status = false + pipeSubPath = pipeName + end + + return status, pipeSubPath + end, + + + make_pipe_name = function( hostnameOrIp, pipeSubPath ) + if pipeSubPath:sub(1,1) ~= "\\" then + pipeSubPath = "\\" .. pipeSubPath + end + + return string.format( "\\\\%s\\pipe%s", hostnameOrIp, pipeSubPath ) + end, + + + named_pipe = { + + _smbstate = nil, + _host = nil, + _pipeSubPath = nil, + _overrides = nil, + name = nil, + + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, + + + connect = function( self, host, pipeSubPath, overrides ) + + stdnse.print_debug( 2, "%s: connect() called with %s", NP_LIBRARY_NAME, tostring( pipeSubPath ) ) + self._overrides = overrides or {} + self._host = host + self._pipeSubPath = pipeSubPath + if not host and not host.ip then return false, "host table is required" end + if not pipeSubPath then return false, "pipeSubPath is required" end + + -- If we got a full pipe name, not a sub-path, fix it + if ( pipeSubPath:match( "^\\\\(.-)$" ) ) then + local status + status, self._pipeSubPath = namedpipes.get_pipe_subpath( self._pipeSubPath, true ) + if ( not status ) then + stdnse.print_debug( 1, "%s: Attempt to connect to invalid pipe name: %s", NP_LIBRARY_NAME, tostring( pipeSubPath ) ) + return false, "Invalid pipe name" + end + end + self.name = namedpipes.make_pipe_name( self._host.ip, self._pipeSubPath ) + + stdnse.print_debug( 2, "%s: Connecting to named pipe: %s", NP_LIBRARY_NAME, self.name ) + local status, result, errorMessage + local negotiate_protocol, start_session, disable_extended = true, true, false + status, result = smb.start_ex( self._host, negotiate_protocol, start_session, + "IPC$", self._pipeSubPath, disable_extended, self._overrides ) + + if status then + self._smbstate = result + else + errorMessage = string.format( "Connection failed: %s", result ) + stdnse.print_debug( 2, "%s: Connection to named pipe (%s) failed: %s", + NP_LIBRARY_NAME, self.name, errorMessage ) + end + + return status, errorMessage, result + end, + + + disconnect = function( self ) + if ( self._smbstate ) then + stdnse.print_debug( 2, "%s: Disconnecting named pipe: %s", NP_LIBRARY_NAME, self.name ) + return smb.stop( self._smbstate ) + else + stdnse.print_debug( 2, "%s: disconnect() called, but SMB connection is already closed: %s", NP_LIBRARY_NAME, self.name ) + end + end, + + + send = function( self, messageData ) + if not self._smbstate then + stdnse.print_debug( 2, "%s: send() called on closed pipe (%s)", NP_LIBRARY_NAME, self.name ) + return false, "Failed to send message on named pipe" + end + + local offset = 0 -- offset is actually ignored for named pipes, but we'll define the argument for clarity + local status, result, errorMessage + + status, result = smb.write_file( self._smbstate, messageData, offset, self._overrides ) + + -- if status is true, result is data that we don't need to pay attention to + if not status then + stdnse.print_debug( 2, "%s: Write to named pipe (%s) failed: %s", + NP_LIBRARY_NAME, self.name, result ) + errorMessage = "Failed to send message on named pipe", result + end + + return status, errorMessage + end, + + + receive = function( self ) + if not self._smbstate then + stdnse.print_debug( 2, "%s: receive() called on closed pipe (%s)", NP_LIBRARY_NAME, self.name ) + return false, "Failed to read from named pipe" + end + + local status, result, messageData + -- Packet header values + local offset = 0 -- offset is actually ignored for named pipes, but we'll define the argument for clarity + local MAX_BYTES_PER_READ = 4096 + + status, result = smb.read_file( self._smbstate, offset, MAX_BYTES_PER_READ, self._overrides ) + + if status and result.data then + messageData = result.data + else + stdnse.print_debug( 2, "%s: Read from named pipe (%s) failed: %s", + NP_LIBRARY_NAME, self.name, result ) + return false, "Failed to read from named pipe", result + end + + while (result["status"] == smb.status_codes.NT_STATUS_BUFFER_OVERFLOW) do + status, result = smb.read_file( self._smbstate, offset, MAX_BYTES_PER_READ, self._overrides ) + + if status and result.data then + messageData = messageData .. result.data + else + stdnse.print_debug( 2, "%s: Read additional data from named pipe (%s) failed: %s", + NP_LIBRARY_NAME, self.name, result ) + return false, "Failed to read from named pipe", result + end + end + + return status, messageData + end, + } + +} + +filetype_codes = +{ + FILE_TYPE_DISK = 0x00, + FILE_TYPE_BYTE_MODE_PIPE = 0x01, + FILE_TYPE_MESSAGE_MODE_PIPE = 0x02, + FILE_TYPE_PRINTER = 0x03, + FILE_TYPE_UNKNOWN = 0xFF +} + +for i, v in pairs(filetype_codes) do + filetype_names[v] = i +end diff --git a/scripts/broadcast-ms-sql-discover.nse b/scripts/broadcast-ms-sql-discover.nse index 777f6bb26..59fb51c16 100644 --- a/scripts/broadcast-ms-sql-discover.nse +++ b/scripts/broadcast-ms-sql-discover.nse @@ -1,55 +1,111 @@ +-- -*- mode: lua -*- +-- vim: set filetype=lua : + description = [[ Discovers Microsoft SQL servers in the same broadcast domain. + +SQL Server credentials required: No (will not benefit from +mssql.username & mssql.password). + +The script attempts to discover SQL Server instances in the same broadcast +domain. Any instances found are stored in the Nmap registry for use by any +other ms-sql-* scripts that are run in the same scan. + +In contrast to the ms-sql-discover script, the broadcast version +will use a broadcast method rather than targeting individual hosts. However, the +broadcast version will only use the SQL Server Browser service discovery method. ]] +--- +-- @usage +-- nmap --script broadcast-ms-sql-discover +-- nmap --script broadcast-ms-sql-discover,ms-sql-info --script-args=newtargets -- --- Version 0.1 +-- @output +-- | broadcast-ms-sql-discover: +-- | 192.168.100.128 (WINXP) +-- | [192.168.100.128\MSSQLSERVER] +-- | Name: MSSQLSERVER +-- | Product: Microsoft SQL Server 2000 +-- | TCP port: 1433 +-- | Named pipe: \\192.168.100.128\pipe\sql\query +-- | [192.168.100.128\SQL2K5] +-- | Name: SQL2K5 +-- | Product: Microsoft SQL Server 2005 +-- | Named pipe: \\192.168.100.128\pipe\MSSQL$SQL2K5\sql\query +-- | 192.168.100.150 (SQLSRV) +-- | [192.168.100.150\PROD] +-- | Name: PROD +-- | Product: Microsoft SQL Server 2008 +-- |_ Named pipe: \\192.168.100.128\pipe\sql\query +-- + -- Created 07/12/2010 - v0.1 - created by Patrik Karlsson +-- Revised 02/01/2011 - v0.2 - Added compatibility with changes in mssql.lua (Chris Woodbury) author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" -categories = {"broadcast"} +categories = {"broadcast", "safe", "discovery"} require 'mssql' require 'target' +require 'stdnse' prerule = function() return true end -action = function() - local OUTPUT_TBL = { - ["Server name"] = "info.servername", - ["Version"] = "version.version", - ["Clustered"] = "info.clustered", - ["Named pipe"] = "info.pipe", - ["Tcp port"] = "info.port" - } +--- Adds a label and value to an output table. If the value is a boolean, it is +-- converted to Yes/No; if the value is nil, nothing is added to the table. +local function add_to_output_table( outputTable, outputLabel, outputData ) + + if outputData ~= nil then + if outputData == true then + outputData = "Yes" + elseif outputData == false then + outputData = "No" + end + + table.insert(outputTable, string.format( "%s: %s", outputLabel, outputData ) ) + end + +end + +--- Returns formatted output for the given instance +local function create_instance_output_table( instance ) + + local instanceOutput = {} + + instanceOutput["name"] = string.format( "[%s]", instance:GetName() ) + add_to_output_table( instanceOutput, "Name", instance.instanceName ) + if instance.version then add_to_output_table( instanceOutput, "Product", instance.version.productName ) end + if instance.port then add_to_output_table( instanceOutput, "TCP port", instance.port.number ) end + add_to_output_table( instanceOutput, "Named pipe", instance.pipeName ) + + return instanceOutput + +end + +action = function() - local status, result = mssql.Helper.Discover("255.255.255.255", 1434, true) + local host = { ip = "255.255.255.255" } + local port = { number = 1434, protocol = "udp" } + + local status, result = mssql.Helper.DiscoverBySsrp(host, port, true) if ( not(status) ) then return end - local results = {} - for ip, instances in pairs(result) do - local result_part = {} - if target.ALLOW_NEW_TARGETS then target.add(ip) end - for name, info in pairs(instances) do - local instance = {} - local version - status, version = mssql.Util.DecodeBrowserInfoVersion(info) - - for topic, varname in pairs(OUTPUT_TBL) do - local func = loadstring( "return " .. varname ) - setfenv(func, setmetatable({ info=info; version=version; }, {__index = _G})) - local result = func() - if ( result ) then - table.insert( instance, ("%s: %s"):format(topic, result) ) - end - end - instance.name = version.product - table.insert( result_part, { name = "Instance: " .. info.name, instance } ) + local scriptOutput = {} + for ip, instanceList in pairs(result) do + local serverOutput, serverName = {}, nil + target.add( ip ) + for _, instance in ipairs( instanceList ) do + serverName = serverName or instance.serverName + local instanceOutput = create_instance_output_table( instance ) + table.insert(serverOutput, instanceOutput) end - result_part.name = ip - table.insert( results, result_part ) + serverOutput.name = string.format( "%s (%s)", ip, serverName ) + table.insert( scriptOutput, serverOutput ) end - return stdnse.format_output( true, results ) + + return stdnse.format_output( true, scriptOutput ) + end diff --git a/scripts/ms-sql-brute.nse b/scripts/ms-sql-brute.nse index 3bce7b19a..8bc2088a4 100644 --- a/scripts/ms-sql-brute.nse +++ b/scripts/ms-sql-brute.nse @@ -1,79 +1,301 @@ +-- -*- mode: lua -*- +-- vim: set filetype=lua : + description = [[ -Performs password guessing against Microsoft SQL Server (ms-sql). +Performs password guessing against Microsoft SQL Server (ms-sql). Works best in +conjuction with the ms-sql-discover script. + +SQL Server credentials required: No (will not benefit from +mssql.username & mssql.password). +Run criteria: +* Host script: Will run if the mssql.instance-all, mssql.instance-name +or mssql.instance-port script arguments are used (see mssql.lua). +* Port script: Will run against any services identified as SQL Servers, but only +if the mssql.instance-all, mssql.instance-name +and mssql.instance-port script arguments are NOT used. + +WARNING: SQL Server 2005 and later versions include support for account lockout +policies (which are enforced on a per-user basis). If an account is locked out, +the script will stop running for that instance, unless the +ms-sql-brute.ignore-lockout argument is used. + +NOTE: Communication with instances via named pipes depends on the smb +library. To communicate with (and possibly to discover) instances via named pipes, +the host must have at least one SMB port (e.g. TCP 445) that was scanned and +found to be open. Additionally, named pipe connections may require Windows +authentication to connect to the Windows host (via SMB) in addition to the +authentication required to connect to the SQL Server instances itself. See the +documentation and arguments for the smb library for more information. + +NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate +with ports that were not included in the port list for the Nmap scan. This can +be disabled using the mssql.scanned-ports-only script argument. ]] +--- +-- @usage +-- nmap -p 445 --script ms-sql-brute --script-args mssql.instance-all,userdb=customuser.txt,passdb=custompass.txt +-- nmap -p 1433 --script ms-sql-brute --script-args userdb=customuser.txt,passdb=custompass.txt +-- +-- @output +-- | ms-sql-brute: +-- | [192.168.100.128\TEST] +-- | No credentials found +-- | Warnings: +-- | sa: AccountLockedOut +-- | [192.168.100.128\PROD] +-- | Credentials found: +-- | webshop_reader:secret => Login Success +-- | testuser:secret1234 => PasswordMustChange +-- |_ lordvader:secret1234 => Login Success +-- +---- +-- @args ms-sql-brute.ignore-lockout WARNING! Including this argument will cause +-- the script to continue attempting to brute-forcing passwords for users +-- even after a user has been locked out. This may result in many SQL +-- Server logins being locked out! +-- + +-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson +-- Revised 02/01/2011 - v0.2 (Chris Woodbury) +-- - Added ability to run against all instances on a host; +-- - Added recognition of account-locked out and password-expired error codes; +-- - Added storage of credentials on a per-instance basis +-- - Added compatibility with changes in mssql.lua + author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"auth", "intrusive"} +dependencies = {"ms-sql-discover", "ms-sql-empty-password"} + require 'shortport' require 'stdnse' require 'mssql' require 'unpwdb' ---- --- @output --- PORT STATE SERVICE --- 1433/tcp open ms-sql-s --- | ms-sql-brute: --- | webshop_reader:secret => Login Success --- | testuser:secret1234 => Must change password at next logon --- |_ lordvader:secret1234 => Login Success --- Version 0.1 --- Created 01/17/2010 - v0.1 - created by Patrik Karlsson +hostrule = mssql.Helper.GetHostrule_Standard() +portrule = mssql.Helper.GetPortrule_Standard() + + +--- Returns formatted output for the given instance +local function create_instance_output_table( instance ) + + local instanceOutput = {} + instanceOutput["name"] = string.format( "[%s]", instance:GetName() ) + if ( instance.ms_sql_brute.credentials ) then + local credsOutput = {} + credsOutput["name"] = "Credentials found:" + table.insert( instanceOutput, credsOutput ) + + for username, result in pairs( instance.ms_sql_brute.credentials ) do + local password = result[1] + local errorCode = result[2] + password = password:len()>0 and password or "" + if errorCode then + local errorMessage = mssql.LoginErrorMessage[ errorCode ] or "unknown error" + table.insert( credsOutput, string.format( "%s:%s => %s", username, password, errorMessage ) ) + else + table.insert( credsOutput, string.format( "%s:%s => Login Success", username, password ) ) + end + end + + if ( table.getn( credsOutput ) == 0 ) then + table.insert( instanceOutput, "No credentials found" ) + end + end + + if ( instance.ms_sql_brute.warnings ) then + local warningsOutput = {} + warningsOutput["name"] = "Warnings:" + table.insert( instanceOutput, warningsOutput ) + + for _, warning in ipairs( instance.ms_sql_brute.warnings ) do + table.insert( warningsOutput, warning ) + end + end + + if ( instance.ms_sql_brute.errors ) then + local errorsOutput = {} + errorsOutput["name"] = "Errors:" + table.insert( instanceOutput, errorsOutput ) + + for _, error in ipairs( instance.ms_sql_brute.errors ) do + table.insert( errorsOutput, error ) + end + end + + return instanceOutput + +end + + +local function test_credentials( instance, helper, username, password ) + local database = "tempdb" + local stopUser, stopInstance = false, false + + local status, result = helper:ConnectEx( instance ) + local loginErrorCode + if( status ) then + stdnse.print_debug( 2, "%s: Attempting login to %s as %s/%s", SCRIPT_NAME, instance:GetName(), username, password ) + status, result, loginErrorCode = helper:Login( username, password, database, instance.host.ip ) + end + helper:Disconnect() + + local passwordIsGood, canLogin + if status then + passwordIsGood = true + canLogin = true + elseif ( loginErrorCode ) then + if ( ( loginErrorCode ~= mssql.LoginErrorType.InvalidUsernameOrPassword ) and + ( loginErrorCode ~= mssql.LoginErrorType.NotAssociatedWithTrustedConnection ) ) then + stopUser = true + end + + if ( loginErrorCode == mssql.LoginErrorType.PasswordExpired ) then passwordIsGood = true + elseif ( loginErrorCode == mssql.LoginErrorType.PasswordMustChange ) then passwordIsGood = true + elseif ( loginErrorCode == mssql.LoginErrorType.AccountLockedOut ) then + stdnse.print_debug( 1, "%s: Account %s locked out on %s", SCRIPT_NAME, username, instance:GetName() ) + table.insert( instance.ms_sql_brute.warnings, string.format( "%s: Account is locked out.", username ) ) + if ( not stdnse.get_script_args( "ms-sql-brute.ignore-lockout" ) ) then + stopInstance = true + end + end + if ( mssql.LoginErrorMessage[ loginErrorCode ] == nil ) then + stdnse.print_debug( 2, "%s: Attemping login to %s as (%s/%s): Unknown login error number: %s", + SCRIPT_NAME, instance:GetName(), username, password, loginErrorCode ) + table.insert( instance.ms_sql_brute.warnings, string.format( "Unknown login error number: %s", loginErrorCode ) ) + end + stdnse.print_debug( 3, "%s: Attempt to login to %s as (%s/%s): %d (%s)", + SCRIPT_NAME, instance:GetName(), username, password, loginErrorCode, tostring( mssql.LoginErrorMessage[ loginErrorCode ] ) ) + else + table.insert( instance.ms_sql_brute.errors, string.format("Network error. Skipping instance. Error: %s", result ) ) + stopUser = true + stopInstance = true + end + + if ( passwordIsGood ) then + stopUser = true + + instance.ms_sql_brute.credentials[ username ] = { password, loginErrorCode } + -- Add credentials for other ms-sql scripts to use but don't + -- add accounts that need to change passwords + if ( canLogin ) then + instance.credentials[ username ] = password + -- Legacy storage method (does not distinguish between instances) + nmap.registry.mssqlusers = nmap.registry.mssqlusers or {} + nmap.registry.mssqlusers[username]=password + end + end + + return stopUser, stopInstance +end + +--- Processes a single instance, attempting to detect an empty password for "sa" +local function process_instance( instance ) + + -- One of this script's features is that it will report an instance's + -- in both the port-script results and the host-script results. In order to + -- avoid redundant login attempts on an instance, we will just make the + -- attempt once and then re-use the results. We'll use a mutex to make sure + -- that multiple script instances (e.g. a host-script and a port-script) + -- working on the same SQL Server instance can only enter this block one at + -- a time. + local mutex = nmap.mutex( instance ) + mutex( "lock" ) + + -- If this instance has already been tested (e.g. if we got to it by both the + -- hostrule and the portrule), don't test it again. + if ( instance.tested_brute ~= true ) then + instance.tested_brute = true + + instance.credentials = instance.credentials or {} + instance.ms_sql_brute = instance.ms_sql_brute or {} + instance.ms_sql_brute.credentials = instance.ms_sql_brute.credentials or {} + instance.ms_sql_brute.warnings = instance.ms_sql_brute.warnings or {} + instance.ms_sql_brute.errors = instance.ms_sql_brute.errors or {} + + local result, status + local stopUser, stopInstance + local usernames, passwords, username, password + local helper = mssql.Helper:new() + + if ( not instance:HasNetworkProtocols() ) then + stdnse.print_debug( 1, "%s: %s has no network protocols enabled.", SCRIPT_NAME, instance:GetName() ) + table.insert( instance.ms_sql_brute.errors, "No network protocols enabled." ) + stopInstance = true + end + + status, usernames = unpwdb.usernames() + if ( not(status) ) then + stdnse.print_debug( 1, "%s: Failed to load usernames list.", SCRIPT_NAME ) + table.insert( instance.ms_sql_brute.errors, "Failed to load usernames list." ) + stopInstance = true + end + + if ( status ) then + status, passwords = unpwdb.passwords() + if ( not(status) ) then + stdnse.print_debug( 1, "%s: Failed to load passwords list.", SCRIPT_NAME ) + table.insert( instance.ms_sql_brute.errors, "Failed to load passwords list." ) + stopInstance = true + end + end + + if ( status ) then + for username in usernames do + if stopInstance then break end + + -- See if the password is the same as the username (which may not + -- be in the password list) + stopUser, stopInstance = test_credentials( instance, helper, username, username ) + + for password in passwords do + if stopUser then break end + + stopUser, stopInstance = test_credentials( instance, helper, username, password ) + end + + passwords("reset") + end + end + end + + -- The password testing has been finished. Unlock the mutex. + mutex( "done" ) + + return create_instance_output_table( instance ) + +end -portrule = shortport.port_or_service(1433, "ms-sql-s") action = function( host, port ) - - local result, response, status = {}, nil, nil - local valid_accounts = {} - local usernames, passwords - local username, password - local helper = mssql.Helper:new() + local scriptOutput = {} + local status, instanceList = mssql.Helper.GetTargetInstances( host, port ) - status, usernames = unpwdb.usernames() - if ( not(status) ) then - return " \n\nFailed to load usernames.lst" - end - status, passwords = unpwdb.passwords() - if ( not(status) ) then - return " \n\nFailed to load usernames.lst" - end - - for username in usernames do - for password in passwords do + local domain, bruteWindows = stdnse.get_script_args("mssql.domain", "ms-sql-brute.brute-windows-accounts") - status, result = helper:Connect(host, port) - if( not(status) ) then - return " \n\n" .. result + if ( domain and not(bruteWindows) ) then + local ret = "\n " .. + "Windows authentication was enabled but the argument\n " .. + "ms-sql-brute.brute-windows-accounts was not given. As there is currently no\n " .. + "way of detecting accounts being locked out when Windows authentication is \n " .. + "used, make sure that the amount entries in the password list\n " .. + "(passdb argument) are at least 2 entries below the lockout threshold." + return ret + end + + if ( not status ) then + return stdnse.format_output( false, instanceList ) + else + for _, instance in pairs( instanceList ) do + local instanceOutput = process_instance( instance ) + if instanceOutput then + table.insert( scriptOutput, instanceOutput ) end - - stdnse.print_debug( "Trying %s/%s ...", username, password ) - status, result = helper:Login( username, password, "tempdb", host.ip ) - helper:Disconnect() - - if ( status ) or ( "Must change password at next logon" == result ) then - -- Add credentials for other mysql scripts to use - table.insert( valid_accounts, string.format("%s:%s => %s", username, password:len()>0 and password or "", result ) ) - -- don't add accounts that need to change passwords to the registry - if ( result ~= "Login Success") then - break - end - if nmap.registry.mssqlusers == nil then - nmap.registry.mssqlusers = {} - end - nmap.registry.mssqlusers[username]=password - - break - end - end - passwords("reset") end - - local output = stdnse.format_output(true, valid_accounts) - - return output + + return stdnse.format_output( true, scriptOutput ) end diff --git a/scripts/ms-sql-config.nse b/scripts/ms-sql-config.nse index 5ecec3ca0..54be3aad6 100644 --- a/scripts/ms-sql-config.nse +++ b/scripts/ms-sql-config.nse @@ -1,8 +1,65 @@ +-- -*- mode: lua -*- +-- vim: set filetype=lua : + description = [[ -Queries Microsoft SQL Server (ms-sql) for a list of databases, linked -servers, and configuration settings. +Queries Microsoft SQL Server (ms-sql) instances for a list of databases, linked servers, +and configuration settings. + +SQL Server credentials required: Yes (use ms-sql-brute, ms-sql-empty-password +and/or mssql.username & mssql.password) +Run criteria: +* Host script: Will run if the mssql.instance-all, mssql.instance-name +or mssql.instance-port script arguments are used (see mssql.lua). +* Port script: Will run against any services identified as SQL Servers, but only +if the mssql.instance-all, mssql.instance-name +and mssql.instance-port script arguments are NOT used. + +NOTE: Communication with instances via named pipes depends on the smb +library. To communicate with (and possibly to discover) instances via named pipes, +the host must have at least one SMB port (e.g. TCP 445) that was scanned and +found to be open. Additionally, named pipe connections may require Windows +authentication to connect to the Windows host (via SMB) in addition to the +authentication required to connect to the SQL Server instances itself. See the +documentation and arguments for the smb library for more information. + +NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate +with ports that were not included in the port list for the Nmap scan. This can +be disabled using the mssql.scanned-ports-only script argument. ]] +--- +-- @usage +-- nmap -p 1433 --script ms-sql-config --script-args mssql.username=sa,mssql.password=sa +-- +-- @args ms-sql-config.showall If set, shows all configuration options. +-- +-- @output +-- | ms-sql-config: +-- | [192.168.100.25\MSSQLSERVER] +-- | Databases +-- | name db_size owner +-- | ==== ======= ===== +-- | nmap 2.74 MB MAC-MINI\david +-- | Configuration +-- | name value inuse description +-- | ==== ===== ===== =========== +-- | SQL Mail XPs 0 0 Enable or disable SQL Mail XPs +-- | Database Mail XPs 0 0 Enable or disable Database Mail XPs +-- | SMO and DMO XPs 1 1 Enable or disable SMO and DMO XPs +-- | Ole Automation Procedures 0 0 Enable or disable Ole Automation Procedures +-- | xp_cmdshell 0 0 Enable or disable command shell +-- | Ad Hoc Distributed Queries 0 0 Enable or disable Ad Hoc Distributed Queries +-- | Replication XPs 0 0 Enable or disable Replication XPs +-- | Linked Servers +-- | srvname srvproduct providername +-- | ======= ========== ============ +-- |_ MAC-MINI SQL Server SQLOLEDB +-- + +-- Created 04/02/2010 - v0.1 - created by Patrik Karlsson +-- Revised 02/01/2011 - v0.2 - Added ability to run against all instances on a host; +-- added compatibility with changes in mssql.lua (Chris Woodbury) + author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"discovery", "safe"} @@ -11,57 +68,23 @@ require 'shortport' require 'stdnse' require 'mssql' -dependencies = {"ms-sql-brute", "ms-sql-empty-password"} +dependencies = {"ms-sql-brute", "ms-sql-empty-password", "ms-sql-discover"} ---- --- @args mssql.username specifies the username to use to connect to --- the server. This option overrides any accounts found by --- the mssql-brute and mssql-empty-password scripts. --- --- @args mssql.password specifies the password to use to connect to --- the server. This option overrides any accounts found by --- the mssql-brute and mssql-empty-password scripts. --- --- @args ms-sql-config.showall if set shows all configuration options. --- --- @output --- PORT STATE SERVICE --- 1433/tcp open ms-sql-s --- | ms-sql-config: --- | Databases --- | name db_size owner --- | ==== ======= ===== --- | nmap 2.74 MB MAC-MINI\david --- | Configuration --- | name value inuse description --- | ==== ===== ===== =========== --- | SQL Mail XPs 0 0 Enable or disable SQL Mail XPs --- | Database Mail XPs 0 0 Enable or disable Database Mail XPs --- | SMO and DMO XPs 1 1 Enable or disable SMO and DMO XPs --- | Ole Automation Procedures 0 0 Enable or disable Ole Automation Procedures --- | xp_cmdshell 0 0 Enable or disable command shell --- | Ad Hoc Distributed Queries 0 0 Enable or disable Ad Hoc Distributed Queries --- | Replication XPs 0 0 Enable or disable Replication XPs --- | Linked Servers --- | srvname srvproduct providername --- | ======= ========== ============ --- |_ MAC-MINI SQL Server SQLOLEDB --- Version 0.1 --- Created 04/02/2010 - v0.1 - created by Patrik Karlsson +hostrule = mssql.Helper.GetHostrule_Standard() +portrule = mssql.Helper.GetPortrule_Standard() -portrule = shortport.port_or_service(1433, "ms-sql-s") -action = function( host, port ) - - local status, helper, response - local username = stdnse.get_script_args( 'mssql.username' ) - local password = stdnse.get_script_args( 'mssql.password' ) or "" +--- Processes a set of instances +local function process_instance( instance ) + + local status, errorMessage local result, result_part = {}, {} local conf_filter = stdnse.get_script_args( {'mssql-config.showall', 'ms-sql-config.showall'} ) and "" or " WHERE configuration_id > 16384" local db_filter = stdnse.get_script_args( {'mssql-config.showall', 'ms-sql-config.showall'} ) and "" or " WHERE name NOT IN ('master','model','tempdb','msdb')" + local helper = mssql.Helper:new() local queries = { [2]={ ["Configuration"] = [[ SELECT name, @@ -77,46 +100,25 @@ action = function( host, port ) INSERT INTO #nmap_dbs EXEC sp_helpdb SELECT name, db_size, owner FROM #nmap_dbs ]] .. db_filter .. [[ - DROP DATABASE #nmap_dbs ]] } + DROP TABLE #nmap_dbs ]] } } - if ( not(username) and nmap.registry.mssqlusers ) then - -- do we have a sysadmin? - if ( nmap.registry.mssqlusers.sa ) then - username = "sa" - password = nmap.registry.mssqlusers.sa - else - -- ok were stuck with some non sysadmin account, just get the first one - for user, pass in pairs(nmap.registry.mssqlusers) do - username = user - password = pass - break - end - end - end + status, errorMessage = helper:ConnectEx( instance ) + if ( not(status) ) then result = "ERROR: " .. errorMessage end - -- If we don't have a valid username, simply fail silently - if ( not(username) ) then - return - end - - helper = mssql.Helper:new() - status, response = helper:Connect(host, port) - if ( not(status) ) then - return " \n\n" .. response - end - - status, response = helper:Login( username, password, nil, host.ip ) - if ( not(status) ) then - return " \n\nERROR: " .. response + if status then + status, errorMessage = helper:LoginEx( instance ) + if ( not(status) ) then result = "ERROR: " .. errorMessage end end for _, v in ipairs( queries ) do + if ( not status ) then break end for header, query in pairs(v) do status, result_part = helper:Query( query ) if ( not(status) ) then - return " \n\nERROR: " .. result_part + result = "ERROR: " .. result_part + break end result_part = mssql.Util.FormatOutputTable( result_part, true ) result_part.name = header @@ -126,6 +128,28 @@ action = function( host, port ) helper:Disconnect() - return stdnse.format_output( true, result ) - + local instanceOutput = {} + instanceOutput["name"] = string.format( "[%s]", instance:GetName() ) + table.insert( instanceOutput, result ) + + return instanceOutput +end + + +action = function( host, port ) + local scriptOutput = {} + local status, instanceList = mssql.Helper.GetTargetInstances( host, port ) + + if ( not status ) then + return stdnse.format_output( false, instanceList ) + else + for _, instance in pairs( instanceList ) do + local instanceOutput = process_instance( instance ) + if instanceOutput then + table.insert( scriptOutput, instanceOutput ) + end + end + end + + return stdnse.format_output( true, scriptOutput ) end diff --git a/scripts/ms-sql-discover.nse b/scripts/ms-sql-discover.nse new file mode 100755 index 000000000..6db104309 --- /dev/null +++ b/scripts/ms-sql-discover.nse @@ -0,0 +1,131 @@ +-- -*- mode: lua -*- +-- vim: set filetype=lua : + +description = [[ +Attempts to discover Microsoft SQL Server instances. + +SQL Server credentials required: No (will not benefit from +mssql.username & mssql.password). +Run criteria: +* Host script: Will always run, unless the mssql.scanned-ports-only + script argument was specified (see mssql.lua for more details); in that case, + the script will run if one or more of the following ports were scanned and + weren't found to be closed: 1434/udp, 1433/tcp, an SMB port (see smb.lua). +* Port script: N/A + +The script attempts to discover SQL Server instances. Any instances found are +stored in the Nmap registry for use by any other ms-sql-* scripts that are run +in the same scan. + +The script attempts to discover SQL Server instances by the following three +methods: +* Querying the SQL Server Brower service (UDP port 1434): If this service is +available, it will provide detailed information on each of the instances +installed on the host, including an approximate version number (use ms-sql-info +for more accurate and detailed version information). However, this service may +not be running, even if SQL Server instances are present, and it is also possible +for instances to "hide" themselves from the Browser service. +* Connecting to the default SQL Server listening port (TCP port 1433): The script +will attempt to fingerprint the service (if any) listening on TCP port 1433, SQL +Server's default port. +* Connecting via named pipes to the default pipe names: The script will attempt +to connect over SMB to default pipe names for SQL Server. + +NOTE: Communication with instances via named pipes depends on the smb +library. To communicate with (and possibly to discover) instances via named pipes, +the host must have at least one SMB port (e.g. TCP 445) that was scanned and +found to be open. Additionally, named pipe connections may require Windows +authentication to connect to the Windows host (via SMB) in addition to the +authentication required to connect to the SQL Server instances itself. See the +documentation and arguments for the smb library for more information. + +NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate +with ports that were not included in the port list for the Nmap scan. This can +be disabled using the mssql.scanned-ports-only script argument. +]] + +--- +-- @usage +-- nmap -p 445 --script ms-sql-discover +-- +-- @output +-- | ms-sql-discover: +-- | [192.168.100.128\MSSQLSERVER] +-- | Name: MSSQLSERVER +-- | Product: Microsoft SQL Server 2000 +-- | TCP port: 1433 +-- | Named pipe: \\192.168.100.128\pipe\sql\query +-- | [192.168.100.128\SQL2K5] +-- | Name: SQL2K5 +-- | Product: Microsoft SQL Server 2005 +-- |_ Named pipe: \\192.168.100.128\pipe\MSSQL$SQL2K5\sql\query + +-- rev 1.0 (2011-02-01) - Initial version (Chris Woodbury) + +author = "Chris Woodbury" + +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +categories = {"default", "discovery", "safe"} + +require("mssql") +require("smb") + +hostrule = function(host) + local sqlDefaultPort = nmap.get_port_state( host, {number = 1433, protocol = "tcp"} ) + local sqlBrowserPort = nmap.get_port_state( host, {number = 1434, protocol = "udp"} ) + local smbPortNumber = smb.get_port( host ) + + return (not mssql.SCANNED_PORTS_ONLY) or + (sqlDefaultPort and sqlDefaultPort.state ~= "closed") or + (sqlBrowserPort and sqlBrowserPort.state ~= "closed") or + (smbPortNumber ~= nil) +end + + +--- Adds a label and value to an output table. If the value is a boolean, it is +-- converted to Yes/No; if the value is nil, nothing is added to the table. +local function add_to_output_table( outputTable, outputLabel, outputData ) + + if outputData ~= nil then + if outputData == true then + outputData = "Yes" + elseif outputData == false then + outputData = "No" + end + + table.insert(outputTable, string.format( "%s: %s", outputLabel, outputData ) ) + end + +end + +--- Returns formatted output for the given instance +local function create_instance_output_table( instance ) + + local instanceOutput = {} + + instanceOutput["name"] = string.format( "[%s]", instance:GetName() ) + add_to_output_table( instanceOutput, "Name", instance.instanceName ) + if instance.version then add_to_output_table( instanceOutput, "Product", instance.version.productName ) end + if instance.port then add_to_output_table( instanceOutput, "TCP port", instance.port.number ) end + add_to_output_table( instanceOutput, "Named pipe", instance.pipeName ) + + return instanceOutput + +end + + +action = function(host) + mssql.Helper.Discover( host ) + local scriptOutput, instancesFound = {}, nil + instancesFound = mssql.Helper.GetDiscoveredInstances( host ) + + if ( instancesFound ) then + for _, instance in ipairs( instancesFound ) do + local instanceOutput = create_instance_output_table( instance ) + table.insert(scriptOutput, instanceOutput) + end + stdnse.print_debug( 1, "%s: Found %d instances for %s.", SCRIPT_NAME, #instancesFound, host.ip ) + end + return stdnse.format_output( true, scriptOutput ) +end diff --git a/scripts/ms-sql-empty-password.nse b/scripts/ms-sql-empty-password.nse index 1b945b634..b0bdf7399 100644 --- a/scripts/ms-sql-empty-password.nse +++ b/scripts/ms-sql-empty-password.nse @@ -1,52 +1,181 @@ +-- -*- mode: lua -*- +-- vim: set filetype=lua : + description = [[ -Attempts to authenticate using an empty password for the sysadmin (sa) account. +Attempts to authenticate to Microsoft SQL Servers using an empty password for +the sysadmin (sa) account. + +SQL Server credentials required: No (will not benefit from +mssql.username & mssql.password). +Run criteria: +* Host script: Will run if the mssql.instance-all, mssql.instance-name +or mssql.instance-port script arguments are used (see mssql.lua). +* Port script: Will run against any services identified as SQL Servers, but only +if the mssql.instance-all, mssql.instance-name +and mssql.instance-port script arguments are NOT used. + +WARNING: SQL Server 2005 and later versions include support for account lockout +policies (which are enforced on a per-user basis). + +NOTE: Communication with instances via named pipes depends on the smb +library. To communicate with (and possibly to discover) instances via named pipes, +the host must have at least one SMB port (e.g. TCP 445) that was scanned and +found to be open. Additionally, named pipe connections may require Windows +authentication to connect to the Windows host (via SMB) in addition to the +authentication required to connect to the SQL Server instances itself. See the +documentation and arguments for the smb library for more information. + +NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate +with ports that were not included in the port list for the Nmap scan. This can +be disabled using the mssql.scanned-ports-only script argument. ]] +--- +-- @usage +-- nmap -p 445 --script ms-sql-empty-password --script-args mssql.instance-all +-- nmap -p 1433 --script ms-sql-empty-password +-- +-- @output +-- | ms-sql-empty-password: +-- | [192.168.100.128\PROD] +-- |_ sa: => Login Success +-- + +-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson +-- Revised 02/01/2011 - v0.2 (Chris Woodbury) +-- - Added ability to run against all instances on a host; +-- - Added storage of credentials on a per-instance basis +-- - Added compatibility with changes in mssql.lua + author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"auth","intrusive"} +dependencies = {"ms-sql-discover"} + require 'shortport' require 'stdnse' require 'mssql' ---- --- --- @output --- PORT STATE SERVICE --- 1433/tcp open ms-sql-s --- | ms-sql-empty-password: --- |_ sa: => Login Correct --- --- +hostrule = mssql.Helper.GetHostrule_Standard() +portrule = mssql.Helper.GetPortrule_Standard() --- Version 0.1 --- Created 01/17/2010 - v0.1 - created by Patrik Karlsson +local function test_credentials( instance, helper, username, password ) + local database = "tempdb" + + local status, result = helper:ConnectEx( instance ) + local loginErrorCode + if( status ) then + stdnse.print_debug( 2, "%s: Attempting login to %s", SCRIPT_NAME, instance:GetName() ) + status, result, loginErrorCode = helper:Login( username, password, database, instance.host.ip ) + end + helper:Disconnect() + + local passwordIsGood, canLogin + if status then + passwordIsGood = true + canLogin = true + elseif ( loginErrorCode ) then + if ( loginErrorCode == mssql.LoginErrorType.PasswordExpired ) then passwordIsGood = true end + if ( loginErrorCode == mssql.LoginErrorType.PasswordMustChange ) then passwordIsGood = true end + if ( loginErrorCode == mssql.LoginErrorType.AccountLockedOut ) then + stdnse.print_debug( 1, "%s: Account %s locked out on %s", SCRIPT_NAME, username, instance:GetName() ) + table.insert( instance.ms_sql_empty, string.format("'sa' account is locked out.", result ) ) + end + if ( mssql.LoginErrorMessage[ loginErrorCode ] == nil ) then + stdnse.print_debug( 2, "%s: Attemping login to %s: Unknown login error number: %s", + SCRIPT_NAME, instance:GetName(), loginErrorCode ) + table.insert( instance.ms_sql_empty, string.format( "Unknown login error number: %s", loginErrorCode ) ) + end + else + table.insert( instance.ms_sql_empty, string.format("Network error. Error: %s", result ) ) + end + + if ( passwordIsGood ) then + local loginResultMessage = "Login Success" + if loginErrorCode then + loginResultMessage = mssql.LoginErrorMessage[ errorCode ] or "unknown error" + end + table.insert( instance.ms_sql_empty, string.format( "%s:%s => %s", username, password:len()>0 and password or "", loginResultMessage ) ) + + -- Add credentials for other ms-sql scripts to use but don't + -- add accounts that need to change passwords + if ( canLogin ) then + instance.credentials[ username ] = password + -- Legacy storage method (does not distinguish between instances) + nmap.registry.mssqlusers = nmap.registry.mssqlusers or {} + nmap.registry.mssqlusers[username]=password + end + end +end + +--- Processes a single instance, attempting to detect an empty password for "sa" +local function process_instance( instance ) + + -- One of this script's features is that it will report an instance's + -- in both the port-script results and the host-script results. In order to + -- avoid redundant login attempts on an instance, we will just make the + -- attempt once and then re-use the results. We'll use a mutex to make sure + -- that multiple script instances (e.g. a host-script and a port-script) + -- working on the same SQL Server instance can only enter this block one at + -- a time. + local mutex = nmap.mutex( instance ) + mutex( "lock" ) + + local status, result + + -- If this instance has already been tested (e.g. if we got to it by both the + -- hostrule and the portrule), don't test it again. This will reduce the risk + -- of locking out accounts. + if ( instance.tested_empty ~= true ) then + instance.tested_empty = true + + instance.credentials = instance.credentials or {} + instance.ms_sql_empty = instance.ms_sql_empty or {} + + if not instance:HasNetworkProtocols() then + stdnse.print_debug( 1, "%s: %s has no network protocols enabled.", SCRIPT_NAME, instance:GetName() ) + table.insert( instance.ms_sql_empty, "No network protocols enabled." ) + end + + local helper = mssql.Helper:new() + test_credentials( instance, helper, "sa", "" ) + end + + -- The password testing has been finished. Unlock the mutex. + mutex( "done" ) + + local instanceOutput + if ( instance.ms_sql_empty ) then + instanceOutput = {} + instanceOutput["name"] = string.format( "[%s]", instance:GetName() ) + for _, message in ipairs( instance.ms_sql_empty ) do + table.insert( instanceOutput, message ) + end + if ( nmap.verbosity() > 1 and table.getn( instance.ms_sql_empty ) == 0 ) then + table.insert( instanceOutput, "'sa' account password is not blank." ) + end + end + + return instanceOutput + +end -portrule = shortport.port_or_service(1433, "ms-sql-s") action = function( host, port ) - - local helper, status, result - local username, password, database, valid_accounts = "sa", "", "tempdb", {} + local scriptOutput = {} + local status, instanceList = mssql.Helper.GetTargetInstances( host, port ) - helper = mssql.Helper:new() - status, result = helper:Connect(host, port) - - if( not(status) ) then - return " \n\n" .. result + if ( not status ) then + return stdnse.format_output( false, instanceList ) + else + for _, instance in pairs( instanceList ) do + local instanceOutput = process_instance( instance ) + if instanceOutput then + table.insert( scriptOutput, instanceOutput ) + end + end end - - status, result = helper:Login( username, password, database, host.ip ) - helper:Disconnect() - - if status then - nmap.registry.mssqlusers = nmap.registry.mssqlusers or {} - nmap.registry.mssqlusers[username]=password - - table.insert( valid_accounts, string.format("%s:%s => Login Success", username, password:len()>0 and password or "" ) ) - end - - return stdnse.format_output(true, valid_accounts) + return stdnse.format_output( true, scriptOutput ) end diff --git a/scripts/ms-sql-hasdbaccess.nse b/scripts/ms-sql-hasdbaccess.nse index e349b9fa1..e5de79aa8 100644 --- a/scripts/ms-sql-hasdbaccess.nse +++ b/scripts/ms-sql-hasdbaccess.nse @@ -1,16 +1,68 @@ +-- -*- mode: lua -*- +-- vim: set filetype=lua : + description = [[ -Queries Microsoft SQL Server (ms-sql) for a list of databases a user has +Queries Microsoft SQL Server (ms-sql) instances for a list of databases a user has access to. +SQL Server credentials required: Yes (use ms-sql-brute, ms-sql-empty-password +and/or mssql.username & mssql.password) +Run criteria: +* Host script: Will run if the mssql.instance-all, mssql.instance-name +or mssql.instance-port script arguments are used (see mssql.lua). +* Port script: Will run against any services identified as SQL Servers, but only +if the mssql.instance-all, mssql.instance-name +and mssql.instance-port script arguments are NOT used. + The script needs an account with the sysadmin server role to work. -It needs to be fed credentials through the script arguments or from -the scripts mssql-brute or mssql-empty-password. When run, the script iterates over the credentials and attempts to run -the command until either all credentials are exhausted or until the -command is executed. +the command for each available set of credentials. + +NOTE: The "owner" field in the results will be truncated at 20 characters. This +is a limitation of the sp_MShasdbaccess stored procedure that the +script uses. + +NOTE: Communication with instances via named pipes depends on the smb +library. To communicate with (and possibly to discover) instances via named pipes, +the host must have at least one SMB port (e.g. TCP 445) that was scanned and +found to be open. Additionally, named pipe connections may require Windows +authentication to connect to the Windows host (via SMB) in addition to the +authentication required to connect to the SQL Server instances itself. See the +documentation and arguments for the smb library for more information. + +NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate +with ports that were not included in the port list for the Nmap scan. This can +be disabled using the mssql.scanned-ports-only script argument. ]] +--- +-- @usage +-- nmap -p 1433 --script ms-sql-hasdbaccess --script-args mssql.username=sa,mssql.password=sa +-- +-- @args ms-sql-hasdbaccess.limit limits the amount of databases per-user +-- that are returned (default 5). If set to zero or less all +-- databases the user has access to are returned. +-- +-- @output +-- | ms-sql-hasdbaccess: +-- | [192.168.100.25\MSSQLSERVER] +-- | webshop_reader +-- | dbname owner +-- | ====== ===== +-- | hr sa +-- | finance sa +-- | webshop sa +-- | lordvader +-- | dbname owner +-- | ====== ===== +-- | testdb CQURE-NET\Administr +-- |_ webshop sa + +-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson +-- Revised 02/01/2011 - v0.2 - Added ability to run against all instances on a host; +-- added compatibility with changes in mssql.lua (Chris Woodbury) + author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"auth", "discovery","safe"} @@ -19,54 +71,16 @@ require 'shortport' require 'stdnse' require 'mssql' -dependencies = {"ms-sql-brute", "ms-sql-empty-password"} ---- --- @args mssql.username specifies the username to use to connect to --- the server. This option overrides any accounts found by --- the mssql-brute and mssql-empty-password scripts. --- --- @args mssql.password specifies the password to use to connect to --- the server. This option overrides any accounts found by --- the ms-sql-brute and ms-sql-empty-password scripts. --- --- @args ms-sql-hasdbaccess.limit limits the amount of databases per-user --- that are returned (default 5). If set to zero or less all --- databases the user has access to are returned. --- --- @output --- PORT STATE SERVICE --- 1433/tcp open ms-sql-s --- | ms-sql-hasdbaccess: --- | webshop_reader --- | dbname owner --- | hr sa --- | finance sa --- | webshop sa --- | lordvader --- | dbname owner --- | testdb CQURE-NET\Administr --- |_ webshop sa +dependencies = {"ms-sql-brute", "ms-sql-empty-password", "ms-sql-discover"} --- Version 0.1 --- Created 01/17/2010 - v0.1 - created by Patrik Karlsson -portrule = shortport.port_or_service(1433, "ms-sql-s") +hostrule = mssql.Helper.GetHostrule_Standard() +portrule = mssql.Helper.GetPortrule_Standard() -local function table_contains( tbl, val ) - for k,v in pairs(tbl) do - if ( v == val ) then - return true - end - end - return false -end -action = function( host, port ) - - local status, result, helper, rs - local username = stdnse.get_script_args('mssql.username') - local password = stdnse.get_script_args('mssql.password') or "" - local creds +local function process_instance( instance ) + + local status, result, rs local query, limit local output = {} local exclude_dbs = { "'master'", "'tempdb'", "'model'", "'msdb'" } @@ -90,54 +104,71 @@ action = function( host, port ) ("SELECT %s dbname, owner FROM #hasaccess WHERE dbname NOT IN(%s)"):format(limit, stdnse.strjoin(",", exclude_dbs)), "DROP TABLE #hasaccess" } - if ( username ) then - creds = {} - creds[username] = password - elseif ( not(username) and nmap.registry.mssqlusers ) then - -- do we have a sysadmin? - creds = nmap.registry.mssqlusers - end - - -- If we don't have valid creds, simply fail silently - if ( not(creds) ) then - return - end - - for username, password in pairs( creds ) do - helper = mssql.Helper:new() - status, result = helper:Connect(host, port) - if ( not(status) ) then - return " \n\n" .. result - end - - status, result = helper:Login( username, password, nil, host.ip ) - if ( not(status) ) then - stdnse.print_debug("ERROR: %s", result) - break - end - - for _, q in pairs(query) do - status, result = helper:Query( q ) + local creds = mssql.Helper.GetLoginCredentials_All( instance ) + if ( not creds ) then + output = "ERROR: No login credentials." + else + for username, password in pairs( creds ) do + local helper = mssql.Helper:new() + status, result = helper:ConnectEx( instance ) + if ( not(status) ) then + output = "ERROR: " .. result + break + end + if ( status ) then - -- Only the SELECT statement should produce output - if ( #result.rows > 0 ) then - rs = result + status = helper:Login( username, password, nil, instance.host.ip ) + end + + if ( status ) then + for _, q in pairs(query) do + status, result = helper:Query( q ) + if ( status ) then + -- Only the SELECT statement should produce output + if ( #result.rows > 0 ) then + rs = result + end + end end end - end - - helper:Disconnect() - - if ( status and rs) then - result = mssql.Util.FormatOutputTable( rs, true ) - result.name = username - if ( RS_LIMIT > 0 ) then - result.name = result.name .. (" (Showing %d first results)"):format(RS_LIMIT) - end - table.insert( output, result ) - end - end + + helper:Disconnect() - return stdnse.format_output( true, output ) + if ( status and rs ) then + result = mssql.Util.FormatOutputTable( rs, true ) + result.name = username + if ( RS_LIMIT > 0 ) then + result.name = result.name .. (" (Showing %d first results)"):format(RS_LIMIT) + end + table.insert( output, result ) + end + end + end + + + local instanceOutput = {} + instanceOutput["name"] = string.format( "[%s]", instance:GetName() ) + table.insert( instanceOutput, output ) + + return instanceOutput end + + +action = function( host, port ) + local scriptOutput = {} + local status, instanceList = mssql.Helper.GetTargetInstances( host, port ) + + if ( not status ) then + return stdnse.format_output( false, instanceList ) + else + for _, instance in pairs( instanceList ) do + local instanceOutput = process_instance( instance ) + if instanceOutput then + table.insert( scriptOutput, instanceOutput ) + end + end + end + + return stdnse.format_output( true, scriptOutput ) +end diff --git a/scripts/ms-sql-info.nse b/scripts/ms-sql-info.nse index 8799014f5..b47186eca 100644 --- a/scripts/ms-sql-info.nse +++ b/scripts/ms-sql-info.nse @@ -1,170 +1,251 @@ +-- -*- mode: lua -*- +-- vim: set filetype=lua : + description = [[ -Attempts to extract information from Microsoft SQL Server instances. +Attempts to determine configuration and version information for Microsoft SQL +Server instances. + +SQL Server credentials required: No (will not benefit from +mssql.username & mssql.password). +Run criteria: +* Host script: Will always run. +* Port script: N/A + +NOTE: Unlike previous versions, this script will NOT attempt to log in to SQL +Server instances. Blank passwords can be checked using the +ms-sql-empty-password script. E.g.: +nmap -sn --script ms-sql-empty-password --script-args mssql.instance-all + +The script uses two means of getting version information for SQL Server instances: +* Querying the SQL Server Browser service, which runs by default on UDP port +1434 on servers that have SQL Server 2000 or later installed. However, this +service may be disabled without affecting the functionality of the instances. +Additionally, it provides imprecise version information. +* Sending a probe to the instance, causing the instance to respond with +information including the exact version number. This is the same method that +Nmap uses for service versioning; however, this script can also do the same for +instances accessiable via Windows named pipes, and can target all of the +instances listed by the SQL Server Browser service. + +In the event that the script can connect to the SQL Server Browser service +(UDP 1434) but is unable to connect directly to the instance to obtain more +accurate version information (because ports are blocked or the mssql.scanned-ports-only +argument has been used), the script will rely only upon the version number +provided by the SQL Server Browser/Monitor, which has the following limitations: +* For SQL Server 2000 and SQL Server 7.0 instances, the RTM version number is +always given, regardless of any service packs or patches installed. +* For SQL Server 2005 and later, the version number will reflect the service +pack installed, but the script will not be able to tell whether patches have +been installed. + +Where possible, the script will determine major version numbers, service pack +levels and whether patches have been installed. However, in cases where +particular determinations can not be made, the script will report only what can +be confirmed. + +NOTE: Communication with instances via named pipes depends on the smb +library. To communicate with (and possibly to discover) instances via named pipes, +the host must have at least one SMB port (e.g. TCP 445) that was scanned and +found to be open. Additionally, named pipe connections may require Windows +authentication to connect to the Windows host (via SMB) in addition to the +authentication required to connect to the SQL Server instances itself. See the +documentation and arguments for the smb library for more information. + +NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate +with ports that were not included in the port list for the Nmap scan. This can +be disabled using the mssql.scanned-ports-only script argument. ]] +--- +-- @usage +-- nmap -p 445 --script ms-sql-info +-- nmap -p 1433 --script ms-sql-info --script-args mssql.instance-port=1433 +-- +-- @output +-- | ms-sql-info: +-- | Windows server name: WINXP +-- | [192.168.100.128\PROD] +-- | Instance name: PROD +-- | Version: Microsoft SQL Server 2000 SP3 +-- | Version number: 8.00.760 +-- | Product: Microsoft SQL Server 2005 +-- | Service pack level: SP3 +-- | Post-SP patches applied: No +-- | TCP port: 1278 +-- | Named pipe: \\192.168.100.128\pipe\MSSQL$PROD\sql\query +-- | Clustered: No +-- | [192.168.100.128\SQLFIREWALLED] +-- | Instance name: SQLFIREWALLED +-- | Version: Microsoft SQL Server 2008 RTM +-- | Product: Microsoft SQL Server 2008 +-- | Service pack level: RTM +-- | TCP port: 4343 +-- | Clustered: No +-- | [\\192.168.100.128\pipe\sql\query] +-- | Version: Microsoft SQL Server 2005 SP3+ +-- | Version number: 9.00.4053 +-- | Product: Microsoft SQL Server 2005 +-- | Service pack level: SP3 +-- | Post-SP patches applied: Yes +-- |_ Named pipe: \\192.168.100.128\pipe\sql\query +-- + -- rev 1.0 (2007-06-09) -- rev 1.1 (2009-12-06 - Added SQL 2008 identification T Sellers) -- rev 1.2 (2010-10-03 - Added Broadcast support ) -- rev 1.3 (2010-10-10 - Added prerule and newtargets support ) +-- rev 1.4 (2011-01-24 - Revised logic in order to get version data without logging in; +-- added functionality to interpret version in terms of SP level, etc. +-- added script arg to prevent script from connecting to ports that +-- weren't in original Nmap scan ) +-- rev 1.5 (2011-02-01 - Moved discovery functionality into ms-sql-discover.nse and +-- broadcast-ms-sql-discovery.nse ) -author = "Thomas Buchanan" +author = "Chris Woodbury, Thomas Buchanan" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" -categories = {"default", "discovery", "intrusive"} +categories = {"default", "discovery", "safe"} ---- --- @output --- PORT STATE SERVICE REASON --- 1434/udp open ms-sql-m script-set --- | ms-sql-info: Discovered Microsoft SQL Server 2008 Express Edition --- | Server name: MAC-MINI --- | Server version: 10.0.2531.0 (SP1) --- | Instance name: SQLEXPRESS --- | TCP Port: 1433 --- |_ Could not retrieve actual version information --- +dependencies = {"ms-sql-discover"} require("shortport") -require("target") require("mssql") -prerule = function() return false end -portrule = shortport.portnumber({1433, 1434}, "udp", {"open", "open|filtered"}) - - -local parse_version = function(ver_str) - - local version = {} - version.full = ver_str - version.product_long = ver_str:match("(Microsoft.-)\n") or "" - version.product = ver_str:match("^(Microsoft SQL Server %w-)%s") or "" - version.edition = ver_str:match("\n.-\n.-\n%s*(.-%sEdition)%s") or "" - version.edition_long = ver_str:match("\n.-\n.-\n%s*(.-Build.-)\n") or "" - version.version = ver_str:match("^Microsoft.-%-.-([%.%d+]+)") or "" - version.level = ver_str:match("^Microsoft.-%((.+)%)%s%-") or "" - version.windows = ver_str:match(" on%s(.*)\n$") or "" - version.real = true - - return true, version -end - -local function retrieve_version_as_user( info, user, pass ) - - local helper, status - local SQL_DB = "master" - - if ( info.servername and info.port ) then - local hosts - status, hosts = nmap.resolve(info.servername, nmap.address_family()) - - if ( status ) then - local err - for _, host in ipairs( hosts ) do - helper = mssql.Helper:new() - status, err = helper:Connect(host, info.port) - if ( status ) then break end - end - -- we failed to connect to all of the resolved hostnames, - -- fall back to sql browser ip - if ( not(status) ) then - helper = mssql.Helper:new() - status, err = helper:Connect( info.ip, info.port ) - end - else - -- resolve wasn't successful, fall back to browser service ip - stdnse.print_debug(3, "ERROR: Failed to resolve the hostname %s", info.servername) - helper = mssql.Helper:new() - status, err = helper:Connect( info.ip, info.port ) - end +hostrule = function(host) + if ( mssql.Helper.WasDiscoveryPerformed( host ) ) then + return mssql.Helper.GetDiscoveredInstances( host ) ~= nil else - -- we're missing either the servername or the port - return false, "ERROR: Either servername or tcp port is missing" - end - - if ( not(status) ) then return false, "ERROR: Failed to connect to server" end - - status, result = helper:Login( user, pass, SQL_DB, info.servername ) - if ( not(status) ) then - stdnse.print_debug(3, "%s: login failed, reason: %s", SCRIPT_NAME, result ) - return status, "Could not retrieve actual version information" + return true end - - local query = "SELECT @@version ver" - status, result = helper:Query( query ) - if ( not(status) ) then - stdnse.print_debug(3, "%s: query failed, reason: %s", SCRIPT_NAME, result ) - return status, "Could not retrieve actual version information" - end - - helper:Disconnect() - - if ( result.rows ) then return parse_version( result.rows[1][1] ) end end -local function process_response( serverInfo ) - - local SQL_USER, SQL_PASS = "sa", "" - local TABLE_DATA = { - ["Server name"] = "info.servername", - ["Server version"] = "version.version", - ["Server edition"] = "version.edition_long", - ["Clustered"] = "info.clustered", - ["Named pipe"] = "info.pipe", - ["Tcp port"] = "info.port", - } - local result = {} +--- Adds a label and value to an output table. If the value is a boolean, it is +-- converted to Yes/No; if the value is nil, nothing is added to the table. +local function add_to_output_table( outputTable, outputLabel, outputData ) + if outputData == nil then return end - for _, info in pairs(serverInfo) do - local result_part = {} + if outputData == true then + outputData = "Yes" + elseif outputData == false then + outputData = "No" + end + + table.insert(outputTable, string.format( "%s: %s", outputLabel, outputData ) ) +end - -- The browser service could point to instances on other IP's - -- therefore the correct behavior should be to connect to the - -- servername returned for the instance rather than the browser IP. - -- In case this fails, due to name resolution or something else, fall - -- back to the browser service IP. - local status, version = retrieve_version_as_user(info, SQL_USER, SQL_PASS) - - if (status) then - if ( version.edition ) then - version.product = version.product .. " " .. version.edition - end - version.version = version.version .. (" (%s)"):format(version.level) + +--- Returns formatted output for the given version data +local function create_version_output_table( versionInfo ) + local versionOutput = {} + + versionOutput["name"] = "Version: " .. versionInfo:ToString() + if ( versionInfo.source ~= "SSRP" ) then + add_to_output_table( versionOutput, "Version number", versionInfo.versionNumber ) + end + add_to_output_table( versionOutput, "Product", versionInfo.productName ) + add_to_output_table( versionOutput, "Service pack level", versionInfo.servicePackLevel ) + add_to_output_table( versionOutput, "Post-SP patches applied", versionInfo.patched ) + + return versionOutput +end + + +--- Returns formatted output for the given instance +local function create_instance_output_table( instance ) + + -- if we didn't get anything useful (due to errors or the port not actually + -- being SQL Server), don't report anything + if not ( instance.instanceName or instance.version ) then return nil end + + local instanceOutput = {} + instanceOutput["name"] = string.format( "[%s]", instance:GetName() ) + + add_to_output_table( instanceOutput, "Instance name", instance.instanceName ) + if instance.version then + local versionOutput = create_version_output_table( instance.version ) + table.insert( instanceOutput, versionOutput ) + end + if instance.port then add_to_output_table( instanceOutput, "TCP port", instance.port.number ) end + add_to_output_table( instanceOutput, "Named pipe", instance.pipeName ) + add_to_output_table( instanceOutput, "Clustered", instance.isClustered ) + + return instanceOutput + +end + + +--- Processes a single instance, attempting to determine its version, etc. +local function process_instance( instance ) + + local foundVersion = false + local ssnetlibVersion + + -- If possible and allowed (see 'mssql.scanned-ports-only' argument), we'll + -- connect to the instance to get an accurate version number + if ( instance:HasNetworkProtocols() ) then + local ssnetlibVersion + foundVersion, ssnetlibVersion = mssql.Helper.GetInstanceVersion( instance ) + if ( foundVersion ) then + instance.version = ssnetlibVersion + stdnse.print_debug( 1, "%s: Retrieved SSNetLib version for %s.", SCRIPT_NAME, instance:GetName() ) else - status, version = mssql.Util.DecodeBrowserInfoVersion(info) + stdnse.print_debug( 1, "%s: Could not retrieve SSNetLib version for %s.", SCRIPT_NAME, instance:GetName() ) end + end + + -- If we didn't get a version from SSNetLib, give the user some detail as to why + if ( not foundVersion ) then + if ( not instance:HasNetworkProtocols() ) then + stdnse.print_debug( 1, "%s: %s has no network protocols enabled.", SCRIPT_NAME, instance:GetName() ) + end + if ( instance.version ) then + stdnse.print_debug( 1, "%s: Using version number from SSRP response for %s.", SCRIPT_NAME, instance:GetName() ) + else + stdnse.print_debug( 1, "%s: Version info could not be retrieved for %s.", SCRIPT_NAME, instance:GetName() ) + end + end + + -- 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 - -- format output - for topic, varname in pairs(TABLE_DATA) do - local func = loadstring( "return " .. varname ) - setfenv(func, setmetatable({ info=info; version=version; }, {__index = _G})) - local result = func() - if ( result ) then - table.insert( result_part, ("%s: %s"):format(topic, result) ) +end + + +action = function( host ) + local scriptOutput = {} + + local status, instanceList = mssql.Helper.GetTargetInstances( host ) + -- if no instances were targeted, then display info on all + if ( not status ) then + if ( not mssql.Helper.WasDiscoveryPerformed( host ) ) then + mssql.Helper.Discover( host ) + end + instanceList = mssql.Helper.GetDiscoveredInstances( host ) + end + + + if ( not instanceList ) then + return stdnse.format_output( false, instanceList or "" ) + else + for _, instance in ipairs( instanceList ) do + if instance.serverName then + table.insert(scriptOutput, string.format( "Windows server name: %s", instance.serverName )) + break end end - result_part.name = version.product - - if ( version.real ) then - table.insert(result_part, "WARNING: Database was accessible as SA with empty password!") + for _, instance in pairs( instanceList ) do + process_instance( instance ) + local instanceOutput = create_instance_output_table( instance ) + if instanceOutput then + table.insert( scriptOutput, instanceOutput ) + end end - - table.insert(result, { name = "Instance: " .. info.name, result_part } ) end - return result + + return stdnse.format_output( true, scriptOutput ) end - -action = function( host, port ) - - local status, response = mssql.Helper.Discover( host, port ) - if ( not(status) ) then return end - - local result, serverInfo = process_response( response[host.ip] ) - if ( not(result) ) then return end - - nmap.set_port_state( host, port, "open") - return stdnse.format_output( true, result ) -end - - diff --git a/scripts/ms-sql-query.nse b/scripts/ms-sql-query.nse index fe8954eff..fa07ee216 100644 --- a/scripts/ms-sql-query.nse +++ b/scripts/ms-sql-query.nse @@ -1,7 +1,54 @@ +-- -*- mode: lua -*- +-- vim: set filetype=lua : + description = [[ Runs a query against Microsoft SQL Server (ms-sql). + +SQL Server credentials required: Yes (use ms-sql-brute, ms-sql-empty-password +and/or mssql.username & mssql.password) +Run criteria: +* Host script: Will run if the mssql.instance-all, mssql.instance-name +or mssql.instance-port script arguments are used (see mssql.lua). +* Port script: Will run against any services identified as SQL Servers, but only +if the mssql.instance-all, mssql.instance-name +and mssql.instance-port script arguments are NOT used. + +NOTE: Communication with instances via named pipes depends on the smb +library. To communicate with (and possibly to discover) instances via named pipes, +the host must have at least one SMB port (e.g. TCP 445) that was scanned and +found to be open. Additionally, named pipe connections may require Windows +authentication to connect to the Windows host (via SMB) in addition to the +authentication required to connect to the SQL Server instances itself. See the +documentation and arguments for the smb library for more information. + +NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate +with ports that were not included in the port list for the Nmap scan. This can +be disabled using the mssql.scanned-ports-only script argument. ]] +--- +-- @usage +-- nmap -p 1433 --script ms-sql-query --script-args mssql.username=sa,mssql.password=sa,ms-sql-query.query="SELECT * FROM master..syslogins" +-- +-- @args ms-sql-query.query The query to run against the server. +-- (default: SELECT @@version version) +-- +-- @output +-- | ms-sql-query: +-- | [192.168.100.25\MSSQLSERVER] +-- | Query: SELECT @@version version +-- | version +-- | ======= +-- | Microsoft SQL Server 2005 - 9.00.3068.00 (Intel X86) +-- | Feb 26 2008 18:15:01 +-- | Copyright (c) 1988-2005 Microsoft Corporation +-- |_ Express Edition on Windows NT 5.2 (Build 3790: Service Pack 2) +-- + +-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson +-- Revised 02/01/2011 - v0.2 - Added ability to run against all instances on a host; +-- added compatibility with changes in mssql.lua (Chris Woodbury) + author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"discovery", "safe"} @@ -10,84 +57,62 @@ require 'shortport' require 'stdnse' require 'mssql' -dependencies = {"ms-sql-brute", "ms-sql-empty-password"} +dependencies = {"ms-sql-brute", "ms-sql-empty-password", "ms-sql-discover"} ---- --- @args ms-sql-query.query specifies the query to run against the server. --- (default SELECT @@version version) --- --- @output --- PORT STATE SERVICE --- 1433/tcp open ms-sql-s --- | ms-sql-query: --- | --- | Microsoft SQL Server 2005 - 9.00.3068.00 (Intel X86) --- | Feb 26 2008 18:15:01 --- | Copyright (c) 1988-2005 Microsoft Corporation --- |_ Express Edition on Windows NT 5.2 (Build 3790: Service Pack 2) +hostrule = mssql.Helper.GetHostrule_Standard() +portrule = mssql.Helper.GetPortrule_Standard() --- Version 0.1 --- Created 01/17/2010 - v0.1 - created by Patrik Karlsson - -portrule = shortport.port_or_service(1433, "ms-sql-s") - -action = function( host, port ) - - local status, result, helper - local username = stdnse.get_script_args( 'mssql.username' ) - local password = stdnse.get_script_args( 'mssql.password' ) or "" +--- +local function process_instance( instance ) + local status, result -- the tempdb should be a safe guess, anyway the library is set up -- to continue even if the DB is not accessible to the user local database = stdnse.get_script_args( 'mssql.database' ) or "tempdb" local query = stdnse.get_script_args( {'ms-sql-query.query', 'mssql-query.query' } ) or "SELECT @@version version" + local helper = mssql.Helper:new() + + status, result = helper:ConnectEx( instance ) - if ( not(username) and nmap.registry.mssqlusers ) then - -- do we have a sysadmin? - if ( nmap.registry.mssqlusers.sa ) then - username = "sa" - password = nmap.registry.mssqlusers.sa - else - -- ok were stuck with some n00b account, just get the first one - for user, pass in pairs(nmap.registry.mssqlusers) do - username = user - password = pass - break + if status then + status, result = helper:LoginEx( instance, database ) + if ( not(status) ) then result = "ERROR: " .. result end + end + if status then + status, result = helper:Query( query ) + if ( not(status) ) then result = "ERROR: " .. result end + end + + helper:Disconnect() + + if status then + result = mssql.Util.FormatOutputTable( result, true ) + result["name"] = string.format( "Query: %s", query ) + end + local instanceOutput = {} + instanceOutput["name"] = string.format( "[%s]", instance:GetName() ) + table.insert( instanceOutput, result ) + + return instanceOutput +end + + +action = function( host, port ) + local scriptOutput = {} + local status, instanceList = mssql.Helper.GetTargetInstances( host, port ) + + if ( not status ) then + return stdnse.format_output( false, instanceList ) + else + for _, instance in pairs( instanceList ) do + local instanceOutput = process_instance( instance ) + if instanceOutput then + table.insert( scriptOutput, instanceOutput ) end end + if ( not( stdnse.get_script_args( {'ms-sql-query.query', 'mssql-query.query' } ) ) ) then + table.insert(scriptOutput, 1, "(Use --script-args=ms-sql-query.query='' to change query.)") + end end - -- If we don't have a valid username, simply fail silently - if ( not(username) ) then - return - end - - helper = mssql.Helper:new() - status, result = helper:Connect(host, port) - if ( not(status) ) then - return " \n\n" .. result - end - - status, result = helper:Login( username, password, database, host.ip ) - if ( not(status) ) then - return " \n\nERROR: " .. result - end - - status, result = helper:Query( query ) - helper:Disconnect() - - if ( not(status) ) then - return " \n\nERROR: " .. result - end - - result = mssql.Util.FormatOutputTable( result, true ) - if ( not(nmap.registry.args['mssql-query.query']) ) then - table.insert(result, 1, query) - result = stdnse.format_output( true, result ) - result = "(Use --script-args=mssql-query.query='' to change query.)" .. result - else - result = stdnse.format_output( true, result ) - end - - return result - + return stdnse.format_output( true, scriptOutput ) end diff --git a/scripts/ms-sql-tables.nse b/scripts/ms-sql-tables.nse index f4252dfb4..5d39745be 100644 --- a/scripts/ms-sql-tables.nse +++ b/scripts/ms-sql-tables.nse @@ -1,10 +1,19 @@ +-- -*- mode: lua -*- +-- vim: set filetype=lua : + description = [[ Queries Microsoft SQL Server (ms-sql) for a list of tables per database. -The sysdatabase table should be accessible by more or less everyone -The script attempts to use the sa account over any other if it has -the password in the registry. If not the first account in the -registry is used. +SQL Server credentials required: Yes (use ms-sql-brute, ms-sql-empty-password +and/or mssql.username & mssql.password) +Run criteria: +* Host script: Will run if the mssql.instance-all, mssql.instance-name +or mssql.instance-port script arguments are used (see mssql.lua). +* Port script: Will run against any services identified as SQL Servers, but only +if the mssql.instance-all, mssql.instance-name +and mssql.instance-port script arguments are NOT used. + +The sysdatabase table should be accessible by more or less everyone. Once we have a list of databases we iterate over it and attempt to extract table names. In order for this to succeed we need to have either @@ -13,27 +22,24 @@ database we successfully enumerate tables from we mark as finished, then iterate over known user accounts until either we have exhausted the users or found all tables in all the databases. -Tables installed by default are excluded. +System databases are excluded. + +NOTE: Communication with instances via named pipes depends on the smb +library. To communicate with (and possibly to discover) instances via named pipes, +the host must have at least one SMB port (e.g. TCP 445) that was scanned and +found to be open. Additionally, named pipe connections may require Windows +authentication to connect to the Windows host (via SMB) in addition to the +authentication required to connect to the SQL Server instances itself. See the +documentation and arguments for the smb library for more information. + +NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate +with ports that were not included in the port list for the Nmap scan. This can +be disabled using the mssql.scanned-ports-only script argument. ]] -author = "Patrik Karlsson" -license = "Same as Nmap--See http://nmap.org/book/man-legal.html" -categories = {"discovery", "safe"} - -require 'shortport' -require 'stdnse' -require 'mssql' - -dependencies = {"ms-sql-brute", "ms-sql-empty-password"} - --- --- @args mssql.username specifies the username to use to connect to --- the server. This option overrides any accounts found by --- the ms-sql-brute and ms-sql-empty-password scripts. --- --- @args mssql.password specifies the password to use to connect to --- the server. This option overrides any accounts found by --- the ms-sql-brute and ms-sql-empty-password scripts. +-- @usage +-- nmap -p 1433 --script ms-sql-tables --script-args mssql.username=sa,mssql.password=sa -- -- @args ms-sql-tables.maxdb Limits the amount of databases that are -- processed and returned (default 5). If set to zero or less @@ -46,9 +52,8 @@ dependencies = {"ms-sql-brute", "ms-sql-empty-password"} -- the keywords -- -- @output --- PORT STATE SERVICE --- 1433/tcp open ms-sql-s -- | ms-sql-tables: +-- | [192.168.100.25\MSSQLSERVER] -- | webshop -- | table column type length -- | payments user_id int 4 @@ -72,15 +77,28 @@ dependencies = {"ms-sql-brute", "ms-sql-empty-password"} -- | users password varchar 50 -- |_ users fullname varchar 100 --- Version 0.1 -- Created 01/17/2010 - v0.1 - created by Patrik Karlsson -- Revised 04/02/2010 - v0.2 -- - Added support for filters -- - Changed output formatting of restrictions -- - Added parameter information in output if parameters are using their -- defaults. +-- Revised 02/01/2011 - v0.3 (Chris Woodbury) +-- - Added ability to run against all instances on a host; +-- - Added compatibility with changes in mssql.lua -portrule = shortport.port_or_service(1433, "ms-sql-s") +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require 'shortport' +require 'stdnse' +require 'mssql' + +dependencies = {"ms-sql-brute", "ms-sql-empty-password", "ms-sql-discover"} + +hostrule = mssql.Helper.GetHostrule_Standard() +portrule = mssql.Helper.GetPortrule_Standard() local function table_contains( tbl, val ) for k,v in pairs(tbl) do @@ -91,17 +109,15 @@ local function table_contains( tbl, val ) return false end -action = function( host, port ) - local status, result, dbs, tables, helper - local username = stdnse.get_script_args( 'mssql.username' ) - local password = stdnse.get_script_args( 'mssql.password' ) or "" +local function process_instance( instance ) + + local status, result, dbs, tables local output = {} local exclude_dbs = { "'master'", "'tempdb'", "'model'", "'msdb'" } local db_query local done_dbs = {} - local creds = {} local db_limit, tbl_limit local DB_COUNT = stdnse.get_script_args( {'ms-sql-tables.maxdb', 'mssql-tables.maxdb'} ) @@ -122,8 +138,8 @@ action = function( host, port ) end -- Build the keyword filter - if ( nmap.registry.args['mssql-tables.keywords'] ) then - local keywords = nmap.registry.args['mssql-tables.keywords'] + if ( stdnse.get_script_args( {'ms-sql-tables.keywords', 'mssql-tables.keywords' } ) ) then + local keywords = stdnse.get_script_args( {'ms-sql-tables.keywords', 'mssql-tables.keywords' } ) local tmp_tbl = {} if( type(keywords) == 'string' ) then @@ -142,110 +158,123 @@ action = function( host, port ) db_query = ("SELECT %s name from master..sysdatabases WHERE name NOT IN (%s)"):format(db_limit, stdnse.strjoin(",", exclude_dbs)) - if ( username ) then - creds[username] = password - elseif ( not(username) and nmap.registry.mssqlusers ) then - -- do we have a sysadmin? - if ( nmap.registry.mssqlusers.sa ) then - creds["sa"] = nmap.registry.mssqlusers.sa - else - creds = nmap.registry.mssqlusers - end - end - - -- If we don't have valid creds, simply fail silently - if ( not(creds) ) then - return - end - - for username, password in pairs( creds ) do - helper = mssql.Helper:new() - status, result = helper:Connect(host, port) - if ( not(status) ) then - return " \n\n" .. result - end - - status, result = helper:Login( username, password, nil, host.ip ) - if ( not(status) ) then - stdnse.print_debug("ERROR: %s", result) - break - end - status, dbs = helper:Query( db_query ) - - if ( status ) then - -- all done? - if ( #done_dbs == #dbs.rows ) then + local creds = mssql.Helper.GetLoginCredentials_All( instance ) + if ( not creds ) then + output = "ERROR: No login credentials." + else + for username, password in pairs( creds ) do + local helper = mssql.Helper:new() + status, result = helper:ConnectEx( instance ) + if ( not(status) ) then + table.insert(output, "ERROR: " .. result) break end - - for k, v in pairs(dbs.rows) do - if ( not( table_contains( done_dbs, v[1] ) ) ) then - query = [[ SELECT so.name 'table', sc.name 'column', st.name 'type', sc.length - FROM %s..syscolumns sc, %s..sysobjects so, %s..systypes st - WHERE so.id = sc.id AND sc.xtype=st.xtype AND - so.id IN (SELECT %s id FROM %s..sysobjects WHERE xtype='U') %s ORDER BY so.name, sc.name, st.name]] - query = query:format( v[1], v[1], v[1], tbl_limit, v[1], keywords_filter) - status, tables = helper:Query( query ) - if ( not(status) ) then - stdnse.print_debug(tables) - else - local item = {} - item = mssql.Util.FormatOutputTable( tables, true ) - if ( #item == 0 and keywords_filter ~= "" ) then - table.insert(item, "Filter returned no matches") + + if ( status ) then + status = helper:Login( username, password, nil, instance.host.ip ) + end + + if ( status ) then + status, dbs = helper:Query( db_query ) + end + + if ( status ) then + -- all done? + if ( #done_dbs == #dbs.rows ) then + break + end + + for k, v in pairs(dbs.rows) do + if ( not( table_contains( done_dbs, v[1] ) ) ) then + query = [[ SELECT so.name 'table', sc.name 'column', st.name 'type', sc.length + FROM %s..syscolumns sc, %s..sysobjects so, %s..systypes st + WHERE so.id = sc.id AND sc.xtype=st.xtype AND + so.id IN (SELECT %s id FROM %s..sysobjects WHERE xtype='U') %s ORDER BY so.name, sc.name, st.name]] + query = query:format( v[1], v[1], v[1], tbl_limit, v[1], keywords_filter) + status, tables = helper:Query( query ) + if ( not(status) ) then + stdnse.print_debug(tables) + else + local item = {} + item = mssql.Util.FormatOutputTable( tables, true ) + if ( #item == 0 and keywords_filter ~= "" ) then + table.insert(item, "Filter returned no matches") + end + item.name = v[1] + + table.insert(output, item) + table.insert(done_dbs, v[1]) end - item.name = v[1] - - table.insert(output, item) - table.insert(done_dbs, v[1]) end end end - end - helper:Disconnect() - end + helper:Disconnect() + end - local pos = 1 - local restrict_tbl = {} - - if ( stdnse.get_script_args( {'ms-sql-tables.keywords', 'mssql-tables.keywords' } ) ) then - tmp = stdnse.get_script_args( {'ms-sql-tables.keywords', 'mssql-tables.keywords' } ) - if ( type(tmp) == 'table' ) then - tmp = stdnse.strjoin(',', tmp) - end - table.insert(restrict_tbl, 1, ("Filter: %s"):format(tmp)) - pos = pos + 1 - else - table.insert(restrict_tbl, 1, "No filter (see ms-sql-tables.keywords)") - end - - if ( DB_COUNT > 0 ) then - local tmp = ("Output restricted to %d databases"):format(DB_COUNT) - if ( not(stdnse.get_script_args( { 'ms-sql-tables.maxdb', 'mssql-tables.maxdb' } ) ) ) then - tmp = tmp .. " (see ms-sql-tables.maxdb)" - end - table.insert(restrict_tbl, 1, tmp) - pos = pos + 1 - end - - if ( TABLE_COUNT > 0 ) then - local tmp = ("Output restricted to %d tables"):format(TABLE_COUNT) - if ( not(stdnse.get_script_args( { 'ms-sql-tables.maxtables', 'mssql-tables.maxtables' } ) ) ) then - tmp = tmp .. " (see ms-sql-tables.maxtables)" - end - table.insert(restrict_tbl, 1, tmp) - pos = pos + 1 - end - - if ( 1 < pos and #output > 0) then - restrict_tbl.name = "Restrictions" - table.insert(output, "") - table.insert(output, restrict_tbl) - end - - output = stdnse.format_output( true, output ) + local pos = 1 + local restrict_tbl = {} - return output + if ( stdnse.get_script_args( {'ms-sql-tables.keywords', 'mssql-tables.keywords' } ) ) then + tmp = stdnse.get_script_args( {'ms-sql-tables.keywords', 'mssql-tables.keywords' } ) + if ( type(tmp) == 'table' ) then + tmp = stdnse.strjoin(',', tmp) + end + table.insert(restrict_tbl, 1, ("Filter: %s"):format(tmp)) + pos = pos + 1 + else + table.insert(restrict_tbl, 1, "No filter (see ms-sql-tables.keywords)") + end + + if ( DB_COUNT > 0 ) then + local tmp = ("Output restricted to %d databases"):format(DB_COUNT) + if ( not(stdnse.get_script_args( { 'ms-sql-tables.maxdb', 'mssql-tables.maxdb' } ) ) ) then + tmp = tmp .. " (see ms-sql-tables.maxdb)" + end + table.insert(restrict_tbl, 1, tmp) + pos = pos + 1 + end + + if ( TABLE_COUNT > 0 ) then + local tmp = ("Output restricted to %d tables"):format(TABLE_COUNT) + if ( not(stdnse.get_script_args( { 'ms-sql-tables.maxtables', 'mssql-tables.maxtables' } ) ) ) then + tmp = tmp .. " (see ms-sql-tables.maxtables)" + end + table.insert(restrict_tbl, 1, tmp) + pos = pos + 1 + end + + if ( 1 < pos and type( output ) == "table" and #output > 0) then + restrict_tbl.name = "Restrictions" + table.insert(output, "") + table.insert(output, restrict_tbl) + end + end + + + local instanceOutput = {} + instanceOutput["name"] = string.format( "[%s]", instance:GetName() ) + table.insert( instanceOutput, output ) + + return instanceOutput end + + +action = function( host, port ) + local scriptOutput = {} + local status, instanceList = mssql.Helper.GetTargetInstances( host, port ) + + if ( not status ) then + return stdnse.format_output( false, instanceList ) + else + for _, instance in pairs( instanceList ) do + local instanceOutput = process_instance( instance ) + if instanceOutput then + table.insert( scriptOutput, instanceOutput ) + end + end + end + + return stdnse.format_output( true, scriptOutput ) +end diff --git a/scripts/ms-sql-xp-cmdshell.nse b/scripts/ms-sql-xp-cmdshell.nse index 79ca1f12e..305f59043 100644 --- a/scripts/ms-sql-xp-cmdshell.nse +++ b/scripts/ms-sql-xp-cmdshell.nse @@ -1,17 +1,80 @@ +-- -*- mode: lua -*- +-- vim: set filetype=lua : + description = [[ Attempts to run a command using the command shell of Microsoft SQL Server (ms-sql). +SQL Server credentials required: Yes (use ms-sql-brute, ms-sql-empty-password +and/or mssql.username & mssql.password) +Run criteria: +* Host script: Will run if the mssql.instance-all, mssql.instance-name +or mssql.instance-port script arguments are used (see mssql.lua). +* Port script: Will run against any services identified as SQL Servers, but only +if the mssql.instance-all, mssql.instance-name +and mssql.instance-port script arguments are NOT used. + The script needs an account with the sysadmin server role to work. -It needs to be fed credentials through the script arguments or from -the scripts ms-sql-brute or -ms-sql-empty-password. When run, the script iterates over the credentials and attempts to run the command until either all credentials are exhausted or until the command is executed. + +NOTE: Communication with instances via named pipes depends on the smb +library. To communicate with (and possibly to discover) instances via named pipes, +the host must have at least one SMB port (e.g. TCP 445) that was scanned and +found to be open. Additionally, named pipe connections may require Windows +authentication to connect to the Windows host (via SMB) in addition to the +authentication required to connect to the SQL Server instances itself. See the +documentation and arguments for the smb library for more information. + +NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate +with ports that were not included in the port list for the Nmap scan. This can +be disabled using the mssql.scanned-ports-only script argument. ]] +--- +-- @usage +-- nmap -p 445 --script ms-sql-discover,ms-sql-empty-password,ms-sql-xp-cmdshell +-- nmap -p 1433 --script ms-sql-xp-cmdshell --script-args mssql.username=sa,mssql.password=sa,ms-sql-xp-cmdshell.cmd="net user test test /add" +-- +-- @args ms-sql-xp-cmdshell.cmd The OS command to run (default: ipconfig /all). +-- +-- @output +-- | ms-sql-xp-cmdshell: +-- | [192.168.56.3\MSSQLSERVER] +-- | Command: ipconfig /all +-- | output +-- | ====== +-- | +-- | Windows IP Configuration +-- | +-- | Host Name . . . . . . . . . . . . : EDUSRV011 +-- | Primary Dns Suffix . . . . . . . : cqure.net +-- | Node Type . . . . . . . . . . . . : Unknown +-- | IP Routing Enabled. . . . . . . . : No +-- | WINS Proxy Enabled. . . . . . . . : No +-- | DNS Suffix Search List. . . . . . : cqure.net +-- | +-- | Ethernet adapter Local Area Connection 3: +-- | +-- | Connection-specific DNS Suffix . : +-- | Description . . . . . . . . . . . : AMD PCNET Family PCI Ethernet Adapter #2 +-- | Physical Address. . . . . . . . . : 08-00-DE-AD-C0-DE +-- | DHCP Enabled. . . . . . . . . . . : Yes +-- | Autoconfiguration Enabled . . . . : Yes +-- | IP Address. . . . . . . . . . . . : 192.168.56.3 +-- | Subnet Mask . . . . . . . . . . . : 255.255.255.0 +-- | Default Gateway . . . . . . . . . : +-- | DHCP Server . . . . . . . . . . . : 192.168.56.2 +-- | Lease Obtained. . . . . . . . . . : den 21 mars 2010 00:12:10 +-- | Lease Expires . . . . . . . . . . : den 21 mars 2010 01:12:10 +-- |_ + +-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson +-- Revised 02/01/2011 - v0.2 - Added ability to run against all instances on a host; +-- added compatibility with changes in mssql.lua (Chris Woodbury) + author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"intrusive"} @@ -20,128 +83,81 @@ require 'shortport' require 'stdnse' require 'mssql' -dependencies = {"ms-sql-brute", "ms-sql-empty-password"} ---- --- @args mssql.username specifies the username to use to connect to --- the server. This option overrides any accounts found by --- the ms-sql-brute and ms-sql-empty-password scripts. --- --- @args mssql.password specifies the password to use to connect to --- the server. This option overrides any accounts found by --- the ms-sql-brute and ms-sql-empty-password scripts. --- --- @args ms-sql-xp-cmdshell.cmd specifies the OS command to run. --- (default is ipconfig /all) --- --- @output --- PORT STATE SERVICE --- 1433/tcp open ms-sql-s --- | ms-sql-xp-cmdshell: --- | Command: ipconfig /all; User: sa --- | output --- | --- | Windows IP Configuration --- | --- | Host Name . . . . . . . . . . . . : EDUSRV011 --- | Primary Dns Suffix . . . . . . . : cqure.net --- | Node Type . . . . . . . . . . . . : Unknown --- | IP Routing Enabled. . . . . . . . : No --- | WINS Proxy Enabled. . . . . . . . : No --- | DNS Suffix Search List. . . . . . : cqure.net --- | --- | Ethernet adapter Local Area Connection 3: --- | --- | Connection-specific DNS Suffix . : --- | Description . . . . . . . . . . . : AMD PCNET Family PCI Ethernet Adapter #2 --- | Physical Address. . . . . . . . . : 08-00-DE-AD-C0-DE --- | DHCP Enabled. . . . . . . . . . . : Yes --- | Autoconfiguration Enabled . . . . : Yes --- | IP Address. . . . . . . . . . . . : 192.168.56.3 --- | Subnet Mask . . . . . . . . . . . : 255.255.255.0 --- | Default Gateway . . . . . . . . . : --- | DHCP Server . . . . . . . . . . . : 192.168.56.2 --- | Lease Obtained. . . . . . . . . . : den 21 mars 2010 00:12:10 --- | Lease Expires . . . . . . . . . . : den 21 mars 2010 01:12:10 --- |_ +dependencies = {"ms-sql-brute", "ms-sql-empty-password", "ms-sql-discover"} --- Version 0.1 --- Created 01/17/2010 - v0.1 - created by Patrik Karlsson +hostrule = mssql.Helper.GetHostrule_Standard() +portrule = mssql.Helper.GetPortrule_Standard() -portrule = shortport.port_or_service(1433, "ms-sql-s") -local function table_contains( tbl, val ) - for k,v in pairs(tbl) do - if ( v == val ) then - return true - end - end - return false -end +local function process_instance( instance ) -action = function( host, port ) - - local status, result, helper - local username = stdnse.get_script_args( 'mssql.username' ) - local password = stdnse.get_script_args( 'mssql.password' ) or "" - local creds + local status, result local query local cmd = stdnse.get_script_args( {'ms-sql-xp-cmdshell.cmd', 'mssql-xp-cmdshell.cmd'} ) or 'ipconfig /all' local output = {} query = ("EXEC master..xp_cmdshell '%s'"):format(cmd) - if ( username ) then - creds = {} - creds[username] = password - elseif ( not(username) and nmap.registry.mssqlusers ) then - -- do we have a sysadmin? - creds = {} - if ( nmap.registry.mssqlusers.sa ) then - creds["sa"] = nmap.registry.mssqlusers.sa - else - creds = nmap.registry.mssqlusers - end + local creds = mssql.Helper.GetLoginCredentials_All( instance ) + if ( not creds ) then + output = "ERROR: No login credentials." + else + for username, password in pairs( creds ) do + local helper = mssql.Helper:new() + status, result = helper:ConnectEx( instance ) + if ( not(status) ) then + output = "ERROR: " .. result + break + end + + if ( status ) then + status = helper:Login( username, password, nil, instance.host.ip ) + end + + if ( status ) then + status, result = helper:Query( query ) + end + helper:Disconnect() + + if ( status ) then + output = mssql.Util.FormatOutputTable( result, true ) + output[ "name" ] = string.format( "Command: %s", cmd ) + break + elseif ( result and result:gmatch("xp_configure") ) then + if( nmap.verbosity() > 1 ) then + output = "Procedure xp_cmdshell disabled. For more information see \"Surface Area Configuration\" in Books Online." + end + end + end end - -- If we don't have valid creds, simply fail silently - if ( not(creds) ) then - return - end + local instanceOutput = {} + instanceOutput["name"] = string.format( "[%s]", instance:GetName() ) + table.insert( instanceOutput, output ) - for username, password in pairs( creds ) do - helper = mssql.Helper:new() - status, result = helper:Connect(host, port) - if ( not(status) ) then - return " \n\n" .. result - end - - status, result = helper:Login( username, password, nil, host.ip ) - if ( not(status) ) then - stdnse.print_debug("ERROR: %s", result) - break - end - - status, result = helper:Query( query ) - helper:Disconnect() - - if ( status ) then - output = mssql.Util.FormatOutputTable( result, true ) - if ( not(stdnse.get_script_args( {'ms-sql-xp-cmdshell.cmd', 'mssql-xp-cmdshell.cmd'} ) ) ) then - table.insert(output, 1, cmd) - output = stdnse.format_output( true, output ) - output = "(Use --script-args=ms-sql-xp-cmdshell.cmd='' to change command.)" .. output - else - output = stdnse.format_output( true, output ) - end - - break - elseif ( result:gmatch("xp_configure") ) then - if( nmap.verbosity() > 1 ) then - return " \nProcedure xp_cmdshell disabled, for more information see \"Surface Area Configuration\" in Books Online." - end - end - end - - return output + return instanceOutput end + + +action = function( host, port ) + local scriptOutput = {} + local status, instanceList = mssql.Helper.GetTargetInstances( host, port ) + + if ( not status ) then + return stdnse.format_output( false, instanceList ) + else + for _, instance in pairs( instanceList ) do + local instanceOutput = process_instance( instance ) + if instanceOutput then + table.insert( scriptOutput, instanceOutput ) + end + end + + if ( not(stdnse.get_script_args( {'ms-sql-xp-cmdshell.cmd', 'mssql-xp-cmdshell.cmd'} ) ) ) then + table.insert(scriptOutput, 1, "(Use --script-args=ms-sql-xp-cmdshell.cmd='' to change command.)") + end + end + + return stdnse.format_output( true, scriptOutput ) +end