description = [[
Performs password guessing against IBM DB2
]]

---
-- @usage
-- nmap -p 50000 --script db2-brute <host>
--
-- @output
-- 50000/tcp open  ibm-db2
-- | db2-brute:  
-- |   db2noob:db2noob => Login Correct
-- |_  db2admin:db2admin => Login Correct
--
--
-- @args db2-brute.threads the amount of accounts to attempt to brute force in parallell (default 10)
--

author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories={"intrusive", "auth"}

require "stdnse"
require "shortport"
require "db2"
require "unpwdb"

-- Version 0.2
-- Created 05/08/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 05/09/2010 - v0.2 - re-wrote as multi-threaded <patrik@cqure.net>

portrule = shortport.port_or_service({50000,60000},"ibm-db2", "tcp", {"open", "open|filtered"})

local MAX_THREADS = nmap.registry.args['db2-brute.threads'] and tonumber( nmap.registry.args['db2-brute.threads'] ) or 10
local mutex = nmap.mutex("db2-brute.valid_acoounts");
local thread_count= 0

--- Iterates over the password list and guesses passwords
--
-- @param host table with information as recieved by <code>action</code>
-- @param port table with information as recieved by <code>action</code>
-- @param database string containing the database name
-- @param username string containing the username against which to guess
-- @param valid_accounts table in which to store found accounts
doLogin = function( host, port, database, username, valid_accounts )
	local condvar = nmap.condvar( valid_accounts )
	local helper = db2.Helper:new()
	local status, response, passwords

	status, passwords = unpwdb.passwords()
	if ( not(status) ) then
		return
	end

	for password in passwords do
		stdnse.print_debug( "Trying %s/%s ...", username, password )
		helper:connect( host, port )		
		status, response = helper:login( database, username, password )
		helper:close()
	
		if ( status ) then
			-- Add credentials for future db2 scripts to use
			mutex("lock")
			if nmap.registry.db2users == nil then
				nmap.registry.db2users = {}
			end	
			nmap.registry.db2users[username]=password
			table.insert( valid_accounts, string.format("%s:%s => Login Correct", username, password:len()>0 and password or "<empty>" ) )
			mutex("done")
			break
		end
	end
	
	condvar("broadcast")
end

--- Checks if the supplied database exists
--
-- @param host table with information as recieved by <code>action</code>
-- @param port table with information as recieved by <code>action</code>
-- @param database string containing the database name
-- @return status true on success, false on failure
isValidDb = function( host, port, database )
	local status, response = true, ""
	local helper = db2.Helper:new()
	
	helper:connect( host, port )
	-- Authenticate with a static probe account to see if the db is valid		
	status, response = helper:login( database, "dbnameprobe1234", "dbnameprobe1234" )
	helper:close()

	if ( not(status) and response:match("Database not found") ) then
		return false
	end
	return true
end

--- Returns the amount of currenlty active threads
--
-- @param threads table containing the list of threads
-- @return count number containing the number of non-dead threads
threadCount = function( threads )
	local count = 0
	
	for thread in pairs(threads) do
		if ( coroutine.status(thread) == "dead" ) then
			threads[thread] = nil
		else
			count = count + 1
		end
	end
	return count
end

action = function( host, port )

	local result, response, status = {}, nil, nil
	local valid_accounts, threads = {}, {}	
	local usernames, passwords
	local username, password
	local database = nmap.registry.args['db2-auth.dbname'] or "SAMPLE"
	local condvar = nmap.condvar( valid_accounts )

 	status, usernames = unpwdb.usernames()
	if ( not(status) ) then
		return "Failed to load usernames"
	end
	
	-- make sure we have a valid pw file
	status, passwords = unpwdb.passwords()
	if ( not(status) ) then
		return "Failed to load passwords"
	end
	
	-- Check if the DB specified is valid
	if( not(isValidDb(host, port, database)) ) then
		return ("The databases %s was not found. (Use --script-args db2-auth.dbname=<dbname> to specify database)"):format(database)
	end
	
	stdnse.print_debug("Starting brute force with %d threads", MAX_THREADS )
	
	for username in usernames do
		-- Have we reached the maximum amount of threads?
		while ( threadCount( threads ) >= MAX_THREADS ) do
			condvar("wait")
		end
		local co = stdnse.new_thread( doLogin, host, port, database, username, valid_accounts )
		threads[co] = true
	end

	-- wait for all threads to finnish running
	while threadCount(threads)>0 do
   		condvar("wait")
 	end

	return stdnse.format_output(true, valid_accounts)	

end
