From 18e06c6381700075b083700c0f952d473d7e19d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 28 Jul 2026 11:57:14 +0200 Subject: [PATCH] Fixing MsSQL FOR JSON handling of NULL values --- lib/core/settings.py | 2 +- lib/techniques/union/use.py | 3 +- tests/test_union_engine.py | 70 +++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 16fb6ccb7..5e1c73478 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.215" +VERSION = "1.10.7.216" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/techniques/union/use.py b/lib/techniques/union/use.py index 43de9a31a..9d50ab82a 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -125,7 +125,8 @@ def _oneShotUnionUse(expression, unpack=True, limited=False): if fields: parts = [] for row in json_data: - parts.append("%s%s%s" % (kb.chars.start, kb.chars.delimiter.join(getUnicode(row.get(field) or NULL) for field in fields), kb.chars.stop)) + # is-None (not falsy) check: a real 0 / '' / False are values, only JSON null is NULL + parts.append("%s%s%s" % (kb.chars.start, kb.chars.delimiter.join(NULL if row.get(field) is None else getUnicode(row.get(field)) for field in fields), kb.chars.stop)) retVal = "".join(parts) except: retVal = None diff --git a/tests/test_union_engine.py b/tests/test_union_engine.py index f0592fe4e..3dc81caf4 100644 --- a/tests/test_union_engine.py +++ b/tests/test_union_engine.py @@ -27,9 +27,11 @@ from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.data import conf, kb +from lib.core.datatype import AttribDict from lib.core.enums import PAYLOAD, PLACE from lib.request.connect import Connect import lib.techniques.union.test as ut +import lib.techniques.union.use as uu MARKER = "MARKER42" VALID_PAGE = "results %s" % MARKER @@ -103,6 +105,74 @@ class TestOrderByColumnCount(unittest.TestCase): self.assertEqual(self._detect(25), 25) +class TestMssqlJsonAggFalsyValues(unittest.TestCase): + """Regression: MSSQL UNION dumps use FOR JSON (jsonAggMode), whose output carries native JSON + types. A real 0 / '' / false is a value; only JSON null is SQL NULL. The old `row.get(field) or + NULL` mapped every falsy value to the literal string 'NULL' - proven live (SELECT 0,'','ok' + dumped as NULL,NULL,ok). We drive the REAL _oneShotUnionUse against a mock FOR JSON page.""" + + def setUp(self): + self._s = { + "hexConvert": conf.get("hexConvert"), "parameters": conf.get("parameters"), + "paramDict": conf.get("paramDict"), "base64Parameter": conf.get("base64Parameter"), + "pageEncoding": conf.get("pageEncoding"), "hashDB": conf.get("hashDB"), + "inj": (kb.injection.place, kb.injection.parameter, kb.injection.data), + "jsonAggMode": kb.get("jsonAggMode"), "unionDuplicates": kb.get("unionDuplicates"), + "forcePartialUnion": kb.get("forcePartialUnion"), "tableFrom": kb.get("tableFrom"), + "unionTemplate": kb.get("unionTemplate"), "qp": Connect.queryPage, + } + conf.hexConvert = False + conf.parameters = {PLACE.GET: "id=1"} + conf.paramDict = {PLACE.GET: {"id": "1"}} + conf.base64Parameter = () + conf.pageEncoding = None + conf.hashDB = None + v = AttribDict() + v.vector = (0, 4, "", "", "", "NULL", PAYLOAD.WHERE.NEGATIVE, False, False, None, None) + kb.injection.place = PLACE.GET + kb.injection.parameter = "id" + kb.injection.data = {PAYLOAD.TECHNIQUE.UNION: v} + kb.jsonAggMode = True + kb.unionDuplicates = kb.forcePartialUnion = False + kb.tableFrom = kb.unionTemplate = None + set_dbms("MSSQL") + + def tearDown(self): + conf.hexConvert = self._s["hexConvert"] + conf.parameters = self._s["parameters"] + conf.paramDict = self._s["paramDict"] + conf.base64Parameter = self._s["base64Parameter"] + conf.pageEncoding = self._s["pageEncoding"] + conf.hashDB = self._s["hashDB"] + kb.injection.place, kb.injection.parameter, kb.injection.data = self._s["inj"] + kb.jsonAggMode = self._s["jsonAggMode"] + kb.unionDuplicates = self._s["unionDuplicates"] + kb.forcePartialUnion = self._s["forcePartialUnion"] + kb.tableFrom = self._s["tableFrom"] + kb.unionTemplate = self._s["unionTemplate"] + Connect.queryPage = self._s["qp"] + uu.Request.queryPage = self._s["qp"] + + def _dump(self, jsonstr): + # jsonstr is exactly what MSSQL FOR JSON AUTO, INCLUDE_NULL_VALUES emits (literal to keep key + # order stable across py2/py3); object_pairs_hook=OrderedDict preserves that order downstream + page = "%s%s%s" % (kb.chars.start, jsonstr, kb.chars.stop) + + def oracle(payload=None, content=False, raise404=True, **kwargs): + return (page, {}, 200) if content else True + + Connect.queryPage = staticmethod(oracle) + uu.Request.queryPage = staticmethod(oracle) + kb.jsonAggMode = True + out = uu._oneShotUnionUse("SELECT a,b,c,d FROM users", False) + firstRow = out.replace(kb.chars.start, "").split(kb.chars.stop)[0] + return firstRow.split(kb.chars.delimiter) + + def test_falsy_values_preserved(self): + fields = self._dump('[{"a":0,"b":"","c":"ok","d":null}]') + self.assertEqual(fields, ["0", "", "ok", "NULL"]) # only JSON null -> NULL + + if __name__ == "__main__": unittest.main(verbosity=2)