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", } }