description = [[ Performs password guessing against LDAP ]] --- -- @usage -- nmap -p 389 --script ldap-brute --script-args -- ldap.base='"cn=users,dc=cqure,dc=net"' -- -- @output -- 389/tcp open ldap -- | ldap-brute: -- |_ ldaptest:ldaptest => Login Correct -- -- @args ldap.base If set, the script will use it as a base for the password -- guessing attempts. If unset the user list must either contain the -- distinguished name of each user or the server must support -- authentication using a simple user name. See AD discussion below. -- -- Additional information -- ---------------------- -- This script makes attempts to brute force LDAP authentication. By default -- it uses the builtin user- and password-list to do so. In order to use your -- own lists use the userdb and passdb script arguments. -- -- WARNING: This script does not make ANY attempt to prevent account lockout! -- If the number of passwords in the dictionary exceed the amount of -- allowed tries, accounts will be locked out. This usually happens -- *VERY* quickly. -- -- Active Directory and LDAP -- ------------------------- -- Note: Authenticating against Active Directory using LDAP does not use the -- Windows user name but the user accounts distinguished name. LDAP on Windows -- 2003 allows authentication using a simple user name rather than using the -- fully distinguished name. Eg: -- - Patrik Karlsson vs. cn=Patrik Karlsson,cn=Users,dc=cqure,dc=net -- This type of authentication is not supported on eg. OpenLDAP -- -- This script uses some AD specific support and optimizations: -- -- o LDAP on Windows 2003 reports different error messages depending on whether -- an account exists or not. If the script recieves an error indicating that -- the username does not exist it simply stops guessing passwords for this -- account and moves on to the next. -- -- o The script attempts to authenticate with the username only if no LDAP base -- is specified. The benefit of authenticating this way is that the LDAP path -- of each account does not need to be known in advance as it's looked up by -- the server. -- -- Credits -- ------- -- o The get_random_string function was borrowed from the smb-psexec script. -- author = "Patrik Karlsson" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"intrusive", "auth"} require 'shortport' require 'stdnse' require 'ldap' require 'unpwdb' require 'comm' -- Version 0.3 -- Created 01/20/2010 - v0.1 - created by Patrik Karlsson -- Revised 01/26/2010 - v0.2 - cleaned up unpwdb related code, fixed ssl stuff -- Revised 02/17/2010 - v0.3 - added AD specific checks and fixed bugs related to LDAP base portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"}) --- Tries to determine a valid naming context to use to validate credentials -- -- @param socket socket already connected to LDAP server -- @return string containing a valid naming context function get_naming_context( socket ) local req = { baseObject = "", scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default, attributes = { "defaultNamingContext", "namingContexts" } } local status, searchResEntries = ldap.searchRequest( socket, req ) if not status then return nil end local contexts = ldap.extractAttribute( searchResEntries, "defaultNamingContext" ) -- OpenLDAP does not have a defaultNamingContext if not contexts then contexts = ldap.extractAttribute( searchResEntries, "namingContexts" ) end if #contexts > 0 then return contexts[1] end return nil end --- Attempts to validate the credentials by requesting the base object of the supplied context -- -- @param socket socket already connected to the LDAP server -- @param context string containing the context to search -- @return true if credentials are valid and search was a success, false if not. function is_valid_credential( socket, context ) local req = { baseObject = context, scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default, attributes = nil } local status, searchResEntries = ldap.searchRequest( socket, req ) return status end action = function( host, port ) local result, response, status, context, valid_accounts = {}, nil, nil, nil, {} local usernames, passwords, username, password, fq_username local user_cnt, invalid_account_cnt, tot_tries = 0, 0, 0 local clock_start = nmap.clock_ms() local ldap_anonymous_bind = string.char( 0x30, 0x0c, 0x02, 0x01, 0x01, 0x60, 0x07, 0x02, 0x01, 0x03, 0x04, 0x00, 0x80, 0x00 ) local socket, _, opt = comm.tryssl( host, port, ldap_anonymous_bind, nil ) local base_dn = nmap.registry.args['ldap.base'] if not socket then return end -- We close and re-open the socket so that the anonymous bind does not distract us socket:close() -- set a reasonable timeout value socket:set_timeout(5000) status = socket:connect(host.ip, port.number, opt) if not status then return end context = get_naming_context(socket) if not context then stdnse.print_debug("Failed to retrieve namingContext") socket:close() return end status, usernames = unpwdb.usernames() if not status then return end status, passwords = unpwdb.passwords() if not status then return end for username in usernames do -- if a base DN was set append our username (CN) to the base if base_dn then fq_username = ("cn=%s,%s"):format(username, base_dn) else fq_username = username end user_cnt = user_cnt + 1 for password in passwords do tot_tries = tot_tries + 1 -- handle special case where we want to guess the username as password if password == "%username%" then password = username end stdnse.print_debug( "Trying %s/%s ...", fq_username, password ) status, response = ldap.bindRequest( socket, { version=3, ['username']=fq_username, ['password']=password} ) -- if the DN (username) does not exist, break loop if not status and response:match("invalid DN") then stdnse.print_debug( "%s returned: \"Invalid DN\"", fq_username ) invalid_account_cnt = invalid_account_cnt + 1 break end -- Is AD telling us the account does not exist? if not status and response:match("AcceptSecurityContext error, data 525, vece") then invalid_account_cnt = invalid_account_cnt + 1 break end -- Account Locked Out if not status and response:match("AcceptSecurityContext error, data 775, vece") then table.insert( valid_accounts, string.format("%s => Account locked out", fq_username ) ) break end -- Login correct, account disabled if not status and response:match("AcceptSecurityContext error, data 533, vece") then table.insert( valid_accounts, string.format("%s:%s => Login correct, account disabled", fq_username, password:len()>0 and password or "" ) ) break end -- Login correct, user must change password if not status and response:match("AcceptSecurityContext error, data 773, vece") then table.insert( valid_accounts, string.format("%s:%s => Login correct, user must change password", fq_username, password:len()>0 and password or "" ) ) break end --Login, correct if status then status = is_valid_credential( socket, context ) if status then table.insert( valid_accounts, string.format("%s:%s => Login correct", fq_username, password:len()>0 and password or "" ) ) -- Add credentials for other ldap scripts to use if nmap.registry.ldapaccounts == nil then nmap.registry.ldapaccounts = {} end nmap.registry.ldapaccounts[fq_username]=password break end end end passwords("reset") end stdnse.print_debug( "Finnished brute against LDAP, total tries: %d, tps: %d", tot_tries, ( tot_tries / ( ( nmap.clock_ms() - clock_start ) / 1000 ) ) ) if ( invalid_account_cnt == user_cnt and base_dn ~= nil ) then return "WARNING: All usernames were invalid. Invalid LDAP base?" end local output = stdnse.format_output(true, valid_accounts) or "" return output end