sqlmap/lib/utils/gui.py
2026-06-24 10:57:52 +02:00

1016 lines
44 KiB
Python

#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import os
import subprocess
import sys
import tempfile
import threading
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
# 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("<Enter>", self._show, add="+")
widget.bind("<Leave>", self._hide, add="+")
widget.bind("<ButtonPress>", 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("<Configure>", lambda e: self.navCanvas.configure(scrollregion=self.navCanvas.bbox("all")))
navWin = self.navCanvas.create_window((0, 0), window=self.nav, anchor="nw")
self.navCanvas.bind("<Configure>", lambda e: self.navCanvas.itemconfigure(navWin, width=e.width))
self.navCanvas.configure(yscrollcommand=navScroll.set)
self.navCanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
navScroll.pack(side=tk.RIGHT, fill=tk.Y)
tk.Frame(body, background=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("<Down>", lambda e: self._navKey(1))
self.window.bind("<Up>", lambda e: self._navKey(-1))
for seq in ("<MouseWheel>", "<Button-4>", "<Button-5>"):
self.window.bind_all(seq, self._onWheel)
self._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 selectText(event):
try:
event.widget.tag_add("sel", "1.0", "end-1c")
except Exception:
pass
return "break"
for cls in ("TEntry", "Entry", "TCombobox"):
self.window.bind_class(cls, "<Control-a>", selectEntry)
self.window.bind_class(cls, "<Control-A>", selectEntry)
for seq in ("<Control-a>", "<Control-A>"):
self.window.bind_class("Text", seq, selectText)
def _buildMenu(self):
p = PALETTE
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("<Configure>", 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", "<Button-1>", lambda e: self.run())
c.tag_bind("runbtn", "<Enter>", lambda e: self._hoverRun(True))
c.tag_bind("runbtn", "<Leave>", 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("<Button-1>", lambda e, n=name: self._selectPane(n))
w.bind("<Enter>", lambda e, n=name: self._navHover(n, True))
w.bind("<Leave>", lambda e, n=name: self._navHover(n, False))
self.navItems[name] = (row, strip, icon, lab)
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("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
window_id = canvas.create_window((0, 0), window=inner, anchor="nw")
canvas.bind("<Configure>", lambda e: canvas.itemconfigure(window_id, width=e.width))
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
self.panes[name] = outer
self.canvases[name] = canvas
self.inners[name] = inner
return inner
def _iconColor(self, name):
return PALETTE.get(ICON_COLORS.get(name, "subtext"), PALETTE["subtext"])
def _navHover(self, name, entering):
if 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("<Enter>", lambda e: self.hint.set(helptext), add="+")
widget.bind("<FocusIn>", lambda e: self.hint.set(helptext), add="+")
widget.bind("<Leave>", lambda e: self.hint.set(HINT_DEFAULT), add="+")
widget.bind("<FocusOut>", 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:
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 = 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:
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 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(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)
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)
self.alive = False
stream.close()
thread = threading.Thread(target=enqueue, args=(self.process.stdout, self.queue))
thread.daemon = True
thread.start()
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"
text.bind("<Key>", onKey)
text.bind("<Return>", onReturn)
def pump():
drained = False
try:
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:
pass
if drained:
text.see(tk.END)
if self.alive or not self.queue.empty():
top.after(80, pump)
else:
text.insert(tk.END, "\n--- process finished ---\n")
text.see(tk.END)
self._center(top, 900, 580)
top.after(80, pump)
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()