diff --git a/scripts/http-svn-enum.nse b/scripts/http-svn-enum.nse new file mode 100644 index 000000000..ed48aedd5 --- /dev/null +++ b/scripts/http-svn-enum.nse @@ -0,0 +1,128 @@ +local http = require "http" +local shortport = require "shortport" +local slaxml = require "slaxml" +local stdnse = require "stdnse" +local tab = require "tab" +local table = require "table" + +description = [[Enumerates users of a Subversion repository by examining logs of most recent commits. +]] + +--- +-- @usage nmap --script svn-enum +-- +-- @args http-svn-enum.count The number of logs to fetch. Defaults to the last 1000 commits. +-- @args http-svn-enum.url This is a URL relative to the scanned host eg. /default.html (default: /). +-- +-- @output +-- PORT STATE SERVICE REASON +-- 443/tcp open https syn-ack +-- | http-svn-enum: +-- | Author Count Revision Date +-- | gyani 183 34965 2015-07-24 +-- | robert 1 34566 2015-06-02 +-- | david 2 34785 2015-06-28 +-- +-- @xmloutput +--
+-- +-- Author +-- Count +-- Revision +-- Date +--
+-- +-- gyani +-- 183 +-- 34965 +-- 2015-07-24 +--
+-- +-- robert +-- 1 +-- 34566 +-- 2015-06-02 +--
+-- +-- david +-- 2 +-- 34785 +-- 2015-06-28 +--
+ +author = "Gyanendra Mishra" + +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +categories = {"default", "safe", "discovery"} + +local function get_callback(name, unames, temp) + if name == "creator-displayname" then + return function(content) + unames[content] = unames[content] and {unames[content][1] + 1, temp.revision} or {1, temp.revision} + temp.author = content + end + elseif name == "version-name" then + return function(content) temp.revision = content end + elseif name == "date" and temp.author then + return function(content) table.insert(unames[temp.author], content:sub(1,10)) end + end +end + +portrule = shortport.http + +action = function(host, port) + + local count = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".count")) or 1000 + local url = stdnse.get_script_args(SCRIPT_NAME .. ".url") or "/" + local output, revision, unames = tab.new(), nil, {} + + local options = { + header = { + ["Depth"] = 0, + }, + } + + -- first we fetch the current revision number + local response = http.generic_request(host, port, "PROPFIND", url, options) + if response and response.status == 207 then + + local parser = slaxml.parser:new() + parser._call = {startElement = function(name) + parser._call.text = name == "version-name" and function(content) revision = tonumber(content) end end, + closeElement = function(name) parser._call.text = function() return nil end end + } + parser:parseSAX(response.body, {stripWhitespace=true}) + + if revision then + + local start_revision = revision > count and revision - count or 1 + local content = ' '.. start_revision .. ' ' + + options = { + header = { + ["Depth"] = 1, + }, + content = content + } + + local temp = {} + response = http.generic_request(host, port, "REPORT", url, options) + if response and response.status == 200 then + + parser._call.startElement = function(name) parser._call.text = get_callback(name, unames, temp) end + parser:parseSAX(response.body, {stripWhitespace=true}) + + tab.nextrow(output) + tab.addrow(output, "Author", "Count", "Revision", "Date") + + for revision_author, data in pairs(unames) do + tab.addrow(output, revision_author, data[1], data[2], data[3]) + end + + if next(unames) then return output end + end + end + end +end +