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 /. -- @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 100. -- @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 -- -- @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 -- -- adaptivetheme --
-- -- views -- token -- ctools -- pathauto -- date -- imce -- webform --
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= for deeper analysis)", search_limit) else return nil end end end