diff --git a/CHANGELOG b/CHANGELOG index b2d6a4458..57fcafac9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,8 +2,11 @@ [NOT YET RELEASED] -o [NSE] Added the script ldap-search which queries a LDAP directory - for either all, or a number of pre-defined object types. [Patrik] +o [NSE] Added a new library for PostgreSQL and the script pgsql-brute that uses + it to guess credentials. [Patrik] + +o [NSE] Added the script ldap-search which queries a LDAP directory for either + all, or a number of pre-defined object types. [Patrik] o The redistributable Visual C++ runtime components installer (vcredist_x86.exe) has been upgraded to version 9.0.30729.4148. Axel diff --git a/nselib/pgsql.lua b/nselib/pgsql.lua new file mode 100644 index 000000000..8dda10e44 --- /dev/null +++ b/nselib/pgsql.lua @@ -0,0 +1,623 @@ +--- +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- @author = "Patrik Karlsson " +-- +-- 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 reference +-- ref: http://developer.postgresql.org/pgdocs/postgres/protocol.html +-- ref: http://developer.postgresql.org/pgdocs/postgres/protocol-flow.html +-- ref: http://developer.postgresql.org/pgdocs/postgres/protocol-message-formats.html +-- +-- Version 0.3 +-- Created 02/05/2010 - v0.1 - created by Patrik Karlsson +-- Revised 02/20/2010 - v0.2 - added detectVersion to automaticaly detect and return +-- the correct version class +-- Revised 03/04/2010 - v0.3 - added support for trust authentication method + +module(... or "pgsql",package.seeall) + +require("openssl") +require("bit") + +--- 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) + local padding = len - str:len() + + if ( padding < 0 ) then + return str + end + for i=1,padding do + str = str .. string.char(0x00) + end + return str + end, + + messageDecoder = { + + --- Decodes an Auth Request packet + -- + -- @param data string containing raw data recieved 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 salt and success + -- error string containing error message if pos is -1 + [MessageType.AuthRequest] = function( data, len, pos ) + local _, authtype + local response = {} + pos, authtype = bin.unpack(">I", data, pos) + + if ( authtype == AuthenticationType.MD5 ) then + if ( len - pos + 1 ) < 3 then + return -1, "ERROR: Malformed AuthRequest received" + end + pos, response.salt = bin.unpack("A4", data, pos) + elseif ( authtype == AuthenticationType.Plain ) then + --do nothing + elseif ( authtype == 0 ) then + response.success = true + else + stdnse.print_debug( ("unknown auth type: %d"):format(authtype) ) + end + + response.authtype = authtype + return pos, response + end, + + + + --- Decodes an Error packet + -- + -- @param data string containing raw data recieved 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 error.severity, + -- error.code, error.message, error.file, + -- error.line and error.routine + [MessageType.Error] = function( data, len, pos ) + local tmp = data:sub(pos, pos + len - 4) + local response = {} + local pos_end = pos + len + + response.error = {} + pos, response.error.message = bin.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 + + pos, ptype = bin.unpack("C", 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.print_debug( ("Missing decoder for %d"):format(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.print_debug("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 processResponse + -- 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 = bin.pack(">I>IAAAAA", 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 authtype, version + -- @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 crypthographic 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.print_debug("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 = bin.pack( ">Iz", 40, hash) + try( socket:send( data ) ) + elseif ( params.authtype == AuthenticationType.Plain ) then + local data + data = bin.pack(">Iz", 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 recieved 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 salt and success + -- error string containing error message if pos is -1 + [MessageType.AuthRequest] = function( data, len, pos ) + local _, authtype + local response = {} + + pos, authtype = bin.unpack(">I", data, pos) + + if ( authtype == AuthenticationType.MD5 ) then + if ( len - pos + 1 ) < 3 then + return -1, "ERROR: Malformed AuthRequest received" + end + pos, response.salt = bin.unpack("A4", data, pos) + elseif ( authtype == AuthenticationType.Plain ) then + --do nothing + elseif ( authtype == 0 ) then + response.success = true + else + stdnse.print_debug( "unknown auth type: %d", authtype ) + end + + response.authtype = authtype + return pos, response + end, + + --- Decodes an ParameterStatus packet + -- + -- @param data string containing raw data recieved 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 key and value + [MessageType.ParameterStatus] = function( data, len, pos ) + local tmp, _ + local response = {} + + tmp = data:sub(pos, pos + len - 4) + _, response.key, response.value = bin.unpack("zz", tmp) + return pos + len - 4, response + end, + + --- Decodes an Error packet + -- + -- @param data string containing raw data recieved 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 error.severity, + -- error.code, error.message, error.file, + -- error.line and error.routine + [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 + pos, prefix, value = bin.unpack("Az", 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 recieved 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 pid and key + -- 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 + + pos, response.pid, response.key = bin.unpack(">I>I", data, pos) + return pos, response + end, + + --- Decodes an ReadyForQuery packet + -- + -- @param data string containing raw data recieved 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 status + -- 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 + + pos, response.status = bin.unpack("C", 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) + + local pos = pos or 1 + local data = data or "" + local status = true + local tmp = "" + local ptype, len + local header + + local catch = function() socket:close() stdnse.print_debug("processResponse(): failed") end + local try = nmap.new_try(catch) + + if ( data:len() - pos < 5 ) then + status, tmp = socket:receive_bytes( 5 - ( data:len() - pos ) ) + end + + if not status then + return nil, "Failed to read packet" + end + + if tmp:len() ~= 0 then + data = data .. tmp + end + + pos, header = v3.decodeHeader(data,pos) + + while data:len() < header.len do + data = data .. try(socket:receive_bytes( ( header.len + 1 ) - data:len() )) + 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 type and len + decodeHeader = function(data, pos) + local ptype, len + + pos, ptype, len = bin.unpack("C>I", 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) + local ptype, len, status, response + local pos = pos or 1 + local header + + pos, header = v3.decodeHeader( data, pos ) + + if v3.messageDecoder[header.type] then + pos, response = v3.messageDecoder[header.type](data, header.len, pos) + + if pos ~= -1 then + response.type = header.type + return pos, response + end + else + stdnse.print_debug( "Missing decoder for %d", header.type ) + return -1, ("Missing decoder for %d"):format(header.type) + end + return -1, "Decoding failed" + 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 authtype, version + -- @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 crypthographic 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.print_debug("loginRequest(): failed") end + local try = nmap.new_try(catch) + local response, header = {}, {} + local status, data, len, tmp, _ + local pos = 1 + + if ( params.authtype == AuthenticationType.MD5 ) then + local hash = pgsql.createMD5LoginHash(username, password, salt) + data = bin.pack( "C>Iz", MessageType.PasswordMessage, 40, hash ) + try( socket:send( data ) ) + elseif ( params.authtype == AuthenticationType.Plain ) then + local data + data = bin.pack("C>Iz", MessageType.PasswordMessage, password:len() + 4, password) + try( socket:send( data ) ) + elseif ( params.authtype == AuthenticationType.Success ) then + return true, nil + end + + data, response.params = "", {} + + data = v3.readPacket(socket, data, 1) + 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 + + repeat + data = v3.readPacket(socket, data, pos) + pos, tmp = v3.processResponse(data, pos) + if ( tmp.type == MessageType.ParameterStatus ) then + table.insert(response.params, {name=tmp.key, value=tmp.value}) + end + until pos >= data:len() or pos == -1 + + return true, response + 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 processResponse + -- 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 = bin.pack(">IzzzzH", proto_ver, "user", user, "database", database, 0) + data = bin.pack(">I", 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 = bin.pack( ">I>I", 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 string username +-- @param string password +-- @param string salt +-- @return string suitable for login request +function createMD5LoginHash(username, password, salt) + local md5_1 = select( 2, bin.unpack( "H16", openssl.md5(password..username) ) ):lower() + return "md5" .. select( 2, bin.unpack("H16", openssl.md5( md5_1 .. salt ) ) ):lower() +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.print_debug( ("%s=%s"):format(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.ip, port.number, "tcp") + status, response = v3.sendStartup(socket, "versionprobe", "versionprobe") + socket:close() + + if ( not(status) and response == 'Incorrect version' ) then + return v2 + end + + return v3 +end \ No newline at end of file diff --git a/scripts/pgsql-brute.nse b/scripts/pgsql-brute.nse new file mode 100644 index 000000000..ff13b83df --- /dev/null +++ b/scripts/pgsql-brute.nse @@ -0,0 +1,178 @@ +description = [[ +Performs password guessing against Postgresql +]] + +--- +-- @usage +-- nmap -p 5432 --script pgsql-brute +-- +-- @output +-- 5432/tcp open pgsql +-- | pgsql-brute: +-- | root: => Login Correct +-- |_ test:test => Login Correct +-- +-- @args pgsql.nossl If set to 1 or true disables SSL. +-- @args pgsql.version Force protocol version 2 or 3 +-- +-- SSL Encryption +-- -------------- +-- We need to handle several cases of SSL support +-- o SSL can be supported on a server level +-- o SSL can be enforced per host or network level +-- o SSL can be denied per host or network level +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} + +require 'shortport' +require 'stdnse' +require 'unpwdb' + +-- Version 0.3 +-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson +-- Revised 02/20/2010 - v0.2 - moved version detection to pgsql library +-- Revised 03/04/2010 - v0.3 - added code from ssh-hostkey.nse to check for SSL support +-- - added support for trusted authentication method + +-- ripped from ssh-hostkey.nse +-- openssl is required for this script +if pcall(require,"openssl") then + require("pgsql") +else + portrule = function() return false end + action = function() end + stdnse.print_debug( 3, "Skipping %s script because OpenSSL is missing.", filename ) + return; +end + +portrule = shortport.port_or_service(5432, "postgresql") + +--- Connect a socket to the server with or without SSL +-- +-- @param host table as received by the action function +-- @param port table as received by the action function +-- @param ssl boolean, if true connect using SSL +-- @return socket connected to server +local function connectSocket(host, port, ssl) + local socket = nmap.new_socket() + + -- set a reasonable timeout value + socket:set_timeout(5000) + socket:connect(host.ip, port.number, "tcp") + + -- let's be responsible and avoid sending communication in the clear + if ( ssl ) then + status = pgsql.requestSSL(socket) + if ( status ) then + socket:reconnect_ssl() + end + end + return socket +end + +action = function( host, port ) + + local status, response, ssl_enable, output + local max_time = unpwdb.timelimit() ~= nil and unpwdb.timelimit() * 1000 or -1 + local clock_start = nmap.clock_ms() + local result, response, status, aborted, nossl = {}, nil, nil, false, false + local valid_accounts = {} + local pg + + if ( nmap.registry.args['pgsql.version'] ) then + if ( tonumber(nmap.registry.args['pgsql.version']) == 2 ) then + pg = pgsql.v2 + elseif ( tonumber(nmap.registry.args['pgsql.version']) == 3 ) then + pg = pgsql.v3 + else + stdnse.print_debug("pgsql-brute: Unsupported version %s", nmap.registry.args['pgsql.version']) + return + end + else + pg = pgsql.detectVersion(host, port ) + end + + status, usernames = unpwdb.usernames() + if ( not(status) ) then return end + + status, passwords = unpwdb.passwords() + if ( not(status) ) then return end + + -- If the user explicitly does not disable SSL, enforce it + if ( ( nmap.registry.args['pgsql.nossl'] == 'true' ) or + ( nmap.registry.args['pgsql.nossl'] == '1' ) ) then + nossl = true + end + + for username in usernames do + ssl_enable = not(nossl) + for password in passwords do + if max_time>0 and nmap.clock_ms() - clock_start > max_time then + aborted=true + break + end + + stdnse.print_debug( string.format("Trying %s/%s ...", username, password ) ) + socket = connectSocket( host, port, ssl_enable ) + status, response = pg.sendStartup(socket, username, username) + + -- if nossl is enforced by the user, we're done + if ( not(status) and nossl ) then + break + end + + -- SSL failed, this can occure due to: + -- 1. The server does not do SSL + -- 2. SSL was denied on a per host or network level + -- + -- Attempt SSL connection + if ( not(status) ) then + socket:close() + ssl_enable = false + socket = connectSocket( host, port, ssl_enable ) + status, response = pg.sendStartup(socket, username, username) + if (not(status)) then + if ( response:match("no pg_hba.conf entry for host") ) then + stdnse.print_debug("The host was denied access to db \"%s\" as user \"%s\", aborting ...", username, username ) + break + else + stdnse.print_debug("pgsql-brute: sendStartup returned: %s", response ) + break + end + end + end + + -- Do not attempt to authenticate if authentication type is trusted + if ( response.authtype ~= pgsql.AuthenticationType.Success ) then + status, response = pg.loginRequest( socket, response, username, password, response.salt) + end + + if status then + -- Add credentials for other pgsql scripts to use + if nmap.registry.pgsqlusers == nil then + nmap.registry.pgsqlusers = {} + end + nmap.registry.pgsqlusers[username]=password + if ( response.authtype ~= pgsql.AuthenticationType.Success ) then + table.insert( valid_accounts, string.format("%s:%s => Login Correct", username, password:len()>0 and password or "" ) ) + else + table.insert( valid_accounts, string.format("%s => Trusted authentication", username ) ) + end + break + end + socket:close() + end + passwords("reset") + end + + 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/script.db b/scripts/script.db index 442fae092..99612295c 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -60,6 +60,7 @@ Entry { filename = "nfs-showmount.nse", categories = { "discovery", "safe", } } Entry { filename = "ntp-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "oracle-sid-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "p2p-conficker.nse", categories = { "default", "safe", } } +Entry { filename = "pgsql-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "pjl-ready-message.nse", categories = { "intrusive", } } Entry { filename = "pop3-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "pop3-capabilities.nse", categories = { "default", "discovery", "safe", } }