mirror of
https://github.com/nmap/nmap.git
synced 2025-12-06 12:41:29 +00:00
qscan.delay dns-fuzz.timelimit mssql.timelimit A side effect is that the default units for qscan.delay are seconds, not milliseconds. 0 is now the magic value to disable the time limit in dns-fuzz.
1043 lines
30 KiB
Lua
1043 lines
30 KiB
Lua
--- MSSQL Library supporting a very limited subset of operations
|
|
-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
|
|
--
|
|
-- @author = "Patrik Karlsson <patrik@cqure.net>"
|
|
--
|
|
-- 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:
|
|
--
|
|
-- <code>
|
|
-- 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()
|
|
-- <code>
|
|
--
|
|
-- 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 How long to wait for SQL responses. This is a number
|
|
-- followed by <code>ms</code> for milliseconds, <code>s</code> for seconds,
|
|
-- <code>m</code> for minutes, or <code>h</code> for hours. Default:
|
|
-- <code>30s</code>.
|
|
|
|
--
|
|
-- Version 0.2
|
|
-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
|
|
-- Revised 03/28/2010 - v0.2 - fixed incorrect token types. added 30 seconds timeout
|
|
--
|
|
--
|
|
|
|
module(... or "mssql", package.seeall)
|
|
|
|
require("bit")
|
|
require("bin")
|
|
require("stdnse")
|
|
|
|
do
|
|
local arg = nmap.registry.args and nmap.registry.args["mssql.timeout"] or "30s"
|
|
local timeout, err
|
|
|
|
timeout, err = stdnse.parse_timespec(arg)
|
|
if not timeout then
|
|
error(err)
|
|
end
|
|
MSSQL_TIMEOUT = timeout
|
|
end
|
|
|
|
-- TDS packet types
|
|
PacketType =
|
|
{
|
|
Query = 0x01,
|
|
Response = 0x04,
|
|
Login = 0x10,
|
|
}
|
|
|
|
-- TDS response token types
|
|
TokenType =
|
|
{
|
|
TDS7Results = 0x81,
|
|
ErrorMessage = 0xAA,
|
|
InformationMessage = 0xAB,
|
|
LoginAcknowledgement = 0xAD,
|
|
Row = 0xD1,
|
|
OrderBy = 0xA9,
|
|
EnvironmentChange = 0xE3,
|
|
Done = 0xFD,
|
|
DoneInProc = 0xFF,
|
|
}
|
|
|
|
-- SQL Server/Sybase data types
|
|
DataTypes =
|
|
{
|
|
SYBINTN = 0x26,
|
|
SYBINT2 = 0x34,
|
|
SYBINT4 = 0x38,
|
|
SYBDATETIME = 0x3D,
|
|
SYBDATETIMN = 0x6F,
|
|
XSYBVARBINARY = 0xA5,
|
|
XSYBVARCHAR = 0xA7,
|
|
XSYBNVARCHAR = 0xE7,
|
|
}
|
|
|
|
-- "static" ColumInfo parser class
|
|
ColumnInfo =
|
|
{
|
|
|
|
Parse =
|
|
{
|
|
[DataTypes.XSYBNVARCHAR] = function( data, pos )
|
|
local colinfo = {}
|
|
local tmp
|
|
|
|
pos, colinfo.lts, colinfo.codepage, colinfo.flags, colinfo.charset,
|
|
colinfo.msglen = bin.unpack("<SSSCC", data, pos )
|
|
pos, tmp = bin.unpack("A" .. (colinfo.msglen * 2), data, pos)
|
|
colinfo.text = Util.FromWideChar(tmp)
|
|
|
|
return pos, colinfo
|
|
end,
|
|
|
|
[DataTypes.SYBINT2] = function( data, pos )
|
|
return ColumnInfo.Parse[DataTypes.SYBDATETIME](data, pos)
|
|
end,
|
|
|
|
[DataTypes.SYBINTN] = function( data, pos )
|
|
local colinfo = {}
|
|
local tmp
|
|
|
|
pos, colinfo.unknown, colinfo.msglen = bin.unpack("<CC", data, pos)
|
|
pos, tmp = bin.unpack("A" .. (colinfo.msglen * 2), data, pos )
|
|
colinfo.text = Util.FromWideChar(tmp)
|
|
|
|
return pos, colinfo
|
|
end,
|
|
|
|
[DataTypes.SYBINT4] = function( data, pos )
|
|
return ColumnInfo.Parse[DataTypes.SYBDATETIME](data, pos)
|
|
end,
|
|
|
|
[DataTypes.XSYBVARBINARY] = function( data, pos )
|
|
local colinfo = {}
|
|
local tmp
|
|
|
|
pos, colinfo.lts, colinfo.msglen = bin.unpack("<SC", data, pos)
|
|
pos, tmp = bin.unpack("A" .. (colinfo.msglen * 2), data, pos )
|
|
colinfo.text = Util.FromWideChar(tmp)
|
|
|
|
return pos, colinfo
|
|
end,
|
|
|
|
[DataTypes.SYBDATETIME] = function( data, pos )
|
|
local colinfo = {}
|
|
local tmp
|
|
|
|
pos, colinfo.msglen = bin.unpack("C", data, pos)
|
|
pos, tmp = bin.unpack("A" .. (colinfo.msglen * 2), data, pos )
|
|
colinfo.text = Util.FromWideChar(tmp)
|
|
|
|
return pos, colinfo
|
|
end,
|
|
|
|
[DataTypes.SYBDATETIMN] = function( data, pos )
|
|
return ColumnInfo.Parse[DataTypes.SYBINTN](data, pos)
|
|
end,
|
|
|
|
[DataTypes.XSYBVARCHAR] = function( data, pos )
|
|
return ColumnInfo.Parse[DataTypes.XSYBNVARCHAR](data, pos)
|
|
end,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
-- "static" ColumData parser class
|
|
ColumnData =
|
|
{
|
|
Parse = {
|
|
|
|
[DataTypes.XSYBNVARCHAR] = function( data, pos )
|
|
local size, coldata
|
|
|
|
pos, size = bin.unpack( "<S", data, pos )
|
|
pos, coldata = bin.unpack( "A"..size, data, pos )
|
|
|
|
return pos, Util.FromWideChar(coldata)
|
|
end,
|
|
|
|
[DataTypes.XSYBVARCHAR] = function( data, pos )
|
|
local size, coldata
|
|
|
|
pos, size = bin.unpack( "<S", data, pos )
|
|
pos, coldata = bin.unpack( "A"..size, data, pos )
|
|
|
|
return pos, coldata
|
|
end,
|
|
|
|
[DataTypes.XSYBVARBINARY] = function( data, pos )
|
|
local coldata, size
|
|
|
|
pos, size = bin.unpack( "<S", data, pos )
|
|
pos, coldata = bin.unpack( "A"..size, data, pos )
|
|
|
|
return pos, "0x" .. select(2, bin.unpack("H"..coldata:len(), coldata ) )
|
|
end,
|
|
|
|
[DataTypes.SYBINT4] = function( data, pos )
|
|
local num
|
|
pos, num = bin.unpack("<I", data, pos)
|
|
|
|
return pos, num
|
|
end,
|
|
|
|
[DataTypes.SYBINT2] = function( data, pos )
|
|
local num
|
|
pos, num = bin.unpack("<S", data, pos)
|
|
|
|
return pos, num
|
|
end,
|
|
|
|
[DataTypes.SYBINTN] = function( data, pos )
|
|
local len, num
|
|
pos, len = bin.unpack("C", data, pos)
|
|
|
|
if ( len == 1 ) then
|
|
return bin.unpack("C", data, pos)
|
|
elseif ( len == 2 ) then
|
|
return bin.unpack("<S", data, pos)
|
|
elseif ( len == 4 ) then
|
|
return bin.unpack("<I", data, pos)
|
|
elseif ( len == 8 ) then
|
|
return bin.unpack("<L", data, pos)
|
|
else
|
|
return -1, ("Unhandled length (%d) for SYBINTN"):format(len)
|
|
end
|
|
|
|
return -1, "Error"
|
|
end,
|
|
|
|
[DataTypes.SYBDATETIME] = function( data, pos )
|
|
local hi, lo, dt, result
|
|
pos, hi, lo = bin.unpack("<II", data, pos)
|
|
|
|
-- CET 01/01/1900
|
|
dt = -2208996000
|
|
result = os.date("%x %X", dt + (hi*24*60*60) + (lo/300) )
|
|
|
|
return pos, result
|
|
end,
|
|
|
|
[DataTypes.SYBDATETIMN] = function( data, pos )
|
|
return ColumnData.Parse[DataTypes.SYBINTN]( data, pos )
|
|
end,
|
|
|
|
}
|
|
}
|
|
|
|
-- "static" Token parser class
|
|
Token =
|
|
{
|
|
|
|
Parse = {
|
|
--- Parse error message tokens
|
|
--
|
|
-- @param data string containing "raw" data
|
|
-- @param pos number containing offset into data
|
|
-- @return pos number containing new offset after parse
|
|
-- @return token table containing token specific fields
|
|
[TokenType.ErrorMessage] = function( data, pos )
|
|
local token = {}
|
|
local tmp
|
|
|
|
token.type = TokenType.ErrorMessage
|
|
pos, token.size, token.errno, token.state, token.severity, token.errlen = bin.unpack( "<SICCS", data, pos )
|
|
pos, tmp = bin.unpack("A" .. (token.errlen * 2), data, pos )
|
|
token.error = Util.FromWideChar(tmp)
|
|
pos, token.srvlen = bin.unpack("C", data, pos)
|
|
pos, tmp = bin.unpack("A" .. (token.srvlen * 2), data, pos )
|
|
token.server = Util.FromWideChar(tmp)
|
|
pos, token.proclen = bin.unpack("C", data, pos)
|
|
pos, tmp = bin.unpack("A" .. (token.proclen * 2), data, pos )
|
|
token.proc = Util.FromWideChar(tmp)
|
|
pos, token.lineno = bin.unpack("<S", data, pos)
|
|
|
|
return pos, token
|
|
end,
|
|
|
|
--- Parse environment change tokens
|
|
-- (This function is not implemented and simply moves the pos offset)
|
|
--
|
|
-- @param data string containing "raw" data
|
|
-- @param pos number containing offset into data
|
|
-- @return pos number containing new offset after parse
|
|
-- @return token table containing token specific fields
|
|
[TokenType.EnvironmentChange] = function( data, pos )
|
|
local token = {}
|
|
local tmp
|
|
|
|
token.type = TokenType.EnvironmentChange
|
|
pos, token.size = bin.unpack("<S", data, pos)
|
|
|
|
return pos + token.size, token
|
|
end,
|
|
|
|
--- Parse information message tokens
|
|
--
|
|
-- @param data string containing "raw" data
|
|
-- @param pos number containing offset into data
|
|
-- @return pos number containing new offset after parse
|
|
-- @return token table containing token specific fields
|
|
[TokenType.InformationMessage] = function( data, pos )
|
|
local pos, token = Token.Parse[TokenType.ErrorMessage]( data, pos )
|
|
token.type = TokenType.InformationMessage
|
|
return pos, token
|
|
end,
|
|
|
|
--- Parse login acknowledgment tokens
|
|
--
|
|
-- @param data string containing "raw" data
|
|
-- @param pos number containing offset into data
|
|
-- @return pos number containing new offset after parse
|
|
-- @return token table containing token specific fields
|
|
[TokenType.LoginAcknowledgement] = function( data, pos )
|
|
local token = {}
|
|
local _
|
|
|
|
-- don't do much, just increase the pos offset to next token
|
|
token.type = TokenType.LoginAcknowledgement
|
|
pos, token.size, _, _, _, _, token.textlen = bin.unpack( "<SCCCSC", data, pos )
|
|
pos, token.text = bin.unpack("A" .. token.textlen * 2, data, pos)
|
|
pos, token.version = bin.unpack("<I", data, pos )
|
|
|
|
return pos, token
|
|
end,
|
|
|
|
--- Parse done tokens
|
|
--
|
|
-- @param data string containing "raw" data
|
|
-- @param pos number containing offset into data
|
|
-- @return pos number containing new offset after parse
|
|
-- @return token table containing token specific fields
|
|
[TokenType.Done] = function( data, pos )
|
|
local token = {}
|
|
local _
|
|
|
|
-- don't do much, just increase the pos offset to next token
|
|
token.type = TokenType.Done
|
|
pos, token.flags, token.operation, token.rowcount = bin.unpack( "<SSI", data, pos )
|
|
|
|
return pos, token
|
|
end,
|
|
|
|
--- Parses a DoneInProc token recieved after executing a SP
|
|
--
|
|
-- @param data string containing "raw" data
|
|
-- @param pos number containing offset into data
|
|
-- @return pos number containing new offset after parse
|
|
-- @return token table containing token specific fields
|
|
[TokenType.DoneInProc] = function( data, pos )
|
|
local token
|
|
pos, token = Token.Parse[TokenType.Done]( data, pos )
|
|
token.type = TokenType.DoneInProc
|
|
|
|
return pos, token
|
|
end,
|
|
|
|
--- Parses a OrderBy token
|
|
--
|
|
-- @param data string containing "raw" data
|
|
-- @param pos number containing offset into data
|
|
-- @return pos number containing new offset after parse
|
|
-- @return token table containing token specific fields
|
|
[TokenType.OrderBy] = function( data, pos )
|
|
local token = {}
|
|
|
|
pos, token.size = bin.unpack("<S", data, pos)
|
|
token.type = TokenType.OrderBy
|
|
return pos + token.size, token
|
|
end,
|
|
|
|
|
|
--- Parse TDS result tokens
|
|
--
|
|
-- @param data string containing "raw" data
|
|
-- @param pos number containing offset into data
|
|
-- @return pos number containing new offset after parse
|
|
-- @return token table containing token specific fields
|
|
[TokenType.TDS7Results] = function( data, pos )
|
|
local token = {}
|
|
local _
|
|
|
|
token.type = TokenType.TDS7Results
|
|
pos, token.count = bin.unpack( "<S", data, pos )
|
|
token.colinfo = {}
|
|
|
|
for i=1, token.count do
|
|
local colinfo = {}
|
|
local usertype, flags, ttype
|
|
|
|
pos, usertype, flags, ttype = bin.unpack("<SSC", data, pos )
|
|
if ( not(ColumnInfo.Parse[ttype]) ) then
|
|
return -1, ("Unhandled data type: 0x%X"):format(ttype)
|
|
end
|
|
|
|
pos, colinfo = ColumnInfo.Parse[ttype]( data, pos )
|
|
|
|
colinfo.usertype = usertype
|
|
colinfo.flags = flags
|
|
colinfo.type = ttype
|
|
|
|
table.insert( token.colinfo, colinfo )
|
|
end
|
|
return pos, token
|
|
end,
|
|
},
|
|
|
|
--- Parses the first token at positions pos
|
|
--
|
|
-- @param data string containing "raw" data
|
|
-- @param pos number containing offset into data
|
|
-- @return pos number containing new offset after parse or -1 on error
|
|
-- @return token table containing token specific fields or error message on error
|
|
ParseToken = function( data, pos )
|
|
local ttype
|
|
pos, ttype = bin.unpack("C", data, pos)
|
|
if ( not(Token.Parse[ttype]) ) then
|
|
return -1, ("No parser for token type: 0x%X"):format( ttype )
|
|
end
|
|
|
|
return Token.Parse[ttype](data, pos)
|
|
end,
|
|
|
|
}
|
|
|
|
|
|
--- QueryPacket class
|
|
QueryPacket =
|
|
{
|
|
new = function(self,o)
|
|
o = o or {}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
SetQuery = function( self, query )
|
|
self.query = query
|
|
end,
|
|
|
|
--- Returns the query packet as string
|
|
--
|
|
-- @return string containing the authentication packet
|
|
ToString = function( self )
|
|
return PacketType.Query, Util.ToWideChar( self.query )
|
|
end,
|
|
|
|
}
|
|
|
|
|
|
--- LoginPacket class
|
|
LoginPacket =
|
|
{
|
|
|
|
-- options_1 possible values
|
|
-- 0x80 enable warning messages if SET LANGUAGE issued
|
|
-- 0x40 change to initial database must succeed
|
|
-- 0x20 enable warning messages if USE <database> 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("<IIIIII", self.length, self.version, self.size, self.cli_version, self.cli_pid, self.conn_id )
|
|
data = data .. bin.pack("CCCC", self.options_1, self.options_2, self.sqltype_flag, self.reserved_flag )
|
|
data = data .. bin.pack("<II", self.time_zone, self.collation )
|
|
|
|
-- offsets begin
|
|
data = data .. bin.pack("<SS", offset, self.client:len() )
|
|
offset = offset + self.client:len() * 2
|
|
|
|
data = data .. bin.pack("<SS", offset, self.username:len() )
|
|
offset = offset + self.username:len() * 2
|
|
|
|
data = data .. bin.pack("<SS", offset, self.password:len() )
|
|
offset = offset + self.password:len() * 2
|
|
|
|
data = data .. bin.pack("<SS", offset, self.app:len() )
|
|
offset = offset + self.app:len() * 2
|
|
|
|
data = data .. bin.pack("<SS", offset, self.server:len() )
|
|
offset = offset + self.server:len() * 2
|
|
|
|
-- unknown1 offset
|
|
data = data .. bin.pack("<SS", 0, 0 )
|
|
|
|
data = data .. bin.pack("<SS", offset, self.library:len() )
|
|
offset = offset + self.library:len() * 2
|
|
|
|
data = data .. bin.pack("<SS", offset, self.locale:len() )
|
|
offset = offset + self.locale:len() * 2
|
|
|
|
data = data .. bin.pack("<SS", offset, self.database:len() )
|
|
offset = offset + self.database:len() * 2
|
|
|
|
-- client MAC address, hardcoded to 00:00:00:00:00:00
|
|
data = data .. bin.pack("A", self.MAC)
|
|
|
|
-- offset to auth info
|
|
data = data .. bin.pack("<S", offset)
|
|
-- lenght of nt auth (should be 0 for sql auth)
|
|
data = data .. bin.pack("<S", 0)
|
|
-- next position (same as total packet length)
|
|
data = data .. bin.pack("<S", self.length)
|
|
-- zero pad
|
|
data = data .. bin.pack("<S", 0)
|
|
|
|
-- Auth info wide strings
|
|
data = data .. bin.pack("A", Util.ToWideChar(self.client) )
|
|
data = data .. bin.pack("A", Util.ToWideChar(self.username) )
|
|
data = data .. bin.pack("A", self.TDS7CryptPass(self.password) )
|
|
data = data .. bin.pack("A", Util.ToWideChar(self.app) )
|
|
data = data .. bin.pack("A", Util.ToWideChar(self.server) )
|
|
data = data .. bin.pack("A", Util.ToWideChar(self.library) )
|
|
data = data .. bin.pack("A", Util.ToWideChar(self.locale) )
|
|
data = data .. bin.pack("A", Util.ToWideChar(self.database) )
|
|
|
|
return PacketType.Login, data
|
|
end,
|
|
|
|
--- 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,
|
|
|
|
}
|
|
|
|
-- Handles communication with SQL Server
|
|
TDSStream = {
|
|
|
|
packetno = 0,
|
|
|
|
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, lport, _
|
|
|
|
self.socket = nmap.new_socket()
|
|
-- 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, result = self.socket:connect(host.ip, port.number, port.protocol)
|
|
status, _, lport, _, _ = self.socket:get_info()
|
|
if ( status ) then
|
|
math.randomseed(os.time() * lport )
|
|
else
|
|
math.randomseed(os.time() )
|
|
end
|
|
|
|
if ( not(status) ) then
|
|
return false, "Socket connection failed"
|
|
end
|
|
|
|
return status, result
|
|
end,
|
|
|
|
--- Disconnects from the SQL Server
|
|
--
|
|
-- @return status true on success, false on failure
|
|
-- @return result containing error message on failure
|
|
Disconnect = function( self )
|
|
local status, result = self.socket:close()
|
|
self.socket = nil
|
|
return status, result
|
|
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)
|
|
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
|
|
-- @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
|
|
|
|
self.packetno = self.packetno + 1
|
|
packet = 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 <code>stdnse.output_result</code>
|
|
--
|
|
-- @param tbl as recieved by <code>Helper.Query</code>
|
|
-- @param with_headers boolean true if output should contain column headers
|
|
-- @return table suitable for <code>stdnse.output_result</code>
|
|
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,
|
|
|
|
|
|
}
|