diff --git a/CHANGELOG b/CHANGELOG
index d417f617d..1cd6ec17c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
# Nmap Changelog ($Id$); -*-text-*-
+o [NSE] Added script http-ls. Parses web server directory index pages with
+ optional recursion. [Pierre Lalet]
+
o [NSE] [GH#106] Added a new NSE module, ls.lua, for accumulating and
outputting file and directory listings. The afp-ls, nfs-ls, and smb-ls
scripts have been converted to use this module. [Pierre Lalet]
diff --git a/scripts/http-ls.nse b/scripts/http-ls.nse
new file mode 100644
index 000000000..17c36cc53
--- /dev/null
+++ b/scripts/http-ls.nse
@@ -0,0 +1,193 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local openssl = require "openssl"
+local ls = require "ls"
+
+description = [[
+Shows the content of an "index" Web page.
+
+TODO:
+ - add support for more page formats
+]]
+
+author = "Pierre Lalet"
+license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+---
+-- @usage
+-- nmap -n -p 80 --script http-ls test-debit.free.fr
+--
+-- @args http-ls.checksum compute a checksum for each listed file
+-- (default: false)
+-- @args http-ls.url base URL path to use (default: /)
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-ls:
+-- | Volume /
+-- | maxfiles limit reached (10)
+-- | SIZE TIME FILENAME
+-- | 524288 02-Oct-2013 18:26 512.rnd
+-- | 1048576 02-Oct-2013 18:26 1024.rnd
+-- | 2097152 02-Oct-2013 18:26 2048.rnd
+-- | 4194304 02-Oct-2013 18:26 4096.rnd
+-- | 8388608 02-Oct-2013 18:26 8192.rnd
+-- | 16777216 02-Oct-2013 18:26 16384.rnd
+-- | 33554432 02-Oct-2013 18:26 32768.rnd
+-- | 67108864 02-Oct-2013 18:26 65536.rnd
+-- | 1073741824 03-Oct-2013 16:46 1048576.rnd
+-- | 188 03-Oct-2013 17:15 README.html
+-- |_
+--
+-- @xmloutput
+--
+--
+-- /
+--
+--
+-- 524288
+-- 02-Oct-2013 18:26
+-- 512.rnd
+--
+--
+-- 1048576
+-- 02-Oct-2013 18:26
+-- 1024.rnd
+--
+--
+-- 2097152
+-- 02-Oct-2013 18:26
+-- 2048.rnd
+--
+--
+-- 4194304
+-- 02-Oct-2013 18:26
+-- 4096.rnd
+--
+--
+-- 8388608
+-- 02-Oct-2013 18:26
+-- 8192.rnd
+--
+--
+-- 16777216
+-- 02-Oct-2013 18:26
+-- 16384.rnd
+--
+--
+-- 33554432
+-- 02-Oct-2013 18:26
+-- 32768.rnd
+--
+--
+-- 67108864
+-- 02-Oct-2013 18:26
+-- 65536.rnd
+--
+--
+-- 1073741824
+-- 03-Oct-2013 16:46
+-- 1048576.rnd
+--
+--
+-- 188
+-- 03-Oct-2013 17:15
+-- README.html
+--
+--
+--
+-- maxfiles limit reached (10)
+--
+--
+--
+--
+-- 10
+-- 1207435452
+--
+
+portrule = shortport.http
+
+local function isdir(fname, size)
+ -- we consider a file is (probably) a directory if its name
+ -- terminates with a '/' or if the string representing its size is
+ -- either empty or a single dash ('-').
+ if string.sub(fname, -1, -1) == '/' then
+ return true
+ end
+ if size == '' or size == '-' then
+ return true
+ end
+ return false
+end
+
+local function list_files(host, port, url, output, maxdepth, basedir)
+ basedir = basedir or ""
+
+ local resp = http.get(host, port, url)
+
+ if resp.location or not resp.body then
+ return true
+ end
+
+ if not string.match(resp.body, "<[Tt][Ii][Tt][Ll][Ee][^>]*> *[Ii][Nn][Dd][Ee][Xx] +[Oo][Ff]") then
+ return true
+ end
+
+ local patterns = {
+ '<[Aa] [Hh][Rr][Ee][Ff]="([^"]+)">[^<]+[Aa]>[Tt][Dd]><[Tt][Dd][^>]*> *([0-9]+-[A-Za-z0-9]+-[0-9]+ [0-9]+:[0-9]+) *[Tt][Dd]><[Tt][Dd][^>]*> *([^<]+)[Tt][Dd]>',
+ '<[Aa] [Hh][Rr][Ee][Ff]="([^"]+)">[^<]+[Aa]> *([0-9]+-[A-Za-z0-9]+-[0-9]+ [0-9]+:[0-9]+) *([^ \n]+)',
+ }
+ for _, pattern in ipairs(patterns) do
+ for fname, date, size in string.gmatch(resp.body, pattern) do
+ local continue = true
+ local directory = isdir(fname, size)
+ if ls.config('checksum') and not directory then
+ local checksum = ""
+ local resp = http.get(host, port, url .. fname)
+ if not resp.location and resp.body then
+ checksum = stdnse.tohex(openssl.sha1(resp.body))
+ end
+ continue = ls.add_file(output, {size, date, basedir .. fname, checksum})
+ else
+ continue = ls.add_file(output, {size, date, basedir .. fname})
+ end
+ if not continue then
+ return false
+ end
+ if directory then
+ if string.sub(fname, -1, -1) ~= "/" then fname = fname .. '/' end
+ continue = true
+ if maxdepth > 0 then
+ continue = list_files(host, port, url .. fname, output, maxdepth - 1,
+ basedir .. fname)
+ elseif maxdepth < 0 then
+ continue = list_files(host, port, url .. fname, output, -1,
+ basedir .. fname)
+ end
+ if not continue then
+ return false
+ end
+ end
+ end
+ end
+ return true
+end
+
+action = function(host, port)
+ local url = stdnse.get_script_args(SCRIPT_NAME .. '.url') or "/"
+
+ local output = ls.new_listing()
+ ls.new_vol(output, url, false)
+ local continue = list_files(host, port, url, output, ls.config('maxdepth'))
+ if not continue then
+ ls.report_info(
+ output,
+ string.format("maxfiles limit reached (%d)", ls.config('maxfiles')))
+ end
+ ls.end_vol(output)
+ return ls.end_listing(output)
+end
diff --git a/scripts/script.db b/scripts/script.db
index ba5ec94f2..3398774f2 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -161,8 +161,8 @@ Entry { filename = "http-devframework.nse", categories = { "discovery", "intrusi
Entry { filename = "http-dlink-backdoor.nse", categories = { "exploit", "vuln", } }
Entry { filename = "http-dombased-xss.nse", categories = { "exploit", "intrusive", "vuln", } }
Entry { filename = "http-domino-enum-passwords.nse", categories = { "auth", "intrusive", } }
-Entry { filename = "http-drupal-enum.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "http-drupal-enum-users.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-drupal-enum.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "http-enum.nse", categories = { "discovery", "intrusive", "vuln", } }
Entry { filename = "http-errors.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "http-exif-spider.nse", categories = { "intrusive", } }
@@ -186,6 +186,7 @@ Entry { filename = "http-iis-short-name-brute.nse", categories = { "brute", "int
Entry { filename = "http-iis-webdav-vuln.nse", categories = { "intrusive", "vuln", } }
Entry { filename = "http-joomla-brute.nse", categories = { "brute", "intrusive", } }
Entry { filename = "http-litespeed-sourcecode-download.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-ls.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "http-majordomo2-dir-traversal.nse", categories = { "exploit", "intrusive", "vuln", } }
Entry { filename = "http-malware-host.nse", categories = { "malware", "safe", } }
Entry { filename = "http-method-tamper.nse", categories = { "auth", "vuln", } }
@@ -495,7 +496,7 @@ Entry { filename = "whois-domain.nse", categories = { "discovery", "external", "
Entry { filename = "whois-ip.nse", categories = { "discovery", "external", "safe", } }
Entry { filename = "wsdd-discover.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "x11-access.nse", categories = { "auth", "default", "safe", } }
-Entry { filename = "xmlrpc-methods.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "xdmcp-discover.nse", categories = { "discovery", "safe", } }
+Entry { filename = "xmlrpc-methods.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "xmpp-brute.nse", categories = { "brute", "intrusive", } }
Entry { filename = "xmpp-info.nse", categories = { "default", "discovery", "safe", "version", } }