Skip to content

Commit 567e051

Browse files
committed
Minor patching for binary fields and casting
1 parent 40ccd80 commit 567e051

5 files changed

Lines changed: 52 additions & 15 deletions

File tree

data/xml/queries.xml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
</dbms>
8484

8585
<dbms value="PostgreSQL">
86-
<cast query="CAST(%s AS VARCHAR(10000))"/>
86+
<cast query="CAST(%s AS TEXT)"/>
8787
<length query="LENGTH(%s)"/>
8888
<!-- NOTE: PostgreSQL does not like COALESCE with different data-types (e.g. COALESCE(id,' ')) -->
8989
<isnull query="COALESCE(%s::text,' ')"/>
@@ -119,8 +119,8 @@
119119
<blind query="SELECT DISTINCT(passwd) FROM pg_shadow WHERE usename='%s' OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(passwd)) FROM pg_shadow WHERE usename='%s'"/>
120120
</passwords>
121121
<privileges>
122-
<inband query="SELECT usename,(CASE WHEN usecreatedb THEN 1 ELSE 0 END),(CASE WHEN usesuper THEN 1 ELSE 0 END),(CASE WHEN usecatupd THEN 1 ELSE 0 END) FROM pg_user" condition="usename"/>
123-
<blind query="SELECT (CASE WHEN usecreatedb THEN 1 ELSE 0 END),(CASE WHEN usesuper THEN 1 ELSE 0 END),(CASE WHEN usecatupd THEN 1 ELSE 0 END) FROM pg_user WHERE usename='%s' OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(usename)) FROM pg_user WHERE usename='%s'"/>
122+
<inband query="SELECT usename,(CASE WHEN usecreatedb THEN 1 ELSE 0 END),(CASE WHEN usesuper THEN 1 ELSE 0 END),(CASE WHEN userepl THEN 1 ELSE 0 END) FROM pg_user" condition="usename"/>
123+
<blind query="SELECT (CASE WHEN usecreatedb THEN 1 ELSE 0 END),(CASE WHEN usesuper THEN 1 ELSE 0 END),(CASE WHEN userepl THEN 1 ELSE 0 END) FROM pg_user WHERE usename='%s' OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(usename)) FROM pg_user WHERE usename='%s'"/>
124124
</privileges>
125125
<roles/>
126126
<statements>
@@ -163,7 +163,7 @@
163163
</dbms>
164164

165165
<dbms value="Microsoft SQL Server">
166-
<cast query="CAST(%s AS NVARCHAR(4000))"/>
166+
<cast query="CAST(%s AS NVARCHAR(MAX))"/>
167167
<length query="LTRIM(STR(LEN(%s+'.')-1))"/>
168168
<isnull query="ISNULL(%s,' ')"/>
169169
<delimiter query="+"/>
@@ -384,7 +384,7 @@
384384
</columns>
385385
<dump_table>
386386
<inband query="SELECT %s FROM %s"/>
387-
<blind query="SELECT %s FROM %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s" rowid="rowid" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
387+
<blind query="SELECT %s FROM %s ORDER BY rowid LIMIT %d,1" count="SELECT COUNT(*) FROM %s" rowid="rowid" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
388388
</dump_table>
389389
<search_db/>
390390
<search_table>
@@ -959,8 +959,8 @@
959959

960960
<dbms value="Apache Derby">
961961
<!-- NOTE: CHAR(%s) causes 'A truncation error was encountered trying to shrink CHAR' -->
962-
<cast query="RTRIM(CAST(%s AS CHAR(254)))"/>
963-
<length query="LENGTH(RTRIM(CAST(%s AS CHAR(254))))"/>
962+
<cast query="CAST(%s AS VARCHAR(32672))"/>
963+
<length query="LENGTH(CAST(%s AS VARCHAR(32672)))"/>
964964
<isnull query="COALESCE(%s,' ')"/>
965965
<delimiter query="||"/>
966966
<limit query="OFFSET %d ROWS FETCH FIRST %d ROWS ONLY"/>
@@ -1158,7 +1158,7 @@
11581158
<substring query="SUBSTR(%s,%d,%d)"/>
11591159
<concatenate query="%s||%s"/>
11601160
<case query="SELECT (CASE WHEN (%s) THEN '1' ELSE '0' END)"/>
1161-
<hex query="TO_HEX(%s)"/>
1161+
<hex query="TO_HEX(CAST(%s AS VARBINARY))"/>
11621162
<inference query="CODEPOINT(SUBSTR((%s),%d,1))>%d" dbms_version="&gt;=0.178" query2="SUBSTR((%s),%d,1)>'%c'"/>/>
11631163
<banner query="version()"/>
11641164
<current_user query="CURRENT_USER"/>

lib/core/agent.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -508,10 +508,26 @@ def nullAndCastField(self, field):
508508
if field and Backend.getIdentifiedDbms():
509509
rootQuery = queries[Backend.getIdentifiedDbms()]
510510

511+
kb.binaryField = conf.binaryFields and field in conf.binaryFields
512+
hexConvert = conf.hexConvert or kb.binaryField
513+
514+
# For BINARY fields, wrap the RAW column with the hex function rather than the text-casted one:
515+
# text-casting binary first (e.g. CAST(<binary> AS NCHAR) on MySQL) NULLs non-text bytes, so HEX()
516+
# would encode the NULL placeholder and silently lose the value (e.g. binary-stored password
517+
# hashes). This is limited to binary fields: the blanket '--hex' keeps the cast-first path because
518+
# raw-hexing a numeric/temporal column uses the DBMS's numeric HEX semantics (MySQL HEX(255)='FF'),
519+
# which is NOT the hex of its string representation. Every DBMS hex function takes binary directly
520+
# EXCEPT PostgreSQL's CONVERT_TO() (needs text), so PostgreSQL stays on the cast-first path too.
521+
hexRaw = kb.binaryField and not Backend.isDbms(DBMS.PGSQL)
522+
511523
if field.startswith("(CASE") or field.startswith("(IIF") or conf.noCast and not (field.startswith("COUNT(") and Backend.getIdentifiedDbms() == DBMS.MSSQL):
512524
nulledCastedField = field
525+
if hexConvert:
526+
nulledCastedField = self.hexConvertField(nulledCastedField)
513527
else:
514-
if not (Backend.isDbms(DBMS.SQLITE) and not isDBMSVersionAtLeast('3')):
528+
if hexRaw:
529+
nulledCastedField = self.hexConvertField(field)
530+
elif not (Backend.isDbms(DBMS.SQLITE) and not isDBMSVersionAtLeast('3')):
515531
nulledCastedField = rootQuery.cast.query % field
516532

517533
if re.search(r"COUNT\(", field) and Backend.getIdentifiedDbms() in (DBMS.RAIMA,):
@@ -521,9 +537,8 @@ def nullAndCastField(self, field):
521537
else:
522538
nulledCastedField = rootQuery.isnull.query % nulledCastedField
523539

524-
kb.binaryField = conf.binaryFields and field in conf.binaryFields
525-
if conf.hexConvert or kb.binaryField:
526-
nulledCastedField = self.hexConvertField(nulledCastedField)
540+
if hexConvert and not hexRaw:
541+
nulledCastedField = self.hexConvertField(nulledCastedField)
527542

528543
if suffix:
529544
nulledCastedField += suffix

lib/core/settings.py

Lines changed: 6 additions & 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.75"
23+
VERSION = "1.10.7.76"
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)
@@ -885,6 +885,11 @@
885885
# Regular expression used for automatic hex conversion and hash cracking of (RAW) binary column values
886886
HASH_BINARY_COLUMNS_REGEX = r"(?i)pass|psw|hash"
887887

888+
# Regular expression matching (declared) binary column types, used to auto-hex their values during dumping
889+
# so raw bytes (e.g. password hashes stored in binary form) are not silently truncated at NUL / mangled by
890+
# the text extraction channel (mirrors a manual '--binary-fields', using the already-fetched column type)
891+
BINARY_FIELDS_TYPE_REGEX = r"(?i)binary|blob|bytea|image|\braw\b"
892+
888893
# Maximum number of redirections to any single URL - this is needed because of the state that cookies introduce
889894
MAX_SINGLE_URL_REDIRECTIONS = 4
890895

plugins/generic/entries.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from lib.core.exception import SqlmapMissingMandatoryOptionException
4141
from lib.core.exception import SqlmapNoneDataException
4242
from lib.core.exception import SqlmapUnsupportedFeatureException
43+
from lib.core.settings import BINARY_FIELDS_TYPE_REGEX
4344
from lib.core.settings import CHECK_ZERO_COLUMNS_THRESHOLD
4445
from lib.core.settings import CURRENT_DB
4546
from lib.core.settings import KEYSET_MIN_ROWS
@@ -114,6 +115,8 @@ def dumpTable(self, foundData=None):
114115
for tbl in tblList:
115116
tblList[tblList.index(tbl)] = safeSQLIdentificatorNaming(tbl, True)
116117

118+
binaryFields = conf.binaryFields # user-provided '--binary-fields' (auto-detected ones are added per-table)
119+
117120
for tbl in tblList:
118121
if kb.dumpKeyboardInterrupt:
119122
break
@@ -169,6 +172,18 @@ def dumpTable(self, foundData=None):
169172
colNames = colString = ','.join(column for column in colList)
170173
rootQuery = queries[Backend.getIdentifiedDbms()].dump_table
171174

175+
# Auto-treat binary-typed columns (blob/varbinary/bytea/image/raw) as '--binary-fields', so their
176+
# raw bytes (e.g. password hashes stored in binary form) are hex-extracted instead of being
177+
# silently truncated at a NUL / mangled by the text channel (issues #8, #582, #2827). The column
178+
# type is already known from the enumeration above, so this costs no extra request.
179+
# (PostgreSQL excluded: its bytea already renders as readable '\xHEX' through the default text
180+
# cast, and its hex path needs text input, so auto-hexing would double-encode.)
181+
autoBinary = [] if Backend.isDbms(DBMS.PGSQL) else [column for column in colList if columns.get(column) and re.search(BINARY_FIELDS_TYPE_REGEX, getUnicode(columns[column]))]
182+
conf.binaryFields = (list(binaryFields) if binaryFields else []) + [_ for _ in autoBinary if not (binaryFields and _ in binaryFields)]
183+
if autoBinary:
184+
debugMsg = "auto-treating binary column(s) '%s' as binary fields" % ', '.join(unsafeSQLIdentificatorNaming(_) for _ in autoBinary)
185+
logger.debug(debugMsg)
186+
172187
infoMsg = "fetching entries"
173188
if conf.col:
174189
infoMsg += " of column(s) '%s'" % colNames
@@ -572,6 +587,7 @@ def cellQuery(column, index):
572587
finally:
573588
kb.dumpColumns = None
574589
kb.dumpTable = None
590+
conf.binaryFields = binaryFields # restore user-provided value (drop this table's auto-detected ones)
575591

576592
def dumpAll(self):
577593
if conf.db is not None and conf.tbl is None:

tests/test_dialect.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ class TestNullAndCastField(unittest.TestCase):
6868

6969
CASES = {
7070
DBMS.MYSQL: "IFNULL(CAST(col AS NCHAR),' ')",
71-
DBMS.MSSQL: "ISNULL(CAST(col AS NVARCHAR(4000)),' ')",
71+
# MSSQL/PGSQL casts are unbounded (NVARCHAR(MAX)/TEXT) so long values are not silently truncated
72+
DBMS.MSSQL: "ISNULL(CAST(col AS NVARCHAR(MAX)),' ')",
7273
DBMS.SYBASE: "ISNULL(CONVERT(VARCHAR(4000),col),' ')",
73-
DBMS.PGSQL: "COALESCE(CAST(col AS VARCHAR(10000))::text,' ')",
74+
DBMS.PGSQL: "COALESCE(CAST(col AS TEXT)::text,' ')",
7475
DBMS.ORACLE: "NVL(CAST(col AS VARCHAR(4000)),' ')",
7576
}
7677

0 commit comments

Comments
 (0)