GHSA-whv5-4q2f-q68g OpenSTAManager Affected by Remote Code Execution via Insecure Deserialization in OAuth2
DescriptionThe `oauth2.php` file in OpenSTAManager is an **unauthenticated** endpoint (`$skip_permissions = true`). It loads a record from the `zz_oauth2` table using the attacker-controlled GET parameter `state`, and during the OAuth2 configuration flow calls `unserialize()` on the `access_token` field **without any class restriction**.An attacker who can write to the `zz_oauth2` table (e.g., via the arbitrary SQL injection in the Aggiornamenti module reported in [GHSA-2fr7-cc4f-wh98](https://github.com/devcode-it/openstamanager/security/advisories/GHSA-2fr7-cc4f-wh98)) can insert a malicious serialized PHP object (gadget chain) that upon deserialization executes arbitrary commands on the server as the `www-data` user.Affected codeEntry point — `oauth2.php`Deserialization — `src/Models/OAuth2.php``unserialize()` is called without the `allowed_classes` parameter, allowing instantiation of any class loaded by the Composer autoloader.Execution flowThe HTTP response is 500 (due to the `hasExpired()` error), but the command has already executed via `__destruct()` during error cleanup.Full attack chainThis vulnerability is combined with the arbitrary SQL injection in the Aggiornamenti module ([GHSA-2fr7-cc4f-wh98](https://github.com/devcode-it/openstamanager/security/advisories/GHSA-2fr7-cc4f-wh98)) to achieve unauthenticated RCE:**Payload injection** (requires admin account): Via `op=risolvi-conflitti-database`, arbitrary SQL is executed to insert a malicious serialized object into `zz_oauth2.access_token`**RCE trigger** (unauthenticated): A GET request to `oauth2.php?state=<known_value>&code=x` triggers the deserialization and executes the command**Persistence note**: The `risolvi-conflitti-database` handler ends with `exit;` (line 128), which prevents the outer transaction commit. DML statements (INSERT) would be rolled back. To persist the INSERT, DDL statements (`CREATE TABLE`/`DROP TABLE`) are included to force an implicit MySQL commit.Gadget chainThe chain used is **Laravel/RCE22** (available in [phpggc](https://github.com/ambionics/phpggc)), which exploits classes from the Laravel framework present in the project's dependencies:Proof of ConceptExecution**Terminal 1** — Attacker listener:**Terminal 2** — Exploit:Observed result**Listener receives:**
<img width="683" height="286" alt="image" src="https://github.com/user-attachments/assets/89a78f7e-5f23-435d-97ec-d74ac905cdc1" />
The `id` command was executed on the server as `www-data`, confirming RCE.HTTP requests from the exploit**Step 4 — Injection (authenticated):****Step 5 — Trigger (NO authentication):****Response:** HTTP 500 (expected — the error occurs after `__destruct()` has already executed the command)Exploit — `exploit.py`Listener — `listener.py`Impact**Confidentiality**: Read server files, database credentials, API keys**Integrity**: Write files, install backdoors, modify application code**Availability**: Delete files, denial of service**Scope**: Command execution as `www-data` allows pivoting to other systems on the networkProposed remediationOption A: Restrict `unserialize()` (recommended)Option B: Use safe serializationReplace `serialize()`/`unserialize()` with `json_encode()`/`json_decode()` for storing OAuth2 tokens.Option C: Authenticate `oauth2.php`Remove `$skip_permissions = true` and require authentication for the OAuth2 callback endpoint, or validate the `state` parameter against a value stored in the user's session.CreditsOmar Ramirez
CVSS v3.1 score: 7.2