diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 946335c3669..1f91f51db58 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -166,7 +166,7 @@ cd63cfc6b00c5e47462cd4a35b3a79306d6712f9d607d5c784f9e946f92a8a7f lib/controller 34e9cf166e21ce991b61ca7695c43c892e8425f7e1228daec8cadd38f786acc6 lib/controller/controller.py 49bcd74281297c79a6ae5d4b0d1479ddace4476fddaf4383ca682a6977b553e3 lib/controller/handler.py 4608f21a4333c162ab3c266c903fda4793cc5834de30d06affe9b7566dd09811 lib/controller/__init__.py -ac44a343947162532dbf17bd1f9ab424f8008f677367c5ad3f9f7b715a679818 lib/core/agent.py +774cd68596d6c1b15fe16bfb3901487b15ca7d284bbe8b347b3a316b0e24cc78 lib/core/agent.py 86a9cb82c7e7beb4730264dae20bf3b7cd87c0dcaee587367362cf319f7bb079 lib/core/bigarray.py f6062e324fdeaacf9df0a289fc3f12f755143e3876a70cb65b38aa2e690f73c1 lib/core/common.py 11c748cc96ea2bc507bc6c1930a17fe4bc6fdd2dd2a80430df971cb21428eb00 lib/core/compat.py @@ -179,7 +179,6 @@ a8b398601dae3318d255f936f5bb6acd25ffdc8ef6d6b713ad89ee7136d1c736 lib/core/dicts 20a6edda1d57a7564869e366f57ed7b2ab068dd8716cf7a10ef4a02d154d6c80 lib/core/dump.py 20ea31bb52785900d6bba5e9f2f560a4ed064cb95add75015de105959aa9c4d4 lib/core/enums.py 00a9b29caa81fe4a5ef145202f9c92e6081f90b2a85cd76c878d520d900ad856 lib/core/exception.py -1c48804c10b94da696d3470efbd25d2fff0f0bbf2af0101aaac8f8c097fce02b lib/core/gui.py 4608f21a4333c162ab3c266c903fda4793cc5834de30d06affe9b7566dd09811 lib/core/__init__.py 3d308440fb01d04b5d363bfbe0f337756b098532e5bb7a1c91d5213157ec2c35 lib/core/log.py 2a06dc9b5c17a1efdcdb903545729809399f1ee96f7352cc19b9aaa227394ff3 lib/core/optiondict.py @@ -190,7 +189,7 @@ c4bfb493a03caf84dd362aec7c248097841de804b7413d0e1ecb8a90c8550bc0 lib/core/readl d1bd70c1a55858495c727fbec91e30af267459c8f64d50fabf9e4ee2c007e920 lib/core/replication.py 1d0f80b0193ac5204527bfab4bde1a7aee0f693fd008e86b4b29f606d1ef94f3 lib/core/revision.py d2eb8e4b05ac93551272b3d4abfaf5b9f2d3ac92499a7704c16ed0b4f200db38 lib/core/session.py -f869523fb2f64f4cb415ede7dda998b903dfd885c6a65b860e4f572497675181 lib/core/settings.py +17d2ef081f4e0820b2123e0a213dc44a7a081fa18da9b0b4edf1991c13c3a82d lib/core/settings.py 1c5eab9494eb969bc9ce118a2ea6954690c6851cbe54c18373c723b99734bf09 lib/core/shell.py 4eea6dcf023e41e3c64b210cb5c2efc7ca893b727f5e49d9c924f076bb224053 lib/core/subprocessng.py cdd352e1331c6b535e780f6edea79465cb55af53aa2114dcea0e8bf382e56d1a lib/core/target.py @@ -201,7 +200,7 @@ a9b3dca1c17f56bed8e07973c7f8603932012931947633781f7523c05cb2bed2 lib/core/testi cba481f8c79f4a75bd147b9eb5a1e6e61d70422fceadd12494b1dbaa4f1d27f4 lib/core/wordlist.py 4608f21a4333c162ab3c266c903fda4793cc5834de30d06affe9b7566dd09811 lib/__init__.py 7d1d3e07a1f088428d155c0e1b28e67ecbf5f62775bdeeeb11b4388369dce0f7 lib/parse/banner.py -c6d1527a26014b58b8a78afb851485227b86798e36551e9ac347522ef89d7a99 lib/parse/cmdline.py +a3428f5586c907a323c700c5f3523580248f6f769dfbbf23e0dd3faa15d231e3 lib/parse/cmdline.py f1ad73b6368730b8b8bc2e28b3305445d2b954041717619bede421ccc4381625 lib/parse/configfile.py a96b7093f30b3bf774f5cc7a622867472d64a2ae8b374b43786d155cf6203093 lib/parse/handler.py cfd4857ce17e0a2da312c18dcff28aefaa411f419b4e383b202601c42de40eec lib/parse/headers.py @@ -248,6 +247,7 @@ af67d25e8c16b429a5b471d3c629dc1da262262320bf7cd68465d151c02def16 lib/utils/brut 828940a8eefda29c9eb271c21f29e2c4d1d428ccf0dcc6380e7ee6740300ec55 lib/utils/crawler.py 56b93ba38f127929346f54aa75af0db5f46f9502b16acfe0d674a209de6cad2d lib/utils/deps.py 3aca7632d53ab2569ddef876a1b90f244640a53e19b304c77745f8ddb15e6437 lib/utils/getch.py +a49b6f13bee9d0f30d78c2cd321a61dbd619d9e14949747f8b15a984c7fb8105 lib/utils/gui.py 4979120bbbc030eaef97147ee9d7d564d9683989059b59be317153cdaa23d85b lib/utils/har.py af047a6efc1719a3d166fac0b7ff98ab3d29af7b676ff977e98c31c80e9e883e lib/utils/hashdb.py 8c9caffbd821ad9547c27095c8e55c398ea743b2e44d04b3572e2670389ccf5b lib/utils/hash.py @@ -261,6 +261,7 @@ c0e6e33d2aa115e7ab2459e099cbaeb282065ea158943efc2ff69ba771f03210 lib/utils/sear 8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py 61dfd44fb0a5a308ba225092cb2768491ea2393999683545b7a9c4f190001ab8 lib/utils/sqlalchemy.py 6f5f4b921f8cfe625e4656ee4560bc7d699d1aebf6225e9a8f5cf969d0fa7896 lib/utils/timeout.py +9967e8af7db75fa662a3934e3f4e6fb03f56448a6f96d7fb3761bca7a0f917a5 lib/utils/tui.py 9cb6bd014598515a95945f03861e7484d6c0f9f4b508219eb5cc0c372ed5c173 lib/utils/versioncheck.py bd4975ff9cbc0745d341e6c884e6a11b07b0a414105cc899e950686d2c1f88ba lib/utils/xrange.py 33049ba7ddaea4a8a83346b3be29d5afce52bbe0b9d8640072d45cadc0e6d4bb LICENSE diff --git a/lib/core/agent.py b/lib/core/agent.py index 8958824836a..a6436c4013b 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -119,7 +119,10 @@ def payload(self, place=None, parameter=None, value=None, newValue=None, where=N if place == PLACE.URI: origValue = origValue.split(kb.customInjectionMark)[0] else: - origValue = filterNone(re.search(_, origValue.split(BOUNDED_INJECTION_MARKER)[0]) for _ in (r"\w+\Z", r"[^\"'><]+\Z", r"[^ ]+\Z"))[0].group(0) + try: + origValue = filterNone(re.search(_, origValue.split(BOUNDED_INJECTION_MARKER)[0]) for _ in (r"\w+\Z", r"[^\"'><]+\Z", r"[^ ]+\Z"))[0].group(0) + except IndexError: + pass origValue = origValue[origValue.rfind('/') + 1:] for char in ('?', '=', ':', ',', '&'): if char in origValue: @@ -883,14 +886,16 @@ def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, query = query[len("TOP %s " % topNum):] unionQuery += "TOP %s " % topNum - intoRegExp = re.search(r"(\s+INTO (DUMP|OUT)FILE\s+'(.+?)')", query, re.I) + intoFileRegExp = re.search(r"(\s+INTO (DUMP|OUT)FILE\s+'(.+?)')", query, re.I) - if intoRegExp: - intoRegExp = intoRegExp.group(1) - query = query[:query.index(intoRegExp)] + if intoFileRegExp: + infoFile = intoFileRegExp.group(1) + query = query[:query.index(infoFile)] position = 0 char = NULL + else: + infoFile = None for element in xrange(0, count): if element > 0: @@ -909,8 +914,8 @@ def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, if fromTable and not unionQuery.endswith(fromTable): unionQuery += fromTable - if intoRegExp: - unionQuery += intoRegExp + if infoFile: + unionQuery += infoFile if multipleUnions: unionQuery += " UNION ALL SELECT " diff --git a/lib/core/settings.py b/lib/core/settings.py index 3a02bbffee5..16e805f829d 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -19,7 +19,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.9.12.54" +VERSION = "1.9.12.60" 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) @@ -538,7 +538,7 @@ UNSAFE_DUMP_FILEPATH_REPLACEMENT = '_' # Options that need to be restored in multiple targets run mode -RESTORE_MERGED_OPTIONS = ("col", "db", "dnsDomain", "privEsc", "tbl", "regexp", "string", "textOnly", "threads", "timeSec", "tmpPath", "uChar", "user") +RESTORE_MERGED_OPTIONS = ("col", "db", "dbms", "os", "dnsDomain", "privEsc", "tbl", "regexp", "string", "textOnly", "threads", "timeSec", "tmpPath", "uChar", "user") # Parameters to be ignored in detection phase (upper case) IGNORE_PARAMETERS = ("__VIEWSTATE", "__VIEWSTATEENCRYPTED", "__VIEWSTATEGENERATOR", "__EVENTARGUMENT", "__EVENTTARGET", "__EVENTVALIDATION", "ASPSESSIONID", "ASP.NET_SESSIONID", "JSESSIONID", "CFID", "CFTOKEN") diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index b4d4df7ea9f..696f0168805 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -775,6 +775,9 @@ def cmdLineParser(argv=None): miscellaneous.add_argument("--disable-hashing", dest="disableHashing", action="store_true", help="Disable hash analysis on table dumps") + miscellaneous.add_argument("--gui", dest="gui", action="store_true", + help="Experimental Tkinter GUI") + miscellaneous.add_argument("--list-tampers", dest="listTampers", action="store_true", help="Display list of available tamper scripts") @@ -799,6 +802,9 @@ def cmdLineParser(argv=None): miscellaneous.add_argument("--tmp-dir", dest="tmpDir", help="Local directory for storing temporary files") + miscellaneous.add_argument("--tui", dest="tui", action="store_true", + help="Experimental ncurses TUI") + miscellaneous.add_argument("--unstable", dest="unstable", action="store_true", help="Adjust options for unstable connections") @@ -857,9 +863,6 @@ def cmdLineParser(argv=None): parser.add_argument("--non-interactive", dest="nonInteractive", action="store_true", help=SUPPRESS) - parser.add_argument("--gui", dest="gui", action="store_true", - help=SUPPRESS) - parser.add_argument("--smoke-test", dest="smokeTest", action="store_true", help=SUPPRESS) @@ -933,12 +936,19 @@ def _format_action_invocation(self, action): checkOldOptions(argv) if "--gui" in argv: - from lib.core.gui import runGui + from lib.utils.gui import runGui runGui(parser) raise SqlmapSilentQuitException + elif "--tui" in argv: + from lib.utils.tui import runTui + + runTui(parser) + + raise SqlmapSilentQuitException + elif "--shell" in argv: _createHomeDirectories() diff --git a/lib/core/gui.py b/lib/utils/gui.py similarity index 99% rename from lib/core/gui.py rename to lib/utils/gui.py index a324ba901ba..f1b077949ce 100644 --- a/lib/core/gui.py +++ b/lib/utils/gui.py @@ -67,7 +67,7 @@ def check(self, *args): errMsg = "unable to create GUI window ('%s')" % getSafeExString(ex) raise SqlmapSystemException(errMsg) - window.title(VERSION_STRING) + window.title("sqlmap - Tkinter GUI") # Set theme and colors bg_color = "#f5f5f5" @@ -251,7 +251,7 @@ def enqueue(stream, queue): 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", "Copyright (c) 2006-2025\n\n (%s)" % DEV_EMAIL_ADDRESS)) + 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) diff --git a/lib/utils/tui.py b/lib/utils/tui.py new file mode 100644 index 00000000000..5b85a4bdbb8 --- /dev/null +++ b/lib/utils/tui.py @@ -0,0 +1,770 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import os +import subprocess +import sys +import tempfile + +try: + import curses +except ImportError: + curses = None + +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 IS_WIN +from thirdparty.six.moves import queue as _queue +from thirdparty.six.moves import configparser as _configparser + +class NcursesUI: + def __init__(self, stdscr, parser): + self.stdscr = stdscr + self.parser = parser + self.current_tab = 0 + self.current_field = 0 + self.scroll_offset = 0 + self.tabs = [] + self.fields = {} + self.running = False + self.process = None + self.queue = 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 + + # Setup curses + curses.curs_set(1) + self.stdscr.keypad(1) + + # Parse option groups + self._parse_options() + + def _parse_options(self): + """Parse command line options into tabs and fields""" + for group in self.parser.option_groups: + tab_data = { + 'title': group.title, + 'description': group.get_description() if hasattr(group, 'get_description') and group.get_description() else "", + 'options': [] + } + + for option in group.option_list: + field_data = { + 'dest': option.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', + 'value': '', + 'default': defaults.get(option.dest) if defaults.get(option.dest) else None + } + tab_data['options'].append(field_data) + self.fields[(group.title, option.dest)] = field_data + + self.tabs.append(tab_data) + + 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) + + 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) + + def _get_tab_bar_height(self): + """Calculate how many rows the tab bar uses""" + height, width = self.stdscr.getmaxyx() + y = 1 + x = 0 + + for i, tab in enumerate(self.tabs): + tab_text = " %s " % tab['title'] + + # Check if tab exceeds width, wrap to next line + if x + len(tab_text) >= width: + y += 1 + x = 0 + # Stop if we've used too many lines + if y >= 3: + break + + x += len(tab_text) + 1 + + return y + + def _draw_tabs(self): + """Draw the tab bar""" + height, width = self.stdscr.getmaxyx() + y = 1 + x = 0 + + for i, tab in enumerate(self.tabs): + tab_text = " %s " % tab['title'] + + # Check if tab exceeds width, wrap to next line + if x + len(tab_text) >= width: + y += 1 + x = 0 + # Stop if we've used too many lines + if y >= 3: + break + + if i == self.current_tab: + self.stdscr.attron(curses.color_pair(2) | curses.A_BOLD) + else: + self.stdscr.attron(curses.color_pair(3)) + + try: + self.stdscr.addstr(y, x, tab_text) + except: + pass + + if i == self.current_tab: + self.stdscr.attroff(curses.color_pair(2) | curses.A_BOLD) + else: + self.stdscr.attroff(curses.color_pair(3)) + + x += len(tab_text) + 1 + + 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 " + + try: + self.stdscr.attron(curses.color_pair(1)) + self.stdscr.addstr(height - 1, 0, footer.ljust(width)) + self.stdscr.attroff(curses.color_pair(1)) + except: + pass + + def _draw_current_tab(self): + """Draw the current tab content""" + height, width = self.stdscr.getmaxyx() + tab = self.tabs[self.current_tab] + + # Calculate tab bar height + tab_bar_height = self._get_tab_bar_height() + start_y = tab_bar_height + 1 + + # Clear content area + for y in range(start_y, height - 1): + try: + self.stdscr.addstr(y, 0, " " * width) + except: + pass + + y = start_y + + # Draw description if exists + if tab['description']: + desc_lines = self._wrap_text(tab['description'], width - 4) + for line in desc_lines[:2]: # Limit to 2 lines + try: + self.stdscr.attron(curses.color_pair(5)) + self.stdscr.addstr(y, 2, line) + self.stdscr.attroff(curses.color_pair(5)) + y += 1 + except: + pass + y += 1 + + # Draw options + visible_start = self.scroll_offset + visible_end = visible_start + (height - y - 2) + + for i, option in enumerate(tab['options'][visible_start:visible_end], visible_start): + if y >= height - 2: + break + + is_selected = (i == self.current_field) + + # Draw label + label = option['label'][:25].ljust(25) + try: + if is_selected: + self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD) + else: + self.stdscr.attron(curses.color_pair(7)) + + 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)) + except: + pass + + # Draw value + value_str = "" + if option['type'] == 'bool': + value = option['value'] if option['value'] is not None else option.get('default') + value_str = "[X]" if value else "[ ]" + 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] + + 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) + except: + pass + + # Draw help text + if width > 65: + help_text = option['help'][:width-62] if option['help'] else "" + try: + self.stdscr.attron(curses.color_pair(5)) + self.stdscr.addstr(y, 60, help_text) + self.stdscr.attroff(curses.color_pair(5)) + except: + pass + + y += 1 + + # Draw scroll indicator + 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.attroff(curses.color_pair(6)) + except: + pass + + def _wrap_text(self, text, width): + """Wrap text to fit within width""" + words = text.split() + lines = [] + current_line = "" + + for word in words: + if len(current_line) + len(word) + 1 <= width: + current_line += word + " " + else: + if current_line: + lines.append(current_line.strip()) + current_line = word + " " + + if current_line: + lines.append(current_line.strip()) + + return lines + + def _edit_field(self): + """Edit the current field""" + tab = self.tabs[self.current_tab] + if self.current_field >= len(tab['options']): + return + + option = tab['options'][self.current_field] + + if option['type'] == 'bool': + # Toggle boolean + option['value'] = not option['value'] + else: + # Text input + height, width = self.stdscr.getmaxyx() + + # Create input window + input_win = curses.newwin(5, width - 20, height // 2 - 2, 10) + 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() + + # Get input + curses.echo() + 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) + + try: + new_value = input_win.getstr(2, 9, width - 32).decode('utf-8') + + # Validate and convert based on type + if option['type'] == 'int': + try: + option['value'] = int(new_value) if new_value else None + except ValueError: + option['value'] = None + elif option['type'] == 'float': + try: + option['value'] = float(new_value) if new_value 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) + + # Clear input window + input_win.clear() + input_win.refresh() + del input_win + + def _export_config(self): + """Export current configuration to a file""" + height, width = self.stdscr.getmaxyx() + + # Create input window + input_win = curses.newwin(5, width - 20, height // 2 - 2, 10) + input_win.box() + input_win.attron(curses.color_pair(2)) + input_win.addstr(0, 2, " Export Configuration ") + input_win.attroff(curses.color_pair(2)) + input_win.addstr(2, 2, "File:") + input_win.refresh() + + # Get input + curses.echo() + curses.curs_set(1) + + try: + filename = input_win.getstr(2, 8, width - 32).decode('utf-8').strip() + + if filename: + # Collect all field values + config = {} + for tab in self.tabs: + for option in tab['options']: + dest = option['dest'] + value = option['value'] if option['value'] is not None else option.get('default') + + if option['type'] == 'bool': + config[dest] = bool(value) + elif option['type'] == 'int': + config[dest] = int(value) if value else None + elif option['type'] == 'float': + config[dest] = float(value) if value else None + else: + 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) + + # Save config + try: + saveConfig(config, filename) + + # Show success message + input_win.clear() + input_win.box() + input_win.attron(curses.color_pair(5)) + input_win.addstr(0, 2, " Export Successful ") + input_win.attroff(curses.color_pair(5)) + input_win.addstr(2, 2, "Configuration exported to:") + input_win.addstr(3, 2, filename[:width - 26]) + input_win.refresh() + curses.napms(2000) + except Exception as ex: + # Show error message + input_win.clear() + input_win.box() + input_win.attron(curses.color_pair(6)) + input_win.addstr(0, 2, " Export Failed ") + input_win.attroff(curses.color_pair(6)) + input_win.addstr(2, 2, str(getSafeExString(ex))[:width - 26]) + input_win.refresh() + curses.napms(2000) + except: + pass + + curses.noecho() + curses.curs_set(0) + + # Clear input window + input_win.clear() + input_win.refresh() + del input_win + + def _import_config(self): + """Import configuration from a file""" + height, width = self.stdscr.getmaxyx() + + # Create input window + input_win = curses.newwin(5, width - 20, height // 2 - 2, 10) + input_win.box() + input_win.attron(curses.color_pair(2)) + input_win.addstr(0, 2, " Import Configuration ") + input_win.attroff(curses.color_pair(2)) + input_win.addstr(2, 2, "File:") + input_win.refresh() + + # Get input + curses.echo() + curses.curs_set(1) + + try: + filename = input_win.getstr(2, 8, width - 32).decode('utf-8').strip() + + if filename and os.path.isfile(filename): + try: + # Read config file + config = _configparser.ConfigParser() + config.read(filename) + + imported_count = 0 + + # Load values into fields + for tab in self.tabs: + for option in tab['options']: + dest = option['dest'] + + # Search for option in all sections + for section in config.sections(): + if config.has_option(section, dest): + value = config.get(section, dest) + + # Convert based on type + if option['type'] == 'bool': + option['value'] = value.lower() in ('true', '1', 'yes', 'on') + elif option['type'] == 'int': + try: + option['value'] = int(value) if value else None + except ValueError: + option['value'] = None + elif option['type'] == 'float': + try: + option['value'] = float(value) if value else None + except ValueError: + option['value'] = None + else: + option['value'] = value if value else None + + imported_count += 1 + break + + # Show success message + input_win.clear() + input_win.box() + input_win.attron(curses.color_pair(5)) + input_win.addstr(0, 2, " Import Successful ") + input_win.attroff(curses.color_pair(5)) + input_win.addstr(2, 2, "Imported %d options from:" % imported_count) + input_win.addstr(3, 2, filename[:width - 26]) + input_win.refresh() + curses.napms(2000) + + except Exception as ex: + # Show error message + input_win.clear() + input_win.box() + input_win.attron(curses.color_pair(6)) + input_win.addstr(0, 2, " Import Failed ") + input_win.attroff(curses.color_pair(6)) + input_win.addstr(2, 2, str(getSafeExString(ex))[:width - 26]) + input_win.refresh() + curses.napms(2000) + elif filename: + # File not found + input_win.clear() + input_win.box() + input_win.attron(curses.color_pair(6)) + input_win.addstr(0, 2, " File Not Found ") + input_win.attroff(curses.color_pair(6)) + input_win.addstr(2, 2, "File does not exist:") + input_win.addstr(3, 2, filename[:width - 26]) + input_win.refresh() + curses.napms(2000) + except: + pass + + curses.noecho() + curses.curs_set(0) + + # Clear input window + input_win.clear() + input_win.refresh() + del input_win + + def _run_sqlmap(self): + """Run sqlmap with current configuration""" + config = {} + + # Collect all field values + for tab in self.tabs: + for option in tab['options']: + dest = option['dest'] + value = option['value'] if option['value'] is not None else option.get('default') + + if option['type'] == 'bool': + config[dest] = bool(value) + elif option['type'] == 'int': + config[dest] = int(value) if value else None + elif option['type'] == 'float': + config[dest] = float(value) if value else None + else: + 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) + + # Create temp config file + handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True) + os.close(handle) + + saveConfig(config, configFile) + + # Show console + self._show_console(configFile) + + def _show_console(self, configFile): + """Show console output from sqlmap""" + height, width = self.stdscr.getmaxyx() + + # Create console window + console_win = curses.newwin(height - 4, width - 4, 2, 2) + console_win.box() + console_win.attron(curses.color_pair(2)) + console_win.addstr(0, 2, " sqlmap Console - Press Q to close ") + console_win.attroff(curses.color_pair(2)) + console_win.refresh() + + # Create output area + output_win = console_win.derwin(height - 8, width - 8, 2, 2) + output_win.scrollok(True) + output_win.idlok(True) + + # Start sqlmap process + try: + 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 + ) + + if not IS_WIN: + # Make it non-blocking + import fcntl + flags = fcntl.fcntl(process.stdout, fcntl.F_GETFL) + fcntl.fcntl(process.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + output_win.nodelay(True) + console_win.nodelay(True) + + lines = [] + current_line = "" + + while True: + # Check for user input + try: + key = console_win.getch() + if key in (ord('q'), ord('Q')): + # Kill process + process.terminate() + break + elif key == curses.KEY_ENTER or key == 10: + # Send newline to process + if process.poll() is None: + try: + process.stdin.write(b'\n') + process.stdin.flush() + except: + pass + except: + pass + + # Read output + try: + chunk = process.stdout.read(1024) + if chunk: + current_line += chunk.decode('utf-8', errors='ignore') + + # Split into lines + while '\n' in current_line: + line, current_line = current_line.split('\n', 1) + lines.append(line) + + # Keep only last N lines + if len(lines) > 1000: + lines = lines[-1000:] + + # Display lines + output_win.clear() + start_line = max(0, len(lines) - (height - 10)) + for i, l in enumerate(lines[start_line:]): + try: + output_win.addstr(i, 0, l[:width-10]) + except: + pass + output_win.refresh() + console_win.refresh() + except: + pass + + # Check if process ended + if process.poll() is not None: + # Read remaining output + try: + remaining = process.stdout.read() + if remaining: + current_line += remaining.decode('utf-8', errors='ignore') + for line in current_line.split('\n'): + if line: + lines.append(line) + except: + pass + + # Display final output + output_win.clear() + start_line = max(0, len(lines) - (height - 10)) + for i, l in enumerate(lines[start_line:]): + try: + output_win.addstr(i, 0, l[:width-10]) + except: + pass + + output_win.addstr(height - 9, 0, "--- Process finished. Press Q to close ---") + output_win.refresh() + console_win.refresh() + + # Wait for Q + console_win.nodelay(False) + while True: + key = console_win.getch() + if key in (ord('q'), ord('Q')): + break + + break + + # Small delay + curses.napms(50) + + except Exception as ex: + output_win.addstr(0, 0, "Error: %s" % getSafeExString(ex)) + output_win.refresh() + console_win.nodelay(False) + console_win.getch() + + finally: + # Clean up + try: + os.unlink(configFile) + except: + pass + + console_win.nodelay(False) + output_win.nodelay(False) + del output_win + del console_win + + def run(self): + """Main UI loop""" + while True: + self.stdscr.clear() + + # Draw UI + self._draw_header() + self._draw_tabs() + self._draw_current_tab() + self._draw_footer() + + self.stdscr.refresh() + + # Get input + key = self.stdscr.getch() + + tab = self.tabs[self.current_tab] + + # Handle input + if key == curses.KEY_F10 or key == 27: # F10 or ESC + break + elif key == ord('\t') or key == curses.KEY_RIGHT: # Tab or Right arrow + self.current_tab = (self.current_tab + 1) % len(self.tabs) + self.current_field = 0 + self.scroll_offset = 0 + elif key == curses.KEY_LEFT: # Left arrow + self.current_tab = (self.current_tab - 1) % len(self.tabs) + self.current_field = 0 + self.scroll_offset = 0 + elif key == curses.KEY_UP: # Up arrow + if self.current_field > 0: + self.current_field -= 1 + # Adjust scroll if needed + if self.current_field < self.scroll_offset: + self.scroll_offset = self.current_field + elif key == curses.KEY_DOWN: # Down arrow + if self.current_field < len(tab['options']) - 1: + self.current_field += 1 + # Adjust scroll if needed + height, width = self.stdscr.getmaxyx() + visible_lines = height - 8 + if self.current_field >= self.scroll_offset + visible_lines: + self.scroll_offset = self.current_field - visible_lines + 1 + elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter + self._edit_field() + elif key == curses.KEY_F2: # F2 to run + self._run_sqlmap() + elif key == curses.KEY_F3: # F3 to export + self._export_config() + elif key == curses.KEY_F4: # F4 to import + self._import_config() + elif key == ord(' '): # Space for boolean toggle + option = tab['options'][self.current_field] + if option['type'] == 'bool': + option['value'] = not option['value'] + +def runTui(parser): + """Main entry point for ncurses TUI""" + # 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)") + try: + # Initialize and run + def main(stdscr): + ui = NcursesUI(stdscr, parser) + ui.run() + + curses.wrapper(main) + + except Exception as ex: + errMsg = "unable to create ncurses UI ('%s')" % getSafeExString(ex) + raise SqlmapSystemException(errMsg)