summaryrefslogtreecommitdiffstats
path: root/nselib/pgsql.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/pgsql.lua')
-rw-r--r--nselib/pgsql.lua620
1 files changed, 620 insertions, 0 deletions
diff --git a/nselib/pgsql.lua b/nselib/pgsql.lua
new file mode 100644
index 0000000..387a80c
--- /dev/null
+++ b/nselib/pgsql.lua
@@ -0,0 +1,620 @@
+---
+-- PostgreSQL library supporting both version 2 and version 3 of the protocol.
+-- The library currently contains the bare minimum to perform authentication.
+-- Authentication is supported with or without SSL enabled and using the
+-- plain-text or MD5 authentication mechanisms.
+--
+-- The PGSQL protocol is explained in detail in the following references.
+-- * http://developer.postgresql.org/pgdocs/postgres/protocol.html
+-- * http://developer.postgresql.org/pgdocs/postgres/protocol-flow.html
+-- * http://developer.postgresql.org/pgdocs/postgres/protocol-message-formats.html
+--
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+-- @author Patrik Karlsson <patrik@cqure.net>
+
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local openssl = stdnse.silent_require "openssl"
+local string = require "string"
+local table = require "table"
+_ENV = stdnse.module("pgsql", stdnse.seeall)
+
+-- Version 0.3
+-- Created 02/05/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 02/20/2010 - v0.2 - added detectVersion to automatically detect and return
+-- the correct version class
+-- Revised 03/04/2010 - v0.3 - added support for trust authentication method
+
+--- Supported pgsql message types
+MessageType = {
+ Error = 0x45,
+ BackendKeyData = 0x4b,
+ AuthRequest=0x52,
+ ParameterStatus = 0x53,
+ ReadyForQuery = 0x5a,
+ PasswordMessage = 0x70,
+}
+
+--- Supported authentication types
+AuthenticationType = {
+ Success = 0x00,
+ Plain = 0x03,
+ MD5 = 0x05
+}
+
+-- Version 2 of the protocol
+v2 =
+{
+
+ --- Pad a string with zeroes
+ --
+ -- @param str string containing the string to be padded
+ -- @param len number containing the wanted length
+ -- @return string containing the padded string value
+ zeroPad = function(str, len)
+ return str .. string.rep('\0', len - #str)
+ end,
+
+ messageDecoder = {
+
+ --- Decodes an Auth Request packet
+ --
+ -- @param data string containing raw data received from socket
+ -- @param len number containing the length as retrieved from the header
+ -- @param pos number containing the offset into the data buffer
+ -- @return pos number containing the offset after decoding, -1 on error
+ -- @return response table containing zero or more of the following <code>salt</code> and <code>success</code>
+ -- error string containing error message if pos is -1
+ [MessageType.AuthRequest] = function( data, len, pos )
+ local _, authtype
+ local response = {}
+ authtype, pos = string.unpack(">I4", data, pos)
+
+ if ( authtype == AuthenticationType.MD5 ) then
+ if ( len - pos + 1 ) < 3 then
+ return -1, "ERROR: Malformed AuthRequest received"
+ end
+ response.salt, pos = string.unpack("c4", data, pos)
+ elseif ( authtype == AuthenticationType.Plain ) then
+ --do nothing
+ elseif ( authtype == 0 ) then
+ response.success = true
+ else
+ stdnse.debug1("unknown auth type: %d", authtype)
+ end
+
+ response.authtype = authtype
+ return pos, response
+ end,
+
+
+
+ --- Decodes an Error packet
+ --
+ -- @param data string containing raw data received from socket
+ -- @param len number containing the length as retrieved from the header
+ -- @param pos number containing the offset into the data buffer
+ -- @return pos number containing the offset after decoding
+ -- @return response table containing zero or more of the following <code>error.severity</code>,
+ -- <code>error.code</code>, <code>error.message</code>, <code>error.file</code>,
+ -- <code>error.line</code> and <code>error.routine</code>
+ [MessageType.Error] = function( data, len, pos )
+ local tmp = data:sub(pos, pos + len - 4)
+ local response = {}
+ local pos_end = pos + len
+
+ response.error = {}
+ response.error.message, pos = string.unpack("z", data, pos)
+ return pos, response
+ end,
+
+ },
+
+ --- Process the server response
+ --
+ -- @param data string containing the server response
+ -- @param pos number containing the offset into the data buffer
+ processResponse = function(data, pos)
+ local ptype, len, status, response
+ local pos = pos or 1
+
+ ptype, pos = string.unpack("B", data, pos)
+ len = data:len() - 1
+
+ if v2.messageDecoder[ptype] then
+ pos, response = v2.messageDecoder[ptype](data, len, pos)
+
+ if pos ~= -1 then
+ response.type = ptype
+ return pos, response
+ end
+ else
+ stdnse.debug1("Missing decoder for %d", ptype)
+ return -1, ("Missing decoder for %d"):format(ptype)
+ end
+ return -1, "Decoding failed"
+ end,
+
+
+ --- Reads a packet and handles additional socket reads to retrieve remaining data
+ --
+ -- @param socket socket already connected to the pgsql server
+ -- @param data string containing any data already retrieved from the socket
+ -- @param pos number containing the offset into the data buffer
+ -- @return data string containing the initial and any additional data
+ readPacket=function(socket, data, pos)
+
+ local pos = pos or 1
+ local data = data or ""
+ local status = true
+ local tmp = ""
+ local ptype, len
+
+ local catch = function() socket:close() stdnse.debug1("processResponse(): failed") end
+ local try = nmap.new_try(catch)
+
+ if ( data == nil or data:len() == 0 ) then
+ data = try(socket:receive())
+ end
+ return data
+ end,
+
+ --- Sends a startup message to the server containing the username and database to connect to
+ --
+ -- @param socket socket already connected to the pgsql server
+ -- @param user string containing the name of the user
+ -- @param database string containing the name of the database
+ -- @return status true on success, false on failure
+ -- @return table containing a processed response from <code>processResponse</code>
+ -- string containing error message if status is false
+ sendStartup=function(socket, user, database)
+ local data, response, status, pos
+ local proto_ver, ptype, _, tmp
+
+ local tty, unused, args = "", "", ""
+ proto_ver = 0x0020000
+ user = v2.zeroPad(user, 32)
+ database = v2.zeroPad(database, 64)
+ data = string.pack(">I4I4", 296, proto_ver)
+ .. database
+ .. user
+ .. v2.zeroPad(args, 64)
+ .. v2.zeroPad(unused, 64)
+ .. v2.zeroPad(tty,64)
+
+ socket:send( data )
+
+ -- attempt to verify version
+ status, data = socket:receive_bytes( 1 )
+
+ if ( not(status) ) then
+ return false, "sendStartup failed"
+ end
+
+ data = v2.readPacket(socket, data )
+ pos, response = v2.processResponse( data )
+
+ if ( pos < 0 or response.type == MessageType.Error) then
+ return false, response.error.message or "unknown error"
+ end
+
+ return true, response
+ end,
+
+ --- Attempts to authenticate to the pgsql server
+ -- Supports plain-text and MD5 authentication
+ --
+ -- @param socket socket already connected to the pgsql server
+ -- @param params table containing any additional parameters <code>authtype</code>, <code>version</code>
+ -- @param username string containing the username to use for authentication
+ -- @param password string containing the password to use for authentication
+ -- @param salt string containing the cryptographic salt value
+ -- @return status true on success, false on failure
+ -- @return result table containing parameter status information,
+ -- result string containing an error message if login fails
+ loginRequest = function ( socket, params, username, password, salt )
+
+ local catch = function() socket:close() stdnse.debug1("loginRequest(): failed") end
+ local try = nmap.new_try(catch)
+ local response = {}
+ local status, data, len, pos, tmp
+
+ if ( params.authtype == AuthenticationType.MD5 ) then
+ local hash = createMD5LoginHash(username,password,salt)
+ data = string.pack( ">I4z", 40, hash)
+ try( socket:send( data ) )
+ elseif ( params.authtype == AuthenticationType.Plain ) then
+ local data
+ data = string.pack(">I4z", password:len() + 4, password)
+ try( socket:send( data ) )
+ elseif ( params.authtype == AuthenticationType.Success ) then
+ return true, nil
+ end
+
+ data, response.params = "", {}
+
+ data = v2.readPacket(socket, data, 1)
+ pos, tmp = v2.processResponse(data, 1)
+
+ -- this should contain the AuthRequest packet
+ if tmp.type ~= MessageType.AuthRequest then
+ return false, "Expected AuthRequest got something else"
+ end
+
+ if not tmp.success then
+ return false, "Login failure"
+ end
+
+ return true, response
+ end,
+
+}
+
+-- Version 3 of the protocol
+v3 =
+{
+ messageDecoder = {
+
+ --- Decodes an Auth Request packet
+ --
+ -- @param data string containing raw data received from socket
+ -- @param len number containing the length as retrieved from the header
+ -- @param pos number containing the offset into the data buffer
+ -- @return pos number containing the offset after decoding, -1 on error
+ -- @return response table containing zero or more of the following <code>salt</code> and <code>success</code>
+ -- error string containing error message if pos is -1
+ [MessageType.AuthRequest] = function( data, len, pos )
+ local _, authtype
+ local response = {}
+
+ authtype, pos = string.unpack(">I4", data, pos)
+
+ if ( authtype == AuthenticationType.MD5 ) then
+ if ( len - pos + 1 ) < 3 then
+ return -1, "ERROR: Malformed AuthRequest received"
+ end
+ response.salt, pos = string.unpack("c4", data, pos)
+ elseif ( authtype == AuthenticationType.Plain ) then
+ --do nothing
+ elseif ( authtype == 0 ) then
+ response.success = true
+ else
+ stdnse.debug1("unknown auth type: %d", authtype )
+ end
+
+ response.authtype = authtype
+ return pos, response
+ end,
+
+ --- Decodes an ParameterStatus packet
+ --
+ -- @param data string containing raw data received from socket
+ -- @param len number containing the length as retrieved from the header
+ -- @param pos number containing the offset into the data buffer
+ -- @return pos number containing the offset after decoding
+ -- @return response table containing zero or more of the following <code>key</code> and <code>value</code>
+ [MessageType.ParameterStatus] = function( data, len, pos )
+ local response = {}
+
+ local tmp = data:sub(pos, pos + len - 4)
+ response.key, response.value = string.unpack("zz", tmp)
+ return pos + len - 4, response
+ end,
+
+ --- Decodes an Error packet
+ --
+ -- @param data string containing raw data received from socket
+ -- @param len number containing the length as retrieved from the header
+ -- @param pos number containing the offset into the data buffer
+ -- @return pos number containing the offset after decoding
+ -- @return response table containing zero or more of the following <code>error.severity</code>,
+ -- <code>error.code</code>, <code>error.message</code>, <code>error.file</code>,
+ -- <code>error.line</code> and <code>error.routine</code>
+ [MessageType.Error] = function( data, len, pos )
+ local tmp = data:sub(pos, pos + len - 4)
+ local _, value, prefix
+ local response = {}
+ local pos_end = pos + len
+
+ response.error = {}
+
+ while ( pos < pos_end - 5 ) do
+ prefix, value, pos = string.unpack("c1z", data, pos)
+
+ if prefix == 'S' then
+ response.error.severity = value
+ elseif prefix == 'C' then
+ response.error.code = value
+ elseif prefix == 'M' then
+ response.error.message = value
+ elseif prefix == 'F' then
+ response.error.file = value
+ elseif prefix == 'L' then
+ response.error.line = value
+ elseif prefix == 'R' then
+ response.error.routine = value
+ end
+ end
+ return pos, response
+ end,
+
+ --- Decodes the BackendKeyData packet
+ --
+ -- @param data string containing raw data received from socket
+ -- @param len number containing the length as retrieved from the header
+ -- @param pos number containing the offset into the data buffer
+ -- @return pos number containing the offset after decoding, -1 on error
+ -- @return response table containing zero or more of the following <code>pid</code> and <code>key</code>
+ -- error string containing error message if pos is -1
+ [MessageType.BackendKeyData] = function( data, len, pos )
+ local response = {}
+
+ if len ~= 12 then
+ return -1, "ERROR: Invalid BackendKeyData packet"
+ end
+
+ response.pid, response.key, pos = string.unpack(">I4I4", data, pos)
+ return pos, response
+ end,
+
+ --- Decodes an ReadyForQuery packet
+ --
+ -- @param data string containing raw data received from socket
+ -- @param len number containing the length as retrieved from the header
+ -- @param pos number containing the offset into the data buffer
+ -- @return pos number containing the offset after decoding, -1 on error
+ -- @return response table containing zero or more of the following <code>status</code>
+ -- error string containing error message if pos is -1
+ [MessageType.ReadyForQuery] = function( data, len, pos )
+ local response = {}
+
+ if len ~= 5 then
+ return -1, "ERROR: Invalid ReadyForQuery packet"
+ end
+
+ response.status, pos = string.unpack("B", data, pos )
+ return pos, response
+ end,
+ },
+
+ --- Reads a packet and handles additional socket reads to retrieve remaining data
+ --
+ -- @param socket socket already connected to the pgsql server
+ -- @param data string containing any data already retrieved from the socket
+ -- @param pos number containing the offset into the data buffer
+ -- @return data string containing the initial and any additional data
+ readPacket = function(socket, data, pos)
+ pos = pos or 1
+ data = data or ""
+
+ -- Make sure that we have enough data for the header
+ local targetlen = pos - 1 + 5 -- header is 5 bytes (structure >BI4)
+ while #data < targetlen do
+ local status, extra = socket:receive_bytes(targetlen - #data)
+ if not status then
+ stdnse.debug1("Not enough data for PGSQL response header")
+ return nil, extra
+ end
+ data = data .. extra
+ end
+
+ local _, header = v3.decodeHeader(data, pos)
+
+ targetlen = targetlen + header.len - 4 -- header.len is 4 bytes
+ while #data < targetlen do
+ local status, extra = socket:receive_bytes(targetlen - #data)
+ if not status then
+ stdnse.debug1("Not enough data for declared PGSQL response size")
+ return nil, extra
+ end
+ data = data .. extra
+ end
+ return data
+ end,
+
+ --- Decodes the postgres header
+ --
+ -- @param data string containing the server response
+ -- @param pos number containing the offset into the data buffer
+ -- @return pos number containing the offset after decoding
+ -- @return header table containing <code>type</code> and <code>len</code>
+ decodeHeader = function(data, pos)
+ local ptype, len
+
+ ptype, len, pos = string.unpack(">BI4", data, pos)
+ return pos, { ['type'] = ptype, ['len'] = len }
+ end,
+
+ --- Process the server response
+ --
+ -- @param data string containing the server response
+ -- @param pos number containing the offset into the data buffer
+ -- @return pos number containing offset after decoding
+ -- @return response string containing decoded data
+ -- error message if pos is -1
+ processResponse = function(data, pos)
+ pos = pos or 1
+
+ if not data then
+ return -1, "Not enough response data"
+ end
+
+ local header
+ pos, header = v3.decodeHeader( data, pos )
+ if not v3.messageDecoder[header.type] then
+ stdnse.debug1("Missing decoder for %d", header.type )
+ return -1, ("Missing decoder for %d"):format(header.type)
+ end
+
+ local response
+ pos, response = v3.messageDecoder[header.type](data, header.len, pos)
+ if pos == -1 then
+ return -1, "Decoding failed"
+ end
+
+ response.type = header.type
+ return pos, response
+ end,
+
+ --- Attempts to authenticate to the pgsql server
+ -- Supports plain-text and MD5 authentication
+ --
+ -- @param socket socket already connected to the pgsql server
+ -- @param params table containing any additional parameters <code>authtype</code>, <code>version</code>
+ -- @param username string containing the username to use for authentication
+ -- @param password string containing the password to use for authentication
+ -- @param salt string containing the cryptographic salt value
+ -- @return status true on success, false on failure
+ -- @return result table containing parameter status information,
+ -- result string containing an error message if login fails
+ loginRequest = function ( socket, params, username, password, salt )
+
+ local creds
+ if params.authtype == AuthenticationType.MD5 then
+ creds = createMD5LoginHash(username, password, salt)
+ elseif params.authtype == AuthenticationType.Plain then
+ creds = password
+ elseif params.authtype == AuthenticationType.Success then
+ return true, nil
+ else
+ return false, "Unexpected authentication type 0x" .. stdnse.tohex(params.authtype)
+ end
+
+ local status, err = socket:send(string.pack(">BI4z", MessageType.PasswordMessage, #creds + 5, creds))
+ if not status then
+ return false, "Unable to send AuthRequest: " .. err
+ end
+
+ local data = v3.readPacket(socket)
+ local pos, tmp = v3.processResponse(data, 1)
+
+ -- this should contain the AuthRequest packet
+ if tmp.type ~= MessageType.AuthRequest then
+ return false, "Expected AuthRequest; got something else"
+ end
+
+ if not tmp.success then
+ return false, "Login failure"
+ end
+
+ local params = {}
+ repeat
+ data = v3.readPacket(socket, data, pos)
+ pos, tmp = v3.processResponse(data, pos)
+ if ( tmp.type == MessageType.ParameterStatus ) then
+ table.insert(params, {name=tmp.key, value=tmp.value})
+ end
+ until pos >= data:len() or pos == -1
+
+ return true, {params}
+ end,
+
+ --- Sends a startup message to the server containing the username and database to connect to
+ --
+ -- @param socket socket already connected to the pgsql server
+ -- @param user string containing the name of the user
+ -- @param database string containing the name of the database
+ -- @return status true on success, false on failure
+ -- @return table containing a processed response from <code>processResponse</code>
+ -- string containing error message if status is false
+ sendStartup = function(socket, user, database )
+ local data, response, status, pos
+ local proto_ver, ptype, _, tmp
+
+ proto_ver = 0x0030000
+ data = string.pack(">I4zzzzB", proto_ver, "user", user, "database", database, 0)
+ data = string.pack(">I4", data:len() + 4) .. data
+
+ socket:send( data )
+
+ -- attempt to verify version
+ status, data = socket:receive_bytes( 2 )
+
+ if ( not(status) ) then
+ return false, "sendStartup failed"
+ end
+
+ if ( not(status) or data:match("^EF") ) then
+ return false, "Incorrect version"
+ end
+
+ data = v3.readPacket(socket, data )
+ pos, response = v3.processResponse( data )
+
+ if ( pos < 0 or response.type == MessageType.Error) then
+ return false, response.error.message or "unknown error"
+ end
+
+ return true, response
+ end
+}
+
+
+--- Sends a packet requesting SSL communication to be activated
+--
+-- @param socket socket already connected to the pgsql server
+-- @return boolean true if request was accepted, false if request was denied
+function requestSSL(socket)
+ -- SSLRequest
+ local ssl_req_code = 80877103
+ local data = string.pack( ">I4I4", 8, ssl_req_code)
+ local status, response
+
+ socket:send(data)
+ status, response = socket:receive_bytes(1)
+
+ if ( not(status) ) then
+ return false
+ end
+
+ if ( response == 'S' ) then
+ return true
+ end
+
+ return false
+end
+
+--- Creates a cryptographic hash to be used for login
+--
+-- @param username username
+-- @param password password
+-- @param salt salt
+-- @return string suitable for login request
+function createMD5LoginHash(username, password, salt)
+ local md5_1 = stdnse.tohex(openssl.md5(password..username))
+ return "md5" .. stdnse.tohex(openssl.md5(md5_1 .. salt))
+end
+
+--- Prints the contents of the error table returned from the Error message decoder
+--
+-- @param dberror table containing the error
+function printErrorMessage( dberror )
+ if not dberror then
+ return
+ end
+ for k, v in pairs(dberror) do
+ stdnse.debug1("%s=%s", k, v)
+ end
+end
+
+--- Attempts to determine if the server supports v3 or v2 of the protocol
+--
+-- @param host table
+-- @param port table
+-- @return class v2 or v3
+function detectVersion(host, port)
+ local status, response
+ local socket = nmap.new_socket()
+
+ socket:connect(host, port)
+ status, response = v3.sendStartup(socket, "versionprobe", "versionprobe")
+ socket:close()
+
+ if ( not(status) and response == 'Incorrect version' ) then
+ return v2
+ end
+
+ return v3
+end
+
+return _ENV;