88from __future__ import print_function
99
1010import re
11+ import threading
1112import time
1213
1314from lib .core .agent import agent
1617from lib .core .common import dataToStdout
1718from lib .core .common import unArrayizeValue
1819from lib .core .datatype import AttribDict
20+ from lib .utils .progress import ProgressBar
1921from lib .utils .safe2bin import safecharencode
2022from lib .core .common import Backend
2123from lib .core .common import calculateDeltaSeconds
5860from lib .core .exception import SqlmapNotVulnerableException
5961from lib .core .exception import SqlmapUserQuitException
6062from lib .core .settings import GET_VALUE_UPPERCASE_KEYWORDS
63+ from lib .core .settings import IS_TTY
6164from lib .core .settings import INFERENCE_MARKER
6265from lib .core .settings import MAX_TECHNIQUES_PER_VALUE
6366from lib .core .settings import SQL_SCALAR_REGEX
6467from lib .core .settings import UNICODE_ENCODING
6568from lib .core .threads import getCurrentThreadData
6669from lib .core .threads import runThreads
70+ from lib .core .threads import setDaemon
6771from lib .core .unescaper import unescaper
6872from lib .request .connect import Connect as Request
6973from lib .request .direct import direct
@@ -405,6 +409,25 @@ def _verifyInferredValue(expression, value):
405409 except Exception :
406410 return True
407411
412+ def valueParallelEligible ():
413+ """
414+ Whether blind enumeration/dumping should take the value-parallel path (one whole value per worker)
415+ rather than the classic char loop. It is chosen for concurrency ('--threads') and, independently, to
416+ render a single whole-job progress bar/ETA ('--eta'). A concurrency-safe channel - boolean or the
417+ HTTP/2 timeless oracle - qualifies with either. Classic time-based qualifies only single-threaded under
418+ '--eta': concurrent SLEEP measurements interfere, so it must stay sequential (where it is safe, and
419+ still gets the whole-job ETA that matters most for the slowest channel). Callers add their own extra
420+ guards (e.g. '--dns-domain', per-column comments).
421+ """
422+
423+ concurrencySafe = isTechniqueAvailable (PAYLOAD .TECHNIQUE .BOOLEAN ) or kb .get ("timeless" ) is not None
424+
425+ if conf .threads > 1 :
426+ return concurrencySafe
427+ if conf .eta :
428+ return concurrencySafe or isTechniqueAvailable (PAYLOAD .TECHNIQUE .TIME )
429+ return False
430+
408431def _threadedInferenceValues (exprBuilder , indices , context = None , charsetType = None , dump = False ):
409432 """
410433 Value-parallel blind retrieval.
@@ -446,6 +469,13 @@ def _threadedInferenceValues(exprBuilder, indices, context=None, charsetType=Non
446469 results = [None ] * len (indices )
447470 cursor = iter (xrange (len (indices )))
448471
472+ # With '--eta' show a single value-level bar (values completed / total) instead of the per-value
473+ # stream: it is monotonic and carries a meaningful ETA across the whole set, and - unlike the
474+ # per-char bar drawn inside bisection() - it survives the worker's disableStdOut (drawn forced,
475+ # below), so '--eta' is honoured under '--threads' too. Skipped for trivial single-value sets.
476+ etaProgress = ProgressBar (maxValue = len (indices )) if (conf .eta and len (indices ) > 1 ) else None
477+ completed = [0 ]
478+
449479 def inferenceThread ():
450480 threadData = getCurrentThreadData ()
451481 # Each per-value bisection streams its characters to stdout and mirrors them into
@@ -489,13 +519,36 @@ def inferenceThread():
489519 with kb .locks .value :
490520 results [slot ] = value
491521
522+ if etaProgress is not None :
523+ with kb .locks .io :
524+ completed [0 ] += 1
525+ threadData .disableStdOut = False # let the value-level bar through (per-char streaming stays muted)
526+ try :
527+ etaProgress .progress (completed [0 ])
528+ finally :
529+ threadData .disableStdOut = True
530+
492531 # Stream each retrieved value as it completes (they arrive out of order under threads, exactly
493532 # like the error/union dumps), so a dump shows its data live rather than a silent counter.
494- if conf .verbose >= 1 and not kb .bruteMode and not isNoneValue (value ):
533+ elif conf .verbose >= 1 and not kb .bruteMode and not isNoneValue (value ):
495534 with kb .locks .io :
496535 rendered = safecharencode (unArrayizeValue (value ))
497536 dataToStdout ("[%s] [INFO] retrieved: %s\n " % (time .strftime ("%X" ), "'%s'" % rendered if dump else rendered ), forceOutput = True )
498537
538+ # Keep the '--eta' countdown live between value updates: a value (esp. time-based) can take many
539+ # seconds, so a daemon redraws the bar every second with the ETA decremented by elapsed time (it runs
540+ # on its own thread, so it is not muted by the workers' disableStdOut, and shares kb.locks.io with them).
541+ etaTickerStop = threading .Event ()
542+ etaTicker = None
543+ if etaProgress is not None and IS_TTY :
544+ def _etaTicker ():
545+ while not etaTickerStop .wait (1.0 ):
546+ with kb .locks .io :
547+ etaProgress .tick ()
548+ etaTicker = threading .Thread (target = _etaTicker , name = "eta-ticker" )
549+ setDaemon (etaTicker )
550+ etaTicker .start ()
551+
499552 # Save/restore the calling thread's state: with a single thread runThreads runs the worker
500553 # INLINE on this thread, so the worker's disableStdOut/shared mutations must not leak out.
501554 savedPartRun = kb .partRun
@@ -505,6 +558,9 @@ def inferenceThread():
505558 try :
506559 runThreads (min (conf .threads or 1 , len (indices )) or 1 , inferenceThread )
507560 finally :
561+ etaTickerStop .set ()
562+ if etaTicker is not None :
563+ etaTicker .join (timeout = 2 )
508564 kb .partRun = savedPartRun
509565 mainThreadData .disableStdOut = savedStdOut
510566 mainThreadData .shared = savedShared
0 commit comments