1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
|
local comm = require "comm"
local ldap = require "ldap"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
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, ad_dcs, custom or all. If no
-- value is specified it defaults to all.
-- @args ldap.searchattrib When used with the 'custom' qfilter, this parameter
-- works in conjunction with ldap.searchvalue to allow the user to
-- specify a custom attribute and value as search criteria.
-- @args ldap.searchvalue When used with the 'custom' qfilter, this parameter
-- works in conjunction with ldap.searchattrib to allow the user to
-- specify a custom attribute and value as search criteria.
-- This parameter DOES PERMIT the use of the asterisk '*' as a wildcard.
-- @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.
-- @args ldap.savesearch If set, the script will save the output to a file
-- beginning with the specified path and name. The file suffix of .CSV
-- as well as the hostname and port will automatically be added based on
-- the output type selected.
--
-- @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>
--
-- nmap -p 389 --script ldap-search --script-args 'ldap.username="cn=ldaptest,cn=users,dc=cqure,dc=net",ldap.password=ldaptest,
-- ldap.qfilter=custom,ldap.searchattrib="operatingSystem",ldap.searchvalue="Windows *Server*",ldap.attrib={operatingSystem,whencreated,OperatingSystemServicePack}' <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
--
--
-- PORT STATE SERVICE REASON
-- 389/tcp open ldap syn-ack
-- | ldap-search:
-- | Context: DC=cqure,DC=net; QFilter: custom; Attributes: operatingSystem,whencreated,OperatingSystemServicePack
-- | dn: CN=USDC01,OU=Domain Controllers,DC=cqure,DC=net
-- | whenCreated: 2010/08/27 17:30:16 UTC
-- | operatingSystem: Windows Server 2008 R2 Datacenter
-- | operatingSystemServicePack: Service Pack 1
-- | dn: CN=TESTBOX,OU=Test Servers,DC=cqure,DC=net
-- | whenCreated: 2010/09/04 00:33:02 UTC
-- | operatingSystem: Windows Server 2008 R2 Standard
-- |_ operatingSystemServicePack: Service Pack 1
-- Credit
-- ------
-- o Martin Swende who provided me with the initial code that got me started writing this.
-- Version 0.8
-- 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 dependency 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
-- Revised 09/05/2011 - v0.6 - Added support for saving searches to a file via argument ldap.savesearch
-- Revised 10/29/2011 - v0.7 - Added support for custom searches and the ability to leverage LDAP substring search functionality added to LDAP.lua
-- Revised 10/30/2011 - v0.8 - Added support for ad_dcs (AD domain controller ) searches and the ability to leverage LDAP extensibleMatch filter added to LDAP.lua
author = "Patrik Karlsson"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
dependencies = {"ldap-brute"}
portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"})
local function fail (err) return stdnse.format_output(false, err) end
function action(host,port)
local status
local socket, opt
local args = nmap.registry.args
local username = stdnse.get_script_args('ldap.username')
local password = stdnse.get_script_args('ldap.password')
local qfilter = stdnse.get_script_args('ldap.qfilter')
local searchAttrib = stdnse.get_script_args('ldap.searchattrib')
local searchValue = stdnse.get_script_args('ldap.searchvalue')
local base = stdnse.get_script_args('ldap.base')
local attribs = stdnse.get_script_args('ldap.attrib')
local saveFile = stdnse.get_script_args('ldap.savesearch')
local accounts
local objCount = 0
local maxObjects = tonumber(stdnse.get_script_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 = "\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 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.debug1( "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.debug1("ldap-search failed to bind: %s", errmsg)
return fail("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 == "ad_dcs" then
filter = { op=ldap.FILTER.extensibleMatch, obj='userAccountControl', val='1.2.840.113556.1.4.803:=8192' }
elseif qfilter == "custom" then
if searchAttrib == nil or searchValue == nil then
return fail("Please specify both ldap.searchAttrib and ldap.searchValue using using the custom qfilter.")
end
if string.find(searchValue, '*') == nil then
filter = { op=ldap.FILTER.equalityMatch, obj=searchAttrib, val=searchValue }
else
filter = { op=ldap.FILTER.substrings, obj=searchAttrib, val=searchValue }
end
elseif qfilter == "all" or qfilter == nil then
filter = nil -- { op=ldap.FILTER}
else
return fail("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 fail("Failed to bind as the anonymous user")
else
stdnse.debug1("ldap.searchRequest returned: %s", searchResEntries)
return
end
end
local result_part = ldap.searchResultToTable( searchResEntries )
if saveFile then
local output_file = saveFile .. "_" .. host.ip .. "_" .. port.number .. ".csv"
local save_status, save_err = ldap.searchResultToFile(searchResEntries,output_file)
if not save_status then
stdnse.debug1("%s", save_err)
end
end
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(table.concat(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
|