#!/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("", self._enter, add="+") root.bind_all("", self._leave, add="+") root.bind_all("", self._focusIn, add="+") root.bind_all("", self._focusOut, add="+") root.bind_all("", 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("", self._activateSearchResult) self.searchEntry.bind("", 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("", self._clickSearchResult) self.searchList.bind("", 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("", 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("", 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("", lambda e: self._navKey(1)) self.window.bind("", lambda e: self._navKey(-1)) for seq in ("", "", ""): self.window.bind_all(seq, self._onWheel) self.window.bind("", lambda e: self.run()) self.window.bind("", lambda e: self.run()) self.window.bind("", lambda e: self.run()) self.window.bind("", lambda e: self._focusTarget()) self.window.bind("", lambda e: self._focusSearch()) self.window.bind("", self._escapeAction) self.window.bind("", lambda e: self.saveConfigDialog()) self.window.bind("", 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, "", selectEntry) self.window.bind_class(cls, "", selectEntry) for seq in ("", ""): 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("", 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 / 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", "", lambda e: self._runButtonAction()) c.tag_bind("runbtn", "", lambda e: self._hoverRun(True)) c.tag_bind("runbtn", "", 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("", lambda e, n=name: self._selectPane(n)) w.bind("", lambda e, n=name: self._navHover(n, True)) w.bind("", 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("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) window_id = canvas.create_window((0, 0), window=inner, anchor="nw") canvas.bind("", 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("", 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()