1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-20 22:49:01 +00:00

Updated http-git - bugfixes, and also handles multiple roots with script-args

This commit is contained in:
ron
2012-07-25 01:47:43 +00:00
parent d397ac4076
commit ea5e4e07ae

View File

@@ -13,18 +13,16 @@
-- | BitBucket remote: AlexWebr/nse (accessed over HTTP, pull-only) -- | BitBucket remote: AlexWebr/nse (accessed over HTTP, pull-only)
-- |_ Based on the file '.gitignore', this is a Ruby on Rails application -- |_ Based on the file '.gitignore', this is a Ruby on Rails application
-- --
-- Version 1.0 -- Version 1.1
-- Created 27 June 2012 - written by Alex Weber <alexwebr@gmail.com> -- Created 27 June 2012 - written by Alex Weber <alexwebr@gmail.com>
local http = require("http") local http = require("http")
local shortport = require("shortport") local shortport = require("shortport")
local stdnse = require("stdnse") local stdnse = require("stdnse")
local string = require("string") local strbuf = require("strbuf")
local table = require("table") local table = require("table")
description = [[ Checks for a Git repository found in a website's document root (GET /.git/<something> HTTP/1.1)
description = [[
Checks for a Git repository found in a website's document root (GET /.git/<something> HTTP/1.1)
Gets as much information about the repository as possible, including language/framework, Github Gets as much information about the repository as possible, including language/framework, Github
username, last commit message, and repository description. username, last commit message, and repository description.
]] ]]
@@ -35,35 +33,52 @@ license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
portrule = shortport.http portrule = shortport.http
local STATUS_OK = 200 -- We consider 200 to mean "okay, file exists and we received its contents" local STATUS_OK = 200 -- We consider 200 to mean "okay, file exists and we received its contents"
local out -- The string to return to Nmap
local replies = {}
-- Instead of concatenating everywhere
-- ap is short for 'append'
-- If second argument is nil or false, a new line is made
-- for every call. With 'true', we append to
-- to the most-recently ap()'ed line
local function ap(to_append, append_to_last_entry)
if not out then out = {} end
if append_to_last_entry then
local len = #out
out[len] = out[len] .. to_append
else
table.insert(out, to_append)
end
end
-- This function returns true if we got a 200 OK when
-- fetching '/filename' from the server
local function ok(filename)
return (replies[filename].status == STATUS_OK)
end
function action(host, port) function action(host, port)
-- All methods that we call on this table will be from the table library
local out = {}
setmetatable(out, {__index = table})
local repos_found = 0
-- We can accept a single root, or a table of roots to try
local root_arg = stdnse.get_script_args("http-git.root")
local roots
if type(root_arg) == "table" then
roots = root_arg
elseif type(root_arg) == "string" or type(root_arg) == "number" then
roots = { tostring(root_arg) }
elseif root_arg == nil then -- if we didn't get an argument
roots = { "/" }
end
-- Try each root in succession
for i, root in ipairs(roots) do
root = tostring(root)
root = root or '/'
-- Put a forward slash on the beginning and end of the root, if none was
-- provided. We will print this, so the user will know that we've mangled it
if not string.find(root, "/$") then -- if there is no slash at the end
root = root .. "/"
end
if not string.find(root, "^/") then -- if there is no slash at the beginning
root = "/" .. root
end
-- If we can't get /.git/HEAD, don't even bother continuing -- If we can't get /.git/HEAD, don't even bother continuing
-- We could try for /.git/, but we will not get a 200 if directory -- We could try for /.git/, but we will not get a 200 if directory
-- listings are disallowed. -- listings are disallowed.
if http.get(host, port, "/.git/HEAD").status == STATUS_OK then if http.get(host, port, root .. ".git/HEAD").status == STATUS_OK then
if repos_found > 0 then
out:insert("")
end
repos_found = repos_found + 1
local replies = {}
-- This function returns true if we got a 200 OK when
-- fetching 'filename' from the server
local function ok(filename)
return (replies[filename].status == STATUS_OK)
end
-- These are files that are small, very common, and don't -- These are files that are small, very common, and don't
-- require zlib to read -- require zlib to read
-- These files are created by creating and using the repository, -- These files are created by creating and using the repository,
@@ -77,13 +92,13 @@ function action(host, port)
} }
local count = { ok = 0, tried = 0 } local count = { ok = 0, tried = 0 }
local prequests = {} local prequests = {} -- prequests = pipelined requests (temp)
-- Go through all of the filenames and do an HTTP GET -- Go through all of the filenames and do an HTTP GET
for _, name in ipairs(repo) do -- for every filename for _, name in ipairs(repo) do -- for every filename
http.pipeline_add('/' .. name, nil, prequests) http.pipeline_add(root .. name, nil, prequests)
end end
-- do the requests -- do the requests
replies = http.pipeline(host, port, prequests) replies = http.pipeline_go(host, port, prequests)
for i, reply in ipairs(replies) do for i, reply in ipairs(replies) do
count.tried = count.tried + 1 count.tried = count.tried + 1
-- We want this to be indexed by filename, not an integer, so we convert it -- We want this to be indexed by filename, not an integer, so we convert it
@@ -95,11 +110,12 @@ function action(host, port)
-- Tell the user that we found a repository, and indicate if -- Tell the user that we found a repository, and indicate if
-- we didn't find all the files we were looking for. -- we didn't find all the files we were looking for.
local location = host.ip .. ":" .. port.number .. root .. ".git/"
if count.ok == count.tried then if count.ok == count.tried then
ap("Git repository found in web root") out:insert("Git repository found at " .. location)
else -- if we didn't find all the files we were hoping to, we might not actually have a repo else -- if we didn't find all the files we were hoping to, we might not actually have a repo
ap("Potential Git repository found in web root") out:insert("Potential Git repository found at " .. location .. " (found " ..
ap(" (found " .. tostring(count.ok + 1) .. " of " .. tostring(count.tried + 1) .. " expected files)", true) tostring(count.ok + 1) .. " of " .. tostring(count.tried + 1) .. " expected files)")
-- we already got /.git/HEAD, so add one to 'found' and 'expected' -- we already got /.git/HEAD, so add one to 'found' and 'expected'
end end
@@ -113,18 +129,25 @@ function action(host, port)
end end
return t_to_return return t_to_return
end end
-- Look through all the repo files we grabbed and see if we can find anything interesting -- Look through all the repo files we grabbed and see if we can find anything interesting
local interesting = { "bug", "passw", "pw", "user", "uid", "key", "secret" } local interesting = { "bug", "passw", "pw", "user", "uid", "key", "secret" }
for name, reply in pairs(replies) do for name, reply in pairs(replies) do
if ok(name) then -- for all replies that were successful if ok(name) then -- for all replies that were successful
local found_anything = false -- have we found anything yet? local found_anything = false -- have we found anything yet?
local buf = strbuf.new()
for _, matched in ipairs(match_many(reply.body:lower(), interesting)) do -- match all files against 'interesting' for _, matched in ipairs(match_many(reply.body:lower(), interesting)) do -- match all files against 'interesting'
if not found_anything then -- if this is our first find, print filename and stuff if not found_anything then -- if this is our first find, print filename and stuff
ap("Contents of '" .. name .. "' matched patterns '" .. matched .. "'") buf = (((((buf .. "Contents of '") .. name) .. "' matched patterns '") .. matched) .. "'") -- the '..' is right-associative :(
found_anything = true found_anything = true
else ap(", '" .. matched .. "'", true) end -- if we found something already, tack this pattern onto the end else
buf = ((buf .. ", '" .. matched) .. "'")
end -- if we found something already, tack this pattern onto the end
end -- If we matched anything, finish the line up end -- If we matched anything, finish the line up
if found_anything then ap(" (case-insensitive)", true) end if found_anything then
buf = buf .. " (case-insensitive match)"
out:insert(strbuf.dump(buf))
end
end end
end end
@@ -132,14 +155,16 @@ function action(host, port)
-- a summary of it (the first 60 characters or the first line, whichever is shorter) -- a summary of it (the first 60 characters or the first line, whichever is shorter)
local function append_short_version(description, original_string) local function append_short_version(description, original_string)
local short = string.sub(original_string, 1, 60) -- trim the string first, in case it is huge local short = string.sub(original_string, 1, 60) -- trim the string first, in case it is huge
local no_newline = string.match(short, "(.-)\r?\n") -- we don't want such an open-ended regex on a potentially huge string
-- We try to cut off the newline if we can -- We try to cut off the newline if we can
local s = no_newline or short local no_newline = string.match(short, "(.-)\r?\n") -- we don't want such an open-ended regex on a potentially huge string
ap(description .. ": " .. s) s = no_newline or short
-- If we cut off something, we want to put an ellipsis on the end
if #original_string > #s then if #original_string > #s then
ap("...", true) -- If we cut off something, we want to put an ellipsis on the end
s = description .. ": " .. s .. "..."
else
s = description .. ": " .. s
end end
out:insert(s)
end end
-- Get the first line and trim to 60 characters, if we got a COMMIT_EDITMESSAGE -- Get the first line and trim to 60 characters, if we got a COMMIT_EDITMESSAGE
@@ -157,7 +182,7 @@ function action(host, port)
-- This function will take a Git hosting service URL or a service -- This function will take a Git hosting service URL or a service
-- the allows deployment via Git and find out if there is an entry -- the allows deployment via Git and find out if there is an entry
-- for it in the configuration file -- for it in the configuration file
local function lookforremote(config, url, service, success_string) function lookforremote(config, url, service, success_string)
-- Different combinations of separating characters in the remote can -- Different combinations of separating characters in the remote can
-- indicate the access method - I know about SSH, HTTP, and Smart HTTP -- indicate the access method - I know about SSH, HTTP, and Smart HTTP
local access1, access2, reponame = string.match( local access1, access2, reponame = string.match(
@@ -165,28 +190,31 @@ function action(host, port)
if reponame then if reponame then
-- Try and cut off the '.git' extension -- Try and cut off the '.git' extension
reponame = string.match(reponame, "(.+)%.git") or reponame reponame = string.match(reponame, "(.+)%.git") or reponame
ap(service .. " remote: " .. reponame) s = strbuf.new()
s = (((s .. service) .. " remote: ") .. reponame)
-- git@github:Username... = SSH, https://github.com/Username... = HTTP{S} -- git@github:Username... = SSH, https://github.com/Username... = HTTP{S}
-- ^ ^ We match on these ^ ^ -- ^ ^ We match on these ^ ^
if access1 == "@" and access2 == "/" then if access1 == "@" and access2 == "/" then
-- Smart HTTP uses regular HTTP urls, but includes 'username@github.com...' -- Smart HTTP uses regular HTTP urls, but includes 'username@github.com...'
ap(" (accessed over Smart HTTP)", true) s = s .. " (accessed over Smart HTTP)"
elseif access1 == "@" and access2 == ":" then elseif access1 == "@" and access2 == ":" then
-- SSH syntax is like 'git@github.com:User/repo.git' -- SSH syntax is like 'git@github.com:User/repo.git'
ap(" (accessed over SSH)", true) s = s .. " (accessed over SSH)"
elseif access1 == "/" and access2 == "/" then elseif access1 == "/" and access2 == "/" then
-- 'Dumb' HTTP is read-only, looks like "https://github.com/User/repo.git" -- 'Dumb' HTTP is read-only, looks like "https://github.com/User/repo.git"
ap(" (accessed over HTTP, pull-only)", true) s = s .. " (accessed over HTTP, pull-only)"
else else
-- Not sure what / and : could be... perhaps regular, unencrypted Git protocol? -- Not sure what / and : could be... perhaps regular, unencrypted Git protocol?
ap(" (can't determine access method)") s = s .. " (can't determine access method)"
end end
out:insert(strbuf.dump(s))
-- If we did find an entry for this service in the configuration, that might -- If we did find an entry for this service in the configuration, that might
-- mean something special (example - Heroku remotes might be deployed somewhere) -- mean something special (example - Heroku remotes might be deployed somewhere)
-- We replace '<repo>' with the reponame, <url> with the URL, etc -- We replace '<repo>' with the reponame, <url> with the URL, etc
if success_string then if success_string then
local replace = { reponame = reponame, url = url, service = service } local replace = { reponame = reponame, url = url, service = service }
ap(string.gsub(success_string, "<(.-)>", replace)) local val = " -> " .. string.gsub(success_string, "<(.-)>", replace)
out:insert(val)
end end
end end
end end
@@ -200,9 +228,9 @@ function action(host, port)
-- These are some popular / well-known Git hosting services and/or hosting services -- These are some popular / well-known Git hosting services and/or hosting services
-- that allow deployment via 'git push' -- that allow deployment via 'git push'
local popular_remotes = { local popular_remotes = {
{ "github%.com", "GitHub" }, { "github%.com", "GitHub", "Source might be at https://github.com/<reponame>" },
{ "gitorious%.com", "Gitorious" }, { "gitorious%.com", "Gitorious", "Source might be at https://gitorious.com/<reponame>" },
{ "bitbucket%.org", "BitBucket" }, { "bitbucket%.org", "BitBucket", "Source might be at https://bitbucket.org/<reponame>" },
{ "heroku%.com", "Heroku", "App might be deployed to http://<reponame>.herokuapp.com" }, { "heroku%.com", "Heroku", "App might be deployed to http://<reponame>.herokuapp.com" },
} }
-- Go through all of the popular remotes and look for it in the config file -- Go through all of the popular remotes and look for it in the config file
@@ -220,10 +248,21 @@ function action(host, port)
".git/info/exclude", ".git/info/exclude",
} }
local fingerprints = { local fingerprints = {
{ "/%.bundle", "Ruby on Rails application" }, -- More specific matches (MyFaces > JSF > Java) on top -- Many of these taken from https://github.com/gitignore
{ "%.py[co]", "Python application" }, { "%.scala_dependencies", "Scala application" },
{ "%.jsp", "JSP webapp" }, { "npm%-debug%.log", "node.js application" },
{ "joomla%.xml", "Joomla! site" },
{ "jboss/server", "JBoss Java web application" },
{ "wp%-%*%.php", "WordPress site" },
{ "app/config/database%.php", "CakePHP web application" },
{ "sites/default/settings%.php", "Drupal site" },
{ "local_settings.py", "Django web application" },
{ "/%.bundle", "Ruby on Rails web application" }, -- More specific matches (MyFaces > JSF > Java) on top
{ "%.py[dco]", "Python application" },
{ "%.jsp", "JSP web application" },
{ "%.bundle", "Ruby application" },
{ "%.class", "Java application" }, { "%.class", "Java application" },
{ "%.php", "PHP application" },
} }
local excludefile_that_matched = nil local excludefile_that_matched = nil
local app = nil local app = nil
@@ -232,7 +271,7 @@ function action(host, port)
if ok(file) then -- we only test all fingerprints if we got the file if ok(file) then -- we only test all fingerprints if we got the file
for i, fingerprint in ipairs(fingerprints) do for i, fingerprint in ipairs(fingerprints) do
if string.match(replies[file].body, fingerprint[1]) then if string.match(replies[file].body, fingerprint[1]) then
ap("Based on the file '" .. file .. "', this is a " .. fingerprint[2]) out:insert("Based on the file '" .. file .. "', this is a " .. fingerprint[2])
-- Based on the file '.gitignore', this is a Ruby on Rails application" -- Based on the file '.gitignore', this is a Ruby on Rails application"
break -- we only want to print our first guess (the most specific one that matched) break -- we only want to print our first guess (the most specific one that matched)
end end
@@ -240,8 +279,9 @@ function action(host, port)
end end
end end
end end
end
-- Replace non-printing characters with asterisks -- Replace non-printing characters with asterisks
if out then return string.gsub(stdnse.format_output(true, out), "[^%w%p%s]", "*") if #out > 0 then return string.gsub(stdnse.format_output(true, out), "[^%w%p%s]", "*")
else return nil end else return nil end
end end