mirror of
https://github.com/nmap/nmap.git
synced 2025-12-06 04:31:29 +00:00
add objectClass person to qfilter users so that users are properly shown add error msg for invalid credentials [patrik]
241 lines
8.5 KiB
Lua
241 lines
8.5 KiB
Lua
description = [[
|
|
Attempts to perform an LDAP search and returns all matches.
|
|
|
|
If no username and password is supplied to the script the Nmap registry
|
|
is consulted. If the <code>ldap-brute</code> script has been selected
|
|
and it found a valid account, this account will be used. If not
|
|
anonymous bind will be used as a last attempt.
|
|
]]
|
|
|
|
---
|
|
-- @args ldap.username If set, the script will attempt to perform an LDAP bind using the username and password
|
|
-- @args ldap.password If set, used together with the username to authenticate to the LDAP server
|
|
-- @args ldap.qfilter If set, specifies a quick filter. The library does not support parsing real LDAP filters.
|
|
-- The following values are valid for the filter parameter: computer, users or all. If no value is specified it defaults to all.
|
|
-- @args ldap.base If set, the script will use it as a base for the search. By default the defaultNamingContext is retrieved and used.
|
|
-- If no defaultNamingContext is available the script iterates over the available namingContexts
|
|
-- @args ldap.attrib If set, the search will include only the attributes specified. For a single attribute a string value can be used, if
|
|
-- multiple attributes need to be supplied a table should be used instead.
|
|
-- @args ldap.maxobjects If set, overrides the number of objects returned by the script (default 20).
|
|
-- The value -1 removes the limit completely.
|
|
-- @usage
|
|
-- nmap -p 389 --script ldap-search --script-args ldap.username="'cn=ldaptest,cn=users,dc=cqure,dc=net'",ldap.password=ldaptest,
|
|
-- ldap.qfilter=users,ldap.attrib=sAMAccountName <host>
|
|
--
|
|
-- @output
|
|
-- PORT STATE SERVICE REASON
|
|
-- 389/tcp open ldap syn-ack
|
|
-- | ldap-search:
|
|
-- | DC=cqure,DC=net
|
|
-- | dn: CN=Administrator,CN=Users,DC=cqure,DC=net
|
|
-- | sAMAccountName: Administrator
|
|
-- | dn: CN=Guest,CN=Users,DC=cqure,DC=net
|
|
-- | sAMAccountName: Guest
|
|
-- | dn: CN=SUPPORT_388945a0,CN=Users,DC=cqure,DC=net
|
|
-- | sAMAccountName: SUPPORT_388945a0
|
|
-- | dn: CN=EDUSRV011,OU=Domain Controllers,DC=cqure,DC=net
|
|
-- | sAMAccountName: EDUSRV011$
|
|
-- | dn: CN=krbtgt,CN=Users,DC=cqure,DC=net
|
|
-- | sAMAccountName: krbtgt
|
|
-- | dn: CN=Patrik Karlsson,CN=Users,DC=cqure,DC=net
|
|
-- | sAMAccountName: patrik
|
|
-- | dn: CN=VMABUSEXP008,CN=Computers,DC=cqure,DC=net
|
|
-- | sAMAccountName: VMABUSEXP008$
|
|
-- | dn: CN=ldaptest,CN=Users,DC=cqure,DC=net
|
|
-- |_ sAMAccountName: ldaptest
|
|
|
|
-- Credit
|
|
-- ------
|
|
-- o Martin Swende who provided me with the initial code that got me started writing this.
|
|
|
|
-- Version 0.5
|
|
-- Created 01/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
|
|
-- Revised 01/20/2010 - v0.2 - added SSL support
|
|
-- Revised 01/26/2010 - v0.3 - Changed SSL support to comm.tryssl, prefixed arguments with ldap, changes in determination of namingContexts
|
|
-- Revised 02/17/2010 - v0.4 - Added dependencie to ldap-brute and the abilitity to check for ldap accounts (credentials) stored in nmap registry
|
|
-- Capped output to 20 entries, use ldap.maxObjects to override
|
|
-- Revised 07/16/2010 - v0.5 - Fixed bug with empty contexts, added objectClass person to qfilter users, add error msg for invalid credentials
|
|
|
|
author = "Patrik Karlsson"
|
|
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
|
categories = {"discovery", "safe"}
|
|
|
|
require "ldap"
|
|
require 'shortport'
|
|
require 'comm'
|
|
|
|
dependencies = {"ldap-brute"}
|
|
|
|
portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"})
|
|
|
|
function action(host,port)
|
|
|
|
local status
|
|
local socket, opt
|
|
local args = nmap.registry.args
|
|
local username = args['ldap.username']
|
|
local password = args['ldap.password']
|
|
local qfilter = args['ldap.qfilter']
|
|
local base = args['ldap.base']
|
|
local attribs = args['ldap.attrib']
|
|
local accounts
|
|
local objCount = 0
|
|
local maxObjects = nmap.registry.args['ldap.maxobjects'] and tonumber(nmap.registry.args['ldap.maxobjects']) or 20
|
|
|
|
-- 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
|
|
|
|
-- Check if ldap-brute stored us some credentials
|
|
if ( not(username) and nmap.registry.ldapaccounts~=nil ) then
|
|
accounts = nmap.registry.ldapaccounts
|
|
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)
|
|
|
|
local req
|
|
local searchResEntries
|
|
local contexts = {}
|
|
local result = {}
|
|
local filter
|
|
|
|
if base == nil then
|
|
req = { baseObject = "", scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default, attributes = { "defaultNamingContext", "namingContexts" } }
|
|
status, searchResEntries = ldap.searchRequest( socket, req )
|
|
|
|
if not status then
|
|
socket:close()
|
|
return
|
|
end
|
|
|
|
contexts = ldap.extractAttribute( searchResEntries, "defaultNamingContext" )
|
|
|
|
-- OpenLDAP does not have a defaultNamingContext
|
|
if not contexts then
|
|
contexts = ldap.extractAttribute( searchResEntries, "namingContexts" )
|
|
end
|
|
else
|
|
table.insert(contexts, base)
|
|
end
|
|
|
|
if ( not(contexts) or #contexts == 0 ) then
|
|
stdnse.print_debug( "Failed to retrieve namingContexts" )
|
|
contexts = {""}
|
|
end
|
|
|
|
-- perform a bind only if we have valid credentials
|
|
if ( username ) then
|
|
local bindParam = { version=3, ['username']=username, ['password']=password}
|
|
local status, errmsg = ldap.bindRequest( socket, bindParam )
|
|
|
|
if not status then
|
|
stdnse.print_debug( string.format("ldap-search failed to bind: %s", errmsg) )
|
|
return " \n ERROR: Authentication failed"
|
|
end
|
|
-- or if ldap-brute found us something
|
|
elseif ( accounts ) then
|
|
for username, password in pairs(accounts) do
|
|
local bindParam = { version=3, ['username']=username, ['password']=password}
|
|
local status, errmsg = ldap.bindRequest( socket, bindParam )
|
|
|
|
if status then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if qfilter == "users" then
|
|
filter = { op=ldap.FILTER._or, val=
|
|
{
|
|
{ op=ldap.FILTER.equalityMatch, obj='objectClass', val='user' },
|
|
{ op=ldap.FILTER.equalityMatch, obj='objectClass', val='posixAccount' },
|
|
{ op=ldap.FILTER.equalityMatch, obj='objectClass', val='person' }
|
|
}
|
|
}
|
|
elseif qfilter == "computers" or qfilter == "computer" then
|
|
filter = { op=ldap.FILTER.equalityMatch, obj='objectClass', val='computer' }
|
|
elseif qfilter == "all" or qfilter == nil then
|
|
filter = nil -- { op=ldap.FILTER}
|
|
else
|
|
return " \n\nERROR: Unsupported Quick Filter: " .. qfilter
|
|
end
|
|
|
|
if type(attribs) == 'string' then
|
|
local tmp = attribs
|
|
attribs = {}
|
|
table.insert(attribs, tmp)
|
|
end
|
|
|
|
for _, context in ipairs(contexts) do
|
|
|
|
req = {
|
|
baseObject = context,
|
|
scope = ldap.SCOPE.sub,
|
|
derefPolicy = ldap.DEREFPOLICY.default,
|
|
filter = filter,
|
|
attributes = attribs,
|
|
['maxObjects'] = maxObjects }
|
|
status, searchResEntries = ldap.searchRequest( socket, req )
|
|
|
|
if not status then
|
|
if ( searchResEntries:match("DSID[-]0C090627") and not(username) ) then
|
|
return "ERROR: Failed to bind as the anonymous user"
|
|
else
|
|
stdnse.print_debug( string.format( "ldap.searchRequest returned: %s", searchResEntries ) )
|
|
return
|
|
end
|
|
end
|
|
|
|
local result_part = ldap.searchResultToTable( searchResEntries )
|
|
objCount = objCount + (result_part and #result_part or 0)
|
|
result_part.name = ""
|
|
|
|
if ( context ) then
|
|
result_part.name = ("Context: %s"):format(#context > 0 and context or "<empty>")
|
|
end
|
|
if ( qfilter ) then
|
|
result_part.name = result_part.name .. ("; QFilter: %s"):format(qfilter)
|
|
end
|
|
if ( attribs ) then
|
|
result_part.name = result_part.name .. ("; Attributes: %s"):format(stdnse.strjoin(",", attribs))
|
|
end
|
|
|
|
table.insert( result, result_part )
|
|
|
|
-- catch any softerrors
|
|
if searchResEntries.resultCode ~= 0 then
|
|
local output = stdnse.format_output(true, result )
|
|
output = output .. string.format(" \n\n\n=========== %s ===========", searchResEntries.errorMessage )
|
|
|
|
return output
|
|
end
|
|
|
|
end
|
|
|
|
-- perform a unbind only if we have valid credentials
|
|
if ( username ) then
|
|
status = ldap.unbindRequest( socket )
|
|
end
|
|
|
|
socket:close()
|
|
|
|
-- if taken a way and ldap returns a single result, it ain't shown....
|
|
--result.name = "LDAP Results"
|
|
|
|
local output = stdnse.format_output(true, result )
|
|
|
|
if ( maxObjects ~= -1 and objCount == maxObjects ) then
|
|
output = output .. (" \n\nResult limited to %d objects (see ldap.maxobjects)"):format(maxObjects)
|
|
end
|
|
|
|
return output
|
|
end
|