mirror of
https://github.com/nmap/nmap.git
synced 2025-12-10 09:49:05 +00:00
Added support for LDAP over udp to ldap-rootdse.nse.
Also added version detection and information extraction to match the new LDAP LDAPSearchReq and LDAPSearchReqUDP probes. Closes #362
This commit is contained in:
@@ -84,9 +84,10 @@ Retrieves the LDAP root DSA-specific Entry (DSE)
|
||||
-- Credit goes out to Martin Swende who provided me with the initial code that got me started writing this.
|
||||
--
|
||||
|
||||
-- Version 0.2
|
||||
-- Version 0.3
|
||||
-- Created 01/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
|
||||
-- Revised 01/20/2010 - v0.2 - added SSL support
|
||||
-- Revised 04/09/2016 - v0.3 - added support for LDAP over UDP - Tom Sellers
|
||||
|
||||
author = "Patrik Karlsson"
|
||||
copyright = "Patrik Karlsson"
|
||||
@@ -94,63 +95,112 @@ license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
|
||||
categories = {"discovery", "safe"}
|
||||
dependencies = {"ldap-brute"}
|
||||
|
||||
-- Map domainControllerFunctionality to OS - https://msdn.microsoft.com/en-us/library/cc223272.aspx
|
||||
-- Tested to be valid even when Active Directory functional level is lower than target ADC's OS version
|
||||
DC_FUNCT_ID = {}
|
||||
DC_FUNCT_ID["0"] = "Windows 2000"
|
||||
DC_FUNCT_ID["2"] = "Windows 2003"
|
||||
DC_FUNCT_ID["3"] = "Windows 2008"
|
||||
DC_FUNCT_ID["4"] = "Windows 2008 R2"
|
||||
DC_FUNCT_ID["5"] = "Windows 2012"
|
||||
DC_FUNCT_ID["6"] = "Windows 2012 R2"
|
||||
|
||||
portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"})
|
||||
portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"},{'tcp','udp'})
|
||||
|
||||
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 = "\x30\x0c\x02\x01\x01\x60\x07\x02\x01\x03\x04\x00\x80\x00"
|
||||
local _
|
||||
socket, _, opt = comm.tryssl( host, port, ldap_anonymous_bind, nil )
|
||||
if port.protocol == 'tcp' then
|
||||
|
||||
if not socket then
|
||||
return
|
||||
end
|
||||
local socket = nmap.new_socket()
|
||||
|
||||
-- We close and re-open the socket so that the anonymous bind does not distract us
|
||||
socket:close()
|
||||
status = socket:connect(host, port, opt)
|
||||
socket:set_timeout(10000)
|
||||
-- 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 = "\x30\x0c\x02\x01\x01\x60\x07\x02\x01\x03\x04\x00\x80\x00"
|
||||
local _
|
||||
socket, _, opt = comm.tryssl( host, port, ldap_anonymous_bind, nil )
|
||||
|
||||
-- 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 )
|
||||
if not socket then
|
||||
return
|
||||
end
|
||||
|
||||
-- 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
|
||||
-- We close and re-open the socket so that the anonymous bind does not distract us
|
||||
socket:close()
|
||||
return
|
||||
status = socket:connect(host, port, 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
|
||||
|
||||
socket:close()
|
||||
else
|
||||
-- Port protocol is UDP, indicating that this is an Active Directory Controller LDAP service
|
||||
req = { baseObject = "", scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default}
|
||||
status, searchResEntries = ldap.udpSearchRequest( host, port, req )
|
||||
end
|
||||
|
||||
if not status or not searchResEntries then 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"
|
||||
local scriptResult = stdnse.format_output(true, result )
|
||||
|
||||
-- Start extracting target information
|
||||
-- The following works on Windows AD LDAP as well as VMware's LDAP, VMware uses lower case cn vs AD ucase CN
|
||||
local serverName = string.match(scriptResult,"serverName: [cC][nN]=([^,]+),[cC][nN]=Servers,[cC][nN]=")
|
||||
if serverName then port.version.hostname = serverName end
|
||||
|
||||
-- Check to see if this is Active Directory vs some other product or ADAM
|
||||
-- https://msdn.microsoft.com/en-us/library/cc223359.aspx
|
||||
if string.match(scriptResult,"1.2.840.113556.1.4.800") then
|
||||
port.version.product = 'Microsoft Windows Active Directory LDAP'
|
||||
port.version.name_confidence = 10
|
||||
|
||||
-- Determine Windows version
|
||||
if not port.version.ostype or port.version.ostype == 'Windows' then
|
||||
local DC_Func = string.match(scriptResult,"domainControllerFunctionality: (%d)")
|
||||
if DC_FUNCT_ID[DC_Func] then
|
||||
port.version.ostype = DC_FUNCT_ID[DC_Func]
|
||||
else
|
||||
port.version.ostype = 'Windows'
|
||||
stdnse.debug(1,"Unmatched OS lookup for domainControllerFunctionality: %d", DC_Func)
|
||||
end
|
||||
end
|
||||
|
||||
return stdnse.format_output(true, result )
|
||||
|
||||
local siteName = string.match(scriptResult,"serverName: CN=[^,]+,CN=Servers,CN=([^,]+),CN=Sites,")
|
||||
local domainName = string.match(scriptResult,"rootDomainNamingContext: ([^\n]*)")
|
||||
domainName = string.gsub(domainName,",DC=",".")
|
||||
domainName = string.gsub(domainName,"DC=","")
|
||||
if domainName and siteName then
|
||||
port.version.extrainfo = string.format("Domain: %s, Site: %s", domainName, siteName)
|
||||
end
|
||||
end
|
||||
|
||||
-- Set port information
|
||||
port.version.name = "ldap"
|
||||
nmap.set_port_version(host, port, "hardmatched")
|
||||
nmap.set_port_state(host, port, "open")
|
||||
|
||||
return scriptResult
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user