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)