id = "DNS source port randomness"

description = "Queries porttest.dns-oarc.net to check for the predictable-port DNS recursion vulnerability.  Predictable source ports can make a DNS server vulnerable to cache poisoning attacks (CVE-2008-1447)"

license = "Script: Same as Nmap--See http://nmap.org/book/man-legal.html\n" ..
	  "porttest.dns-oarc.net: https://www.dns-oarc.net/oarc/services/porttest"

author = "Script: Brandon Enright <bmenrigh@ucsd.edu>\n" ..
	 "porttest.dns-oarc.net: Duane Wessels <wessels@dns-oarc.net>"

-- This script uses (with permission) Duane Wessels' porttest.dns-oarc.net
-- service.  Duane/OARC believe the service is valuable to the community
-- and have no plans to ever turn the service off.
-- The likely long-term availability makes this script a good candidate
-- for inclusion in Nmap proper.

categories = {"intrusive"}

require "bit"
require "comm"
require "shortport"

portrule = shortport.portnumber(53, "udp")

action = function(host, port)

        -- TXID: 0xbeef
        -- Flags: 0x0100
        -- Questions: 1
        -- Answer RRs: 0
        -- Authority RRs: 0
        -- Additional RRs: 0

        -- Query:
                -- Name: porttest, dns-oarc, net
                -- Type: TXT (0x0010)
                -- Class: IN (0x0001)

        local query =   string.char(    0xbe, 0xef, -- TXID
                                        0x01, 0x00, -- Flags
                                        0x00, 0x01, -- Questions
                                        0x00, 0x00, -- Answer RRs
                                        0x00, 0x00, -- Authority RRs
                                        0x00, 0x00, -- Additional RRs
                                        0x08) .. "porttest" ..
                        string.char(    0x08) .. "dns-oarc" ..
                        string.char(    0x03) .. "net" ..
                        string.char(    0x00, -- Name terminator
                                        0x00, 0x10, -- Type (TXT)
                                        0x00, 0x01) -- Class (IN)

        -- This doesn't work without the bytes= setting...
        local status, result = comm.exchange(host, port, query, {proto="udp",
                                                                bytes=1,
                                                                timeout=20000})

        -- Fail gracefully
        if not status then
           if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then
                return "ERROR: TIMEOUT"
           else
                return
           end
        end

        -- Update the port
        nmap.set_port_state(host, port, "open")

        -- Now we need to "parse" the results to check to see if they are good

        -- We need a minimum of 5 bytes...
        if (string.len(result) < 5) then
           if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then
                return "ERROR: Malformed response"
           else
                return
           end
        end

        -- Check TXID
        if (string.byte(result, 1) ~= 0xbe
                or string.byte(result, 2) ~= 0xef) then
           if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then
                return "ERROR: Invalid Transaction ID"
           else
                return
           end
        end

        -- Check response flag and recursion
        if not (bit.band(string.byte(result, 3), 0x80) == 0x80
                   and bit.band(string.byte(result, 4), 0x80) == 0x80) then
           if (nmap.verbosity() >= 1 or nmap.debugging() >= 1) then
                return "ERROR: Server refused recursion"
           else
                return
           end
        end

        -- Check error flag
        if (bit.band(string.byte(result, 4), 0x0F) ~= 0x00) then
           if (nmap.verbosity() >= 1 or nmap.debugging() >= 1) then
                return "ERROR: Server failure"
           else
                return
           end
        end
        
        -- Check for two Answer RRs and 1 Authority RR
        if (string.byte(result, 5) ~= 0x00
                or string.byte(result, 6) ~= 0x01
                or string.byte(result, 7) ~= 0x00
                or string.byte(result, 8) ~= 0x02) then
           if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then
                return "ERROR: Response did not include expected answers"
           else
                return
           end
        end

        -- We need a minimum of 128 bytes...
        if (string.len(result) < 128) then
           if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then
                return "ERROR: Truncated response"
           else
                return
           end
        end

        -- Here is the really fragile part.  If the DNS response changes
        -- in any way, this won't work and will fail.
        -- Jump to second answer and check to see that it is TXT, IN
        -- then grab the length and display that text...
        
        -- Check for TXT
        if (string.byte(result, 118) ~= 0x00
                or string.byte(result, 119) ~= 0x10)
        then
           if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then
                return "ERROR: Answer record not of type TXT"
           else
                return
           end
        end

        -- Check for IN
        if (string.byte(result, 120) ~= 0x00
                or string.byte(result, 121) ~= 0x01) then
           if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then
                return "ERROR: Answer record not of type IN"
           else
                return
           end
        end

        -- Get TXT length
        local txtlen = string.byte(result, 128)

        -- We now need a minimum of 128 + txtlen bytes + 1...
        if (string.len(result) < 128 + txtlen) then
           if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then
                return "ERROR: Truncated response"
           else
                return
           end
        end

        -- GET TXT record
        local txtrd = string.sub(result, 129, 128 + txtlen)

        return txtrd
end
