diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 6c970b8ac..77490f731 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -180,6 +180,7 @@ c9d1f64648062d7962caf02c4e2e7d84e8feb2a14451146f627112aae889afcd lib/core/dump. 1c48804c10b94da696d3470efbd25d2fff0f0bbf2af0101aaac8f8c097fce02b lib/core/gui.py 4608f21a4333c162ab3c266c903fda4793cc5834de30d06affe9b7566dd09811 lib/core/__init__.py 3d308440fb01d04b5d363bfbe0f337756b098532e5bb7a1c91d5213157ec2c35 lib/core/log.py +3c6702f14ecd14f12fdab02c8b28fa4d9fdc477b7fa743e743728b56b89d4db4 lib/core/ncgui.py 2a06dc9b5c17a1efdcdb903545729809399f1ee96f7352cc19b9aaa227394ff3 lib/core/optiondict.py d33dbc25635e2ae42c70e5997f28097143966279adfbf98e95b0d09ad4976e88 lib/core/option.py fd449fe2c707ce06c929fc164cbabb3342f3e4e2b86c06f3efc1fc09ac98a25a lib/core/patch.py @@ -199,7 +200,7 @@ f7245b99c17ef88cd9a626ca09c0882a5e172bb10a38a5dec9d08da6c8e2d076 lib/core/updat cba481f8c79f4a75bd147b9eb5a1e6e61d70422fceadd12494b1dbaa4f1d27f4 lib/core/wordlist.py 4608f21a4333c162ab3c266c903fda4793cc5834de30d06affe9b7566dd09811 lib/__init__.py 7d1d3e07a1f088428d155c0e1b28e67ecbf5f62775bdeeeb11b4388369dce0f7 lib/parse/banner.py -c6d1527a26014b58b8a78afb851485227b86798e36551e9ac347522ef89d7a99 lib/parse/cmdline.py +380881c528f70290b0d3ca888d1d306e6aa19eff3fe22ebf10fe3248bec01044 lib/parse/cmdline.py f1ad73b6368730b8b8bc2e28b3305445d2b954041717619bede421ccc4381625 lib/parse/configfile.py a96b7093f30b3bf774f5cc7a622867472d64a2ae8b374b43786d155cf6203093 lib/parse/handler.py cfd4857ce17e0a2da312c18dcff28aefaa411f419b4e383b202601c42de40eec lib/parse/headers.py diff --git a/lib/core/ncgui.py b/lib/core/ncgui.py new file mode 100644 index 000000000..e050931a9 --- /dev/null +++ b/lib/core/ncgui.py @@ -0,0 +1,769 @@ +#!/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 GUI " + 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_str = "[X]" if option['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'] 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'] 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 + ) + + # 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 runNcGui(parser): + """Main entry point for ncurses GUI""" + # Check if ncurses is available + if curses is None: + raise SqlmapMissingDependence("missing 'curses' module (try installing 'windows-curses' on Windows)") + + 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) diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index b4d4df7ea..b8f7c4ce7 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -860,6 +860,9 @@ def cmdLineParser(argv=None): parser.add_argument("--gui", dest="gui", action="store_true", help=SUPPRESS) + parser.add_argument("--ncgui", dest="ncgui", action="store_true", + help=SUPPRESS) + parser.add_argument("--smoke-test", dest="smokeTest", action="store_true", help=SUPPRESS) @@ -939,6 +942,13 @@ def cmdLineParser(argv=None): raise SqlmapSilentQuitException + elif "--ncgui" in argv: + from lib.core.ncgui import runNcGui + + runNcGui(parser) + + raise SqlmapSilentQuitException + elif "--shell" in argv: _createHomeDirectories()