#dokydoky
[CVE-2013-2028] Nginx stack-based buffer overflow(1) - source code 본문
[CVE-2013-2028] Nginx stack-based buffer overflow(1) - source code
dokydoky 2016. 12. 25. 02:48안녕하세요. dokydoky입니다.
Nginx 1.3.9/1.4.0에서 unsigned int와 signed int의 잘못된 Type Conversion으로 stack buffer overflow가 발생될 수 있는 취약점이 있습니다.
2013년 5월에 발표된 취약점(CVE-2013-2028)이지만, 취약점을 공부하는데 좋은 예라고 생각되어 정리해봤습니다.
우선 왜 취약점이 발생하는지 코드를 통해 살펴본 후, Ubuntu x64 환경에 3가지 mitigation(NX, ASLR, STACK CANARY)을 하나씩 추가하면서 어떻게 익스플로잇을 하는지 알아보겠습니다.
* Environment
Github에 Vagrant로 작업환경을 만들어둔 감사한 분이 있어서, 해당 환경을 이용했습니다.(Ubuntu 14.04 LTS 64bit)
하지만, 만들어주신 환경에 gdb 설치가 기본적으로 안되서, apt-get update 명령어를 추가하고,
remote shell을 위해 12345 -> 12345로 포트 포워딩 설정을 추가했습니다.
수정된 버전은 여기 링크에서 받으신 후 "vagrant up" 명령어를 이용해 환경을 뚝딱 만들 수 있습니다.
이하 본문에서는 존칭어를 생략합니다.
0x02. Vulnerable code
먼저 취약점이 발생하는 코드를 확인해보자.
[ngx_http_read_discarded_request_body]
static ngx_int_t
ngx_http_read_discarded_request_body(ngx_http_request_t *r)
{
size_t size;
ssize_t n;
ngx_int_t rc;
ngx_buf_t b;
u_char buffer[NGX_HTTP_DISCARD_BUFFER_SIZE];
...
size = (size_t) ngx_min(r->headers_in.content_length_n,
NGX_HTTP_DISCARD_BUFFER_SIZE);
n = r->connection->recv(r->connection, buffer, size);
...
}
}
size 변수의 타입은 size_t로 unsigned long이며, r->headers_in.content_length_n(이하 content_length_n)는 off_t로 _int_64_t이다.
위 코드에서 content_length_n의 값이 0x8000000000000000와 같은 음수값이라면 ngx_min의 값은 content_length_n의 값이 되지만,
이 값이 Unsigned 타입으로 캐스팅되면서 size는 큰 양수값이 된다.
그러면 recv함수에서 NGX_HTTP_DISCARD_BUFFER_SIZE(4096) 크기의 buffer에 4096 바이트 이상의 문자열을 넣어 BOF를 발생시킬 수 있다.
(r->connection->recv는 ngx_unix_recv함수로 recv함수를 랩핑한 함수다.)
content_length_n의 값은 ngx_http_parse_chunked 함수에서 HTTP body로부터 읽어드리기 때문에, 조작할 수 있는 값이다.(아래 코드에서 ctx->length)
ngx_int_t
ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b,
ngx_http_chunked_t *ctx)
{
...
case sw_chunk_start:
if (ch >= '0' && ch <= '9') {
state = sw_chunk_size;
ctx->size = ch - '0';
break;
}
c = (u_char) (ch | 0x20);
if (c >= 'a' && c <= 'f') {
state = sw_chunk_size;
ctx->size = c - 'a' + 10;
break;
}
goto invalid;
case sw_chunk_size:
if (ch >= '0' && ch <= '9') {
ctx->size = ctx->size * 16 + (ch - '0');
break;
}
c = (u_char) (ch | 0x20);
if (c >= 'a' && c <= 'f') {
ctx->size = ctx->size * 16 + (c - 'a' + 10);
break;
}
...
data:
...
case sw_chunk_size:
ctx->length = 2 /* LF LF */
+ (ctx->size ? ctx->size + 4 /* LF "0" LF LF */ : 0);
break;
case sw_chunk_extension:
case sw_chunk_extension_almost_done:
ctx->length = 1 /* LF */ + ctx->size + 4 /* LF "0" LF LF */;
break;
...
}
0x03. How to Trigger
취약점이 발생하는 지점까지 도달하기 위해 어떤 경로를 거쳐야 하는지 알아보자.
Nginx 서버에 Request를 보내면 1024바이트의 데이터를 읽어들인 후, ngx_http_request_t 구조체에 정보를 저장한다.
그 후, ngx_http_static_handler가 함수가 호출되고 함수 내에서 ngx_http_discard_request_body 함수가 호출된다.
[Overview]
ngx_http_static_handler -> ngx_http_discard_request_body -> ngx_http_discard_request_body_filter->ngx_http_parse_chunked
-> ngx_http_read_discarded_request_body (BOF)
[ngx_http_discard_request_body]
ngx_int_t
ngx_http_discard_request_body(ngx_http_request_t *r)
{
...
if (size || r->headers_in.chunked) {
rc = ngx_http_discard_request_body_filter(r, r->header_in);
if (rc != NGX_OK) {
return rc;
}
if (r->headers_in.content_length_n == 0) {
return NGX_OK;
}
}
// vulnerable function
rc = ngx_http_read_discarded_request_body(r);
...
}
ngx_http_discard_request_body 함수에서, chunked(Transfer-Encoding: chunked) Request면 ngx_http_discard_request_body_filter를 호출한다.
위 코드에서 ngx_http_read_discarded_request_body는 취약점이 발생하는 함수다.
이 취약한 함수가 호출되기 위해서는 (rc == NGX_OK) && (r->headers_in.content_length_n != 0) 이여야 한다.
[ngx_http_discard_request_body_filter]
static ngx_int_t
ngx_http_discard_request_body_filter(ngx_http_request_t *r, ngx_buf_t *b)
{
...
rc = ngx_http_parse_chunked(r, b, rb->chunked);
if (rc == NGX_OK) {
...
}
if (rc == NGX_DONE) {
/* a whole response has been parsed successfully */
r->headers_in.content_length_n = 0;
break;
}
if (rc == NGX_AGAIN) {
/* set amount of data we want to see next time */
r->headers_in.content_length_n = rb->chunked->length;
break;
}
...
}
취약점이 발생되는 코드(ngx_http_read_discarded_request_body)에서 content_length_n는 rc가 NGX_AGAIN일 때, rb->chunked->length 값이 입력된다.
따라서, ngx_http_parse_chunked의 리턴값은 NGX_AGAIN이 되어야 한다.
그리고 rb->chunked->length는 ngx_http_parse_chunked 함수내에서 변경된다.
ngx_int_t
ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b,
ngx_http_chunked_t *ctx)
{
...
rc = NGX_AGAIN;
for (pos = b->pos; pos < b->last; pos++) {
ch = *pos;
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"http chunked byte: %02Xd s:%d", ch, state);
switch (state) {
case sw_chunk_start:
if (ch >= '0' && ch <= '9') {
state = sw_chunk_size;
ctx->size = ch - '0';
break;
}
c = (u_char) (ch | 0x20);
if (c >= 'a' && c <= 'f') {
state = sw_chunk_size;
ctx->size = c - 'a' + 10;
break;
}
goto invalid;
case sw_chunk_size:
if (ch >= '0' && ch <= '9') {
ctx->size = ctx->size * 16 + (ch - '0');
break;
}
c = (u_char) (ch | 0x20);
if (c >= 'a' && c <= 'f') {
ctx->size = ctx->size * 16 + (c - 'a' + 10);
break;
}
if (ctx->size == 0) {
switch (ch) {
case CR:
state = sw_last_chunk_extension_almost_done;
break;
case LF:
state = sw_trailer;
break;
case ';':
case ' ':
case '\t':
state = sw_last_chunk_extension;
break;
default:
goto invalid;
}
break;
}
switch (ch) {
case CR:
state = sw_chunk_extension_almost_done;
break;
case LF:
state = sw_chunk_data;
break;
case ';':
case ' ':
case '\t':
state = sw_chunk_extension;
break;
default:
goto invalid;
}
break;
case sw_chunk_extension:
switch (ch) {
case CR:
state = sw_chunk_extension_almost_done;
break;
case LF:
state = sw_chunk_data;
}
break;
...
}
}
data:
ctx->state = state;
b->pos = pos;
switch (state) {
...
case sw_chunk_extension:
case sw_chunk_extension_almost_done:
ctx->length = 1 /* LF */ + ctx->size + 4 /* LF "0" LF LF */;
break;
...
}
...
}
ngx_http_parse_chunked 함수에서는 for문을 돌면서 초기에 1024바이트 읽어들인 Request의 body부분을 읽어들인다.
(초기 상태는 sw_chunk_start로 시작)
이 함수에서 NGX_AGAIN을 리턴하고, ctx->length의 값에 음수의 값을 넣기 위해서는 HTTP body 처음 부분에 0x8000000000000000을 넣고,
';', ' ', '\t' 문자 중 하나를 넣은 후, 1024바이트의 나머지 부분을 CR(\x0d), LF(\x0a)이 아닌 값으로 채워주면 된다.
위 함수에서 NGX_AGAIN이 리턴되면, ngx_http_discard_request_body_filter 함수에서 아래 코드가 실행되고,
"0x02. Vulnerable code" 에서 살펴본 것처럼 ngx_http_read_discarded_request_body 함수에서 취약점이 발생한다.
r->headers_in.content_length_n = rb->chunked->length;
결과적으로 BOF를 발생시킬 수 있는 Request의 형태는 아래와 같다.
chunked 옵션, body의 첫부분에 16진수 형태의 음수 length값, space, 나머지 1024바이트를 채우기 위한 문자열.
GET / HTTP/1.1
Host: 127.0.0.1:8080
Transfer-Encoding: chunked
8000000000000000 AAAAAAAAAAAAAAAAAAAAAAA...AAAAA...
이번 편에서는 NGINX소스에서 취약점이 발생되는 부분을 소스코드에서 살펴봤습니다.
전체적인 코드를 보고 싶으신 분은 Github에서 Nginx 1.4.0의 full code를 보면서 위에서 설명한 부분 위주로 다시 살펴보시기 바랍니다.
다음 편에서는 mitigation중 NX만 적용되어 있는 환경에서 어떻게 익스플로잇하여 remote shell을 얻는지 알아보겠습니다.
'System Hacking' 카테고리의 다른 글
[CVE-2013-2028] Nginx stack-based buffer overflow(3) - NX, ASLR (0) | 2016.12.26 |
---|---|
[CVE-2013-2028] Nginx stack-based buffer overflow(2) - NX (0) | 2016.12.25 |
[Shellcode] pwntools을 이용한 shellcode 만들기 (0) | 2016.12.20 |
[Remote exploit] Remote shell을 얻는 방법.(Xinetd, standalone) (0) | 2016.12.20 |
Stack Buffer OverFlow (1) | 2011.08.24 |