diff --git a/CHANGELOG b/CHANGELOG index 7c39340d6..7c2714abd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,10 @@ # Nmap Changelog ($Id$); -*-text-*- + +o [NSE] Added scripts cvs-brute.nse, cvs-brute-repository.nse and the cvs + library. The cvs-brute-repository script allows for guessing possible + repository names needed in order to perform password guessing using the + cvs-brute.nse script. [Patrik] + o [Zenmap] The Zenmap crash handler now instructs you to mail in crash information to nmap-dev. [Colin Rice] diff --git a/nselib/cvs.lua b/nselib/cvs.lua new file mode 100644 index 000000000..a60c37740 --- /dev/null +++ b/nselib/cvs.lua @@ -0,0 +1,93 @@ +--- +-- A minimal CVS pserver protocol implementation support authentication only +-- for the time being. +-- +-- @author Patrik Karlsson +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- + +-- Version 0.1 +-- Created 07/13/2011 - v0.1 - created by Patrik Karlsson + + +module(... or "cvs", package.seeall) + + +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, + + login = function(self, repo, user, pass ) + local auth_tab = {} + assert(repo, "No repository was specified") + assert(user, "No user was specified") + assert(pass, "No pass was specified") + + -- Add a leading slash if it's missing + if ( repo:sub(1,1) ~= "/" ) then repo = "/" .. repo end + + table.insert(auth_tab, "BEGIN AUTH REQUEST") + table.insert(auth_tab, repo) + table.insert(auth_tab, user) + table.insert(auth_tab, Util.pwscramble(pass)) + table.insert(auth_tab, "END AUTH REQUEST") + + local data = stdnse.strjoin("\n", auth_tab) .. "\n" + local status = self.socket:send(data) + if ( not(status) ) then return false, "Failed to send login request" end + + local status, response = self.socket:receive() + if ( not(status) ) then return false, "Failed to read login response" end + + if ( response == "I LOVE YOU\n" ) then return true end + return false, response + end, + + close = function(self) + return self.socket:close() + end + +} + +Util = { + + --- Scrambles a password + -- + -- @param password string containing the password to scramble + pwscramble = function(password) + local shifts = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 114,120, 53, 79, 96,109, 72,108, 70, 64, 76, 67,116, 74, 68, 87, + 111, 52, 75,119, 49, 34, 82, 81, 95, 65,112, 86,118,110,122,105, + 41, 57, 83, 43, 46,102, 40, 89, 38,103, 45, 50, 42,123, 91, 35, + 125, 55, 54, 66,124,126, 59, 47, 92, 71,115, 78, 88,107,106, 56, + 36,121,117,104,101,100, 69, 73, 99, 63, 94, 93, 39, 37, 61, 48, + 58,113, 32, 90, 44, 98, 60, 51, 33, 97, 62, 77, 84, 80, 85,223, + 225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190, + 199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193, + 174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212, + 207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246, + 192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176, + 227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127, + 182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195, + 243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,152 }; + + local result = "" + for i = 1, #password do + result = result .. string.char(shifts[password:byte(i)+1]) + end + return 'A' .. result + end + +} diff --git a/scripts/cvs-brute-repository.nse b/scripts/cvs-brute-repository.nse new file mode 100644 index 000000000..e799c55be --- /dev/null +++ b/scripts/cvs-brute-repository.nse @@ -0,0 +1,123 @@ +description = [[ +Attempts to guess the name of the CVS repository hosted on the remote server. +With knowledge of the correct repository name, usernames and passwords can be guessed. +]] + +--- +-- @usage +-- nmap -p 2401 --script cvs-brute-repository +-- +-- @output +-- PORT STATE SERVICE REASON +-- 2401/tcp open cvspserver syn-ack +-- | cvs-brute-repository: +-- | Repositories +-- | /myrepos +-- | /demo +-- | Statistics +-- |_ Performed 14 guesses in 1 seconds, average tps: 14 +-- +-- @args cvs-brute-repository.nodefault when set the script does not attempt to +-- guess the list of hardcoded repositories +-- @args cvs-brute-repository.repofile a file containing a list of repositories +-- to guess + +-- Version 0.1 +-- Created 07/13/2010 - v0.1 - created by Patrik Karlsson + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} + +require 'cvs' +require 'brute' +require 'creds' +require 'shortport' + +portrule = shortport.port_or_service(2401, "cvspserver") + +Driver = +{ + + new = function(self, host, port ) + local o = { host = host, helper = cvs.Helper:new(host, port) } + setmetatable(o, self) + self.__index = self + return o + end, + + connect = function( self ) + self.helper:connect() + return true + end, + + login = function( self, username, password ) + username = "" + if ( password:sub(1,1) ~= "/" ) then password = "/" .. password end + local status, err = self.helper:login( password, "repository", "repository" ) + if ( not(status) and err:match("I HATE YOU") ) then + -- let's store the repositories in the registry so the brute + -- script can use them later. + nmap.registry.cvs = nmap.registry.cvs or {} + nmap.registry.cvs[self.host.ip] = nmap.registry.cvs[self.host.ip] or {} + nmap.registry.cvs[self.host.ip].repos = nmap.registry.cvs[self.host.ip].repos or {} + table.insert(nmap.registry.cvs[self.host.ip].repos, password) + return true, brute.Account:new(username, password, 0) + end + return false, brute.Error:new( "Incorrect password" ) + end, + + disconnect = function( self ) + self.helper:close() + end, + +} + + +action = function(host, port) + + local status, result + local engine = brute.Engine:new(Driver, host, port) + + -- a list of "common" repository names: + -- the first two are Debian/Ubuntu default names + -- the rest were found during tests or in google searches + local repos = {"myrepos", "demo", "cvs", "cvsroot", "prod", "src", "test", + "source", "devel", "cvsroot", "/var/lib/cvsroot", + "cvs-repository", "/home/cvsroot", "/var/cvs", + "/usr/local/cvs"} + + local repofile = stdnse.get_script_args("cvs-brute-repository.repofile") + local f + + if ( repofile ) then + f = io.open( repofile, "r" ) + if ( not(f) ) then + return ("\n ERROR: Failed to open repository file: %s"):format(repofile) + end + end + + repository_iterator = function() + local function next_repo() + for line in f:lines() do + if ( not(line:match("#!comment")) ) then + coroutine.yield("", line) + end + end + while(true) do coroutine.yield(nil, nil) end + end + return coroutine.wrap(next_repo) + end + + engine.options:setTitle("Repositories") + engine.options.script_name = SCRIPT_NAME + engine.options.passonly = true + engine.options.firstonly = false + engine.options.nostore = true + engine:addIterator(brute.Iterators.account_iterator({""}, repos, "user")) + if ( repofile ) then engine:addIterator(repository_iterator()) end + status, result = engine:start() + + return result +end + diff --git a/scripts/cvs-brute.nse b/scripts/cvs-brute.nse new file mode 100644 index 000000000..5c4bf653c --- /dev/null +++ b/scripts/cvs-brute.nse @@ -0,0 +1,107 @@ +description = [[ +Performs password guessing against CVS pserver. +]] + +--- +-- @usage +-- nmap -p 2401 --script cvs-brute +-- +-- @output +-- 2401/tcp open cvspserver syn-ack +-- | cvs-brute: +-- | Accounts +-- | hotchner:francisco - Account is valid +-- | reid:secret - Account is valid +-- | Statistics +-- |_ Performed 544 guesses in 14 seconds, average tps: 38 +-- +-- @args cvs-brute.repo string containing the name of the repository to brute +-- if no repo was given the script checks the registry for any +-- repositories discovered by the cvs-brute-repository script. If the +-- registry contains any discovered repositories, the script attempts to +-- brute force the credentials for the first one. + +-- Version 0.1 +-- Created 07/13/2010 - v0.1 - created by Patrik Karlsson + + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"intrusive", "auth"} +dependencies = {"cvs-brute-repository"} + +require 'cvs' +require 'brute' +require 'creds' +require 'shortport' + +portrule = shortport.port_or_service(2401, "cvspserver") + +Driver = +{ + + new = function(self, host, port, repo) + local o = { repo = repo, helper = cvs.Helper:new(host, port) } + setmetatable(o, self) + self.__index = self + return o + end, + + connect = function( self ) + self.helper:connect() + return true + end, + + login = function( self, username, password ) + local status, err = self.helper:login( self.repo, username, password ) + if ( status ) then + return true, brute.Account:new(username, password, creds.State.VALID) + end + + -- This error seems to indicate tha the user does not exist + if ( err:match("E PAM start error%: Critical error %- immediate abort\0$") ) then + stdnse.print_debug(2, "%s: The user %s does not exist", SCRIPT_NAME, username) + local err = brute.Error:new("Account invalid") + err:setInvalidAccount(username) + return false, err + end + return false, brute.Error:new( "Incorrect password" ) + end, + + disconnect = function( self ) + self.helper:close() + end, + +} + +local function getDiscoveredRepos(host) + + if ( not(nmap.registry.cvs) or + not(nmap.registry.cvs[host.ip]) or + not(nmap.registry.cvs[host.ip].repos) + ) then + return + end + + return nmap.registry.cvs[host.ip].repos +end + +action = function(host, port) + + local repo = stdnse.get_script_args("cvs-brute.repo") and + { stdnse.get_script_args("cvs-brute.repo") } or + getDiscoveredRepos(host) + if ( not(repo) ) then return "\n ERROR: No CVS repository specified (see cvs-brute.repo)" end + + local status, result + + -- If repositories were discovered and not overridden by argument + -- only attempt to brute force the first one. + local engine = brute.Engine:new(Driver, host, port, repo[1]) + + engine.options.script_name = SCRIPT_NAME + status, result = engine:start() + + return result +end + diff --git a/scripts/script.db b/scripts/script.db index 819a4917b..a563ab9a9 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -27,6 +27,8 @@ Entry { filename = "citrix-enum-servers.nse", categories = { "discovery", "safe" Entry { filename = "couchdb-databases.nse", categories = { "discovery", "safe", } } Entry { filename = "couchdb-stats.nse", categories = { "discovery", "safe", } } Entry { filename = "creds-summary.nse", categories = { "auth", "default", "safe", } } +Entry { filename = "cvs-brute-repository.nse", categories = { "auth", "intrusive", } } +Entry { filename = "cvs-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "daap-get-library.nse", categories = { "discovery", "safe", } } Entry { filename = "daytime.nse", categories = { "discovery", "safe", } } Entry { filename = "db2-das-info.nse", categories = { "discovery", "safe", "version", } }