Skip to content

Commit cb8298d

Browse files
committed
Couple of trivial bug fixes
1 parent 579e559 commit cb8298d

15 files changed

Lines changed: 195 additions & 32 deletions

File tree

‎data/xml/queries.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1050,7 +1050,7 @@
10501050
<hostname query="SELECT MIN(node_name) FROM v_catalog.nodes"/>
10511051
<table_comment query="SELECT comment FROM v_catalog.comments WHERE object_type='TABLE' AND object_schema='%s' AND object_name='%s'"/>
10521052
<!-- NOTE: Vertica uses "projection columns" in case of column comments (e.g. testusers_super.surname) -->
1053-
<column_comment query="SELECT comment FROM v_catalog.comments WHERE object_type='COLUMN' AND object_schema='%s' AND object_name LIKE '%.%s'"/>
1053+
<column_comment query="SELECT comment FROM v_catalog.comments WHERE object_type='COLUMN' AND object_schema='%s' AND object_name LIKE '%s%%.%s'"/>
10541054
<is_dba query="(SELECT is_super_user FROM v_catalog.users WHERE user_name=CURRENT_USER OFFSET 0 LIMIT 1)"/>
10551055
<check_udf query="(SELECT procedure_name='%s' FROM v_catalog.user_procedures WHERE procedure_name='%s' OFFSET 0 LIMIT 1)"/>
10561056
<users>

‎lib/core/common.py‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4300,19 +4300,19 @@ def decodeStringEscape(value):
43004300
43014301
>>> decodeStringEscape("a" + chr(92) + "tb") == "a" + chr(9) + "b"
43024302
True
4303+
>>> decodeStringEscape(chr(92) + chr(0)) == chr(92) + chr(0) # a NUL in the data must be preserved, not rewritten to a backslash
4304+
True
43034305
"""
43044306

43054307
retVal = value
43064308

43074309
if value and '\\' in value:
4308-
# Note: shield an escaped backslash ('\\\\') behind a marker BEFORE decoding the whitespace
4309-
# escapes, then restore it - otherwise decoding '\\\\' -> '\\' first turns a literal '\\n'
4310-
# into a newline (i.e. the round-trip with encodeStringEscape was not lossless)
4311-
_marker = "\x00"
4312-
retVal = retVal.replace("\\\\", _marker)
4313-
for _ in string.whitespace.replace(" ", ""):
4314-
retVal = retVal.replace(repr(_).strip("'"), _)
4315-
retVal = retVal.replace(_marker, "\\")
4310+
# Note: single left-to-right pass so an escaped backslash ('\\\\') shields the next char
4311+
# (a literal '\\n' stays '\\n', not a newline) WITHOUT a sentinel that could collide with a
4312+
# pre-existing byte (e.g. a NUL in the data) and get rewritten on restore
4313+
_mapping = dict((repr(_).strip("'"), _) for _ in string.whitespace.replace(" ", ""))
4314+
_mapping["\\\\"] = "\\"
4315+
retVal = re.sub("|".join(re.escape(_) for _ in ["\\\\"] + list(_mapping)), lambda match: _mapping[match.group(0)], retVal)
43164316

43174317
return retVal
43184318

‎lib/core/dump.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
from lib.core.exception import SqlmapSystemException
4949
from lib.core.exception import SqlmapValueException
5050
from lib.core.replication import Replication
51-
from lib.core.settings import CHECK_SQLITE_TYPE_THRESHOLD
5251
from lib.core.settings import DUMP_FILE_BUFFER_SIZE
5352
from lib.core.settings import HTML_DUMP_CSS_STYLE
5453
from lib.core.settings import IS_WIN
@@ -554,7 +553,11 @@ def dbTableValues(self, tableValues):
554553
if column != "__infos__":
555554
colType = Replication.INTEGER
556555

557-
for i in xrange(min(CHECK_SQLITE_TYPE_THRESHOLD, len(tableValues[column]['values']))):
556+
# Note: the type must hold for EVERY value that will be inserted - sampling only a
557+
# prefix would type the column INTEGER/REAL while a later leading-zero/signed/overflow
558+
# value gets silently rewritten by SQLite's affinity (the INTEGER scan breaks early on
559+
# the first non-conforming value, so a genuine TEXT column costs almost nothing)
560+
for i in xrange(len(tableValues[column]['values'])):
558561
value = tableValues[column]['values'][i]
559562
try:
560563
if not value or value == " ": # NULL
@@ -571,7 +574,7 @@ def dbTableValues(self, tableValues):
571574
if colType is None:
572575
colType = Replication.REAL
573576

574-
for i in xrange(min(CHECK_SQLITE_TYPE_THRESHOLD, len(tableValues[column]['values']))):
577+
for i in xrange(len(tableValues[column]['values'])):
575578
value = tableValues[column]['values'][i]
576579
try:
577580
if not value or value == " ": # NULL

‎lib/core/settings.py‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.7.253"
23+
VERSION = "1.10.8.0"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
@@ -1407,9 +1407,6 @@
14071407
# Check for empty columns only if table is sufficiently large
14081408
CHECK_ZERO_COLUMNS_THRESHOLD = 10
14091409

1410-
# Threshold for checking types of columns in case of SQLite dump format
1411-
CHECK_SQLITE_TYPE_THRESHOLD = 100
1412-
14131410
# Boldify all logger messages containing these "patterns"
14141411
BOLD_PATTERNS = ("' injectable", "provided empty", "leftover chars", "might be injectable", "' is vulnerable", "is not injectable", "does not seem to be", "test failed", "test passed", "live test final result", "test shows that", "the back-end DBMS is", "created Github", "blocked by the target server", "protection is involved", "CAPTCHA", "specific response", "NULL connection is supported", "PASSED", "FAILED", "for more than", "connection to ", "will be trimmed", "counterpart to database")
14151412

‎lib/request/comparison.py‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,16 +230,20 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
230230
else:
231231
key = (hash(seq1), hash(seq2))
232232

233-
try:
234-
seqMatcher.set_seq1(seq1)
235-
seqMatcher.set_seq2(seq2)
236-
except:
237-
seqMatcher.set_seq1(repr(seq1))
238-
seqMatcher.set_seq2(repr(seq2))
239-
240233
ratio = kb.cache.comparison.get(key) if key else None
241234

242235
if ratio is None:
236+
# Note: populate the matcher only on a cache MISS - set_seq2() eagerly builds difflib's
237+
# O(len(page)) b2j index, and since each response is a fresh string that whole build was
238+
# thrown away on every cache hit (the common case after warmup: responses cluster into a
239+
# few distinct pages). seqMatcher carries no state across calls that a hit would read.
240+
try:
241+
seqMatcher.set_seq1(seq1)
242+
seqMatcher.set_seq2(seq2)
243+
except:
244+
seqMatcher.set_seq1(repr(seq1))
245+
seqMatcher.set_seq2(repr(seq2))
246+
243247
try:
244248
try:
245249
ratio = seqMatcher.quick_ratio() if not kb.heavilyDynamic else seqMatcher.ratio()

‎lib/request/redirecthandler.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,12 @@ def http_error_302(self, req, fp, code, msg, headers):
157157
elif last:
158158
cookies[last] += "%s%s" % (delimiter, part)
159159

160-
if HTTP_HEADER.SET_COOKIE in headers:
161-
for match in re.finditer(r"(?:^|,\s*)([^=;,]+)=([^;,]+)", headers[HTTP_HEADER.SET_COOKIE]):
160+
# Note: multiple cookies arrive as SEPARATE Set-Cookie headers (RFC-6265 forbids folding
161+
# them into one comma-joined value), and __getitem__ returns only the FIRST - iterate all
162+
# values so 2nd+ cookies (e.g. a CSRF token) are not silently dropped across the redirect
163+
setCookies = headers.get_all(HTTP_HEADER.SET_COOKIE) if hasattr(headers, "get_all") else [headers[HTTP_HEADER.SET_COOKIE]]
164+
for setCookie in setCookies:
165+
for match in re.finditer(r"(?:^|,\s*)([^=;,]+)=([^;,]+)", setCookie):
162166
key = match.group(1).strip()
163167
if key.lower() not in ("expires", "path", "domain", "max-age", "secure", "httponly", "samesite"):
164168
cookies[key] = match.group(2).strip()

‎lib/techniques/nosql/inject.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -990,7 +990,7 @@ def _resolve(place, parameter, key):
990990
falseModel = _reproduced(lambda: _fetch(place, parameter, "$in", NOSQL_SENTINEL, isArray=True)) # matches nothing
991991
return Vector(_fingerprintMongo(place, parameter),
992992
lambda value: _fetch(place, parameter, "$regex", value),
993-
lambda n: "^.{%d,}$" % n,
993+
lambda n: "(?s)^.{%d,}$" % n, # (?s): a value containing '\n' must still match its own length (else the $-anchored probe fails for every n -> empty result)
994994
lambda known, klass: "^%s%s" % (re.escape(known), klass),
995995
template=template, bypass='{"$ne": null}', falseModel=falseModel)
996996

‎lib/utils/har.py‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,17 @@ def toDict(self):
125125
}
126126

127127
if self.postBody:
128-
contentType = self.headers.get("Content-Type")
129-
out["postData"] = {
130-
"mimeType": contentType,
131-
"text": getText(self.postBody).rstrip("\r\n"),
132-
}
128+
out["postData"] = {"mimeType": self.headers.get("Content-Type")}
129+
130+
# HAR text must be UTF-8: a binary POST body (e.g. a file upload) that does not decode is
131+
# base64-encoded losslessly rather than mangled through a lossy text decode - mirroring the
132+
# Response.toDict() contract below (otherwise the exported HAR cannot reproduce the request)
133+
raw = self.postBody if isinstance(self.postBody, bytes) else getBytes(self.postBody)
134+
try:
135+
out["postData"]["text"] = raw.decode("utf-8").rstrip("\r\n")
136+
except UnicodeDecodeError:
137+
out["postData"]["encoding"] = "base64"
138+
out["postData"]["text"] = getText(base64.b64encode(raw))
133139

134140
return out
135141

‎plugins/generic/users.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ def getPrivileges(self, query2=False):
405405
# Set containing the list of DBMS administrators
406406
areAdmins = set()
407407

408-
if not kb.data.cachedUsersPrivileges and any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct:
408+
if not kb.data.cachedUsersPrivileges and (any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct):
409409
if Backend.isDbms(DBMS.MYSQL) and not kb.data.has_information_schema:
410410
query = rootQuery.inband.query2
411411
condition = rootQuery.inband.condition2

‎tests/test_datafiles.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ def test_every_dbms_has_core_tags(self):
6262
missing = [t for t in self.CORE_TAGS if t not in present]
6363
self.assertEqual(missing, [], msg="%s missing core tags: %s" % (dbms.get("value"), missing))
6464

65+
def test_column_comment_queries_format_with_three_args(self):
66+
# Regression: getColumns() formats every column_comment query with exactly (db, tbl, name)
67+
# via '%'-formatting (plugins/generic/databases.py). A literal LIKE wildcard that is not
68+
# escaped to '%%' (or a wrong placeholder count) raises at format time and aborts
69+
# '--columns --comments' before any request. Vertica's entry had 'LIKE '%.%s'' (ValueError).
70+
tree = ET.parse(os.path.join(ROOT, "data", "xml", "queries.xml"))
71+
for dbms in tree.findall(".//dbms"):
72+
for node in dbms.iter("column_comment"):
73+
query = node.get("query")
74+
if query:
75+
try:
76+
query % ("db", "tbl", "col")
77+
except (ValueError, TypeError) as ex:
78+
self.fail("%s column_comment query cannot be formatted with (db, tbl, name): %r (%s)"
79+
% (dbms.get("value"), query, ex))
80+
6581

6682
class TestErrorsXmlCompile(unittest.TestCase):
6783
def test_all_error_regexes_compile(self):

0 commit comments

Comments
 (0)