diff options
Diffstat (limited to '')
-rw-r--r-- | scripts/dns-check-zone.nse | 450 |
1 files changed, 450 insertions, 0 deletions
diff --git a/scripts/dns-check-zone.nse b/scripts/dns-check-zone.nse new file mode 100644 index 0000000..043caca --- /dev/null +++ b/scripts/dns-check-zone.nse @@ -0,0 +1,450 @@ +local dns = require "dns" +local stdnse = require "stdnse" +local table = require "table" +local ipOps = require "ipOps" + +description = [[ +Checks DNS zone configuration against best practices, including RFC 1912. +The configuration checks are divided into categories which each have a number +of different tests. +]] + +--- +-- @usage +-- nmap -sn -Pn ns1.example.com --script dns-check-zone --script-args='dns-check-zone.domain=example.com' +-- +-- @output +-- | dns-check-zone: +-- | DNS check results for domain: example.com +-- | SOA +-- | PASS - SOA REFRESH +-- | SOA REFRESH was within recommended range (7200s) +-- | PASS - SOA RETRY +-- | SOA RETRY was within recommended range (3600s) +-- | PASS - SOA EXPIRE +-- | SOA EXPIRE was within recommended range (1209600s) +-- | FAIL - SOA MNAME entry check +-- | SOA MNAME record is NOT listed as DNS server +-- | PASS - Zone serial numbers +-- | Zone serials match +-- | MX +-- | ERROR - Reverse MX A records +-- | Failed to retrieve list of mail servers +-- | NS +-- | PASS - Recursive queries +-- | None of the servers allow recursive queries. +-- | PASS - Multiple name servers +-- | Server has 2 name servers +-- | PASS - DNS name server IPs are public +-- | All DNS IPs were public +-- | PASS - DNS server response +-- | All servers respond to DNS queries +-- | PASS - Missing nameservers reported by parent +-- | All DNS servers match +-- | PASS - Missing nameservers reported by your nameservers +-- |_ All DNS servers match +-- +-- @args dns-check-zone.domain the dns zone to check +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"discovery", "safe", "external"} + +local arg_domain = stdnse.get_script_args(SCRIPT_NAME .. '.domain') + + +hostrule = function(host) return ( arg_domain ~= nil ) end + +local PROBE_HOST = "scanme.nmap.org" + +local Status = { + PASS = "PASS", + FAIL = "FAIL", +} + +local function isValidSOA(res) + if ( not(res) or type(res.answers) ~= "table" or type(res.answers[1].SOA) ~= "table" ) then + return false + end + return true +end + +local dns_checks = { + + ["NS"] = { + { + desc = "Recursive queries", + func = function(domain, server) + local status, res = dns.query(domain, { host = server, dtype='NS', retAll = true }) + local result = {} + + if ( not(status) ) then + return false, "Failed to retrieve list of DNS servers" + end + for _, srv in ipairs(res or {}) do + local status, res = dns.query(PROBE_HOST, { host = srv, dtype='A' }) + if ( status ) then + table.insert(result, res) + end + end + + local output = "None of the servers allow recursive queries." + if ( 0 < #result ) then + output = ("The following servers allow recursive queries: %s"):format(table.concat(result, ", ")) + return true, { status = Status.FAIL, output = output } + end + return true, { status = Status.PASS, output = output } + end + }, + + { + desc = "Multiple name servers", + func = function(domain, server) + local status, res = dns.query(domain, { host = server, dtype='NS', retAll = true }) + + if ( not(status) ) then + return false, "Failed to retrieve list of DNS servers" + end + + local status = Status.FAIL + if ( 1 < #res ) then + status = Status.PASS + end + return true, { status = status, output = ("Server has %d name servers"):format(#res) } + end + }, + + { + desc = "DNS name server IPs are public", + func = function(domain, server) + + local status, res = dns.query(domain, { host = server, dtype='NS', retAll = true }) + if ( not(status) ) then + return false, "Failed to retrieve list of DNS servers" + end + + local result = {} + for _, srv in ipairs(res or {}) do + local status, res = dns.query(srv, { dtype='A', retAll = true }) + if ( not(status) ) then + return false, ("Failed to retrieve IP for DNS: %s"):format(srv) + end + for _, ip in ipairs(res) do + if ( ipOps.isPrivate(ip) ) then + table.insert(result, ip) + end + end + end + + local output = "All DNS IPs were public" + if ( 0 < #result ) then + output = ("The following private IPs were detected: %s"):format(table.concat(result, ", ")) + status = Status.FAIL + else + status = Status.PASS + end + + return true, { status = status, output = output } + end + }, + + { + desc = "DNS server response", + func = function(domain, server) + local status, res = dns.query(domain, { host = server, dtype='NS', retAll = true }) + if ( not(status) ) then + return false, "Failed to retrieve list of DNS servers" + end + + local result = {} + for _, srv in ipairs(res or {}) do + local status, res = dns.query(domain, { host = srv, dtype='SOA', retPkt = true }) + if ( not(status) ) then + table.insert(result, res) + end + end + + local output = "All servers respond to DNS queries" + if ( 0 < #result ) then + output = ("The following servers did not respond to DNS queries: %s"):format(table.concat(result, ", ")) + return true, { status = Status.FAIL, output = output } + end + return true, { status = Status.PASS, output = output } + end + }, + + { + desc = "Missing nameservers reported by parent", + func = function(domain, server) + local tld = domain:match("%.(.*)$") + local status, res = dns.query(tld, { dtype = "NS", retAll = true }) + if ( not(status) ) then + return false, "Failed to retrieve list of TLD DNS servers" + end + + local status, parent_res = dns.query(domain, { host = res, dtype = "NS", retAll = true, retPkt = true, noauth = true } ) + if ( not(status) ) then + return false, "Failed to retrieve a list of parent DNS servers" + end + + if ( not(status) or not(parent_res) or type(parent_res.auth) ~= "table" ) then + return false, "Failed to retrieve a list of parent DNS servers" + end + + local parent_dns = {} + for _, auth in ipairs(parent_res.auth) do + parent_dns[auth.domain] = true + end + + status, res = dns.query(domain, { host = server, dtype = "NS", retAll = true } ) + if ( not(status) ) then + return false, "Failed to retrieve a list of DNS servers" + end + + local domain_dns = {} + for _,srv in ipairs(res) do domain_dns[srv] = true end + + local result = {} + for srv in pairs(domain_dns) do + if ( not(parent_dns[srv]) ) then + table.insert(result, srv) + end + end + + if ( 0 < #result ) then + local output = ("The following servers were found in the zone, but not in the parent: %s"):format(table.concat(result, ", ")) + return true, { status = Status.FAIL, output = output } + end + + return true, { status = Status.PASS, output = "All DNS servers match" } + end, + }, + + + { + desc = "Missing nameservers reported by your nameservers", + func = function(domain, server) + local tld = domain:match("%.(.*)$") + local status, res = dns.query(tld, { dtype = "NS", retAll = true }) + if ( not(status) ) then + return false, "Failed to retrieve list of TLD DNS servers" + end + + local status, parent_res = dns.query(domain, { host = res, dtype = "NS", retAll = true, retPkt = true, noauth = true } ) + if ( not(status) ) then + return false, "Failed to retrieve a list of parent DNS servers" + end + + if ( not(status) or not(parent_res) or type(parent_res.auth) ~= "table" ) then + return false, "Failed to retrieve a list of parent DNS servers" + end + + local parent_dns = {} + for _, auth in ipairs(parent_res.auth) do + parent_dns[auth.domain] = true + end + + status, res = dns.query(domain, { host = server, dtype = "NS", retAll = true } ) + if ( not(status) ) then + return false, "Failed to retrieve a list of DNS servers" + end + + local domain_dns = {} + for _,srv in ipairs(res) do domain_dns[srv] = true end + + local result = {} + for srv in pairs(parent_dns) do + if ( not(domain_dns[srv]) ) then + table.insert(result, srv) + end + end + + if ( 0 < #result ) then + local output = ("The following servers were found in the parent, but not in the zone: %s"):format(table.concat(result, ", ")) + return true, { status = Status.FAIL, output = output } + end + + return true, { status = Status.PASS, output = "All DNS servers match" } + end, + }, + + }, + + ["SOA"] = + { + { + desc = "SOA REFRESH", + func = function(domain, server) + local status, res = dns.query(domain, { host = server, dtype='SOA', retPkt=true }) + if ( not(status) or not(isValidSOA(res)) ) then + return false, "Failed to retrieve SOA record" + end + + local refresh = tonumber(res.answers[1].SOA.refresh) + if ( not(refresh) ) then + return false, "Failed to retrieve SOA REFRESH" + end + + if ( refresh < 1200 or refresh > 43200 ) then + return true, { status = Status.FAIL, output = ("SOA REFRESH was NOT within recommended range (%ss)"):format(refresh) } + else + return true, { status = Status.PASS, output = ("SOA REFRESH was within recommended range (%ss)"):format(refresh) } + end + end + }, + + { + desc = "SOA RETRY", + func = function(domain, server) + local status, res = dns.query(domain, { host = server, dtype='SOA', retPkt=true }) + if ( not(status) or not(isValidSOA(res)) ) then + return false, "Failed to retrieve SOA record" + end + + local retry = tonumber(res.answers[1].SOA.retry) + if ( not(retry) ) then + return false, "Failed to retrieve SOA RETRY" + end + + if ( retry < 180 ) then + return true, { status = Status.FAIL, output = ("SOA RETRY was NOT within recommended range (%ss)"):format(retry) } + else + return true, { status = Status.PASS, output = ("SOA RETRY was within recommended range (%ss)"):format(retry) } + end + end + }, + + { + desc = "SOA EXPIRE", + func = function(domain, server) + local status, res = dns.query(domain, { host = server, dtype='SOA', retPkt=true }) + if ( not(status) or not(isValidSOA(res)) ) then + return false, "Failed to retrieve SOA record" + end + + local expire = tonumber(res.answers[1].SOA.expire) + if ( not(expire) ) then + return false, "Failed to retrieve SOA EXPIRE" + end + + if ( expire < 1209600 or expire > 2419200 ) then + return true, { status = Status.FAIL, output = ("SOA EXPIRE was NOT within recommended range (%ss)"):format(expire) } + else + return true, { status = Status.PASS, output = ("SOA EXPIRE was within recommended range (%ss)"):format(expire) } + end + end + }, + + { + desc = "SOA MNAME entry check", + func = function(domain, server) + local status, res = dns.query(domain, { host = server, dtype='SOA', retPkt=true }) + if ( not(status) or not(isValidSOA(res)) ) then + return false, "Failed to retrieve SOA record" + end + local mname = res.answers[1].SOA.mname + + status, res = dns.query(domain, { host = server, dtype='NS', retAll = true }) + if ( not(status) ) then + return false, "Failed to retrieve list of DNS servers" + end + + for _, srv in ipairs(res or {}) do + if ( srv == mname ) then + return true, { status = Status.PASS, output = "SOA MNAME record is listed as DNS server" } + end + end + return true, { status = Status.FAIL, output = "SOA MNAME record is NOT listed as DNS server" } + end + }, + + { + desc = "Zone serial numbers", + func = function(domain, server) + local status, res = dns.query(domain, { host = server, dtype='NS', retAll = true }) + if ( not(status) ) then + return false, "Failed to retrieve list of DNS servers" + end + + local result = {} + local serial + + for _, srv in ipairs(res or {}) do + local status, res = dns.query(domain, { host = srv, dtype='SOA', retPkt = true }) + if ( not(status) or not(isValidSOA(res)) ) then + return false, "Failed to retrieve SOA record" + end + + local s = res.answers[1].SOA.serial + if ( not(serial) ) then + serial = s + elseif( serial ~= s ) then + return true, { status = Status.FAIL, output = "Different zone serials were detected" } + end + end + + return true, { status = Status.PASS, output = "Zone serials match" } + end, + }, + }, + + ["MX"] = { + + { + desc = "Reverse MX A records", + func = function(domain, server) + local status, res = dns.query(domain, { host = server, dtype='MX', retAll = true }) + if ( not(status) ) then + return false, "Failed to retrieve list of mail servers" + end + + local result = {} + for _, record in ipairs(res or {}) do + local prio, mx = record:match("^(%d*):([^:]*)") + local ips + status, ips = dns.query(mx, { dtype='A', retAll=true }) + if ( not(status) ) then + return false, "Failed to retrieve A records for MX" + end + + for _, ip in ipairs(ips) do + local status, res = dns.query(dns.reverse(ip), { dtype='PTR' }) + if ( not(status) ) then + table.insert(result, ip) + end + end + end + + local output = "All MX records have PTR records" + if ( 0 < #result ) then + output = ("The following IPs do not have PTR records: %s"):format(table.concat(result, ", ")) + return true, { status = Status.FAIL, output = output } + end + return true, { status = Status.PASS, output = output } + end + }, + + } +} + +action = function(host, port) + local server = host.ip + local output = { name = ("DNS check results for domain: %s"):format(arg_domain) } + + for group in pairs(dns_checks) do + local group_output = { name = group } + for _, check in ipairs(dns_checks[group]) do + local status, res = check.func(arg_domain, server) + if ( status ) then + local test_res = ("%s - %s"):format(res.status, check.desc) + table.insert(group_output, { name = test_res, res.output }) + else + local test_res = ("ERROR - %s"):format(check.desc) + table.insert(group_output, { name = test_res, res }) + end + end + table.insert(output, group_output) + end + return stdnse.format_output(true, output) +end |