diff --git a/CHANGELOG b/CHANGELOG index de56b0776..a466f9f3d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,18 @@ o [Ncat] Added --lua-exec. This feature is basically an equivalent of ncat redirecting all stdin and stdout operations to the socket connection. [Jacek Wielemborek] +o [NSE] Oops, there was a vulnerability in one of our 437 NSE scripts. + If you ran the (fortunately non-default) http-domino-enum-passwords + script with the (fortunately also non-default) + domino-enum-passwords.idpath parameter against a malicious server, + it could cause an arbitrarily named file to to be written to the + client system. Thanks to Trustwave researcher Piotr Duszynski for + discovering and reporting the problem. We've fixed that script, and + also updated several other scripts to use a new + stdnse.filename_escape function for extra safety. This breaks our + record of never having a vulnerability in the 16 years that Nmap has + existed, but that's still a fairly good run. [David, Fyodor] + o [NSE] Added teamspeak2-version.nse by Marin Maržić. o Nmap's routing table is now sorted first by netmask, then by metric. diff --git a/nselib/stdnse.lua b/nselib/stdnse.lua index 7ceb61ef0..36d0bc4c1 100644 --- a/nselib/stdnse.lua +++ b/nselib/stdnse.lua @@ -1195,4 +1195,36 @@ function pretty_printer (obj, printer) return aux(obj, "") end +-- This pattern must match the percent sign '%' since it is used in +-- escaping. +local FILESYSTEM_UNSAFE = "[^a-zA-Z0-9._-]" +--- +-- Escape a string to remove bytes and strings that may have meaning to +-- a filesystem, such as slashes. All bytes are escaped, except for: +-- * alphabetic a-z and A-Z, digits 0-9, . _ - +-- In addition, the strings "." and ".." have +-- their characters escaped. +-- +-- Bytes are escaped by a percent sign followed by the two-digit +-- hexadecimal representation of the byte value. +-- * filename_escape("filename.ext") --> "filename.ext" +-- * filename_escape("input/output") --> "input%2foutput" +-- * filename_escape(".") --> "%2e" +-- * filename_escape("..") --> "%2e%2e" +-- This escaping is somewhat like that of JavaScript +-- encodeURIComponent, except that fewer bytes are +-- whitelisted, and it works on bytes, not Unicode characters or UTF-16 +-- code points. +function filename_escape(s) + if s == "." then + return "%2e" + elseif s == ".." then + return "%2e%2e" + else + return (string.gsub(s, FILESYSTEM_UNSAFE, function (c) + return string.format("%%%02x", string.byte(c)) + end)) + end +end + return _ENV; diff --git a/scripts/domino-enum-users.nse b/scripts/domino-enum-users.nse index 0006d5f68..1481fbc8b 100644 --- a/scripts/domino-enum-users.nse +++ b/scripts/domino-enum-users.nse @@ -103,7 +103,7 @@ action = function(host, port) helper:disconnect() if ( status and data and path ) then - local filename = ("%s/%s.id"):format(path, username ) + local filename = path .. "/" .. stdnse.filename_escape(u_details.fullname .. ".id") local status, err = saveIDFile( filename, data ) if ( status ) then diff --git a/scripts/hostmap-bfk.nse b/scripts/hostmap-bfk.nse index 7a4b89c4e..6701a58a2 100644 --- a/scripts/hostmap-bfk.nse +++ b/scripts/hostmap-bfk.nse @@ -68,7 +68,7 @@ categories = {"external", "discovery", "intrusive"} local HOSTMAP_SERVER = "www.bfk.de" -local filename_escape, write_file +local write_file hostrule = function(host) return not ipOps.isPrivate(host.ip) @@ -106,7 +106,7 @@ action = function(host) local filename_prefix = stdnse.get_script_args("hostmap-bfk.prefix") if filename_prefix then - local filename = filename_prefix .. filename_escape(host.targetname or host.ip) + local filename = filename_prefix .. stdnse.filename_escape(host.targetname or host.ip) local status, err = write_file(filename, hostnames_str .. "\n") if status then output_tab.filename = filename @@ -118,13 +118,6 @@ action = function(host) return output_tab end --- Escape some potentially unsafe characters in a string meant to be a filename. -function filename_escape(s) - return string.gsub(s, "[\0/=]", function(c) - return string.format("=%02X", string.byte(c)) - end) -end - function write_file(filename, contents) local f, err = io.open(filename, "w") if not f then diff --git a/scripts/hostmap-ip2hosts.nse b/scripts/hostmap-ip2hosts.nse index aa50813a7..36b6b23dd 100644 --- a/scripts/hostmap-ip2hosts.nse +++ b/scripts/hostmap-ip2hosts.nse @@ -53,7 +53,7 @@ local target = require "target" local HOSTMAP_BING_SERVER = "www.ip2hosts.com" local HOSTMAP_DEFAULT_PROVIDER = "ALL" -local filename_escape, write_file +local write_file hostrule = function(host) return not ipOps.isPrivate(host.ip) @@ -99,7 +99,7 @@ action = function(host) output_tab.hosts = hostnames --write to file if filename_prefix then - local filename = filename_prefix .. filename_escape(host.targetname or host.ip) + local filename = filename_prefix .. stdnse.filename_escape(host.targetname or host.ip) hostnames_str = stdnse.strjoin("\n", hostnames) local status, err = write_file(filename, hostnames_str) if status then @@ -112,13 +112,6 @@ action = function(host) return output_tab end --- Escape some potentially unsafe characters in a string meant to be a filename. -function filename_escape(s) - return string.gsub(s, "[%z/=]", function(c) - return string.format("=%02X", string.byte(c)) - end) -end - function write_file(filename, contents) local f, err = io.open(filename, "w") if not f then diff --git a/scripts/http-config-backup.nse b/scripts/http-config-backup.nse index af06d6322..1c80755a2 100644 --- a/scripts/http-config-backup.nse +++ b/scripts/http-config-backup.nse @@ -209,7 +209,7 @@ action = function (host, port) if (response.status == 200) then -- check it if is valid before inserting if cfg.check(response.body) then - local filename = ((host.targetname or host.ip) .. url_path):gsub("/", "-"); + local filename = stdnse.escape_filename((host.targetname or host.ip) .. url_path) -- save the content if save then diff --git a/scripts/http-domino-enum-passwords.nse b/scripts/http-domino-enum-passwords.nse index 19065e3c2..cec136235 100644 --- a/scripts/http-domino-enum-passwords.nse +++ b/scripts/http-domino-enum-passwords.nse @@ -315,9 +315,10 @@ action = function(host, port) http_response = http.get( vhost or host, port, u_details.idfile, { auth = { username = user, password = pass }, no_cache = true }) if ( http_response.status == 200 ) then - local status, err = saveIDFile( ("%s/%s.id"):format(download_path, u_details.fullname), http_response.body ) + local filename = download_path .. "/" .. stdnse.filename_escape(u_details.fullname .. ".id") + local status, err = saveIDFile( filename, http_response.body ) if ( status ) then - table.insert( id_files, ("%s ID File has been downloaded (%s/%s.id)"):format(u_details.fullname, download_path, u_details.fullname) ) + table.insert( id_files, ("%s ID File has been downloaded (%s)"):format(u_details.fullname, filename) ) else table.insert( id_files, ("%s ID File was not saved (error: %s)"):format(u_details.fullname, err ) ) end diff --git a/scripts/ms-sql-dump-hashes.nse b/scripts/ms-sql-dump-hashes.nse index 7b4f06780..159e1aeb5 100644 --- a/scripts/ms-sql-dump-hashes.nse +++ b/scripts/ms-sql-dump-hashes.nse @@ -119,7 +119,7 @@ action = function( host, port ) local filename if ( dir ) then local instance = instance:GetName():match("%\\+(.+)$") or instance:GetName() - filename = ("%s/%s_%s_ms-sql_hashes.txt"):format(dir, host.ip, instance) + filename = dir .. "/" .. stdnse.filename_escape(("%s_%s_ms-sql_hashes.txt"):format(host.ip, instance)) saveToFile(filename, instanceOutput[1]) end end diff --git a/scripts/snmp-ios-config.nse b/scripts/snmp-ios-config.nse index 80b998ebe..ff8dab654 100644 --- a/scripts/snmp-ios-config.nse +++ b/scripts/snmp-ios-config.nse @@ -184,7 +184,7 @@ action = function(host, port) result = ( infile and infile:getContent() ) if ( tftproot ) then - local fname = tftproot .. host.ip .. "-config" + local fname = tftproot .. stdnse.filename_escape(host.ip .. "-config") local file, err = io.open(fname, "w") if ( file ) then file:write(result) diff --git a/scripts/stuxnet-detect.nse b/scripts/stuxnet-detect.nse index e0deb47f4..6d1a28c75 100644 --- a/scripts/stuxnet-detect.nse +++ b/scripts/stuxnet-detect.nse @@ -81,7 +81,7 @@ local function check_infected(host, path, save) fmt = save:gsub("%%h", host.ip) fmt = fmt:gsub("%%v", version) - file = io.open(fmt, "w") + file = io.open(stdnse.filename_escape(fmt), "w") if file then stdnse.print_debug(1, "Wrote %d bytes to file %s.", #result.arguments, fmt) file:write(result.arguments)