diff --git a/CHANGELOG b/CHANGELOG index 2b2ee9bd1..09bcca07a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added authentication support to MongoDB library and modified existing + scripts to support it. Added the script mongodb-brute to perform password + brute force guessing. [Patrik] + o Added a --nsock-engine option to nmap, nping and ncat to enforce use of a given nsock IO engine. [Henri] diff --git a/nselib/mongodb.lua b/nselib/mongodb.lua index 1e7540980..4fa6c8994 100644 --- a/nselib/mongodb.lua +++ b/nselib/mongodb.lua @@ -4,17 +4,27 @@ -- @author Martin Holst Swende -- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html -- --- Version 0.1 +-- Version 0.2 +-- +-- @args mongodb.db - the database to use for authentication -- Created 01/13/2010 - v0.1 - created by Martin Holst Swende +-- Revised 01/03/2012 - v0.2 - added authentication support + module("mongodb", package.seeall) require("bin") ---require("bson") +stdnse.silent_require "openssl" + + +-- this is not yet widely implemented but at least used for authentication +-- ideally, it would be used to set the database against which operations, +-- that do not require a specific database, should run +local arg_DB = stdnse.get_script_args("mongodb.db") -- Some lazy shortcuts local function dbg(str,...) - stdnse.print_debug("MngoDb:"..str, unpack(arg)) + stdnse.print_debug(3, "MngoDb:"..str, unpack(arg)) end --local dbg =stdnse.print_debug @@ -96,17 +106,23 @@ end --@return result : a string of binary data OR error message function toBson(dict) - local elements = "" + local elements = "" --Put id first if dict._id then local status,res = _element_to_bson("_id", dict._id) if not status then return false, res end elements = elements..res + elseif ( dict._cmd ) then + for k, v in pairs(dict._cmd) do + local status,res = _element_to_bson(k, v) + if not status then return false, res end + elements = elements..res + end end --Concatenate binary values for key, value in pairs( dict ) do - dbg("dictionary to bson : key,value =(%s,%s)",key,value) - if key ~= "_id" then + if key ~= "_id" and key ~= "_cmd" then + dbg("dictionary to bson : key,value =(%s,%s)",key,value) local status,res = _element_to_bson(key,value) if not status then return false, res end elements = elements..res @@ -463,7 +479,6 @@ function buildInfoQuery(responseTo) local query = {buildinfo = 1} return createQuery(collectionName, query) end - --Reads an int32 from data --@return int32 value --@return data unread @@ -578,6 +593,38 @@ function query(socket, data) end return true,result, residualData end + +function login(socket, db, username, password) + + local collectionName = ("%s.$cmd"):format(arg_DB or db) + local query = { getnonce = 1 } + local status, packet = createQuery(collectionName, query) + local response + status, response = mongodb.query(socket, packet) + if ( not(status) or not(response.nonce) ) then + return false, "Failed to retrieve nonce" + end + + local nonce = response.nonce + local pwdigest = stdnse.tohex(openssl.md5(username .. ':mongo:' ..password)) + local digest = stdnse.tohex(openssl.md5(nonce .. username .. pwdigest)) + + query = { user = username, nonce = nonce, key = digest } + query._cmd = { authenticate = 1 } + + local status, packet = createQuery(collectionName, query) + status, response = mongodb.query(socket, packet) + if ( not(status) ) then + return status, response + elseif ( response.errmsg == "auth fails" ) then + return false, "Authentication failed" + elseif ( response.errmsg ) then + return false, response.errmsg + end + return status, response +end + + --- Converts a quert result as received from MongoDB query into nmap "result" table -- @param resultTable table as returned from a quer -- @return table suitable for stdnse.format_output diff --git a/scripts/mongodb-brute.nse b/scripts/mongodb-brute.nse new file mode 100644 index 000000000..0ff27246f --- /dev/null +++ b/scripts/mongodb-brute.nse @@ -0,0 +1,101 @@ +description = [[ +Performs brute force password guessing against the MongoDB database. +]] + +--- +-- @usage +-- nmap -p 27017 --script mongodb-brute +-- +-- @output +-- PORT STATE SERVICE +-- 27017/tcp open mongodb +-- | mongodb-brute: +-- | Accounts +-- | root:Password1 - Valid credentials +-- | Statistics +-- |_ Performed 3542 guesses in 9 seconds, average tps: 393 +-- + +require 'brute' +require 'mongodb' +require 'shortport' + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "brute"} + +local arg_db = stdnse.get_script_args(SCRIPT_NAME .. ".db") or "admin" + +portrule = shortport.port_or_service({27017}, {"mongodb"}) + +Driver = { + + new = function(self, host, port, options) + local o = { host = host, port = port, sock = nmap.new_socket() } + setmetatable(o, self) + self.__index = self + return o + end, + + connect = function(self) + return self.sock:connect(self.host, self.port) + end, + + login = function(self, username, password) + local status, resp = mongodb.login(self.sock, arg_db, username, password) + if ( status ) then + return true, brute.Account:new(username, password, creds.State.VALID) + elseif ( resp ~= "Authentication failed" ) then + local err = brute.Error:new( err ) + err:setRetry( true ) + return false, err + end + return false, brute.Error:new( "Incorrect password" ) + end, + + disconnect = function(self) + return self.sock:close() + end, + +} + +local function needsAuth(host, port) + local socket = nmap.new_socket() + local status, result = socket:connect(host, port) + if ( not(status) ) then + return false, "Failed to connect to server" + end + + local packet + status, packet = mongodb.listDbQuery() + if ( not(status) ) then + return false, result + end + + --- Send packet + status, result = mongodb.query(socket, packet) + if ( not(status) ) then + return false, result + end + + socket:close() + if ( status and result.errmsg ) then + return true + end + return false +end + +action = function(host, port) + + if ( not(needsAuth(host, port)) ) then + return "No authentication needed" + end + + local engine = brute.Engine:new(Driver, host, port ) + + engine.options.script_name = SCRIPT_NAME + engine.options.firstonly = true + status, result = engine:start() + + return result +end diff --git a/scripts/mongodb-databases.nse b/scripts/mongodb-databases.nse index 11736d557..4c354cd00 100644 --- a/scripts/mongodb-databases.nse +++ b/scripts/mongodb-databases.nse @@ -29,18 +29,22 @@ Attempts to get a list of tables from a MongoDB database. -- | name = admin -- |_ totalSize = 167772160 --- version 0.1 +-- version 0.2 -- Created 01/12/2010 - v0.1 - created by Martin Holst Swende - +-- Revised 01/03/2012 - v0.2 - added authentication support author = "Martin Holst Swende" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"default", "discovery", "safe"} +dependencies = {"mongodb-brute"} + +require "creds" require "mongodb" require "shortport" portrule = shortport.port_or_service({27017}, {"mongodb"}) + function action(host,port) local socket = nmap.new_socket() @@ -56,6 +60,19 @@ function action(host,port) try( socket:connect(host, port) ) + -- uglyness to allow creds.mongodb to work, as the port is not recognized + -- as mongodb, unless a service scan was run + local ps = port.service + port.service = 'mongodb' + local c = creds.Credentials:new(creds.ALL_DATA, host, port) + for cred in c:getCredentials(creds.State.VALID + creds.State.PARAM) do + local status, err = mongodb.login(socket, "admin", cred.user, cred.pass) + if ( not(status) ) then + return err + end + end + port.service = ps + local req, result, packet, err, status --Build packet status, packet = mongodb.listDbQuery() diff --git a/scripts/mongodb-info.nse b/scripts/mongodb-info.nse index 3ff334836..b0001bb7e 100644 --- a/scripts/mongodb-info.nse +++ b/scripts/mongodb-info.nse @@ -41,18 +41,25 @@ Attempts to get build info and server status from a MongoDB database. -- | note = fields vary by platform -- |_ page_faults = 0 --- version 0.2 +-- version 0.3 -- Created 01/12/2010 - v0.1 - created by Martin Holst Swende +-- Revised 01/03/2012 - v0.3 - added authentication support author = "Martin Holst Swende" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"default", "discovery", "safe"} +dependencies = {"mongodb-brute"} + +require "creds" require "mongodb" require "shortport" +local arg_db = stdnse.get_script_args(SCRIPT_NAME .. ".db") or "admin" + portrule = shortport.port_or_service({27017}, {"mongodb"}) + function action(host,port) local socket = nmap.new_socket() @@ -68,9 +75,22 @@ function action(host,port) try( socket:connect(host, port) ) - local req, status, statusresponse, buildinfo, packet, err + local req, statusresponse, buildinfo, err - status, packet = mongodb.serverStatusQuery() + -- uglyness to allow creds.mongodb to work, as the port is not recognized + -- as mongodb, unless a service scan was run + local ps = port.service + port.service = 'mongodb' + local c = creds.Credentials:new(creds.ALL_DATA, host, port) + for cred in c:getCredentials(creds.State.VALID + creds.State.PARAM) do + local status, err = mongodb.login(socket, arg_db, cred.user, cred.pass) + if ( not(status) ) then + return err + end + end + port.service = ps + + local status, packet = mongodb.serverStatusQuery() if not status then return packet end status,statQResult = mongodb.query(socket, packet) diff --git a/scripts/script.db b/scripts/script.db index 15d73be97..5ea7abae5 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -183,6 +183,7 @@ Entry { filename = "membase-http-info.nse", categories = { "discovery", "safe", Entry { filename = "memcached-info.nse", categories = { "discovery", "safe", } } Entry { filename = "metasploit-xmlrpc-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "modbus-discover.nse", categories = { "discovery", "intrusive", } } +Entry { filename = "mongodb-brute.nse", categories = { "brute", "intrusive", } } 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 = { "brute", "intrusive", } }