Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/controller/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,11 @@ def start():
for targetUrl, targetMethod, targetData, targetCookie, targetHeaders in kb.targets:
targetCount += 1

# --report-json: give each target its own taskid in the shared collector, so a multi-target
# run (e.g. '-m' bulk file) doesn't have later targets overwrite earlier ones (see _reportData)
if conf.reportJson:
kb.reportTaskId = targetCount

try:
if conf.checkInternet:
infoMsg = "checking for Internet connection"
Expand Down
2 changes: 1 addition & 1 deletion lib/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,7 @@ def setColor(message, color=None, bold=False, level=None, istty=None):

if bold or color:
retVal = colored(message, color=color, on_color=None, attrs=("bold",) if bold else None)
elif level:
elif level and hasattr(LOGGER_HANDLER, "colorize"):
try:
level = getattr(logging, level, None)
except:
Expand Down
6 changes: 5 additions & 1 deletion lib/core/dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,15 @@ def _reportData(self, data, content_type):
collector is active - which is only ever the case for a CLI --report-json run, never under
--api - so this never double-captures alongside StdDbOut. A None content_type is resolved
via the kb.partRun fallback (e.g. the fingerprint line), mirroring the API exactly.

Keyed by kb.reportTaskId rather than the fixed REPORT_TASKID so that a multi-target run
(e.g. '-m' bulk file) keeps each target's results separate instead of later targets
overwriting earlier ones under the same content_type.
"""

if conf.get("reportCollector") is not None:
from lib.utils.api import _storeData, REPORT_TASKID
_storeData(conf.reportCollector, REPORT_TASKID, stdoutEncode(clearColors(data)), CONTENT_STATUS.COMPLETE, content_type)
_storeData(conf.reportCollector, kb.get("reportTaskId", REPORT_TASKID), stdoutEncode(clearColors(data)), CONTENT_STATUS.COMPLETE, content_type)

def flush(self):
if self._outputFP:
Expand Down
2 changes: 1 addition & 1 deletion lib/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from thirdparty import six

# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.9.12"
VERSION = "1.10.9.14"
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)
Expand Down
15 changes: 13 additions & 2 deletions lib/utils/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,19 @@ def writeReportJson(collector, filepath):
"""
Writes the collected results to filepath as JSON, in the same shape as the REST API's
/scan/<id>/data response, wrapped with a small 'meta' block for standalone consumers.

A multi-target run (e.g. '-m' bulk file) stores each target under its own taskid (see
kb.reportTaskId), so every taskid present in the collector is assembled here; a single-target
run still yields exactly one taskid and keeps the flat {success, data, error} shape.
"""

result = _assembleData(collector, REPORT_TASKID)
taskids = sorted(row[0] for row in collector.execute("SELECT DISTINCT taskid FROM data")) or [REPORT_TASKID]

if len(taskids) > 1:
result = {"success": True, "targets": [_assembleData(collector, taskid) for taskid in taskids]}
else:
result = _assembleData(collector, taskids[0])

result["meta"] = {
"api_version": int(RESTAPI_VERSION.split(".")[0]), # MAJOR only - the part that matters for client compatibility
"sqlmap_version": VERSION_STRING,
Expand Down Expand Up @@ -467,7 +477,8 @@ def __init__(self, collector):

def emit(self, record):
try:
self.collector.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (REPORT_TASKID, str(record.msg % record.args if record.args else record.msg)))
taskid = kb.get("reportTaskId", REPORT_TASKID)
self.collector.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (taskid, str(record.msg % record.args if record.args else record.msg)))
except Exception:
pass

Expand Down
44 changes: 44 additions & 0 deletions tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
safeSQLIdentificatorNaming,
saveConfig,
serializeObject,
setColor,
setTechnique,
splitFields,
trimAlphaNum,
Expand Down Expand Up @@ -1784,6 +1785,49 @@ def test_no_old_options_is_noop(self):
self.assertIsNone(checkOldOptions(["-u", "http://test.invalid/?id=1", "--banner"]))


class TestSetColorHandlerMismatch(unittest.TestCase):
"""
Regression test for issue #6122: setColor() decided whether to colorize purely from
conf.disableColoring/IS_TTY, independent of whether the installed LOGGER_HANDLER actually
supports colorize() (a plain logging.StreamHandler - installed for --disable-coloring, or as
the ansistrm-unavailable fallback - never defines it). A multiprocessing hash-cracking worker
hit exactly this mismatch (conf.disableColoring not carried over into the worker) and crashed
with an AttributeError that got misreported as "there was a problem while hashing entry".
"""

def setUp(self):
import lib.core.common as common_mod
self._common_mod = common_mod
self._saved_handler = common_mod.LOGGER_HANDLER
self._saved_disableColoring = conf.get("disableColoring")

def tearDown(self):
self._common_mod.LOGGER_HANDLER = self._saved_handler
conf.disableColoring = self._saved_disableColoring

def test_plain_handler_without_colorize_does_not_raise(self):
import logging
self._common_mod.LOGGER_HANDLER = logging.StreamHandler() # no .colorize(), like the --disable-coloring handler
conf.disableColoring = False # the desync: coloring "should" apply, but handler can't
result = setColor("[INFO] current status: abcde", istty=True) # must not raise
self.assertIsInstance(result, str)

def test_colorizing_handler_still_used(self):
# sanity check: a handler that DOES define colorize() is unaffected by the guard
calls = []

class _FakeColorizingHandler(object):
def colorize(self, message, levelno, force=False):
calls.append((message, levelno, force))
return "COLORIZED"

self._common_mod.LOGGER_HANDLER = _FakeColorizingHandler()
conf.disableColoring = False
result = setColor("[INFO] current status: abcde", istty=True)
self.assertEqual(result, "COLORIZED")
self.assertEqual(len(calls), 1)


if __name__ == "__main__":
unittest.main(verbosity=2)

Expand Down
93 changes: 93 additions & 0 deletions tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,5 +222,98 @@ def test_file_is_valid_json_with_meta(self):
os.remove(path)


class TestMultiTargetTaskId(_CollectorCase):
"""
Regression coverage for issue #6123: a '-m' bulk-file run shares ONE process (and thus one
report collector) across many targets. _storeData()'s COMPLETE-status branch deletes any
existing row for a (taskid, content_type) key before inserting the new one, so if every
target reused the same fixed REPORT_TASKID, a later target's TARGET/TECHNIQUES write would
silently delete an earlier target's row of the same content_type. The fix keys each target's
writes by kb.reportTaskId instead.
"""

def setUp(self):
super(TestMultiTargetTaskId, self).setUp()
from lib.core.dump import Dump
self._saved_reportTaskId = kb.get("reportTaskId")
self._saved_dumper = conf.get("dumper")
self._saved_reportCollector = conf.get("reportCollector")
conf.dumper = Dump()
conf.reportCollector = self.c

def tearDown(self):
kb.reportTaskId = self._saved_reportTaskId
conf.dumper = self._saved_dumper
conf.reportCollector = self._saved_reportCollector
super(TestMultiTargetTaskId, self).tearDown()

def test_second_target_does_not_overwrite_first(self):
kb.reportTaskId = 1
conf.dumper._reportData({"url": "http://host1/?id=1"}, CONTENT_TYPE.TARGET)

kb.reportTaskId = 2
conf.dumper._reportData({"url": "http://host2/?id=1"}, CONTENT_TYPE.TARGET)

first = api._assembleData(self.c, 1)["data"]
second = api._assembleData(self.c, 2)["data"]
self.assertEqual(first[0]["value"]["url"], "http://host1/?id=1") # not clobbered by target #2
self.assertEqual(second[0]["value"]["url"], "http://host2/?id=1")

def test_write_report_json_wraps_multiple_targets(self):
kb.reportTaskId = 1
conf.dumper._reportData({"url": "http://host1/?id=1"}, CONTENT_TYPE.TARGET)
kb.reportTaskId = 2
conf.dumper._reportData({"url": "http://host2/?id=1"}, CONTENT_TYPE.TARGET)

fd, path = tempfile.mkstemp(suffix=".json")
os.close(fd)
try:
api.writeReportJson(self.c, path)
with io.open(path, encoding="utf-8") as f:
loaded = json.load(f)
self.assertIn("targets", loaded)
self.assertEqual(len(loaded["targets"]), 2)
urls = [t["data"][0]["value"]["url"] for t in loaded["targets"]]
self.assertEqual(urls, ["http://host1/?id=1", "http://host2/?id=1"])
finally:
os.remove(path)

def test_single_target_report_keeps_flat_shape(self):
# backward compatibility: exactly one taskid -> no 'targets' wrapper, same shape as before
kb.reportTaskId = 1
conf.dumper._reportData({"url": "http://host1/?id=1"}, CONTENT_TYPE.TARGET)

fd, path = tempfile.mkstemp(suffix=".json")
os.close(fd)
try:
api.writeReportJson(self.c, path)
with io.open(path, encoding="utf-8") as f:
loaded = json.load(f)
self.assertNotIn("targets", loaded)
self.assertEqual(loaded["data"][0]["value"]["url"], "http://host1/?id=1")
finally:
os.remove(path)

def test_error_recorded_under_active_target(self):
import logging
from lib.core.data import logger

saved_level = logger.level
logger.setLevel(logging.ERROR)
# mute pre-existing handlers (e.g. console) but not the ReportErrorRecorder added by setUp
muted = [(handler, handler.level) for handler in logger.handlers if not isinstance(handler, api.ReportErrorRecorder)]
for handler, _ in muted:
handler.setLevel(logging.CRITICAL + 1)
try:
kb.reportTaskId = 2
logger.error("boom for target 2")
self.assertTrue(any("boom for target 2" in _ for _ in api._assembleData(self.c, 2)["error"]))
self.assertEqual(api._assembleData(self.c, 1)["error"], []) # not attributed to target #1
finally:
logger.setLevel(saved_level)
for handler, level in muted:
handler.setLevel(level)


if __name__ == "__main__":
unittest.main(verbosity=2)