From 0430f780ab2ce827b3d0fca126c9c36a6f3d7fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 24 Jun 2026 10:57:52 +0200 Subject: [PATCH 1/2] Improving --gui and --tui --- data/txt/sha256sums.txt | 8 +- lib/core/settings.py | 2 +- lib/parse/cmdline.py | 6 +- lib/utils/gui.py | 1326 ++++++++++++++++++++++++++++----------- lib/utils/tui.py | 331 +++++++--- 5 files changed, 1198 insertions(+), 475 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 772e791c8b5..b1e6dddf229 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -c7a6dd94cf738716cc48f1daacdd402ddb0e78a6c9260233e319cde4f9054a60 lib/core/settings.py +a6e15ece62113241870feacc9cda691c64be9b849ce2df169b35ee695a517165 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -200,7 +200,7 @@ b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unesc 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -386065c4c40e07a10875d0b73b4ca2fb682c598e8d52b41d0b6b08d5c2c7b3c1 lib/parse/cmdline.py +6060d2d11fab39796b87ace30a872302f365dea3b14d24670915fdb9edc86011 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -249,7 +249,7 @@ da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/craw a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dialect.py 51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py -853c3595e1d2efc54b8bfb6ab12c55d1efc1603be266978e3a7d96d553d91a52 lib/utils/gui.py +417029b70afe672f3121746a7909887aa996766c92547b4566c50343fff76131 lib/utils/gui.py 972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py 0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py 71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py @@ -264,7 +264,7 @@ de4be7e291db0962cd59f9c04b3f7259f846e315df1fd9b323954f89fae0b2db lib/utils/sear 8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py 2760c4b82382e501f16bb98edec9531f46e5b286fbf004b346545b9b62f84824 lib/utils/sqlalchemy.py f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py -f821dc39a75ea48dccfa758788de15d38b9ca6a780a98f59935fb6610f75508c lib/utils/tui.py +f19a6761284e689fca7d2e07120193f7b9c4f9c506ecaf87e82c2e97cca4db63 lib/utils/tui.py e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py b3c5109394f6c3cdd73a524a737b36cca7ecc56619f2a5f801eb1e7f1bfdb78b lib/utils/wafbypass.py 1b439fc59fd202c21c74978ed9f36d1c309533226c77907eae159461525f9fef lib/utils/xrange.py diff --git a/lib/core/settings.py b/lib/core/settings.py index c25f9e0f6a3..2c3537f7ebf 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.157" +VERSION = "1.10.6.158" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index e0b2b5793bf..d35c61b958f 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -443,7 +443,7 @@ def cmdLineParser(argv=None): help="Load second-order HTTP request from file") # Fingerprint options - fingerprint = parser.add_argument_group("Fingerprint") + fingerprint = parser.add_argument_group("Fingerprint", "These options can be used to perform a back-end database management system version fingerprint") fingerprint.add_argument("-f", "--fingerprint", dest="extensiveFp", action="store_true", help="Perform an extensive DBMS version fingerprint") @@ -789,7 +789,7 @@ def cmdLineParser(argv=None): help="Disable hash analysis on table dumps") miscellaneous.add_argument("--gui", dest="gui", action="store_true", - help="Experimental Tkinter GUI") + help="Graphical user interface (Tkinter)") miscellaneous.add_argument("--list-tampers", dest="listTampers", action="store_true", help="Display list of available tamper scripts") @@ -816,7 +816,7 @@ def cmdLineParser(argv=None): help="Local directory for storing temporary files") miscellaneous.add_argument("--tui", dest="tui", action="store_true", - help="Experimental ncurses TUI") + help="Textual user interface (ncurses)") miscellaneous.add_argument("--unstable", dest="unstable", action="store_true", help="Adjust options for unstable connections") diff --git a/lib/utils/gui.py b/lib/utils/gui.py index 3e3500bc507..ed6dd13b329 100644 --- a/lib/utils/gui.py +++ b/lib/utils/gui.py @@ -6,8 +6,6 @@ """ import os -import re -import socket import subprocess import sys import tempfile @@ -30,397 +28,989 @@ from lib.core.settings import WIKI_PAGE from thirdparty.six.moves import queue as _queue -alive = None -line = "" -process = None -queue = None +# Classic Windows (NT/9x) palette: silver 3D face, navy title/selection, white sunken fields, +# black text, and saturated VGA-style accents for the icons (presentation only) +PALETTE = { + "base": "#c0c0c0", # window / control face (silver) + "mantle": "#c0c0c0", # bars (classic is uniform gray, separated by bevels) + "crust": "#ffffff", # console / edit background + "surface0": "#ffffff", # field (edit) background + "surface1": "#808080", # 3D shadow + "surface2": "#dfdfdf", # 3D light (soft) + "light": "#ffffff", # 3D highlight + "dark": "#404040", # 3D dark shadow + "text": "#000000", + "subtext": "#000000", + "overlay": "#404040", + "title2": "#1084d0", # active title-bar gradient end + "blue": "#000080", # navy: title, selection, accents + "sapphire": "#0050b0", + "sky": "#0070c0", + "green": "#008000", + "teal": "#008080", + "red": "#c00000", + "maroon": "#800000", + "mauve": "#9000a8", + "pink": "#c000b0", + "peach": "#c06000", + "yellow": "#c08000", + "lavender": "#4858c0", + "flamingo": "#c04070", + "gold": "#e0a800", +} + +# 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." + +# --- 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 "") + +class _Tooltip(object): + """Lightweight hover tooltip for a widget""" + + def __init__(self, widget, text, tk, palette): + self._widget = widget + self._text = text + self._tk = tk + self._palette = palette + self._tip = None + widget.bind("", self._show, add="+") + widget.bind("", self._hide, add="+") + widget.bind("", self._hide, add="+") + + def _show(self, event=None): + if self._tip or not self._text: + return + x = self._widget.winfo_rootx() + 18 + y = self._widget.winfo_rooty() + self._widget.winfo_height() + 6 + self._tip = tw = self._tk.Toplevel(self._widget) + tw.wm_overrideredirect(True) + tw.wm_geometry("+%d+%d" % (x, y)) + self._tk.Label(tw, text=self._text, justify="left", background=self._palette["surface0"], + foreground=self._palette["text"], relief="flat", borderwidth=0, + wraplength=460, padx=10, pady=7).pack() + + def _hide(self, event=None): + if self._tip: + self._tip.destroy() + 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 Tk variable) + self.vars = {} # dest -> shared Tk variable (one per option, bound to every widget for it) + self.optionByDest = {} + for group in _parserGroups(parser): + for option in _groupOptions(group): + if _optDest(option): + self.optionByDest[_optDest(option)] = option + + self.panes = {} # name -> outer frame + self.navItems = {} # name -> (row frame, accent strip, icon canvas, label) + 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.paneOrder = [] # nav order, for Up/Down navigation + self.currentPane = None + self.process = None + self.alive = False + self.queue = None + + try: + self.window = tk.Tk() + except Exception as ex: + raise SqlmapSystemException("unable to create GUI window ('%s')" % getSafeExString(ex)) + + self._initFonts() + self._initStyle() + self._buildLayout() + + 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, light, light2, shadow, dark = p["base"], p["light"], p["surface2"], p["surface1"], p["dark"] + navy, white, black, field = p["blue"], "#ffffff", p["text"], p["surface0"] + style = self.ttk.Style() + if "clam" in style.theme_names(): + style.theme_use("clam") + + style.configure(".", background=face, foreground=black, fieldbackground=field, + bordercolor=shadow, lightcolor=light, darkcolor=shadow, + troughcolor=face, focuscolor=face, insertcolor=black, font=self.fonts["body"]) + + for name in ("TFrame", "Bar.TFrame", "Nav.TFrame", "Card.TFrame"): + style.configure(name, background=face) + + style.configure("TLabel", background=face, foreground=black) + style.configure("Title.TLabel", background=navy, foreground=white, font=self.fonts["title"]) + style.configure("Subtitle.TLabel", background=navy, foreground=white, font=self.fonts["subtitle"]) + style.configure("Hint.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Field.TLabel", background=face, foreground=black) + style.configure("Desc.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Pane.TLabel", background=face, foreground=navy, font=self.fonts["title"]) + style.configure("Stat.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Prompt.TLabel", background=field, foreground=black, font=self.fonts["mono"]) + + # classic raised 3D push button + style.configure("TButton", background=face, foreground=black, relief="raised", borderwidth=2, + lightcolor=light, darkcolor=dark, bordercolor=shadow, focuscolor=black, padding=(12, 4)) + style.map("TButton", background=[("active", face)], relief=[("pressed", "sunken")]) + + # sunken white edit fields + for name in ("TEntry", "Target.TEntry"): + style.configure(name, fieldbackground=field, foreground=black, relief="sunken", borderwidth=2, + bordercolor=shadow, lightcolor=shadow, darkcolor=light, insertcolor=black, padding=4) + + style.configure("TCheckbutton", background=face, foreground=black, focuscolor=face, padding=2, + indicatorbackground=field, indicatorforeground=black, indicatorrelief="sunken", indicatorborderwidth=2, + bordercolor=shadow, lightcolor=shadow, darkcolor=light) + style.map("TCheckbutton", background=[("active", face)], indicatorbackground=[("active", field), ("selected", field)]) + + style.configure("TCombobox", fieldbackground=field, background=face, foreground=black, arrowcolor=black, + relief="sunken", borderwidth=2, bordercolor=shadow, lightcolor=shadow, darkcolor=light, padding=3) + + # classic chunky scrollbar (raised gray thumb, light trough) + style.configure("Vertical.TScrollbar", background=face, troughcolor=light2, bordercolor=shadow, + lightcolor=light, darkcolor=dark, arrowcolor=black, relief="raised", width=17) + style.map("Vertical.TScrollbar", background=[("active", face)]) + + self.window.configure(background=face) + + # --- layout --------------------------------------------------------- + + def _buildLayout(self): + tk = self.tk + self.window.title("sqlmap") + self.window.minsize(960, 680) + self._buildMenu() + self._buildHeader() + + target = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 12, 20, 14)) + target.pack(fill=tk.X) + labelRow = self.ttk.Frame(target, style="Bar.TFrame") + labelRow.pack(fill=tk.X, pady=(0, 4)) + self.ttk.Label(labelRow, text="TARGET URL", style="Hint.TLabel").pack(side=tk.LEFT) + self.ttk.Label(labelRow, text=" e.g. %s" % TARGET_PLACEHOLDER, style="Stat.TLabel").pack(side=tk.LEFT) + urlVar = self._destVar("url", False) + self.targetEntry = self.ttk.Entry(target, style="Target.TEntry", textvariable=urlVar) + self.targetEntry.pack(fill=tk.X, ipady=2) + self.widgets["url"] = ("string", urlVar) + + body = self.ttk.Frame(self.window, style="TFrame") + body.pack(expand=True, fill=tk.BOTH) + + navHolder = self.ttk.Frame(body, style="Nav.TFrame", width=202) + navHolder.pack(side=tk.LEFT, fill=tk.Y) + navHolder.pack_propagate(False) + self.navCanvas = tk.Canvas(navHolder, background=PALETTE["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=PALETTE["surface1"], 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=(20, 8)) + cmdBar.pack(fill=tk.X) + self.ttk.Label(cmdBar, text="Command:", style="Hint.TLabel").pack(side=tk.LEFT, padx=(0, 8)) + self.command = tk.StringVar(value="sqlmap.py") + cmdEntry = tk.Entry(cmdBar, textvariable=self.command, font=self.fonts["mono"], + bg="#ffffff", fg=PALETTE["blue"], readonlybackground="#ffffff", + disabledforeground=PALETTE["blue"], relief="sunken", borderwidth=2, + highlightthickness=0, state="readonly") + cmdEntry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + hintBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 9)) + hintBar.pack(fill=tk.X) + 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._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._enableSelectAll() + self._tickStats() + self._prebuildPanes() + self._center(self.window, 1000, 720) + + def _prebuildPanes(self): + # Tk isn't thread-safe, so widgets must be built on the main thread; instead of blocking, + # build the not-yet-visited panes one per idle tick so they are ready (instant) by the time + # the user navigates to them, while the UI stays responsive (on-demand build is the fallback) + pending = [_ for _ in self.paneOrder if _ not in self.built] + + def step(): + while pending and pending[0] in self.built: + pending.pop(0) + if not pending: + return + name = pending.pop(0) + try: + self.builders[name](self.inners[name]) + self.built.add(name) + except Exception: + pass + if pending: + self.window.after(30, step) + + self.window.after(250, step) + + 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 runGui(parser): - try: - from thirdparty.six.moves import tkinter as _tkinter - from thirdparty.six.moves import tkinter_scrolledtext as _tkinter_scrolledtext - from thirdparty.six.moves import tkinter_ttk as _tkinter_ttk - from thirdparty.six.moves import tkinter_messagebox as _tkinter_messagebox - except ImportError as ex: - raise SqlmapMissingDependence("missing dependence ('%s')" % getSafeExString(ex)) + def selectText(event): + try: + event.widget.tag_add("sel", "1.0", "end-1c") + except Exception: + pass + return "break" - # Reference: https://www.reddit.com/r/learnpython/comments/985umy/limit_user_input_to_only_int_with_tkinter/e4dj9k9?utm_source=share&utm_medium=web2x - class ConstrainedEntry(_tkinter.Entry): - def __init__(self, master=None, **kwargs): - self.var = _tkinter.StringVar() - self.regex = kwargs["regex"] - del kwargs["regex"] - _tkinter.Entry.__init__(self, master, textvariable=self.var, **kwargs) - self.old_value = '' - self.var.trace('w', self.check) - self.get, self.set = self.var.get, self.var.set - - def check(self, *args): - if re.search(self.regex, self.get()): - self.old_value = self.get() + 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 + menubar = self.tk.Menu(self.window, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"], borderwidth=0) + filemenu = self.tk.Menu(menubar, tearoff=0, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"]) + filemenu.add_command(label="Load configuration...", command=self.loadConfig) + filemenu.add_command(label="Save configuration...", command=self.saveConfigDialog) + filemenu.add_separator() + filemenu.add_command(label="Exit", command=self.window.quit) + menubar.add_cascade(label="File", menu=filemenu) + menubar.add_command(label="Run", command=self.run) + helpmenu = self.tk.Menu(menubar, tearoff=0, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"]) + 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("", lambda e: 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): + p = PALETTE + c = self.header + c.delete("all") + width = c.winfo_width() + height = 76 + steps = max(1, width // 4) + for i in range(steps): + c.create_rectangle(i * width / steps, 0, (i + 1) * width / steps + 1, height, + outline="", fill=self._interp(p["blue"], p["title2"], i / float(steps))) + c.create_text(24, 27, text="sqlmap", anchor="w", fill="#ffffff", font=self.fonts["title"]) + c.create_text(122, 31, text=VERSION_STRING.replace("sqlmap/", "v"), anchor="w", fill="#c7d8ef", font=self.fonts["subtitle"]) + c.create_text(24, 54, text="automatic SQL injection and database takeover tool", anchor="w", fill="#dfe8f6", font=self.fonts["small"]) + self._drawRunButton(width, height) + + def _drawRunButton(self, width, height): + p = PALETTE + c = self.header + bw, bh = 116, 34 + x0 = width - bw - 22 + y0 = (height - bh) // 2 + x1, y1 = x0 + bw, y0 + bh + c.create_rectangle(x0, y0, x1, y1, fill=p["base"], outline="", tags=("runbtn", "runpill")) + # classic raised 3D bevel (white top/left, dark bottom/right) + c.create_line(x0, y0, x1, y0, fill="#ffffff", tags="runbtn") + c.create_line(x0, y0, x0, y1, fill="#ffffff", tags="runbtn") + c.create_line(x0, y1, x1, y1, fill=p["dark"], tags="runbtn") + c.create_line(x1, y0, x1, y1 + 1, fill=p["dark"], tags="runbtn") + c.create_line(x0 + 1, y1 - 1, x1 - 1, y1 - 1, fill=p["surface1"], tags="runbtn") + c.create_line(x1 - 1, y0 + 1, x1 - 1, y1 - 1, fill=p["surface1"], tags="runbtn") + cy = (y0 + y1) // 2 + tx = x0 + 24 + c.create_polygon(tx, cy - 6, tx, cy + 6, tx + 10, cy, fill=p["blue"], outline="", tags=("runbtn", "runico")) + c.create_text((x0 + x1) // 2 + 8, cy, text="Run", fill=p["text"], font=self.fonts["bodyBold"], tags=("runbtn", "runico")) + c.tag_bind("runbtn", "", lambda e: self.run()) + c.tag_bind("runbtn", "", lambda e: self._hoverRun(True)) + c.tag_bind("runbtn", "", lambda e: self._hoverRun(False)) + + def _hoverRun(self, on): + self._runHover = on + self.header.itemconfigure("runpill", fill="#ccccc6" if on else PALETTE["base"]) + try: + 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)) + lab = tk.Label(row, text=navText, background=p["mantle"], foreground=p["subtext"], + 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): + 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) + self.paneOrder.append(name) + + outer = self.ttk.Frame(self.content, style="Card.TFrame") + canvas = tk.Canvas(outer, background=p["base"], 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 name == self.currentPane: + return + bg = PALETTE["surface2"] if entering else PALETTE["mantle"] + row, strip, icon, lab = self.navItems[name] + for w in (row, strip, icon, lab): + 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): + if name not in self.built: # lazy: populate the pane on first visit + self.builders[name](self.inners[name]) + self.built.add(name) + if self.currentPane == name: + return + p = PALETTE + if self.currentPane: + self.panes[self.currentPane].pack_forget() + row, strip, icon, lab = self.navItems[self.currentPane] + for w in (row, strip, icon): + w.configure(background=p["mantle"]) + lab.configure(background=p["mantle"], foreground=p["text"], font=self.fonts["nav"]) + self._drawIcon(icon, self.currentPane, self._iconColor(self.currentPane)) + self.panes[name].pack(expand=True, fill=self.tk.BOTH) + row, strip, icon, lab = self.navItems[name] + for w in (row, strip, icon): + w.configure(background=p["blue"]) + lab.configure(background=p["blue"], foreground="#ffffff", font=self.fonts["bodyBold"]) + self._drawIcon(icon, name, "#ffffff") + self.currentPane = name + self._ensureNavVisible(name) + + 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] + self.nav.update_idletasks() + 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 to whichever scroll region the pointer is over (sidebar or content) + delta = 1 if getattr(event, "num", None) == 5 or getattr(event, "delta", 0) < 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 None and self.currentPane: + target = self.canvases.get(self.currentPane) + if target is not None: + target.yview_scroll(delta, "units") + return "break" + + def _buildQuickStartPane(self): + name = "Quick start" + self._addPane(name, name) + + def build(inner): + self.ttk.Label(inner, text="Quick start", style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") + self.ttk.Label(inner, text="The options people reach for most. Set the target above, tick what you want, then Run.", + style="Desc.TLabel", wraplength=640, justify="left").grid(row=1, column=0, columnspan=2, sticky="w", pady=(2, 14)) + row = 2 + for dest in QUICK_START_DESTS: + option = self.optionByDest.get(dest) + if option is not None: + row = self._buildFieldRow(inner, option, row) + inner.columnconfigure(1, weight=1) + + self.builders[name] = build + + def _buildGroupPane(self, group): + title = _groupTitle(group) + self._addPane(title, NAV_ALIASES.get(title, title)) + + def build(inner, group=group, title=title): + self.ttk.Label(inner, text=title, style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") + row = 1 + description = _groupDescription(group) + if description: + self.ttk.Label(inner, text=description, style="Desc.TLabel", wraplength=640, justify="left").grid( + row=row, column=0, columnspan=2, sticky="w", pady=(2, 14)) + row += 1 + for option in _groupOptions(group): + row = self._buildFieldRow(inner, option, row) + inner.columnconfigure(1, weight=1) + + self.builders[title] = build + + def _destVar(self, dest, is_bool): + # one shared variable per option, so every widget that edits it (Quick start pane, + # the proper group pane, the target bar) reflects into the same value both ways + if dest not in self.vars: + self.vars[dest] = self.tk.IntVar() if is_bool else self.tk.StringVar() + return self.vars[dest] + + def _buildFieldRow(self, parent, option, row): + p = PALETTE + tk = self.tk + label = _optionLabel(option) + helptext = _optHelp(option) + dest = _optDest(option) + is_bool = not _optTakesValue(option) + firstSeen = dest not in self.vars + + def bindHint(widget): + widget.bind("", lambda e: self.hint.set(helptext), add="+") + widget.bind("", lambda e: self.hint.set(helptext), add="+") + widget.bind("", lambda e: self.hint.set(HINT_DEFAULT), add="+") + widget.bind("", lambda e: self.hint.set(HINT_DEFAULT), add="+") + + if is_bool: + var = self._destVar(dest, True) + chk = self.ttk.Checkbutton(parent, text=label, variable=var, takefocus=True) + chk.grid(row=row, column=0, columnspan=2, sticky="w", pady=5) + _Tooltip(chk, helptext, tk, p) + bindHint(chk) + if firstSeen: + self.widgets[dest] = ("bool", var) + else: + otype = _optValueType(option) + var = self._destVar(dest, False) + if firstSeen: + default = defaults.get(dest) + if default not in (None, False): + var.set(default) + self.widgets[dest] = (otype, var) + lab = self.ttk.Label(parent, text=label, style="Field.TLabel") + lab.grid(row=row, column=0, sticky="w", padx=(0, 18), pady=6) + _Tooltip(lab, helptext, tk, p) + bindHint(lab) + choices = _optChoices(option) + if choices: + widget = self.ttk.Combobox(parent, values=list(choices), state="readonly", textvariable=var) else: - self.set(self.old_value) - - try: - window = _tkinter.Tk() - except Exception as ex: - errMsg = "unable to create GUI window ('%s')" % getSafeExString(ex) - raise SqlmapSystemException(errMsg) - - window.title("sqlmap - Tkinter GUI") - - # Set theme and colors - bg_color = "#f5f5f5" - fg_color = "#333333" - accent_color = "#2c7fb8" - window.configure(background=bg_color) - - # Configure styles - style = _tkinter_ttk.Style() - - # Try to use a more modern theme if available - available_themes = style.theme_names() - if 'clam' in available_themes: - style.theme_use('clam') - elif 'alt' in available_themes: - style.theme_use('alt') - - # Configure notebook style - style.configure("TNotebook", background=bg_color) - style.configure("TNotebook.Tab", - padding=[10, 4], - background="#e1e1e1", - font=('Helvetica', 9)) - style.map("TNotebook.Tab", - background=[("selected", accent_color), ("active", "#7fcdbb")], - foreground=[("selected", "white"), ("active", "white")]) - - # Configure button style - style.configure("TButton", - padding=4, - relief="flat", - background=accent_color, - foreground="white", - font=('Helvetica', 9)) - style.map("TButton", - background=[('active', '#41b6c4')]) - - # Reference: https://stackoverflow.com/a/10018670 - def center(window): + 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) + _Tooltip(widget, helptext, tk, p) + bindHint(widget) + return row + 1 + + def _constrain(self, entry, otype): + check = (lambda s: s == "" or s.replace(".", "", 1).isdigit()) if otype == "float" else (lambda s: s == "" or s.isdigit()) + vcmd = (self.window.register(lambda proposed: bool(check(proposed))), "%P") + entry.configure(validate="key", validatecommand=vcmd) + + # --- helpers -------------------------------------------------------- + + def _center(self, window, width=None, height=None): window.update_idletasks() - width = window.winfo_width() - frm_width = window.winfo_rootx() - window.winfo_x() - win_width = width + 2 * frm_width - height = window.winfo_height() - titlebar_height = window.winfo_rooty() - window.winfo_y() - win_height = height + titlebar_height + frm_width - x = window.winfo_screenwidth() // 2 - win_width // 2 - y = window.winfo_screenheight() // 2 - win_height // 2 - window.geometry('{}x{}+{}+{}'.format(width, height, x, y)) - window.deiconify() - - def onKeyPress(event): - global line - global queue - - if process: - if event.char == '\b': - line = line[:-1] - else: - line += event.char - - def onReturnPress(event): - global line - global queue - - if process: + 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 _updateStats(self): + count = 0 + for dest, (otype, var) in self.widgets.items(): try: - process.stdin.write(("%s\n" % line.strip()).encode()) - process.stdin.flush() - except socket.error: - line = "" - event.widget.master.master.destroy() - return "break" - except: - return - - event.widget.insert(_tkinter.END, "\n") - - return "break" - - def run(): - global alive - global process - global queue - + if otype == "bool": + if var.get(): + count += 1 + else: + raw = var.get() + if raw not in (None, "") and str(raw) != str(defaults.get(dest, "")): + count += 1 + except Exception: + pass + self.stat.set("%d option%s set" % (count, "" if count == 1 else "s")) + + def _buildCommandString(self): + parts = ["sqlmap.py"] + for dest, (otype, var) in self.widgets.items(): + option = self.optionByDest.get(dest) + if option is None: + continue + strings = _optStrings(option) + if not strings: + continue + flag = strings[0] + try: + if otype == "bool": + if var.get(): + parts.append(flag) + else: + raw = var.get() + if raw not in (None, "") and str(raw) != str(defaults.get(dest, "")): + value = str(raw) + if " " in value or '"' in value: + value = '"%s"' % value.replace('"', '\\"') + parts.append("%s %s" % (flag, value)) + except Exception: + pass + return " ".join(parts) + + def _tickStats(self): + self._updateStats() + self.command.set(self._buildCommandString()) + self.window.after(1200, self._tickStats) + + def _collectConfig(self): config = {} - - for key in window._widgets: - dest, widget_type = key - widget = window._widgets[key] - - if hasattr(widget, "get") and not widget.get(): + 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 - elif widget_type == "string": - value = widget.get() - elif widget_type == "float": - value = float(widget.get()) - elif widget_type == "int": - value = int(widget.get()) - else: - value = bool(widget.var.get()) - config[dest] = value - - for option in parser.option_list: - # Only set default if not already set by the user - if option.dest not in config or config[option.dest] is None: - config[option.dest] = defaults.get(option.dest, None) - + 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(1 if value else 0) + else: + var.set("" if value in (None, False) else value) + except Exception: + pass + + # --- 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.read(path) + count = 0 + for section in parser.sections(): + for name, value in parser.items(section): + if name in self.widgets: + if self.widgets[name][0] == "bool": + self._setWidgetValue(name, str(value).lower() in ("1", "true", "yes", "on")) + else: + self._setWidgetValue(name, value) + count += 1 + self.hint.set("Loaded %d options from %s" % (count, os.path.basename(path))) + except Exception as ex: + 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): + config = self._collectConfig() handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True) os.close(handle) - saveConfig(config, configFile) - def enqueue(stream, queue): - global alive + self.alive = True + self.process = 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=1, close_fds=not IS_WIN) + self.queue = _queue.Queue() + def enqueue(stream, queue): for line in iter(stream.readline, b''): queue.put(line) - - alive = False + self.alive = False stream.close() - alive = True - - process = 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=1, close_fds=not IS_WIN) - - # Reference: https://stackoverflow.com/a/4896288 - queue = _queue.Queue() - thread = threading.Thread(target=enqueue, args=(process.stdout, queue)) + thread = threading.Thread(target=enqueue, args=(self.process.stdout, self.queue)) thread.daemon = True thread.start() - - top = _tkinter.Toplevel() - top.title("Console") - top.configure(background=bg_color) - - # Create a frame for the console - console_frame = _tkinter.Frame(top, bg=bg_color) - console_frame.pack(fill=_tkinter.BOTH, expand=True, padx=10, pady=10) - - # Reference: https://stackoverflow.com/a/13833338 - text = _tkinter_scrolledtext.ScrolledText(console_frame, undo=True, wrap=_tkinter.WORD, - bg="#2c3e50", fg="#ecf0f1", - insertbackground="white", - font=('Consolas', 10)) - text.bind("", onKeyPress) - text.bind("", onReturnPress) - text.pack(fill=_tkinter.BOTH, expand=True) + self._openConsole() + + def _openConsole(self): + p = PALETTE + tk = self.tk + top = tk.Toplevel(self.window) + top.title("sqlmap - console") + top.configure(background=p["crust"]) + frame = self.ttk.Frame(top, style="Card.TFrame", padding=10) + frame.configure(style="Card.TFrame") + frame.pack(fill=tk.BOTH, expand=True) + + text = self.scrolledtext.ScrolledText(frame, wrap=tk.WORD, bg=p["crust"], fg=p["text"], + insertbackground=p["blue"], relief="flat", borderwidth=0, + font=self.fonts["mono"], padx=12, pady=10) + text.pack(fill=tk.BOTH, expand=True) text.focus() + lineBuffer = {"value": ""} + + def onKey(event): + if self.process: + if event.char == "\b": + lineBuffer["value"] = lineBuffer["value"][:-1] + elif event.char: + lineBuffer["value"] += event.char + + def onReturn(event): + if self.process: + try: + self.process.stdin.write(("%s\n" % lineBuffer["value"].strip()).encode()) + self.process.stdin.flush() + except Exception: + pass + lineBuffer["value"] = "" + text.insert(tk.END, "\n") + return "break" - center(top) + text.bind("", onKey) + text.bind("", onReturn) - while True: - line = "" + def pump(): + drained = False try: - line = queue.get(timeout=.1) - text.insert(_tkinter.END, line) + while True: + line = self.queue.get_nowait() + text.insert(tk.END, line.decode("utf-8", errors="replace") if isinstance(line, bytes) else line) + drained = True except _queue.Empty: - text.see(_tkinter.END) - text.update_idletasks() - - if not alive: - break - - # Create a menu bar - menubar = _tkinter.Menu(window, bg=bg_color, fg=fg_color) - - filemenu = _tkinter.Menu(menubar, tearoff=0, bg=bg_color, fg=fg_color) - filemenu.add_command(label="Open", state=_tkinter.DISABLED) - filemenu.add_command(label="Save", state=_tkinter.DISABLED) - filemenu.add_separator() - filemenu.add_command(label="Exit", command=window.quit) - menubar.add_cascade(label="File", menu=filemenu) - - menubar.add_command(label="Run", command=run) - - helpmenu = _tkinter.Menu(menubar, tearoff=0, bg=bg_color, fg=fg_color) - helpmenu.add_command(label="Official site", command=lambda: webbrowser.open(SITE)) - helpmenu.add_command(label="Github pages", command=lambda: webbrowser.open(GIT_PAGE)) - helpmenu.add_command(label="Wiki pages", 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: _tkinter_messagebox.showinfo("About", "%s\n\n (%s)" % (VERSION_STRING, DEV_EMAIL_ADDRESS))) - menubar.add_cascade(label="Help", menu=helpmenu) - - window.config(menu=menubar, bg=bg_color) - window._widgets = {} - - # Create header frame - header_frame = _tkinter.Frame(window, bg=bg_color, height=60) - header_frame.pack(fill=_tkinter.X, pady=(0, 5)) - header_frame.pack_propagate(0) - - # Add header label - title_label = _tkinter.Label(header_frame, text="Configuration", - font=('Helvetica', 14), - fg=accent_color, bg=bg_color) - title_label.pack(side=_tkinter.LEFT, padx=15) - - # Add run button in header - run_button = _tkinter_ttk.Button(header_frame, text="Run", command=run, width=12) - run_button.pack(side=_tkinter.RIGHT, padx=15) - - # Create notebook - notebook = _tkinter_ttk.Notebook(window) - notebook.pack(expand=1, fill="both", padx=5, pady=(0, 5)) - - # Store tab information for background loading - tab_frames = {} - tab_canvases = {} - tab_scrollable_frames = {} - tab_groups = {} - - # Create empty tabs with scrollable areas first (fast) - for group in parser.option_groups: - # Create a frame with scrollbar for the tab - tab_frame = _tkinter.Frame(notebook, bg=bg_color) - tab_frames[group.title] = tab_frame - - # Create a canvas with scrollbar - canvas = _tkinter.Canvas(tab_frame, bg=bg_color, highlightthickness=0) - scrollbar = _tkinter_ttk.Scrollbar(tab_frame, orient="vertical", command=canvas.yview) - scrollable_frame = _tkinter.Frame(canvas, bg=bg_color) - - # Store references - tab_canvases[group.title] = canvas - tab_scrollable_frames[group.title] = scrollable_frame - tab_groups[group.title] = group - - # Configure the canvas scrolling - scrollable_frame.bind( - "", - lambda e, canvas=canvas: canvas.configure(scrollregion=canvas.bbox("all")) - ) - - canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") - canvas.configure(yscrollcommand=scrollbar.set) - - # Pack the canvas and scrollbar - canvas.pack(side="left", fill="both", expand=True) - scrollbar.pack(side="right", fill="y") - - # Add the tab to the notebook - notebook.add(tab_frame, text=group.title) - - # Add a loading indicator - loading_label = _tkinter.Label(scrollable_frame, text="Loading options...", - font=('Helvetica', 12), - fg=accent_color, bg=bg_color) - loading_label.pack(expand=True) - - # Function to populate a tab in the background - def populate_tab(tab_name): - group = tab_groups[tab_name] - scrollable_frame = tab_scrollable_frames[tab_name] - canvas = tab_canvases[tab_name] - - # Remove loading indicator - for child in scrollable_frame.winfo_children(): - child.destroy() - - # Add content to the scrollable frame - row = 0 - - if group.get_description(): - desc_label = _tkinter.Label(scrollable_frame, text=group.get_description(), - wraplength=600, justify="left", - font=('Helvetica', 9), - fg="#555555", bg=bg_color) - desc_label.grid(row=row, column=0, columnspan=3, sticky="w", padx=10, pady=(10, 5)) - row += 1 - - for option in group.option_list: - # Option label - option_label = _tkinter.Label(scrollable_frame, - text=parser.formatter._format_option_strings(option) + ":", - font=('Helvetica', 9), - fg=fg_color, bg=bg_color, - anchor="w") - option_label.grid(row=row, column=0, sticky="w", padx=10, pady=2) - - # Input widget - if option.type == "string": - widget = _tkinter.Entry(scrollable_frame, font=('Helvetica', 9), - relief="sunken", bd=1, width=20) - widget.grid(row=row, column=1, sticky="w", padx=5, pady=2) - elif option.type == "float": - widget = ConstrainedEntry(scrollable_frame, regex=r"\A\d*\.?\d*\Z", - font=('Helvetica', 9), - relief="sunken", bd=1, width=10) - widget.grid(row=row, column=1, sticky="w", padx=5, pady=2) - elif option.type == "int": - widget = ConstrainedEntry(scrollable_frame, regex=r"\A\d*\Z", - font=('Helvetica', 9), - relief="sunken", bd=1, width=10) - widget.grid(row=row, column=1, sticky="w", padx=5, pady=2) + pass + if drained: + text.see(tk.END) + if self.alive or not self.queue.empty(): + top.after(80, pump) else: - var = _tkinter.IntVar() - widget = _tkinter.Checkbutton(scrollable_frame, variable=var, - bg=bg_color, activebackground=bg_color) - widget.var = var - widget.grid(row=row, column=1, sticky="w", padx=5, pady=2) - - # Help text (truncated to improve performance) - help_text = option.help - if len(help_text) > 100: - help_text = help_text[:100] + "..." - - help_label = _tkinter.Label(scrollable_frame, text=help_text, - font=('Helvetica', 8), - fg="#666666", bg=bg_color, - wraplength=400, justify="left") - help_label.grid(row=row, column=2, sticky="w", padx=5, pady=2) - - # Store widget reference - window._widgets[(option.dest, option.type)] = widget - - # Set default value - default = defaults.get(option.dest) - if default: - if hasattr(widget, "insert"): - widget.insert(0, default) - elif hasattr(widget, "var"): - widget.var.set(1 if default else 0) - - row += 1 - - # Add some padding at the bottom - _tkinter.Label(scrollable_frame, bg=bg_color, height=1).grid(row=row, column=0) - - # Update the scroll region after adding all widgets - canvas.update_idletasks() - canvas.configure(scrollregion=canvas.bbox("all")) - - # Update the UI to show the tab is fully loaded - window.update_idletasks() + text.insert(tk.END, "\n--- process finished ---\n") + text.see(tk.END) - # Function to populate tabs in the background - def populate_tabs_background(): - for tab_name in tab_groups.keys(): - # Schedule each tab to be populated with a small delay between them - window.after(100, lambda name=tab_name: populate_tab(name)) + self._center(top, 900, 580) + top.after(80, pump) - # Start populating tabs in the background after a short delay - window.after(500, populate_tabs_background) - - # Set minimum window size - window.update() - window.minsize(800, 500) - - # Center the window on screen - center(window) +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)) - # Start the GUI - window.mainloop() + app = SqlmapGui(parser, _tkinter, _ttk, _scrolledtext, _messagebox, _filedialog, _font) + app.window.mainloop() diff --git a/lib/utils/tui.py b/lib/utils/tui.py index d785e5f7673..476ecceccff 100644 --- a/lib/utils/tui.py +++ b/lib/utils/tui.py @@ -25,6 +25,75 @@ from lib.core.settings import IS_WIN from thirdparty.six.moves import configparser as _configparser +# Options surfaced on the curated "Quick start" tab (by destination), in display order +QUICK_START_DESTS = ( + "url", "data", "cookie", "dbms", "level", "risk", "technique", + "getCurrentUser", "getCurrentDb", "getBanner", "isDba", + "getDbs", "getTables", "getColumns", "getPasswordHashes", "dumpTable", + "batch", "threads", "proxy", "tor", +) + +# Short tab labels so the (sometimes verbose) option-group titles fit the top bar +TAB_ALIASES = { + "Optimization": "Optimize", + "Enumeration": "Enumerate", + "Brute force": "Brute", + "User-defined function injection": "UDF", + "File system access": "Files", + "Operating system access": "OS", + "Windows registry access": "Registry", + "Miscellaneous": "Misc", +} + +# --- 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"): + 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 _optTakesValue(option): + if hasattr(option, "takes_value"): + try: + return option.takes_value() + except Exception: + pass + return getattr(option, "nargs", 1) != 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" + class NcursesUI: def __init__(self, stdscr, parser): self.stdscr = stdscr @@ -38,61 +107,110 @@ def __init__(self, stdscr, parser): self.process = None # Initialize colors - curses.start_color() - curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) # Header - curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE) # Active tab - curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE) # Inactive tab - curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Selected field - curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK) # Help text - curses.init_pair(6, curses.COLOR_RED, curses.COLOR_BLACK) # Error/Important - curses.init_pair(7, curses.COLOR_CYAN, curses.COLOR_BLACK) # Label + self._init_colors() # Setup curses - curses.curs_set(1) + curses.curs_set(0) self.stdscr.keypad(1) # Parse option groups self._parse_options() + def _init_colors(self): + """Cohesive palette: a flat 256-color scheme with a graceful 8-color fallback""" + curses.start_color() + try: + curses.use_default_colors() + default_bg = -1 + except curses.error: + default_bg = curses.COLOR_BLACK + + if curses.COLORS >= 256: + accent, accent_fg, sel_bg = 75, 234, 237 + text, muted, green, red = 252, 245, 114, 210 + curses.init_pair(1, accent_fg, accent) # header / footer bar + curses.init_pair(2, accent_fg, accent) # active tab + curses.init_pair(3, muted, 236) # inactive tab + curses.init_pair(4, accent, sel_bg) # selected field row + curses.init_pair(5, muted, default_bg) # help / description + curses.init_pair(6, red, default_bg) # error / important + curses.init_pair(7, text, default_bg) # label / value + curses.init_pair(8, green, default_bg) # value that has been set + curses.init_pair(9, muted, sel_bg) # help text on the highlighted row + else: + curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) + curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_CYAN) + curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLUE) + curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_CYAN) + curses.init_pair(5, curses.COLOR_GREEN, default_bg) + curses.init_pair(6, curses.COLOR_RED, default_bg) + curses.init_pair(7, curses.COLOR_WHITE, default_bg) + curses.init_pair(8, curses.COLOR_GREEN, default_bg) + curses.init_pair(9, curses.COLOR_BLACK, curses.COLOR_CYAN) + def _parse_options(self): """Parse command line options into tabs and fields""" - for group in self.parser.option_groups: + self.all_options = [] + for group in _parserGroups(self.parser): + title = _groupTitle(group) tab_data = { - 'title': group.title, - 'description': group.get_description() if hasattr(group, 'get_description') and group.get_description() else "", + 'title': title, + 'description': _groupDescription(group), 'options': [] } - for option in group.option_list: + for option in _groupOptions(group): + dest = _optDest(option) + if not dest: + continue field_data = { - 'dest': option.dest, + 'dest': dest, 'label': self._format_option_strings(option), - 'help': option.help if option.help else "", - 'type': option.type if hasattr(option, 'type') and option.type else 'bool', + 'help': _optHelp(option), + 'type': _optValueType(option) if _optTakesValue(option) else 'bool', 'value': '', - 'default': defaults.get(option.dest) if defaults.get(option.dest) else None + 'default': defaults.get(dest) if defaults.get(dest) else None } tab_data['options'].append(field_data) - self.fields[(group.title, option.dest)] = field_data + self.fields[(title, dest)] = field_data + self.all_options.append(field_data) self.tabs.append(tab_data) + # curated "Quick start" tab; references the same field objects as the group tabs, + # so a value edited in either place stays in sync + seen = {} + for tab in self.tabs: + for option in tab['options']: + seen.setdefault(option['dest'], option) + quick = { + 'title': 'Quick start', + 'description': "The options people reach for most. Fill these in, then press F2 to run.", + 'options': [seen[dest] for dest in QUICK_START_DESTS if dest in seen], + } + if quick['options']: + self.tabs.insert(0, quick) + def _format_option_strings(self, option): """Format option strings for display""" - parts = [] - if hasattr(option, '_short_opts') and option._short_opts: - parts.extend(option._short_opts) - if hasattr(option, '_long_opts') and option._long_opts: - parts.extend(option._long_opts) - return ', '.join(parts) + return ', '.join(_optStrings(option)) + + def _tab_title(self, tab): + return TAB_ALIASES.get(tab['title'], tab['title']) def _draw_header(self): """Draw the header bar""" height, width = self.stdscr.getmaxyx() - header = " sqlmap - ncurses TUI " self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD) - self.stdscr.addstr(0, 0, header.center(width)) - self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD) + self.stdscr.addstr(0, 0, " " * width) + self.stdscr.addstr(0, 1, "sqlmap") + self.stdscr.attroff(curses.A_BOLD) + right = "F2 Run - F10 Quit " + try: + self.stdscr.addstr(0, max(8, width - len(right)), right) + except: + pass + self.stdscr.attroff(curses.color_pair(1)) def _get_tab_bar_height(self): """Calculate how many rows the tab bar uses""" @@ -101,16 +219,12 @@ def _get_tab_bar_height(self): x = 0 for i, tab in enumerate(self.tabs): - tab_text = " %s " % tab['title'] - - # Check if tab exceeds width, wrap to next line + tab_text = " %s " % self._tab_title(tab) if x + len(tab_text) >= width: y += 1 x = 0 - # Stop if we've used too many lines - if y >= 3: + if y >= 4: break - x += len(tab_text) + 1 return y @@ -122,14 +236,11 @@ def _draw_tabs(self): x = 0 for i, tab in enumerate(self.tabs): - tab_text = " %s " % tab['title'] - - # Check if tab exceeds width, wrap to next line + tab_text = " %s " % self._tab_title(tab) if x + len(tab_text) >= width: y += 1 x = 0 - # Stop if we've used too many lines - if y >= 3: + if y >= 4: break if i == self.current_tab: @@ -152,11 +263,11 @@ def _draw_tabs(self): def _draw_footer(self): """Draw the footer with help text""" height, width = self.stdscr.getmaxyx() - footer = " [Tab] Next | [Arrows] Navigate | [Enter] Edit | [F2] Run | [F3] Export | [F4] Import | [F10] Quit " + footer = " Tab/<-/-> Section Up/Down Field Enter/Space Edit F2 Run F3 Export F4 Import F10 Quit " try: self.stdscr.attron(curses.color_pair(1)) - self.stdscr.addstr(height - 1, 0, footer.ljust(width)) + self.stdscr.addstr(height - 1, 0, footer.ljust(width)[:width - 1]) self.stdscr.attroff(curses.color_pair(1)) except: pass @@ -202,51 +313,58 @@ def _draw_current_tab(self): is_selected = (i == self.current_field) - # Draw label + # full-width highlight bar for the selected row + if is_selected: + try: + self.stdscr.attron(curses.color_pair(4)) + self.stdscr.addstr(y, 0, " " * (width - 1)) + self.stdscr.attroff(curses.color_pair(4)) + except: + pass + + # label label = option['label'][:25].ljust(25) + label_attr = curses.color_pair(4) | curses.A_BOLD if is_selected else curses.color_pair(7) try: - if is_selected: - self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD) - else: - self.stdscr.attron(curses.color_pair(7)) - + self.stdscr.attron(label_attr) self.stdscr.addstr(y, 2, label) - - if is_selected: - self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD) - else: - self.stdscr.attroff(curses.color_pair(7)) + self.stdscr.attroff(label_attr) except: pass - # Draw value - value_str = "" + # value (green once the user has set one, muted "(default)" otherwise) + has_value = option['value'] not in (None, "", False) if option['type'] == 'bool': value = option['value'] if option['value'] is not None else option.get('default') - value_str = "[X]" if value else "[ ]" + value_str = "[x]" if value else "[ ]" + value_attr = curses.color_pair(8) if value else curses.color_pair(5) + elif has_value: + value_str = str(option['value']) + value_attr = curses.color_pair(8) + elif option['default'] not in (None, False): + value_str = "(%s)" % str(option['default']) + value_attr = curses.color_pair(5) else: - value_str = str(option['value']) if option['value'] else "" - if option['default'] and not option['value']: - value_str = "(%s)" % str(option['default']) - - value_str = value_str[:30] + value_str = "" + value_attr = curses.color_pair(5) + if is_selected: + value_attr = curses.color_pair(4) | curses.A_BOLD try: - if is_selected: - self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD) - self.stdscr.addstr(y, 28, value_str) - if is_selected: - self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD) + self.stdscr.attron(value_attr) + self.stdscr.addstr(y, 28, value_str[:30]) + self.stdscr.attroff(value_attr) except: pass - # Draw help text + # help text (always shown, including on the highlighted row so it stays readable) if width > 65: - help_text = option['help'][:width-62] if option['help'] else "" + help_text = option['help'][:width - 62] if option['help'] else "" + help_attr = curses.color_pair(9) if is_selected else curses.color_pair(5) try: - self.stdscr.attron(curses.color_pair(5)) - self.stdscr.addstr(y, 60, help_text) - self.stdscr.attroff(curses.color_pair(5)) + self.stdscr.attron(help_attr) + self.stdscr.addstr(y, 60, help_text.ljust(width - 61)[:width - 61]) + self.stdscr.attroff(help_attr) except: pass @@ -292,50 +410,57 @@ def _edit_field(self): # Toggle boolean option['value'] = not option['value'] else: - # Text input + # Text input (manual key loop so Esc can cancel and Enter can save) height, width = self.stdscr.getmaxyx() - - # Create input window input_win = curses.newwin(5, width - 20, height // 2 - 2, 10) + input_win.keypad(True) input_win.box() input_win.attron(curses.color_pair(2)) input_win.addstr(0, 2, " Edit %s " % option['label'][:20]) input_win.attroff(curses.color_pair(2)) - input_win.addstr(2, 2, "Value:") - input_win.refresh() + input_win.attron(curses.color_pair(5)) + input_win.addstr(3, 2, "[Enter] save [Esc] cancel") + input_win.attroff(curses.color_pair(5)) - # Get input - curses.echo() + buffer = str(option['value']) if option['value'] not in (None, "") else "" + max_len = max(1, width - 34) + curses.noecho() curses.curs_set(1) - # Pre-fill with existing value - current_value = str(option['value']) if option['value'] else "" - input_win.addstr(2, 9, current_value) - input_win.move(2, 9) + while True: + shown = buffer[-max_len:] + input_win.addstr(2, 2, "Value: ") + input_win.addstr(2, 9, shown.ljust(max_len)[:max_len]) + input_win.move(2, 9 + len(shown)) + input_win.refresh() - try: - new_value = input_win.getstr(2, 9, width - 32).decode('utf-8') + ch = input_win.getch() + if ch == 27: # Esc -> cancel, keep old value + buffer = None + break + elif ch in (curses.KEY_ENTER, 10, 13): # Enter -> commit + break + elif ch in (curses.KEY_BACKSPACE, 127, 8): + buffer = buffer[:-1] + elif 32 <= ch <= 126: + buffer += chr(ch) - # Validate and convert based on type + curses.curs_set(0) + + if buffer is not None: if option['type'] == 'int': try: - option['value'] = int(new_value) if new_value else None + option['value'] = int(buffer) if buffer else None except ValueError: option['value'] = None elif option['type'] == 'float': try: - option['value'] = float(new_value) if new_value else None + option['value'] = float(buffer) if buffer else None except ValueError: option['value'] = None else: - option['value'] = new_value if new_value else None - except: - pass - - curses.noecho() - curses.curs_set(0) + option['value'] = buffer if buffer else None - # Clear input window input_win.clear() input_win.refresh() del input_win @@ -378,9 +503,9 @@ def _export_config(self): config[dest] = value # Set defaults for unset options - for option in self.parser.option_list: - if option.dest not in config or config[option.dest] is None: - config[option.dest] = defaults.get(option.dest, None) + for field in self.all_options: + if field['dest'] not in config or config[field['dest']] is None: + config[field['dest']] = defaults.get(field['dest'], None) # Save config try: @@ -537,9 +662,9 @@ def _run_sqlmap(self): config[dest] = value # Set defaults for unset options - for option in self.parser.option_list: - if option.dest not in config or config[option.dest] is None: - config[option.dest] = defaults.get(option.dest, None) + for field in self.all_options: + if field['dest'] not in config or config[field['dest']] is None: + config[field['dest']] = defaults.get(field['dest'], None) # Create temp config file handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True) @@ -713,7 +838,7 @@ def run(self): tab = self.tabs[self.current_tab] # Handle input - if key == curses.KEY_F10 or key == 27: # F10 or ESC + if key == curses.KEY_F10: # F10 quits; Esc intentionally does NOT (it only cancels field edits) break elif key == ord('\t') or key == curses.KEY_RIGHT: # Tab or Right arrow self.current_tab = (self.current_tab + 1) % len(self.tabs) @@ -755,9 +880,17 @@ def runTui(parser): # Check if ncurses is available if curses is None: raise SqlmapMissingDependence("missing 'curses' module (optional Python module). Use a Python build that includes curses/ncurses, or install the platform-provided equivalent (e.g. for Windows: pip install windows-curses)") + # ncurses waits ESCDELAY ms (default 1000) after Esc to disambiguate escape sequences, which + # makes Esc feel like it hangs for ~1s; shrink it so Esc reacts immediately + os.environ.setdefault("ESCDELAY", "25") try: # Initialize and run def main(stdscr): + if hasattr(curses, "set_escdelay"): + try: + curses.set_escdelay(25) + except curses.error: + pass ui = NcursesUI(stdscr, parser) ui.run() From 0a331f2f891e88395acb576df45c323de640e467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 24 Jun 2026 11:11:03 +0200 Subject: [PATCH 2/2] Minor update --- data/txt/sha256sums.txt | 6 ++--- lib/core/settings.py | 2 +- lib/utils/gui.py | 56 ++++++++++++++++++++++++++++++++++------- lib/utils/tui.py | 40 ++++++++++++++++++++++++++--- 4 files changed, 87 insertions(+), 17 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index b1e6dddf229..7a941787127 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -a6e15ece62113241870feacc9cda691c64be9b849ce2df169b35ee695a517165 lib/core/settings.py +2db950a79f3f8a4bbb0f35731d4e2eef220150961be55d8ba4b1f9565bdd483a lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -249,7 +249,7 @@ da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/craw a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dialect.py 51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py -417029b70afe672f3121746a7909887aa996766c92547b4566c50343fff76131 lib/utils/gui.py +3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py 972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py 0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py 71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py @@ -264,7 +264,7 @@ de4be7e291db0962cd59f9c04b3f7259f846e315df1fd9b323954f89fae0b2db lib/utils/sear 8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py 2760c4b82382e501f16bb98edec9531f46e5b286fbf004b346545b9b62f84824 lib/utils/sqlalchemy.py f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py -f19a6761284e689fca7d2e07120193f7b9c4f9c506ecaf87e82c2e97cca4db63 lib/utils/tui.py +f28693d5d2783f3d5069b1df3d12e01730ce783f4a40ef31656ef2c879d2f027 lib/utils/tui.py e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py b3c5109394f6c3cdd73a524a737b36cca7ecc56619f2a5f801eb1e7f1bfdb78b lib/utils/wafbypass.py 1b439fc59fd202c21c74978ed9f36d1c309533226c77907eae159461525f9fef lib/utils/xrange.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 2c3537f7ebf..0a2fc08ab84 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.158" +VERSION = "1.10.6.159" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/utils/gui.py b/lib/utils/gui.py index ed6dd13b329..97beb328b5c 100644 --- a/lib/utils/gui.py +++ b/lib/utils/gui.py @@ -208,6 +208,8 @@ def __init__(self, parser, tk, ttk, scrolledtext, messagebox, filedialog, font): 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.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.process = None @@ -329,6 +331,7 @@ def _buildLayout(self): cmdBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 8)) cmdBar.pack(fill=tk.X) self.ttk.Label(cmdBar, text="Command:", style="Hint.TLabel").pack(side=tk.LEFT, padx=(0, 8)) + self.ttk.Button(cmdBar, text="Copy", command=self._copyCommand, takefocus=False).pack(side=tk.RIGHT, padx=(8, 0)) self.command = tk.StringVar(value="sqlmap.py") cmdEntry = tk.Entry(cmdBar, textvariable=self.command, font=self.fonts["mono"], bg="#ffffff", fg=PALETTE["blue"], readonlybackground="#ffffff", @@ -352,6 +355,12 @@ def _buildLayout(self): 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.saveConfigDialog()) + self.window.bind("", lambda e: self.loadConfig()) self._enableSelectAll() self._tickStats() self._prebuildPanes() @@ -586,14 +595,17 @@ def _addPane(self, name, navText): 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["blue"], 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["subtext"], 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): + 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) + self.navItems[name] = (row, strip, icon, lab, badge) self.paneOrder.append(name) outer = self.ttk.Frame(self.content, style="Card.TFrame") @@ -618,8 +630,8 @@ def _navHover(self, name, entering): if name == self.currentPane: return bg = PALETTE["surface2"] if entering else PALETTE["mantle"] - row, strip, icon, lab = self.navItems[name] - for w in (row, strip, icon, lab): + row, strip, icon, lab, badge = self.navItems[name] + for w in (row, strip, icon, lab, badge): w.configure(background=bg) def _navKey(self, delta): @@ -643,16 +655,18 @@ def _selectPane(self, name): p = PALETTE if self.currentPane: self.panes[self.currentPane].pack_forget() - row, strip, icon, lab = self.navItems[self.currentPane] + 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["text"], font=self.fonts["nav"]) + badge.configure(background=p["mantle"], foreground=p["blue"]) self._drawIcon(icon, self.currentPane, self._iconColor(self.currentPane)) self.panes[name].pack(expand=True, fill=self.tk.BOTH) - row, strip, icon, lab = self.navItems[name] + row, strip, icon, lab, badge = self.navItems[name] for w in (row, strip, icon): w.configure(background=p["blue"]) lab.configure(background=p["blue"], foreground="#ffffff", font=self.fonts["bodyBold"]) + badge.configure(background=p["blue"], foreground="#ffffff") self._drawIcon(icon, name, "#ffffff") self.currentPane = name self._ensureNavVisible(name) @@ -704,6 +718,7 @@ def _onWheel(self, event): 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): self.ttk.Label(inner, text="Quick start", style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") @@ -721,6 +736,7 @@ def build(inner): 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.ttk.Label(inner, text=title, style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") @@ -806,19 +822,25 @@ def _center(self, window, width=None, height=None): window.geometry("%dx%d+%d+%d" % (width, height, x, y)) def _updateStats(self): - count = 0 + setDests = set() for dest, (otype, var) in self.widgets.items(): try: if otype == "bool": if var.get(): - count += 1 + setDests.add(dest) else: raw = var.get() if raw not in (None, "") and str(raw) != str(defaults.get(dest, "")): - count += 1 + setDests.add(dest) except Exception: pass + count = len(setDests) self.stat.set("%d option%s set" % (count, "" if count == 1 else "s")) + 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): parts = ["sqlmap.py"] @@ -850,6 +872,22 @@ def _tickStats(self): self.command.set(self._buildCommandString()) self.window.after(1200, self._tickStats) + 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 _focusTarget(self): + try: + self.targetEntry.focus_set() + self.targetEntry.select_range(0, "end") + except Exception: + pass + return "break" + def _collectConfig(self): config = {} for dest, (otype, var) in self.widgets.items(): diff --git a/lib/utils/tui.py b/lib/utils/tui.py index 476ecceccff..3f5d6f43ead 100644 --- a/lib/utils/tui.py +++ b/lib/utils/tui.py @@ -260,6 +260,37 @@ def _draw_tabs(self): x += len(tab_text) + 1 + def _build_command(self): + """Assemble the equivalent sqlmap command line from the current field values""" + parts = ["sqlmap.py"] + for opt in self.all_options: + flag = opt['label'].split(',')[0].strip() if opt['label'] else "" + if not flag: + continue + value = opt['value'] + if opt['type'] == 'bool': + if value: + parts.append(flag) + elif value not in (None, "") and str(value) != str(opt.get('default') or ""): + text = str(value) + if ' ' in text or '"' in text: + text = '"%s"' % text.replace('"', '\\"') + parts.append("%s %s" % (flag, text)) + return " ".join(parts) + + def _draw_command(self): + """Live preview of the command being built, just above the footer""" + height, width = self.stdscr.getmaxyx() + cmd = "$ " + self._build_command() + if len(cmd) > width - 2: + cmd = cmd[:width - 5] + "..." + try: + self.stdscr.attron(curses.color_pair(8) | curses.A_BOLD) + self.stdscr.addstr(height - 2, 1, cmd.ljust(width - 2)[:width - 2]) + self.stdscr.attroff(curses.color_pair(8) | curses.A_BOLD) + except curses.error: + pass + def _draw_footer(self): """Draw the footer with help text""" height, width = self.stdscr.getmaxyx() @@ -303,12 +334,12 @@ def _draw_current_tab(self): pass y += 1 - # Draw options + # Draw options (leave height-2 for the command preview, height-1 for the footer) visible_start = self.scroll_offset - visible_end = visible_start + (height - y - 2) + visible_end = visible_start + (height - y - 3) for i, option in enumerate(tab['options'][visible_start:visible_end], visible_start): - if y >= height - 2: + if y >= height - 3: break is_selected = (i == self.current_field) @@ -374,7 +405,7 @@ def _draw_current_tab(self): if len(tab['options']) > visible_end - visible_start: try: self.stdscr.attron(curses.color_pair(6)) - self.stdscr.addstr(height - 2, width - 10, "[More...]") + self.stdscr.addstr(height - 3, width - 10, "[More...]") self.stdscr.attroff(curses.color_pair(6)) except: pass @@ -828,6 +859,7 @@ def run(self): self._draw_header() self._draw_tabs() self._draw_current_tab() + self._draw_command() self._draw_footer() self.stdscr.refresh()