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:
@@ -1,5 +1,9 @@
|
||||
# Nmap Changelog ($Id$); -*-text-*-
|
||||
|
||||
o [NSE][GH362] 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. [Tom Sellers]
|
||||
|
||||
o [GH#354] Added new version detection Probes for LDAP services, LDAPSearchReq
|
||||
and LDAPSearchReqUDP. The second is Microsoft Active Directory specific. The
|
||||
Probes will elicit responses from target services that allow better finger
|
||||
|
||||
123
nselib/ldap.lua
123
nselib/ldap.lua
@@ -6,7 +6,7 @@
|
||||
--
|
||||
-- Credit goes out to Martin Swende who provided me with the initial code that got me started writing this.
|
||||
--
|
||||
-- Version 0.6
|
||||
-- Version 0.7
|
||||
-- Created 01/12/2010 - v0.1 - Created by Patrik Karlsson <patrik@cqure.net>
|
||||
-- Revised 01/28/2010 - v0.2 - Revised to fit better fit ASN.1 library
|
||||
-- Revised 02/02/2010 - v0.3 - Revised to fit OO ASN.1 Library
|
||||
@@ -14,6 +14,7 @@
|
||||
-- formats
|
||||
-- Revised 10/29/2011 - v0.5 - Added support for performing wildcard searches via the substring filter.
|
||||
-- Revised 10/30/2011 - v0.6 - Added support for the ldap extensibleMatch filter type for searches
|
||||
-- Revised 04/04/2016 - v0.7 - Added support for searchRequest over upd ( udpSearchRequest ) - Tom Sellers
|
||||
--
|
||||
|
||||
local asn1 = require "asn1"
|
||||
@@ -24,6 +25,7 @@ local os = require "os"
|
||||
local stdnse = require "stdnse"
|
||||
local string = require "string"
|
||||
local table = require "table"
|
||||
local comm = require "comm"
|
||||
_ENV = stdnse.module("ldap", stdnse.seeall)
|
||||
|
||||
local ldapMessageId = 1
|
||||
@@ -221,7 +223,7 @@ end
|
||||
-- @param params table containing at least <code>scope</code>, <code>derefPolicy</code>, <code>baseObject</code>
|
||||
-- the field <code>maxObjects</code> may also be included to restrict the amount of records returned
|
||||
-- @return success true or false.
|
||||
-- @return err string containing error message
|
||||
-- @return searchResEntries containing results or a string containing error message
|
||||
function searchRequest( socket, params )
|
||||
|
||||
local searchResEntries = { errorMessage="", resultCode = 0}
|
||||
@@ -334,6 +336,123 @@ function searchRequest( socket, params )
|
||||
return true, searchResEntries
|
||||
end
|
||||
|
||||
--- Performs an LDAP Search request over UDP
|
||||
--
|
||||
-- This function has a concept of softerrors which populates the return tables error information
|
||||
-- while returning a true status. The reason for this is that LDAP may return a number of records
|
||||
-- and then finish off with an error like SIZE LIMIT EXCEEDED. We still want to return the records
|
||||
-- that were received prior to the error. In order to achieve this and not terminating the script
|
||||
-- by returning a false status a true status is returned together with a table containing all searchentries.
|
||||
-- This table has the <code>errorMessage</code> and <code>resultCode</code> entries set with the error information.
|
||||
-- As a <code>try</code> won't catch this error it's up to the script to do so. See ldap-search.nse for an example.
|
||||
--
|
||||
-- @param host The host to connect to
|
||||
-- @param port The port on the host
|
||||
-- @param params table containing at least <code>scope</code>, <code>derefPolicy</code>, <code>baseObject</code>
|
||||
-- the field <code>maxObjects</code> may also be included to restrict the amount of records returned
|
||||
-- @return success true or false.
|
||||
-- @return searchResEntries containing results or a string containing error message
|
||||
|
||||
function udpSearchRequest( host, port, params )
|
||||
|
||||
local searchResEntries = { errorMessage="", resultCode = 0}
|
||||
local catch = function() stdnse.debug1("udpSearchRequest failed") end
|
||||
local try = nmap.new_try(catch)
|
||||
local attributes = params.attributes
|
||||
local request = encode(params.baseObject)
|
||||
local attrSeq = ''
|
||||
local requestData, messageSeq, data
|
||||
local maxObjects = params.maxObjects or -1
|
||||
|
||||
local encoder = asn1.ASN1Encoder:new()
|
||||
local decoder = asn1.ASN1Decoder:new()
|
||||
|
||||
encoder:registerTagEncoders(tagEncoder)
|
||||
decoder:registerTagDecoders(tagDecoder)
|
||||
|
||||
request = request .. encode( { _ldap='0A', params.scope } )--scope
|
||||
request = request .. encode( { _ldap='0A', params.derefPolicy } )--derefpolicy
|
||||
request = request .. encode( params.sizeLimit or 0)--sizelimit
|
||||
request = request .. encode( params.timeLimit or 0)--timelimit
|
||||
request = request .. encode( params.typesOnly or false)--TypesOnly
|
||||
|
||||
if params.filter then
|
||||
request = request .. createFilter( params.filter )
|
||||
else
|
||||
request = request .. encode( { _ldaptype='87', "objectclass" } )-- filter : string, presence
|
||||
end
|
||||
if attributes~= nil then
|
||||
for _,attr in ipairs(attributes) do
|
||||
attrSeq = attrSeq .. encode(attr)
|
||||
end
|
||||
end
|
||||
|
||||
request = request .. encoder:encodeSeq(attrSeq)
|
||||
requestData = encodeLDAPOp(APPNO.SearchRequest, true, request)
|
||||
messageSeq = encode(ldapMessageId)
|
||||
ldapMessageId = ldapMessageId +1
|
||||
messageSeq = messageSeq .. requestData
|
||||
data = encoder:encodeSeq(messageSeq)
|
||||
local status, response = comm.exchange(host, port, data)
|
||||
|
||||
while true do
|
||||
local len, pos, messageId = 0, 0, -1
|
||||
local tmp = ""
|
||||
local _, objectName, attributes, ldapOp
|
||||
local attributes
|
||||
local searchResEntry = {}
|
||||
|
||||
if ( maxObjects == 0 ) then
|
||||
break
|
||||
elseif ( maxObjects > 0 ) then
|
||||
maxObjects = maxObjects - 1
|
||||
end
|
||||
|
||||
pos, tmp = bin.unpack("C", response, pos)
|
||||
pos, len = decoder.decodeLength( response, pos )
|
||||
pos, messageId = decode( response, pos )
|
||||
pos, tmp = bin.unpack("C", response, pos)
|
||||
pos, len = decoder.decodeLength( response, pos )
|
||||
ldapOp = asn1.intToBER( tmp )
|
||||
searchResEntry = {}
|
||||
|
||||
if ldapOp.number == APPNO.SearchResDone then
|
||||
pos, searchResEntry.resultCode = decode( response, pos )
|
||||
-- errors may occur after a large amount of response has been received (eg. size limit exceeded)
|
||||
-- we want to be able to return the response received prior to this error to the user
|
||||
-- however, we also need to alert the user of the error. This is achieved through "softerrors"
|
||||
-- softerrors populate the error fields of the table while returning a true status
|
||||
-- this allows for the caller to output response while still being able to catch the error
|
||||
if ( searchResEntry.resultCode ~= 0 ) then
|
||||
local error_msg
|
||||
pos, searchResEntry.matchedDN = decode( response, pos )
|
||||
pos, searchResEntry.errorMessage = decode( response, pos )
|
||||
error_msg = ERROR_MSG[searchResEntry.resultCode]
|
||||
-- if the table is empty return a hard error
|
||||
if #searchResEntries == 0 then
|
||||
return false, string.format("Code: %d|Error: %s|Details: %s", searchResEntry.resultCode, error_msg or "", searchResEntry.errorMessage or "" )
|
||||
else
|
||||
searchResEntries.errorMessage = string.format("Code: %d|Error: %s|Details: %s", searchResEntry.resultCode, error_msg or "", searchResEntry.errorMessage or "" )
|
||||
searchResEntries.resultCode = searchResEntry.resultCode
|
||||
return true, searchResEntries
|
||||
end
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
pos, searchResEntry.objectName = decode( response, pos )
|
||||
if ldapOp.number == APPNO.SearchResponse then
|
||||
pos, searchResEntry.attributes = decode( response, pos )
|
||||
table.insert( searchResEntries, searchResEntry )
|
||||
end
|
||||
if response:len() > pos then
|
||||
response = response:sub(pos)
|
||||
else
|
||||
response = ""
|
||||
end
|
||||
end
|
||||
return true, searchResEntries
|
||||
end
|
||||
|
||||
--- Attempts to bind to the server using the credentials given
|
||||
--
|
||||
|
||||
@@ -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