mirror of
https://github.com/nmap/nmap.git
synced 2025-12-06 20:51:30 +00:00
the version that checked for LOGO.png instead of LICENSE.txt. Some Drupal instances returns 200 for all GET Requests, hence the check for the identification string was necessary.
230 lines
6.7 KiB
Lua
230 lines
6.7 KiB
Lua
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
|
|
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
|
|
]]
|
|
|
|
---
|
|
-- @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 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)
|
|
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 .. stdnse.generate_random_string(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 |