mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-30 05:21:15 +00:00
Update of tests
This commit is contained in:
parent
cb20a446ae
commit
2297c81309
32 changed files with 7177 additions and 7304 deletions
|
|
@ -4,26 +4,29 @@
|
|||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Mocked-oracle / canned-input coverage for three self-contained injection engines:
|
||||
Mocked-oracle / canned-input coverage for the self-contained extraction /
|
||||
inference engines under lib/techniques/*:
|
||||
|
||||
* lib/techniques/union/use.py - _oneShotUnionUse value extraction + unionUse
|
||||
* lib/techniques/union/use.py - _oneShotUnionUse / unionUse / configUnion
|
||||
* lib/techniques/error/use.py - _oneShotErrorUse / _errorFields / errorUse
|
||||
* lib/techniques/ldap/inject.py - boolean-blind LDAP oracle + blind char inference
|
||||
* lib/techniques/graphql/inject.py - schema walk, query building, blind-SQLi inference
|
||||
* lib/techniques/blind/inference.py - bisection / queryOutputLength edge branches
|
||||
|
||||
The established pattern (see tests/test_inference.py, tests/test_union_engine.py) is
|
||||
followed: the network seam (Request.queryPage / Request.getPage / the per-module _send /
|
||||
_gqlSend) is replaced by a deterministic in-process oracle that answers against a known
|
||||
secret, so the REAL extraction / parsing / bisection logic runs with no live target,
|
||||
The established pattern (see tests/test_inference_engine.py,
|
||||
tests/test_union_engine.py) is followed: the network seam (Request.queryPage /
|
||||
Request.getPage / the per-module _send / _gqlSend) and the forge/escape chain are
|
||||
replaced by a deterministic in-process oracle that answers against a known secret,
|
||||
so the REAL extraction / parsing / bisection logic runs with no live target,
|
||||
no network and no DBMS.
|
||||
|
||||
configUnion is already covered by tests/test_inference.py - not duplicated here.
|
||||
|
||||
stdlib unittest only; works on Python 2.7 and 3.x.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
|
@ -32,14 +35,26 @@ bootstrap()
|
|||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.datatype import AttribDict
|
||||
from lib.core.enums import PAYLOAD, PLACE
|
||||
from lib.core.common import decodeDbmsHexValue
|
||||
from lib.core.common import getCurrentThreadData
|
||||
from lib.core.common import hashDBWrite
|
||||
from lib.core.common import setTechnique
|
||||
from lib.core.enums import CHARSET_TYPE
|
||||
from lib.core.enums import PAYLOAD
|
||||
from lib.core.enums import PLACE
|
||||
from lib.core.exception import SqlmapSyntaxException
|
||||
from lib.core.settings import PARTIAL_VALUE_MARKER
|
||||
from lib.core.agent import agent
|
||||
from lib.core.unescaper import unescaper
|
||||
from lib.request.connect import Connect
|
||||
from lib.request.connect import Connect as Request
|
||||
from lib.utils.hashdb import HashDB
|
||||
|
||||
import lib.techniques.union.use as uu
|
||||
import lib.techniques.error.use as eu
|
||||
import lib.techniques.ldap.inject as ldap
|
||||
import lib.techniques.graphql.inject as gql
|
||||
import lib.techniques.blind.inference as inf
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
|
|
@ -278,6 +293,497 @@ class TestUnionUse(_UnionCase):
|
|||
self.assertIsNone(value)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# UNION-based: lib/techniques/union/use.py (partial / LIMIT-loop branches)
|
||||
# ===========================================================================
|
||||
|
||||
# Distinct from the scalar _UNION_VECTOR / _UU_CONF / _UU_KB above: these drive the
|
||||
# partial / LIMIT-loop path (NEGATIVE where, forcePartial on, jsonAgg disabled).
|
||||
_UNION_VECTOR_LIMIT = (1, 2, None, "", "", "NULL", PAYLOAD.WHERE.NEGATIVE, False, False, None, None)
|
||||
|
||||
_UU_CONF_LIMIT = {"hexConvert": False, "limitStart": 0, "limitStop": 0, "pageEncoding": None,
|
||||
"forcePartial": True, "disableJson": True, "binaryFields": None,
|
||||
"reportJson": False, "api": False, "threads": 1, "verbose": 0, "eta": False,
|
||||
"noTruncate": True, "uFrom": None}
|
||||
_UU_KB_LIMIT = {"jsonAggMode": False, "respTruncated": False, "unionDuplicates": False,
|
||||
"forcePartialUnion": False, "tableFrom": None, "unionTemplate": None,
|
||||
"nchar": False, "pageEncoding": None, "bruteMode": False, "partRun": None,
|
||||
"suppressResumeInfo": False, "threadContinue": True}
|
||||
|
||||
|
||||
class _UnionLimitCase(unittest.TestCase):
|
||||
"""Drive unionUse() down the partial / LIMIT-loop path (jsonAgg disabled, NEGATIVE where,
|
||||
forcePartial on). The forge chain is a pass-through; concatQuery records the per-row
|
||||
expression so the oracle can recover the LIMIT offset and answer from a known row set."""
|
||||
|
||||
def setUp(self):
|
||||
self._sc = {k: conf.get(k) for k in _UU_CONF_LIMIT}
|
||||
self._sk = {k: kb.get(k) for k in _UU_KB_LIMIT}
|
||||
self._sqp = Request.queryPage
|
||||
self._scounters = kb.get("counters")
|
||||
self._sinj_data = kb.injection.data
|
||||
self._shashdb = conf.get("hashDB")
|
||||
self._sbatch = conf.get("batch")
|
||||
self._s_forge = agent.forgeUnionQuery
|
||||
self._s_concat = agent.concatQuery
|
||||
self._s_payload = agent.payload
|
||||
self._s_escape = unescaper.escape
|
||||
self._s_lastexpr = getattr(agent, "_lastexpr", None)
|
||||
self._s_initTechnique = uu.initTechnique
|
||||
|
||||
for k, v in _UU_CONF_LIMIT.items():
|
||||
conf[k] = v
|
||||
for k, v in _UU_KB_LIMIT.items():
|
||||
kb[k] = v
|
||||
|
||||
conf.batch = True
|
||||
conf.hashDB = None
|
||||
kb.counters = {}
|
||||
|
||||
entry = AttribDict()
|
||||
entry.vector = _UNION_VECTOR_LIMIT
|
||||
entry.where = PAYLOAD.WHERE.NEGATIVE
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.UNION: entry}
|
||||
|
||||
# record the expression seen by each _oneShotUnionUse so the oracle can branch on it
|
||||
def rec_concat(expression, unpack=True):
|
||||
agent._lastexpr = expression
|
||||
return expression
|
||||
agent.concatQuery = rec_concat
|
||||
agent.forgeUnionQuery = lambda *a, **k: "UNION-FORGED"
|
||||
agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: "PAYLOAD"
|
||||
unescaper.escape = lambda expression, *a, **k: expression
|
||||
uu.initTechnique = lambda technique=None: None
|
||||
|
||||
self._s_columns = os.environ.get("COLUMNS")
|
||||
os.environ["COLUMNS"] = "80"
|
||||
|
||||
set_dbms("MySQL")
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._sc.items():
|
||||
conf[k] = v
|
||||
for k, v in self._sk.items():
|
||||
kb[k] = v
|
||||
conf.batch = self._sbatch
|
||||
Request.queryPage = self._sqp
|
||||
uu.Request.queryPage = self._sqp
|
||||
kb.counters = self._scounters
|
||||
kb.injection.data = self._sinj_data
|
||||
conf.hashDB = self._shashdb
|
||||
agent.forgeUnionQuery = self._s_forge
|
||||
agent.concatQuery = self._s_concat
|
||||
agent.payload = self._s_payload
|
||||
unescaper.escape = self._s_escape
|
||||
agent._lastexpr = self._s_lastexpr
|
||||
uu.initTechnique = self._s_initTechnique
|
||||
|
||||
if self._s_columns is None:
|
||||
os.environ.pop("COLUMNS", None)
|
||||
else:
|
||||
os.environ["COLUMNS"] = self._s_columns
|
||||
|
||||
def _install_row_oracle(self, rows, count=None):
|
||||
"""rows: list of tuples (per-row columns). Oracle answers COUNT and per-LIMIT rows
|
||||
from the recorded expression (agent._lastexpr), wrapping in real start/stop markers."""
|
||||
start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter
|
||||
total = count if count is not None else len(rows)
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
expr = getattr(agent, "_lastexpr", "") or ""
|
||||
if "COUNT" in expr.upper():
|
||||
body = str(total)
|
||||
else:
|
||||
m = re.search(r"LIMIT (\d+),1", expr)
|
||||
idx = int(m.group(1)) if m else 0
|
||||
row = rows[idx] if 0 <= idx < len(rows) else ("?",)
|
||||
body = delim.join(row)
|
||||
page = "%s%s%s" % (start, body, stop)
|
||||
return (page, AttribDict(), 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
uu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
|
||||
class TestUnionPartialDump(_UnionLimitCase):
|
||||
def test_multi_row_two_columns(self):
|
||||
rows = [("1", "alice"), ("2", "bob"), ("3", "carol")]
|
||||
self._install_row_oracle(rows)
|
||||
value = uu.unionUse("SELECT id,name FROM users")
|
||||
self.assertEqual(list(value), [["1", "alice"], ["2", "bob"], ["3", "carol"]])
|
||||
|
||||
def test_multi_row_single_column(self):
|
||||
rows = [("alice",), ("bob",)]
|
||||
self._install_row_oracle(rows)
|
||||
value = uu.unionUse("SELECT name FROM users")
|
||||
self.assertEqual([uu.unArrayizeValue(v) for v in value], ["alice", "bob"])
|
||||
|
||||
def test_query_count_matches_rows(self):
|
||||
# one COUNT query + one query per row = 4 UNION requests for 3 rows
|
||||
rows = [("1", "a"), ("2", "b"), ("3", "c")]
|
||||
self._install_row_oracle(rows)
|
||||
uu.unionUse("SELECT id,name FROM users")
|
||||
self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.UNION), 1 + len(rows))
|
||||
|
||||
def test_count_returns_zero_empty(self):
|
||||
# COUNT yields "0" -> empty-table sentinel (the function returns []), no row queries
|
||||
self._install_row_oracle([], count=0)
|
||||
value = uu.unionUse("SELECT id,name FROM users")
|
||||
self.assertEqual(value, [])
|
||||
|
||||
def test_single_row_count_one(self):
|
||||
# COUNT yields "1": the multi-row thread loop is skipped, falls through to one one-shot
|
||||
rows = [("solo",)]
|
||||
self._install_row_oracle(rows, count=1)
|
||||
value = uu.unionUse("SELECT name FROM users")
|
||||
self.assertEqual(uu.unArrayizeValue(value), "solo")
|
||||
|
||||
def test_length_limited_window(self):
|
||||
# conf.limitStart/limitStop windowing (dump=True): only rows in [start, stop) survive.
|
||||
# With limitStart=2, limitStop=4 over a 5-row table the engine COUNTs then walks
|
||||
# offsets 1..3 -> rows index 1,2,3 -> "b","c","d".
|
||||
conf.forcePartial = False
|
||||
conf.limitStart = 2
|
||||
conf.limitStop = 4
|
||||
rows = [("a",), ("b",), ("c",), ("d",), ("e",)]
|
||||
self._install_row_oracle(rows, count=5)
|
||||
value = uu.unionUse("SELECT name FROM users", dump=True)
|
||||
self.assertEqual([uu.unArrayizeValue(v) for v in value], ["b", "c", "d"])
|
||||
|
||||
|
||||
class TestOneShotUnionUseLimited(_UnionLimitCase):
|
||||
"""_oneShotUnionUse called directly with the `limited` flag set (the per-row caller's mode)."""
|
||||
|
||||
def test_limited_single_row(self):
|
||||
start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter
|
||||
body = delim.join(("7", "zed"))
|
||||
page = "%s%s%s" % (start, body, stop)
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
return (page, AttribDict(), 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
uu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
retVal = uu._oneShotUnionUse("SELECT id,name FROM t LIMIT 0,1", unpack=True, limited=True)
|
||||
self.assertEqual(retVal, page)
|
||||
# one wrapped multi-column entry -> one row of two columns
|
||||
self.assertEqual(list(uu.parseUnionPage(retVal)), [["7", "zed"]])
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# ERROR-based: lib/techniques/error/use.py
|
||||
# ===========================================================================
|
||||
|
||||
# An error injection vector is consumed by agent.prefixQuery/suffixQuery (here stubbed
|
||||
# to a pass-through that just yields the "[QUERY]" placeholder the engine substitutes into).
|
||||
_ERR_VECTOR = ("pref", "suff", 2, "", "", "NULL", PAYLOAD.WHERE.ORIGINAL, False, False, None, None)
|
||||
|
||||
_ERR_CONF = {"hexConvert": False, "noEscape": None, "verbose": 0, "api": False,
|
||||
"reportJson": False, "limitStart": 0, "limitStop": 0, "noTruncate": True,
|
||||
"threads": 1, "eta": False}
|
||||
_ERR_KB = {"testMode": True, "safeCharEncode": False, "errorChunkLength": None,
|
||||
"fileReadMode": False, "bruteMode": False, "threadContinue": True,
|
||||
"suppressResumeInfo": False, "dumpTable": None}
|
||||
|
||||
|
||||
class _ErrorCase(unittest.TestCase):
|
||||
"""Stub the forge/escape/transport seam so _oneShotErrorUse's OWN parsing (marker
|
||||
extraction, trim repair, char restoration) is what is exercised."""
|
||||
|
||||
def setUp(self):
|
||||
self._sc = {k: conf.get(k) for k in _ERR_CONF}
|
||||
self._sk = {k: kb.get(k) for k in _ERR_KB}
|
||||
self._sqp = Request.queryPage
|
||||
self._scounters = kb.get("counters")
|
||||
self._stechnique = kb.get("technique")
|
||||
self._sinj_data = kb.injection.data
|
||||
self._shashdb = conf.get("hashDB")
|
||||
self._sbatch = conf.get("batch")
|
||||
|
||||
self._s_prefix = agent.prefixQuery
|
||||
self._s_suffix = agent.suffixQuery
|
||||
self._s_payload = agent.payload
|
||||
self._s_nullcast = agent.nullAndCastField
|
||||
self._s_escape = unescaper.escape
|
||||
|
||||
# restore thread state we touch
|
||||
td = getCurrentThreadData()
|
||||
self._s_td_uid = td.lastRequestUID
|
||||
self._s_td_httperr = td.lastHTTPError
|
||||
self._s_td_redirect = td.lastRedirectMsg
|
||||
|
||||
for k, v in _ERR_CONF.items():
|
||||
conf[k] = v
|
||||
for k, v in _ERR_KB.items():
|
||||
kb[k] = v
|
||||
|
||||
conf.batch = True
|
||||
conf.hashDB = None # disable session resume in these tests
|
||||
kb.counters = {}
|
||||
kb.technique = PAYLOAD.TECHNIQUE.ERROR
|
||||
setTechnique(PAYLOAD.TECHNIQUE.ERROR)
|
||||
|
||||
entry = AttribDict()
|
||||
entry.vector = _ERR_VECTOR
|
||||
entry.where = PAYLOAD.WHERE.ORIGINAL
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.ERROR: entry}
|
||||
|
||||
# pass-through forge chain: the produced payload text carries the injExpression so
|
||||
# the oracle can (optionally) branch on the requested field; agent.payload returns
|
||||
# exactly the newValue it is handed.
|
||||
agent.prefixQuery = lambda vector, *a, **k: "[QUERY]"
|
||||
agent.suffixQuery = lambda query, *a, **k: query
|
||||
agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: newValue
|
||||
agent.nullAndCastField = lambda field: field
|
||||
unescaper.escape = lambda expression, *a, **k: expression
|
||||
|
||||
# getConsoleWidth() in _errorFields hits curses with no tty; pin COLUMNS so it doesn't
|
||||
self._s_columns = os.environ.get("COLUMNS")
|
||||
os.environ["COLUMNS"] = "80"
|
||||
|
||||
set_dbms("MySQL")
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._sc.items():
|
||||
conf[k] = v
|
||||
for k, v in self._sk.items():
|
||||
kb[k] = v
|
||||
conf.batch = self._sbatch
|
||||
Request.queryPage = self._sqp
|
||||
eu.Request.queryPage = self._sqp
|
||||
kb.counters = self._scounters
|
||||
kb.technique = self._stechnique
|
||||
setTechnique(None)
|
||||
kb.injection.data = self._sinj_data
|
||||
conf.hashDB = self._shashdb
|
||||
|
||||
agent.prefixQuery = self._s_prefix
|
||||
agent.suffixQuery = self._s_suffix
|
||||
agent.payload = self._s_payload
|
||||
agent.nullAndCastField = self._s_nullcast
|
||||
unescaper.escape = self._s_escape
|
||||
|
||||
td = getCurrentThreadData()
|
||||
td.lastRequestUID = self._s_td_uid
|
||||
td.lastHTTPError = self._s_td_httperr
|
||||
td.lastRedirectMsg = self._s_td_redirect
|
||||
|
||||
if self._s_columns is None:
|
||||
os.environ.pop("COLUMNS", None)
|
||||
else:
|
||||
os.environ["COLUMNS"] = self._s_columns
|
||||
|
||||
@staticmethod
|
||||
def _wrap(body):
|
||||
return "%s%s%s" % (kb.chars.start, body, kb.chars.stop)
|
||||
|
||||
def _install_page(self, page):
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
return (page, {}, 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
def _install_field_oracle(self, mapping):
|
||||
"""Oracle that branches on which field name appears in the forged payload (the
|
||||
injExpression is passed through agent.payload unchanged, so it is in `payload`)."""
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
body = "?"
|
||||
for field, value in mapping.items():
|
||||
if field in (payload or ""):
|
||||
body = value
|
||||
break
|
||||
page = "<html>%s</html>" % self._wrap(body)
|
||||
return (page, {}, 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
|
||||
class TestOneShotErrorUse(_ErrorCase):
|
||||
def test_single_value_extracted(self):
|
||||
self._install_page("<html>%s</html>" % self._wrap("admin"))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT name"), "admin")
|
||||
|
||||
def test_space_char_restored(self):
|
||||
# the kb.chars.space placeholder (used to survive transport) is restored to a literal
|
||||
# space by _errorReplaceChars. NOTE: the other char tokens (dollar/at/hash) are random
|
||||
# per-run and may collide with the space token, so only space is asserted here.
|
||||
body = "hello%sworld" % kb.chars.space
|
||||
self._install_page(self._wrap(body))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hello world")
|
||||
|
||||
def test_no_markers_returns_none(self):
|
||||
self._install_page("<html>no useful markers here</html>")
|
||||
self.assertIsNone(eu._oneShotErrorUse("SELECT x"))
|
||||
|
||||
def test_html_entities_unescaped(self):
|
||||
# retVal goes through htmlUnescape() and <br> -> newline on the way out
|
||||
self._install_page(self._wrap("a & b<br>c"))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "a & b\nc")
|
||||
|
||||
def test_counter_incremented(self):
|
||||
self._install_page(self._wrap("v"))
|
||||
eu._oneShotErrorUse("SELECT x")
|
||||
self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.ERROR), 1)
|
||||
|
||||
def test_field_substituted_into_expression(self):
|
||||
# field is replaced (once) by nullAndCastField(field) before forging; the oracle keys
|
||||
# on the field name in the resulting payload to prove the right column was requested
|
||||
self._install_field_oracle({"surname": "Smith"})
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT surname FROM users", field="surname"), "Smith")
|
||||
|
||||
def test_recovered_from_http_error_body(self):
|
||||
# page itself carries no markers; the delimited value lives in the 500-error body
|
||||
td = getCurrentThreadData()
|
||||
td.lastRequestUID = 4242
|
||||
td.lastHTTPError = (4242, 500, "<html>%s</html>" % self._wrap("from-error-page"))
|
||||
self._install_page("<html>regular page, no markers</html>")
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "from-error-page")
|
||||
|
||||
def test_recovered_from_response_header(self):
|
||||
# neither page nor error body has it; it is carried back in a response header value
|
||||
body = self._wrap("hdr-value")
|
||||
page = "<html>nothing</html>"
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
headers = {"X-Leak": body}
|
||||
return (page, headers, 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hdr-value")
|
||||
|
||||
def test_hex_convert_decoded(self):
|
||||
# --hex: the delimited body is a hex string decoded by decodeDbmsHexValue
|
||||
conf.hexConvert = True
|
||||
self._install_page(self._wrap("48656C6C6F")) # "Hello"
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "Hello")
|
||||
|
||||
def test_empty_value_between_markers(self):
|
||||
self._install_page(self._wrap(""))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "")
|
||||
|
||||
|
||||
class TestOneShotErrorUseChunking(_ErrorCase):
|
||||
"""The MySQL multi-chunk reassembly loop: with kb.errorChunkLength set, output >= chunk
|
||||
length triggers another request at the next offset; the engine concatenates the pieces."""
|
||||
|
||||
def setUp(self):
|
||||
_ErrorCase.setUp(self)
|
||||
kb.testMode = False # honor the chunk-offset loop
|
||||
kb.errorChunkLength = 4 # pre-set so the length-probe search is skipped
|
||||
conf.verbose = 0
|
||||
|
||||
def test_multi_chunk_reassembled(self):
|
||||
# secret returned 4 chars at a time via SUBSTRING(expr, offset, 4); the loop walks offsets
|
||||
secret = "abcdefghij"
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
# MySQL substring is rendered as MID((field),offset,length)
|
||||
m = re.search(r"(?:MID|SUBSTRING)\(.*?,(\d+),(\d+)\)", payload or "")
|
||||
if m:
|
||||
off, length = int(m.group(1)), int(m.group(2))
|
||||
chunk = secret[off - 1:off - 1 + length]
|
||||
else:
|
||||
chunk = secret
|
||||
return ("%s%s%s" % (kb.chars.start, chunk, kb.chars.stop), {}, 200) if content else None
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
# a field is required for the SUBSTRING windowing branch to engage
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT data FROM t", field="data"), secret)
|
||||
|
||||
|
||||
class TestErrorFields(_ErrorCase):
|
||||
"""_errorFields iterates the field list, recovering one value per column."""
|
||||
|
||||
def test_multi_field_values(self):
|
||||
self._install_field_oracle({"user": "alice", "pass": "s3cr3t"})
|
||||
values = eu._errorFields("SELECT user,pass FROM t", "user,pass",
|
||||
["user", "pass"], suppressOutput=True)
|
||||
self.assertEqual(values, ["alice", "s3cr3t"])
|
||||
|
||||
def test_single_field_value(self):
|
||||
self._install_field_oracle({"email": "root@localhost"})
|
||||
values = eu._errorFields("SELECT email FROM t", "email", ["email"], suppressOutput=True)
|
||||
self.assertEqual(values, ["root@localhost"])
|
||||
|
||||
def test_empty_field_yields_null(self):
|
||||
# a field listed in emptyFields is short-circuited to the NULL sentinel (no oracle hit)
|
||||
from lib.core.settings import NULL
|
||||
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("oracle must not be called for an empty field")
|
||||
Request.queryPage = staticmethod(boom)
|
||||
eu.Request.queryPage = staticmethod(boom)
|
||||
values = eu._errorFields("SELECT col FROM t", "col", ["col"],
|
||||
emptyFields=["col"], suppressOutput=True)
|
||||
self.assertEqual(values, [NULL])
|
||||
|
||||
def test_rownum_field_skipped(self):
|
||||
# a "ROWNUM " field is skipped entirely (Oracle limit artifact)
|
||||
self._install_field_oracle({"name": "bob"})
|
||||
values = eu._errorFields("SELECT name FROM t", "name",
|
||||
["ROWNUM x", "name"], suppressOutput=True)
|
||||
self.assertEqual(values, ["bob"])
|
||||
|
||||
|
||||
class TestErrorUse(_ErrorCase):
|
||||
"""errorUse() orchestration. initTechnique() needs a full injection session; stub it so
|
||||
the orchestration + _errorFields extraction + result shaping is what is exercised."""
|
||||
|
||||
def setUp(self):
|
||||
_ErrorCase.setUp(self)
|
||||
self._s_initTechnique = eu.initTechnique
|
||||
eu.initTechnique = lambda technique=None: None
|
||||
|
||||
def tearDown(self):
|
||||
eu.initTechnique = self._s_initTechnique
|
||||
_ErrorCase.tearDown(self)
|
||||
|
||||
def test_scalar_value(self):
|
||||
# scalar expression (no FROM): single one-shot extraction, unwrapped from the list
|
||||
self._install_page(self._wrap("5.7.40"))
|
||||
self.assertEqual(eu.errorUse("SELECT VERSION()"), "5.7.40")
|
||||
|
||||
def test_scalar_no_output_none(self):
|
||||
self._install_page("<html>no markers</html>")
|
||||
self.assertIsNone(eu.errorUse("SELECT VERSION()"))
|
||||
|
||||
def test_multi_row_dump(self):
|
||||
# dump=True over a FROM-table query: errorUse COUNTs the rows then LIMIT-walks them,
|
||||
# reconstructing each row's single column value in order
|
||||
conf.limitStart = 1
|
||||
conf.limitStop = 3
|
||||
rows = {0: "alice", 1: "bob", 2: "carol"}
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
nv = payload or ""
|
||||
if "COUNT" in nv.upper():
|
||||
body = "3"
|
||||
else:
|
||||
m = re.search(r"LIMIT (\d+),1", nv)
|
||||
idx = int(m.group(1)) if m else 0
|
||||
body = rows.get(idx, "?")
|
||||
return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
value = eu.errorUse("SELECT name FROM users", dump=True)
|
||||
self.assertEqual([eu.unArrayizeValue(v) for v in value], ["alice", "bob", "carol"])
|
||||
|
||||
def test_dump_zero_count_returns_empty(self):
|
||||
# COUNT yields "0" (non-positive) -> the query returned no output -> None
|
||||
conf.limitStart = 1
|
||||
conf.limitStop = 10
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
nv = payload or ""
|
||||
body = "0" if "COUNT" in nv.upper() else "x"
|
||||
return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
# a "0" count is truthy-but-not-positive -> empty-table sentinel (returns [])
|
||||
self.assertEqual(eu.errorUse("SELECT name FROM users", dump=True), [])
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# LDAP: lib/techniques/ldap/inject.py
|
||||
# ===========================================================================
|
||||
|
|
@ -371,7 +877,6 @@ class _LdapOracleCase(unittest.TestCase):
|
|||
conf.cookieDel = None
|
||||
|
||||
directory = self.DIRECTORY
|
||||
sentinel = ldap.SENTINEL
|
||||
|
||||
def fake_send(place, parameter, value):
|
||||
assertions = re.findall(r"\((\w+)=([^()]*)", value)
|
||||
|
|
@ -765,5 +1270,251 @@ class TestGraphqlParseRows(unittest.TestCase):
|
|||
self.assertIn("| 1 | 22 |", out)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Blind inference: lib/techniques/blind/inference.py
|
||||
# ===========================================================================
|
||||
|
||||
# bisection forges: safeStringFormat(payload, (expression, idx, posValue)); '>' is the
|
||||
# greater-char marker (swapped to '=' on the final equality check). A parseable template
|
||||
# lets the mock oracle recover (idx, operator, threshold) and answer against a known secret.
|
||||
TEMPLATE = "EXPR=%s IDX=%d CMP>%d"
|
||||
_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)")
|
||||
|
||||
# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path
|
||||
_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False,
|
||||
"charset": None, "firstChar": None, "lastChar": None, "timeSec": 5, "eta": False,
|
||||
"repair": False, "flushSession": None, "freshQueries": None, "hashDB": None}
|
||||
_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False,
|
||||
"disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False,
|
||||
"resumeValues": True, "inferenceMode": False}
|
||||
|
||||
|
||||
class _InferenceCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._saved_conf = {k: conf.get(k) for k in _CONF}
|
||||
self._saved_kb = {k: kb.get(k) for k in _KB}
|
||||
self._saved_qp = Connect.queryPage
|
||||
self._saved_processChar = kb.data.get("processChar")
|
||||
for k, v in _CONF.items():
|
||||
conf[k] = v
|
||||
for k, v in _KB.items():
|
||||
kb[k] = v
|
||||
kb.data.processChar = None
|
||||
set_dbms("MySQL")
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
for k, v in self._saved_kb.items():
|
||||
kb[k] = v
|
||||
kb.data.processChar = self._saved_processChar
|
||||
Connect.queryPage = self._saved_qp
|
||||
inf.Request.queryPage = self._saved_qp
|
||||
|
||||
def _install_oracle(self, secret):
|
||||
def oracle(payload=None, *args, **kwargs):
|
||||
m = _PARSE.search(payload)
|
||||
idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3))
|
||||
ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0
|
||||
return (ch > threshold) if op == ">" else (ch == threshold)
|
||||
|
||||
Connect.queryPage = staticmethod(oracle)
|
||||
inf.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
@staticmethod
|
||||
def _reset_thread():
|
||||
td = getCurrentThreadData()
|
||||
td.shared.value = ""
|
||||
td.shared.index = [0]
|
||||
td.shared.start = 0
|
||||
td.shared.count = 0
|
||||
|
||||
def _bisect(self, secret, expression="SELECT secret", length=None, **kwargs):
|
||||
self._install_oracle(secret)
|
||||
self._reset_thread()
|
||||
if length is None:
|
||||
length = len(secret)
|
||||
return inf.bisection(TEMPLATE, expression, length=length, **kwargs)
|
||||
|
||||
|
||||
class TestTrivialReturns(_InferenceCase):
|
||||
def test_none_payload(self):
|
||||
# payload is None -> (0, None) without ever touching the oracle
|
||||
self.assertEqual(inf.bisection(None, "SELECT x"), (0, None))
|
||||
|
||||
def test_zero_length(self):
|
||||
# length == 0 -> (0, "") short-circuit
|
||||
self._install_oracle("ignored")
|
||||
self._reset_thread()
|
||||
self.assertEqual(inf.bisection(TEMPLATE, "SELECT x", length=0), (0, ""))
|
||||
|
||||
|
||||
class TestRangeLimiting(_InferenceCase):
|
||||
SECRET = "ABCDEFGH"
|
||||
|
||||
def test_first_char_arg(self):
|
||||
# firstChar=3 -> start from the 3rd character (1-based) -> drop "AB"
|
||||
_, value = self._bisect(self.SECRET, firstChar=3)
|
||||
self.assertEqual(value, "CDEFGH")
|
||||
|
||||
def test_last_char_arg(self):
|
||||
# lastChar=4 -> stop after the 4th character
|
||||
_, value = self._bisect(self.SECRET, lastChar=4)
|
||||
self.assertEqual(value, "ABCD")
|
||||
|
||||
def test_conf_first_char(self):
|
||||
conf.firstChar = 4
|
||||
_, value = self._bisect(self.SECRET)
|
||||
self.assertEqual(value, "DEFGH")
|
||||
|
||||
def test_conf_last_char(self):
|
||||
conf.lastChar = 3
|
||||
_, value = self._bisect(self.SECRET)
|
||||
self.assertEqual(value, "ABC")
|
||||
|
||||
def test_first_and_last_window(self):
|
||||
# combined window: chars 3..6 inclusive -> "CDEF"
|
||||
_, value = self._bisect(self.SECRET, firstChar=3, lastChar=6)
|
||||
self.assertEqual(value, "CDEF")
|
||||
|
||||
|
||||
class TestHexConvert(_InferenceCase):
|
||||
def test_hex_output_decoded(self):
|
||||
# --hex: the retrieved value is a hex string the engine decodes on the way out
|
||||
conf.hexConvert = True
|
||||
hexed = "48656C6C6F" # "Hello"
|
||||
_, value = self._bisect(hexed)
|
||||
self.assertEqual(value, "Hello")
|
||||
self.assertEqual(value, decodeDbmsHexValue(hexed))
|
||||
|
||||
|
||||
class TestProcessCharHook(_InferenceCase):
|
||||
def test_process_char_applied_to_each_char(self):
|
||||
# kb.data.processChar transforms every assembled character
|
||||
kb.data.processChar = lambda c: c.upper()
|
||||
_, value = self._bisect("abcde")
|
||||
self.assertEqual(value, "ABCDE")
|
||||
|
||||
|
||||
class TestResumeFromHashDB(_InferenceCase):
|
||||
"""bisection() consults the session store first (hashDBRetrieve(checkConf=True)).
|
||||
Exercised against a REAL temporary SQLite HashDB (same approach as test_hashdb.py)."""
|
||||
|
||||
def setUp(self):
|
||||
_InferenceCase.setUp(self)
|
||||
fd, self.path = tempfile.mkstemp(suffix=".sqlite")
|
||||
os.close(fd)
|
||||
os.remove(self.path) # HashDB creates it lazily
|
||||
conf.hashDB = HashDB(self.path)
|
||||
# hashDBRetrieve/Write key off these
|
||||
self._saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port"))
|
||||
conf.hostname = "test.invalid"
|
||||
conf.path = "/"
|
||||
conf.port = 80
|
||||
|
||||
def tearDown(self):
|
||||
conf.hostname, conf.path, conf.port = self._saved_loc
|
||||
try:
|
||||
conf.hashDB.closeAll()
|
||||
except Exception:
|
||||
pass
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
_InferenceCase.tearDown(self)
|
||||
|
||||
def test_full_value_resumed(self):
|
||||
# a complete cached value short-circuits the whole bisection (0 queries)
|
||||
hashDBWrite("SELECT cached", "RESUMED")
|
||||
conf.hashDB.flush()
|
||||
count, value = self._bisect("ignored-secret", expression="SELECT cached", length=7)
|
||||
self.assertEqual(value, "RESUMED")
|
||||
self.assertEqual(count, 0)
|
||||
|
||||
def test_partial_value_continued(self):
|
||||
# a PARTIAL_VALUE_MARKER value is resumed-from: bisection keeps the prefix
|
||||
# and extracts only the remaining characters
|
||||
kb.inferenceMode = True # partial markers are honored only in inference mode
|
||||
hashDBWrite("SELECT partial", "%sAB" % PARTIAL_VALUE_MARKER)
|
||||
conf.hashDB.flush()
|
||||
count, value = self._bisect("ABCDE", expression="SELECT partial", length=5)
|
||||
self.assertEqual(value, "ABCDE")
|
||||
self.assertGreater(count, 0) # it did real work for "CDE"
|
||||
|
||||
|
||||
class TestQueryOutputLength(_InferenceCase):
|
||||
def test_length_retrieved(self):
|
||||
# queryOutputLength forges a LENGTH() expression and runs bisection with the
|
||||
# DIGITS charset; the mock "secret" is the textual length itself
|
||||
self._install_oracle("42")
|
||||
self._reset_thread()
|
||||
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 42)
|
||||
|
||||
def test_length_single_digit(self):
|
||||
self._install_oracle("7")
|
||||
self._reset_thread()
|
||||
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 7)
|
||||
|
||||
def test_digits_charset_extracts_number(self):
|
||||
# direct bisection with the DIGITS charset (queryOutputLength's inner call)
|
||||
_, value = self._bisect("2026", charsetType=CHARSET_TYPE.DIGITS)
|
||||
self.assertEqual(value, "2026")
|
||||
|
||||
|
||||
class TestConfigUnion(unittest.TestCase):
|
||||
"""lib/techniques/union/use.py configUnion - pure parsing of --union-char / --union-cols."""
|
||||
|
||||
_CONF = {"uChar": None, "uCols": None, "uColsStart": 1, "uColsStop": 50}
|
||||
|
||||
def setUp(self):
|
||||
self._saved = {k: conf.get(k) for k in self._CONF}
|
||||
self._saved_uchar = kb.get("uChar")
|
||||
for k, v in self._CONF.items():
|
||||
conf[k] = v
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved.items():
|
||||
conf[k] = v
|
||||
kb.uChar = self._saved_uchar
|
||||
|
||||
def test_char_and_range(self):
|
||||
uu.configUnion(char="NULL", columns="2-6")
|
||||
self.assertEqual(kb.uChar, "NULL")
|
||||
self.assertEqual((conf.uColsStart, conf.uColsStop), (2, 6))
|
||||
|
||||
def test_single_column(self):
|
||||
uu.configUnion(char="NULL", columns="4")
|
||||
self.assertEqual((conf.uColsStart, conf.uColsStop), (4, 4))
|
||||
|
||||
def test_uchar_substitution_quoted(self):
|
||||
# conf.uChar (non-digit) gets quoted and substituted into the [CHAR] template
|
||||
conf.uChar = "test"
|
||||
uu.configUnion(char="x[CHAR]x", columns="1")
|
||||
self.assertEqual(kb.uChar, "x'test'x")
|
||||
|
||||
def test_uchar_substitution_digit(self):
|
||||
# a digit conf.uChar is substituted unquoted
|
||||
conf.uChar = "88"
|
||||
uu.configUnion(char="[CHAR]", columns="1")
|
||||
self.assertEqual(kb.uChar, "88")
|
||||
|
||||
def test_conf_ucols_overrides_columns_arg(self):
|
||||
# conf.uCols takes precedence over the columns argument
|
||||
conf.uCols = "3-9"
|
||||
uu.configUnion(char="NULL", columns="1-2")
|
||||
self.assertEqual((conf.uColsStart, conf.uColsStop), (3, 9))
|
||||
|
||||
def test_non_integer_range_raises(self):
|
||||
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="abc")
|
||||
|
||||
def test_inverted_range_raises(self):
|
||||
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="9-2")
|
||||
|
||||
def test_non_string_char_ignored(self):
|
||||
# a non-string char leaves kb.uChar untouched (early return)
|
||||
kb.uChar = "SENTINEL"
|
||||
uu.configUnion(char=None, columns="1")
|
||||
self.assertEqual(kb.uChar, "SENTINEL")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue