diff --git a/CHANGELOG b/CHANGELOG index d02c823c9..1bed409f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,11 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the script redis-info that lists version and statistic information + gathered from the Redis network key-value store. [Patrik] + +o [NSE] Added the script redis-brute that performs brute force password + guessing against the Redis network key-value store. [Patrik] + o [NSE] Added the script http-proxy-brute that performs brute force password guessing against HTTP proxy servers. [Patrik] diff --git a/nselib/redis.lua b/nselib/redis.lua new file mode 100644 index 000000000..233ec80be --- /dev/null +++ b/nselib/redis.lua @@ -0,0 +1,142 @@ + + + +module(... or "redis", package.seeall) + +local match = require 'match' + + +Request = { + + new = function(self, cmd, ...) + local o = { cmd = cmd, args = {...} } + setmetatable (o,self) + self.__index = self + return o + end, + + __tostring = function(self) + local output = ("*%s\r\n$%d\r\n%s\r\n"):format(#self.args + 1, #self.cmd, self.cmd) + + for _, arg in ipairs(self.args) do + arg = tostring(arg) + output = output .. ("$%s\r\n%s\r\n"):format(#arg, arg) + end + + return output + end + +} + + +Response = { + + Type = { + STATUS = 0, + ERROR = 1, + INTEGER = 2, + BULK = 3, + MULTIBULK = 4, + }, + + new = function(self, socket) + local o = { socket = socket } + setmetatable (o,self) + self.__index = self + return o + end, + + receive = function(self) + local status, data = self.socket:receive_buf("\r\n") + if ( not(status) ) then + return false, "Failed to receive data from server" + end + + -- if we have a status, integer or error message + if ( data:match("^[%-%+%:]") ) then + local response = { data = data } + local t = data:match("^([-+:])") + if ( t == "-" ) then + response.type = Response.Type.ERROR + elseif ( t == "+" ) then + response.type = Response.Type.STATUS + elseif ( t == ":" ) then + response.type = Response.Type.INTEGER + end + + return true, response + end + + -- process bulk reply + if ( data:match("^%$") ) then + -- non existing key + if ( data == "$-1" ) then + return true, nil + end + + local len = tonumber(data:match("^%$(%d*)")) + -- we should only have a single line, so we can just peel of the length + status, data = self.socket:receive_buf(match.numbytes(len)) + if( not(status) ) then + return false, "Failed to receive data from server" + end + + return true, { data = data, type = Response.Type.BULK } + end + + -- process multi-bulk reply + if ( data:match("^%*%d*") ) then + local count = data:match("^%*(%d*)") + local results = {} + + for i=1, count do + -- peel of the length + local status = self.socket:receive_buf("\r\n") + if( not(status) ) then + return false, "Failed to receive data from server" + end + + status, data = self.socket:receive_buf("\r\n") + if( not(status) ) then + return false, "Failed to receive data from server" + end + table.insert(results, data) + end + return true, { data = results, type = MULTIBULK } + end + + return false, "Unsupported response" + end, + + + +} + +Helper = { + + new = function(self, host, port) + local o = { host = host, port = port } + setmetatable (o,self) + self.__index = self + return o + end, + + connect = function(self) + self.socket = nmap.new_socket() + return self.socket:connect(self.host, self.port) + end, + + reqCmd = function(self, cmd, ...) + local req = Request:new(cmd, ...) + local status, err = self.socket:send(tostring(req)) + if (not(status)) then + return false, "Failed to send command to server" + end + return Response:new(self.socket):receive() + end, + + close = function(self) + return self.socket:close() + end + +} \ No newline at end of file diff --git a/scripts/redis-brute.nse b/scripts/redis-brute.nse new file mode 100644 index 000000000..9c9585d4f --- /dev/null +++ b/scripts/redis-brute.nse @@ -0,0 +1,108 @@ +description = [[ +Performs brute force passwords guessing against a Redis key-value store +]] + +--- +-- @usage +-- nmap -p 6379 --script redis-brute +-- +-- @output +-- PORT STATE SERVICE +-- 6379/tcp open unknown +-- | redis-brute: +-- | Accounts +-- | toledo - Valid credentials +-- | Statistics +-- |_ Performed 5000 guesses in 3 seconds, average tps: 1666 +-- +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "brute"} + +require 'brute' +require 'redis' +require 'shortport' + +portrule = shortport.port_or_service(6379, "redis-server") + +local function fail(err) return ("\n ERROR: %s"):format(err) end + +Driver = { + + new = function(self, host, port) + local o = { host = host, port = port } + setmetatable(o, self) + self.__index = self + return o + end, + + connect = function( self ) + self.helper = redis.Helper:new(self.host, self.port) + return self.helper:connect() + end, + + login = function( self, username, password ) + local status, response = self.helper:reqCmd("AUTH", password) + + -- some error occured, attempt to retry + if ( status and response.type == redis.Response.Type.ERROR and + "-ERR invalid password" == response.data ) then + return false, brute.Error:new( "Incorrect password" ) + elseif ( status and response.type == redis.Response.Type.STATUS and + "+OK" ) then + return true, brute.Account:new( "", password, creds.State.VALID) + else + local err = brute.Error:new( err ) + err:setRetry( true ) + return false, err + end + + end, + + disconnect = function(self) + return self.helper:close() + end, + +} + + +local function checkRedis(host, port) + + local helper = redis.Helper:new(host, port) + local status = helper:connect() + if( not(status) ) then + return false, "Failed to connect to server" + end + + local status, response = helper:reqCmd("INFO") + if ( not(status) ) then + return false, "Failed to request INFO command" + end + + if ( redis.Response.Type.ERROR == response.type ) then + if ( "-ERR operation not permitted" == response.data ) then + return true + end + end + + return false, "Server does not require authentication" +end + +action = function(host, port) + + local status, err = checkRedis(host, port) + if ( not(status) ) then + return fail(err) + end + + local engine = brute.Engine:new(Driver, host, port ) + + engine.options.script_name = SCRIPT_NAME + engine.options.firstonly = true + engine.options:setOption( "passonly", true ) + + status, result = engine:start() + return result +end \ No newline at end of file diff --git a/scripts/redis-info.nse b/scripts/redis-info.nse new file mode 100644 index 000000000..7c0ea4d54 --- /dev/null +++ b/scripts/redis-info.nse @@ -0,0 +1,113 @@ +description = [[ +Gets information from a Redis key-value store +]] + +--- +-- @usage +-- nmap -p 6379 --script redis-info +-- +-- @output +-- PORT STATE SERVICE +-- 6379/tcp open unknown +-- | redis-info: +-- | Version 2.2.11 +-- | Architecture 64 bits +-- | Process ID 17821 +-- | Used CPU (sys) 2.37 +-- | Used CPU (user) 1.02 +-- | Connected clients 1 +-- | Connected slaves 0 +-- | Used memory 780.16K +-- |_ Role master +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} +dependencies = {"redis-brute"} + +require 'creds' +require 'redis' +require 'shortport' +require 'tab' + +portrule = shortport.port_or_service(6379, "redis-server") + +local function fail(err) return ("\n ERROR: %s"):format(err) end + +local filter = { + + ["redis_version"] = { name = "Version" }, + ["arch_bits"] = { name = "Architecture", func = function(v) return ("%s bits"):format(v) end }, + ["process_id"] = { name = "Process ID"}, + ["uptime"] = { name = "Uptime", func = function(v) return ("%s seconds"):format(v) end }, + ["used_cpu_sys"]= { name = "Used CPU (sys)"}, + ["used_cpu_user"] = { name = "Used CPU (user)"}, + ["connected_clients"] = { name = "Connected clients"}, + ["connected_slaves"] = { name = "Connected slaves"}, + ["used_memory_human"] = { name = "Used memory"}, + ["role"] = { name = "Role"} + +} + +local order = { + "redis_version", "arch_bits", "process_id", "used_cpu_sys", + "used_cpu_user", "connected_clients", "connected_slaves", + "used_memory_human", "role" +} + +action = function(host, port) + + local helper = redis.Helper:new(host, port) + local status = helper:connect() + if( not(status) ) then + return fail("Failed to connect to server") + end + + -- do we have a service password + local c = creds.Credentials:new(creds.ALL_DATA, host, port) + local cred = c:getCredentials(creds.State.VALID + creds.State.PARAM)() + + if ( cred and cred.pass ) then + local status, response = helper:reqCmd("AUTH", cred.pass) + if ( not(status) ) then + helper:close() + return fail(response) + end + end + + local status, response = helper:reqCmd("INFO") + if ( not(status) ) then + helper:close() + return fail(response) + end + helper:close() + + if ( redis.Response.Type.ERROR == response.type ) then + if ( "-ERR operation not permitted" == response.data ) then + return fail("Authentication required") + end + return fail(response.data) + end + + local restab = stdnse.strsplit("\r\n", response.data) + if ( not(restab) or 0 == #restab ) then + return fail("Failed to parse response from server") + end + + local kvs = {} + for _, item in ipairs(restab) do + local k, v = item:match("^([^:]*):(.*)$") + kvs[k] = v + end + + local result = tab.new(2) + for _, item in ipairs(order) do + if ( kvs[item] ) then + local name = filter[item].name + local val = ( filter[item].func and filter[item].func(kvs[item]) or kvs[item] ) + tab.addrow(result, name, val) + end + end + return stdnse.format_output(true, tab.dump(result)) +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 323a7363d..45942db13 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -218,6 +218,8 @@ Entry { filename = "qscan.nse", categories = { "discovery", "safe", } } Entry { filename = "quake3-info.nse", categories = { "default", "discovery", "safe", "version", } } Entry { filename = "quake3-master-getservers.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "realvnc-auth-bypass.nse", categories = { "auth", "default", "safe", } } +Entry { filename = "redis-brute.nse", categories = { "brute", "intrusive", } } +Entry { filename = "redis-info.nse", categories = { "discovery", "safe", } } Entry { filename = "resolveall.nse", categories = { "discovery", "safe", } } Entry { filename = "reverse-index.nse", categories = { "safe", } } Entry { filename = "rexec-brute.nse", categories = { "brute", "intrusive", } }