summaryrefslogtreecommitdiffstats
path: root/scripts/http-drupal-enum.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/http-drupal-enum.nse')
-rw-r--r--scripts/http-drupal-enum.nse233
1 files changed, 233 insertions, 0 deletions
diff --git a/scripts/http-drupal-enum.nse b/scripts/http-drupal-enum.nse
new file mode 100644
index 0000000..49a24f2
--- /dev/null
+++ b/scripts/http-drupal-enum.nse
@@ -0,0 +1,233 @@
+local coroutine = require "coroutine"
+local http = require "http"
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local rand = require "rand"
+
+description = [[
+Enumerates the installed Drupal modules/themes by using a list of known modules and themes.
+
+The script works by iterating over module/theme names and requesting
+MODULE_PATH/MODULE_NAME/LICENSE.txt for modules and THEME_PATH/THEME_NAME/LICENSE.txt.
+MODULE_PATH/THEME_PATH which is either provided by the user, grepped for in the html body
+or defaulting to sites/all/modules/.
+
+If the response status code is 200, it means that the module/theme is installed. By
+default, the script checks for the top 100 modules/themes (by downloads), given the
+huge number of existing modules (~18k) and themes(~1.4k).
+
+If you want to update your themes or module list refer to the link below.
+
+* https://svn.nmap.org/nmap-exp/gyani/misc/drupal-update.py
+]]
+
+---
+-- @see http-vuln-cve2014-3704.nse
+--
+-- @args http-drupal-enum.root The base path. Defaults to <code>/</code>.
+-- @args http-drupal-enum.number Number of modules to check.
+-- Use this option with a number or "all" as an argument to test for all modules.
+-- Defaults to <code>100</code>.
+-- @args http-drupal-enum.modules_path Direct Path for Modules
+-- @args http-drupal-enum.themes_path Direct Path for Themes
+-- @args http-drupal-enum.type default all.choose between "themes" and "modules"
+--
+-- @usage nmap -p 80 --script http-drupal-enum <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-drupal-enum:
+-- | Themes:
+-- | adaptivetheme
+-- | Modules:
+-- | views
+-- | token
+-- | ctools
+-- | pathauto
+-- | date
+-- | imce
+-- |_ webform
+--
+-- Final times for host: srtt: 329644 rttvar: 185712 to: 1072492
+--
+-- @xmloutput
+-- <table key="Themes">
+-- <elem>adaptivetheme</elem>
+-- </table>
+-- <table key="Modules">
+-- <elem>views</elem>
+-- <elem>token</elem>
+-- <elem>ctools</elem>
+-- <elem>pathauto</elem>
+-- <elem>date</elem>
+-- <elem>imce</elem>
+-- <elem>webform</elem>
+-- </table>
+
+
+author = {
+ "Hani Benhabiles",
+ "Gyanendra Mishra",
+}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {
+ "discovery",
+ "intrusive",
+}
+
+local DEFAULT_SEARCH_LIMIT = 100
+local DEFAULT_MODULES_PATH = 'sites/all/modules/'
+local DEFAULT_THEMES_PATH = 'sites/all/themes/'
+local IDENTIFICATION_STRING = "GNU GENERAL PUBLIC LICENSE"
+
+portrule = shortport.http
+
+--Reads database
+local function read_data (file)
+ return coroutine.wrap(function ()
+ for line in file:lines() do
+ if not line:match "^%s*#" and not line:match "^%s*$" then
+ coroutine.yield(line)
+ end
+ end
+ end)
+end
+
+--Checks if the module/theme file exists
+local function assign_file (act_file)
+ if not act_file then
+ return false
+ end
+ local temp_file = io.open(act_file, "r")
+ if not temp_file then
+ return false
+ end
+ return temp_file
+end
+
+--- Attempts to find modules path
+local get_path = function (host, port, root, type_of)
+ local default_path
+ if type_of == "themes" then
+ default_path = DEFAULT_THEMES_PATH
+ else
+ default_path = DEFAULT_MODULES_PATH
+ end
+ local body = http.get(host, port, root).body or ""
+ local pattern = "sites/[%w.-/]*/" .. type_of .. "/"
+ local found_path = body:match(pattern)
+ return found_path or default_path
+end
+
+
+function action (host, port)
+ local result = stdnse.output_table()
+ local file = {}
+ local all = {}
+ local requests = {}
+ local method = "HEAD"
+
+ --Read script arguments
+ local resource_type = stdnse.get_script_args(SCRIPT_NAME .. ".type") or "all"
+ local root = stdnse.get_script_args(SCRIPT_NAME .. ".root") or "/"
+ local search_limit = stdnse.get_script_args(SCRIPT_NAME .. ".number") or DEFAULT_SEARCH_LIMIT
+ local themes_path = stdnse.get_script_args(SCRIPT_NAME .. ".themes_path")
+ local modules_path = stdnse.get_script_args(SCRIPT_NAME .. ".modules_path")
+
+ local themes_file = nmap.fetchfile "nselib/data/drupal-themes.lst"
+ local modules_file = nmap.fetchfile "nselib/data/drupal-modules.lst"
+
+ if resource_type == "themes" or resource_type == "all" then
+ local theme_db = assign_file(themes_file)
+ if not theme_db then
+ return false, "Couldn't find drupal-themes.lst in /nselib/data/"
+ else
+ file['Themes'] = theme_db
+ end
+ end
+
+ if resource_type == "modules" or resource_type == "all" then
+ local modules_db = assign_file(modules_file)
+ if not modules_db then
+ return false, "Couldn't find drupal-modules.lst in /nselib/data/"
+ else
+ file['Modules'] = modules_db
+ end
+ end
+
+ if search_limit == "all" then
+ search_limit = nil
+ else
+ search_limit = tonumber(search_limit)
+ end
+
+ if not themes_path then
+ themes_path = (root .. get_path(host, port, root, "themes")):gsub("//", "/")
+ end
+ if not modules_path then
+ modules_path = (root .. get_path(host, port, root, "modules")):gsub("//", "/")
+ end
+
+ -- We default to HEAD requests unless the server returns
+ -- non 404 (200 or other) status code
+
+ local response = http.head(host, port, modules_path .. rand.random_alpha(8) .. "/LICENSE.txt")
+ if response.status ~= 404 then
+ method = "GET"
+ end
+
+ for key, value in pairs(file) do
+ local count = 0
+ for resource_name in read_data(value) do
+ count = count + 1
+ if search_limit and count > search_limit then
+ break
+ end
+ -- add request to pipeline
+ if key == "Modules" then
+ all = http.pipeline_add(modules_path .. resource_name .. "/LICENSE.txt", nil, all, method)
+ else
+ all = http.pipeline_add(themes_path .. resource_name .. "/LICENSE.txt", nil, all, method)
+ end
+ -- add to requests buffer
+ table.insert(requests, resource_name)
+ end
+
+ -- send requests
+ local pipeline_responses = http.pipeline_go(host, port, all)
+ if not pipeline_responses then
+ stdnse.print_debug(1, "No answers from pipelined requests")
+ return nil
+ end
+
+ for i, response in ipairs(pipeline_responses) do
+ -- Module exists if 200 on HEAD.
+ -- A lot Drupal of instances return 200 for all GET requests,
+ -- hence we check for the identifcation string.
+ if response.status == 200 and (method == "HEAD" or (method == "GET" and response.body:match(IDENTIFICATION_STRING))) then
+ result[key] = result[key] or {}
+ table.insert(result[key], requests[i])
+ end
+ end
+ requests = {}
+ all = {}
+ end
+
+ if result['Themes'] or result['Modules'] then
+ return result
+ else
+ if nmap.verbosity() > 1 then
+ return string.format("Nothing found amongst the top %s resources," .. "use --script-args number=<number|all> for deeper analysis)", search_limit)
+ else
+ return nil
+ end
+ end
+
+end