mirror of
https://github.com/nmap/nmap.git
synced 2025-12-15 12:19:02 +00:00
o [NSE] Added a new library for LDAP and two new scripts:
- ldap-brute uses the unpwdb library to guess credentials for LDAP - ldap-rootdse retrieves the LDAP root DSA-specific Entry (DSE)
This commit is contained in:
255
scripts/ldap-brute.nse
Normal file
255
scripts/ldap-brute.nse
Normal file
@@ -0,0 +1,255 @@
|
||||
description = [[
|
||||
Performs password guessing against LDAP
|
||||
]]
|
||||
|
||||
---
|
||||
-- @usage
|
||||
-- nmap -p 389 --script ldap-brute --script-args
|
||||
-- ldap.base='"cn=users,dc=cqure,dc=net"' <host>
|
||||
--
|
||||
-- @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 aborted
|
||||
|
||||
local max_time = unpwdb.timelimit() ~= nil and unpwdb.timelimit() * 1000 or -1
|
||||
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
|
||||
-- is the aborted flag set
|
||||
if ( aborted ) then
|
||||
break
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
-- Should we abort?
|
||||
if max_time>0 and nmap.clock_ms() - clock_start > max_time then
|
||||
aborted=true
|
||||
break
|
||||
end
|
||||
|
||||
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 "<empty>" ) )
|
||||
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 "<empty>" ) )
|
||||
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 "<empty>" ) )
|
||||
|
||||
-- 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 ""
|
||||
|
||||
if ( max_time > 0 and aborted ) then
|
||||
output = output .. string.format(" \n\nNOTE: script aborted execution after %d seconds", max_time/1000 )
|
||||
end
|
||||
|
||||
return output
|
||||
|
||||
end
|
||||
151
scripts/ldap-rootdse.nse
Normal file
151
scripts/ldap-rootdse.nse
Normal file
@@ -0,0 +1,151 @@
|
||||
description = [[
|
||||
Retrieves the LDAP root DSA-specific Entry (DSE)
|
||||
]]
|
||||
|
||||
---
|
||||
--
|
||||
-- @usage
|
||||
-- nmap -p 389 --script ldap-rootdse <host>
|
||||
--
|
||||
-- @output
|
||||
-- PORT STATE SERVICE
|
||||
-- 389/tcp open ldap
|
||||
-- | ldap-rootdse:
|
||||
-- | currentTime: 20100112092616.0Z
|
||||
-- | subschemaSubentry: CN=Aggregate,CN=Schema,CN=Configuration,DC=cqure,DC=net
|
||||
-- | dsServiceName: CN=NTDS Settings,CN=LDAPTEST001,CN=Servers,CN=Default-First-Site,CN=Sites,CN=Configuration,DC=cqure,DC=net
|
||||
-- | namingContexts: DC=cqure,DC=net
|
||||
-- | namingContexts: CN=Configuration,DC=cqure,DC=net
|
||||
-- | namingContexts: CN=Schema,CN=Configuration,DC=cqure,DC=net
|
||||
-- | namingContexts: DC=DomainDnsZones,DC=cqure,DC=net
|
||||
-- | namingContexts: DC=ForestDnsZones,DC=cqure,DC=net
|
||||
-- | namingContexts: DC=TAPI3Directory,DC=cqure,DC=net
|
||||
-- | defaultNamingContext: DC=cqure,DC=net
|
||||
-- | schemaNamingContext: CN=Schema,CN=Configuration,DC=cqure,DC=net
|
||||
-- | configurationNamingContext: CN=Configuration,DC=cqure,DC=net
|
||||
-- | rootDomainNamingContext: DC=cqure,DC=net
|
||||
-- | supportedControl: 1.2.840.113556.1.4.319
|
||||
-- | .
|
||||
-- | .
|
||||
-- | supportedControl: 1.2.840.113556.1.4.1948
|
||||
-- | supportedLDAPVersion: 3
|
||||
-- | supportedLDAPVersion: 2
|
||||
-- | supportedLDAPPolicies: MaxPoolThreads
|
||||
-- | supportedLDAPPolicies: MaxDatagramRecv
|
||||
-- | supportedLDAPPolicies: MaxReceiveBuffer
|
||||
-- | supportedLDAPPolicies: InitRecvTimeout
|
||||
-- | supportedLDAPPolicies: MaxConnections
|
||||
-- | supportedLDAPPolicies: MaxConnIdleTime
|
||||
-- | supportedLDAPPolicies: MaxPageSize
|
||||
-- | supportedLDAPPolicies: MaxQueryDuration
|
||||
-- | supportedLDAPPolicies: MaxTempTableSize
|
||||
-- | supportedLDAPPolicies: MaxResultSetSize
|
||||
-- | supportedLDAPPolicies: MaxNotificationPerConn
|
||||
-- | supportedLDAPPolicies: MaxValRange
|
||||
-- | highestCommittedUSN: 126991
|
||||
-- | supportedSASLMechanisms: GSSAPI
|
||||
-- | supportedSASLMechanisms: GSS-SPNEGO
|
||||
-- | supportedSASLMechanisms: EXTERNAL
|
||||
-- | supportedSASLMechanisms: DIGEST-MD5
|
||||
-- | dnsHostName: EDUSRV011.cqure.local
|
||||
-- | ldapServiceName: cqure.net:edusrv011$@CQURE.NET
|
||||
-- | serverName: CN=EDUSRV011,CN=Servers,CN=Default-First-Site,CN=Sites,CN=Configuration,DC=cqure,DC=net
|
||||
-- | supportedCapabilities: 1.2.840.113556.1.4.800
|
||||
-- | supportedCapabilities: 1.2.840.113556.1.4.1670
|
||||
-- | supportedCapabilities: 1.2.840.113556.1.4.1791
|
||||
-- | isSynchronized: TRUE
|
||||
-- | isGlobalCatalogReady: TRUE
|
||||
-- | domainFunctionality: 0
|
||||
-- | forestFunctionality: 0
|
||||
-- |_ domainControllerFunctionality: 2
|
||||
--
|
||||
--
|
||||
-- The root DSE object may contain a number of different attributes as described in RFC 2251 section 3.4:
|
||||
-- * namingContexts: naming contexts held in the server
|
||||
-- * subschemaSubentry: subschema entries (or subentries) known by this server
|
||||
-- * altServer: alternative servers in case this one is later unavailable.
|
||||
-- * supportedExtension: list of supported extended operations.
|
||||
-- * supportedControl: list of supported controls.
|
||||
-- * supportedSASLMechanisms: list of supported SASL security features.
|
||||
-- * supportedLDAPVersion: LDAP versions implemented by the server.
|
||||
--
|
||||
-- The above example, which contains a lot more information is from Windows 2003 accessible without authentication.
|
||||
-- The same request against OpenLDAP will result in significantly less information.
|
||||
--
|
||||
-- The ldap-search script queries the root DSE for the namingContexts and/or defaultNamingContexts, which it sets as base
|
||||
-- if no base object was specified
|
||||
--
|
||||
-- Credit goes out to Martin Swende who provided me with the initial code that got me started writing this.
|
||||
--
|
||||
|
||||
-- Version 0.2
|
||||
-- Created 01/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
|
||||
-- Revised 01/20/2010 - v0.2 - added SSL support
|
||||
|
||||
author = "Patrik Karlsson"
|
||||
copyright = "Patrik Karlsson"
|
||||
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
||||
categories = {"discovery", "safe"}
|
||||
|
||||
require "ldap"
|
||||
require 'shortport'
|
||||
require 'comm'
|
||||
|
||||
portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"})
|
||||
|
||||
function action(host,port)
|
||||
|
||||
local socket = nmap.new_socket()
|
||||
local status, searchResEntries, req, result, opt
|
||||
|
||||
-- In order to discover what protocol to use (SSL/TCP) we need to send a few bytes to the server
|
||||
-- An anonymous bind should do it
|
||||
local ldap_anonymous_bind = string.char( 0x30, 0x0c, 0x02, 0x01, 0x01, 0x60, 0x07, 0x02, 0x01, 0x03, 0x04, 0x00, 0x80, 0x00 )
|
||||
socket, _, opt = comm.tryssl( host, port, ldap_anonymous_bind, nil )
|
||||
|
||||
if not socket then
|
||||
return
|
||||
end
|
||||
|
||||
-- We close and re-open the socket so that the anonymous bind does not distract us
|
||||
socket:close()
|
||||
status = socket:connect(host.ip, port.number, opt)
|
||||
socket:set_timeout(10000)
|
||||
|
||||
-- Searching for an empty argument list against LDAP on W2K3 returns all attributes
|
||||
-- This is not the case for OpenLDAP, so we do a search for an empty attribute list
|
||||
-- Then we compare the results against some known and expected returned attributes
|
||||
req = { baseObject = "", scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default }
|
||||
status, searchResEntries = ldap.searchRequest( socket, req )
|
||||
|
||||
-- Check if we were served all the results or not?
|
||||
if not ldap.extractAttribute( searchResEntries, "namingContexts" ) and
|
||||
not ldap.extractAttribute( searchResEntries, "supportedLDAPVersion" ) then
|
||||
|
||||
-- The namingContexts was not there, try to query all attributes instead
|
||||
-- Attributes extracted from Windows 2003 and complemented from RFC
|
||||
local attribs = {"_domainControllerFunctionality","configurationNamingContext","currentTime","defaultNamingContext",
|
||||
"dnsHostName","domainFunctionality","dsServiceName","forestFunctionality","highestCommittedUSN",
|
||||
"isGlobalCatalogReady","isSynchronized","ldap-get-baseobject","ldapServiceName","namingContexts",
|
||||
"rootDomainNamingContext","schemaNamingContext","serverName","subschemaSubentry",
|
||||
"supportedCapabilities","supportedControl","supportedLDAPPolicies","supportedLDAPVersion",
|
||||
"supportedSASLMechanisms", "altServer", "supportedExtension"}
|
||||
|
||||
req = { baseObject = "", scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default, attributes = attribs }
|
||||
status, searchResEntries = ldap.searchRequest( socket, req )
|
||||
end
|
||||
|
||||
if not status then
|
||||
socket:close()
|
||||
return
|
||||
end
|
||||
|
||||
result = ldap.searchResultToTable( searchResEntries )
|
||||
socket:close()
|
||||
|
||||
-- if taken a way and ldap returns a single result, it ain't shown....
|
||||
result.name = "LDAP Results"
|
||||
|
||||
return stdnse.format_output(true, result )
|
||||
|
||||
end
|
||||
@@ -39,6 +39,8 @@ Entry { filename = "http-vmware-path-vuln.nse", categories = { "default", "safe"
|
||||
Entry { filename = "iax2-version.nse", categories = { "version", } }
|
||||
Entry { filename = "imap-capabilities.nse", categories = { "default", "safe", } }
|
||||
Entry { filename = "irc-info.nse", categories = { "default", "discovery", "safe", } }
|
||||
Entry { filename = "ldap-brute.nse", categories = { "auth", "intrusive", } }
|
||||
Entry { filename = "ldap-rootdse.nse", categories = { "discovery", "safe", } }
|
||||
Entry { filename = "lexmark-config.nse", categories = { "discovery", "safe", } }
|
||||
Entry { filename = "mongodb-databases.nse", categories = { "default", "discovery", "safe", } }
|
||||
Entry { filename = "mongodb-info.nse", categories = { "default", "discovery", "safe", } }
|
||||
|
||||
Reference in New Issue
Block a user