252 lines
8.9 KiB
Lua
252 lines
8.9 KiB
Lua
local http = require "http"
|
|
local httpspider = require "httpspider"
|
|
local io = require "io"
|
|
local lfs = require "lfs"
|
|
local nmap = require "nmap"
|
|
local shortport = require "shortport"
|
|
local stdnse = require "stdnse"
|
|
local string = require "string"
|
|
local stringaux = require "stringaux"
|
|
local table = require "table"
|
|
|
|
description = [[The script is used to fetch files from servers.
|
|
|
|
The script supports three different use cases:
|
|
* The paths argument isn't provided, the script spiders the host
|
|
and downloads files in their respective folders relative to
|
|
the one provided using "destination".
|
|
* The paths argument(a single item or list) is provided and the path starts
|
|
with "/", the script tries to fetch the path relative to the url
|
|
provided via the argument "url".
|
|
* The paths argument(a single item or list) is provided and the path doesn't
|
|
start with "/". Then the script spiders the host and tries to find
|
|
files which contain the path(now treated as a pattern).
|
|
]]
|
|
|
|
---
|
|
-- @usage nmap --script http-fetch --script-args destination=/tmp/mirror <target>
|
|
-- nmap --script http-fetch --script-args 'paths={/robots.txt,/favicon.ico}' <target>
|
|
-- nmap --script http-fetch --script-args 'paths=.html' <target>
|
|
-- nmap --script http-fetch --script-args 'url=/images,paths={.jpg,.png,.gif}' <target>
|
|
--
|
|
-- @args http-fetch.destination - The full path of the directory to save the file(s) to preferably with the trailing slash.
|
|
-- @args http-fetch.files - The name of the file(s) to be fetched.
|
|
-- @args http-fetch.url The base URL to start fetching. Default: "/"
|
|
-- @args http-fetch.paths A list of paths to fetch. If relative, then the site will be spidered to find matching filenames.
|
|
-- Otherwise, they will be fetched relative to the url script-arg.
|
|
-- @args http-fetch.maxdepth The maximum amount of directories beneath
|
|
-- the initial url to spider. A negative value disables the limit.
|
|
-- (default: 3)
|
|
-- @args http-fetch.maxpagecount The maximum amount of pages to fetch.
|
|
-- @args http-fetch.noblacklist By default files like jpg, rar, png are blocked. To
|
|
-- fetch such files set noblacklist to true.
|
|
-- @args http-fetch.withinhost The default behavior is to fetch files from the same host. Set to False
|
|
-- to do otherwise.
|
|
-- @args http-fetch.withindomain If set to true then the crawling would be restricted to the domain provided
|
|
-- by the user.
|
|
--
|
|
-- @output
|
|
-- | http-fetch:
|
|
-- | Successfully Downloaded:
|
|
-- | http://scanme.nmap.org:80/ as /tmp/mirror/45.33.32.156/80/index.html
|
|
-- |_ http://scanme.nmap.org/shared/css/insecdb.css as /tmp/mirror/45.33.32.156/80/shared/css/insecdb.css
|
|
--
|
|
-- @xmloutput
|
|
-- <table key="Successfully Downloaded">
|
|
-- <elem>http://scanme.nmap.org:80/ as /tmp/mirror/45.33.32.156/80/index.html</elem>
|
|
-- <elem>http://scanme.nmap.org/shared/css/insecdb.css as /tmp/mirror/45.33.32.156/80/shared/css/insecdb.css</elem>
|
|
-- </table>
|
|
-- <elem key="result">Successfully Downloaded Everything At: /tmp/mirror/45.33.32.156/80/</elem>
|
|
|
|
author = "Gyanendra Mishra"
|
|
|
|
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
|
|
|
|
categories = {"safe"}
|
|
|
|
portrule = shortport.http
|
|
|
|
local SEPARATOR = lfs.get_path_separator()
|
|
|
|
local function build_path(file, url)
|
|
local path = '/' .. url .. file
|
|
return path:gsub('//', '/')
|
|
end
|
|
|
|
local function create_directory(path)
|
|
local status, err = lfs.mkdir(path)
|
|
if status then
|
|
stdnse.debug2("Created path %s", path)
|
|
return true
|
|
elseif err == "No such file or directory" then
|
|
stdnse.debug2("Parent directory doesn't exist %s", path)
|
|
local index = string.find(path:sub(1, path:len() -1), SEPARATOR .. "[^" .. SEPARATOR .. "]*$")
|
|
local sub_path = path:sub(1, index)
|
|
stdnse.debug2("Trying path...%s", sub_path)
|
|
create_directory(sub_path)
|
|
lfs.mkdir(path)
|
|
end
|
|
end
|
|
|
|
local function save_file(content, file_name, destination, url)
|
|
|
|
local file_path
|
|
|
|
if file_name then
|
|
file_path = destination .. file_name
|
|
else
|
|
file_path = destination .. url:getDir()
|
|
create_directory(file_path)
|
|
if url:getDir() == url:getFile() then
|
|
file_path = file_path .. "index.html"
|
|
else
|
|
file_path = file_path .. stringaux.filename_escape(url:getFile():gsub(url:getDir(),""))
|
|
end
|
|
end
|
|
|
|
file_path = file_path:gsub("//", "/")
|
|
file_path = file_path:gsub("\\/", "\\")
|
|
|
|
local file,err = io.open(file_path,"r")
|
|
if not err then
|
|
stdnse.debug1("File Already Exists")
|
|
return true, file_path
|
|
end
|
|
file, err = io.open(file_path,"w")
|
|
if file then
|
|
stdnse.debug1("Saving to ...%s",file_path)
|
|
file:write(content)
|
|
file:close()
|
|
return true, file_path
|
|
else
|
|
stdnse.debug1("Error encountered in writing file.. %s",err)
|
|
return false, err
|
|
end
|
|
end
|
|
|
|
local function fetch_recursively(host, port, url, destination, patterns, output)
|
|
local crawler = httpspider.Crawler:new(host, port, url, { scriptname = SCRIPT_NAME })
|
|
crawler:set_timeout(10000)
|
|
while(true) do
|
|
local status, r = crawler:crawl()
|
|
if ( not(status) ) then
|
|
if ( r.err ) then
|
|
return stdnse.format_output(false, r.reason)
|
|
else
|
|
break
|
|
end
|
|
end
|
|
local body = r.response.body
|
|
local url_string = tostring(r.url)
|
|
local file = r.url:getFile():gsub(r.url:getDir(),"")
|
|
if body and r.response.status == 200 and patterns then
|
|
for _, pattern in pairs(patterns) do
|
|
if file:find(pattern, nil, true) then
|
|
local status, err_message = save_file(r.response.body, nil, destination, r.url)
|
|
if status then
|
|
output['Matches'] = output['Matches'] or {}
|
|
output['Matches'][pattern] = output['Matches'][pattern] or {}
|
|
table.insert(output['Matches'][pattern], string.format("%s as %s",r.url:getFile()),err_message)
|
|
else
|
|
output['ERROR'] = output['ERROR'] or {}
|
|
output['ERROR'][url_string] = err_message
|
|
end
|
|
break
|
|
end
|
|
end
|
|
elseif body and r.response.status == 200 then
|
|
stdnse.debug1("Processing url.......%s",url_string)
|
|
local stat, path_or_err = save_file(body, nil, destination, r.url)
|
|
if stat then
|
|
output['Successfully Downloaded'] = output['Successfully Downloaded'] or {}
|
|
table.insert(output['Successfully Downloaded'], string.format("%s as %s", url_string, path_or_err))
|
|
else
|
|
output['ERROR'] = output['ERROR'] or {}
|
|
output['ERROR'][url_string] = path_or_err
|
|
end
|
|
else
|
|
if not r.response.body then
|
|
stdnse.debug1("No Body For: %s",url_string)
|
|
elseif r.response and r.response.status ~= 200 then
|
|
stdnse.debug1("Status not 200 For: %s",url_string)
|
|
else
|
|
stdnse.debug1("False URL picked by spider!: %s",url_string)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function fetch(host, port, url, destination, path, output)
|
|
local response = http.get(host, port, build_path(path, url), nil)
|
|
if response and response.status and response.status == 200 then
|
|
local file = path:sub(path:find("/[^/]*$") + 1)
|
|
local save_as = (host.targetname or host.ip) .. SEPARATOR .. tostring(port.number) .. "-" .. file
|
|
local status, err_message = save_file(response.body, save_as, destination)
|
|
if status then
|
|
output['Successfully Downloaded'] = output['Successfully Downloaded'] or {}
|
|
table.insert(output['Successfully Downloaded'], string.format("%s as %s", path, save_as))
|
|
else
|
|
output['ERROR'] = output['ERROR'] or {}
|
|
output['ERROR'][path] = err_message
|
|
end
|
|
else
|
|
stdnse.debug1("%s doesn't exist on server at %s.", path, url)
|
|
end
|
|
end
|
|
|
|
action = function(host, port)
|
|
|
|
local destination = stdnse.get_script_args(SCRIPT_NAME..".destination") or false
|
|
local url = stdnse.get_script_args(SCRIPT_NAME..".url") or "/"
|
|
local paths = stdnse.get_script_args(SCRIPT_NAME..'.paths') or nil
|
|
|
|
local output = stdnse.output_table()
|
|
local patterns = {}
|
|
|
|
if not destination then
|
|
output.ERROR = "Please enter the complete path of the directory to save data in."
|
|
return output, output.ERROR
|
|
end
|
|
|
|
local sub_directory = tostring(host.ip) .. SEPARATOR .. tostring(port.number) .. SEPARATOR
|
|
|
|
if destination:sub(-1) == '\\' or destination:sub(-1) == '/' then
|
|
destination = destination .. sub_directory
|
|
else
|
|
destination = destination .. SEPARATOR .. sub_directory
|
|
end
|
|
|
|
if paths then
|
|
if type(paths) ~= 'table' then
|
|
paths = {paths}
|
|
end
|
|
for _, path in pairs(paths) do
|
|
if path:sub(1, 1) == "/" then
|
|
fetch(host, port, url, destination, path, output)
|
|
else
|
|
table.insert(patterns, path)
|
|
end
|
|
end
|
|
if #patterns > 0 then
|
|
fetch_recursively(host, port, url, destination, patterns, output)
|
|
end
|
|
else
|
|
fetch_recursively(host, port, url, destination, nil, output)
|
|
end
|
|
|
|
if #output > 0 then
|
|
if paths then
|
|
return output
|
|
else
|
|
if nmap.verbosity() > 1 then
|
|
return output
|
|
else
|
|
output.result = "Successfully Downloaded Everything At: " .. destination
|
|
return output, output.result
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|