1local http = require "http"
2local shortport = require "shortport"
3local stdnse = require "stdnse"
4local string = require "string"
5local ls = require "ls"
6local have_ssl, openssl = pcall(require,'openssl')
7
8description = [[
9Shows the content of an "index" Web page.
10
11TODO:
12  - add support for more page formats
13]]
14
15author = "Pierre Lalet"
16license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
17categories = {"default", "discovery", "safe"}
18
19---
20-- @usage
21-- nmap -n -p 80 --script http-ls test-debit.free.fr
22--
23-- @args http-ls.checksum compute a checksum for each listed file. Requires OpenSSL.
24--       (default: false)
25-- @args http-ls.url base URL path to use (default: /)
26--
27-- @output
28-- PORT   STATE SERVICE
29-- 80/tcp open  http
30-- | http-ls:
31-- | Volume /
32-- | maxfiles limit reached (10)
33-- | SIZE        TIME               FILENAME
34-- | 524288      02-Oct-2013 18:26  512.rnd
35-- | 1048576     02-Oct-2013 18:26  1024.rnd
36-- | 2097152     02-Oct-2013 18:26  2048.rnd
37-- | 4194304     02-Oct-2013 18:26  4096.rnd
38-- | 8388608     02-Oct-2013 18:26  8192.rnd
39-- | 16777216    02-Oct-2013 18:26  16384.rnd
40-- | 33554432    02-Oct-2013 18:26  32768.rnd
41-- | 67108864    02-Oct-2013 18:26  65536.rnd
42-- | 1073741824  03-Oct-2013 16:46  1048576.rnd
43-- | 188         03-Oct-2013 17:15  README.html
44-- |_
45--
46-- @xmloutput
47-- <table key="volumes">
48--   <table>
49--     <elem key="volume">/</elem>
50--     <table key="files">
51--       <table>
52--         <elem key="size">524288</elem>
53--         <elem key="time">02-Oct-2013 18:26</elem>
54--         <elem key="filename">512.rnd</elem>
55--       </table>
56--       <table>
57--         <elem key="size">1048576</elem>
58--         <elem key="time">02-Oct-2013 18:26</elem>
59--         <elem key="filename">1024.rnd</elem>
60--       </table>
61--       <table>
62--         <elem key="size">2097152</elem>
63--         <elem key="time">02-Oct-2013 18:26</elem>
64--         <elem key="filename">2048.rnd</elem>
65--       </table>
66--       <table>
67--         <elem key="size">4194304</elem>
68--         <elem key="time">02-Oct-2013 18:26</elem>
69--         <elem key="filename">4096.rnd</elem>
70--       </table>
71--       <table>
72--         <elem key="size">8388608</elem>
73--         <elem key="time">02-Oct-2013 18:26</elem>
74--         <elem key="filename">8192.rnd</elem>
75--       </table>
76--       <table>
77--         <elem key="size">16777216</elem>
78--         <elem key="time">02-Oct-2013 18:26</elem>
79--         <elem key="filename">16384.rnd</elem>
80--       </table>
81--       <table>
82--         <elem key="size">33554432</elem>
83--         <elem key="time">02-Oct-2013 18:26</elem>
84--         <elem key="filename">32768.rnd</elem>
85--       </table>
86--       <table>
87--         <elem key="size">67108864</elem>
88--         <elem key="time">02-Oct-2013 18:26</elem>
89--         <elem key="filename">65536.rnd</elem>
90--       </table>
91--       <table>
92--         <elem key="size">1073741824</elem>
93--         <elem key="time">03-Oct-2013 16:46</elem>
94--         <elem key="filename">1048576.rnd</elem>
95--       </table>
96--       <table>
97--         <elem key="size">188</elem>
98--         <elem key="time">03-Oct-2013 17:15</elem>
99--         <elem key="filename">README.html</elem>
100--       </table>
101--     </table>
102--     <table key="info">
103--       <elem>maxfiles limit reached (10)</elem>
104--     </table>
105--   </table>
106-- </table>
107-- <table key="total">
108--   <elem key="files">10</elem>
109--   <elem key="bytes">1207435452</elem>
110-- </table>
111
112portrule = shortport.http
113
114local function isdir(fname, size)
115  -- we consider a file is (probably) a directory if its name
116  -- terminates with a '/' or if the string representing its size is
117  -- either empty or a single dash ('-').
118  if string.sub(fname, -1, -1) == '/' then
119    return true
120  end
121  if size == '' or size == '-' then
122    return true
123  end
124  return false
125end
126
127local function list_files(host, port, url, output, maxdepth, basedir)
128  basedir = basedir or ""
129
130  local resp = http.get(host, port, url)
131
132  if resp.location or not resp.body then
133    return true
134  end
135
136  if not string.match(resp.body, "<[Tt][Ii][Tt][Ll][Ee][^>]*> *[Ii][Nn][Dd][Ee][Xx] +[Oo][Ff]") then
137    return true
138  end
139
140  local patterns = {
141    '<[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]>',
142    '<[Aa] [Hh][Rr][Ee][Ff]="([^"]+)">[^<]+</[Aa]> *([0-9]+-[A-Za-z0-9]+-[0-9]+ [0-9]+:[0-9]+) *([^ \r\n]+)',
143  }
144  for _, pattern in ipairs(patterns) do
145    for fname, date, size in string.gmatch(resp.body, pattern) do
146      local continue = true
147      local directory = isdir(fname, size)
148      if have_ssl and ls.config('checksum') and not directory then
149        local checksum = ""
150        local resp = http.get(host, port, url .. fname)
151        if not resp.location and resp.body then
152          checksum = stdnse.tohex(openssl.sha1(resp.body))
153        end
154        continue = ls.add_file(output, {size, date, basedir .. fname, checksum})
155      else
156        continue = ls.add_file(output, {size, date, basedir .. fname})
157      end
158      if not continue then
159        return false
160      end
161      if directory then
162        if string.sub(fname, -1, -1) ~= "/" then fname = fname .. '/' end
163        continue = true
164        if maxdepth > 0 then
165          continue = list_files(host, port, url .. fname, output, maxdepth - 1,
166            basedir .. fname)
167        elseif maxdepth < 0 then
168          continue = list_files(host, port, url .. fname, output, -1,
169            basedir .. fname)
170        end
171        if not continue then
172          return false
173        end
174      end
175    end
176  end
177  return true
178end
179
180action = function(host, port)
181  local url = stdnse.get_script_args(SCRIPT_NAME .. '.url') or "/"
182
183  local output = ls.new_listing()
184  ls.new_vol(output, url, false)
185  local continue = list_files(host, port, url, output, ls.config('maxdepth'))
186  if not continue then
187    ls.report_info(
188      output,
189      string.format("maxfiles limit reached (%d)", ls.config('maxfiles')))
190  end
191  ls.end_vol(output)
192  return ls.end_listing(output)
193end
194