diff --git a/CHANGELOG b/CHANGELOG index 6f3dcf1dc..38898d5c6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added support for dynamic updates to the DNS library. Added the + script dns-update.nse, which attempts to add a DNS record to a given zone. + [Patrik] + o [Ncat] Make --exec and --idle-timeout work when connecting with --proxy. Florian Roth reported the bug. [David] diff --git a/nselib/dns.lua b/nselib/dns.lua index 800386c71..952c1a424 100644 --- a/nselib/dns.lua +++ b/nselib/dns.lua @@ -57,6 +57,11 @@ types = { ANY = 255 } +CLASS = { + IN = 1, + ANY = 255 +} + --- -- Repeatedly sends UDP packets to host, waiting for an answer. @@ -676,6 +681,22 @@ function findNiceAdditional(dtype, dec, retAll) end end +-- +-- Encodes a FQDN +-- @param fqdn containing the fully qualified domain name +-- @return encQ containing the encoded value +local function encodeFQDN(fqdn) + if ( not(fqdn) or #fqdn == 0 ) then return end + + local parts = stdnse.strsplit("%.", fqdn) + local encQ = "" + for _, part in ipairs(parts) do + encQ = encQ .. bin.pack("p", part) + end + encQ = encQ .. string.char(0) + return encQ +end + --- -- Encodes the question part of a DNS request. -- @param questions Table of questions. @@ -684,16 +705,30 @@ local function encodeQuestions(questions) if type(questions) ~= "table" then return nil end local encQ = "" for _, v in ipairs(questions) do - local parts = stdnse.strsplit("%.", v.dname) - for _, part in ipairs(parts) do - encQ = encQ .. bin.pack("p", part) - end - encQ = encQ .. string.char(0) + encQ = encQ .. encodeFQDN(v.dname) encQ = encQ .. bin.pack(">SS", v.dtype, v.class) end return encQ end +--- +-- Encodes the zone part of a DNS request. +-- @param questions Table of questions. +-- @return Encoded question string. +local function encodeZones(zones) + return encodeQuestions(zones) +end + +local function encodeUpdates(updates) + if type(updates) ~= "table" then return nil end + local encQ = "" + for _, v in ipairs(updates) do + encQ = encQ .. encodeFQDN(v.dname) + encQ = encQ .. bin.pack(">SSISA", v.dtype, v.class, v.ttl, #v.data, v.data) + end + return encQ +end + --- -- Encodes DNS flags to a binary digit string. -- @param flags Flag table, each entry representing a flag (QR, OCx, AA, TC, RD, @@ -730,8 +765,21 @@ end function encode(pkt) if type(pkt) ~= "table" then return nil end local encFlags = encodeFlags(pkt.flags) - local encQs = encodeQuestions(pkt.questions) - local encStr = bin.pack(">SBS4", pkt.id, encFlags, #pkt.questions, #pkt.answers, #pkt.auth, #pkt.additional) .. encQs + local data = encodeQuestions(pkt.questions) + local qorzlen = #pkt.questions + local aorplen = #pkt.answers + local aorulen = #pkt.auth + + if ( #pkt.questions < 1 ) then + -- The packet has no questions, assume we're dealing with an update + data = encodeZones( pkt.zones ) + qorzlen = #pkt.zones + + aorulen = #pkt.updates + data = data .. encodeUpdates( pkt.updates ) + end + + local encStr = bin.pack(">SBS4", pkt.id, encFlags, qorzlen, aorplen, aorulen, #pkt.additional) .. data return encStr end @@ -1037,14 +1085,20 @@ function decode(data) -- for now, don't decode the flags pkt.flags = decodeFlags(encFlags) - pos, pkt.questions = decodeQuestions(data, cnt.q, pos) - - pos, pkt.answers = decodeRR(data, cnt.a, pos) - - pos, pkt.auth = decodeRR(data, cnt.auth, pos) - - pos, pkt.add = decodeRR(data, cnt.add, pos) - + -- + -- check whether this is an update response or not + -- a quick fix to allow decoding of non updates and not break for updates + -- the flags are enough for the current code to determine whether an update was successful or not + -- + local strflags=encodeFlags(pkt.flags) + if ( strflags:sub(1,4) == "1010" ) then + return pkt + else + pos, pkt.questions = decodeQuestions(data, cnt.q, pos) + pos, pkt.answers = decodeRR(data, cnt.a, pos) + pos, pkt.auth = decodeRR(data, cnt.auth, pos) + pos, pkt.add = decodeRR(data, cnt.add, pos) + end return pkt end @@ -1058,6 +1112,8 @@ function newPacket() pkt.flags = {} pkt.flags.RD = true pkt.questions = {} + pkt.zones = {} + pkt.updates = {} pkt.answers = {} pkt.auth = {} pkt.additional = {} @@ -1076,7 +1132,7 @@ function addQuestion(pkt, dname, dtype) local q = {} q.dname = dname q.dtype = dtype - q.class = 1 + q.class = CLASS.IN table.insert(pkt.questions, q) return pkt end @@ -1087,3 +1143,131 @@ get_default_timeout = function() return timeout[nmap.timing_level()] or 4000 end +--- +-- Adds a zone to a DNS packet table +-- @param pkt Table representing DNS packet. +-- @param dname Domain name to be asked. +function addZone(pkt, dname) + if ( type(pkt) ~= "table" ) or (type(pkt.updates) ~= "table") then return nil end + table.insert(pkt.zones, { dname=dname, dtype=types.SOA, class=CLASS.IN }) + return pkt +end + +--- +-- Adds a update to a DNS packet table +-- @param pkt Table representing DNS packet. +-- @param dname Domain name to be asked. +-- @param dtype to be updated +-- @param ttl the time-to-live of the record +-- @param data type specific data +function addUpdate(pkt, dname, dtype, ttl, data, class) + if ( type(pkt) ~= "table" ) or (type(pkt.updates) ~= "table") then return nil end + table.insert(pkt.updates, { dname=dname, dtype=dtype, class=class, ttl=ttl, data=(data or "") } ) + return pkt +end + + +--- Adds a record to the Zone +-- @param dname containing the hostname to add +-- @param options A table containing any of the following fields: +-- * dtype: Desired DNS record type (default: "A"). +-- * host: DNS server to be queried (default: DNS servers known to Nmap). +-- * timeout: The time to wait for a response +-- * sendCount: The number of send attempts to perform +-- * zone: If not supplied deduced from hostname +-- * data: Table or string containing update data (depending on record type): +-- A - String containing the IP address +-- CNAME - String containing the FQDN +-- MX - Table containing pref, mx +-- SRV - Table containing prio, weight, port, target +-- +-- @return status true on success false on failure +-- @return msg containing the error message +-- +-- Examples +-- +-- Adding different types of records to a server +-- * update( "www.cqure.net", { host=host, port=port, dtype="A", data="10.10.10.10" } ) +-- * update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="www.cqure.net" } ) +-- * update( "cqure.net", { host=host, port=port, dtype="MX", data={ pref=10, mx="mail.cqure.net"} }) +-- * update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data={ prio=0, weight=100, port=389, target="ldap.cqure.net" } } ) +-- +-- Removing the above records by setting an empty data and a ttl of zero +-- * update( "www.cqure.net", { host=host, port=port, dtype="A", data="", ttl=0 } ) +-- * update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="", ttl=0 } ) +-- * update( "cqure.net", { host=host, port=port, dtype="MX", data="", ttl=0 } ) +-- * update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data="", ttl=0 } ) +-- +function update(dname, options) + local options = options or {} + local pkt = newPacket() + local flags = pkt.flags + local host, port = options.host, options.port + local timeout = ( type(options.timeout) == "number" ) and options.timeout or get_default_timeout() + local sendcount = options.sendCount or 2 + local dtype = ( type(options.dtype) == "string" ) and types[options.dtype] or types.A + local updata = options.data + local ttl = options.ttl or 86400 + local zone = options.zone or dname:match("^.-%.(.+)$") + local class = CLASS.IN + + assert(host, "dns.update needs a valid host in options") + assert(port, "dns.update needs a valid port in options") + + if ( options.zone ) then dname = dname .. "." .. options.zone end + + if ( not(zone) and not( dname:match("^.-%..+") ) ) then + return false, "hostname needs to be supplied as FQDN" + end + + flags.RD = false + flags.OC1, flags.OC2, flags.OC3, flags.OC4 = false, true, false, true + + -- If ttl is zero and updata is string and zero length or nil, assume delete record + if ( ttl == 0 and ( ( type(updata) == "string" and #updata == 0 ) or not(updata) ) ) then + class = CLASS.ANY + updata = "" + if ( types.MX == dtype and not(options.zone) ) then zone=dname end + if ( types.SRV == dtype and not(options.zone) ) then + zone=dname:match("^_.-%._.-%.(.+)$") + end + -- if not, let's try to update the zone + else + if ( dtype == types.A ) then + updata = updata and bin.pack(">I", ipOps.todword(updata)) or "" + elseif( dtype == types.CNAME ) then + updata = encodeFQDN(updata) + elseif( dtype == types.MX ) then + assert( not( type(updata) ~= "table" ), "dns.update expected options.data to be a table") + if ( not(options.zone) ) then zone = dname end + local data = bin.pack(">S", updata.pref) + data = data .. encodeFQDN(updata.mx) + updata = data + elseif ( dtype == types.SRV ) then + assert( not( type(updata) ~= "table" ), "dns.update expected options.data to be a table") + local data = bin.pack(">SSS", updata.prio, updata.weight, updata.port ) + data = data .. encodeFQDN(updata.target) + updata = data + zone = options.zone or dname:match("^_.-%._.-%.(.+)$") + else + return false, "Unsupported record type" + end + end + + pkt = addZone(pkt, zone) + pkt = addUpdate(pkt, dname, dtype, ttl, updata, class) + + local data = encode(pkt) + local status, response = sendPackets(data, host, port, timeout, sendcount, false) + + if ( status ) then + local decoded = decode(response[1].data) + local flags=encodeFlags(decoded.flags) + if (flags:sub(-4) == "0000") then + return true + end + end + return false +end + + diff --git a/scripts/dns-update.nse b/scripts/dns-update.nse new file mode 100644 index 000000000..9a775f70e --- /dev/null +++ b/scripts/dns-update.nse @@ -0,0 +1,100 @@ +description = [[ +Attempts to perform a dynamic DNS update without authentication +]] + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +--- +-- @output +-- PORT STATE SERVICE +-- 53/udp open domain +-- | dns-update: +-- | Successfully added the record "nmap-test.cqure.net" +-- |_ Successfully deleted the record "nmap-test.cqure.net" +-- + +-- +-- Examples +-- +-- Adding different types of records to a server +-- * dns.update( "www.cqure.net", { host=host, port=port, dtype="A", data="10.10.10.10" } ) +-- * dns.update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="www.cqure.net" } ) +-- * dns.update( "cqure.net", { host=host, port=port, dtype="MX", data={ pref=10, mx="mail.cqure.net"} }) +-- * dns.update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data={ prio=0, weight=100, port=389, target="ldap.cqure.net" } } ) +-- +-- Removing the above records by setting an empty data and a ttl of zero +-- * dns.update( "www.cqure.net", { host=host, port=port, dtype="A", data="", ttl=0 } ) +-- * dns.update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="", ttl=0 } ) +-- * dns.update( "cqure.net", { host=host, port=port, dtype="MX", data="", ttl=0 } ) +-- * dns.update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data="", ttl=0 } ) +-- +-- @args dns-update.hostname the name of the host to add to the zone +-- @args dns-update.ip the ip address of the host to add to the zone +-- + +-- Version 0.2 + +-- Created 01/09/2011 - v0.1 - created by Patrik Karlsson +-- Revised 01/10/2011 - v0.2 - added test function + +require 'shortport' +require 'dns' + +portrule = shortport.port_or_service( 53, "dns", "udp", {"open", "open|filtered"} ) + +local function test(host, port) + + local status, err = dns.update( "www.cqure.net", { host=host, port=port, dtype="A", data="10.10.10.10" } ) + if ( status ) then stdnse.print_debug("SUCCESS") else stdnse.print_debug("FAIL: " .. (err or "")) end + status, err = dns.update( "www2", { zone="cqure.net", host=host, port=port, dtype="A", data="10.10.10.10" } ) + if ( status ) then stdnse.print_debug("SUCCESS") else stdnse.print_debug("FAIL: " .. (err or "")) end + status, err = dns.update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="www.cqure.net" } ) + if ( status ) then stdnse.print_debug("SUCCESS") else stdnse.print_debug("FAIL: " .. (err or "")) end + status, err = dns.update( "cqure.net", { host=host, port=port, dtype="MX", data={ pref=10, mx="mail.cqure.net"} }) + if ( status ) then stdnse.print_debug("SUCCESS") else stdnse.print_debug("FAIL: " .. (err or "")) end + status, err = dns.update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data={ prio=0, weight=100, port=389, target="ldap.cqure.net" } } ) + if ( status ) then stdnse.print_debug("SUCCESS") else stdnse.print_debug("FAIL: " .. (err or "")) end + + status, err = dns.update( "www.cqure.net", { host=host, port=port, dtype="A", data="", ttl=0 } ) + if ( status ) then stdnse.print_debug("SUCCESS") else stdnse.print_debug("FAIL: " .. (err or "")) end + status, err = dns.update( "www2.cqure.net", { host=host, port=port, dtype="A", data="", ttl=0 } ) + if ( status ) then stdnse.print_debug("SUCCESS") else stdnse.print_debug("FAIL: " .. (err or "")) end + status, err = dns.update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="", ttl=0 } ) + if ( status ) then stdnse.print_debug("SUCCESS") else stdnse.print_debug("FAIL: " .. (err or "")) end + status, err = dns.update( "cqure.net", { host=host, port=port, dtype="MX", data="", ttl=0 } ) + if ( status ) then stdnse.print_debug("SUCCESS") else stdnse.print_debug("FAIL: " .. (err or "")) end + status, err = dns.update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data="", ttl=0 } ) + if ( status ) then stdnse.print_debug("SUCCESS") else stdnse.print_debug("FAIL: " .. (err or "")) end + +end + +action = function(host, port) + + local t = stdnse.get_script_args('dns-update.test') + local name, ip = stdnse.get_script_args('dns-update.hostname', 'dns-update.ip') + + if ( t ) then return test(host, port) end + if ( not(name) or not(ip) ) then return end + + -- we really need an ip or name to continue + -- we could attempt a random name, but we need to know at least the name of the zone + local status, err = dns.update( name, { host=host, port=port, dtype="A", data=ip } ) + + if ( status ) then + local result = {} + table.insert(result, ("Successfully added the record \"%s\""):format(name)) + local status = dns.update( name, { host=host, port=port, dtype="A", data="", ttl=0 } ) + if ( status ) then + table.insert(result, ("Successfully deleted the record \"%s\""):format(name)) + else + table.insert(result, ("Failed to delete the record \"%s\""):format(name)) + end + nmap.set_port_state(host, port, "open") + return stdnse.format_output(true, result) + elseif ( err ) then + return "\n ERROR: " .. err + end + +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 6e21e48eb..7c5f6731c 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -29,6 +29,7 @@ Entry { filename = "dns-random-srcport.nse", categories = { "external", "intrusi Entry { filename = "dns-random-txid.nse", categories = { "external", "intrusive", } } Entry { filename = "dns-recursion.nse", categories = { "default", "intrusive", } } Entry { filename = "dns-service-discovery.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "dns-update.nse", categories = { "discovery", "safe", } } Entry { filename = "dns-zone-transfer.nse", categories = { "default", "discovery", "intrusive", } } Entry { filename = "domcon-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "domcon-cmd.nse", categories = { "auth", "intrusive", } }