diff --git a/CHANGELOG b/CHANGELOG index db17f0977..43ccb2dbc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added cassandra-brute and cassandra-info by Vlatko Kosturjak, + scripts for the Apache Cassandra database. + o [NSE] Added ipv6-ra-flood script by Adam Števko. This script sends a flood of router advertisements, which can DoS certain operating systems including Windows. diff --git a/nselib/cassandra.lua b/nselib/cassandra.lua new file mode 100644 index 000000000..86f895f15 --- /dev/null +++ b/nselib/cassandra.lua @@ -0,0 +1,210 @@ +--- +-- Library methods for handling Cassandra Thrift communication as client +-- +-- @author Vlatko Kosturjak +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- +-- Version 0.1 +-- + +local bin = require "bin" +local nmap = require "nmap" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" +_ENV = stdnse.module("cassandra", stdnse.seeall) + +--[[ + + Cassandra Thrift protocol implementation. + + For more information about Cassandra, see: + + http://cassandra.apache.org/ + +]]-- + +-- Protocol magic strings +CASSANDRAREQ = string.char(0x80,0x01,0x00,0x01) +CASSANDRARESP = string.char(0x80,0x01,0x00,0x02) +CASSLOGINMAGIC = string.char(0x00, 0x00,0x00,0x01,0x0c,0x00,0x01,0x0d,0x00,0x01,0x0b,0x0b,0x00,0x00,0x00,0x02) +LOGINSUCC = string.char(0x00,0x00,0x00,0x01,0x00) +LOGINFAIL = string.char(0x00,0x00,0x00,0x01,0x0b) +LOGINACC = string.char(0x00,0x00,0x00,0x01,0x0c) + +--Returns string in format length+string itself +--@param str to format +--@return str : string in format length+string itself +function pack4str (str) + return (bin.pack(">I",string.len(str)) .. str) +end + +--Returns string in cassandra format for login +--@param username to put in format +--@param password to put in format +--@return str : string in cassandra format for login +function loginstr (username, password) + local str = CASSANDRAREQ .. pack4str ("login") + str = str .. CASSLOGINMAGIC + str = str .. pack4str("username") + str = str .. pack4str(username) + str = str .. pack4str("password") + str = str .. pack4str(password) + str = str .. string.char (0x00, 0x00) -- add two null on the end + return str +end + +--Invokes command over socket and returns the response +--@param socket to connect to +--@param command to invoke +--@param cnt is protocol count +--@return status : true if ok; false if bad +--@return result : value if status ok, error msg if bad +function cmdstr (command,cnt) + local str = CASSANDRAREQ .. pack4str (command) + str = str .. bin.pack(">I",cnt) + str = str .. string.char (0x00) -- add null on the end + return str +end + +--Invokes command over socket and returns the response +--@param socket to connect to +--@param command to invoke +--@param cnt is protocol count +--@return status : true if ok; false if bad +--@return result : value if status ok, error msg if bad +function sendcmd (socket, command, cnt) + local cmdstr = cmdstr (command,cnt) + local response + + local status, err = socket:send(bin.pack(">I",string.len(cmdstr))) + if ( not(status) ) then + return false, "error sending packet length" + end + + status, err = socket:send(cmdstr) + if ( not(status) ) then + return false, "error sending packet payload" + end + + status, response = socket:receive_bytes(4) + if ( not(status) ) then + return false, "error receiving length" + end + _,size = bin.unpack(">I",response,1) + + if (string.len(response) < size+4 ) then + status, resp2 = socket:receive_bytes(size+4 - string.len(response)) + if ( not(status) ) then + return false, "error receiving payload" + end + response = response .. resp2 + end + + -- magic response starts at 5th byte for 4 bytes, 4 byte for length + length of string commmand + if (string.sub(response,5,8+4+string.len(command)) ~= CASSANDRARESP..pack4str(command)) then + return false, "protocol response error" + end + + return true, response +end + +--Return Cluster Name +--@param socket to connect to +--@param cnt is protocol count +--@return status : true if ok; false if bad +--@return result : value if status ok, error msg if bad +function describe_cluster_name (socket,cnt) + local cname = "describe_cluster_name" + local size + local status,resp = sendcmd(socket,cname,cnt) + + if (not(status)) then + stdnse.print_debug(1, "sendcmd"..resp) + return false, "error in communication" + end + + -- grab the size + -- pktlen(4) + CASSANDRARESP(4) + lencmd(4) + lencmd(v) + params(7) + next byte position + position = 12+string.len(cname)+7+1 + _,size = bin.unpack(">I",resp,position) + + -- read the string after the size + local value = string.sub(resp,position+4,position+4+size-1) + return true, value +end + +--Return API version +--@param socket to connect to +--@param cnt is protocol count +--@return status : true if ok; false if bad +--@return result : value if status ok, error msg if bad +function describe_version (socket,cnt) + local cname = "describe_version" + local size + local status,resp = sendcmd(socket,cname,cnt) + + if (not(status)) then + stdnse.print_debug(1, "sendcmd"..resp) + return false, "error in communication" + end + + -- grab the size + -- pktlen(4) + CASSANDRARESP(4) + lencmd(4) + lencmd(v) + params(7) + next byte position + position = 12+string.len(cname)+7+1 + _,size = bin.unpack(">I",resp,position) + + -- read the string after the size + local value = string.sub(resp,position+4,position+4+size-1) + return true, value +end + +--Login to Cassandra +--@param socket to connect to +--@param username to connect to +--@param password to connect to +--@return status : true if ok; false if bad +--@return result : table of status ok, error msg if bad +--@return if status ok : remaining data read from socket but not used +function login (socket,username,password) + local loginstr = loginstr (username, password) + local combo = username..":"..password + + local status, err = socket:send(bin.pack(">I",string.len(loginstr))) + if ( not(status) ) then + stdnse.print_debug(3, "cannot send len "..combo) + return false, "Failed to connect to server" + end + + status, err = socket:send(loginstr) + if ( not(status) ) then + stdnse.print_debug(3, "Sent packet for "..combo) + return false, err + end + + status, response = socket:receive_bytes(22) + if ( not(status) ) then + stdnse.print_debug(3, "Receive packet for "..combo) + return false, err + end + _, size = bin.unpack(">I", response, 1) + + loginresp = string.sub(response,5,17) + if (loginresp ~= CASSANDRARESP..pack4str("login")) then + return false, "protocol error" + end + + magic = string.sub(response,18,22) + stdnse.print_debug(3, "packet for "..combo) + stdnse.print_debug(3, "packet hex: %s", stdnse.tohex(response) ) + stdnse.print_debug(3, "size packet hex: %s", stdnse.tohex(size) ) + stdnse.print_debug(3, "magic packet hex: %s", stdnse.tohex(magic) ) + + if (magic == LOGINSUCC) then + return true + else + return false, "Login failed." + end +end + +return _ENV; diff --git a/scripts/cassandra-brute.nse b/scripts/cassandra-brute.nse new file mode 100644 index 000000000..16fb37e02 --- /dev/null +++ b/scripts/cassandra-brute.nse @@ -0,0 +1,131 @@ +local brute = require "brute" +local creds = require "creds" +local nmap = require "nmap" +local shortport = require "shortport" +local stdnse = require "stdnse" +local cassandra = require "cassandra" + +description = [[ +Performs brute force password auditing against the Cassandra database. + +For more information about Cassandra, see: +http://cassandra.apache.org/ +]] + +--- +-- @usage +-- nmap -p 9160 --script=cassandra-brute +-- +-- @output +-- PORT STATE SERVICE VERSION +-- 9160/tcp open apani1? +-- | cassandra-brute: +-- | Accounts +-- | admin:lover - Valid credentials +-- | Statistics +-- |_ Performed 4581 guesses in 1 seconds, average tps: 4581 +-- + +author = "Vlatko Kosturjak" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "brute"} + +portrule = shortport.port_or_service({9160}, {"cassandra"}) + +Driver = { + + new = function(self, host, port, options) + local o = { host = host, port = port, socket = nmap.new_socket() } + setmetatable(o, self) + self.__index = self + return o + end, + + connect = function(self) + return self.socket:connect(self.host, self.port) + end, + + -- bit faster login function than in cassandra library (no protocol error checks) + login = function(self, username, password) + local response, magic, size + local loginstr = cassandra.loginstr (username, password) + + local status, err = self.socket:send(bin.pack(">I",string.len(loginstr))) + local combo = username..":"..password + if ( not(status) ) then + local err = brute.Error:new( "couldn't send length:"..combo ) + err:setAbort( true ) + return false, err + end + + status, err = self.socket:send(loginstr) + if ( not(status) ) then + local err = brute.Error:new( "couldn't send login packet: "..combo ) + err:setAbort( true ) + return false, err + end + + status, response = self.socket:receive_bytes(22) + if ( not(status) ) then + local err = brute.Error:new( "couldn't receive login reply size: "..combo ) + err:setAbort( true ) + return false, err + end + + _, size = bin.unpack(">I", response, 1) + + magic = string.sub(response,18,22) + + if (magic == cassandra.LOGINSUCC) then + stdnse.print_debug(3, "Account SUCCESS: "..combo) + return true, brute.Account:new(username, password, creds.State.VALID) + elseif (magic == cassandra.LOGINFAIL) then + stdnse.print_debug(3,"Account FAIL: "..combo) + return false, brute.Error:new( "Incorrect password" ) + elseif (magic == cassandra.LOGINACC) then + stdnse.print_debug(3, "Account VALID, but wrong password: "..combo) + return false, brute.Error:new( "Good user, bad password" ) + else + stdnse.print_debug(3, "Unrecognized packet for "..combo) + stdnse.print_debug(3, "packet hex: %s", stdnse.tohex(response) ) + stdnse.print_debug(3, "size packet hex: %s", stdnse.tohex(size) ) + stdnse.print_debug(3, "magic packet hex: %s", stdnse.tohex(magic) ) + local err = brute.Error:new( response ) + err:setRetry( true ) + return false, err + end + end, + + disconnect = function(self) + return self.socket:close() + end, + +} + +local function noAuth(host, port) + local socket = nmap.new_socket() + local status, result = socket:connect(host, port) + + local stat,err = cassandra.login (socket,"default","") + socket:close() + if (stat) then + return true + else + return false + end +end + +action = function(host, port) + + if ( noAuth(host, port) ) then + return "Any username and password would do, 'default' was used to test." + end + + local engine = brute.Engine:new(Driver, host, port ) + + engine.options.script_name = SCRIPT_NAME + engine.options.firstonly = true + local status, result = engine:start() + + return result +end diff --git a/scripts/cassandra-info.nse b/scripts/cassandra-info.nse new file mode 100644 index 000000000..d09ff8ef2 --- /dev/null +++ b/scripts/cassandra-info.nse @@ -0,0 +1,93 @@ +local creds = require "creds" +local nmap = require "nmap" +local shortport = require "shortport" +local stdnse = require "stdnse" +local bin = require "bin" +local string = require "string" + +local cassandra = stdnse.silent_require "cassandra" + +description = [[ +Attempts to get basic info and server status from a Cassandra database. + +For more information about Cassandra, see: +http://cassandra.apache.org/ +]] + +--- +-- @usage +-- nmap -p 9160 --script=cassandra-info +-- +-- @output +-- PORT STATE SERVICE REASON +-- 9160/tcp open cassandra syn-ack +-- | cassandra-info: +-- | Cluster name: Test Cluster +-- |_ Version: 19.10.0 +-- + +-- version 0.1 +-- Created 14/09/2012 - v0.1 - created by Vlatko Kosturjak + +author = "Vlatko Kosturjak" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"default", "discovery", "safe"} + +dependencies = {"cassandra-brute"} + +portrule = shortport.port_or_service({9160}, {"cassandra"}) + +function action(host,port) + + local socket = nmap.new_socket() + local cassinc = 2 -- cmd/resp starts at 2 + + -- set a reasonable timeout value + socket:set_timeout(10000) + -- do some exception / cleanup + local catch = function() + socket:close() + end + + local try = nmap.new_try(catch) + + try( socket:connect(host, port) ) + + local results = {} + + -- uglyness to allow creds.cassandra to work, as the port is not recognized + -- as cassandra even when service scan was run, taken from mongodb + local ps = port.service + port.service = 'cassandra' + 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 = cassandra.login(socket, cred.user, cred.pass) + table.insert(results, ("Using credentials: %s"):format(cred.user.."/"..cred.pass)) + if ( not(status) ) then + return err + end + end + port.service = ps + + local status, val = cassandra.describe_cluster_name(socket,cassinc) + if (not(status)) then + return "Error getting cluster name: " .. val + end + cassinc = cassinc + 1 + port.version.name ='cassandra' + port.version.product='Cassandra' + port.version.name_confidence = 100 + nmap.set_port_version(host,port) + table.insert(results, ("Cluster name: %s"):format(val)) + + local status, val = cassandra.describe_version(socket,cassinc) + if (not(status)) then + return "Error getting version: " .. val + end + cassinc = cassinc + 1 + port.version.product='Cassandra ('..val..')' + nmap.set_port_version(host,port) + table.insert(results, ("Version: %s"):format(val)) + + return stdnse.format_output(true, results) +end diff --git a/scripts/script.db b/scripts/script.db index d9ffcd8eb..4cbfe08b1 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -52,6 +52,8 @@ Entry { filename = "broadcast-wake-on-lan.nse", categories = { "broadcast", "saf Entry { filename = "broadcast-wpad-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-wsdd-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-xdmcp-discover.nse", categories = { "broadcast", "safe", } } +Entry { filename = "cassandra-brute.nse", categories = { "brute", "intrusive", } } +Entry { filename = "cassandra-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "cccam-version.nse", categories = { "version", } } Entry { filename = "citrix-brute-xml.nse", categories = { "auth", "intrusive", } } Entry { filename = "citrix-enum-apps-xml.nse", categories = { "discovery", "safe", } }