mirror of
https://github.com/nmap/nmap.git
synced 2025-12-09 06:01:28 +00:00
socket:connect(host.ip, port.number) socket:connect(host.ip, port.number, port.protocol) to this: socket:connect(host, port) connect can take host and port tables now, and the default protocol is taken from the port table if possible.
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, port, 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
|