Nmap Development mailing list archives

[NSE] apache-userdir-enum


From: jah <jah () zadkiel plus com>
Date: Sun, 12 Jul 2009 23:37:06 +0100

Hi folks,

Attached is a little script that checks for HTTP 200 or 403 responses
from HTTP requests for /~some_user/ in attempt to enumerate some valid
usernames where apache mod_userdir is enabled.
OpenVAS (written for Nessus in 2001 [2]) has a similar script [1], but
this one goes the extra mile to try and limit false positives by testing
for a directory which is highly unlikely to exist, before it starts
testing proper.
Although this is a well-known and low risk issue [3], it's still quite
useful sometimes and occasionally one might come across something nice -
like the following host:

Interesting ports on X.X.X.101:
PORT    STATE SERVICE REASON  VERSION
80/tcp  open  http    syn-ack Apache httpd 2.2.11
|_ apache-userdir-enum: Valid Users: root (403), netadmin (200)

When I visited http://X.X.X.101/~netadmin/ I was presented with a
phpSysInfo [4] page which was a treasure trove of interesting info about
the host, with headings such as System Vital, Hardware Information,
Network Usage, Memory Usage and Mounted Filesystems.

The script uses nselib/data/usernames.lst which contains 11 common
usernames and you can supply --script-args apacheuserdir.u= and an
alternative list of usernames.

Worth inclusion?

Regards,

jah

[1]
https://svn.wald.intevation.org/svn/openvas/trunk/openvas-plugins/scripts/apache_username.nasl
[2] http://www.nessus.org/plugins/index.php?view=single&id=10766
<http://www.nessus.org/plugins/index.php?view=single&id=10766>
[3] http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2001-1013
[4] http://phpsysinfo.sourceforge.net/


author = "jah <jah () zadkiel plus com>"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html";
categories = {"discovery"}
description = [[
Attempts to enumerate valid usernames on an apcahe server running with mod_userdir.

The Apache mod_userdir module allows user-specific directories to be accessed using the http://example.com/~user/ 
syntax.
This script makes http requests in order to discover valid user-specific directories and infer valid usernames.
By default, the script will use Nmaps nselib/data/usernames.lst
An http response status of 200 or 403 means the username is likely a valid one and the username will be output in the
script results along with the status code (in parentheses).

This script makes an attempt to avoid false positives by requesting a directory which is unlikely to exist.  If the
server responds with 200 or 403 then the script will not continue testing it.

Refs: CVE-2001-1013 - http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2001-1013
]]

---
-- @args
-- u=path/to/custom/usernames.list or
-- apacheuserdir.u=path/to/custom/usernames.list
--
-- @output
-- 80/tcp open  http    syn-ack Apache httpd 2.2.9
-- |_ apache-userdir-enum: Valid Users: root (403), user (200), test (200)

local http      = require 'http'
local stdnse    = require 'stdnse'
local datafiles = require 'datafiles'



---
-- The script will run against http[s] and http[s]-alt ports.  If version detection
-- is performed and discovers a service which does not contain 'apache' the script
-- will not run.
portrule = function(host, port)
  local svc = { std = { ["http"] = 1, ["http-alt"] = 1 },
                ssl = { ["https"] = 1, ["https-alt"] = 1 } }
  if port.protocol ~= 'tcp' or not
  ( svc.std[port.service] or svc.ssl[port.service] ) then
    return false
  end
  -- Don't bother running on SSL ports if we don't have SSL.
  if (svc.ssl[port.service] or port.version.service_tunnel == 'ssl') and not
  nmap.have_ssl() then
    return false
  end
  -- Don't bother running if version detection says something other than Apache
  if port.version and type(port.version.product) == 'string' and not
  port.version.product:lower():match('apache') then -- TODO also matches tomcat coyote - has mod_userdir or similar?
    return false
  end
  return true
end



action = function(host, port)

  if not nmap.registry.apacheuserdir then init() end
  local usernames = nmap.registry.apacheuserdir
  if #usernames == 0 then return nil end  -- speedy exit if no usernames

  local filename = filename:match( "[\\/]([^\\/]+)\.nse$" )
  local found = {}
  local data

  for i, uname in ipairs(usernames) do

    data = http.get( host, port, ("/~%s/"):format(uname) )
    if data and type(data.status) == 'number' then
      if (data.status == 403 or data.status == 200) and i == 1 then
        -- This server is unlikely to yield useful information since it returned
        -- 200 or 403 to a request for a directory which is highly unlikely to exist.
        stdnse.print_debug( 1, "%s detected false positives at %s:%d - status was %d", filename, host.ip, port.number, 
data.status)
        break
      elseif data.status == 403 or data.status == 200 then
        found[#found+1] = ("%s (%d)"):format(uname, data.status)
      -- else we didn't get an interesting status code
      end
    else
      stdnse.print_debug( 2, "%s got a bad or zero response from %s:%d", filename, host.ip, port.number)
    end

  end

  if #found == 0 then
    stdnse.print_debug( 2, "%s found Zero users at %s:%d", filename, host.ip, port.number)
    return nil
  end

  return ("Valid Users: %s"):format(table.concat(found, ", "))

end



---
-- Parses a file containing usernames (1 per line), defaulting to "nselib/data/usernames.lst"
-- and stores the resulting array of usernames in the registry for use by all threads of this
-- script.  This means file access is done only once per Nmap invocation.
-- init() also adds a random string to the array (in the first position) to attempt to catch
-- false positives.
-- @return nil

function init()
  local filename = filename and filename:match( "[\\/]([^\\/]+)\.nse$" ) or ""
  local customlist = nmap.registry.args.u or
    (nmap.registry.args.apacheuserdir and nmap.registry.args.apacheuserdir.u) or
    nmap.registry.args['apacheuserdir.u']
  local read, usernames = datafiles.parse_file(customlist or "nselib/data/usernames.lst", {})
  if not read then
    stdnse.print_debug(1, "%s %s %s", filename, usernames and "" or "had issues reading usernames:",
      usernames or "No Error message")
    nmap.registry.apacheuserdir = {}
    return nil
  end
  -- random dummy username to catch false positives
  if #usernames > 0 then table.insert(usernames, 1, randomstring()) end
  nmap.registry.apacheuserdir = usernames
  stdnse.print_debug(1, "%s Testing %d usernames.", filename, #usernames)
  return nil
end



---
-- Uses openssl.rand_pseudo_bytes (if available, os.time() if not) and base64.enc
-- to produce a randomish string of at least 11 alphanumeric chars.
-- @return String

function randomstring()
  local bin    = require"bin"
  local base64 = require"base64"
  local rnd, s, l, _
  if pcall(require, "openssl") then
    rnd = openssl.rand_pseudo_bytes
  end

  s = rnd and rnd(8) or tostring( os.time() )
  -- increase the length of the string by 0 to 7 chars
  _, l = bin.unpack(">C", s, 8) -- eighth byte should be safe for os.time() too
  s = l%8 > 0 and s .. s:sub(1,l%8) or s
  -- base 64 encode and replace any non alphanum chars (with 'n' for nmap!)
  s = base64.enc(s):sub(1,-2):gsub("%W", "n")
  return s
end

_______________________________________________
Sent through the nmap-dev mailing list
http://cgi.insecure.org/mailman/listinfo/nmap-dev
Archived at http://SecLists.Org

Current thread: