Skip to content

Commit 4c12dda

Browse files
committed
Fixing --eta
1 parent 767feba commit 4c12dda

7 files changed

Lines changed: 284 additions & 18 deletions

File tree

‎lib/core/settings.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.7.55"
23+
VERSION = "1.10.7.56"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
@@ -249,6 +249,9 @@
249249
# Maximum number of techniques used in inject.py/getValue() per one value
250250
MAX_TECHNIQUES_PER_VALUE = 2
251251

252+
# Fraction of the currently displayed progress-bar ETA kept when a fresh estimate arrives (eases the on-screen countdown toward the new value instead of snapping); 0 disables smoothing, higher is smoother but laggier
253+
ETA_DISPLAY_SMOOTHING = 0.5
254+
252255
# In case of missing piece of partial union dump, buffered array must be flushed after certain size
253256
MAX_BUFFERED_PARTIAL_UNION_LENGTH = 1024
254257

‎lib/request/inject.py‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import print_function
99

1010
import re
11+
import threading
1112
import time
1213

1314
from lib.core.agent import agent
@@ -16,6 +17,7 @@
1617
from lib.core.common import dataToStdout
1718
from lib.core.common import unArrayizeValue
1819
from lib.core.datatype import AttribDict
20+
from lib.utils.progress import ProgressBar
1921
from lib.utils.safe2bin import safecharencode
2022
from lib.core.common import Backend
2123
from lib.core.common import calculateDeltaSeconds
@@ -58,12 +60,14 @@
5860
from lib.core.exception import SqlmapNotVulnerableException
5961
from lib.core.exception import SqlmapUserQuitException
6062
from lib.core.settings import GET_VALUE_UPPERCASE_KEYWORDS
63+
from lib.core.settings import IS_TTY
6164
from lib.core.settings import INFERENCE_MARKER
6265
from lib.core.settings import MAX_TECHNIQUES_PER_VALUE
6366
from lib.core.settings import SQL_SCALAR_REGEX
6467
from lib.core.settings import UNICODE_ENCODING
6568
from lib.core.threads import getCurrentThreadData
6669
from lib.core.threads import runThreads
70+
from lib.core.threads import setDaemon
6771
from lib.core.unescaper import unescaper
6872
from lib.request.connect import Connect as Request
6973
from 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+
408431
def _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

‎lib/utils/progress.py‎

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from lib.core.convert import getUnicode
1414
from lib.core.data import conf
1515
from lib.core.data import kb
16+
from lib.core.settings import ETA_DISPLAY_SMOOTHING
17+
from lib.core.settings import IS_TTY
1618

1719
class ProgressBar(object):
1820
"""
@@ -26,7 +28,9 @@ def __init__(self, minValue=0, maxValue=10, totalWidth=None):
2628
self._span = max(self._max - self._min, 0.001)
2729
self._width = totalWidth if totalWidth else conf.progressWidth
2830
self._amount = 0
29-
self._start = None
31+
self._start = time.time() # begin timing at construction, so the first completed item already yields an estimate
32+
self._eta = None # last estimated seconds-remaining and when it was computed, so tick()
33+
self._etaAt = None # can keep the countdown live between (possibly slow) item updates
3034
self.update()
3135

3236
def _convertSeconds(self, value):
@@ -73,24 +77,49 @@ def update(self, newAmount=0):
7377

7478
def progress(self, newAmount):
7579
"""
76-
This method saves item delta time and shows updated progress bar with calculated eta
80+
Redraw the bar with an ETA from the average time per completed item so far, applied to the items
81+
still remaining: (elapsed / done) * (max - newAmount). The remaining-item count is (max - newAmount)
82+
- i.e. at 1/3 it estimates the 2 items left, at 2/3 the 1 left - not just the current item.
7783
"""
7884

79-
if self._start is None or newAmount > self._max:
80-
self._start = time.time()
81-
eta = None
85+
now = time.time()
86+
if newAmount > self._max: # counter rollover/reset -> restart timing
87+
self._start = now
88+
self._eta = None
89+
90+
done = newAmount - self._min
91+
elapsed = now - self._start
92+
target = (elapsed / done) * (self._max - newAmount) if (done > 0 and elapsed > 0) else None
93+
94+
if target is None:
95+
self._eta = None
96+
elif self._eta is None:
97+
self._eta = target # first estimate: nothing to ease from
8298
else:
83-
delta = time.time() - self._start
84-
eta = (self._max - self._min) * (1.0 * delta / newAmount) - delta
99+
current = max(0, self._eta - (now - self._etaAt)) # what is on screen now (already decremented by tick())
100+
self._eta = ETA_DISPLAY_SMOOTHING * current + (1 - ETA_DISPLAY_SMOOTHING) * target # ease into the fresh estimate
85101

102+
self._etaAt = now
86103
self.update(newAmount)
104+
self.draw(self._eta)
105+
106+
def tick(self):
107+
"""
108+
Redraw the current bar with its ETA decremented by real elapsed time, so the countdown stays
109+
live between updates (e.g. during a long time-based wait) instead of freezing at the last estimate
110+
"""
111+
112+
eta = None if self._eta is None else max(0, self._eta - (time.time() - self._etaAt))
87113
self.draw(eta)
88114

89115
def draw(self, eta=None):
90116
"""
91117
This method draws the progress bar if it has changed
92118
"""
93119

120+
if not IS_TTY: # a progress bar is a terminal animation; suppress it when piped/redirected (as done for other '\r' output)
121+
return
122+
94123
dataToStdout("\r%s %d/%d%s" % (self._progBar, self._amount, self._max, (" (ETA %s)" % (self._convertSeconds(int(eta)) if eta is not None else "??:??"))))
95124
if self._amount >= self._max:
96125
dataToStdout("\r%s\r" % (" " * self._width))

‎plugins/generic/databases.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -403,12 +403,13 @@ def getTables(self, bruteForce=None):
403403

404404
# Value-parallel, prediction-assisted name enumeration for the DBMSes using the
405405
# generic "<db>, <index>" blind template. Retrieves whole names concurrently (one per
406-
# worker, each decoded sequentially so wordlist prediction applies). Used only with
407-
# '--threads' (like getColumns below); single-thread stays on the classic getValue loop,
408-
# which still gets predictive inference via getPartRun. The special templates stay serial.
406+
# worker, each decoded sequentially so wordlist prediction applies). Used with '--threads'
407+
# and under '--eta' (one whole-job bar/ETA) per valueParallelEligible(); plain single-thread
408+
# stays on the classic getValue loop, which still gets predictive inference via getPartRun.
409+
# The special templates stay serial.
409410
genericTemplate = Backend.getIdentifiedDbms() not in (DBMS.SYBASE, DBMS.MAXDB, DBMS.ACCESS, DBMS.MCKOI, DBMS.EXTREMEDB, DBMS.SQLITE, DBMS.FIREBIRD, DBMS.HSQLDB, DBMS.INFORMIX, DBMS.FRONTBASE, DBMS.VIRTUOSO, DBMS.SPANNER)
410411

411-
if genericTemplate and conf.threads > 1 and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN):
412+
if genericTemplate and inject.valueParallelEligible():
412413
for table in (inject._threadedInferenceValues(lambda index: _query % (unsafeSQLIdentificatorNaming(db), index), indexRange, context="Tables") or []):
413414
if not isNoneValue(table):
414415
kb.hintValue = table
@@ -899,9 +900,11 @@ def columnNameQuery(index):
899900

900901
# Value-parallel column-NAME enumeration: the same axis/mechanism as getTables (one name per
901902
# worker, decoded sequentially - no length probe, predictive inference applies, names stream
902-
# live). Serial fallback for single-thread and when also fetching per-column comments.
903+
# live, and under '--eta' a single whole-job bar/ETA is drawn). Eligibility is shared via
904+
# valueParallelEligible(); serial fallback only when also fetching per-column comments (those
905+
# need an extra per-column query).
903906
columnNames = None
904-
if conf.threads > 1 and not conf.getComments and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN):
907+
if not conf.getComments and inject.valueParallelEligible():
905908
columnNames = inject._threadedInferenceValues(columnNameQuery, indexList, context="Columns")
906909

907910
for position, index in enumerate(indexList):

‎plugins/generic/entries.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,9 +444,11 @@ def cellQuery(column, index):
444444
# value's characters across threads) and the per-column Huffman model + low-cardinality
445445
# guessing engage under concurrency. Used for the boolean channel with '--threads', and
446446
# for the HTTP/2 timeless oracle (its per-thread connections make concurrency safe and
447-
# deterministic - unlike classic time-based, whose char-parallel loop interleaves its
448-
# per-thread output). Single-thread and classic time-based keep the char-parallel loop.
449-
if conf.threads > 1 and not conf.dnsDomain and (isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) or kb.get("timeless") is not None):
447+
# deterministic). Also used under '--eta' - including single-threaded time-based, the
448+
# slowest channel - to drive one whole-job progress bar/ETA (how long the dump takes)
449+
# instead of a per-cell counter. Eligibility (channel x threads/eta) is valueParallelEligible();
450+
# '--dns-domain' keeps the classic loop (its OOB fast path bypasses bisection).
451+
if not conf.dnsDomain and inject.valueParallelEligible():
450452
# One value-parallel pass over every (non-empty) cell, so there is a single
451453
# thread pool and values stream live as they complete - out of order, exactly
452454
# like the error/union dumps - instead of a silent progress counter.

0 commit comments

Comments
 (0)