diff --git a/nselib/data/http-devframework-fingerprints.lua b/nselib/data/http-devframework-fingerprints.lua new file mode 100644 index 000000000..09179bd82 --- /dev/null +++ b/nselib/data/http-devframework-fingerprints.lua @@ -0,0 +1,363 @@ +local http = require "http" +local table = require "table" +local url = require "url" + +--- +-- http-devframework-fingerprints.lua +-- This file contains fingerprint data for http-devframework.nse +-- +-- STRUCTURE: +-- * name - Descriptive name +-- * rapidDetect - Callback function that is called in the beginning +-- of detection process. It takes the host and port of the target website as +-- arguments. +-- * consumingDetect - Callback function that is called for each +-- spidered page. It takes the body of the response (HTML source code) and the +-- requested path as arguments. +--- + + +tools = { Django = { rapidDetect = function(host, port) + + -- Check if the site gives that familiar Django admin login page. + response = http.get(host, port, "/admin/") + + if response.body then + if string.find(response.body, "Log in | Django site admin") or + string.find(response.body, "this_is_the_login_form") or + string.find(response.body, "csrfmiddlewaretoken") then + return "Django detected. Found Django admin login page on /admin/" + end + end + + -- In Django, the cookie sessionid is being set when you log in + -- and forms will probably set a cookie called csrftoken. + if response.cookies then + for _, c in pairs(response.cookies) do + if c.name == "csrftoken" then + return "Django detected. Found sessionid cookie which means the contrib.auth package for authentication is enabled." + elseif c.name == "sessionid" then + return "Django detected. Found csrftoken cookie." + end + end + end + + -- See if DEBUG mode still happens to be true. + response = http.get(host, port, "/random404page/") + + if response.body then + if string.find(response.body, "DEBUG = True") then + return "Django detected. Found Django error page on /random404page/" + end + end + + end, + + consumingDetect = function(page, path) + if page then + if string.find(page, "csrfmiddlewaretoken") then + return "Django detected. Found csrfmiddlewaretoken on " .. path + end + if string.find(page, "id=\"id_") then + return "Django detected. Found id_ preffix in id attribute name on " .. path + end + if string.find(page, "%-TOTAL%-FORMS") or string.find(page, "%-DELETE") then + return "Django detected. Found -TOTAL-FORMS and -DELETE hidden inputs, which means there is a Django formset on " .. path + end + end + end + }, + + RubyOnRails = { rapidDetect = function(host, port) + + response = http.get(host, port, "/") + + -- Check for Mongrel or Passenger in the "Server" or "X-Powered-By" header + for h, v in pairs(response.header) do + if h == "x-powered-by" or h == "server" then + local vl = v:lower() + local m = vl:match("mongrel") or vl:match("passenger") + if m then + return "RoR detected. Found '" .. m .. "' in " .. h .. " header sent by the server." + end + end + end + + -- /rails/info/propertires shows project info when in development mode + response = http.get(host, port, "/rails/info/properties") + + if response.body then + if string.find(response.body, "Ruby version") then + return "RoR detected. Found properties file on /rails/info/properties/" + end + end + + -- Make up a bad path and match the error page + response = http.get(host, port, "/random404page/") + + if response.body then + if string.find(response.body, "Routing Error") then + return "RoR detected. Found RoR routing error page on /random404page/" + end + end + + end, + + consumingDetect = function(page, path) + + -- Check the source and look for csrf patterns. + if page then + if string.find(page, "csrf%-param") or string.find(page, "csrf%-token") then + return "RoR detected. Found csrf field on" .. path + end + end + + end + }, + + + ASPdotNET = { rapidDetect = function(host, port) + + response = http.get(host, port, "/") + + -- Look for an ASP.NET header. + for h, v in pairs(response.header) do + vl = v:lower() + if h == "x-aspnet-version" or string.find(vl, "asp") then + return "ASP.NET detected. Found related header." + end + end + + if response.cookies then + for _, c in pairs(response.cookies) do + if c.name == "aspnetsessionid" then + return "ASP.NET detected. Found aspnetsessionid cookie." + end + end + end + end, + + consumingDetect = function(page, path) + -- Check the source and look for common traces. + if page then + if string.find(page, " __VIEWSTATE") or + string.find(page, "__EVENT") or + string.find(page, "__doPostBack") or + string.find(page, "aspnetForm") or + string.find(page, "ctl00_") then + return "ASP.NET detected. Found common traces on" .. path + end + end + end + }, + + CodeIgniter = { rapidDetect = function(host, port) + + -- Match default error page. + response = http.get(host, port, "/random404page/") + + if response.body then + if string.find(response.body, "#990000") and + string.find(response.body, "404 Page Not Found") then + return "CodeIgniter detected. Found CodeIgniter default error page on /random404page/" + end + end + + end, + + consumingDetect = function(page, path) + return + end + }, + + CakePHP = { rapidDetect = function(host, port) + + + -- Find CAKEPHP header. + response = http.get(host, port, "/") + + for h, v in pairs(response.header) do + vl = v:lower() + if string.find(vl, "cakephp") then + return "CakePHP detected. Found related header." + end + end + + end, + + consumingDetect = function(page, path) + return + end + }, + + Symfony = { rapidDetect = function(host, port) + + -- Find Symfony header. + response = http.get(host, port, "/") + + for h, v in pairs(response.header) do + vl = v:lower() + if string.find(vl, "symfony") then + return "Symfony detected. Found related header." + end + end + + end, + + consumingDetect = function(page, path) + return + end + }, + + Wordpress = { rapidDetect = function(host, port) + + -- Check for common traces in the source code. + response = http.get(host, port, "/") + + if response.body then + if string.find(response.body, "content=[\"']WordPress") or + string.find(response.body, "wp%-content") then + return "Wordpress detected. Found common traces on /" + end + end + + -- Check if the default login page exists. + response = http.get(host, port, "/wp%-login") + + if response.status == "200" then + return "Wordpress detected. Found WP login page on /wp-login" + end + end, + + consumingDetect = function(page, path) + if page then + if string.find(page, "content=[\"']WordPress") or + string.find(page, "wp-content") then + return "Wordpress detected. Found common traces on " .. page + end + end + end + }, + + Joomla = { rapidDetect = function(host, port) + + + -- Check for common traces in the source code. + response = http.get(host, port, "/") + + if response.body then + if string.find(response.body, "content=[\"']Joomla!") then + return "Joomla detected. Found common traces on /" + end + end + + -- Check if the default login page exists. + response = http.get(host, port, "/administrator") + + if response.body and string.find(response.body, "Joomla") then + return "Joomla detected. Found Joomla login page on /administrator/" + end + + end, + + consumingDetect = function(page, path) + if page and string.find(page, "content=[\"']Joomla!") then + return "Joomla detected. Found common traces on " .. page + end + end + }, + + Drupal = { rapidDetect = function(host, port) + + -- Check for common traces in the source code. + response = http.get(host, port, "/") + + if response.body then + if string.find(response.body, "content=[\"']Drupal") then + return "Drupal detected. Found common traces on /" + end + end + end, + + consumingDetect = function(page, path) + if page and string.find(page, "content=[\"']Drupal") then + return "Drupal detected. Found common traces on " .. page + end + end + }, + + MediaWiki = { rapidDetect = function(host, port) + + -- Check for common traces in the source code. + response = http.get(host, port, "/") + + if response.body then + if string.find(response.body, "content=[\"']MediaWiki") or + string.find(response.body, "/mediawiki/") then + return "MediaWiki detected. Found common traces on /" + end + end + end, + + consumingDetect = function(page, path) + if page and string.find(page, "content=[\"']MediaWiki") or + string.find(page, "/mediawiki/") then + return "MediaWiki detected. Found common traces on " .. page + end + end + }, + + ColdFusion = { rapidDetect = function(host, port) + + response = http.get(host, port, "/") + + if response.cookies then + for _, c in pairs(response.cookies) do + if c.name == "cfid" or c.name == "cftoken" then + return "ColdFusion detected. Found " .. c.name .. " cookie." + end + end + end + end, + + consumingDetect = function(page, path) + return + end + }, + + Broadvision = { rapidDetect = function(host, port) + + response = http.get(host, port, "/") + + if response.cookies then + for _, c in pairs(response.cookies) do + if string.find(c.name, "bv_") then + return "Broadvision detected. Found " .. c.name .. " cookie." + end + end + end + end, + + consumingDetect = function(page, path) + return + end + }, + + WebSphereCommerce = { rapidDetect = function(host, port) + + response = http.get(host, port, "/") + + if response.cookies then + for _, c in pairs(response.cookies) do + if string.find(c.name, "wc_") then + return "WebSphere Commerce detected. Found " .. c.name .. " cookie." + end + end + end + end, + + consumingDetect = function(page, path) + return + end + }, + } diff --git a/scripts/http-devframework.nse b/scripts/http-devframework.nse new file mode 100644 index 000000000..03f468800 --- /dev/null +++ b/scripts/http-devframework.nse @@ -0,0 +1,145 @@ +description = [[ + +Tries to find out the technology behind the target website. + +The script checks for certain defaults that might not have been changed, like +common headers or URLs or HTML content. + +While the script does some guessing, note that overall there's no way to +determine what technologies a given site is using. + +You can help improve this script by adding new entries to +nselib/data/http-tools-fingerprints.lua + +Each entry must have: +* rapidDetect - Callback function that is called in the beginning +of detection process. It takes the host and port of target website as arguments. +* consumingDetect - Callback function that is called for each +spidered page. It takes the body of the response (HTML code) and the requested +path as arguments. + +Note that the consumingDetect callback will not take place only if +rapid option is enabled. + +]] + +--- +-- @usage nmap -p80 --script http-devframework.nse +-- +-- @args http-errors.rapid boolean value that determines if a rapid detection +-- should take place. The main difference of a rapid vs a lengthy detection +-- is that second one requires crawling through the website. Default: false +-- (lengthy detection is performed) +-- +-- @output +-- PORT STATE SERVICE REASON +-- 80/tcp open http syn-ack +-- |_http-devframework: Django detected. Found Django admin login page on /admin/ +--- + +categories = {"discovery", "intrusive"} +author = "George Chatzisofroniou" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +local http = require "http" +local shortport = require "shortport" +local stdnse = require "stdnse" +local table = require "table" +local string = require "string" +local httpspider = require "httpspider" + +portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open") + +local function loadFingerprints(filename, cat) + + local file, fingerprints + + -- Find the file + filename = nmap.fetchfile('nselib/data/' .. filename) or filename + + -- Load the file + stdnse.print_debug(1, "%s: Loading fingerprints: %s", SCRIPT_NAME, filename) + local env = setmetatable({fingerprints = {}}, {__index = _G}); + file = loadfile(filename, "t", env) + + if( not(file) ) then + stdnse.print_debug(1, "%s: Couldn't load the file: %s", SCRIPT_NAME, filename) + return + end + + file() + fingerprints = env.tools + + return fingerprints + +end + +action = function(host, port) + + local tools = stdnse.get_script_args("http-devframework.fingerprintfile") or loadFingerprints("nselib/data/http-devframework-fingerprints.lua") + local rapid = stdnse.get_script_args("http-devframework.rapid") + + local d + + -- Run rapidDetect() callbacks. + for f, method in pairs(tools) do + d = method["rapidDetect"](host, port) + if d then + return d + end + end + + local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME, + maxpagecount = 40, + maxdepth = -1, + withinhost = 1 + }) + + if rapid then + return "Couldn't determine the underlying framework or CMS. Try turning off 'rapid' mode." + end + + crawler.options.doscraping = function(url) + if crawler:iswithinhost(url) + and not crawler:isresource(url, "js") + and not crawler:isresource(url, "css") then + return true + end + end + + crawler:set_timeout(10000) + + while (true) do + + local response, path + + 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(true, ("ERROR: %s"):format(r.reason)) + else + break + end + end + + response = r.response + path = tostring(r.url) + + if (response.body) then + + -- Run consumingDetect() callbacks. + for f, method in pairs(tools) do + d = method["consumingDetect"](response.body, path) + if d then + return d + end + end + end + + return "Couldn't determine the underlying framework or CMS. Try increasing 'httpspider.maxpagecount' value to spider more pages." + + end + +end