From 1d26975ede05ffd0d17f8fa4e1784cfca5e62eab Mon Sep 17 00:00:00 2001 From: patrik Date: Sun, 4 Apr 2010 10:11:54 +0000 Subject: [PATCH] o [NSE] Added a library for Microsoft SQL Server and 7 new scripts. The new scripts are: - ms-sql-brute.nse uses the unpwdb library to guess credentials for MSSQL - ms-sql-config retrieves various configuration details from the server - ms-sql-empty-password checks if the sa account has an empty password - ms-sql-hasdbaccess lists database access per user - ms-sql-query add support for running custom queries against the database - ms-sql-tables lists databases, tables, columns and datatypes with optional keyword filtering - ms-sql-xp-cmdshell adds support for OS command execution to privileged users [Patrik] --- CHANGELOG | 13 + nselib/mssql.lua | 1030 +++++++++++++++++++++++++++++ scripts/ms-sql-brute.nse | 93 +++ scripts/ms-sql-config.nse | 109 +++ scripts/ms-sql-empty-password.nse | 52 ++ scripts/ms-sql-hasdbaccess.nse | 143 ++++ scripts/ms-sql-query.nse | 96 +++ scripts/ms-sql-tables.nse | 251 +++++++ scripts/ms-sql-xp-cmdshell.nse | 146 ++++ scripts/script.db | 7 + 10 files changed, 1940 insertions(+) create mode 100644 nselib/mssql.lua create mode 100644 scripts/ms-sql-brute.nse create mode 100644 scripts/ms-sql-config.nse create mode 100644 scripts/ms-sql-empty-password.nse create mode 100644 scripts/ms-sql-hasdbaccess.nse create mode 100644 scripts/ms-sql-query.nse create mode 100644 scripts/ms-sql-tables.nse create mode 100644 scripts/ms-sql-xp-cmdshell.nse diff --git a/CHANGELOG b/CHANGELOG index b147eea09..b806c7138 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,18 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a library for Microsoft SQL Server and 7 new scripts. The new + scripts are: + - ms-sql-brute.nse uses the unpwdb library to guess credentials for MSSQL + - ms-sql-config retrieves various configuration details from the server + - ms-sql-empty-password checks if the sa account has an empty password + - ms-sql-hasdbaccess lists database access per user + - ms-sql-query add support for running custom queries against the database + - ms-sql-tables lists databases, tables, columns and datatypes with optional + keyword filtering + - ms-sql-xp-cmdshell adds support for OS command execution to privileged + users + [Patrik] + o [NSE] Fixed bug in rpc.lua library that incorrectly required file handles to be 32 octects when calling the ReadDir function. The bug was reported by Djalal Harouni. [Patrik] diff --git a/nselib/mssql.lua b/nselib/mssql.lua new file mode 100644 index 000000000..26b082e9c --- /dev/null +++ b/nselib/mssql.lua @@ -0,0 +1,1030 @@ +--- MSSQL Library supporting a very limited subset of operations +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- +-- @author = "Patrik Karlsson " +-- +-- Summary +-- ------- +-- The library was designed and tested against Microsoft SQL Server 2005. +-- However, it should work with versions 7.0, 2000, 2005 and 2008. +-- Only a minimal amount of parsers have been added for tokens, column types +-- and column data in order to support the first scripts. +-- +-- The code has been implemented based on traffic analysis and the following +-- documentation: +-- o TDS Protocol Documentation +-- http://www.freetds.org/tds.html +-- +-- o The JTDS source code +-- http://jtds.sourceforge.net/index.html +-- +-- Overview +-- -------- +-- o ColumInfo - Class containing parsers for column types which are present +-- before the row data in all query response packets. The column +-- information contains information relevant to the data type +-- used to hold the data eg. precision, character sets, size etc. +-- +-- o ColumnData - Class containing parsers for the actual column information +-- +-- o Token - Class containing parsers for tokens returned in all TDS responses. +-- A server response may hold one or more tokens with information +-- from the server. Each token has a type which has a number of +-- type specific fields. +-- +-- o QueryPacket - Class used to hold a query and convert it to a string +-- suitable for transmission over a socket. +-- +-- o LoginPacket - Class used to hold login specific data which can easily +-- be converted to a string suitable for transmission over +-- a socket. +-- +-- o TDSStream - Class that handles communication over the Tabular Data Stream +-- protocol used by SQL serve. It is used to transmit the the +-- Query- and Login-packets to the server. +-- +-- o Helper - Class which facilitates the use of the library by through action +-- oriented functions with descriptive names. +-- +-- o Util - "static" class containing mostly character and type conversion +-- functions. +-- +-- Example +-- ------- +-- The following sample code illustrates how scripts can use the Helper class +-- to interface the library: +-- +-- +-- local helper = mssql.Helper:new() +-- status, result = helper:Login( username, password, "temdpb", host.ip ) +-- status, result = helper:Query( "SELECT name FROM master..syslogins") +-- helper:Disconnect() +-- +-- +-- Known limitations +-- ----------------- +-- o The library does not support SSL. The foremost reason being the akward +-- choice of implementation where the SSL handshake is performed within +-- the TDS data block. By default, servers support connections over non +-- SSL connections though. +-- +-- o Version 7 and ONLY version 7 of the protocol is supported. This should +-- cover Microsoft SQL Server 7.0 and later. +-- +-- o TDS Responses contain one or more response tokens which are parsed based +-- on their type. The supported tokens are listed in the TokenType table and +-- their respective parsers can be found in the Token class. Note that some +-- token parsers are not fully implemented and simply move the offset the +-- right number of bytes to continue processing of the response. +-- +-- o The library only supports a limited subsets of datatypes and will abort +-- execution and return an error if it detects an unsupported type. The +-- supported data types are listed in the DataTypes table. In order to add +-- additional data types a parser function has to be added to both the +-- ColumnInfo and ColumnData class. +-- +-- o No functionality for languages, localization or characted codepages has +-- been considered or implemented. +-- +-- o The library does database authentication only. No OS authentication or +-- use of the integrated security model is supported. +-- +-- o Queries using SELECT, INSERT, DELETE and EXEC of procedures have been +-- tested while developing scripts. +-- +-- +-- +-- @args mssql.timeout Specifies the amount of seconds to wait for SQL +-- responses (default 30) + +-- +-- 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 +-- +-- + +module(... or "mssql", package.seeall) + +require("bit") +require("bin") + +MSSQL_TIMEOUT = ( nmap.registry.args and nmap.registry.args['mssql.timeout'] and tonumber(nmap.registry.args['mssql.timeout']) ) and tonumber(nmap.registry.args['mssql.timeout']) or 30 + +-- TDS packet types +PacketType = +{ + Query = 0x01, + Response = 0x04, + Login = 0x10, +} + +-- TDS response token types +TokenType = +{ + TDS7Results = 0x81, + ErrorMessage = 0xAA, + InformationMessage = 0xAB, + LoginAcknowledgement = 0xAD, + Row = 0xD1, + OrderBy = 0xA9, + EnvironmentChange = 0xE3, + Done = 0xFD, + DoneInProc = 0xFF, +} + +-- SQL Server/Sybase data types +DataTypes = +{ + SYBINTN = 0x26, + SYBINT2 = 0x34, + SYBINT4 = 0x38, + SYBDATETIME = 0x3D, + SYBDATETIMN = 0x6F, + XSYBVARBINARY = 0xA5, + XSYBVARCHAR = 0xA7, + XSYBNVARCHAR = 0xE7, +} + +-- "static" ColumInfo parser class +ColumnInfo = +{ + + Parse = + { + [DataTypes.XSYBNVARCHAR] = function( data, pos ) + local colinfo = {} + local tmp + + pos, colinfo.lts, colinfo.codepage, colinfo.flags, colinfo.charset, + colinfo.msglen = bin.unpack(" issued + -- 0x10 enable BCP + + -- options_2 possible values + -- 0x80 enable domain login security + -- 0x40 "USER_SERVER - reserved" + -- 0x20 user type is "DQ login" + -- 0x10 user type is "replication login" + -- 0x08 "fCacheConnect" + -- 0x04 "fTranBoundary" + -- 0x02 client is an ODBC driver + -- 0x01 change to initial language must succeed + length = 0, + version = 0x71000001, -- Version 7.1 + size = 0, + cli_version = 7, -- From jTDS JDBC driver + cli_pid = 0, -- Dummy value + conn_id = 0, + options_1 = 0xa0, + options_2 = 0x03, + sqltype_flag = 0, + reserved_flag= 0, + time_zone = 0, + collation = 0, + + -- Strings + client = "Nmap", + username = nil, + password = nil, + app = "Nmap NSE", + server = nil, + library = "mssql.lua", + locale = "", + database = "master", --nil, + MAC = string.char(0x00,0x00,0x00,0x00,0x00,0x00), -- should contain client MAC, jTDS uses all zeroes + + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, + + --- Sets the username used for authentication + -- + -- @param username string containing the username to user for authentication + SetUsername = function(self, username) + self.username = username + end, + + --- Sets the password used for authentication + -- + -- @param password string containing the password to user for authentication + SetPassword = function(self, password) + self.password = password + end, + + --- Sets the database used in authentication + -- + -- @param database string containing the database name + SetDatabase = function(self, database) + self.database = database + end, + + --- Sets the server's name used in authentication + -- + -- @param server string containing the name or ip of the server + SetServer = function(self, server) + self.server = server + end, + + --- Returns the authentication packet as string + -- + -- @return string containing the authentication packet + ToString = function(self) + local data + local offset = 86 + + self.cli_pid = math.random(100000) + + self.length = offset + 2 * ( self.client:len() + self.username:len() + self.password:len() + + self.app:len() + self.server:len() + self.library:len() + self.database:len() ) + + data = bin.pack("CCSSCCA", pkt_type, last, len, channel, self.packetno, window, data ) + return self.socket:send( packet ) + end, + + --- Recieves responses from SQL Server + -- The function continues to read and assemble a response until the server + -- responds with the last response flag set + -- + -- @return status true on success, false on failure + -- @return result containing raw data contents or error message on failure + Receive = function( self ) + local status + local pkt_type, last, size, channel, packet_no, window, tmp, needed + local data, response = "", "" + local pos = 1 + + repeat + if( response:len() - pos < 4 ) then + status, tmp = self.socket:receive_bytes(4) + response = response .. tmp + end + + if ( not(status) ) then + return false, "Failed to receive packet from MSSQL server" + end + + pos, pkt_type, last, size = bin.unpack(">CCS", response, pos ) + if ( pkt_type ~= PacketType.Response ) then + return false, "Server returned invalid packet" + end + + needed = size - ( response:len() - pos + 5 ) + if ( needed > 0 ) then + status, tmp = self.socket:receive_bytes(needed) + if ( not(status) ) then + return false, "Failed to receive packet from MSSQL server" + end + response = response .. tmp + + end + pos, channel, packet_no, window, tmp = bin.unpack(">SccA" .. ( size - 8 ), response, pos) + data = data .. tmp + until last == 1 + + -- return only the data section ie. without the headers + return status, data + end, + +} + +--- Helper class +Helper = +{ + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, + + --- Establishes a connection to the SQL server + -- + -- @param host table containing host information + -- @param port table containing port information + -- @return status true on success, false on failure + -- @return result containing error message on failure + Connect = function( self, host, port ) + local status, result + self.stream = TDSStream:new() + status, result = self.stream:Connect(host, port) + if ( not(status) ) then + return false, result + end + + return true + end, + + --- Disconnects from the SQL Server + -- + -- @return status true on success, false on failure + -- @return result containing error message on failure + Disconnect = function( self ) + if ( not(self.stream) ) then + return false, "Not connected to server" + end + + self.stream:Disconnect() + self.stream = nil + + return true + end, + + --- Authenticates to SQL Server + -- + -- @param username string containing the username for authentication + -- @param password string containing the password for authentication + -- @param database string containing the database to access + -- @param servername string containing the name or ip of the remote server + -- @return status true on success, false on failure + -- @return result containing error message on failure + Login = function( self, username, password, database, servername ) + local loginPacket = LoginPacket:new() + local status, result, data, token + local servername = servername or "DUMMY" + local pos = 1 + + if ( nil == self.stream ) then + return false, "Not connected to server" + end + + loginPacket:SetUsername(username) + loginPacket:SetPassword(password) + loginPacket:SetDatabase(database) + loginPacket:SetServer(servername) + + status, result = self.stream:Send( loginPacket:ToString() ) + if ( not(status) ) then + return false, result + end + + status, data = self.stream:Receive() + if ( not(status) ) then + return false, data + end + + while( pos < data:len() ) do + pos, token = Token.ParseToken( data, pos ) + if ( -1 == pos ) then + return false, token + end + -- Let's check for user must change password, it appears as if this is + -- reported as ERROR 18488 + if ( token.type == TokenType.ErrorMessage and token.errno == 18488 ) then + return true, "Must change password at next logon" + elseif ( token.type == TokenType.LoginAcknowledgement ) then + return true, "Login Success" + end + end + + return false, "Login Failed" + end, + + --- Performs a SQL query and parses the response + -- + -- @param query string containing the SQL query + -- @return status true on success, false on failure + -- @return table containing a table of columns for each row + -- or error message on failure + Query = function( self, query ) + + local queryPacket = QueryPacket:new() + local status, result, data, token, colinfo, rows + local pos = 1 + + if ( nil == self.stream ) then + return false, "Not connected to server" + end + + queryPacket:SetQuery( query ) + status, result = self.stream:Send( queryPacket:ToString() ) + if ( not(status) ) then + return false, result + end + + status, data = self.stream:Receive() + if ( not(status) ) then + return false, data + end + + -- Iterate over tokens until we get to a rowtag + while( pos < data:len() ) do + local rowtag = select(2, bin.unpack("C", data, pos)) + + if ( rowtag == TokenType.Row ) then + break + end + + pos, token = Token.ParseToken( data, pos ) + if ( -1 == pos ) then + return false, token + end + if ( token.type == TokenType.ErrorMessage ) then + return false, token.error + elseif ( token.type == TokenType.TDS7Results ) then + colinfo = token.colinfo + end + end + + + rows = {} + + while(true) do + local rowtag + pos, rowtag = bin.unpack("C", data, pos ) + + if ( rowtag ~= TokenType.Row ) then + break + end + + if ( rowtag == TokenType.Row and colinfo and #colinfo > 0 ) then + local columns = {} + + for i=1, #colinfo do + local val + + if ( ColumnData.Parse[colinfo[i].type] ) then + pos, val = ColumnData.Parse[colinfo[i].type](data, pos) + if ( -1 == pos ) then + return false, val + end + table.insert(columns, val) + else + return false, ("unknown datatype=0x%X"):format(colinfo[i].type) + end + end + table.insert(rows, columns) + end + end + + result = {} + result.rows = rows + result.colinfo = colinfo + + return true, result + end, + +} + +--- "static" Utility class containing mostly conversion functions +Util = +{ + --- Converts a string to a wide string + -- + -- @param str string to be converted + -- @return string containing a two byte representation of str where a zero + -- byte character has been tagged on to each character. + ToWideChar = function( str ) + return str:gsub("(.)", "%1" .. string.char(0x00) ) + end, + + + --- Concerts a wide string to string + -- + -- @param wstr containing the wide string to convert + -- @return string with every other character removed + FromWideChar = function( wstr ) + local str = "" + if ( nil == wstr ) then + return nil + end + for i=1, wstr:len(), 2 do + str = str .. wstr:sub(i, i) + end + return str + end, + + --- Takes a table as returned by Query and does some fancy formatting + -- better suitable for stdnse.output_result + -- + -- @param tbl as recieved by Helper.Query + -- @param with_headers boolean true if output should contain column headers + -- @return table suitable for stdnse.output_result + FormatOutputTable = function ( tbl, with_headers ) + local new_tbl = {} + local col_names = {} + + if ( not(tbl) ) then + return + end + + if ( with_headers and tbl.rows and #tbl.rows > 0 ) then + local headers + table.foreach( tbl.colinfo, function( k, v ) table.insert( col_names, v.text) end) + headers = stdnse.strjoin("\t", col_names) + table.insert( new_tbl, headers) + headers = headers:gsub("[^%s]", "=") + table.insert( new_tbl, headers ) + end + + for _, v in ipairs( tbl.rows ) do + table.insert( new_tbl, stdnse.strjoin("\t", v) ) + end + + return new_tbl + end, + + +} \ No newline at end of file diff --git a/scripts/ms-sql-brute.nse b/scripts/ms-sql-brute.nse new file mode 100644 index 000000000..5b033f300 --- /dev/null +++ b/scripts/ms-sql-brute.nse @@ -0,0 +1,93 @@ +description = [[ +Performs password guessing against Microsoft SQL Server (mssql) +]] + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"auth", "intrusive"} + +require 'shortport' +require 'stdnse' +require 'mssql' +require 'unpwdb' + +--- +-- +-- @output +-- PORT STATE SERVICE +-- 1433/tcp open ms-sql-s +-- | mssql-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 + +portrule = shortport.port_or_service(1433, "ms-sql-s") + +action = function( host, port ) + + local result, response, status, aborted = {}, nil, nil, false + local valid_accounts = {} + local usernames, passwords + local username, password + local max_time = unpwdb.timelimit() ~= nil and unpwdb.timelimit() * 1000 or -1 + local clock_start = nmap.clock_ms() + local helper = mssql.Helper:new() + + 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 + + if max_time>0 and nmap.clock_ms() - clock_start > max_time then + aborted=true + break + end + + status, result = helper:Connect(host, port) + if( not(status) ) then + return " \n\n" .. result + end + + stdnse.print_debug( "Trying %s/%s ...", username, password ) + status, result = helper:Login( username, password, "tempdb", host.ip ) + helper:Disconnect() + + if status 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) + + if max_time > 0 and aborted then + output = output .. string.format(" \n\nscript aborted execution after %d seconds", max_time/1000 ) + end + + return output +end \ No newline at end of file diff --git a/scripts/ms-sql-config.nse b/scripts/ms-sql-config.nse new file mode 100644 index 000000000..58490ba93 --- /dev/null +++ b/scripts/ms-sql-config.nse @@ -0,0 +1,109 @@ +description = [[ +Queries Microsoft SQL Server (MSSQL) for a list of: +* Databases +* Linked Servers +* Configuration settings +]] + +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 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 mssql-config.showall if set shows all configuration options. +-- + +-- Version 0.1 +-- Created 04/02/2010 - v0.1 - created by Patrik Karlsson + +portrule = shortport.port_or_service(1433, "ms-sql-s") + +action = function( host, port ) + + local status, helper, response + local username = nmap.registry.args['mssql.username'] + local password = nmap.registry.args['mssql.password'] or "" + local result, result_part = {}, {} + local conf_filter = ( nmap.registry.args['mssql-config.showall'] ) and "" or " WHERE configuration_id > 16384" + local db_filter = ( nmap.registry.args['mssql-config.showall'] ) and "" or " WHERE name NOT IN ('master','model','tempdb','msdb')" + + local queries = { + [2]={ ["Configuration"] = [[ SELECT name, + cast(value as varchar) value, + cast(value_in_use as varchar) inuse, + description + FROM sys.configurations ]] .. conf_filter }, + [3]={ ["Linked Servers"] = [[ SELECT srvname, srvproduct, providername + FROM master..sysservers + WHERE srvid > 0 ]] }, + [1]={ ["Databases"] = [[ CREATE TABLE #nmap_dbs(name varchar(255), db_size varchar(255), owner varchar(255), + dbid int, created datetime, status varchar(512), compatibility_level int ) + INSERT INTO #nmap_dbs EXEC sp_helpdb + SELECT name, db_size, owner + FROM #nmap_dbs ]] .. db_filter .. [[ + DROP DATABASE #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 + + -- 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 + end + + for _, v in ipairs( queries ) do + for header, query in pairs(v) do + status, result_part = helper:Query( query ) + + if ( not(status) ) then + return " \n\nERROR: " .. result_part + end + result_part = mssql.Util.FormatOutputTable( result_part, true ) + result_part.name = header + table.insert( result, result_part ) + end + end + + helper:Disconnect() + + return stdnse.format_output( true, result ) + +end \ No newline at end of file diff --git a/scripts/ms-sql-empty-password.nse b/scripts/ms-sql-empty-password.nse new file mode 100644 index 000000000..5691f33c0 --- /dev/null +++ b/scripts/ms-sql-empty-password.nse @@ -0,0 +1,52 @@ +description = [[ +Attempts to authenticate using an empty password for the sysadmin (sa) account. +]] + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"auth","intrusive"} + +require 'shortport' +require 'stdnse' +require 'mssql' + +--- +-- +-- @output +-- PORT STATE SERVICE +-- 1433/tcp open ms-sql-s +-- | mssql-empty-password: +-- |_ sa: => Login Correct +-- +-- + +-- 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 helper, status, result + local username, password, database, valid_accounts = "sa", "", "tempdb", {} + + 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 ) + 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) + +end diff --git a/scripts/ms-sql-hasdbaccess.nse b/scripts/ms-sql-hasdbaccess.nse new file mode 100644 index 000000000..44f03860f --- /dev/null +++ b/scripts/ms-sql-hasdbaccess.nse @@ -0,0 +1,143 @@ +description = [[ +Queries Microsoft SQL Server (MSSQL) for a list of databases a user has access to. +]] + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"auth", "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 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 mssql-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 +-- | mssql-hasdbaccess: +-- | webshop_reader +-- | dbname owner +-- | hr sa +-- | finance sa +-- | webshop sa +-- | lordvader +-- | dbname owner +-- | testdb CQURE-NET\Administr +-- |_ webshop sa + +-- +-- 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 mssq-brute or mssq-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. +-- + +-- Version 0.1 +-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson + +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 + +action = function( host, port ) + + local status, result, helper, rs + local username = nmap.registry.args['mssql.username'] + local password = nmap.registry.args['mssql.password'] or "" + local creds + local query, limit + local output = {} + local exclude_dbs = { "'master'", "'tempdb'", "'model'", "'msdb'" } + + local RS_LIMIT = nmap.registry.args["mssql-hasdbaccess.limit"] and tonumber(nmap.registry.args["mssql-hasdbaccess.limit"]) or 5 + + if ( RS_LIMIT <= 0 ) then + limit = "" + else + limit = string.format( "TOP %d", RS_LIMIT ) + end + + local query = { [[CREATE table #hasaccess(dbname varchar(255), owner varchar(255), + DboOnly bit, ReadOnly bit, SingelUser bit, Detached bit, + Suspect bit, Offline bit, InLoad bit, EmergencyMode bit, + StandBy bit, [ShutDown] bit, InRecovery bit, NotRecovered bit )]], + + + "INSERT INTO #hasaccess EXEC sp_MShasdbaccess", + ("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 ) + if ( status ) then + -- Only the SELECT statement should produce output + if ( #result.rows > 0 ) then + rs = result + end + end + end + + helper:Disconnect() + + if ( status ) 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 + + return stdnse.format_output( true, output ) + +end \ No newline at end of file diff --git a/scripts/ms-sql-query.nse b/scripts/ms-sql-query.nse new file mode 100644 index 000000000..a366a4925 --- /dev/null +++ b/scripts/ms-sql-query.nse @@ -0,0 +1,96 @@ +description = [[ +Runs a Query against Microsoft SQL Server (MSSQL). +]] + +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-query.query specifies the query to run against the server. +-- (default SELECT @@version version) +-- +-- @output +-- +-- PORT STATE SERVICE +-- 1433/tcp open ms-sql-s +-- | mssql-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) +-- +-- + +-- 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 = nmap.registry.args['mssql.username'] + local password = nmap.registry.args['mssql.password'] or "" + -- 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 = nmap.registry.args['mssql.database'] or "tempdb" + local query = nmap.registry.args['mssql-query.query'] or "SELECT @@version version" + + 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 + end + 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 + +end \ No newline at end of file diff --git a/scripts/ms-sql-tables.nse b/scripts/ms-sql-tables.nse new file mode 100644 index 000000000..4c9b59511 --- /dev/null +++ b/scripts/ms-sql-tables.nse @@ -0,0 +1,251 @@ +description = [[ +Queries Microsoft SQL Server (MSSQL) for a list of tables per database. +]] + +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 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 mssql-tables.maxdb Limits the amount of databases that are +-- processed and returned (default 5). If set to zero or less +-- all databases are processed. +-- +-- @args mssql-tables.maxtables Limits the amount of tables returned +-- (default 5). If set to zero or less all tables are returned. +-- +-- @args mssql-tables.keywords If set shows only tables or columns matching +-- the keywords +-- +-- @output +-- PORT STATE SERVICE +-- 1433/tcp open ms-sql-s +-- | mssql-tables: +-- | webshop +-- | table column type length +-- | payments user_id int 4 +-- | payments purchase_id int 4 +-- | payments cardholder varchar 50 +-- | payments cardtype varchar 50 +-- | payments cardno varchar 50 +-- | payments expiry varchar 50 +-- | payments cvv varchar 4 +-- | products id int 4 +-- | products manu varchar 50 +-- | products model varchar 50 +-- | products productname varchar 100 +-- | products price float 8 +-- | products imagefile varchar 255 +-- | products quantity int 4 +-- | products keywords varchar 100 +-- | products description text 16 +-- | users id int 4 +-- | users username varchar 50 +-- | users password varchar 50 +-- |_ users fullname varchar 100 +-- +-- +-- The sysdatabase table should be accessible by more or less everyone +-- The script attempts to use the sa account over some n00b if it has +-- the password in the registry. If not the first account in the +-- registry is used. +-- +-- Once we have a list of DBs we iterate over it and attempt to extract +-- table names. In order for this to succeed we need to have either +-- sysadmin privileges or an account with access to the db. So, for each +-- db we successfully enumerate tables from we mark as finnished, we then +-- iterate over our know user accounts until either we exhausted our users +-- or we found all tables in all dbs. +-- +-- Oh, and exclude all MS default dbs from this excercise. +-- + +-- 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. + +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 + +action = function( host, port ) + + local status, result, dbs, tables, helper + local username = nmap.registry.args['mssql.username'] + local password = nmap.registry.args['mssql.password'] or "" + + 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 = nmap.registry.args["mssql-tables.maxdb"] and tonumber(nmap.registry.args["mssql-tables.maxdb"]) or 5 + local TABLE_COUNT = nmap.registry.args["mssql-tables.maxtables"] and tonumber(nmap.registry.args["mssql-tables.maxtables"]) or 2 + local keywords_filter = "" + + if ( DB_COUNT <= 0 ) then + db_limit = "" + else + db_limit = string.format( "TOP %d", DB_COUNT ) + end + if (TABLE_COUNT <= 0 ) then + tbl_limit = "" + else + tbl_limit = string.format( "TOP %d", TABLE_COUNT ) + end + + -- Build the keyword filter + if ( nmap.registry.args['mssql-tables.keywords'] ) then + local keywords = nmap.registry.args['mssql-tables.keywords'] + local tmp_tbl = {} + + if( type(keywords) == 'string' ) then + keywords = { keywords } + end + + for _, v in ipairs(keywords) do + table.insert(tmp_tbl, ("'%s'"):format(v)) + end + + keywords_filter = (" AND ( so.name IN (%s) or sc.name IN (%s) ) "):format( + stdnse.strjoin(",", tmp_tbl), + stdnse.strjoin(",", tmp_tbl) + ) + end + + 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 + 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 + end + end + end + helper:Disconnect() + end + + local pos = 1 + local restrict_tbl = {} + + if ( nmap.registry.args['mssql-tables.keywords'] ) then + tmp = nmap.registry.args['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 mssql-tables.keywords)") + end + + if ( DB_COUNT > 0 ) then + local tmp = ("Output restricted to %d databases"):format(DB_COUNT) + if ( not(nmap.registry.args['mssql-tables.maxdb']) ) then + tmp = tmp .. " (see mssql-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(nmap.registry.args['mssql-tables.maxtables']) ) then + tmp = tmp .. " (see mssql-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 ) + + return output + +end \ No newline at end of file diff --git a/scripts/ms-sql-xp-cmdshell.nse b/scripts/ms-sql-xp-cmdshell.nse new file mode 100644 index 000000000..055520d92 --- /dev/null +++ b/scripts/ms-sql-xp-cmdshell.nse @@ -0,0 +1,146 @@ +description = [[ +Queries Microsoft SQL Server (MSSQL) for a list of tables per database. +]] + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive"} + +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 mssql-brute and mssql-empty-password scripts. +-- +-- @args mssql-xp-cmdshell.cmd specifies the OS command to run. +-- (default is ipconfig /all) +-- +-- @output +-- PORT STATE SERVICE +-- 1433/tcp open ms-sql-s +-- | mssql-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 +-- |_ +-- +-- 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 mssq-brute or mssq-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. +-- + +-- Version 0.1 +-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson + +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 + +action = function( host, port ) + + local status, result, helper + local username = nmap.registry.args['mssql.username'] + local password = nmap.registry.args['mssql.password'] or "" + local creds + local query + local cmd = nmap.registry.args['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 + 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, result = helper:Query( query ) + helper:Disconnect() + + if ( status ) then + output = mssql.Util.FormatOutputTable( result, true ) + if ( not(nmap.registry.args['mssql-xp-cmdshell.cmd']) ) then + table.insert(output, 1, cmd) + output = stdnse.format_output( true, output ) + output = "(Use --script-args=mssql-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 + +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index c60f9e985..0f3d3948b 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -52,7 +52,14 @@ Entry { filename = "ldap-search.nse", categories = { "discovery", "safe", } } Entry { filename = "lexmark-config.nse", categories = { "discovery", "safe", } } Entry { filename = "mongodb-databases.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "mongodb-info.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "ms-sql-brute.nse", categories = { "auth", "intrusive", } } +Entry { filename = "ms-sql-config.nse", categories = { "discovery", "safe", } } +Entry { filename = "ms-sql-empty-password.nse", categories = { "auth", "intrusive", } } +Entry { filename = "ms-sql-hasdbaccess.nse", categories = { "auth", "discovery", "safe", } } Entry { filename = "ms-sql-info.nse", categories = { "default", "discovery", "intrusive", } } +Entry { filename = "ms-sql-query.nse", categories = { "discovery", "safe", } } +Entry { filename = "ms-sql-tables.nse", categories = { "discovery", "safe", } } +Entry { filename = "ms-sql-xp-cmdshell.nse", categories = { "intrusive", } } Entry { filename = "mysql-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "mysql-databases.nse", categories = { "discovery", "intrusive", } } Entry { filename = "mysql-empty-password.nse", categories = { "auth", "intrusive", } }