mirror of
https://github.com/nmap/nmap.git
synced 2025-12-12 10:49:02 +00:00
o [NSE] Added a new library for LDAP and two new scripts:
- ldap-brute uses the unpwdb library to guess credentials for LDAP - ldap-rootdse retrieves the LDAP root DSA-specific Entry (DSE)
This commit is contained in:
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
[NOT YET RELEASED]
|
[NOT YET RELEASED]
|
||||||
|
|
||||||
|
o [NSE] Added a new library for LDAP and two new scripts:
|
||||||
|
- ldap-brute uses the unpwdb library to guess credentials for LDAP
|
||||||
|
- ldap-rootdse retrieves the LDAP root DSA-specific Entry (DSE)
|
||||||
|
[Patrik]
|
||||||
|
|
||||||
o The -v and -d options are now handled in the same way. The three
|
o The -v and -d options are now handled in the same way. The three
|
||||||
forms are equivalent:
|
forms are equivalent:
|
||||||
-v -v -v -vvv -v3
|
-v -v -v -vvv -v3
|
||||||
|
|||||||
475
nselib/ldap.lua
Normal file
475
nselib/ldap.lua
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
--- Library methods for handling LDAP.
|
||||||
|
--
|
||||||
|
-- @author Patrik Karlsson
|
||||||
|
-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
|
||||||
|
--
|
||||||
|
-- Credit goes out to Martin Swende who provided me with the initial code that got me started writing this.
|
||||||
|
--
|
||||||
|
-- Version 0.3
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
module("ldap", package.seeall)
|
||||||
|
|
||||||
|
require("asn1")
|
||||||
|
|
||||||
|
local ldapMessageId = 1
|
||||||
|
|
||||||
|
ERROR_MSG = {}
|
||||||
|
ERROR_MSG[1] = "Intialization of LDAP library failed."
|
||||||
|
ERROR_MSG[4] = "Size limit exceeded."
|
||||||
|
ERROR_MSG[32] = "No such object"
|
||||||
|
ERROR_MSG[49] = "The supplied credential is invalid."
|
||||||
|
|
||||||
|
ERRORS = {
|
||||||
|
LDAP_SUCCESS = 0,
|
||||||
|
LDAP_SIZELIMIT_EXCEEDED = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Application constants
|
||||||
|
APPNO = {
|
||||||
|
BindRequest = 0,
|
||||||
|
BindResponse = 1,
|
||||||
|
UnbindRequest = 2,
|
||||||
|
SearchRequest = 3,
|
||||||
|
SearchResponse = 4,
|
||||||
|
SearchResDone = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Filter operation constants
|
||||||
|
FILTER = {
|
||||||
|
_and = 0,
|
||||||
|
_or = 1,
|
||||||
|
_not = 2,
|
||||||
|
equalityMatch = 3,
|
||||||
|
substrings = 4,
|
||||||
|
greaterOrEqual = 5,
|
||||||
|
lessOrEqual = 6,
|
||||||
|
present = 7,
|
||||||
|
approxMatch = 8,
|
||||||
|
extensibleMatch = 9
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Scope constants
|
||||||
|
SCOPE = {
|
||||||
|
base=0,
|
||||||
|
one=1,
|
||||||
|
sub= 2,
|
||||||
|
children=3,
|
||||||
|
default = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Deref policy constants
|
||||||
|
DEREFPOLICY = {
|
||||||
|
never=0,
|
||||||
|
searching=1,
|
||||||
|
finding = 2,
|
||||||
|
always=3,
|
||||||
|
default = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
-- LDAP specific tag encoders
|
||||||
|
local tagEncoder = {}
|
||||||
|
|
||||||
|
tagEncoder['table'] = function(self, val)
|
||||||
|
if (val._ldap == '0A') then
|
||||||
|
local ival = self.encodeInt(val[1])
|
||||||
|
local len = self.encodeLength(string.len(ival))
|
||||||
|
return bin.pack('HAA', '0A', len, ival)
|
||||||
|
end
|
||||||
|
if (val._ldaptype) then
|
||||||
|
local len
|
||||||
|
if val[1] == nil or string.len(val[1]) == 0 then
|
||||||
|
return bin.pack('HC', val._ldaptype, 0)
|
||||||
|
else
|
||||||
|
len = self.encodeLength(string.len(val[1]))
|
||||||
|
return bin.pack('HAA', val._ldaptype, len, val[1])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local encVal = ""
|
||||||
|
for _, v in ipairs(val) do
|
||||||
|
encVal = encVal .. encode(v) -- todo: buffer?
|
||||||
|
end
|
||||||
|
local tableType = bin.pack("H", "30")
|
||||||
|
if (val["_snmp"]) then
|
||||||
|
tableType = bin.pack("H", val["_snmp"])
|
||||||
|
end
|
||||||
|
return bin.pack('AAA', tableType, self.encodeLength(string.len(encVal)), encVal)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
---
|
||||||
|
-- Encodes a given value according to ASN.1 basic encoding rules for SNMP
|
||||||
|
-- packet creation.
|
||||||
|
-- @param val Value to be encoded.
|
||||||
|
-- @return Encoded value.
|
||||||
|
function encode(val)
|
||||||
|
|
||||||
|
local encoder = asn1.ASN1Encoder:new()
|
||||||
|
local encValue
|
||||||
|
|
||||||
|
encoder:registerTagEncoders(tagEncoder)
|
||||||
|
encValue = encoder:encode(val)
|
||||||
|
|
||||||
|
if encValue then
|
||||||
|
return encValue
|
||||||
|
end
|
||||||
|
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- LDAP specific tag decoders
|
||||||
|
local tagDecoder = {}
|
||||||
|
|
||||||
|
tagDecoder["0A"] = function( self, encStr, elen, pos )
|
||||||
|
return self.decodeInt(encStr, elen, pos)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- null decoder
|
||||||
|
tagDecoder["31"] = function( self, encStr, elen, pos )
|
||||||
|
return pos, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
-- Decodes an LDAP packet or a part of it according to ASN.1 basic encoding
|
||||||
|
-- rules.
|
||||||
|
-- @param encStr Encoded string.
|
||||||
|
-- @param pos Current position in the string.
|
||||||
|
-- @return The position after decoding
|
||||||
|
-- @return The decoded value(s).
|
||||||
|
function decode(encStr, pos)
|
||||||
|
-- register the LDAP specific tag decoders
|
||||||
|
local decoder = asn1.ASN1Decoder:new()
|
||||||
|
decoder:registerTagDecoders( tagDecoder )
|
||||||
|
return decoder:decode( encStr, pos )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
-- Decodes a sequence according to ASN.1 basic encoding rules.
|
||||||
|
-- @param encStr Encoded string.
|
||||||
|
-- @param len Length of sequence in bytes.
|
||||||
|
-- @param pos Current position in the string.
|
||||||
|
-- @return The position after decoding.
|
||||||
|
-- @return The decoded sequence as a table.
|
||||||
|
local function decodeSeq(encStr, len, pos)
|
||||||
|
local seq = {}
|
||||||
|
local sPos = 1
|
||||||
|
local sStr
|
||||||
|
pos, sStr = bin.unpack("A" .. len, encStr, pos)
|
||||||
|
if(sStr==nil) then
|
||||||
|
return pos,seq
|
||||||
|
end
|
||||||
|
while (sPos < len) do
|
||||||
|
local newSeq
|
||||||
|
sPos, newSeq = decode(sStr, sPos)
|
||||||
|
table.insert(seq, newSeq)
|
||||||
|
end
|
||||||
|
return pos, seq
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Encodes an LDAP Application operation and its data as a sequence
|
||||||
|
--
|
||||||
|
-- @param appno LDAP application number @see <code>APPNO</code>
|
||||||
|
-- @param isConstructed boolean true if constructed, false if primitive
|
||||||
|
-- @param data string containing the LDAP operation content
|
||||||
|
-- @return string containing the encoded LDAP operation
|
||||||
|
function encodeLDAPOp( appno, isConstructed, data )
|
||||||
|
local encoded_str = ""
|
||||||
|
local asn1_type = asn1.BERtoInt( asn1.BERCLASS.Application, isConstructed, appno )
|
||||||
|
|
||||||
|
encoded_str = encode( { _ldaptype = bin.pack("A", string.format("%X", asn1_type)), data } )
|
||||||
|
return encoded_str
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Performs an LDAP Search request
|
||||||
|
--
|
||||||
|
-- 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 finnish of 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 socket socket already connected to the ldap server
|
||||||
|
-- @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
|
||||||
|
function searchRequest( socket, params )
|
||||||
|
|
||||||
|
local searchResEntries = { errorMessage="", resultCode = 0}
|
||||||
|
local catch = function() socket:close() stdnse.print_debug(string.format("SearchRequest 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)
|
||||||
|
try( socket:send( data ) )
|
||||||
|
data = ""
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local len, pos, messageId = 0, 2, -1
|
||||||
|
local tmp = ""
|
||||||
|
local _, objectName, attributes, ldapOp
|
||||||
|
local attributes
|
||||||
|
local searchResEntry = {}
|
||||||
|
|
||||||
|
if ( maxObjects == 0 ) then
|
||||||
|
break
|
||||||
|
elseif ( maxObjects > 0 ) then
|
||||||
|
maxObjects = maxObjects - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if data:len() > 6 then
|
||||||
|
pos, len = decoder.decodeLength( data, pos )
|
||||||
|
else
|
||||||
|
data = data .. try( socket:receive() )
|
||||||
|
pos, len = decoder.decodeLength( data, pos )
|
||||||
|
end
|
||||||
|
-- pos should be at the right position regardless if length is specified in 1 or 2 bytes
|
||||||
|
while ( len + pos - 1 > data:len() ) do
|
||||||
|
data = data .. try( socket:receive() )
|
||||||
|
end
|
||||||
|
|
||||||
|
pos, messageId = decode( data, pos )
|
||||||
|
pos, tmp = bin.unpack("C", data, pos)
|
||||||
|
pos, len = decoder.decodeLength( data, pos )
|
||||||
|
ldapOp = asn1.intToBER( tmp )
|
||||||
|
searchResEntry = {}
|
||||||
|
|
||||||
|
if ldapOp.number == APPNO.SearchResDone then
|
||||||
|
pos, searchResEntry.resultCode = decode( data, pos )
|
||||||
|
-- errors may occur after a large amount of data has been received (eg. size limit exceeded)
|
||||||
|
-- we want to be able to return the data 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 data while still being able to catch the error
|
||||||
|
if ( searchResEntry.resultCode ~= 0 ) then
|
||||||
|
local error_msg
|
||||||
|
pos, searchResEntry.matchedDN = decode( data, pos )
|
||||||
|
pos, searchResEntry.errorMessage = decode( data, 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( data, pos )
|
||||||
|
if ldapOp.number == APPNO.SearchResponse then
|
||||||
|
pos, searchResEntry.attributes = decode( data, pos )
|
||||||
|
|
||||||
|
table.insert( searchResEntries, searchResEntry )
|
||||||
|
end
|
||||||
|
if data:len() > pos then
|
||||||
|
data = data:sub(pos)
|
||||||
|
else
|
||||||
|
data = ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true, searchResEntries
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Attempts to bind to the server using the credentials given
|
||||||
|
--
|
||||||
|
-- @param socket socket already connected to the ldap server
|
||||||
|
-- @param params table containing <code>version</code>, <code>username</code> and <code>password</code>
|
||||||
|
-- @return success true or false
|
||||||
|
-- @return err string containing error message
|
||||||
|
function bindRequest( socket, params )
|
||||||
|
|
||||||
|
local catch = function() socket:close() stdnse.print_debug(string.format("bindRequest failed")) end
|
||||||
|
local try = nmap.new_try(catch)
|
||||||
|
local ldapAuth = encode( { _ldaptype = 80, params.password } )
|
||||||
|
local bindReq = encode( params.version ) .. encode( params.username ) .. ldapAuth
|
||||||
|
local ldapMsg = encode(ldapMessageId) .. encodeLDAPOp( APPNO.BindRequest, true, bindReq )
|
||||||
|
local packet
|
||||||
|
local pos, packet_len, resultCode, tmp, len, _
|
||||||
|
local response = {}
|
||||||
|
|
||||||
|
local encoder = asn1.ASN1Encoder:new()
|
||||||
|
local decoder = asn1.ASN1Decoder:new()
|
||||||
|
|
||||||
|
encoder:registerTagEncoders(tagEncoder)
|
||||||
|
decoder:registerTagDecoders(tagDecoder)
|
||||||
|
|
||||||
|
packet = encoder:encodeSeq( ldapMsg )
|
||||||
|
ldapMessageId = ldapMessageId +1
|
||||||
|
try( socket:send( packet ) )
|
||||||
|
packet = try( socket:receive() )
|
||||||
|
|
||||||
|
pos, packet_len = decoder.decodeLength( packet, 2 )
|
||||||
|
pos, response.messageID = decode( packet, pos )
|
||||||
|
pos, tmp = bin.unpack("C", packet, pos)
|
||||||
|
pos, len = decoder.decodeLength( packet, pos )
|
||||||
|
response.protocolOp = asn1.intToBER( tmp )
|
||||||
|
|
||||||
|
if response.protocolOp.number ~= APPNO.BindResponse then
|
||||||
|
return false, string.format("Recieved incorrect Op in packet: %d, expected %d", response.protocolOp.number, APPNO.BindResponse)
|
||||||
|
end
|
||||||
|
|
||||||
|
pos, response.resultCode = decode( packet, pos )
|
||||||
|
|
||||||
|
if ( response.resultCode ~= 0 ) then
|
||||||
|
local error_msg
|
||||||
|
pos, response.matchedDN = decode( packet, pos )
|
||||||
|
pos, response.errorMessage = decode( packet, pos )
|
||||||
|
error_msg = ERROR_MSG[response.resultCode]
|
||||||
|
return false, string.format("Error: %s\nDetails: %s", error_msg or "", response.errorMessage or "" )
|
||||||
|
else
|
||||||
|
return true, "Success"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Performs an LDAP Unbind
|
||||||
|
--
|
||||||
|
-- @param socket socket already connected to the ldap server
|
||||||
|
-- @return success true or false
|
||||||
|
-- @return err string containing error message
|
||||||
|
function unbindRequest( socket )
|
||||||
|
|
||||||
|
local ldapMsg, packet
|
||||||
|
local catch = function() socket:close() stdnse.print_debug(string.format("bindRequest failed")) end
|
||||||
|
local try = nmap.new_try(catch)
|
||||||
|
|
||||||
|
local encoder = asn1.ASN1Encoder:new()
|
||||||
|
encoder:registerTagEncoders(tagEncoder)
|
||||||
|
|
||||||
|
ldapMessageId = ldapMessageId +1
|
||||||
|
ldapMsg = encode( ldapMessageId )
|
||||||
|
ldapMsg = ldapMsg .. encodeLDAPOp( APPNO.UnbindRequest, false, nil)
|
||||||
|
packet = encoder:encodeSeq( ldapMsg )
|
||||||
|
try( socket:send( packet ) )
|
||||||
|
return true, ""
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Creates an ASN1 structure from a filter table
|
||||||
|
--
|
||||||
|
-- @param filter table containing the filter to be created
|
||||||
|
-- @return string containing the ASN1 byte sequence
|
||||||
|
function createFilter( filter )
|
||||||
|
local asn1_type = asn1.BERtoInt( asn1.BERCLASS.ContextSpecific, true, filter.op )
|
||||||
|
local filter_str = ""
|
||||||
|
|
||||||
|
if type(filter.val) == 'table' then
|
||||||
|
for _, v in ipairs( filter.val ) do
|
||||||
|
filter_str = filter_str .. createFilter( v )
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local obj = encode( filter.obj )
|
||||||
|
local val = encode( filter.val )
|
||||||
|
|
||||||
|
filter_str = filter_str .. obj .. val
|
||||||
|
end
|
||||||
|
return encode( { _ldaptype=bin.pack("A", string.format("%X", asn1_type)), filter_str } )
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Converts a search result as received from searchRequest to a "result" table
|
||||||
|
--
|
||||||
|
-- Does some limited decoding of LDAP attributes
|
||||||
|
--
|
||||||
|
-- TODO: Add decoding of missing attributes
|
||||||
|
-- TODO: Add decoding of userParameters
|
||||||
|
-- TODO: Add decoding of loginHours
|
||||||
|
--
|
||||||
|
-- @param searchEntries table as returned from searchRequest
|
||||||
|
-- @return table suitable for <code>stdnse.format_output</code>
|
||||||
|
function searchResultToTable( searchEntries )
|
||||||
|
local result = {}
|
||||||
|
for _, v in ipairs( searchEntries ) do
|
||||||
|
local result_part = {}
|
||||||
|
if v.objectName and v.objectName:len() > 0 then
|
||||||
|
result_part.name = string.format("dn: %s", v.objectName)
|
||||||
|
else
|
||||||
|
result_part.name = "<ROOT>"
|
||||||
|
end
|
||||||
|
|
||||||
|
local attribs = {}
|
||||||
|
if ( v.attributes ~= nil ) then
|
||||||
|
for _, attrib in ipairs( v.attributes ) do
|
||||||
|
for i=2, #attrib do
|
||||||
|
-- do some additional Windows decoding
|
||||||
|
if ( attrib[1] == "objectSid" ) then
|
||||||
|
table.insert( attribs, string.format( "%s: %d", attrib[1], decode( attrib[i] ) ) )
|
||||||
|
elseif ( attrib[1] == "objectGUID") then
|
||||||
|
local _, o1, o2, o3, o4, o5, o6, o7, o8, o9, oa, ob, oc, od, oe, of = bin.unpack("C16", attrib[i] )
|
||||||
|
table.insert( attribs, string.format( "%s: %x%x%x%x-%x%x-%x%x-%x%x-%x%x%x%x%x", attrib[1], o4, o3, o2, o1, o5, o6, o7, o8, o9, oa, ob, oc, od, oe, of ) )
|
||||||
|
else
|
||||||
|
table.insert( attribs, string.format( "%s: %s", attrib[1], attrib[i] ) )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert( result_part, attribs )
|
||||||
|
end
|
||||||
|
table.insert( result, result_part )
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Extract naming context from a search response
|
||||||
|
--
|
||||||
|
-- @param searchEntries table containing searchEntries from a searchResponse
|
||||||
|
-- @param attributeName string containing the attribute to extract
|
||||||
|
-- @return table containing the attribute values
|
||||||
|
function extractAttribute( searchEntries, attributeName )
|
||||||
|
local attributeTbl = {}
|
||||||
|
for _, v in ipairs( searchEntries ) do
|
||||||
|
if ( v.attributes ~= nil ) then
|
||||||
|
for _, attrib in ipairs( v.attributes ) do
|
||||||
|
local attribType = attrib[1]
|
||||||
|
for i=2, #attrib do
|
||||||
|
if attribType == attributeName then
|
||||||
|
table.insert( attributeTbl, attrib[i])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ( #attributeTbl > 0 and attributeTbl or nil )
|
||||||
|
end
|
||||||
255
scripts/ldap-brute.nse
Normal file
255
scripts/ldap-brute.nse
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
description = [[
|
||||||
|
Performs password guessing against LDAP
|
||||||
|
]]
|
||||||
|
|
||||||
|
---
|
||||||
|
-- @usage
|
||||||
|
-- nmap -p 389 --script ldap-brute --script-args
|
||||||
|
-- ldap.base='"cn=users,dc=cqure,dc=net"' <host>
|
||||||
|
--
|
||||||
|
-- @output
|
||||||
|
-- 389/tcp open ldap
|
||||||
|
-- | ldap-brute:
|
||||||
|
-- |_ ldaptest:ldaptest => Login Correct
|
||||||
|
--
|
||||||
|
-- @args ldap.base If set, the script will use it as a base for the password
|
||||||
|
-- guessing attempts. If unset the user list must either contain the
|
||||||
|
-- distinguished name of each user or the server must support
|
||||||
|
-- authentication using a simple user name. See AD discussion below.
|
||||||
|
--
|
||||||
|
-- Additional information
|
||||||
|
-- ----------------------
|
||||||
|
-- This script makes attempts to brute force LDAP authentication. By default
|
||||||
|
-- it uses the builtin user- and password-list to do so. In order to use your
|
||||||
|
-- own lists use the userdb and passdb script arguments.
|
||||||
|
--
|
||||||
|
-- WARNING: This script does not make ANY attempt to prevent account lockout!
|
||||||
|
-- If the number of passwords in the dictionary exceed the amount of
|
||||||
|
-- allowed tries, accounts will be locked out. This usually happens
|
||||||
|
-- *VERY* quickly.
|
||||||
|
--
|
||||||
|
-- Active Directory and LDAP
|
||||||
|
-- -------------------------
|
||||||
|
-- Note: Authenticating against Active Directory using LDAP does not use the
|
||||||
|
-- Windows user name but the user accounts distinguished name. LDAP on Windows
|
||||||
|
-- 2003 allows authentication using a simple user name rather than using the
|
||||||
|
-- fully distinguished name. Eg:
|
||||||
|
-- - Patrik Karlsson vs. cn=Patrik Karlsson,cn=Users,dc=cqure,dc=net
|
||||||
|
-- This type of authentication is not supported on eg. OpenLDAP
|
||||||
|
--
|
||||||
|
-- This script uses some AD specific support and optimizations:
|
||||||
|
--
|
||||||
|
-- o LDAP on Windows 2003 reports different error messages depending on whether
|
||||||
|
-- an account exists or not. If the script recieves an error indicating that
|
||||||
|
-- the username does not exist it simply stops guessing passwords for this
|
||||||
|
-- account and moves on to the next.
|
||||||
|
--
|
||||||
|
-- o The script attempts to authenticate with the username only if no LDAP base
|
||||||
|
-- is specified. The benefit of authenticating this way is that the LDAP path
|
||||||
|
-- of each account does not need to be known in advance as it's looked up by
|
||||||
|
-- the server.
|
||||||
|
--
|
||||||
|
-- Credits
|
||||||
|
-- -------
|
||||||
|
-- o The get_random_string function was borrowed from the smb-psexec script.
|
||||||
|
--
|
||||||
|
|
||||||
|
author = "Patrik Karlsson"
|
||||||
|
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
||||||
|
categories = {"intrusive", "auth"}
|
||||||
|
|
||||||
|
require 'shortport'
|
||||||
|
require 'stdnse'
|
||||||
|
require 'ldap'
|
||||||
|
require 'unpwdb'
|
||||||
|
require 'comm'
|
||||||
|
|
||||||
|
-- Version 0.3
|
||||||
|
-- Created 01/20/2010 - v0.1 - created by Patrik Karlsson
|
||||||
|
-- Revised 01/26/2010 - v0.2 - cleaned up unpwdb related code, fixed ssl stuff
|
||||||
|
-- Revised 02/17/2010 - v0.3 - added AD specific checks and fixed bugs related to LDAP base
|
||||||
|
|
||||||
|
portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"})
|
||||||
|
|
||||||
|
--- Tries to determine a valid naming context to use to validate credentials
|
||||||
|
--
|
||||||
|
-- @param socket socket already connected to LDAP server
|
||||||
|
-- @return string containing a valid naming context
|
||||||
|
function get_naming_context( socket )
|
||||||
|
|
||||||
|
local req = { baseObject = "", scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default, attributes = { "defaultNamingContext", "namingContexts" } }
|
||||||
|
local status, searchResEntries = ldap.searchRequest( socket, req )
|
||||||
|
|
||||||
|
if not status then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local contexts = ldap.extractAttribute( searchResEntries, "defaultNamingContext" )
|
||||||
|
|
||||||
|
-- OpenLDAP does not have a defaultNamingContext
|
||||||
|
if not contexts then
|
||||||
|
contexts = ldap.extractAttribute( searchResEntries, "namingContexts" )
|
||||||
|
end
|
||||||
|
|
||||||
|
if #contexts > 0 then
|
||||||
|
return contexts[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Attempts to validate the credentials by requesting the base object of the supplied context
|
||||||
|
--
|
||||||
|
-- @param socket socket already connected to the LDAP server
|
||||||
|
-- @param context string containing the context to search
|
||||||
|
-- @return true if credentials are valid and search was a success, false if not.
|
||||||
|
function is_valid_credential( socket, context )
|
||||||
|
local req = { baseObject = context, scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default, attributes = nil }
|
||||||
|
local status, searchResEntries = ldap.searchRequest( socket, req )
|
||||||
|
|
||||||
|
return status
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
action = function( host, port )
|
||||||
|
|
||||||
|
local result, response, status, context, valid_accounts = {}, nil, nil, nil, {}
|
||||||
|
local usernames, passwords, username, password, fq_username
|
||||||
|
local user_cnt, invalid_account_cnt, tot_tries = 0, 0, 0
|
||||||
|
local aborted
|
||||||
|
|
||||||
|
local max_time = unpwdb.timelimit() ~= nil and unpwdb.timelimit() * 1000 or -1
|
||||||
|
local clock_start = nmap.clock_ms()
|
||||||
|
|
||||||
|
local ldap_anonymous_bind = string.char( 0x30, 0x0c, 0x02, 0x01, 0x01, 0x60, 0x07, 0x02, 0x01, 0x03, 0x04, 0x00, 0x80, 0x00 )
|
||||||
|
local socket, _, opt = comm.tryssl( host, port, ldap_anonymous_bind, nil )
|
||||||
|
|
||||||
|
local base_dn = nmap.registry.args['ldap.base']
|
||||||
|
|
||||||
|
if not socket then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- We close and re-open the socket so that the anonymous bind does not distract us
|
||||||
|
socket:close()
|
||||||
|
-- set a reasonable timeout value
|
||||||
|
socket:set_timeout(5000)
|
||||||
|
status = socket:connect(host.ip, port.number, opt)
|
||||||
|
if not status then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
context = get_naming_context(socket)
|
||||||
|
|
||||||
|
if not context then
|
||||||
|
stdnse.print_debug("Failed to retrieve namingContext")
|
||||||
|
socket:close()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
status, usernames = unpwdb.usernames()
|
||||||
|
if not status then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
status, passwords = unpwdb.passwords()
|
||||||
|
if not status then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for username in usernames do
|
||||||
|
-- is the aborted flag set
|
||||||
|
if ( aborted ) then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- if a base DN was set append our username (CN) to the base
|
||||||
|
if base_dn then
|
||||||
|
fq_username = ("cn=%s,%s"):format(username, base_dn)
|
||||||
|
else
|
||||||
|
fq_username = username
|
||||||
|
end
|
||||||
|
user_cnt = user_cnt + 1
|
||||||
|
for password in passwords do
|
||||||
|
|
||||||
|
-- Should we abort?
|
||||||
|
if max_time>0 and nmap.clock_ms() - clock_start > max_time then
|
||||||
|
aborted=true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
tot_tries = tot_tries + 1
|
||||||
|
|
||||||
|
-- handle special case where we want to guess the username as password
|
||||||
|
if password == "%username%" then
|
||||||
|
password = username
|
||||||
|
end
|
||||||
|
|
||||||
|
stdnse.print_debug( "Trying %s/%s ...", fq_username, password )
|
||||||
|
status, response = ldap.bindRequest( socket, { version=3, ['username']=fq_username, ['password']=password} )
|
||||||
|
|
||||||
|
-- if the DN (username) does not exist, break loop
|
||||||
|
if not status and response:match("invalid DN") then
|
||||||
|
stdnse.print_debug( "%s returned: \"Invalid DN\"", fq_username )
|
||||||
|
invalid_account_cnt = invalid_account_cnt + 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Is AD telling us the account does not exist?
|
||||||
|
if not status and response:match("AcceptSecurityContext error, data 525, vece") then
|
||||||
|
invalid_account_cnt = invalid_account_cnt + 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Account Locked Out
|
||||||
|
if not status and response:match("AcceptSecurityContext error, data 775, vece") then
|
||||||
|
table.insert( valid_accounts, string.format("%s => Account locked out", fq_username ) )
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Login correct, account disabled
|
||||||
|
if not status and response:match("AcceptSecurityContext error, data 533, vece") then
|
||||||
|
table.insert( valid_accounts, string.format("%s:%s => Login correct, account disabled", fq_username, password:len()>0 and password or "<empty>" ) )
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Login correct, user must change password
|
||||||
|
if not status and response:match("AcceptSecurityContext error, data 773, vece") then
|
||||||
|
table.insert( valid_accounts, string.format("%s:%s => Login correct, user must change password", fq_username, password:len()>0 and password or "<empty>" ) )
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
--Login, correct
|
||||||
|
if status then
|
||||||
|
status = is_valid_credential( socket, context )
|
||||||
|
if status then
|
||||||
|
table.insert( valid_accounts, string.format("%s:%s => Login correct", fq_username, password:len()>0 and password or "<empty>" ) )
|
||||||
|
|
||||||
|
-- Add credentials for other ldap scripts to use
|
||||||
|
if nmap.registry.ldapaccounts == nil then
|
||||||
|
nmap.registry.ldapaccounts = {}
|
||||||
|
end
|
||||||
|
nmap.registry.ldapaccounts[fq_username]=password
|
||||||
|
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
passwords("reset")
|
||||||
|
end
|
||||||
|
|
||||||
|
stdnse.print_debug( "Finnished brute against LDAP, total tries: %d, tps: %d", tot_tries, ( tot_tries / ( ( nmap.clock_ms() - clock_start ) / 1000 ) ) )
|
||||||
|
|
||||||
|
if ( invalid_account_cnt == user_cnt and base_dn ~= nil ) then
|
||||||
|
return "WARNING: All usernames were invalid. Invalid LDAP base?"
|
||||||
|
end
|
||||||
|
|
||||||
|
local output = stdnse.format_output(true, valid_accounts) or ""
|
||||||
|
|
||||||
|
if ( max_time > 0 and aborted ) then
|
||||||
|
output = output .. string.format(" \n\nNOTE: script aborted execution after %d seconds", max_time/1000 )
|
||||||
|
end
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
end
|
||||||
151
scripts/ldap-rootdse.nse
Normal file
151
scripts/ldap-rootdse.nse
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
description = [[
|
||||||
|
Retrieves the LDAP root DSA-specific Entry (DSE)
|
||||||
|
]]
|
||||||
|
|
||||||
|
---
|
||||||
|
--
|
||||||
|
-- @usage
|
||||||
|
-- nmap -p 389 --script ldap-rootdse <host>
|
||||||
|
--
|
||||||
|
-- @output
|
||||||
|
-- PORT STATE SERVICE
|
||||||
|
-- 389/tcp open ldap
|
||||||
|
-- | ldap-rootdse:
|
||||||
|
-- | currentTime: 20100112092616.0Z
|
||||||
|
-- | subschemaSubentry: CN=Aggregate,CN=Schema,CN=Configuration,DC=cqure,DC=net
|
||||||
|
-- | dsServiceName: CN=NTDS Settings,CN=LDAPTEST001,CN=Servers,CN=Default-First-Site,CN=Sites,CN=Configuration,DC=cqure,DC=net
|
||||||
|
-- | namingContexts: DC=cqure,DC=net
|
||||||
|
-- | namingContexts: CN=Configuration,DC=cqure,DC=net
|
||||||
|
-- | namingContexts: CN=Schema,CN=Configuration,DC=cqure,DC=net
|
||||||
|
-- | namingContexts: DC=DomainDnsZones,DC=cqure,DC=net
|
||||||
|
-- | namingContexts: DC=ForestDnsZones,DC=cqure,DC=net
|
||||||
|
-- | namingContexts: DC=TAPI3Directory,DC=cqure,DC=net
|
||||||
|
-- | defaultNamingContext: DC=cqure,DC=net
|
||||||
|
-- | schemaNamingContext: CN=Schema,CN=Configuration,DC=cqure,DC=net
|
||||||
|
-- | configurationNamingContext: CN=Configuration,DC=cqure,DC=net
|
||||||
|
-- | rootDomainNamingContext: DC=cqure,DC=net
|
||||||
|
-- | supportedControl: 1.2.840.113556.1.4.319
|
||||||
|
-- | .
|
||||||
|
-- | .
|
||||||
|
-- | supportedControl: 1.2.840.113556.1.4.1948
|
||||||
|
-- | supportedLDAPVersion: 3
|
||||||
|
-- | supportedLDAPVersion: 2
|
||||||
|
-- | supportedLDAPPolicies: MaxPoolThreads
|
||||||
|
-- | supportedLDAPPolicies: MaxDatagramRecv
|
||||||
|
-- | supportedLDAPPolicies: MaxReceiveBuffer
|
||||||
|
-- | supportedLDAPPolicies: InitRecvTimeout
|
||||||
|
-- | supportedLDAPPolicies: MaxConnections
|
||||||
|
-- | supportedLDAPPolicies: MaxConnIdleTime
|
||||||
|
-- | supportedLDAPPolicies: MaxPageSize
|
||||||
|
-- | supportedLDAPPolicies: MaxQueryDuration
|
||||||
|
-- | supportedLDAPPolicies: MaxTempTableSize
|
||||||
|
-- | supportedLDAPPolicies: MaxResultSetSize
|
||||||
|
-- | supportedLDAPPolicies: MaxNotificationPerConn
|
||||||
|
-- | supportedLDAPPolicies: MaxValRange
|
||||||
|
-- | highestCommittedUSN: 126991
|
||||||
|
-- | supportedSASLMechanisms: GSSAPI
|
||||||
|
-- | supportedSASLMechanisms: GSS-SPNEGO
|
||||||
|
-- | supportedSASLMechanisms: EXTERNAL
|
||||||
|
-- | supportedSASLMechanisms: DIGEST-MD5
|
||||||
|
-- | dnsHostName: EDUSRV011.cqure.local
|
||||||
|
-- | ldapServiceName: cqure.net:edusrv011$@CQURE.NET
|
||||||
|
-- | serverName: CN=EDUSRV011,CN=Servers,CN=Default-First-Site,CN=Sites,CN=Configuration,DC=cqure,DC=net
|
||||||
|
-- | supportedCapabilities: 1.2.840.113556.1.4.800
|
||||||
|
-- | supportedCapabilities: 1.2.840.113556.1.4.1670
|
||||||
|
-- | supportedCapabilities: 1.2.840.113556.1.4.1791
|
||||||
|
-- | isSynchronized: TRUE
|
||||||
|
-- | isGlobalCatalogReady: TRUE
|
||||||
|
-- | domainFunctionality: 0
|
||||||
|
-- | forestFunctionality: 0
|
||||||
|
-- |_ domainControllerFunctionality: 2
|
||||||
|
--
|
||||||
|
--
|
||||||
|
-- The root DSE object may contain a number of different attributes as described in RFC 2251 section 3.4:
|
||||||
|
-- * namingContexts: naming contexts held in the server
|
||||||
|
-- * subschemaSubentry: subschema entries (or subentries) known by this server
|
||||||
|
-- * altServer: alternative servers in case this one is later unavailable.
|
||||||
|
-- * supportedExtension: list of supported extended operations.
|
||||||
|
-- * supportedControl: list of supported controls.
|
||||||
|
-- * supportedSASLMechanisms: list of supported SASL security features.
|
||||||
|
-- * supportedLDAPVersion: LDAP versions implemented by the server.
|
||||||
|
--
|
||||||
|
-- The above example, which contains a lot more information is from Windows 2003 accessible without authentication.
|
||||||
|
-- The same request against OpenLDAP will result in significantly less information.
|
||||||
|
--
|
||||||
|
-- The ldap-search script queries the root DSE for the namingContexts and/or defaultNamingContexts, which it sets as base
|
||||||
|
-- if no base object was specified
|
||||||
|
--
|
||||||
|
-- Credit goes out to Martin Swende who provided me with the initial code that got me started writing this.
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Version 0.2
|
||||||
|
-- Created 01/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
|
||||||
|
-- Revised 01/20/2010 - v0.2 - added SSL support
|
||||||
|
|
||||||
|
author = "Patrik Karlsson"
|
||||||
|
copyright = "Patrik Karlsson"
|
||||||
|
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
||||||
|
categories = {"discovery", "safe"}
|
||||||
|
|
||||||
|
require "ldap"
|
||||||
|
require 'shortport'
|
||||||
|
require 'comm'
|
||||||
|
|
||||||
|
portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"})
|
||||||
|
|
||||||
|
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 = 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
|
||||||
|
|
||||||
|
-- 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)
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
if not status then
|
||||||
|
socket:close()
|
||||||
|
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"
|
||||||
|
|
||||||
|
return stdnse.format_output(true, result )
|
||||||
|
|
||||||
|
end
|
||||||
@@ -39,6 +39,8 @@ Entry { filename = "http-vmware-path-vuln.nse", categories = { "default", "safe"
|
|||||||
Entry { filename = "iax2-version.nse", categories = { "version", } }
|
Entry { filename = "iax2-version.nse", categories = { "version", } }
|
||||||
Entry { filename = "imap-capabilities.nse", categories = { "default", "safe", } }
|
Entry { filename = "imap-capabilities.nse", categories = { "default", "safe", } }
|
||||||
Entry { filename = "irc-info.nse", categories = { "default", "discovery", "safe", } }
|
Entry { filename = "irc-info.nse", categories = { "default", "discovery", "safe", } }
|
||||||
|
Entry { filename = "ldap-brute.nse", categories = { "auth", "intrusive", } }
|
||||||
|
Entry { filename = "ldap-rootdse.nse", categories = { "discovery", "safe", } }
|
||||||
Entry { filename = "lexmark-config.nse", categories = { "discovery", "safe", } }
|
Entry { filename = "lexmark-config.nse", categories = { "discovery", "safe", } }
|
||||||
Entry { filename = "mongodb-databases.nse", categories = { "default", "discovery", "safe", } }
|
Entry { filename = "mongodb-databases.nse", categories = { "default", "discovery", "safe", } }
|
||||||
Entry { filename = "mongodb-info.nse", categories = { "default", "discovery", "safe", } }
|
Entry { filename = "mongodb-info.nse", categories = { "default", "discovery", "safe", } }
|
||||||
|
|||||||
Reference in New Issue
Block a user