mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-08-04 14:55:40 +00:00
1911 lines
78 KiB
Python
1911 lines
78 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
|
See the file 'LICENSE' for copying permission
|
|
"""
|
|
|
|
import io
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import webbrowser
|
|
|
|
from lib.core.common import getSafeExString
|
|
from lib.core.common import saveConfig
|
|
from lib.core.data import paths
|
|
from lib.core.defaults import defaults
|
|
from lib.core.enums import MKSTEMP_PREFIX
|
|
from lib.core.exception import SqlmapMissingDependence
|
|
from lib.core.exception import SqlmapSystemException
|
|
from lib.core.settings import DEV_EMAIL_ADDRESS
|
|
from lib.core.settings import IS_WIN
|
|
from lib.core.settings import ISSUES_PAGE
|
|
from lib.core.settings import GIT_PAGE
|
|
from lib.core.settings import SITE
|
|
from lib.core.settings import VERSION_STRING
|
|
from lib.core.settings import WIKI_PAGE
|
|
from thirdparty.six.moves import queue as _queue
|
|
|
|
try:
|
|
_text_type = unicode
|
|
except NameError:
|
|
_text_type = str
|
|
|
|
_binary_type = str if sys.version_info[0] < 3 else bytes
|
|
_clock = getattr(time, "perf_counter", getattr(time, "clock", time.time))
|
|
|
|
def _toText(value):
|
|
"""Return a Unicode text value on both Python 2.7 and Python 3.x."""
|
|
if value is None:
|
|
return u""
|
|
if isinstance(value, _text_type):
|
|
return value
|
|
if isinstance(value, _binary_type):
|
|
try:
|
|
return value.decode("utf-8", "replace")
|
|
except Exception:
|
|
return _text_type(value)
|
|
try:
|
|
return _text_type(value)
|
|
except Exception:
|
|
return _text_type(repr(value))
|
|
|
|
def _toBytes(value):
|
|
"""Return UTF-8 bytes suitable for a binary subprocess pipe."""
|
|
if isinstance(value, _binary_type):
|
|
return value
|
|
return _toText(value).encode("utf-8", "replace")
|
|
|
|
def _waitForProcess(process, timeout):
|
|
"""Python 2 compatible replacement for Popen.wait(timeout=...)."""
|
|
deadline = _clock() + max(0.0, timeout)
|
|
while process.poll() is None and _clock() < deadline:
|
|
time.sleep(0.03)
|
|
return process.poll()
|
|
|
|
def _list2cmdline(arguments):
|
|
values = [_toText(_) for _ in arguments]
|
|
if sys.version_info[0] < 3:
|
|
return _toText(subprocess.list2cmdline([_toBytes(_) for _ in values]))
|
|
return _toText(subprocess.list2cmdline(values))
|
|
|
|
# A restrained security-tool palette: the layout stays familiar, while the darker
|
|
# navigation, cyan accents and terminal surfaces add a light Havij-era character.
|
|
PALETTE = {
|
|
"base": "#d7dce1",
|
|
"mantle": "#243545",
|
|
"crust": "#101820",
|
|
"surface0": "#f8fafb",
|
|
"surface1": "#8d98a3",
|
|
"surface2": "#e7ebef",
|
|
"light": "#ffffff",
|
|
"dark": "#3c4650",
|
|
"text": "#17212b",
|
|
"subtext": "#33414f",
|
|
"overlay": "#657381",
|
|
"title2": "#0b79a5",
|
|
"blue": "#164d73",
|
|
"sapphire": "#087caf",
|
|
"sky": "#169ec1",
|
|
"green": "#2d9659",
|
|
"teal": "#178b86",
|
|
"red": "#bd3f45",
|
|
"maroon": "#8c3d56",
|
|
"mauve": "#86549a",
|
|
"pink": "#b34e83",
|
|
"peach": "#c56d35",
|
|
"yellow": "#b78a18",
|
|
"lavender": "#6172b8",
|
|
"flamingo": "#bf5b72",
|
|
"gold": "#d29b22",
|
|
"navText": "#eef4f8",
|
|
"navMuted": "#a9bac8",
|
|
"navHover": "#31485d",
|
|
"panel": "#eef2f5",
|
|
"border": "#9ca7b1",
|
|
"success": "#2f9b5b",
|
|
"command": "#13232e",
|
|
"commandText": "#8de19b",
|
|
"consoleText": "#d8e7de",
|
|
"consoleMuted": "#8fa69a",
|
|
}
|
|
|
|
# a distinct accent color per section, so the sidebar icons read as a colorful, scannable set
|
|
ICON_COLORS = {
|
|
"Quick start": "yellow",
|
|
"Target": "red",
|
|
"Request": "sapphire",
|
|
"Optimization": "teal",
|
|
"Injection": "mauve",
|
|
"Detection": "sky",
|
|
"Techniques": "maroon",
|
|
"Fingerprint": "lavender",
|
|
"Enumeration": "green",
|
|
"Brute force": "peach",
|
|
"User-defined function injection": "pink",
|
|
"File system access": "gold",
|
|
"Operating system access": "blue",
|
|
"Windows registry access": "sapphire",
|
|
"General": "teal",
|
|
"Miscellaneous": "overlay",
|
|
}
|
|
|
|
# Options surfaced on the curated "Quick start" pane (by destination), in display order
|
|
QUICK_START_DESTS = (
|
|
"data", "cookie", "dbms", "level", "risk", "technique",
|
|
"getCurrentUser", "getCurrentDb", "getBanner", "isDba",
|
|
"getDbs", "getTables", "getColumns", "getPasswordHashes", "dumpTable",
|
|
"batch", "threads", "proxy", "tor",
|
|
)
|
|
|
|
# Short, readable sidebar labels for the (sometimes verbose) option-group titles
|
|
NAV_ALIASES = {
|
|
"User-defined function injection": "UDF injection",
|
|
"Operating system access": "OS access",
|
|
"Windows registry access": "Windows registry",
|
|
"File system access": "File system",
|
|
}
|
|
|
|
TARGET_PLACEHOLDER = "http://www.target.com/vuln.php?id=1"
|
|
|
|
HINT_DEFAULT = "Hover or focus a field to see what it does."
|
|
MAX_CONSOLE_LINES = 12000
|
|
MAX_SEARCH_RESULTS = 12
|
|
|
|
# --- parser-backend compatibility (works for both optparse and argparse objects) ---
|
|
|
|
def _parserGroups(parser):
|
|
groups = getattr(parser, "option_groups", None)
|
|
if groups is None:
|
|
groups = [_ for _ in getattr(parser, "_action_groups", []) if getattr(_, "title", None) not in (None, "positional arguments", "optional arguments", "options")]
|
|
return groups or []
|
|
|
|
def _groupOptions(group):
|
|
for attr in ("option_list", "_group_actions"):
|
|
if hasattr(group, attr):
|
|
return getattr(group, attr)
|
|
return []
|
|
|
|
def _groupTitle(group):
|
|
return getattr(group, "title", "") or ""
|
|
|
|
def _groupDescription(group):
|
|
if hasattr(group, "get_description"):
|
|
return group.get_description() or ""
|
|
return getattr(group, "description", "") or ""
|
|
|
|
def _optStrings(option):
|
|
if hasattr(option, "option_strings"): # argparse
|
|
return list(option.option_strings)
|
|
return list(getattr(option, "_short_opts", None) or []) + list(getattr(option, "_long_opts", None) or [])
|
|
|
|
def _optDest(option):
|
|
return getattr(option, "dest", None)
|
|
|
|
def _optHelp(option):
|
|
return getattr(option, "help", "") or ""
|
|
|
|
def _optChoices(option):
|
|
return getattr(option, "choices", None)
|
|
|
|
def _optTakesValue(option):
|
|
if hasattr(option, "takes_value"): # optparse Option
|
|
try:
|
|
return option.takes_value()
|
|
except Exception:
|
|
pass
|
|
return getattr(option, "nargs", 1) != 0 # argparse: store_true/false has nargs 0
|
|
|
|
def _optValueType(option):
|
|
kind = getattr(option, "type", None)
|
|
if kind in ("int", int):
|
|
return "int"
|
|
if kind in ("float", float):
|
|
return "float"
|
|
return "string"
|
|
|
|
def _optionLabel(option):
|
|
return ", ".join(_optStrings(option)) or (_optDest(option) or "")
|
|
|
|
def _preferredFlag(option):
|
|
strings = _optStrings(option)
|
|
longOptions = [_ for _ in strings if _.startswith("--")]
|
|
return (longOptions or strings or [""])[0]
|
|
|
|
def _quoteArg(value):
|
|
value = _toText(value)
|
|
if IS_WIN:
|
|
return _list2cmdline([value])
|
|
if not value:
|
|
return u"''"
|
|
safe = u"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_@%+=:,./-"
|
|
if all(character in safe for character in value):
|
|
return value
|
|
return u"'" + value.replace(u"'", u"'\"'\"'") + u"'"
|
|
|
|
class _TooltipManager(object):
|
|
"""One shared tooltip/hint dispatcher for every option control.
|
|
|
|
Per-widget Python/Tcl bindings are surprisingly expensive when a pane contains
|
|
dozens of options. Widgets only receive a small Python attribute; four global
|
|
bindings handle the whole application.
|
|
"""
|
|
|
|
def __init__(self, owner, root, tk, palette, delay=500):
|
|
self._owner = owner
|
|
self._root = root
|
|
self._tk = tk
|
|
self._palette = palette
|
|
self._delay = delay
|
|
self._widget = None
|
|
self._tip = None
|
|
self._job = None
|
|
root.bind_all("<Enter>", self._enter, add="+")
|
|
root.bind_all("<Leave>", self._leave, add="+")
|
|
root.bind_all("<FocusIn>", self._focusIn, add="+")
|
|
root.bind_all("<FocusOut>", self._focusOut, add="+")
|
|
root.bind_all("<ButtonPress>", self._hide, add="+")
|
|
|
|
def attach(self, widget, text):
|
|
if text:
|
|
widget._sqlmap_help = text
|
|
|
|
def _textFor(self, widget):
|
|
return getattr(widget, "_sqlmap_help", "")
|
|
|
|
def _setHint(self, text):
|
|
try:
|
|
if hasattr(self._owner, "hint"):
|
|
self._owner.hint.set(text or HINT_DEFAULT)
|
|
except Exception:
|
|
pass
|
|
|
|
def _enter(self, event):
|
|
text = self._textFor(event.widget)
|
|
if not text:
|
|
return
|
|
self._widget = event.widget
|
|
self._setHint(text)
|
|
self._cancel()
|
|
try:
|
|
self._job = self._root.after(self._delay, self._show)
|
|
except Exception:
|
|
self._job = None
|
|
|
|
def _leave(self, event):
|
|
if event.widget is self._widget:
|
|
self._widget = None
|
|
self._cancel()
|
|
self._hide()
|
|
self._setHint(HINT_DEFAULT)
|
|
|
|
def _focusIn(self, event):
|
|
text = self._textFor(event.widget)
|
|
if text:
|
|
self._setHint(text)
|
|
|
|
def _focusOut(self, event):
|
|
if self._textFor(event.widget):
|
|
self._setHint(HINT_DEFAULT)
|
|
|
|
def _cancel(self):
|
|
if self._job is not None:
|
|
try:
|
|
self._root.after_cancel(self._job)
|
|
except Exception:
|
|
pass
|
|
self._job = None
|
|
|
|
def _show(self):
|
|
self._job = None
|
|
widget = self._widget
|
|
text = self._textFor(widget) if widget is not None else ""
|
|
if not text:
|
|
return
|
|
try:
|
|
if not widget.winfo_exists():
|
|
return
|
|
x = widget.winfo_rootx() + 18
|
|
y = widget.winfo_rooty() + widget.winfo_height() + 6
|
|
self._tip = tw = self._tk.Toplevel(widget)
|
|
# Toplevels are initially mapped by Tk at the default 0,0 position.
|
|
# Keep the tooltip withdrawn until its children have been measured and
|
|
# its final geometry has been assigned; otherwise X11 briefly paints an
|
|
# empty box in the screen corner before the real tooltip appears.
|
|
tw.withdraw()
|
|
tw.wm_overrideredirect(True)
|
|
try:
|
|
tw.wm_transient(self._root)
|
|
except Exception:
|
|
pass
|
|
self._tk.Label(tw, text=text, justify="left", background=self._palette["surface0"],
|
|
foreground=self._palette["text"], relief="solid", borderwidth=1,
|
|
wraplength=460, padx=10, pady=7).pack()
|
|
tw.update_idletasks()
|
|
width = max(1, tw.winfo_reqwidth())
|
|
height = max(1, tw.winfo_reqheight())
|
|
x = min(x, max(0, tw.winfo_screenwidth() - width - 8))
|
|
y = min(y, max(0, tw.winfo_screenheight() - height - 8))
|
|
tw.wm_geometry("%dx%d+%d+%d" % (width, height, x, y))
|
|
tw.deiconify()
|
|
tw.lift()
|
|
except Exception:
|
|
if self._tip is not None:
|
|
try:
|
|
self._tip.destroy()
|
|
except Exception:
|
|
pass
|
|
self._tip = None
|
|
|
|
def _hide(self, event=None):
|
|
self._cancel()
|
|
if self._tip is not None:
|
|
try:
|
|
self._tip.destroy()
|
|
except Exception:
|
|
pass
|
|
self._tip = None
|
|
|
|
class SqlmapGui(object):
|
|
def __init__(self, parser, tk, ttk, scrolledtext, messagebox, filedialog, font):
|
|
self.parser = parser
|
|
self.tk = tk
|
|
self.ttk = ttk
|
|
self.scrolledtext = scrolledtext
|
|
self.messagebox = messagebox
|
|
self.filedialog = filedialog
|
|
self.font = font
|
|
|
|
self.widgets = {} # dest -> (type, shared effective-value Tk variable)
|
|
self.vars = {} # dest -> shared Tk variable (one per option)
|
|
self.optionByDest = {}
|
|
self.optionOrder = []
|
|
self.sectionByDest = {}
|
|
self.searchIndex = []
|
|
for group in _parserGroups(parser):
|
|
title = _groupTitle(group)
|
|
for option in _groupOptions(group):
|
|
dest = _optDest(option)
|
|
if dest:
|
|
if dest not in self.optionByDest:
|
|
self.optionOrder.append(dest)
|
|
self.optionByDest[dest] = option
|
|
self.sectionByDest[dest] = title
|
|
|
|
for index, dest in enumerate(self.optionOrder):
|
|
option = self.optionByDest[dest]
|
|
section = self.sectionByDest.get(dest, "")
|
|
label = _optionLabel(option)
|
|
flag = _preferredFlag(option)
|
|
self.searchIndex.append((
|
|
dest,
|
|
index,
|
|
label,
|
|
section,
|
|
flag,
|
|
" ".join((label, dest, section, _optHelp(option))).lower(),
|
|
))
|
|
|
|
self.panes = {} # name -> outer frame
|
|
self.navItems = {} # name -> (row frame, accent strip, icon canvas, label, badge)
|
|
self.canvases = {} # name -> canvas (for wheel binding)
|
|
self.inners = {} # name -> scrollable inner frame (populated lazily)
|
|
self.builders = {} # name -> callable that populates the inner frame
|
|
self.built = set() # names whose content has been built
|
|
self.buildStates = {} # name -> generator for incremental pane construction
|
|
self._prebuildQueue = []
|
|
self._prebuildJob = None
|
|
self.badges = {} # name -> sidebar count badge label
|
|
self.sectionDests = {} # name -> [option dests in that section]
|
|
self.paneOrder = [] # nav order, for Up/Down navigation
|
|
self.currentPane = None
|
|
self.controlsByDest = {} # dest -> [(pane name, interactive widget)]
|
|
self.searchMatches = []
|
|
|
|
self.process = None
|
|
self.processQueue = None
|
|
self.processConfigFile = None
|
|
self.consoleWindow = None
|
|
self.consoleText = None
|
|
self.consoleStatus = None
|
|
self._runSerial = 0
|
|
self._refreshJob = None
|
|
self._searchJob = None
|
|
self._headerJob = None
|
|
self._suspendRefresh = False
|
|
|
|
try:
|
|
self.window = tk.Tk()
|
|
except Exception as ex:
|
|
raise SqlmapSystemException("unable to create GUI window ('%s')" % getSafeExString(ex))
|
|
|
|
self.tooltip = _TooltipManager(self, self.window, tk, PALETTE)
|
|
self._initializeVariables()
|
|
self._initFonts()
|
|
self._initStyle()
|
|
self._buildLayout()
|
|
self.window.protocol("WM_DELETE_WINDOW", self._closeApplication)
|
|
|
|
def _initializeVariables(self):
|
|
for dest in self.optionOrder:
|
|
option = self.optionByDest[dest]
|
|
isBool = not _optTakesValue(option)
|
|
otype = "bool" if isBool else _optValueType(option)
|
|
default = defaults.get(dest)
|
|
if isBool:
|
|
var = self.tk.BooleanVar(value=bool(default))
|
|
else:
|
|
var = self.tk.StringVar(value="" if default in (None, False) else default)
|
|
self.vars[dest] = var
|
|
self.widgets[dest] = (otype, var)
|
|
try:
|
|
var.trace("w", self._onOptionChanged)
|
|
except Exception:
|
|
pass
|
|
|
|
def _onOptionChanged(self, *unused):
|
|
if not self._suspendRefresh:
|
|
self._scheduleRefresh()
|
|
|
|
def _scheduleRefresh(self, delay=70):
|
|
if self._refreshJob is not None:
|
|
try:
|
|
self.window.after_cancel(self._refreshJob)
|
|
except Exception:
|
|
pass
|
|
self._refreshJob = self.window.after(delay, self._refreshDerivedState)
|
|
|
|
def _refreshDerivedState(self):
|
|
self._refreshJob = None
|
|
self._updateStats()
|
|
self.command.set(self._buildCommandString())
|
|
self._updateStatusLight()
|
|
|
|
def _updateStatusLight(self):
|
|
try:
|
|
canvas = self.statusLight
|
|
except AttributeError:
|
|
return
|
|
try:
|
|
canvas.delete("all")
|
|
if self._isRunning():
|
|
color = PALETTE["success"]
|
|
elif any(self._isOptionSet(_) for _ in self.widgets):
|
|
color = PALETTE["sky"]
|
|
else:
|
|
color = PALETTE["surface1"]
|
|
canvas.create_oval(2, 2, 10, 10, fill=color, outline=PALETTE["dark"])
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _initFonts(self):
|
|
family = self.font.nametofont("TkDefaultFont").actual("family")
|
|
self.fonts = {
|
|
"body": (family, 10),
|
|
"bodyBold": (family, 10, "bold"),
|
|
"small": (family, 9),
|
|
"nav": (family, 10),
|
|
"title": (family, 18, "bold"),
|
|
"subtitle": (family, 9),
|
|
"mono": (self.font.nametofont("TkFixedFont").actual("family"), 10),
|
|
}
|
|
|
|
def _initStyle(self):
|
|
p = PALETTE
|
|
face = p["base"]
|
|
field = p["surface0"]
|
|
style = self.ttk.Style()
|
|
if "clam" in style.theme_names():
|
|
style.theme_use("clam")
|
|
|
|
style.configure(".", background=face, foreground=p["text"], fieldbackground=field,
|
|
bordercolor=p["border"], lightcolor=p["light"], darkcolor=p["surface1"],
|
|
troughcolor=p["surface2"], focuscolor=p["blue"], insertcolor=p["text"],
|
|
font=self.fonts["body"])
|
|
|
|
style.configure("TFrame", background=face)
|
|
style.configure("Bar.TFrame", background=p["panel"])
|
|
style.configure("Nav.TFrame", background=p["mantle"])
|
|
style.configure("Card.TFrame", background=p["panel"])
|
|
style.configure("Panel.TFrame", background=p["surface0"])
|
|
style.configure("PaneHeader.TFrame", background=p["surface0"])
|
|
|
|
style.configure("TLabel", background=face, foreground=p["text"])
|
|
style.configure("Title.TLabel", background=p["blue"], foreground="#ffffff", font=self.fonts["title"])
|
|
style.configure("Subtitle.TLabel", background=p["blue"], foreground="#dceaf2", font=self.fonts["subtitle"])
|
|
style.configure("Hint.TLabel", background=p["panel"], foreground=p["overlay"], font=self.fonts["small"])
|
|
style.configure("PanelHint.TLabel", background=p["surface0"], foreground=p["overlay"], font=self.fonts["small"])
|
|
style.configure("PanelLabel.TLabel", background=p["surface0"], foreground=p["blue"], font=self.fonts["bodyBold"])
|
|
style.configure("NavHint.TLabel", background=p["mantle"], foreground=p["navMuted"], font=self.fonts["small"])
|
|
style.configure("NavTitle.TLabel", background=p["mantle"], foreground=p["navText"], font=self.fonts["bodyBold"])
|
|
style.configure("Field.TLabel", background=p["panel"], foreground=p["text"])
|
|
style.configure("Desc.TLabel", background=p["panel"], foreground=p["overlay"], font=self.fonts["small"])
|
|
style.configure("Pane.TLabel", background=p["surface0"], foreground=p["blue"], font=self.fonts["title"])
|
|
style.configure("PaneCount.TLabel", background=p["surface0"], foreground=p["overlay"], font=self.fonts["small"])
|
|
style.configure("Stat.TLabel", background=p["panel"], foreground=p["overlay"], font=self.fonts["small"])
|
|
style.configure("Prompt.TLabel", background=field, foreground=p["text"], font=self.fonts["mono"])
|
|
|
|
style.configure("TButton", background=p["surface2"], foreground=p["text"], relief="raised", borderwidth=1,
|
|
lightcolor=p["light"], darkcolor=p["surface1"], bordercolor=p["border"],
|
|
focuscolor=p["blue"], padding=(11, 5))
|
|
style.map("TButton", background=[("active", p["surface0"]), ("pressed", p["surface1"])],
|
|
relief=[("pressed", "sunken")])
|
|
style.configure("Tool.TButton", padding=(9, 4), font=self.fonts["small"])
|
|
style.configure("Primary.TButton", background=p["success"], foreground="#ffffff", bordercolor=p["green"],
|
|
lightcolor="#78c89a", darkcolor="#17643a", padding=(12, 5), font=self.fonts["bodyBold"])
|
|
style.map("Primary.TButton", background=[("active", "#39aa68"), ("pressed", "#247c49")],
|
|
foreground=[("disabled", "#d7e4dc")])
|
|
|
|
style.configure("TEntry", fieldbackground=field, foreground=p["text"], relief="sunken", borderwidth=1,
|
|
bordercolor=p["border"], lightcolor=p["surface1"], darkcolor=p["light"],
|
|
insertcolor=p["text"], padding=5)
|
|
style.configure("Target.TEntry", fieldbackground="#ffffff", foreground=p["text"], relief="sunken", borderwidth=1,
|
|
bordercolor=p["sapphire"], lightcolor=p["surface1"], darkcolor=p["light"],
|
|
insertcolor=p["text"], padding=7, font=self.fonts["body"])
|
|
style.configure("Search.TEntry", fieldbackground="#192936", foreground=p["navText"], relief="flat", borderwidth=1,
|
|
bordercolor="#4c6376", lightcolor="#4c6376", darkcolor="#17242f",
|
|
insertcolor="#ffffff", padding=6)
|
|
|
|
style.configure("TCheckbutton", background=p["panel"], foreground=p["text"], focuscolor=p["panel"], padding=2,
|
|
indicatorbackground=field, indicatorforeground=p["blue"], indicatorrelief="sunken",
|
|
indicatorborderwidth=1, bordercolor=p["border"], lightcolor=p["surface1"], darkcolor=p["light"])
|
|
style.map("TCheckbutton", background=[("active", p["panel"])],
|
|
indicatorbackground=[("active", field), ("selected", field)])
|
|
|
|
style.configure("TCombobox", fieldbackground=field, background=p["surface2"], foreground=p["text"],
|
|
arrowcolor=p["blue"], relief="sunken", borderwidth=1, bordercolor=p["border"],
|
|
lightcolor=p["surface1"], darkcolor=p["light"], padding=4)
|
|
|
|
style.configure("Vertical.TScrollbar", background=p["surface2"], troughcolor=p["panel"],
|
|
bordercolor=p["border"], lightcolor=p["light"], darkcolor=p["surface1"],
|
|
arrowcolor=p["text"], relief="raised", width=16)
|
|
style.map("Vertical.TScrollbar", background=[("active", p["surface0"])])
|
|
|
|
self.window.configure(background=face)
|
|
|
|
def _buildLayout(self):
|
|
tk = self.tk
|
|
p = PALETTE
|
|
self.window.title("sqlmap GUI")
|
|
self.window.minsize(980, 690)
|
|
self._buildMenu()
|
|
self._buildHeader()
|
|
|
|
targetShell = tk.Frame(self.window, background=p["border"], borderwidth=0)
|
|
targetShell.pack(fill=tk.X, padx=16, pady=(10, 8))
|
|
target = self.ttk.Frame(targetShell, style="Panel.TFrame", padding=(14, 10, 14, 12))
|
|
target.pack(fill=tk.X, padx=1, pady=1)
|
|
tk.Frame(target, background=p["red"], height=3).pack(fill=tk.X, pady=(0, 9))
|
|
|
|
labelRow = self.ttk.Frame(target, style="Panel.TFrame")
|
|
labelRow.pack(fill=tk.X, pady=(0, 5))
|
|
self.ttk.Label(labelRow, text="TARGET URL", style="PanelLabel.TLabel").pack(side=tk.LEFT)
|
|
self.ttk.Label(labelRow, text="Ctrl+L", style="PanelHint.TLabel").pack(side=tk.RIGHT)
|
|
self.ttk.Label(labelRow, text=" e.g. %s" % TARGET_PLACEHOLDER, style="PanelHint.TLabel").pack(side=tk.LEFT)
|
|
|
|
targetRow = self.ttk.Frame(target, style="Panel.TFrame")
|
|
targetRow.pack(fill=tk.X)
|
|
urlVar = self._destVar("url", False)
|
|
self.targetEntry = self.ttk.Entry(targetRow, style="Target.TEntry", textvariable=urlVar)
|
|
self.targetEntry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=1)
|
|
self.ttk.Button(targetRow, text="Paste", style="Tool.TButton", command=self._pasteTarget,
|
|
takefocus=False).pack(side=tk.LEFT, padx=(8, 0))
|
|
self.ttk.Button(targetRow, text="Clear", style="Tool.TButton", command=self._clearTarget,
|
|
takefocus=False).pack(side=tk.LEFT, padx=(6, 0))
|
|
self.controlsByDest.setdefault("url", []).append((None, self.targetEntry))
|
|
|
|
body = self.ttk.Frame(self.window, style="TFrame")
|
|
body.pack(expand=True, fill=tk.BOTH)
|
|
|
|
navHolder = self.ttk.Frame(body, style="Nav.TFrame", width=224)
|
|
navHolder.pack(side=tk.LEFT, fill=tk.Y)
|
|
navHolder.pack_propagate(False)
|
|
|
|
searchBar = self.ttk.Frame(navHolder, style="Nav.TFrame", padding=(11, 11, 11, 8))
|
|
searchBar.pack(fill=tk.X)
|
|
searchTitle = self.ttk.Frame(searchBar, style="Nav.TFrame")
|
|
searchTitle.pack(fill=tk.X, pady=(0, 5))
|
|
self.ttk.Label(searchTitle, text="OPTION FINDER", style="NavTitle.TLabel").pack(side=tk.LEFT)
|
|
self.ttk.Label(searchTitle, text="Ctrl+K", style="NavHint.TLabel").pack(side=tk.RIGHT)
|
|
self.searchVar = tk.StringVar(value="")
|
|
self.searchEntry = self.ttk.Entry(searchBar, style="Search.TEntry", textvariable=self.searchVar)
|
|
self.searchEntry.pack(fill=tk.X)
|
|
self.searchEntry.bind("<Return>", self._activateSearchResult)
|
|
self.searchEntry.bind("<Down>", self._searchMoveDown)
|
|
try:
|
|
self.searchVar.trace("w", self._scheduleSearch)
|
|
except Exception:
|
|
pass
|
|
|
|
self.searchList = tk.Listbox(navHolder, height=6, activestyle="dotbox", exportselection=False,
|
|
bg="#192936", fg=p["navText"], selectbackground=p["sapphire"],
|
|
selectforeground="#ffffff", relief="flat", borderwidth=1,
|
|
highlightthickness=1, highlightbackground="#4c6376",
|
|
font=self.fonts["small"])
|
|
self.searchList.bind("<ButtonRelease-1>", self._clickSearchResult)
|
|
self.searchList.bind("<Return>", self._activateSearchResult)
|
|
|
|
self.navCanvas = tk.Canvas(navHolder, background=p["mantle"], highlightthickness=0, borderwidth=0)
|
|
navScroll = self.ttk.Scrollbar(navHolder, orient="vertical", command=self.navCanvas.yview,
|
|
style="Vertical.TScrollbar")
|
|
self.nav = self.ttk.Frame(self.navCanvas, style="Nav.TFrame")
|
|
self.nav.bind("<Configure>", lambda e: self.navCanvas.configure(scrollregion=self.navCanvas.bbox("all")))
|
|
navWin = self.navCanvas.create_window((0, 0), window=self.nav, anchor="nw")
|
|
self.navCanvas.bind("<Configure>", lambda e: self.navCanvas.itemconfigure(navWin, width=e.width))
|
|
self.navCanvas.configure(yscrollcommand=navScroll.set)
|
|
self.navCanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
navScroll.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
tk.Frame(body, background=p["border"], width=1).pack(side=tk.LEFT, fill=tk.Y)
|
|
|
|
self.content = self.ttk.Frame(body, style="Card.TFrame")
|
|
self.content.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
|
|
|
|
cmdBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(16, 8))
|
|
cmdBar.pack(fill=tk.X)
|
|
self.ttk.Label(cmdBar, text=">_", style="PanelLabel.TLabel").pack(side=tk.LEFT, padx=(0, 8))
|
|
self.ttk.Button(cmdBar, text="Copy", style="Tool.TButton", command=self._copyCommand,
|
|
takefocus=False).pack(side=tk.RIGHT, padx=(7, 0))
|
|
self.ttk.Button(cmdBar, text="Reset", style="Tool.TButton", command=self.resetOptions,
|
|
takefocus=False).pack(side=tk.RIGHT, padx=(7, 0))
|
|
self.command = tk.StringVar(value="sqlmap.py")
|
|
cmdEntry = tk.Entry(cmdBar, textvariable=self.command, font=self.fonts["mono"],
|
|
bg=p["command"], fg=p["commandText"], readonlybackground=p["command"],
|
|
disabledforeground=p["commandText"], relief="flat", borderwidth=0,
|
|
highlightthickness=1, highlightbackground=p["border"],
|
|
highlightcolor=p["sapphire"], state="readonly")
|
|
cmdEntry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=5)
|
|
|
|
hintBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(16, 8))
|
|
hintBar.pack(fill=tk.X)
|
|
self.statusLight = tk.Canvas(hintBar, width=12, height=12, background=p["panel"],
|
|
highlightthickness=0, borderwidth=0)
|
|
self.statusLight.pack(side=tk.LEFT, padx=(0, 8))
|
|
self.stat = tk.StringVar(value="")
|
|
self.ttk.Label(hintBar, textvariable=self.stat, style="Stat.TLabel", anchor="e").pack(side=tk.RIGHT, padx=(12, 0))
|
|
self.hint = tk.StringVar(value=HINT_DEFAULT)
|
|
self.ttk.Label(hintBar, textvariable=self.hint, style="Hint.TLabel", anchor="w").pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
self._buildQuickStartPane()
|
|
for group in _parserGroups(self.parser):
|
|
self._buildGroupPane(group)
|
|
|
|
self._prebuildQueue = list(self.paneOrder)
|
|
self._selectPane("Quick start")
|
|
self.window.bind("<Down>", lambda e: self._navKey(1))
|
|
self.window.bind("<Up>", lambda e: self._navKey(-1))
|
|
for seq in ("<MouseWheel>", "<Button-4>", "<Button-5>"):
|
|
self.window.bind_all(seq, self._onWheel)
|
|
self.window.bind("<F5>", lambda e: self.run())
|
|
self.window.bind("<Control-r>", lambda e: self.run())
|
|
self.window.bind("<Control-Return>", lambda e: self.run())
|
|
self.window.bind("<Control-l>", lambda e: self._focusTarget())
|
|
self.window.bind("<Control-k>", lambda e: self._focusSearch())
|
|
self.window.bind("<Escape>", self._escapeAction)
|
|
self.window.bind("<Control-s>", lambda e: self.saveConfigDialog())
|
|
self.window.bind("<Control-o>", lambda e: self.loadConfig())
|
|
self._enableSelectAll()
|
|
self._refreshDerivedState()
|
|
self._center(self.window, 1060, 750)
|
|
self._schedulePanePrebuild(60)
|
|
|
|
def _enableSelectAll(self):
|
|
# Tk binds Ctrl-A to "cursor to line start" by default; rebind it to select-all,
|
|
# which is what users expect (covers entries, comboboxes and the console text widget)
|
|
def selectEntry(event):
|
|
try:
|
|
event.widget.select_range(0, "end")
|
|
event.widget.icursor("end")
|
|
except Exception:
|
|
pass
|
|
return "break"
|
|
|
|
def selectText(event):
|
|
try:
|
|
event.widget.tag_add("sel", "1.0", "end-1c")
|
|
except Exception:
|
|
pass
|
|
return "break"
|
|
|
|
for cls in ("TEntry", "Entry", "TCombobox"):
|
|
self.window.bind_class(cls, "<Control-a>", selectEntry)
|
|
self.window.bind_class(cls, "<Control-A>", selectEntry)
|
|
for seq in ("<Control-a>", "<Control-A>"):
|
|
self.window.bind_class("Text", seq, selectText)
|
|
|
|
def _buildMenu(self):
|
|
p = PALETTE
|
|
menuKw = dict(bg=p["panel"], fg=p["text"], activebackground=p["sapphire"],
|
|
activeforeground="#ffffff")
|
|
menubar = self.tk.Menu(self.window, borderwidth=0, **menuKw)
|
|
filemenu = self.tk.Menu(menubar, tearoff=0, **menuKw)
|
|
filemenu.add_command(label="Load configuration...", command=self.loadConfig)
|
|
filemenu.add_command(label="Save configuration...", command=self.saveConfigDialog)
|
|
filemenu.add_command(label="Reset all options", command=self.resetOptions)
|
|
filemenu.add_separator()
|
|
filemenu.add_command(label="Exit", command=self._closeApplication)
|
|
menubar.add_cascade(label="File", menu=filemenu)
|
|
menubar.add_command(label="Run", command=self.run)
|
|
helpmenu = self.tk.Menu(menubar, tearoff=0, **menuKw)
|
|
helpmenu.add_command(label="Official site", command=lambda: webbrowser.open(SITE))
|
|
helpmenu.add_command(label="GitHub", command=lambda: webbrowser.open(GIT_PAGE))
|
|
helpmenu.add_command(label="Wiki", command=lambda: webbrowser.open(WIKI_PAGE))
|
|
helpmenu.add_command(label="Report issue", command=lambda: webbrowser.open(ISSUES_PAGE))
|
|
helpmenu.add_separator()
|
|
helpmenu.add_command(label="About", command=lambda: self.messagebox.showinfo(
|
|
"About", "%s\n\n (%s)" % (VERSION_STRING, DEV_EMAIL_ADDRESS)))
|
|
menubar.add_cascade(label="Help", menu=helpmenu)
|
|
self.window.config(menu=menubar)
|
|
|
|
def _buildHeader(self):
|
|
self._runHover = False
|
|
self.header = self.tk.Canvas(self.window, height=76, highlightthickness=0, borderwidth=0, background=PALETTE["base"])
|
|
self.header.pack(fill=self.tk.X)
|
|
self.header.bind("<Configure>", self._scheduleHeaderDraw)
|
|
|
|
def _scheduleHeaderDraw(self, event=None):
|
|
if self._headerJob is not None:
|
|
try:
|
|
self.window.after_cancel(self._headerJob)
|
|
except Exception:
|
|
pass
|
|
self._headerJob = self.window.after(35, self._drawHeader)
|
|
|
|
def _interp(self, color1, color2, ratio):
|
|
a = [int(color1[_:_ + 2], 16) for _ in (1, 3, 5)]
|
|
b = [int(color2[_:_ + 2], 16) for _ in (1, 3, 5)]
|
|
return "#%02x%02x%02x" % tuple(int(a[_] + (b[_] - a[_]) * ratio) for _ in range(3))
|
|
|
|
def _drawHeader(self):
|
|
"""Draw the header only for resize or process-state changes.
|
|
|
|
Keep this deliberately cheap. Redrawing a canvas from an <Enter>/<Leave>
|
|
callback can remove the item currently under the pointer, which generates a
|
|
matching leave/enter pair and can turn into an event/redraw loop on Tk/X11.
|
|
"""
|
|
self._headerJob = None
|
|
p = PALETTE
|
|
c = self.header
|
|
c.delete("all")
|
|
width = max(1, c.winfo_width())
|
|
height = 76
|
|
|
|
# A small, fixed number of primitives paints faster and more consistently
|
|
# than a strip-per-gradient header, especially on X11 and remote displays.
|
|
c.create_rectangle(0, 0, width, height, outline="", fill="#17445f")
|
|
c.create_rectangle(0, 0, 6, height, outline="", fill=p["sky"])
|
|
c.create_rectangle(6, height - 4, width, height, outline="", fill="#0e7697")
|
|
c.create_line(22, 64, max(22, width - 160), 64, fill="#39738a")
|
|
|
|
c.create_text(26, 26, text="sqlmap", anchor="w", fill="#ffffff", font=self.fonts["title"])
|
|
c.create_text(124, 30, text=VERSION_STRING.replace("sqlmap/", "v"), anchor="w",
|
|
fill="#bfe1ed", font=self.fonts["subtitle"])
|
|
c.create_text(26, 52, text="automatic SQL injection and database takeover tool", anchor="w",
|
|
fill="#dcecf2", font=self.fonts["small"])
|
|
self._drawRunButton(width, height)
|
|
|
|
def _isRunning(self):
|
|
return self.process is not None and self.process.poll() is None
|
|
|
|
def _drawRunButton(self, width, height):
|
|
p = PALETTE
|
|
c = self.header
|
|
running = self._isRunning()
|
|
bw, bh = 116, 34
|
|
x0 = width - bw - 22
|
|
y0 = (height - bh) // 2
|
|
x1, y1 = x0 + bw, y0 + bh
|
|
baseFill = p["red"] if running else p["success"]
|
|
fill = ("#d15056" if running else "#3bae6b") if self._runHover else baseFill
|
|
c.create_rectangle(x0, y0, x1, y1, fill=fill, outline="#d9f1e3", width=1,
|
|
tags=("runbtn", "runpill"))
|
|
c.create_line(x0 + 1, y0 + 1, x1 - 1, y0 + 1, fill="#8fd2aa" if not running else "#ef9da1",
|
|
tags="runbtn")
|
|
c.create_line(x0 + 1, y1 - 1, x1 - 1, y1 - 1, fill="#17613a" if not running else "#75252a",
|
|
tags="runbtn")
|
|
cy = (y0 + y1) // 2
|
|
tx = x0 + 23
|
|
if running:
|
|
c.create_rectangle(tx, cy - 6, tx + 11, cy + 6, fill="#ffffff", outline="",
|
|
tags=("runbtn", "runico"))
|
|
else:
|
|
c.create_polygon(tx, cy - 6, tx, cy + 6, tx + 10, cy, fill="#ffffff", outline="",
|
|
tags=("runbtn", "runico"))
|
|
c.create_text((x0 + x1) // 2 + 8, cy, text=("Stop" if running else "Run"), fill="#ffffff",
|
|
font=self.fonts["bodyBold"], tags=("runbtn", "runico"))
|
|
c.tag_bind("runbtn", "<Button-1>", lambda e: self._runButtonAction())
|
|
c.tag_bind("runbtn", "<Enter>", lambda e: self._hoverRun(True))
|
|
c.tag_bind("runbtn", "<Leave>", lambda e: self._hoverRun(False))
|
|
|
|
def _runButtonAction(self):
|
|
if self._isRunning():
|
|
self.stopProcess()
|
|
else:
|
|
self.run()
|
|
|
|
def _hoverRun(self, on):
|
|
"""Update only the existing button items; never rebuild the header here."""
|
|
self._runHover = on
|
|
try:
|
|
running = self._isRunning()
|
|
if on:
|
|
fill = "#d15056" if running else "#3bae6b"
|
|
else:
|
|
fill = PALETTE["red"] if running else PALETTE["success"]
|
|
self.header.itemconfigure("runpill", fill=fill)
|
|
self.header.configure(cursor="hand2" if on else "")
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _drawIcon(self, c, name, col):
|
|
# minimal line-art icons, drawn as vectors so they render everywhere and need no assets
|
|
c.delete("all")
|
|
|
|
def line(*pts, **kw):
|
|
c.create_line(*pts, fill=col, width=2, capstyle="round", joinstyle="round", **kw)
|
|
|
|
def oval(x0, y0, x1, y1, filled=False):
|
|
c.create_oval(x0, y0, x1, y1, outline=col, width=2, fill=(col if filled else ""))
|
|
|
|
def rect(x0, y0, x1, y1, filled=False):
|
|
c.create_rectangle(x0, y0, x1, y1, outline=col, width=2, fill=(col if filled else ""))
|
|
|
|
def poly(*pts):
|
|
c.create_polygon(*pts, fill=col, outline="")
|
|
|
|
def arc(x0, y0, x1, y1, start, extent):
|
|
c.create_arc(x0, y0, x1, y1, start=start, extent=extent, outline=col, width=2, style="arc")
|
|
|
|
def dot(x, y, r=2):
|
|
c.create_oval(x - r, y - r, x + r, y + r, fill=col, outline="")
|
|
|
|
def glyph(text, size=11):
|
|
c.create_text(11, 11, text=text, fill=col, font=(self.fonts["bodyBold"][0], size, "bold"))
|
|
|
|
if name == "Quick start":
|
|
poly(12, 3, 6, 12, 10, 12, 9, 19, 16, 9, 11, 9)
|
|
elif name == "Target":
|
|
oval(4, 4, 18, 18)
|
|
dot(11, 11, 2)
|
|
elif name == "Request":
|
|
line(4, 8, 17, 8, arrow="last")
|
|
line(18, 14, 5, 14, arrow="last")
|
|
elif name == "Optimization":
|
|
arc(4, 6, 18, 20, 0, 180)
|
|
line(11, 13, 15, 8)
|
|
elif name == "Injection":
|
|
# syringe: thumb rest + plunger rod + flange + barrel + needle (no arrowhead, so it reads as a needle not a cross)
|
|
line(9, 2, 13, 2)
|
|
line(11, 2, 11, 5)
|
|
line(6, 5, 16, 5)
|
|
rect(8, 5, 14, 14)
|
|
line(11, 14, 11, 20)
|
|
elif name == "Detection":
|
|
oval(4, 4, 13, 13)
|
|
line(12, 12, 18, 18)
|
|
elif name == "Techniques":
|
|
oval(7, 7, 15, 15)
|
|
line(11, 2, 11, 6)
|
|
line(11, 16, 11, 20)
|
|
line(2, 11, 6, 11)
|
|
line(16, 11, 20, 11)
|
|
elif name == "Fingerprint":
|
|
# tightly nested tall loops with the gap at the bottom (fingertip ridges), plus a central core
|
|
arc(3, 1, 19, 21, 285, 330)
|
|
arc(5, 4, 17, 18, 285, 330)
|
|
arc(7, 7, 15, 15, 285, 330)
|
|
arc(9, 10, 13, 12, 285, 330)
|
|
elif name == "Enumeration":
|
|
oval(4, 3, 18, 7)
|
|
line(4, 5, 4, 16)
|
|
line(18, 5, 18, 16)
|
|
arc(4, 12, 18, 18, 180, 180)
|
|
elif name == "Brute force":
|
|
oval(3, 7, 11, 15)
|
|
line(9, 11, 19, 11)
|
|
line(16, 11, 16, 15)
|
|
line(19, 11, 19, 14)
|
|
elif name == "User-defined function injection":
|
|
glyph("fx", 11)
|
|
elif name == "File system access":
|
|
poly(3, 7, 8, 7, 10, 9, 19, 9, 19, 17, 3, 17)
|
|
elif name == "Operating system access":
|
|
rect(3, 5, 19, 17)
|
|
line(6, 9, 9, 11)
|
|
line(6, 13, 9, 13)
|
|
elif name == "Windows registry access":
|
|
# the waving Windows flag (4 slanted panes) rather than a plain 2x2 grid
|
|
poly(4, 6, 10, 5, 10, 11, 4, 12)
|
|
poly(12, 5, 18, 4, 18, 10, 12, 11)
|
|
poly(4, 13, 10, 12, 10, 18, 4, 19)
|
|
poly(12, 12, 18, 11, 18, 17, 12, 18)
|
|
elif name == "General":
|
|
line(4, 6, 18, 6)
|
|
dot(14, 6)
|
|
line(4, 11, 18, 11)
|
|
dot(8, 11)
|
|
line(4, 16, 18, 16)
|
|
dot(13, 16)
|
|
elif name == "Miscellaneous":
|
|
dot(5, 11)
|
|
dot(11, 11)
|
|
dot(17, 11)
|
|
else:
|
|
dot(11, 11, 3)
|
|
|
|
def _addPane(self, name, navText):
|
|
p = PALETTE
|
|
tk = self.tk
|
|
row = tk.Frame(self.nav, background=p["mantle"])
|
|
row.pack(fill=tk.X)
|
|
strip = tk.Frame(row, background=p["mantle"], width=3)
|
|
strip.pack(side=tk.LEFT, fill=tk.Y)
|
|
icon = tk.Canvas(row, width=22, height=22, highlightthickness=0, borderwidth=0, background=p["mantle"])
|
|
icon.pack(side=tk.LEFT, padx=(13, 0), pady=8)
|
|
self._drawIcon(icon, name, self._iconColor(name))
|
|
badge = tk.Label(row, text="", background=p["mantle"], foreground=p["navMuted"], font=self.fonts["small"])
|
|
badge.pack(side=tk.RIGHT, padx=(0, 12))
|
|
self.badges[name] = badge
|
|
lab = tk.Label(row, text=navText, background=p["mantle"], foreground=p["navText"],
|
|
font=self.fonts["nav"], anchor="w", padx=10, pady=9)
|
|
lab.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
for w in (row, lab, strip, icon, badge):
|
|
w.bind("<Button-1>", lambda e, n=name: self._selectPane(n))
|
|
w.bind("<Enter>", lambda e, n=name: self._navHover(n, True))
|
|
w.bind("<Leave>", lambda e, n=name: self._navHover(n, False))
|
|
self.navItems[name] = (row, strip, icon, lab, badge)
|
|
self.paneOrder.append(name)
|
|
|
|
outer = self.ttk.Frame(self.content, style="Card.TFrame")
|
|
canvas = tk.Canvas(outer, background=p["panel"], highlightthickness=0, borderwidth=0)
|
|
scrollbar = self.ttk.Scrollbar(outer, orient="vertical", command=canvas.yview, style="Vertical.TScrollbar")
|
|
inner = self.ttk.Frame(canvas, style="Card.TFrame", padding=(24, 20))
|
|
inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
|
window_id = canvas.create_window((0, 0), window=inner, anchor="nw")
|
|
canvas.bind("<Configure>", lambda e: canvas.itemconfigure(window_id, width=e.width))
|
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|
canvas.pack(side="left", fill="both", expand=True)
|
|
scrollbar.pack(side="right", fill="y")
|
|
self.panes[name] = outer
|
|
self.canvases[name] = canvas
|
|
self.inners[name] = inner
|
|
return inner
|
|
|
|
def _iconColor(self, name):
|
|
return PALETTE.get(ICON_COLORS.get(name, "subtext"), PALETTE["subtext"])
|
|
|
|
def _navHover(self, name, entering):
|
|
if entering:
|
|
self._prioritizePaneBuild(name)
|
|
if name == self.currentPane:
|
|
return
|
|
bg = PALETTE["navHover"] if entering else PALETTE["mantle"]
|
|
row, strip, icon, lab, badge = self.navItems[name]
|
|
for w in (row, strip, icon, lab, badge):
|
|
w.configure(background=bg)
|
|
|
|
def _navKey(self, delta):
|
|
try:
|
|
focused = self.window.focus_get()
|
|
except Exception:
|
|
focused = None
|
|
if isinstance(focused, (self.ttk.Entry, self.ttk.Combobox)):
|
|
return None
|
|
if self.paneOrder:
|
|
index = self.paneOrder.index(self.currentPane)
|
|
self._selectPane(self.paneOrder[(index + delta) % len(self.paneOrder)])
|
|
return "break"
|
|
|
|
def _selectPane(self, name):
|
|
# Build only a tiny, time-bounded slice synchronously so a never-visited pane
|
|
# appears immediately. The remaining rows are completed by the idle prebuilder.
|
|
if name not in self.built:
|
|
self._prioritizePaneBuild(name)
|
|
self._buildPaneSlice(name, budgetMs=14, minimumSteps=5)
|
|
if self.currentPane == name:
|
|
return
|
|
p = PALETTE
|
|
if self.currentPane:
|
|
self.panes[self.currentPane].pack_forget()
|
|
row, strip, icon, lab, badge = self.navItems[self.currentPane]
|
|
for w in (row, strip, icon):
|
|
w.configure(background=p["mantle"])
|
|
lab.configure(background=p["mantle"], foreground=p["navText"], font=self.fonts["nav"])
|
|
badge.configure(background=p["mantle"], foreground=p["navMuted"])
|
|
self._drawIcon(icon, self.currentPane, self._iconColor(self.currentPane))
|
|
self.panes[name].pack(expand=True, fill=self.tk.BOTH)
|
|
row, strip, icon, lab, badge = self.navItems[name]
|
|
for w in (row, icon):
|
|
w.configure(background=p["blue"])
|
|
strip.configure(background=p["sky"])
|
|
lab.configure(background=p["blue"], foreground="#ffffff", font=self.fonts["bodyBold"])
|
|
badge.configure(background=p["blue"], foreground="#d9edf5")
|
|
self._drawIcon(icon, name, "#ffffff")
|
|
self.currentPane = name
|
|
# Geometry flushing here used to make first-time pane switches feel much
|
|
# slower than the widget creation itself. Sidebar visibility can be fixed
|
|
# on the next idle turn without blocking the click handler.
|
|
self.window.after_idle(lambda n=name: self._ensureNavVisible(n) if self.currentPane == n else None)
|
|
|
|
if hasattr(self, "hint"): # don't leave the previous section's option hint lingering
|
|
self.hint.set(HINT_DEFAULT)
|
|
|
|
def _ensureNavVisible(self, name):
|
|
# scroll the sidebar so the active item stays in view (e.g. when paging with Up/Down)
|
|
try:
|
|
row = self.navItems[name][0]
|
|
total = self.nav.winfo_height()
|
|
viewH = self.navCanvas.winfo_height()
|
|
if total <= 1 or viewH <= 1:
|
|
return
|
|
top = row.winfo_y()
|
|
bottom = top + row.winfo_height()
|
|
curTop = self.navCanvas.yview()[0] * total
|
|
if top < curTop:
|
|
self.navCanvas.yview_moveto(float(top) / total)
|
|
elif bottom > curTop + viewH:
|
|
self.navCanvas.yview_moveto(float(bottom - viewH) / total)
|
|
except Exception:
|
|
pass
|
|
|
|
def _onWheel(self, event):
|
|
# Route the wheel only when the pointer is actually over this window's sidebar/content.
|
|
rawDelta = getattr(event, "delta", 0)
|
|
if getattr(event, "num", None) == 5:
|
|
delta = 1
|
|
elif getattr(event, "num", None) == 4:
|
|
delta = -1
|
|
else:
|
|
delta = -int(rawDelta / 120) if abs(rawDelta) >= 120 else (-1 if rawDelta > 0 else 1)
|
|
target = None
|
|
node = self.window.winfo_containing(event.x_root, event.y_root)
|
|
while node is not None:
|
|
if node is self.navCanvas:
|
|
target = self.navCanvas
|
|
break
|
|
if self.currentPane and node is self.canvases.get(self.currentPane):
|
|
target = self.canvases[self.currentPane]
|
|
break
|
|
try:
|
|
node = node.master
|
|
except Exception:
|
|
break
|
|
if target is not None:
|
|
target.yview_scroll(delta, "units")
|
|
return "break"
|
|
return None
|
|
|
|
def _schedulePanePrebuild(self, delay=1):
|
|
if self._prebuildJob is not None:
|
|
return
|
|
try:
|
|
self._prebuildJob = self.window.after(delay, self._pumpPanePrebuild)
|
|
except Exception:
|
|
self._prebuildJob = None
|
|
|
|
def _prioritizePaneBuild(self, name):
|
|
if name in self.built:
|
|
return
|
|
try:
|
|
self._prebuildQueue.remove(name)
|
|
except ValueError:
|
|
pass
|
|
self._prebuildQueue.insert(0, name)
|
|
self._schedulePanePrebuild()
|
|
|
|
def _buildPaneSlice(self, name, budgetMs=7, minimumSteps=1):
|
|
if name in self.built:
|
|
return True
|
|
builder = self.builders.get(name)
|
|
if builder is None:
|
|
self.built.add(name)
|
|
return True
|
|
state = self.buildStates.get(name)
|
|
if state is None:
|
|
state = builder(self.inners[name])
|
|
self.buildStates[name] = state
|
|
|
|
deadline = _clock() + max(0.001, budgetMs / 1000.0)
|
|
steps = 0
|
|
while steps < minimumSteps or _clock() < deadline:
|
|
try:
|
|
next(state)
|
|
steps += 1
|
|
except StopIteration:
|
|
self.buildStates.pop(name, None)
|
|
self.built.add(name)
|
|
try:
|
|
self._prebuildQueue.remove(name)
|
|
except ValueError:
|
|
pass
|
|
return True
|
|
except Exception:
|
|
# A broken optional field should not make the whole GUI unusable.
|
|
self.buildStates.pop(name, None)
|
|
self.built.add(name)
|
|
try:
|
|
self._prebuildQueue.remove(name)
|
|
except ValueError:
|
|
pass
|
|
return True
|
|
return False
|
|
|
|
def _pumpPanePrebuild(self):
|
|
self._prebuildJob = None
|
|
while self._prebuildQueue and self._prebuildQueue[0] in self.built:
|
|
self._prebuildQueue.pop(0)
|
|
if not self._prebuildQueue:
|
|
return
|
|
|
|
name = self._prebuildQueue[0]
|
|
finished = self._buildPaneSlice(name, budgetMs=7, minimumSteps=1)
|
|
if finished and self._prebuildQueue and self._prebuildQueue[0] == name:
|
|
self._prebuildQueue.pop(0)
|
|
# Yield to pointer, keyboard, expose and paint events after every small slice.
|
|
self._schedulePanePrebuild(1)
|
|
|
|
def _scheduleSearch(self, *unused):
|
|
if self._searchJob is not None:
|
|
try:
|
|
self.window.after_cancel(self._searchJob)
|
|
except Exception:
|
|
pass
|
|
self._searchJob = self.window.after(90, self._applySearch)
|
|
|
|
def _applySearch(self):
|
|
self._searchJob = None
|
|
query = self.searchVar.get().strip().lower()
|
|
if not query:
|
|
self.searchMatches = []
|
|
self.searchList.delete(0, self.tk.END)
|
|
self.searchList.pack_forget()
|
|
return
|
|
|
|
tokens = query.split()
|
|
matches = []
|
|
for dest, index, label, section, flag, haystack in self.searchIndex:
|
|
if not all(token in haystack for token in tokens):
|
|
continue
|
|
score = 0
|
|
if dest.lower().startswith(query):
|
|
score -= 40
|
|
if flag.lower().startswith(query) or flag.lower().startswith("--" + query):
|
|
score -= 30
|
|
if label.lower().startswith(query):
|
|
score -= 20
|
|
if section.lower().startswith(query):
|
|
score -= 10
|
|
matches.append((score, index, dest, label, section))
|
|
|
|
matches.sort()
|
|
matches = matches[:MAX_SEARCH_RESULTS]
|
|
self.searchMatches = [item[2] for item in matches]
|
|
self.searchList.delete(0, self.tk.END)
|
|
for _, _, dest, label, section in matches:
|
|
shortSection = NAV_ALIASES.get(section, section)
|
|
self.searchList.insert(self.tk.END, "%s [%s]" % (label, shortSection))
|
|
|
|
if matches:
|
|
self.searchList.configure(height=min(7, len(matches)))
|
|
self.searchList.pack(fill=self.tk.X, padx=9, pady=(0, 7), before=self.navCanvas)
|
|
self.searchList.selection_clear(0, self.tk.END)
|
|
self.searchList.selection_set(0)
|
|
self.searchList.activate(0)
|
|
else:
|
|
self.searchList.insert(self.tk.END, "No matching options")
|
|
self.searchList.configure(height=1)
|
|
self.searchList.pack(fill=self.tk.X, padx=9, pady=(0, 7), before=self.navCanvas)
|
|
|
|
def _searchMoveDown(self, event=None):
|
|
if self.searchMatches:
|
|
self.searchList.focus_set()
|
|
self.searchList.selection_clear(0, self.tk.END)
|
|
self.searchList.selection_set(0)
|
|
self.searchList.activate(0)
|
|
return "break"
|
|
|
|
def _clickSearchResult(self, event):
|
|
if not self.searchMatches:
|
|
return "break"
|
|
try:
|
|
index = int(self.searchList.nearest(event.y))
|
|
except Exception:
|
|
index = 0
|
|
if index < 0 or index >= len(self.searchMatches):
|
|
return "break"
|
|
# Resolve the clicked row ourselves instead of depending on Listbox class
|
|
# bindings, whose selection update happens after this widget binding.
|
|
self.searchList.selection_clear(0, self.tk.END)
|
|
self.searchList.selection_set(index)
|
|
self.searchList.activate(index)
|
|
return self._activateSearchIndex(index)
|
|
|
|
def _activateSearchResult(self, event=None):
|
|
if not self.searchMatches:
|
|
return "break"
|
|
selection = self.searchList.curselection()
|
|
index = int(selection[0]) if selection else 0
|
|
return self._activateSearchIndex(index)
|
|
|
|
def _activateSearchIndex(self, index):
|
|
if not self.searchMatches:
|
|
return "break"
|
|
if index < 0 or index >= len(self.searchMatches):
|
|
index = 0
|
|
dest = self.searchMatches[index]
|
|
section = self.sectionByDest.get(dest)
|
|
self.searchVar.set("")
|
|
if section in self.panes:
|
|
self._selectPane(section)
|
|
self._focusOptionWhenReady(dest, section)
|
|
elif dest == "url":
|
|
self._focusTarget()
|
|
return "break"
|
|
|
|
def _focusOptionWhenReady(self, dest, paneName):
|
|
# A search can jump to an option that the incremental pane builder has not
|
|
# created yet. Continue that pane in tiny slices and focus as soon as the
|
|
# requested widget exists, without blocking the click handler.
|
|
for candidatePane, candidateWidget in self.controlsByDest.get(dest, ()):
|
|
if candidatePane == paneName:
|
|
self.window.after_idle(lambda d=dest, p=paneName: self._focusOption(d, p))
|
|
return
|
|
if paneName not in self.built:
|
|
self._prioritizePaneBuild(paneName)
|
|
self._buildPaneSlice(paneName, budgetMs=6, minimumSteps=1)
|
|
self.window.after(1, lambda d=dest, p=paneName: self._focusOptionWhenReady(d, p))
|
|
|
|
def _focusOption(self, dest, paneName):
|
|
candidates = self.controlsByDest.get(dest, ())
|
|
widget = None
|
|
for candidatePane, candidateWidget in candidates:
|
|
if candidatePane == paneName:
|
|
widget = candidateWidget
|
|
break
|
|
if widget is None and candidates:
|
|
widget = candidates[0][1]
|
|
if widget is None:
|
|
return
|
|
try:
|
|
widget.focus_set()
|
|
if isinstance(widget, (self.ttk.Entry, self.ttk.Combobox)):
|
|
widget.select_range(0, "end")
|
|
canvas = self.canvases.get(paneName)
|
|
inner = self.inners.get(paneName)
|
|
if canvas is not None and inner is not None:
|
|
inner.update_idletasks()
|
|
total = max(1, inner.winfo_height())
|
|
canvas.yview_moveto(max(0.0, min(1.0, float(widget.winfo_y() - 30) / total)))
|
|
except Exception:
|
|
pass
|
|
|
|
def _buildPaneHeading(self, parent, title, description, optionCount):
|
|
p = PALETTE
|
|
card = self.tk.Frame(parent, background=p["surface0"], highlightthickness=1,
|
|
highlightbackground=p["border"], borderwidth=0)
|
|
card.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 16))
|
|
accent = self.tk.Frame(card, background=self._iconColor(title), width=5)
|
|
accent.pack(side=self.tk.LEFT, fill=self.tk.Y)
|
|
content = self.ttk.Frame(card, style="PaneHeader.TFrame", padding=(14, 10, 14, 10))
|
|
content.pack(side=self.tk.LEFT, fill=self.tk.BOTH, expand=True)
|
|
titleRow = self.ttk.Frame(content, style="PaneHeader.TFrame")
|
|
titleRow.pack(fill=self.tk.X)
|
|
self.ttk.Label(titleRow, text=title, style="Pane.TLabel").pack(side=self.tk.LEFT)
|
|
self.ttk.Label(titleRow, text="%d option%s" % (optionCount, "" if optionCount == 1 else "s"),
|
|
style="PaneCount.TLabel").pack(side=self.tk.RIGHT, pady=(6, 0))
|
|
if description:
|
|
self.ttk.Label(content, text=description, style="PanelHint.TLabel", wraplength=690,
|
|
justify="left").pack(fill=self.tk.X, pady=(3, 0))
|
|
|
|
def _buildQuickStartPane(self):
|
|
name = "Quick start"
|
|
self._addPane(name, name)
|
|
self.sectionDests[name] = [_ for _ in QUICK_START_DESTS if _ in self.optionByDest]
|
|
|
|
def build(inner):
|
|
description = "The options people reach for most. Set the target above, choose what you need, then Run."
|
|
self._buildPaneHeading(inner, name, description, len(self.sectionDests[name]))
|
|
yield
|
|
row = 1
|
|
for dest in QUICK_START_DESTS:
|
|
option = self.optionByDest.get(dest)
|
|
if option is not None:
|
|
row = self._buildFieldRow(inner, option, row, paneName=name)
|
|
yield
|
|
inner.columnconfigure(1, weight=1)
|
|
|
|
self.builders[name] = build
|
|
|
|
def _buildGroupPane(self, group):
|
|
title = _groupTitle(group)
|
|
self._addPane(title, NAV_ALIASES.get(title, title))
|
|
self.sectionDests[title] = [_optDest(_) for _ in _groupOptions(group) if _optDest(_)]
|
|
|
|
def build(inner, group=group, title=title):
|
|
self._buildPaneHeading(inner, title, _groupDescription(group), len(self.sectionDests[title]))
|
|
yield
|
|
row = 1
|
|
for option in _groupOptions(group):
|
|
row = self._buildFieldRow(inner, option, row, paneName=title)
|
|
yield
|
|
inner.columnconfigure(1, weight=1)
|
|
|
|
self.builders[title] = build
|
|
|
|
def _destVar(self, dest, is_bool):
|
|
# One shared effective-value variable per option, reflected in every duplicate control.
|
|
if dest not in self.vars:
|
|
var = self.tk.BooleanVar(value=False) if is_bool else self.tk.StringVar(value="")
|
|
self.vars[dest] = var
|
|
self.widgets[dest] = ("bool" if is_bool else "string", var)
|
|
try:
|
|
var.trace("w", self._onOptionChanged)
|
|
except Exception:
|
|
pass
|
|
return self.vars[dest]
|
|
|
|
def _buildFieldRow(self, parent, option, row, labelText=None, paneName=None):
|
|
label = labelText or _optionLabel(option)
|
|
helptext = _optHelp(option)
|
|
dest = _optDest(option)
|
|
if not dest:
|
|
return row
|
|
is_bool = not _optTakesValue(option)
|
|
|
|
if is_bool:
|
|
var = self._destVar(dest, True)
|
|
default = bool(defaults.get(dest))
|
|
chk = self.ttk.Checkbutton(parent, text=label, variable=var,
|
|
onvalue=(not default), offvalue=default, takefocus=True)
|
|
chk.grid(row=row, column=0, columnspan=2, sticky="w", pady=5)
|
|
self.tooltip.attach(chk, helptext)
|
|
self.controlsByDest.setdefault(dest, []).append((paneName, chk))
|
|
else:
|
|
otype = _optValueType(option)
|
|
var = self._destVar(dest, False)
|
|
lab = self.ttk.Label(parent, text=label, style="Field.TLabel")
|
|
lab.grid(row=row, column=0, sticky="w", padx=(0, 18), pady=6)
|
|
self.tooltip.attach(lab, helptext)
|
|
choices = _optChoices(option)
|
|
if choices:
|
|
widget = self.ttk.Combobox(parent, values=list(choices), state="readonly", textvariable=var)
|
|
else:
|
|
widget = self.ttk.Entry(parent, textvariable=var)
|
|
if otype in ("int", "float"):
|
|
self._constrain(widget, otype)
|
|
widget.grid(row=row, column=1, sticky="ew", pady=6)
|
|
self.tooltip.attach(widget, helptext)
|
|
self.controlsByDest.setdefault(dest, []).append((paneName, widget))
|
|
return row + 1
|
|
|
|
def _constrain(self, entry, otype):
|
|
def check(proposed):
|
|
if proposed in ("", "+", "-", ".", "+.", "-."):
|
|
return True
|
|
try:
|
|
if otype == "int":
|
|
int(proposed)
|
|
else:
|
|
float(proposed)
|
|
return True
|
|
except (TypeError, ValueError):
|
|
return False
|
|
|
|
vcmd = (self.window.register(check), "%P")
|
|
entry.configure(validate="key", validatecommand=vcmd)
|
|
|
|
|
|
# --- helpers --------------------------------------------------------
|
|
|
|
def _center(self, window, width=None, height=None):
|
|
window.update_idletasks()
|
|
width = width or window.winfo_width()
|
|
height = height or window.winfo_height()
|
|
x = window.winfo_screenwidth() // 2 - width // 2
|
|
y = window.winfo_screenheight() // 2 - height // 2
|
|
window.geometry("%dx%d+%d+%d" % (width, height, x, y))
|
|
|
|
def _isOptionSet(self, dest):
|
|
item = self.widgets.get(dest)
|
|
if item is None:
|
|
return False
|
|
otype, var = item
|
|
try:
|
|
value = var.get()
|
|
except Exception:
|
|
return False
|
|
default = defaults.get(dest)
|
|
if otype == "bool":
|
|
return bool(value) != bool(default)
|
|
if value in (None, ""):
|
|
return False
|
|
displayDefault = "" if default in (None, False) else str(default)
|
|
return str(value) != displayDefault
|
|
|
|
def _updateStats(self):
|
|
setDests = set(_ for _ in self.widgets if self._isOptionSet(_))
|
|
count = len(setDests)
|
|
status = "%d option%s set" % (count, "" if count == 1 else "s")
|
|
if self._isRunning():
|
|
status += " | running"
|
|
self.stat.set(status)
|
|
for name, dests in self.sectionDests.items():
|
|
badge = self.badges.get(name)
|
|
if badge is not None:
|
|
hits = sum(1 for _ in dests if _ in setDests)
|
|
badge.configure(text=(str(hits) if hits else ""))
|
|
|
|
def _buildCommandString(self):
|
|
argv = ["sqlmap.py"]
|
|
for dest in self.optionOrder:
|
|
if not self._isOptionSet(dest):
|
|
continue
|
|
option = self.optionByDest.get(dest)
|
|
flag = _preferredFlag(option) if option is not None else ""
|
|
if not flag:
|
|
continue
|
|
otype, var = self.widgets[dest]
|
|
try:
|
|
argv.append(flag)
|
|
if otype != "bool":
|
|
argv.append(_toText(var.get()))
|
|
except Exception:
|
|
pass
|
|
if IS_WIN:
|
|
return _list2cmdline(argv)
|
|
return " ".join(_quoteArg(_) for _ in argv)
|
|
|
|
def _copyCommand(self):
|
|
try:
|
|
self.window.clipboard_clear()
|
|
self.window.clipboard_append(self.command.get())
|
|
self.hint.set("Command copied to clipboard")
|
|
except Exception:
|
|
pass
|
|
|
|
def _pasteTarget(self):
|
|
try:
|
|
value = self.window.clipboard_get()
|
|
self.vars["url"].set(_toText(value).strip())
|
|
self.targetEntry.focus_set()
|
|
self.targetEntry.icursor("end")
|
|
except Exception:
|
|
self.hint.set("Clipboard does not contain text")
|
|
|
|
def _clearTarget(self):
|
|
try:
|
|
self.vars["url"].set("")
|
|
self.targetEntry.focus_set()
|
|
except Exception:
|
|
pass
|
|
|
|
def _focusTarget(self):
|
|
try:
|
|
self.targetEntry.focus_set()
|
|
self.targetEntry.select_range(0, "end")
|
|
except Exception:
|
|
pass
|
|
return "break"
|
|
|
|
def _focusSearch(self):
|
|
try:
|
|
self.searchEntry.focus_set()
|
|
self.searchEntry.select_range(0, "end")
|
|
except Exception:
|
|
pass
|
|
return "break"
|
|
|
|
def _escapeAction(self, event=None):
|
|
try:
|
|
if self.searchVar.get():
|
|
self.searchVar.set("")
|
|
self.searchEntry.focus_set()
|
|
return "break"
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def _collectConfig(self):
|
|
config = {}
|
|
for dest, (otype, var) in self.widgets.items():
|
|
try:
|
|
if otype == "bool":
|
|
value = bool(var.get())
|
|
else:
|
|
raw = var.get()
|
|
if raw in (None, ""):
|
|
value = None
|
|
elif otype == "int":
|
|
value = int(raw)
|
|
elif otype == "float":
|
|
value = float(raw)
|
|
else:
|
|
value = raw
|
|
except Exception:
|
|
value = None
|
|
config[dest] = value
|
|
for option in self.optionByDest.values():
|
|
dest = _optDest(option)
|
|
if config.get(dest) is None:
|
|
config[dest] = defaults.get(dest, None)
|
|
return config
|
|
|
|
def _setWidgetValue(self, dest, value):
|
|
if dest not in self.widgets:
|
|
return
|
|
otype, var = self.widgets[dest]
|
|
try:
|
|
if otype == "bool":
|
|
var.set(bool(value))
|
|
else:
|
|
var.set("" if value in (None, False) else value)
|
|
except Exception:
|
|
pass
|
|
|
|
def resetOptions(self):
|
|
self._suspendRefresh = True
|
|
try:
|
|
for dest, (otype, var) in self.widgets.items():
|
|
default = defaults.get(dest)
|
|
if otype == "bool":
|
|
var.set(bool(default))
|
|
else:
|
|
var.set("" if default in (None, False) else default)
|
|
finally:
|
|
self._suspendRefresh = False
|
|
self._refreshDerivedState()
|
|
self.hint.set("All options reset to their defaults")
|
|
|
|
|
|
# --- actions --------------------------------------------------------
|
|
|
|
def loadConfig(self):
|
|
path = self.filedialog.askopenfilename(title="Load configuration", filetypes=[("sqlmap config", "*.conf *.ini"), ("All files", "*.*")])
|
|
if not path:
|
|
return
|
|
try:
|
|
from thirdparty.six.moves import configparser as _configparser
|
|
parser = _configparser.ConfigParser()
|
|
parser.optionxform = str
|
|
parser.read(path)
|
|
byLower = dict((_.lower(), _) for _ in self.widgets)
|
|
count = 0
|
|
self._suspendRefresh = True
|
|
try:
|
|
for section in parser.sections():
|
|
for name, value in parser.items(section):
|
|
dest = name if name in self.widgets else byLower.get(name.lower())
|
|
if dest is None:
|
|
continue
|
|
if self.widgets[dest][0] == "bool":
|
|
self._setWidgetValue(dest, str(value).lower() in ("1", "true", "yes", "on"))
|
|
else:
|
|
self._setWidgetValue(dest, value)
|
|
count += 1
|
|
finally:
|
|
self._suspendRefresh = False
|
|
self._refreshDerivedState()
|
|
self.hint.set("Loaded %d options from %s" % (count, os.path.basename(path)))
|
|
except Exception as ex:
|
|
self._suspendRefresh = False
|
|
self.messagebox.showerror("Load failed", getSafeExString(ex))
|
|
|
|
def saveConfigDialog(self):
|
|
path = self.filedialog.asksaveasfilename(title="Save configuration", defaultextension=".conf", filetypes=[("sqlmap config", "*.conf")])
|
|
if not path:
|
|
return
|
|
try:
|
|
saveConfig(self._collectConfig(), path)
|
|
self.hint.set("Saved configuration to %s" % os.path.basename(path))
|
|
except Exception as ex:
|
|
self.messagebox.showerror("Save failed", getSafeExString(ex))
|
|
|
|
def run(self):
|
|
if self._isRunning():
|
|
self.hint.set("sqlmap is already running")
|
|
try:
|
|
self.consoleWindow.deiconify()
|
|
self.consoleWindow.lift()
|
|
except Exception:
|
|
pass
|
|
return
|
|
|
|
configFile = None
|
|
try:
|
|
config = self._collectConfig()
|
|
handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True)
|
|
os.close(handle)
|
|
saveConfig(config, configFile)
|
|
|
|
env = os.environ.copy()
|
|
env.setdefault("PYTHONIOENCODING", "utf-8")
|
|
proc = subprocess.Popen(
|
|
[sys.executable or "python", os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap.py"), "-c", configFile],
|
|
shell=False,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
stdin=subprocess.PIPE,
|
|
bufsize=0,
|
|
close_fds=not IS_WIN,
|
|
universal_newlines=False,
|
|
env=env,
|
|
)
|
|
except Exception as ex:
|
|
self._cleanupConfigFile(configFile)
|
|
self.messagebox.showerror("Unable to start sqlmap", getSafeExString(ex))
|
|
return
|
|
|
|
self._runSerial += 1
|
|
serial = self._runSerial
|
|
outputQueue = _queue.Queue()
|
|
self.process = proc
|
|
self.processQueue = outputQueue
|
|
self.processConfigFile = configFile
|
|
|
|
def enqueue(stream, queue):
|
|
try:
|
|
for line in iter(stream.readline, b""):
|
|
if not line:
|
|
break
|
|
queue.put(_toText(line))
|
|
except Exception as ex:
|
|
queue.put("\n[console reader error: %s]\n" % getSafeExString(ex))
|
|
finally:
|
|
try:
|
|
stream.close()
|
|
except Exception:
|
|
pass
|
|
queue.put(None)
|
|
|
|
thread = threading.Thread(target=enqueue, args=(proc.stdout, outputQueue))
|
|
thread.daemon = True
|
|
thread.start()
|
|
|
|
self.hint.set("sqlmap started")
|
|
self._scheduleHeaderDraw()
|
|
self._refreshDerivedState()
|
|
self._openConsole(proc, outputQueue, serial)
|
|
self.window.after(200, lambda: self._watchProcess(proc, serial, configFile))
|
|
|
|
def _watchProcess(self, proc, serial, configFile):
|
|
if proc.poll() is None:
|
|
try:
|
|
self.window.after(200, lambda: self._watchProcess(proc, serial, configFile))
|
|
except Exception:
|
|
pass
|
|
return
|
|
|
|
self._cleanupConfigFile(configFile)
|
|
if self.process is proc and self._runSerial == serial:
|
|
self.process = None
|
|
self.processQueue = None
|
|
self.processConfigFile = None
|
|
self._scheduleHeaderDraw()
|
|
self._refreshDerivedState()
|
|
self.hint.set("sqlmap finished with exit code %s" % proc.returncode)
|
|
|
|
def stopProcess(self, proc=None):
|
|
proc = proc or self.process
|
|
if proc is None or proc.poll() is not None:
|
|
return
|
|
self.hint.set("Stopping sqlmap...")
|
|
try:
|
|
if self.consoleStatus is not None:
|
|
self.consoleStatus.set("Stopping...")
|
|
except Exception:
|
|
pass
|
|
try:
|
|
proc.terminate()
|
|
except Exception as ex:
|
|
self.messagebox.showerror("Unable to stop sqlmap", getSafeExString(ex))
|
|
return
|
|
|
|
def forceKill():
|
|
if proc.poll() is None:
|
|
try:
|
|
proc.kill()
|
|
except Exception:
|
|
pass
|
|
|
|
self.window.after(1800, forceKill)
|
|
|
|
def _cleanupConfigFile(self, path):
|
|
if path:
|
|
try:
|
|
os.remove(path)
|
|
except OSError:
|
|
pass
|
|
|
|
def _appendConsole(self, text, content, forceScroll=False):
|
|
if not content:
|
|
return
|
|
try:
|
|
atBottom = text.yview()[1] >= 0.985
|
|
text.configure(state="normal")
|
|
text.insert(self.tk.END, content)
|
|
lineCount = int(float(text.index("end-1c").split(".")[0]))
|
|
if lineCount > MAX_CONSOLE_LINES:
|
|
text.delete("1.0", "%d.0" % (lineCount - MAX_CONSOLE_LINES))
|
|
text.configure(state="disabled")
|
|
if forceScroll or atBottom:
|
|
text.see(self.tk.END)
|
|
except Exception:
|
|
pass
|
|
|
|
def _openConsole(self, proc, outputQueue, serial):
|
|
p = PALETTE
|
|
tk = self.tk
|
|
try:
|
|
if self.consoleWindow is not None and self.consoleWindow.winfo_exists():
|
|
self.consoleWindow.destroy()
|
|
except Exception:
|
|
pass
|
|
|
|
top = tk.Toplevel(self.window)
|
|
self.consoleWindow = top
|
|
top.title("sqlmap - console")
|
|
top.configure(background=p["base"])
|
|
|
|
toolbar = self.ttk.Frame(top, style="Bar.TFrame", padding=(10, 8))
|
|
toolbar.pack(fill=tk.X)
|
|
status = tk.StringVar(value="Running")
|
|
self.consoleStatus = status
|
|
self.ttk.Label(toolbar, textvariable=status, style="Stat.TLabel").pack(side=tk.LEFT)
|
|
stopButton = self.ttk.Button(toolbar, text="Stop", command=lambda: self.stopProcess(proc))
|
|
stopButton.pack(side=tk.RIGHT, padx=(8, 0))
|
|
self.ttk.Button(toolbar, text="Save log...", command=lambda: self._saveConsoleLog(text)).pack(side=tk.RIGHT, padx=(8, 0))
|
|
self.ttk.Button(toolbar, text="Clear", command=lambda: self._clearConsole(text)).pack(side=tk.RIGHT)
|
|
|
|
frame = self.ttk.Frame(top, style="Card.TFrame", padding=(10, 0, 10, 8))
|
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
text = self.scrolledtext.ScrolledText(frame, wrap=tk.NONE, bg=p["crust"], fg=p["consoleText"],
|
|
insertbackground=p["commandText"], selectbackground=p["sapphire"],
|
|
selectforeground="#ffffff", relief="sunken", borderwidth=2,
|
|
font=self.fonts["mono"], padx=12, pady=10, state="disabled")
|
|
text.pack(fill=tk.BOTH, expand=True)
|
|
self.consoleText = text
|
|
self._appendConsole(text, "$ %s\n\n" % self.command.get(), forceScroll=True)
|
|
|
|
inputBar = self.ttk.Frame(top, style="Bar.TFrame", padding=(10, 0, 10, 10))
|
|
inputBar.pack(fill=tk.X)
|
|
self.ttk.Label(inputBar, text="Input:", style="Hint.TLabel").pack(side=tk.LEFT, padx=(0, 8))
|
|
inputVar = tk.StringVar(value="")
|
|
inputEntry = self.ttk.Entry(inputBar, textvariable=inputVar)
|
|
inputEntry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
def sendInput(event=None):
|
|
value = inputVar.get()
|
|
if proc.poll() is not None:
|
|
return "break"
|
|
try:
|
|
proc.stdin.write(_toBytes(_toText(value) + u"\n"))
|
|
proc.stdin.flush()
|
|
self._appendConsole(text, "> %s\n" % value, forceScroll=True)
|
|
inputVar.set("")
|
|
except Exception as ex:
|
|
self._appendConsole(text, "[input error: %s]\n" % getSafeExString(ex), forceScroll=True)
|
|
return "break"
|
|
|
|
self.ttk.Button(inputBar, text="Send", command=sendInput).pack(side=tk.RIGHT, padx=(8, 0))
|
|
inputEntry.bind("<Return>", sendInput)
|
|
inputEntry.focus_set()
|
|
|
|
state = {"readerDone": False, "finishedShown": False}
|
|
|
|
def pump():
|
|
try:
|
|
if not top.winfo_exists():
|
|
return
|
|
except Exception:
|
|
return
|
|
|
|
chunks = []
|
|
size = 0
|
|
for _ in range(256):
|
|
try:
|
|
item = outputQueue.get_nowait()
|
|
except _queue.Empty:
|
|
break
|
|
if item is None:
|
|
state["readerDone"] = True
|
|
break
|
|
chunks.append(item)
|
|
size += len(item)
|
|
if size >= 131072:
|
|
break
|
|
if chunks:
|
|
self._appendConsole(text, "".join(chunks))
|
|
|
|
finished = proc.poll() is not None and state["readerDone"] and outputQueue.empty()
|
|
if finished and not state["finishedShown"]:
|
|
state["finishedShown"] = True
|
|
code = proc.returncode
|
|
self._appendConsole(text, "\n--- process finished (exit code %s) ---\n" % code, forceScroll=True)
|
|
status.set("Finished (exit code %s)" % code)
|
|
try:
|
|
inputEntry.configure(state="disabled")
|
|
stopButton.configure(state="disabled")
|
|
except Exception:
|
|
pass
|
|
return
|
|
top.after(45 if chunks else 90, pump)
|
|
|
|
def closeConsole():
|
|
if proc.poll() is None:
|
|
if not self.messagebox.askyesno("Close console", "Stop the running sqlmap process and close the console?"):
|
|
return
|
|
self.stopProcess(proc)
|
|
try:
|
|
top.destroy()
|
|
except Exception:
|
|
pass
|
|
if self.consoleWindow is top:
|
|
self.consoleWindow = None
|
|
self.consoleText = None
|
|
self.consoleStatus = None
|
|
|
|
top.protocol("WM_DELETE_WINDOW", closeConsole)
|
|
self._center(top, 920, 600)
|
|
top.after(45, pump)
|
|
|
|
def _clearConsole(self, text):
|
|
try:
|
|
text.configure(state="normal")
|
|
text.delete("1.0", self.tk.END)
|
|
text.configure(state="disabled")
|
|
except Exception:
|
|
pass
|
|
|
|
def _saveConsoleLog(self, text):
|
|
path = self.filedialog.asksaveasfilename(title="Save console log", defaultextension=".log",
|
|
filetypes=[("Log file", "*.log"), ("Text file", "*.txt"), ("All files", "*.*")])
|
|
if not path:
|
|
return
|
|
try:
|
|
with io.open(path, "w", encoding="utf-8") as handle:
|
|
handle.write(_toText(text.get("1.0", "end-1c")))
|
|
self.hint.set("Saved console log to %s" % os.path.basename(path))
|
|
except Exception as ex:
|
|
self.messagebox.showerror("Save log failed", getSafeExString(ex))
|
|
|
|
def _closeApplication(self):
|
|
proc = self.process
|
|
if proc is not None and proc.poll() is None:
|
|
if not self.messagebox.askyesno("Exit sqlmap GUI", "Stop the running sqlmap process and exit?"):
|
|
return
|
|
try:
|
|
proc.terminate()
|
|
try:
|
|
_waitForProcess(proc, 1.2)
|
|
if proc.poll() is None:
|
|
proc.kill()
|
|
except Exception:
|
|
proc.kill()
|
|
except Exception:
|
|
pass
|
|
self._cleanupConfigFile(self.processConfigFile)
|
|
try:
|
|
self.window.destroy()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def runGui(parser):
|
|
try:
|
|
from thirdparty.six.moves import tkinter as _tkinter
|
|
from thirdparty.six.moves import tkinter_scrolledtext as _scrolledtext
|
|
from thirdparty.six.moves import tkinter_ttk as _ttk
|
|
from thirdparty.six.moves import tkinter_messagebox as _messagebox
|
|
from thirdparty.six.moves import tkinter_filedialog as _filedialog
|
|
from thirdparty.six.moves import tkinter_font as _font
|
|
except ImportError as ex:
|
|
raise SqlmapMissingDependence("missing dependence ('%s')" % getSafeExString(ex))
|
|
|
|
app = SqlmapGui(parser, _tkinter, _ttk, _scrolledtext, _messagebox, _filedialog, _font)
|
|
app.window.mainloop()
|