|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +""" |
| 4 | +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) |
| 5 | +See the file 'LICENSE' for copying permission |
| 6 | +
|
| 7 | +Payload assembly helpers in lib/core/agent.py. |
| 8 | +
|
| 9 | +These are the (mostly) DBMS-independent string transforms that wrap, fold and |
| 10 | +clean a payload on its way to the wire: prefix/suffix, payload delimiters, |
| 11 | +field extraction, CONCAT folding, and RAND-marker cleanup. All values below |
| 12 | +were probed from real output, not assumed. |
| 13 | +""" |
| 14 | + |
| 15 | +import os |
| 16 | +import sys |
| 17 | +import unittest |
| 18 | + |
| 19 | +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) |
| 20 | +from _testutils import bootstrap, set_dbms |
| 21 | +bootstrap() |
| 22 | + |
| 23 | +from lib.core.agent import agent |
| 24 | +from lib.core.enums import DBMS |
| 25 | +from lib.core.settings import PAYLOAD_DELIMITER |
| 26 | + |
| 27 | + |
| 28 | +class TestPayloadDelimiters(unittest.TestCase): |
| 29 | + def test_add(self): |
| 30 | + self.assertEqual(agent.addPayloadDelimiters("1 AND 1=1"), |
| 31 | + "%s1 AND 1=1%s" % (PAYLOAD_DELIMITER, PAYLOAD_DELIMITER)) |
| 32 | + |
| 33 | + def test_remove(self): |
| 34 | + wrapped = "%spayload%s" % (PAYLOAD_DELIMITER, PAYLOAD_DELIMITER) |
| 35 | + self.assertEqual(agent.removePayloadDelimiters(wrapped), "payload") |
| 36 | + |
| 37 | + def test_remove_none_is_none(self): |
| 38 | + self.assertIsNone(agent.removePayloadDelimiters(None)) |
| 39 | + |
| 40 | + def test_roundtrip(self): |
| 41 | + for p in ["1=1", "1 AND SLEEP(5)", "' OR '1'='1", "", "a%sb" % "x"]: |
| 42 | + self.assertEqual(agent.removePayloadDelimiters(agent.addPayloadDelimiters(p)), p, |
| 43 | + msg="delimiter round-trip for %r" % p) |
| 44 | + |
| 45 | + |
| 46 | +class TestPrefixSuffix(unittest.TestCase): |
| 47 | + def test_prefix_default_pads_space(self): |
| 48 | + # with no configured prefix, a single leading space is prepended |
| 49 | + self.assertEqual(agent.prefixQuery("1=1"), " 1=1") |
| 50 | + |
| 51 | + def test_suffix_default_identity(self): |
| 52 | + self.assertEqual(agent.suffixQuery("1=1"), "1=1") |
| 53 | + |
| 54 | + |
| 55 | +class TestGetFields(unittest.TestCase): |
| 56 | + def test_extracts_select_list(self): |
| 57 | + # getFields(query) returns an 8-tuple; the fields-bearing slots are: |
| 58 | + # [0],[1] = regex match objects for the SELECT/expression (must be found, not None) |
| 59 | + # [5] = parsed field list, [6] = raw fields string |
| 60 | + # (asserting the match objects guards against a refactor that silently shifts the tuple) |
| 61 | + result = agent.getFields("SELECT a,b FROM t") |
| 62 | + self.assertIsNotNone(result[0], msg="getFields did not match the SELECT") |
| 63 | + self.assertEqual(result[5], ["a", "b"]) |
| 64 | + self.assertEqual(result[6], "a,b") |
| 65 | + |
| 66 | + |
| 67 | +class TestConcatQuery(unittest.TestCase): |
| 68 | + def test_mysql_concat_folding(self): |
| 69 | + set_dbms(DBMS.MYSQL) |
| 70 | + q = agent.concatQuery("SELECT a FROM t") |
| 71 | + # folds the field through CONCAT with the start/stop delimiters and keeps the FROM |
| 72 | + self.assertTrue(q.startswith("CONCAT("), msg=q) |
| 73 | + self.assertIn("IFNULL(CAST(a AS NCHAR),' ')", q) |
| 74 | + self.assertTrue(q.endswith("FROM t"), msg=q) |
| 75 | + |
| 76 | + |
| 77 | +class TestCleanupPayload(unittest.TestCase): |
| 78 | + def test_randnum_marker_replaced_with_digits(self): |
| 79 | + out = agent.cleanupPayload("SELECT [RANDNUM]") |
| 80 | + self.assertNotIn("[RANDNUM]", out, msg="marker not replaced: %r" % out) # actually substituted |
| 81 | + self.assertTrue(out.startswith("SELECT "), msg=out) |
| 82 | + self.assertTrue(out.split()[-1].isdigit(), msg=out) # ...and replaced with a concrete number |
| 83 | + |
| 84 | + |
| 85 | +if __name__ == "__main__": |
| 86 | + unittest.main(verbosity=2) |
0 commit comments