diff --git a/lib/core/settings.py b/lib/core/settings.py index 00ae7277e..ee4089ada 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.8.25" +VERSION = "1.10.8.26" 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/odata/inject.py b/lib/techniques/odata/inject.py index d8806412f..10031d1dd 100644 --- a/lib/techniques/odata/inject.py +++ b/lib/techniques/odata/inject.py @@ -55,6 +55,7 @@ from lib.core.settings import ODATA_KEY_CANDIDATES from lib.core.settings import ODATA_MAX_KEY from lib.core.settings import ODATA_MAX_LENGTH from lib.core.settings import ODATA_MAX_RECORDS +from lib.core.settings import SQLMAP_ENVIRONMENT_PREFIX from lib.core.settings import UPPER_RATIO_BOUND from lib.request.connect import Connect as Request from lib.utils.xrange import xrange @@ -416,6 +417,9 @@ def _findKeyAndEntities(place, parameter, boundary, emptyPage, errorSurface=True else: misses += 1 if present: + if len(present) >= ODATA_MAX_RECORDS: + logger.warning("stopped at the %d-entity cap, so further entities are left undumped " + "(raise it with %s_ODATA_MAX_RECORDS)" % (ODATA_MAX_RECORDS, SQLMAP_ENVIRONMENT_PREFIX)) return candidate, present return None, [] @@ -463,6 +467,12 @@ def _inferField(oracle, key, keyValue, field, maxLen=ODATA_MAX_LENGTH, useIn=Non hi = mid - 1 length = lo + if length >= maxLen: + # the bisection is bounded, so a value at the bound is a FLOOR, not a measurement - saying + # nothing here hands back a prefix that reads exactly like a complete value + logger.warning("property '%s' is at least %d characters; the recovered value is truncated " + "at the cap (raise it with %s_ODATA_MAX_LENGTH)" % (field, maxLen, SQLMAP_ENVIRONMENT_PREFIX)) + chars = [] for pos in xrange(length): # A whole candidate set is asked about in ONE request, so a probe halves the space just as diff --git a/lib/techniques/sparql/inject.py b/lib/techniques/sparql/inject.py index bd058c333..af1bf52fe 100644 --- a/lib/techniques/sparql/inject.py +++ b/lib/techniques/sparql/inject.py @@ -58,6 +58,7 @@ from lib.core.settings import SPARQL_ERROR_SIGNATURES from lib.core.settings import SPARQL_MAX_LENGTH from lib.core.settings import SPARQL_MAX_PREDICATES from lib.core.settings import SPARQL_MAX_RECORDS +from lib.core.settings import SQLMAP_ENVIRONMENT_PREFIX from lib.core.settings import UPPER_RATIO_BOUND from lib.request.connect import Connect as Request from lib.utils.xrange import xrange @@ -397,6 +398,12 @@ def _inferString(truth, inner, maxLen=SPARQL_MAX_LENGTH): hi = mid - 1 length = lo + if length >= maxLen: + # the bisection is bounded, so a value at the bound is a FLOOR, not a measurement - saying + # nothing here hands back a prefix that reads exactly like a complete IRI or literal + logger.warning("value is at least %d characters; it is truncated at the cap " + "(raise it with %s_SPARQL_MAX_LENGTH)" % (maxLen, SQLMAP_ENVIRONMENT_PREFIX)) + chars = [] last = len(_CS_ORDS) - 1 for pos in xrange(1, length + 1): @@ -445,7 +452,13 @@ def _dumpGraph(truth): if predicates is None or triples is None: logger.warning("dump aborted: the oracle could not resolve the graph size; detection stands") return - logger.info("default graph holds %d distinct predicate(s) and %d triple(s)" % (predicates, triples)) + # _inferCount bisects inside a bound, so a count sitting ON that bound is a floor, not a count + saturated = predicates >= SPARQL_MAX_PREDICATES + logger.info("default graph holds %s%d distinct predicate(s) and %d triple(s)" + % ("at least " if saturated else "", predicates, triples)) + if saturated: + logger.warning("the predicate count hit the %d cap, so the list below is partial " + "(raise it with %s_SPARQL_MAX_PREDICATES)" % (SPARQL_MAX_PREDICATES, SQLMAP_ENVIRONMENT_PREFIX)) predRows = [] for offset in xrange(min(predicates, SPARQL_MAX_PREDICATES)): diff --git a/tests/test_odata.py b/tests/test_odata.py index feca25624..fd33583d6 100644 --- a/tests/test_odata.py +++ b/tests/test_odata.py @@ -340,6 +340,52 @@ class TestDetectionAndExtraction(unittest.TestCase): self.assertIsNone(template) +class TestCapsAreDisclosed(unittest.TestCase): + """Every bound here is a bisection bound, so a result sitting ON it is a FLOOR, not a measurement. + Handing back a capped prefix (or a capped entity list) with no word said reads exactly like a + complete answer - the one failure mode a blind dumper must never have.""" + + def setUp(self): + self.saved, self.savedParams = odata._send, odata.conf.parameters + odata._send = _mockSend + odata.conf.parameters = {odata.PLACE.GET: "name=luther"} + odata.SENTINEL = "zzsentinelzz" + self.savedWarning, self.warnings = odata.logger.warning, [] + odata.logger.warning = lambda message, *args: self.warnings.append(message % args if args else message) + + def tearDown(self): + odata._send, odata.conf.parameters = self.saved, self.savedParams + odata.logger.warning = self.savedWarning + + def _oracle(self): + _t, _p, boundary = odata._detectBoolean(odata.PLACE.GET, "name") + oracle = odata._makeOracle(odata.PLACE.GET, "name", boundary, truePredicate="(Id eq 1)") + self.assertIsNotNone(oracle) + return oracle + + def test_truncated_value_is_reported(self): + value = odata._inferField(self._oracle(), "Id", 1, "Secret", maxLen=4) + self.assertEqual(value, "S3CR") + self.assertTrue(any("at least 4 characters" in _ for _ in self.warnings), self.warnings) + + def test_value_that_fits_is_silent(self): + value = odata._inferField(self._oracle(), "Id", 1, "Name", maxLen=64) + self.assertEqual(value, "luther") + self.assertEqual(self.warnings, []) + + def test_entity_cap_is_reported(self): + savedCap, odata.ODATA_MAX_RECORDS = odata.ODATA_MAX_RECORDS, 2 + try: + _t, _p, boundary = odata._detectBoolean(odata.PLACE.GET, "name") + key, keys = odata._findKeyAndEntities(odata.PLACE.GET, "name", boundary, _EMPTY) + finally: + odata.ODATA_MAX_RECORDS = savedCap + + self.assertEqual(key, "Id") + self.assertEqual(keys, [1, 2]) # the third entity exists but is beyond the cap... + self.assertTrue(any("2-entity cap" in _ for _ in self.warnings), self.warnings) + + _LITERAL = r"'(?:[^']|'')*'" _IN_REGEX = re.compile(r"(?Psubstring\(\w+,\d+,1\)|%s) in \((?P%s(?:,%s)*)\)" % (_LITERAL, _LITERAL, _LITERAL)) diff --git a/tests/test_sparql.py b/tests/test_sparql.py index 3a2ab1d5a..a4efd84b2 100644 --- a/tests/test_sparql.py +++ b/tests/test_sparql.py @@ -325,5 +325,60 @@ class TestDetectionAndExtraction(unittest.TestCase): self.assertIsNone(template) # no true/false divergence -> not even boolean-detected +class _Dumper(object): + """Stands in for conf.dumper, which the offline bootstrap does not build.""" + + def __init__(self): + self.output = [] + + def singleString(self, value): + self.output.append(value) + + +class TestCapsAreDisclosed(unittest.TestCase): + """Both bounds here are bisection bounds, so a result sitting ON one is a FLOOR, not a measurement. + A capped IRI reads exactly like a whole one, and a capped predicate count reads like the size of + the graph - the reader has no way to tell unless it is said.""" + + def setUp(self): + self.saved, self.savedParams = sparql._send, sparql.conf.parameters + sparql._send = _mockSend + sparql.conf.parameters = {sparql.PLACE.GET: "q=luther"} + sparql.SENTINEL = "zzsentinelzz" + self.savedDumper, sparql.conf.dumper = sparql.conf.get("dumper"), _Dumper() + self.savedWarning, self.warnings = sparql.logger.warning, [] + sparql.logger.warning = lambda message, *args: self.warnings.append(message % args if args else message) + + def tearDown(self): + sparql._send, sparql.conf.parameters = self.saved, self.savedParams + sparql.conf.dumper = self.savedDumper + sparql.logger.warning = self.savedWarning + + def _truth(self): + _t, _p, boundary = sparql._detectBoolean(sparql.PLACE.GET, "q") + truth = sparql._makeOracle(sparql.PLACE.GET, "q", boundary) + self.assertIsNotNone(truth) + return truth + + def test_truncated_value_is_reported(self): + value = sparql._inferString(self._truth(), sparql._nthObject(0), maxLen=4) + self.assertEqual(value, _OBJECTS[0][:4]) + self.assertTrue(any("at least 4 characters" in _ for _ in self.warnings), self.warnings) + + def test_value_that_fits_is_silent(self): + value = sparql._inferString(self._truth(), sparql._nthObject(0), maxLen=64) + self.assertEqual(value, _OBJECTS[0]) + self.assertEqual(self.warnings, []) + + def test_saturated_predicate_count_is_reported_as_a_floor(self): + saved, sparql.SPARQL_MAX_PREDICATES = sparql.SPARQL_MAX_PREDICATES, 2 + try: + sparql._dumpGraph(self._truth()) + finally: + sparql.SPARQL_MAX_PREDICATES = saved + + self.assertTrue(any("hit the 2 cap" in _ for _ in self.warnings), self.warnings) + + if __name__ == "__main__": unittest.main(verbosity=2)