Skip to content

Commit 579e559

Browse files
committed
Adding one more data correctness patch
1 parent 5be154b commit 579e559

3 files changed

Lines changed: 67 additions & 3 deletions

File tree

lib/core/settings.py

Lines changed: 1 addition & 1 deletion
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.252"
23+
VERSION = "1.10.7.253"
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)

lib/request/inject.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,34 @@ def _(item):
594594
applyFunctionRecursively(value, _)
595595
return retVal[0]
596596

597+
def _looksLikeMisdecodedUtf8(value):
598+
"""
599+
True if a retrieved value looks like UTF-8 data shown through a single-byte (ISO-8859-1) charset -
600+
the '<page charset too WIDE>' mismatch that leaves NO undecodable bytes (latin-1 accepts every byte),
601+
so _pageCharsetCorrupted() cannot see it, yet the data is silently mojibake (a UTF-8 accent shows as
602+
two spurious latin-1 chars).
603+
604+
Precise by construction: real mojibake re-encodes to latin-1 and decodes back to a DIFFERENT valid
605+
UTF-8 string, whereas correctly-decoded UTF-8 ('cafe' with U+00E9) and genuine latin-1 text ('resume')
606+
do not (a lone accent is a UTF-8 lead byte with no continuation -> decode fails). So it fires only on
607+
the corruption, not on clean data. Complements _pageCharsetCorrupted() (the too-NARROW direction).
608+
"""
609+
610+
retVal = [False]
611+
612+
def _(item):
613+
if not retVal[0] and isinstance(item, six.string_types) and any(0x80 <= ord(_) <= 0xFF for _ in item):
614+
try:
615+
decoded = item.encode("latin-1").decode("utf-8")
616+
except (UnicodeEncodeError, UnicodeDecodeError):
617+
return item
618+
if decoded != item and any(ord(_) > 0x7F for _ in decoded):
619+
retVal[0] = True
620+
return item
621+
622+
applyFunctionRecursively(value, _)
623+
return retVal[0]
624+
597625
@lockedmethod
598626
@stackedmethod
599627
def getValue(expression, blind=True, union=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True):
@@ -699,7 +727,7 @@ def getValue(expression, blind=True, union=True, error=True, time=True, fromUser
699727
if (found and not conf.hexConvert and not conf.binaryFields and expected not in (EXPECTED.BOOL, EXPECTED.INT)
700728
and getTechnique() in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)
701729
and Backend.getIdentifiedDbms() and hasattr(queries[Backend.getIdentifiedDbms()], "hex")
702-
and _pageCharsetCorrupted(value)):
730+
and (_pageCharsetCorrupted(value) or _looksLikeMisdecodedUtf8(value))):
703731
warnMsg = "retrieved data appears corrupted because of a charset mismatch between the "
704732
warnMsg += "DBMS and the web page. Re-fetching using hexadecimal encoding"
705733
singleTimeWarnMessage(warnMsg)
@@ -710,7 +738,10 @@ def getValue(expression, blind=True, union=True, error=True, time=True, fromUser
710738
finally:
711739
conf.hexConvert = False
712740

713-
if _value is not None:
741+
# only adopt the hex re-fetch if it is actually cleaner: a rare mis-trigger (or a
742+
# column whose real charset the hex decode still cannot resolve) must never replace
743+
# the original with equal-or-worse data
744+
if _value is not None and not _pageCharsetCorrupted(_value) and not _looksLikeMisdecodedUtf8(_value):
714745
value = _value
715746

716747
if found and conf.dnsDomain:

tests/test_charset.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
bootstrap()
2323

2424
from lib.request.basic import checkCharEncoding
25+
from lib.request.inject import _pageCharsetCorrupted, _looksLikeMisdecodedUtf8
2526
from lib.core.common import extractRegexResult, paramToDict
2627
from lib.core.enums import PLACE
2728
from lib.core.settings import META_CHARSET_REGEX, HTML_TITLE_REGEX, META_REFRESH_REGEX
@@ -58,6 +59,38 @@ def test_no_match_is_none(self):
5859
self.assertIsNone(extractRegexResult(HTML_TITLE_REGEX, "<body>no title here</body>"))
5960

6061

62+
class TestCharsetMismatchDetection(unittest.TestCase):
63+
"""The auto-hex recovery triggers on two complementary mismatch directions:
64+
_pageCharsetCorrupted (page charset too NARROW -> undecodable bytes) and
65+
_looksLikeMisdecodedUtf8 (page charset too WIDE -> UTF-8 shown as latin-1, no
66+
undecodable byte). Both must fire on the corruption and NOT on clean data."""
67+
68+
# correctly-decoded UTF-8 and genuine latin-1 text: neither detector may fire (no wasted re-fetch).
69+
# \u escapes keep this source pure-ASCII (py2): cafe / naive / Zurich / CJK / Cyrillic / resume / garcon
70+
CLEAN = [u"admin", u"caf\u00e9", u"na\u00efve", u"Z\u00fcrich", u"\u65e5\u672c\u8a9e",
71+
u"\u0417\u0434\u0440\u0430\u0432\u0435\u0439", u"r\u00e9sum\u00e9", u"gar\u00e7on"]
72+
73+
def test_clean_values_trigger_neither(self):
74+
for v in self.CLEAN:
75+
self.assertFalse(_pageCharsetCorrupted(v), msg="narrow FP: %r" % v)
76+
self.assertFalse(_looksLikeMisdecodedUtf8(v), msg="wide FP: %r" % v)
77+
78+
def test_utf8_shown_as_latin1_is_caught(self):
79+
# gap #1: UTF-8 column bytes decoded as latin-1 -> valid mojibake, no undecodable byte
80+
for word in (u"caf\u00e9", u"\u65e5\u672c\u8a9e", u"\u0417\u0434\u0440\u0430\u0432\u0435\u0439", u"\u20ac"):
81+
mojibake = word.encode("utf-8").decode("latin-1")
82+
self.assertFalse(_pageCharsetCorrupted(mojibake), msg="narrow should miss: %r" % mojibake)
83+
self.assertTrue(_looksLikeMisdecodedUtf8(mojibake), msg="wide should catch: %r" % mojibake)
84+
85+
def test_undecodable_bytes_still_caught_by_narrow(self):
86+
# the other direction (page charset too narrow) leaves reversible \xNN escapes
87+
self.assertTrue(_pageCharsetCorrupted(u"foo\\xe9bar"))
88+
89+
def test_list_and_nonstring_inputs(self):
90+
self.assertTrue(_looksLikeMisdecodedUtf8([u"ok", u"caf\u00c3\u00a9"])) # 'cafe' mojibake
91+
self.assertFalse(_looksLikeMisdecodedUtf8([u"ok", 123, None]))
92+
93+
6194
class TestParamToDict(unittest.TestCase):
6295
# NOTE: GET parsing is covered in test_urls.py; here we only cover the COOKIE place,
6396
# which uses a different (semicolon) delimiter and is a distinct code path.

0 commit comments

Comments
 (0)