1
0
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:
tomsellers
2016-04-09 21:33:26 +00:00
parent 799048e9fc
commit ee4ed66956
3 changed files with 219 additions and 46 deletions

View File

@@ -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