local http = require "http" local nmap = require "nmap" local shortport = require "shortport" local stdnse = require "stdnse" local string = require "string" local base64 = require "base64" local math = require "math" local table = require "table" description = [[ A script to check if a WebDAV installation has insecure permsisions. It's based on the script ideas page. *https://secwiki.org/w/Nmap/Script_Ideas# http-webdav This script takes inspiration from the various scripts listed here: *http://carnal0wnage.attackresearch.com/2010/05/more-with-metasploit-and-webdav.html *https://github.com/sussurro/Metasploit-Tools/blob/master/modules/auxiliary/scanner/http/webdav_test.rb *http://code.google.com/p/davtest/ ]] --- -- @usage -- nmap --script http-webdav -p80,8080 -- -- @args folder The folder to start in; eg, "/web" will try "/web/xxx". -- -- @output -- | http-webdav-perms: -- | Uploadable Files: -- | shtml -- | aspx -- | txt -- | pl -- | jhtml -- | jsp -- | asp -- | php -- | html -- | cfm -- | cgi -- | Executable Files: -- | txt -- | html -- | Renamable Files: -- | shtml -- | aspx -- | pl -- | jhtml -- | jsp -- | asp -- | php -- | html -- | cfm -- | cgi -- | Executable after rename: -- |_ html -- -- @xmloutput -- -- cgi -- cfm -- shtml -- jsp -- html -- aspx -- txt -- jhtml -- asp -- pl -- php --
-- -- html -- txt --
-- -- cgi -- cfm -- shtml -- jsp -- html -- aspx -- jhtml -- asp -- pl -- php --
-- -- html --
----------------------------------------------------------------------- author = "Gyanendra Mishra" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = { "exploit", "intrusive" } portrule = shortport.http local files = { ['asp'] = '<% response.write (!N1! * !N2!) %>', ['aspx'] = '<% response.write (!N1! * !N2!) %>', ['cfm'] = 'WriteOutput(!N1!*!N2!);', ['cgi'] = "#!/usr/bin/perl\nprint \"Content-Type: text/html\n\r\n\r\" . !N1! * !N2!;", ['html'] = '!S1!
', ['jhtml'] = '<%= System.out.println(!N1! * !N2!); %>', ['jsp'] = '<%= System.out.println(!N1! * !N2!); %>', ['php'] = '', ['pl'] = "#!/usr/bin/perl\nprint \"Content-Type: text/html\n\r\n\r\" . !N1! * !N2!;", ['shtml'] = '
', ['txt'] = '!S1!', } local jpg_file = base64.dec("/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAAA//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AR//Z") local function create_dir (host, port, dir) local options = { header = { ["Content-Length"] = 0, }, } local response = http.generic_request(host, port, 'MKCOL', dir, options) if response and response.status and response.status >= 200 and response.status <= 300 then return true end return false end local function delete_dir (host, port, dir) local options = { header = { ["Content-Length"] = 0, }, } local response = http.generic_request(host, port, "DELETE", dir, options) if response and response.status >= 200 and response.status <= 300 then return true end return false end local function check_extensions (host, port, path) local result = {} local all_put = {} local all_get = {} local answers = {} for extension, payload in pairs(files) do stdnse.debug2('Trying to upload extension %s', extension) local answer = nil local fname = stdnse.generate_random_string(15, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") local file_path = path .. "/" .. fname .. "." .. extension if payload:find '!N1!' then local n1 = math.random(10000) / 100 * 10 local n2 = math.random(10000) / 100 * 10 answer = tostring(n1 * n2) payload = payload:gsub('!N1', n1) payload = payload:gsub('!N2', n2) else answer = stdnse.generate_random_string(25, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") payload = payload:gsub('!S1!', answer) end answers[file_path] = answer payload = payload .. "\n\n" local options = { header = { ["Content-Length"] = payload:len(), }, timeout = 10, content = payload, } all_put = http.pipeline_add(file_path, options, all_put, "PUT") end local pipeline_returns = http.pipeline_go(host, port, all_put) if not pipeline_returns then stdnse.debug1("No response from pipelined queries, PUT method(check_extensions).") return result end for i, response in pairs(pipeline_returns) do if not response or response.status ~= 201 then table.insert(result, { extension, false, false, }) else all_get = http.pipeline_add(all_put[i].path, nil, all_get, "GET") end end pipeline_returns = http.pipeline_go(host, port, all_get) if not pipeline_returns then stdnse.debug1("No response from pipelined queries, GET method(check_extensions).") return result end for i, response in pairs(pipeline_returns) do if not response or response.status ~= 200 or not response.body:find(answers[all_get[i].path]) or response.body:find "#exec" then table.insert(result, { extension, true, false, }) else table.insert(result, { extension, true, true, }) end end return result end local function check_rename (host, port, path) local result = {} local destinations = {} local files_to_get = {} local all_put = {} local all_move = {} local all_get = {} local answers = {} for extension, payload in pairs(files) do stdnse.debug2("Trying to rename extension %s", extension) local answer = nil local fname = stdnse.generate_random_string(15, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") files_to_get[extension] = path .. "/" .. fname .. "." .. extension .. ';.jpg' local file_path = path .. "/" .. fname .. ".txt" file_path = file_path:gsub('//', '/') local file_path_move = nil if host.targetname then file_path_move = host.targetname .. "/" .. path .. "/" .. fname .. "." .. extension .. ';.jpg' else file_path_move = host.ip .. "/" .. path .. "/" .. fname .. "." .. extension .. ';.jpg' end file_path_move = 'http://' .. file_path_move:gsub('//', '/') table.insert(destinations, file_path_move) if payload:find '!N1!' then local n1 = math.random(10000) / 100 * 10 local n2 = math.random(10000) / 100 * 10 answer = tostring(n1 * n2) payload = payload:gsub('!N1', n1) payload = payload:gsub('!N2', n2) else answer = stdnse.generate_random_string(25, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") payload = payload:gsub('!S1!', answer) end answers[files_to_get] = answer payload = jpg_file .. payload .. '\n\n' local options = { header = { ["Content-Length"] = payload:len(), }, timeout = 5, content = payload, } all_put = http.put(file_path, options, all_put, "PUT") end local pipeline_returns = http.pipeline_go(host, port, all_put) if not pipeline_returns then stdnse.debug1("No response from pipelined queries, PUT method(check_rename).") return result end for i, response in pairs(pipeline_returns) do if not response or response.status ~= 201 then table.insert(result, { extension, false, false, }) else options = { header = { ["Destination"] = destinations[i], }, timeout = 5, } all_move = http.pipeline_add(all_put[i].path, options, all_move, "MOVE") end end pipeline_returns = http.pipeline_go(host, port, all_move) if not pipeline_returns then stdnse.debug1("No response from pipelined queries, PUT method(check_rename).") return result end for i, response in pairs(pipeline_returns) do if not response or not (response.status == 204 or response.status == 201) then table.insert(result, { extension, false, false, }) else all_get = http.pipelined_add(files_to_get[all_move[i].options.header['Destination']:match('%.(.-);')], nil, all_get, "GET") end end pipeline_returns = http.pipeline_go(host, port, all_get) if not pipeline_returns then stdnse.debug1("No response from pipelined queries, PUT method(check_rename).") return result end for i, response in pairs(pipeline_returns) do if not response or response.status ~= 200 or not response.body:find(answers[all_get[i].path]) or response.body:find "#exec" then table.insert(result, { extension, true, false, }) else table.insert(result, { extension, true, true, }) end end return result end function action (host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".folder") or '/' local output = stdnse.output_table() local random_string = stdnse.generate_random_string(10, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") local test_dir = path .. '/' .. 'WebDavTest_' .. random_string test_dir = test_dir:gsub('//', '/') -- creating a random named folder stdnse.debug1("Attempting to create folder %s", test_dir) if create_dir(host, port, test_dir) then stdnse.debug1 "The HOST is WRITABLE" else stdnse.debug1 "The HOST is not WRITABLE" return end -- checking uploadable extensions stdnse.debug1 "Checking extensions for upload and execution" local results_1 = check_extensions(host, port, test_dir) -- checking renamable extensions stdnse.debug1 "Checking if rename is possible or not" local results_2 = check_rename(host, port, test_dir) -- deleting the mess we just created. stdnse.debug1("Deleting directory %s", test_dir) if delete_dir(host, port, test_dir) then stdnse.debug1("Delete Succesfull") end local uploadable = {} local executable = {} local renamable_ex = {} local renamable = {} for _, result in pairs(results_1) do if result[2] == true then table.insert(uploadable, result[1]) end if result[3] == true then table.insert(executable, result[1]) end end for _, result in pairs(results_2) do if result[2] == true then table.insert(renamable, result[1]) end if result[3] == true then table.insert(renamable_ex, result[1]) end end output['Uploadable Files'] = uploadable output['Executable Files'] = executable output['Renamable Files'] = renamable output['Executable after rename'] = renamable_ex return output end