From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- scripts/dns-fuzz.nse | 306 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 scripts/dns-fuzz.nse (limited to 'scripts/dns-fuzz.nse') diff --git a/scripts/dns-fuzz.nse b/scripts/dns-fuzz.nse new file mode 100644 index 0000000..dade20a --- /dev/null +++ b/scripts/dns-fuzz.nse @@ -0,0 +1,306 @@ +local comm = require "comm" +local dns = require "dns" +local math = require "math" +local nmap = require "nmap" +local shortport = require "shortport" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" + +description = [[ +Launches a DNS fuzzing attack against DNS servers. + +The script induces errors into randomly generated but valid DNS packets. +The packet template that we use includes one uncompressed and one +compressed name. + +Use the dns-fuzz.timelimit argument to control how long the +fuzzing lasts. This script should be run for a long time. It will send a +very large quantity of packets and thus it's pretty invasive, so it +should only be used against private DNS servers as part of a software +development lifecycle. +]] + +--- +-- @usage +-- nmap -sU --script dns-fuzz --script-args timelimit=2h +-- +-- @args dns-fuzz.timelimit How long to run the fuzz attack. This is a +-- number followed by a suffix: s for seconds, +-- m for minutes, and h for hours. Use +-- 0 for an unlimited amount of time. Default: +-- 10m. +-- +-- @output +-- Host script results: +-- |_dns-fuzz: Server stopped responding... He's dead, Jim. + +author = "Michael Pattrick" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"fuzzer", "intrusive"} + + +portrule = shortport.portnumber(53, {"tcp", "udp"}) + +-- How many ms should we wait for the server to respond. +-- Might want to make this an argument, but 500 should always be more then enough. +DNStimeout = 500 + +-- Will the DNS server only respond to recursive questions +recursiveOnly = false + +-- We only perform a DNS lookup of this site +recursiveServer = "scanme.nmap.org" + +--- +-- Checks if the server is alive/DNS +-- @param host The host which the server should be running on +-- @param port The servers port +-- @return Bool, true if and only if the server is alive +function pingServer (host, port, attempts) + local status, response, result + -- If the server doesn't respond to the first in a multiattempt probe, slow down + local slowDown = 1 + if not recursiveOnly then + -- try to get a server status message + -- The method that nmap uses by default + local data + local pkt = dns.newPacket() + pkt.id = math.random(65535) + + pkt.flags.OC3 = true + + data = dns.encode(pkt) + + for i = 1, attempts do + status, result = comm.exchange(host, port, data, {timeout=DNStimeout^slowDown}) + if status then + return true + end + slowDown = slowDown + 0.25 + end + + return false + else + -- just do a vanilla recursive lookup of scanme.nmap.org + for i = 1, attempts do + status, response = dns.query(recursiveServer, {host=host.ip, port=port.number, proto=port.protocol, tries=1, timeout=DNStimeout^slowDown}) + if status then + return true + end + slowDown = slowDown + 0.25 + end + return false + end +end + +--- +-- Generate a random 'label', a string of ascii characters do be used in +-- the requested domain names +-- @return Random string of lowercase characters +function makeWord () + local len = math.random(3,7) + local name = {string.char(len)} + for i = 1, len do + -- this next line assumes ascii + name[i+1] = string.char(math.random(string.byte("a"),string.byte("z"))) + end + return table.concat(name) +end + +--- +-- Turns random labels from makeWord into a valid domain name. +-- Includes the option to compress any given name by including a pointer +-- to the first record. Obviously the first record should not be compressed. +-- @param compressed Bool, whether or not this record should have a compressed field +-- @return A dns host string +function makeHost (compressed) + -- randomly choose between 2 to 4 levels in this domain + local levels = math.random(2,4) + local name = {} + for i = 1, levels do + name[#name+1] = makeWord () + end + if compressed then + name[#name+1] = "\xc0\x0c" + else + name[#name+1] = "\x00" + end + + return table.concat(name) +end + +--- +-- Concatenate all the bytes of a valid dns packet, including names generated by +-- makeHost(). This packet is to be corrupted. +-- @return Always returns a valid packet +function makePacket() + local recurs = 0x00 + if recursiveOnly then + recurs = 0x01 + end + return + string.char( math.random(0,255), math.random(0,255), -- TXID + recurs, 0x00, -- Flags, recursion disabled by default for obvious reasons + 0x00, 0x02, -- Questions + 0x00, 0x00, -- Answer RRs + 0x00, 0x00, -- Authority RRs + 0x00, 0x00) -- Additional RRs + -- normal host + .. makeHost (false) .. -- Hostname + string.char( 0x00, 0x01, -- Type (A) + 0x00, 0x01) -- Class (IN) + -- compressed host + .. makeHost (true) .. -- Hostname + string.char( 0x00, 0x05, -- Type (CNAME) + 0x00, 0x01) -- Class (IN) +end + +--- +-- Introduce bit errors into a packet at a rate of 1/50 +-- As Charlie Miller points out in "Fuzz by Number" +-- -> cansecwest.com/csw08/csw08-miller.pdf +-- It's difficult to tell how much random you should insert into packets +-- "If data is too valid, might not cause problems, If data is too invalid, +-- might be quickly rejected" +-- so 1/50 is arbitrary +-- @param dnsPacket A packet, generated by makePacket() +-- @return The same packet, but with bit flip errors +function nudgePacket (dnsPacket) + local chunks = {} + local pos = 1 + for i = 1, #dnsPacket do + -- Induce bit errors at a rate of 1/50. + if math.random(50) == 25 then + table.insert(chunks, dnsPacket:sub(pos, i - 1)) + table.insert(chunks, string.char(dnsPacket:byte(i) ~ (1 << math.random(0, 7)))) + pos = i + 1 + end + end + table.insert(chunks, dnsPacket:sub(pos)) + return table.concat(chunks) +end + +--- +-- Instead of flipping a bit, we drop an entire byte +-- @param dnsPacket A packet, generated by makePacket() +-- @return The same packet, but with a single byte missing +function dropByte (dnsPacket) + local pos = math.random(#dnsPacket) + return dnsPacket:sub(1, pos - 1) .. dnsPacket:sub(pos + 1) +end + +--- +-- Instead of dropping an entire byte, insert a random byte +-- @param dnsPacket A packet, generated by makePacket() +-- @return The same packet, but with a single byte missing +function injectByte (dnsPacket) + local pos = math.random(#dnsPacket + 1) + return dnsPacket:sub(1, pos - 1) .. string.char(math.random(0,255)) .. dnsPacket:sub(pos) +end + +--- +-- Instead of inserting a byte, truncate the packet at random position +-- @param dnsPacket A packet, generated by makePacket() +-- @return The same packet, but truncated +function truncatePacket (dnsPacket) + -- at least 12 bytes to make sure the packet isn't dropped as a tinygram + local pos = math.random(12, #dnsPacket - 1) + return dnsPacket:sub(1, pos) +end + +--- +-- As the name of this function suggests, we corrupt the packet, and then send it. +-- We choose at random one of three corruption functions, and then corrupt/send +-- the packet a maximum of 10 times +-- @param host The servers IP +-- @param port The servers port +-- @param query An uncorrupted DNS packet +-- @return A string if the server died, else nil +function corruptAndSend (host, port, query) + local randCorr = math.random(0,4) + local status + local result + -- 10 is arbitrary, but seemed like a good number + for j = 1, 10 do + if randCorr<=1 then + -- slight bias to nudging because it seems to work better + query = nudgePacket(query) + elseif randCorr==2 then + query = dropByte(query) + elseif randCorr==3 then + query = injectByte(query) + elseif randCorr==4 then + query = truncatePacket(query) + end + + status, result = comm.exchange(host, port, query, {timeout=DNStimeout}) + if not status then + if not pingServer(host,port,3) then + -- no response after three tries, the server is probably dead + return "Server stopped responding... He's dead, Jim.\n".. + "Offending packet: 0x".. stdnse.tohex(query) + else + -- We corrupted the packet too much, the server will just drop it + -- No point in using it again + return nil + end + end + if randCorr==4 then + -- no point in using this function more then once + return nil + end + end + return nil +end + +action = function(host, port) + local endT + local timelimit, err + local retStr + local query + + for _, k in ipairs({"dns-fuzz.timelimit", "timelimit"}) do + if nmap.registry.args[k] then + timelimit, err = stdnse.parse_timespec(nmap.registry.args[k]) + if not timelimit then + error(err) + end + break + end + end + if timelimit and timelimit > 0 then + -- seconds to milliseconds plus the current time + endT = timelimit*1000 + nmap.clock_ms() + elseif not timelimit then + -- 10 minutes + endT = 10*60*1000 + nmap.clock_ms() + end + + + -- Check if the server is a DNS server. + if not pingServer(host,port,1) then + -- David reported that his DNS server doesn't respond to + recursiveOnly = true + if not pingServer(host,port,1) then + return "Server didn't response to our probe, can't fuzz" + end + end + nmap.set_port_state (host, port, "open") + + -- If the user specified that we should run for n seconds, then don't run for too much longer + -- If 0 seconds, then run forever + while not endT or nmap.clock_ms()