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"
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
MODULES_PATH/MODULE_NAME/LICENSE.txt same for theme except logo.png is searched for. MODULES_PATH 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 (by downloads), given the
huge number of existing modules (~10k).
]]
---
-- @args http-drupal-enum.root The base path. Defaults to /.
-- @args http-drupal-enum.search-limit 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.direct_path_modules Direct Path for Modules
-- @args http-drupal-enum.direct_path_themes Direct Path for Themes
-- @args http-drupal-enum.type default all.choose between "themes" and "modules"
-- @usage
-- nmap -p 80 --script http-drupal-enum --script-args direct_path_modules="sites/all/modules/",direct_path_themes="themes/",search-limit=10
--
--
--@output
-- PORT STATE SERVICE REASON
-- 80/tcp open http syn-ack
-- | http-drupal-enum:
-- | Search limited to top 10 themes/modules
-- | modules
-- | ckeditor
-- | views
-- | token
-- | pathauto
-- | cck
-- | admin_menu
-- | themes
-- |_ theme470
-- Final times for host: srtt: 329644 rttvar: 185712 to: 1072492
-- TODO version checking
-- TODO xml-output
-- TODO better paths as some use /contrib and /customs. need to search deeper.
author = {
"Hani Benhabiles",
"Gyanendra Mishra",
}
license = "Same as Nmap--See http://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 (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 existence_check_assign (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
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 = {}
local file = {}
local all = {}
local requests = {}
local drupal_autoroot
local method = "HEAD"
--Read script arguments
local operation_type_arg = stdnse.get_script_args(SCRIPT_NAME .. ".type") or "all"
local root = stdnse.get_script_args(SCRIPT_NAME .. ".root") or "/"
local resource_search_arg = stdnse.get_script_args(SCRIPT_NAME .. ".search-limit") or DEFAULT_SEARCH_LIMIT
local direct_path_themes = stdnse.get_script_args(SCRIPT_NAME .. ".direct_path_themes")
local direct_path_modules = stdnse.get_script_args(SCRIPT_NAME .. ".direct_path_modules")
local drupal_themes_file = nmap.fetchfile "nselib/data/drupal-themes.lst"
local drupal_modules_file = nmap.fetchfile "nselib/data/drupal-modules.lst"
if operation_type_arg == "themes" or operation_type_arg == "all" then
local theme_db = existence_check_assign(drupal_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 operation_type_arg == "modules" or operation_type_arg == "all" then
local modules_db = existence_check_assign(drupal_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
local resource_search
if resource_search_arg == "all" then
resource_search = nil
else
resource_search = tonumber(resource_search_arg)
end
-- search the website root for evidences of a Drupal path
local theme_path = direct_path_themes
local module_path = direct_path_modules
if not direct_path_themes then
theme_path = get_path(host, port, root, "themes")
end
if not direct_path_modules then
module_path = get_path(host, port, root, "modules")
end
-- We default to HEAD requests unless the server returns
-- non 404 (200 or other) status code
local response = http.head(host, port, root .. module_path .. "randomaBcD/LICENSE.txt")
if response.status ~= 404 then
method = "GET"
end
for key, value in pairs(file) do
local temp_table = {}
temp_table['name'] = key
local count = 0
for obj_name in read_data_file(value) do
count = count + 1
if resource_search and count > resource_search then
break
end
-- add request to pipeline
if key == "modules" then
all = http.pipeline_add(root .. module_path .. obj_name .. "/LICENSE.txt", nil, all, method)
else
all = http.pipeline_add(root .. theme_path .. obj_name .. "/logo.png", nil, all, method)
end
-- add to requests buffer
table.insert(requests, obj_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 pairs(pipeline_responses) do
-- Module exists if 200 on HEAD
-- or contains identification string for GET or key is themes and is image
if method == "HEAD" and response.status == 200 or method == "GET" and response.status == 200 and (string.match(response.body, IDENTIFICATION_STRING) or key == "themes") then
table.insert(temp_table, requests[i])
end
end
table.insert(result, temp_table)
requests = {}
all = {}
end
local len = 0
for i, v in ipairs(result) do
len = len >= #v and len or #v
end
if len > 0 then
result.name = string.format("Search limited to top %s themes/modules", resource_search)
return stdnse.format_output(true, result)
else
if nmap.verbosity() > 1 then
return string.format("Nothing found amongst the top %s resources," .. "use --script-args search-limit= for deeper analysis)", resource_search)
else
return nil
end
end
end