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

@@ -1,5 +1,9 @@
# Nmap Changelog ($Id$); -*-text-*- # 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 o [GH#354] Added new version detection Probes for LDAP services, LDAPSearchReq
and LDAPSearchReqUDP. The second is Microsoft Active Directory specific. The and LDAPSearchReqUDP. The second is Microsoft Active Directory specific. The
Probes will elicit responses from target services that allow better finger Probes will elicit responses from target services that allow better finger

View File

@@ -6,7 +6,7 @@
-- --
-- Credit goes out to Martin Swende who provided me with the initial code that got me started writing this. -- 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> -- 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 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 -- Revised 02/02/2010 - v0.3 - Revised to fit OO ASN.1 Library
@@ -14,6 +14,7 @@
-- formats -- formats
-- Revised 10/29/2011 - v0.5 - Added support for performing wildcard searches via the substring filter. -- 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 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" local asn1 = require "asn1"
@@ -24,6 +25,7 @@ local os = require "os"
local stdnse = require "stdnse" local stdnse = require "stdnse"
local string = require "string" local string = require "string"
local table = require "table" local table = require "table"
local comm = require "comm"
_ENV = stdnse.module("ldap", stdnse.seeall) _ENV = stdnse.module("ldap", stdnse.seeall)
local ldapMessageId = 1 local ldapMessageId = 1
@@ -221,7 +223,7 @@ end
-- @param params table containing at least <code>scope</code>, <code>derefPolicy</code>, <code>baseObject</code> -- @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 -- the field <code>maxObjects</code> may also be included to restrict the amount of records returned
-- @return success true or false. -- @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 ) function searchRequest( socket, params )
local searchResEntries = { errorMessage="", resultCode = 0} local searchResEntries = { errorMessage="", resultCode = 0}
@@ -334,6 +336,123 @@ function searchRequest( socket, params )
return true, searchResEntries return true, searchResEntries
end 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 --- Attempts to bind to the server using the credentials given
-- --

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. -- 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> -- Created 01/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 01/20/2010 - v0.2 - added SSL support -- 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" author = "Patrik Karlsson"
copyright = "Patrik Karlsson" copyright = "Patrik Karlsson"
@@ -94,13 +95,24 @@ license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"} categories = {"discovery", "safe"}
dependencies = {"ldap-brute"} 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) function action(host,port)
local status, searchResEntries, req, result, opt
if port.protocol == 'tcp' then
local socket = nmap.new_socket() 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 -- 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 -- An anonymous bind should do it
@@ -140,17 +152,55 @@ function action(host,port)
status, searchResEntries = ldap.searchRequest( socket, req ) status, searchResEntries = ldap.searchRequest( socket, req )
end end
if not status then
socket:close() socket:close()
return 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 end
if not status or not searchResEntries then return end
result = ldap.searchResultToTable( searchResEntries ) result = ldap.searchResultToTable( searchResEntries )
socket:close()
-- if taken a way and ldap returns a single result, it ain't shown.... -- if taken a way and ldap returns a single result, it ain't shown....
result.name = "LDAP Results" result.name = "LDAP Results"
local scriptResult = stdnse.format_output(true, result )
return 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
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 end