diff --git a/CHANGELOG b/CHANGELOG index 5e252ebc2..397a77822 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ #Nmap Changelog ($Id$); -*-text-*- +o [NSE][GH#2393] Fix script-terminating error when unknown BSON data types are + encountered. Added parsers for most standard data types. [Daniel Miller] + o [Ncat] Fix hostname/certificate comparison and matching to handle ASN.1 strings without null terminators, a similar bug to OpenSSL's CVE-2021-3712. diff --git a/nselib/mongodb.lua b/nselib/mongodb.lua index b193a5fd6..2a993b8e8 100644 --- a/nselib/mongodb.lua +++ b/nselib/mongodb.lua @@ -13,6 +13,7 @@ local nmap = require "nmap" local stdnse = require "stdnse" local string = require "string" local table = require "table" +local math = require "math" local openssl = stdnse.silent_require "openssl" _ENV = stdnse.module("mongodb", stdnse.seeall) @@ -27,9 +28,6 @@ local arg_DB = stdnse.get_script_args("mongodb.db") local function dbg(str,...) stdnse.debug3("MngoDb:"..str, ...) end ---local dbg =stdnse.debug1 - -local err =stdnse.debug1 ---------------------------------------------------------------------- -- First of all comes a Bson parsing library. This can easily be moved out into a separate library should other @@ -51,9 +49,8 @@ local err =stdnse.debug1 -- Created 01/13/2010 - v0.1 - created by Martin Holst Swende --module("bson", package.seeall) local function dbg_err(str,...) - stdnse.debug1("Bson-ERR:"..str, ...) + stdnse.debug2("Bson-ERR:"..str, ...) end ---local err =stdnse.log_error --Converts an element (key, value) into bson binary data --@param key the key name, must *NOT* contain . (period) or start with $ @@ -73,18 +70,27 @@ local function _element_to_bson(key, value) return false, ("key %r must not contain '.'"):format(tostring(key)) end - local name = string.pack("z", key) -- null-terminated string if type(value) == 'string' then - local cstring = string.pack("z", value) -- null-terminated string - local length = string.pack(" 0x7fffffff or value < -0x80000000 then -- long + return true, string.pack(" 1 then + dbg_err("C-string did not contain NULL char") + return nil, data end - local value = data:sub(1,length-1) - - --dbg("Found char at pos %d, data is %s c-string is %s",length, data, value) - - return value, data:sub(length+1) + return value, data:sub(pos) end +local function get_bson_str (data) + local v, pos = string.unpack(" - local value = get_c_string(data:sub(5), len) - -- Count position as header (=4) + length of string (=len)+ null char (=1) - return 4+len+1,value - elseif 3 == code or 4 == code then -- table or array - local object, err - - -- Need to know the length, to return later - local obj_size = string.unpack(" 1 do - key, value, data = _element_to_dict(data) + local key, value, err + while data and data:len() > 0 do + key, value, data, err = _element_to_dict(data) if not key then - -- TODO: handle failures better; report error up the stack? - dbg_err("Failed to parse element, returning truncated table") - return result + return result, ("Failed to parse element: %s"):format(err) end dbg("Parsed (%s='%s'), data left : %d", tostring(key),tostring(value), data:len()) - if type(value) ~= 'table' then value=tostring(value) end + --if type(value) ~= 'table' then value=tostring(value) end result[key] = value end return result @@ -290,9 +400,17 @@ function fromBson(data) return {},data, err_msg end - local element_portion = data:sub(5,object_size) + if data:byte(object_size) ~= 0 then + local err_msg = "Invalid BSON: no null terminator" + dbg(err_msg) + return nil, data, err_msg + end + + local element_portion = data:sub(5,object_size - 1) -- terminator belongs to outer doc local remainder = data:sub(object_size+1) - return _elements_to_dict(element_portion), remainder + dbg("element: %s\nremainder: %s", stdnse.tohex(element_portion), stdnse.tohex(remainder)) + local dict, err = _elements_to_dict(element_portion) + return dict, remainder, err end @@ -646,4 +764,210 @@ function queryResultToTable( resultTable ) end +local unittest = require "unittest" +if not unittest.testing() then + return _ENV +end + +-- https://github.com/mongodb/mongo-python-driver/blob/master/test/bson_corpus/ +local TESTS = { + -- 0x01 = BSONNUM, float + { desc = "BSONNUM: +1.0", + bson = "10000000016400000000000000F03F00", + obj = {d = {1.0}} + }, + { desc = "BSONNUM: -1.0", + bson = "10000000016400000000000000F0BF00", + obj = {d = {-1.0}} + }, + { desc = "BSONNUM: +1.0001220703125", + bson = "10000000016400000000008000F03F00", + obj = {d = {1.0001220703125}} + }, + { desc = "BSONNUM: -1.0001220703125", + bson = "10000000016400000000008000F0BF00", + obj = {d = {-1.0001220703125}} + }, + { desc = "BSONNUM: 1.2345678921232E+18", + bson = "100000000164002a1bf5f41022b14300", + obj = {d = {1.2345678921232e18}} + }, + { desc = "BSONNUM: -1.2345678921232E+18", + bson = "100000000164002a1bf5f41022b1c300", + obj = {d = {-1.2345678921232e18}} + }, + { desc = "BSONNUM: 0.0", + bson = "10000000016400000000000000000000", + obj = {d = {0.0}} + }, + { desc = "BSONNUM: -0.0", + bson = "10000000016400000000000000008000", + obj = {d = {-0.0}} + }, + -- Lua 5.3 safely round-trips all of these floats! + { desc = "BSONNUM: NaN", + bson = "10000000016400000000000000F87F00", + test = function(o) return tostring(o.d) == "nan" end + }, + { desc = "BSONNUM: NaN with payload", + bson = "10000000016400120000000000F87F00", + test = function(o) return tostring(o.d) == "nan" end + }, + { desc = "BSONNUM: Inf", + bson = "10000000016400000000000000F07F00", + test = function(o) return tostring(o.d) == "inf" end + }, + { desc = "BSONNUM: -Inf", + bson = "10000000016400000000000000F0FF00", + test = function(o) return tostring(o.d) == "-inf" end + }, + { desc = "bad BSONNUM: double truncated", invalid = true, + bson = "0B0000000164000000F03F00" + }, + -- 0x02 = BSONSTR, string + { desc = "BSONSTR: Empty string", bson = "0D000000026100010000000000", + obj = {a = ""} + }, + { desc = "BSONSTR: Single character", bson = "0E00000002610002000000620000", + obj = {a = "b"} + }, + { desc = "BSONSTR: Multi-character", + bson = "190000000261000D0000006162616261626162616261620000", + obj = {a = "abababababab"} + }, + { desc = "BSONSTR: Embedded nulls", + bson = "190000000261000D0000006162006261620062616261620000", + obj = {a = "ab\x00bab\x00babab"} + }, + { desc = "BSONSTR: bad string length: 0 (but no 0x00 either)", invalid = true, + bson = "0C0000000261000000000000" + }, + { desc = "BSONSTR: bad string length: -1", invalid = true, + bson = "0C000000026100FFFFFFFF00" + }, + { desc = "BSONSTR: bad string length: eats terminator", invalid = true, + bson = "10000000026100050000006200620000" + }, + { desc = "BSONSTR: bad string length: longer than rest of document", invalid = true, + bson = "120000000200FFFFFF00666F6F6261720000" + }, + { desc = "BSONSTR: string is not null-terminated", invalid = true, + bson = "1000000002610004000000616263FF00" + }, + { desc = "BSONSTR: empty string, but extra null", invalid = true, + bson = "0E00000002610001000000000000" + }, + { desc = "Empty array", bson = "0D000000046100050000000000", + -- Should probably use json.make_array and json.typeof for this. + FAIL = "Can't distinguish array vs object table", + obj = {a = {}} + }, + { desc = "single element array", bson = "140000000461000C0000001030000A0000000000", + FAIL = "Can't distinguish array vs object table", + obj = {a = {10}} + }, + { desc = "single element with empty index", + bson = "130000000461000B00000010000A0000000000", + fixed = "140000000461000C0000001030000A0000000000", + FAIL = "Can't distinguish array vs object table", + obj = {a = {10}} + }, + { desc = "bad array: too long", invalid = true, + bson = "140000000461000D0000001030000A0000000000", + }, + { desc = "bad array: too short", invalid = true, + bson = "140000000461000B0000001030000A0000000000" + }, + { desc = "bad array: bad string length", invalid = true, + bson = "1A00000004666F6F00100000000230000500000062617A000000" + }, + { desc = "BSONBIN: subtype 0x00 (Zero-length)", bson = "0D000000057800000000000000", + FAIL = "No encoder for BSONBIN type", + obj = {x = "Binary subtype 0: "} + }, + { desc = "BSONBIN: subtype 0x00", bson = "0F0000000578000200000000FFFF00", + FAIL = "No encoder for BSONBIN type", + obj = {x = "Binary subtype 0: ffff"} + }, + { desc = "BSONBIN: subtype 0x01", bson = "0F0000000578000200000001FFFF00", + FAIL = "No encoder for BSONBIN type", + obj = {x = "Binary subtype 1: ffff"} + }, + { desc = "BSONBIN: subtype 0x03", + bson = "1D000000057800100000000373FFD26444B34C6990E8E7D1DFC035D400", + FAIL = "No encoder for BSONBIN type", + obj = {x = "Binary subtype 3: 73ffd26444b34c6990e8e7d1dfc035d4"} + }, + { desc = "BSONBIN: Length longer than document", invalid = true, + bson = "1D000000057800FF0000000573FFD26444B34C6990E8E7D1DFC035D400" + }, + { desc = "BSONBIN: Negative length", invalid = true, + bson = "0D000000057800FFFFFFFF0000" + }, + { desc = "BSONBIN: subtype 0x02 length too long ", invalid = true, + bson = "13000000057800060000000203000000FFFF00" + }, + { desc = "BSONBIN: subtype 0x02 length too short", invalid = true, + bson = "13000000057800060000000201000000FFFF00" + }, + { desc = "BSONBIN: subtype 0x02 length negative one", invalid = true, + bson = "130000000578000600000002FFFFFFFFFFFF00" + }, + { desc = "Int32 MinValue", bson = "0C0000001069000000008000", + obj = {i = -2147483648} + }, + { desc = "Int32: MaxValue", bson = "0C000000106900FFFFFF7F00", + obj = {i = 2147483647} + }, + { desc = "Int32: -1", bson = "0C000000106900FFFFFFFF00", + obj = {i = -1} + }, + { desc = "Int32: 0", bson = "0C0000001069000000000000", + obj = {i = 0} + }, + { desc = "Int32: 1", bson = "0C0000001069000100000000", + obj = {i = 1} + }, + { desc = "Int32: Bad int32 field length", invalid = true, + bson = "090000001061000500" + } +} +test_suite = unittest.TestSuite:new() + +local equal = unittest.equal +local is_nil = unittest.is_nil +local is_true = unittest.is_true +local type_is = unittest.type_is + +for _, test in ipairs(TESTS) do + dbg("Loading test %s...", test.desc) + local fmt = function(description) + return ("%s: %s"):format(test.desc, description) + end + + local binary = stdnse.fromhex(test.bson) + local obj, rem, err = fromBson(binary) + if test.invalid then + dbg("Expect error, type is %s: %s", type(err), err) + test_suite:add_test(type_is("string", err), fmt("Error reported for invalid BSON")) + else + test_suite:add_test(type_is("table", obj), fmt("BSON parsed to table")) + test_suite:add_test(is_nil(err), fmt("No error reported for valid BSON")) + + local status, bsonout = toBson(obj) + test_suite:add_test(is_true(status), fmt("toBson succeeds")) + test_suite:add_test(equal(type(bsonout), "string"), fmt("toBson returns string")) + + -- round-trip test. Some "bad" encodings are ok but will generate different bson + local rttest = equal(stdnse.tohex(bsonout), test.fixed and test.fixed or stdnse.tohex(binary)) + -- Our library is incomplete in some ways as noted in FAIL + if test.FAIL then + rttest = unittest.expected_failure(rttest) + end + test_suite:add_test(rttest, fmt("Round-trip encoding matches")) + if test.test then + test_suite:add_test(is_true(test.test(obj)), fmt("Extra test")) + end + end +end return _ENV; diff --git a/nselib/unittest.lua b/nselib/unittest.lua index ac90c3182..53606af3e 100644 --- a/nselib/unittest.lua +++ b/nselib/unittest.lua @@ -302,7 +302,7 @@ end is_nil = function(value) return value == nil end -is_nil = make_test(is_nil, "Expected not nil, got %s") +is_nil = make_test(is_nil, "Expected nil, got %s") --- Test for not nil -- @param value The value to test @@ -312,6 +312,15 @@ not_nil = function(value) end not_nil = make_test(not_nil, "Expected not nil, got %s") +--- Test for Lua type +-- @param typ The type that value should be +-- @param value The value to test +-- @return bool True if type(value) == typ +type_is = function (typ, value) + return type(value) == typ +end +type_is = make_test(type_is, "Value is not a '%s': %s") + --- Test tables for equality, 1 level deep -- @param a The first table to test -- @param b The second table to test @@ -506,6 +515,7 @@ test_suite:add_test(expected_failure(keys_equal({one=1,two=2},{[3]="three",one=1 test_suite:add_test(identical(0, 0), "integer === integer") test_suite:add_test(identical(nil, nil), "nil === nil") test_suite:add_test(identical({}, {}), "{} === {}") -test_suite:add_test(length_is(test_suite.tests, 15), "Number of tests is 15") +test_suite:add_test(type_is("table", {}), "{} is a table") +test_suite:add_test(length_is(test_suite.tests, 16), "Number of tests is 16") return _ENV;