From f79a11aeebaa42629869f333952591b43b14ac1c Mon Sep 17 00:00:00 2001 From: fyodor Date: Mon, 29 Jul 2013 06:19:24 +0000 Subject: [PATCH] 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] --- CHANGELOG | 12 ++++++++++ nselib/stdnse.lua | 32 ++++++++++++++++++++++++++ scripts/domino-enum-users.nse | 2 +- scripts/hostmap-bfk.nse | 11 ++------- scripts/hostmap-ip2hosts.nse | 11 ++------- scripts/http-config-backup.nse | 2 +- scripts/http-domino-enum-passwords.nse | 5 ++-- scripts/ms-sql-dump-hashes.nse | 2 +- scripts/snmp-ios-config.nse | 2 +- scripts/stuxnet-detect.nse | 2 +- 10 files changed, 56 insertions(+), 25 deletions(-) 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)