summaryrefslogtreecommitdiffstats
path: root/scripts/http-grep.nse
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--scripts/http-grep.nse326
1 files changed, 326 insertions, 0 deletions
diff --git a/scripts/http-grep.nse b/scripts/http-grep.nse
new file mode 100644
index 0000000..75785df
--- /dev/null
+++ b/scripts/http-grep.nse
@@ -0,0 +1,326 @@
+local string = require "string"
+local httpspider = require "httpspider"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local tableaux = require "tableaux"
+
+
+description = [[
+Spiders a website and attempts to match all pages and urls against a given
+string. Matches are counted and grouped per url under which they were
+discovered.
+
+Features built in patterns like email, ip, ssn, discover, amex and more.
+The script searches for email and ip by default.
+
+]]
+
+---
+-- @usage
+-- nmap -p 80 www.example.com --script http-grep --script-args='match="[A-Za-z0-9%.%%%+%-]+@[A-Za-z0-9%.%%%+%-]+%.%w%w%w?%w?",breakonmatch'
+-- nmap -p 80 www.example.com --script http-grep --script-args 'http-grep.builtins ={"mastercard", "discover"}, http-grep.url="example.html"'
+-- @output
+-- | http-grep:
+-- | (1) https://nmap.org/book/man-bugs.html:
+-- | (1) email:
+-- | + dev@nmap.org
+-- | (1) https://nmap.org/book/install.html:
+-- | (1) email:
+-- | + fyodor@nmap.org
+-- | (16) https://nmap.org/changelog.html:
+-- | (7) ip:
+-- | + 255.255.255.255
+-- | + 10.99.24.140
+-- | + 74.125.53.103
+-- | + 64.147.188.3
+-- | + 203.65.42.255
+-- | + 192.31.33.7
+-- | + 168.0.40.135
+-- | (9) email:
+-- | + d1n@inbox.com
+-- | + fyodor@insecure.org
+-- | + uce@ftc.gov
+-- | + rhundt@fcc.gov
+-- | + jquello@fcc.gov
+-- | + sness@fcc.gov
+-- | + president@whitehouse.gov
+-- | + haesslich@loyalty.org
+-- | + rchong@fcc.gov
+-- | (6) https://nmap.org/5/#5changes:
+-- | (6) ip:
+-- | + 207.68.200.30
+-- | + 64.13.134.52
+-- | + 4.68.105.6
+-- | + 209.245.176.2
+-- | + 69.63.179.23
+-- |_ + 69.63.180.12
+--
+--
+-- @args http-grep.match the string to match in urls and page contents or list of patterns separated by delimiter
+-- @args http-grep.maxdepth the maximum amount of directories beneath
+-- the initial url to spider. A negative value disables the limit.
+-- (default: 3)
+-- @args http-grep.maxpagecount the maximum amount of pages to visit.
+-- A negative value disables the limit (default: 20)
+-- @args http-grep.url the url to start spidering. This is a URL
+-- relative to the scanned host eg. /default.html (default: /)
+-- @args http-grep.withinhost only spider URLs within the same host.
+-- (default: true)
+-- @args http-grep.withindomain only spider URLs within the same
+-- domain. This widens the scope from <code>withinhost</code> and can
+-- not be used in combination. (default: false)
+-- @args http-grep.breakonmatch Returns output if there is a match for a single pattern type.
+-- @args http-grep.builtins supply a single or a list of built in types. supports email, phone, mastercard, discover,
+-- visa, amex, ssn and ip addresses. If you just put in script-args http-grep.builtins then all will be enabled.
+--
+-- @xmloutput
+-- <table key="(1) https://nmap.org/book/man-bugs.html">
+-- <table key="(1) email">
+-- <elem>+ dev@nmap.org</elem>
+-- </table>
+-- </table>
+-- <table key="(1) https://nmap.org/book/install.html">
+-- <table key="(1) email">
+-- <elem>+ fyodor@nmap.org</elem>
+-- </table>
+-- </table>
+-- <table key="(16) https://nmap.org/changelog.html">
+-- <table key="(7) ip">
+-- <elem>+ 255.255.255.255</elem>
+-- <elem>+ 10.99.24.140</elem>
+-- <elem>+ 74.125.53.103</elem>
+-- <elem>+ 64.147.188.3</elem>
+-- <elem>+ 203.65.42.255</elem>
+-- <elem>+ 192.31.33.7</elem>
+-- <elem>+ 168.0.40.135</elem>
+-- </table>
+-- <table key="(9) email">
+-- <elem>+ d1n@inbox.com</elem>
+-- <elem>+ fyodor@insecure.org</elem>
+-- <elem>+ uce@ftc.gov</elem>
+-- <elem>+ rhundt@fcc.gov</elem>
+-- <elem>+ jquello@fcc.gov</elem>
+-- <elem>+ sness@fcc.gov</elem>
+-- <elem>+ president@whitehouse.gov</elem>
+-- <elem>+ haesslich@loyalty.org</elem>
+-- <elem>+ rchong@fcc.gov</elem>
+-- </table>
+-- </table>
+-- <table key="(6) https://nmap.org/5/#5changes">
+-- <table key="(6) ip">
+-- <elem>+ 207.68.200.30</elem>
+-- <elem>+ 64.13.134.52</elem>
+-- <elem>+ 4.68.105.6</elem>
+-- <elem>+ 209.245.176.2</elem>
+-- <elem>+ 69.63.179.23</elem>
+-- <elem>+ 69.63.180.12</elem>
+-- </table>
+-- </table>
+
+author = {"Patrik Karlsson", "Gyanendra Mishra"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.http
+
+-- Shortens a matching string if it exceeds 60 characters
+-- All characters after 60 will be replaced with ...
+local function shortenMatch(match)
+ if ( #match > 60 ) then
+ return match:sub(1, 60) .. " ..."
+ else
+ return match
+ end
+end
+
+-- A function to validate IP addresses.
+local function ip(matched_ip)
+ local oct_1, oct_2, oct_3, oct_4 = matched_ip:match('(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d)%.(%d%d?%d?)')
+ oct_1, oct_2, oct_3, oct_4 = tonumber(oct_1), tonumber(oct_2), tonumber(oct_3), tonumber(oct_4)
+ if oct_1 > 255 or oct_2 > 255 or oct_3 > 255 or oct_4 > 255 then
+ return false
+ end
+ return true
+end
+
+-- A function to validate credit card numbers.
+local function luhn(matched_ccno)
+ local ccno = matched_ccno:gsub("%D", ""):reverse()
+ local sum = 0
+ for i = 1, #ccno do
+ local d = tonumber(ccno:sub(i,i))
+ if i % 2 == 0 then
+ local dd = 2 * d
+ d = dd // 10 + dd % 10
+ end
+ sum = sum + d
+ end
+ return sum % 10 == 0
+end
+
+-- A function to validate ssn numbers.
+local bad_ssn = {
+ -- https://www.ssa.gov/history/ssn/misused.html
+ ["078-05-1120"] = true,
+ ["219-09-9999"] = true,
+ -- Obvious fakes
+ ["123-12-1234"] = true,
+ ["123-45-6789"] = true,
+ ["321-21-4321"] = true,
+ ["111-11-1111"] = true,
+ ["222-22-2222"] = true,
+ ["333-33-3333"] = true,
+ ["444-44-4444"] = true,
+ ["555-55-5555"] = true,
+ ["666-66-6666"] = true,
+ ["777-77-7777"] = true,
+ ["888-88-8888"] = true,
+ ["999-99-9999"] = true,
+}
+local bad_group_1 = {
+ ["000"] = true,
+ ["333"] = true,
+ ["666"] = true,
+}
+local function ssn(matched_ssn)
+ if bad_ssn[matched_ssn] then return false end
+ local group_1, group_2, group_3 = matched_ssn:match('(%d%d%d)%-(%d%d)%-(%d%d%d%d)')
+ if bad_group_1[group_1] then return false end
+ if group_2 == "00" or group_3 == "0000" then return false end
+ group_1 = tonumber(group_1)
+ -- This line rules out ITINs, which may also be of interest.
+ if 900 <= group_1 and group_1 <= 999 then return false end
+ return true
+end
+
+-- The default function if there is no validator.
+local function default()
+ return true
+end
+
+action = function(host, port)
+ -- a list of popular patterns with their validators.
+ local BUILT_IN_PATTERNS = {
+ ['email'] = {'[A-Za-z0-9%.%%%+%-]+@[A-Za-z0-9%.%%%+%-]+%.%w%w%w?%w?'},
+ ['phone'] = {'%f[%d]%d%d%d%-%d%d%d%d%f[^%d]','%f[%d%(]%(%d%d%d%)%s%d%d%d%-%d%d%d%f[^%d]','%f[%d%+]%+%-%d%d%d%-%d%d%d%-%d%d%d%d%f[^%d]','%d%d%d%-%d%d%d%-%d%d%d%d%f[^%d]'},
+ ['mastercard']= {'%f[%d]5%d%d%d%s?%-?%d%d%d%d%s?%-?%d%d%d%d%s?%-?%d%d%d%d%f[^%d]', ['validate'] = luhn},
+ ['visa'] = {'%f[%d]4%d%d%d%s?%-?%d%d%d%d%s?%-?%d%d%d%d%s?%-?%d%d%d%d%f[^%d]', ['validate'] = luhn},
+ ['discover']={'%f[%d]6011%s?%-?%d%d%d%d%s?%-?%d%d%d%d%s?%-?%d%d%d%d%f[^%d]', ['validate'] = luhn},
+ ['amex'] ={'%f[%d]3%d%d%d%s?%-?%d%d%d%d%d%d%s?%-?%d%d%d%d%d%f[^%d]', ['validate'] = luhn},
+ ['ssn'] = {'%f[%d]%d%d%d%-%d%d%-%d%d%d%d%f[^%d]', ['validate'] = ssn},
+ ['ip']={'%f[%d]%d%d?%d?%.%d%d?%d?%.%d%d?%d%.%d%d?%d?%f[^%d]', ['validate'] = ip},
+ }
+
+ -- read script specific arguments
+ local match = stdnse.get_script_args(SCRIPT_NAME .. ".match")
+ local break_on_match = stdnse.get_script_args(SCRIPT_NAME .. ".breakonmatch")
+ local builtins = stdnse.get_script_args(SCRIPT_NAME .. ".builtins")
+ local to_be_searched = {}
+
+ local crawler = httpspider.Crawler:new(host, port, nil, { scriptname = SCRIPT_NAME } )
+ local results = stdnse.output_table()
+ local all_match = {} -- a table that stores all matches. used to eliminate duplicates.
+
+ -- check if builtin argument is a table or a single value
+ if builtins and builtins == 1 then
+ for name, patterns in pairs(BUILT_IN_PATTERNS) do
+ to_be_searched[name] = {}
+ for _, pattern in ipairs(patterns) do
+ table.insert(to_be_searched[name], pattern)
+ end
+ end
+ elseif builtins and type(builtins) ~= 'table' then
+ if BUILT_IN_PATTERNS[builtins] ~= nil then
+ to_be_searched[builtins] = {}
+ for _, pattern in ipairs(BUILT_IN_PATTERNS[builtins]) do
+ table.insert(to_be_searched[builtins], pattern)
+ end
+ end
+ elseif builtins and type(builtins) == 'table' then
+ for _, builtin in ipairs(builtins) do
+ if BUILT_IN_PATTERNS[builtin] ~= nil then
+ to_be_searched[builtin] = {}
+ for _, pattern in ipairs(BUILT_IN_PATTERNS[builtin]) do
+ table.insert(to_be_searched[builtin], pattern)
+ end
+ end
+ end
+ end
+
+ -- check if match argument is a table or a single value
+ if match and type(match) ~= 'table' then
+ to_be_searched['User Pattern 1'] = {}
+ table.insert(to_be_searched['User Pattern 1'], match)
+ elseif type(match) == 'table' then
+ for i, pattern in pairs(match) do
+ local k = 'User Pattern ' .. tostring(i)
+ to_be_searched[k] = {}
+ table.insert(to_be_searched[k], pattern)
+ end
+ end
+
+ -- if nothing is specified then email and ip are checked.
+ if not next(to_be_searched) then
+ to_be_searched['email'] = {}
+ to_be_searched['ip'] = {}
+ table.insert(to_be_searched['email'], BUILT_IN_PATTERNS["email"][1])
+ table.insert(to_be_searched['ip'], BUILT_IN_PATTERNS["ip"][1])
+ end
+
+ -- set timeout to 10 seconds
+ crawler:set_timeout(10000)
+
+ while(true) do
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+ local count = 0 -- pattern matches per url
+ local pattern_count = 0 -- number of matches for particual pattern type say 'email'
+ local matches = {} -- a table that stores matches for all pattern types
+ local pattern_type = {} -- a table that resets for every pattern type
+ for pattern_name, pattern_table in pairs(to_be_searched) do
+ pattern_type = {}
+ pattern_count = 0
+ for _, pattern in ipairs(pattern_table) do
+ local body = r.response.body
+ -- try to match the url and body
+ if body and ( body:match( pattern ) or tostring(r.url):match(pattern) ) then
+ pattern_count = select(2, body:gsub(pattern, ""))
+ count = count + pattern_count
+ for match in body:gmatch(pattern) do
+ local validate = BUILT_IN_PATTERNS[pattern_name]and BUILT_IN_PATTERNS[pattern_name]['validate'] or default
+ if validate(match) and not tableaux.contains(all_match, match) then
+ table.insert(pattern_type, "+ " .. shortenMatch(match))
+ table.insert(all_match, match)
+ else
+ count = count - 1
+ pattern_count = pattern_count - 1
+ end
+ end
+ end
+ end
+ if pattern_count > 0 then
+ matches[("(%d) %s"):format(pattern_count, pattern_name)] = pattern_type
+ end
+ end
+ if count > 0 then
+ results[("(%d) %s"):format(count,tostring(r.url))] = matches
+ end
+ -- should we continue to search for matches?
+ if break_on_match and pattern_count > 0 then
+ crawler:stop()
+ break
+ end
+ end
+ if #results > 0 then return results end
+end
+