description = [[
This script launches a DNS fuzzing attack against any DNS server.
Originally designed to test bind10, this script induces several errors
into otherwise valid - randomly generated - DNS packets. The packet
template that we use includes one standard name and one compressed name.
This script should be run for a long time(TM). 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 --script dns-fuzz [--script-args timelimit=2h] target
-- @args 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 http://nmap.org/book/man-legal.html"
categories = {"fuzzer", "intrusive"}
require "bit"
require "dns"
require "stdnse"
require "comm"
require "shortport"
portrule = shortport.portnumber(53, "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
-- 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, {proto="udp", timeout=math.pow(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, respons = dns.query(recursiveServer, {host=host.ip, port=port.number, tries=1, timeout=math.pow(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 = name .. string.char(math.random(string.byte("a"),string.byte("z")))
end
return 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 .. makeWord ()
end
if compressed then
name = name .. string.char(0xC0) .. string.char(0x0C)
else
name = name .. string.char(0x00)
end
return 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 newPacket = ""
-- Iterate over every byte in the packet
dnsPacket:gsub(".", function(c)
-- Induce bit errors at a rate of 1/50.
if math.random(50) == 25 then
-- Bitflip algorithm: c ^ 1<<(rand()%7)
newPacket = newPacket .. string.char( bit.bxor(c:byte(), bit.lshift(1, math.random(0,7))) )
else
newPacket = newPacket .. c
end
end)
return newPacket
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 newPacket = ""
local byteToDrop = math.random(dnsPacket:len())-1
local i = 0
-- Iterate over every byte in the packet
dnsPacket:gsub(".", function(c)
i=i+1
if not i==byteToDrop then
newPacket = newPacket .. c
end
end)
return newPacket
end
---
-- Instead of dropping an entire byte, in 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 newPacket = ""
local byteToInject = math.random(dnsPacket:len())-1
local i = 0
-- Iterate over every byte in the packet
dnsPacket:gsub(".", function(c)
i=i+1
if i==byteToInject then
newPacket = newPacket .. string.char(math.random(0,255))
end
newPacket = newPacket .. c
end)
return newPacket
end
---
-- Instead of dropping an entire byte, in insert a random byte
-- @param dnsPacket A packet, generated by makePacket()
-- @return The same packet, but with a single byte missing
function truncatePacket (dnsPacket)
local newPacket = ""
-- at least 12 bytes to make sure the packet isn't dropped as a tinygram
local eatPacketPos = math.random(12,dnsPacket:len())-1
local i = 0
-- Iterate over every byte in the packet
dnsPacket:gsub(".", function(c)
i=i+1
if i==eatPacketPos then
return
end
newPacket = newPacket .. c
end)
return newPacket
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, {proto="udp", 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)
math.randomseed(os.time())
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()