307 lines
9.9 KiB
Lua
307 lines
9.9 KiB
Lua
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 <code>dns-fuzz.timelimit</code> 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 <target>
|
|
--
|
|
-- @args dns-fuzz.timelimit How long to run the fuzz attack. This is a
|
|
-- number followed by a suffix: <code>s</code> for seconds,
|
|
-- <code>m</code> for minutes, and <code>h</code> for hours. Use
|
|
-- <code>0</code> for an unlimited amount of time. Default:
|
|
-- <code>10m</code>.
|
|
--
|
|
-- @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()<endT do
|
|
-- Forge an initial packet
|
|
-- We start off with an only slightly corrupted packet, then add more and more corruption
|
|
-- if we corrupt the packet too much then the server will just drop it, so we only recorrupt several times
|
|
-- then start all over
|
|
query = makePacket ()
|
|
-- induce random jitter
|
|
retStr = corruptAndSend (host, port, query)
|
|
if retStr then
|
|
return retStr
|
|
end
|
|
end
|
|
return "The server seems impervious to our assault."
|
|
end
|