mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-08-04 14:55:40 +00:00
Implementing support for 429 (rate limit)
This commit is contained in:
parent
075d009bb7
commit
2d9e9ed959
6 changed files with 94 additions and 4 deletions
|
|
@ -282,6 +282,7 @@ class HTTP_HEADER(object):
|
|||
RANGE = "Range"
|
||||
REFERER = "Referer"
|
||||
REFRESH = "Refresh" # Reference: http://stackoverflow.com/a/283794
|
||||
RETRY_AFTER = "Retry-After"
|
||||
SERVER = "Server"
|
||||
SET_COOKIE = "Set-Cookie"
|
||||
TRANSFER_ENCODING = "Transfer-Encoding"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from lib.core.enums import OS
|
|||
from thirdparty import six
|
||||
|
||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||
VERSION = "1.10.7.181"
|
||||
VERSION = "1.10.7.182"
|
||||
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
||||
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
|
||||
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
|
||||
|
|
@ -58,6 +58,17 @@ IPS_WAF_CHECK_TIMEOUT = 10
|
|||
# false positive) rather than the back-end actually answering.
|
||||
WAF_BLOCK_HTTP_CODES = (403, 406, 429, 451, 501, 503)
|
||||
|
||||
# HTTP status signalling that the client is being rate-limited (kept as a literal because Python 2's
|
||||
# httplib has no such constant)
|
||||
TOO_MANY_REQUESTS_HTTP_CODE = 429
|
||||
|
||||
# Adaptive rate-limit handling: one-time backoff used when a rate-limited response carries no usable
|
||||
# 'Retry-After', the additive step by which the inter-request delay is raised on each hit, and the
|
||||
# ceiling for both the honored backoff and the auto-throttle (seconds)
|
||||
RATE_LIMIT_DEFAULT_DELAY = 1.0
|
||||
RATE_LIMIT_DELAY_STEP = 0.5
|
||||
RATE_LIMIT_MAX_DELAY = 60.0
|
||||
|
||||
# Candidate tamper scripts for automatic WAF-bypass, ordered by empirical WAF-bypass value
|
||||
# (structural token-substitution first, camouflage last; per identYwaf data). The back-end DBMS
|
||||
# is not pre-filtered here: semantics-preservation is verified at runtime by re-running detection
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ def vulnTest(tests=None, label="vuln"):
|
|||
("-u <url> --data=\"security_level=5\" -p id --flush-session --technique=B", ("unable to automatically bypass the WAF/IPS", "does not seem to be injectable")), # automatic WAF-bypass honest bail: a libinjection-class WAF that no User-Agent or tamper can defeat
|
||||
("-u <url> -p id --flush-session --technique=B --proof", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --proof: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof
|
||||
("-u <base> --mine-params --flush-session --technique=B", ("mining for hidden GET parameters", "found hidden parameter 'id'", "held back parameter(s) that break the base request", "Parameter: id (GET)", "Type: boolean-based blind")), # --mine-params: discover an injectable parameter absent from a bare URL, hold back the raw-SQL sink that would shadow it, then confirm the injection on the mined 'id'
|
||||
("-u \"<base>ratelimit?id=1\" --flush-session --technique=B", ("target appears to be rate-limiting", "Parameter: id (GET)", "Type: boolean-based blind")), # adaptive rate-limit handling: the endpoint answers 429 with 'Retry-After' first, so detection only succeeds if sqlmap honors the backoff, throttles, and retries rather than treating 429 as a hard block
|
||||
("-r <request> --flush-session -v 5 --test-skip=\"heavy\" --save=<config>", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")),
|
||||
("-c <config>", ("CloudFlare", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind")),
|
||||
("-l <log> --flush-session --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ See the file 'LICENSE' for copying permission
|
|||
"""
|
||||
|
||||
import binascii
|
||||
import calendar
|
||||
import email.utils
|
||||
import inspect
|
||||
import io
|
||||
import logging
|
||||
|
|
@ -119,9 +121,13 @@ from lib.core.settings import PERMISSION_DENIED_REGEX
|
|||
from lib.core.settings import PLAIN_TEXT_CONTENT_TYPE
|
||||
from lib.core.settings import RANDOM_INTEGER_MARKER
|
||||
from lib.core.settings import RANDOM_STRING_MARKER
|
||||
from lib.core.settings import RATE_LIMIT_DEFAULT_DELAY
|
||||
from lib.core.settings import RATE_LIMIT_DELAY_STEP
|
||||
from lib.core.settings import RATE_LIMIT_MAX_DELAY
|
||||
from lib.core.settings import REPLACEMENT_MARKER
|
||||
from lib.core.settings import SAFE_HEX_MARKER
|
||||
from lib.core.settings import TEXT_CONTENT_TYPE_REGEX
|
||||
from lib.core.settings import TOO_MANY_REQUESTS_HTTP_CODE
|
||||
from lib.core.settings import UNENCODED_ORIGINAL_VALUE
|
||||
from lib.core.settings import UNICODE_ENCODING
|
||||
from lib.core.settings import URI_HTTP_HEADER
|
||||
|
|
@ -223,6 +229,49 @@ class Connect(object):
|
|||
kwargs['retrying'] = True
|
||||
return Connect._getPageProxy(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _parseRetryAfter(responseHeaders):
|
||||
"""
|
||||
Parses a 'Retry-After' response header (RFC 7231 delta-seconds or an HTTP-date) into a number
|
||||
of seconds to wait, or None when it is absent or unparseable.
|
||||
"""
|
||||
|
||||
value = (responseHeaders.get(HTTP_HEADER.RETRY_AFTER) if responseHeaders else None) or ""
|
||||
value = value.strip()
|
||||
|
||||
if value.isdigit():
|
||||
return float(value)
|
||||
|
||||
parsed = email.utils.parsedate(value)
|
||||
return max(0.0, calendar.timegm(parsed) - time.time()) if parsed else None
|
||||
|
||||
@staticmethod
|
||||
def _rateLimitRetry(responseHeaders, code, **kwargs):
|
||||
"""
|
||||
Handles a rate-limited response by honoring its 'Retry-After' (capped), adaptively raising the
|
||||
inter-request delay so subsequent requests self-throttle under the limit, then re-issuing the
|
||||
request. Returns the retried (page, headers, code) or None when the retry budget is exhausted,
|
||||
so the caller can surface the rate-limited response as-is.
|
||||
"""
|
||||
|
||||
threadData = getCurrentThreadData()
|
||||
if threadData.retriesCount >= conf.retries or kb.threadException:
|
||||
return None
|
||||
|
||||
retryAfter = Connect._parseRetryAfter(responseHeaders)
|
||||
backoff = min(retryAfter if retryAfter is not None else RATE_LIMIT_DEFAULT_DELAY, RATE_LIMIT_MAX_DELAY)
|
||||
|
||||
# additive-increase throttle: nudge the inter-request delay up toward a sustainable pace. The
|
||||
# auto-throttle is capped, but a larger user-set '--delay' is never lowered. It is monotonic,
|
||||
# so a lost concurrent update across threads self-heals on the next hit.
|
||||
conf.delay = max(conf.delay or 0, min(RATE_LIMIT_MAX_DELAY, (conf.delay or 0) + RATE_LIMIT_DELAY_STEP))
|
||||
|
||||
singleTimeWarnMessage("target appears to be rate-limiting requests; sqlmap is backing off and throttling accordingly (consider raising '--delay' or lowering '--threads')")
|
||||
logger.debug("rate-limited (HTTP %d)%s; sleeping %.1f second(s), inter-request delay now %.1f second(s)" % (code, " honoring 'Retry-After'" if retryAfter is not None else "", backoff, conf.delay))
|
||||
|
||||
time.sleep(backoff)
|
||||
return Connect._retryProxy(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _connReadProxy(conn):
|
||||
parts = []
|
||||
|
|
@ -844,7 +893,13 @@ class Connect(object):
|
|||
raise SystemExit
|
||||
|
||||
if ex.code not in (conf.ignoreCode or []):
|
||||
if ex.code == _http_client.UNAUTHORIZED:
|
||||
if ex.code == TOO_MANY_REQUESTS_HTTP_CODE or (ex.code == _http_client.SERVICE_UNAVAILABLE and Connect._parseRetryAfter(responseHeaders) is not None):
|
||||
retried = Connect._rateLimitRetry(responseHeaders, ex.code, **kwargs)
|
||||
if retried is not None:
|
||||
return retried
|
||||
debugMsg = "target kept rate-limiting after %d retries (%d)" % (conf.retries, code)
|
||||
logger.debug(debugMsg)
|
||||
elif ex.code == _http_client.UNAUTHORIZED:
|
||||
errMsg = "not authorized, try to provide right HTTP "
|
||||
errMsg += "authentication type and valid credentials (%d). " % code
|
||||
errMsg += "If this is intended, try to rerun by providing "
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue