From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- scripts/http-wordpress-enum.nse | 299 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 scripts/http-wordpress-enum.nse (limited to 'scripts/http-wordpress-enum.nse') diff --git a/scripts/http-wordpress-enum.nse b/scripts/http-wordpress-enum.nse new file mode 100644 index 0000000..35d7a2a --- /dev/null +++ b/scripts/http-wordpress-enum.nse @@ -0,0 +1,299 @@ +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 themes and plugins of Wordpress installations. The script can also detect + outdated plugins by comparing version numbers with information pulled from api.wordpress.org. + +The script works with two separate databases for themes (wp-themes.lst) and plugins (wp-plugins.lst). +The databases are sorted by popularity and the script will search only the top 100 entries by default. +The theme database has around 32,000 entries while the plugin database has around 14,000 entries. + +The script determines the version number of a plugin by looking at the readme.txt file inside the plugin +directory and it uses the file style.css inside a theme directory to determine the theme version. +If the script argument check-latest is set to true, the script will query api.wordpress.org to obtain +the latest version number available. This check is disabled by default since it queries an external service. + +This script is a combination of http-wordpress-plugins.nse and http-wordpress-themes.nse originally +submited by Ange Gutek and Peter Hill. + +TODO: +-Implement version checking for themes. +]] + +--- +-- @see http-vuln-cve2014-8877.nse +-- +-- @usage nmap -sV --script http-wordpress-enum +-- @usage nmap --script http-wordpress-enum --script-args check-latest=true,search-limit=10 +-- @usage nmap --script http-wordpress-enum --script-args type="themes" +-- +-- @args http-wordpress-enum.root Base path. By default the script will try to find a WP directory +-- installation or fall back to '/'. +-- @args http-wordpress-enum.search-limit Number of entries or the string "all". Default:100. +-- @args http-wordpress-enum.type Search type. Available options:plugins, themes or all. Default:all. +-- @args http-wordpress-enum.check-latest Retrieves latest plugin version information from wordpress.org. +-- Default:false. +-- +-- @output +-- PORT STATE SERVICE +-- 80/tcp open http +-- | http-wordpress-enum: +-- | Search limited to top 100 themes/plugins +-- | plugins +-- | akismet +-- | contact-form-7 4.1 (latest version:4.1) +-- | all-in-one-seo-pack (latest version:2.2.5.1) +-- | google-sitemap-generator 4.0.7.1 (latest version:4.0.8) +-- | jetpack 3.3 (latest version:3.3) +-- | wordfence 5.3.6 (latest version:5.3.6) +-- | better-wp-security 4.6.4 (latest version:4.6.6) +-- | google-analytics-for-wordpress 5.3 (latest version:5.3) +-- | themes +-- | twentytwelve +-- |_ twentyfourteen +-- +-- @xmloutput +-- +-- 5.1 +-- 5.3 +-- google-analytics-for-wordpress +-- /wp-content/plugins/google-analytics-for-wordpress/ +-- plugins +--
+-- +-- themes +-- /wp-content/themes/twentytwelve/ +-- twentytwelve +--
+-- Search limited to top 100 themes/plugins +--- + +author = {"Ange Gutek", "Peter Hill", "Gyanendra Mishra", "Paulino Calderon"} + +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" + +categories = {"discovery", "intrusive"} + +local DEFAULT_SEARCH_LIMIT = 100 +local DEFAULT_PLUGINS_PATH = '/wp-content/plugins/' +local WORDPRESS_API_URL = 'http://api.wordpress.org/plugins/info/1.0/' + +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 plugin/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 + +--Obtains version from readme.txt or style.css +local function get_version(path, typeof, host, port) + local pattern, version, versioncheck + + if typeof == 'plugins' then + path = path .. "readme.txt" + pattern = 'Stable tag: ([.0-9]*)' + else + path = path .. "style.css" + pattern = 'Version: ([.0-9]*)' + end + + stdnse.debug1("Extracting version of path:%s", path) + versioncheck = http.get(host, port, path) + if versioncheck.body then + version = versioncheck.body:match(pattern) + end + stdnse.debug1("Version found: %s", version) + return version +end + +-- check if the plugin is the latest +local function get_latest_plugin_version(plugin) + stdnse.debug1("Retrieving the latest version of %s", plugin) + local apiurl = WORDPRESS_API_URL .. plugin .. ".json" + local latestpluginapi = http.get('api.wordpress.org', '80', apiurl) + local latestpluginpattern = '","version":"([.0-9]*)' + local latestpluginversion = latestpluginapi.body:match(latestpluginpattern) + stdnse.debug1("Latest version:%s", latestpluginversion) + return latestpluginversion +end + +action = function(host, port) + + local result = {} + local file = {} + local all = {} + local bfqueries = {} + local wp_autoroot + local output_table = stdnse.output_table() + + --Read script arguments + local operation_type_arg = stdnse.get_script_args(SCRIPT_NAME .. ".type") or "all" + local apicheck = stdnse.get_script_args(SCRIPT_NAME .. ".check-latest") + local wp_root = stdnse.get_script_args(SCRIPT_NAME .. ".root") + local resource_search_arg = stdnse.get_script_args(SCRIPT_NAME .. ".search-limit") or DEFAULT_SEARCH_LIMIT + + local wp_themes_file = nmap.fetchfile("nselib/data/wp-themes.lst") + local wp_plugins_file = nmap.fetchfile("nselib/data/wp-plugins.lst") + + if operation_type_arg == "themes" or operation_type_arg == "all" then + local theme_db = existence_check_assign(wp_themes_file) + if not theme_db then + return false, "Couldn't find wp-themes.lst in /nselib/data/" + else + file['themes'] = theme_db + end + end + if operation_type_arg == "plugins" or operation_type_arg == "all" then + local plugin_db = existence_check_assign(wp_plugins_file) + if not plugin_db then + return false, "Couldn't find wp-plugins.lst in /nselib/data/" + else + file['plugins'] = plugin_db + end + end + + local resource_search + if resource_search_arg == "all" then + resource_search = nil + else + resource_search = tonumber(resource_search_arg) + end + + -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests + local status_404, result_404, known_404 = http.identify_404(host,port) + if ( status_404 and result_404 == 200 ) then + stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number) + return nil + end + + -- search the website root for evidences of a Wordpress path + if not wp_root then + local target_index = http.get(host,port, "/") + + if target_index.status and target_index.body then + wp_autoroot = string.match(target_index.body, "http://[%w%-%.]-/([%w%-%./]-)wp%-content") + if wp_autoroot then + wp_autoroot = "/" .. wp_autoroot + stdnse.debug(1,"WP root directory: %s", wp_autoroot) + else + stdnse.debug(1,"WP root directory: wp_autoroot was unable to find a WP content dir (root page returns %d).", target_index.status) + end + end + end + + --build a table of both directories to brute force and the corresponding WP resources' name + local resource_count=0 + for key,value in pairs(file) do + local l_file = value + resource_count = 0 + for line in read_data_file(l_file) do + if resource_search and resource_count >= resource_search then + break + end + + local target + if wp_root then + -- Give user-supplied argument the priority + target = wp_root .. string.gsub(DEFAULT_PLUGINS_PATH, "plugins", key) .. line .. "/" + elseif wp_autoroot then + -- Maybe the script has discovered another Wordpress content directory + target = wp_autoroot .. string.gsub(DEFAULT_PLUGINS_PATH, "plugins", key) .. line .. "/" + else + -- Default WP directory is root + target = string.gsub(DEFAULT_PLUGINS_PATH, "plugins", key) .. line .. "/" + end + + + target = string.gsub(target, "//", "/") + table.insert(bfqueries, {target, line}) + all = http.pipeline_add(target, nil, all, "GET") + resource_count = resource_count + 1 + + end + -- release hell... + local pipeline_returns = http.pipeline_go(host, port, all) + if not pipeline_returns then + stdnse.verbose1("got no answers from pipelined queries") + return nil + end + local response = {} + response['name'] = key + for i, data in pairs(pipeline_returns) do + -- if it's not a four-'o-four, it probably means that the plugin is present + if http.page_exists(data, result_404, known_404, bfqueries[i][1], true) then + stdnse.debug(1,"Found a plugin/theme:%s", bfqueries[i][2]) + local version = get_version(bfqueries[i][1],key,host,port) + local output = nil + + --We format the table for XML output + bfqueries[i].path = bfqueries[i][1] + bfqueries[i].category = key + bfqueries[i].name = bfqueries[i][2] + bfqueries[i][1] = nil + bfqueries[i][2] = nil + + if version then + output = bfqueries[i].name .." ".. version + bfqueries[i].installation_version = version + --Right now we can only get the version number of plugins through api.wordpress.org + if apicheck == "true" and key=="plugins" then + local latestversion = get_latest_plugin_version(bfqueries[i].name) + if latestversion then + output = output .. " (latest version:" .. latestversion .. ")" + bfqueries[i].latest_version = latestversion + end + end + else + output = bfqueries[i].name + end + output_table[bfqueries[i].name] = bfqueries[i] + table.insert(response, output) + end + end + table.insert(result, response) + bfqueries={} + all = {} + +end + local len = 0 + for i, v in ipairs(result) do len = len >= #v and len or #v end + if len > 0 then + output_table.title = string.format("Search limited to top %s themes/plugins", resource_count) + result.name = output_table.title + return output_table, 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_count) + else + return nil + end + end + +end + -- cgit v1.2.3