UMGUM.COM 

Тестирование аутентификации ( Пишем простой сервис выявления доступных методов аутентификации пользователя посредством web-интерфейса. )

11 августа 2019  (обновлено 24 октября 2019)

OS: "Linux Debian 8/9 (Jessie/Stretch)", "Linux Ubuntu 16/18 (Xenial/Bionic) LTS".
Application: "Freeradius v3", "Nginx", "Inetd", "Bash", "EAP Testing".

Задача: создание пользовательского инструмента с web-интерфейсом, посредством которого можно выявлять доступные способы аутентификации через сервер "Freeradius", в данном случае обслуживающий сервис "Eduroam".

В качестве терминатора интерфейса воспользуемся web-сервером "Nginx". Фоновые процедуры возложим на спарку "Inetd + Bash", написав простейший обработчик запросов. Тестирование аутентификации как таковой потребует применения специализированной утилиты "EAP Testing".

В итоге web-интерфейс, состоящий из двух последовательных частей, будет выглядеть примерно так:

размер: 320 400 640 800 1024 1280
Пример web-интерфейса тестирования доступных методов аутентификации.
Пример web-интерфейса тестирования доступных методов аутентификации.


Исходим от того, что конфигурация сервера аутентификации "Freeradius" соответствует приведённой в располагающейся рядом инструкции.

Установка утилиты "EAP Testing".

В репозиториях основных дистрибутивов утилита "eapol_test" отсутствует, так что придётся её установить вручную.

Предварительно установим необходимые для сборки системные библиотеки и утилиты:

# aptitude install --without-recommends git libssl-dev devscripts pkg-config libnl-3-dev libnl-genl-3-dev

Загружаем исходный код утилиты, собираем её и размещаем в подходящем месте файловой системы:

# cd /usr/src
# git clone --depth 1 --no-single-branch https://github.com/FreeRADIUS/freeradius-server.git
# cd freeradius-server/scripts/travis/ && ./eapol_test-build.sh
# cp ./eapol_test/eapol_test /usr/local/bin/

Настройка точки подключения к "Freeradius".

Чтобы утилита "EAP Testing" могла обращаться к "Freeradius" необходимо добавить в конфигурацию такового описание точки подключения в роли клиента (вроде WiFi-AP), с той лишь разницей, что разрешено оно будет только через "локальную сетевую петлю":

# vi /etc/freeradius/3.0/clients.conf

....
# Точка входа для web-клиента тестирования поддерживаемых методов аутентификации
client web_eapol_test {
  ipaddr = 127.0.0.1
  secret = strongPassword
  require_message_authenticator = yes
  nas_type = other
  virtual_server = outer-example-eduroam
}
....

Проверяем корректность конфигурации и применяем её, перезагружая сервис:

# freeradius -C -X && systemctl restart freeradius

Установка и преднастройка "Inetd".

Вначале для запуска bash-скрипта, посредством которого осуществляется тестирование аутентификации, была применена обёртка "fcgiwrap", но в процессе наладки функционала выяснилось, что она в принципе не поддерживает порционный обмен данными между web-сервером и клиентом - а мне хотелось не заставлять пользователя ждать, пока пройдут все тесты, информируя его о прохождении таковых поэтапно. Для этого наилучшим образом подходил запуск скрипта через супер-сервис "inetd" и выдача ответов клиенту посредством механизма "chunked transfer encoding".

Устанавливаем простейший "inetd", игнорируя его наследников "xinetd" и "rlinetd" - их функциональность для решения задачи не требуется:

# aptitude install openbsd-inetd

Анонсируем используемый "inetd" сетевой порт в перечне известных несущей операционной системе:

# vi /etc/services

....
# Local services
eg-auth-test 8889/tcp # Eduroam Gateway Auth Test

Регистрируем inetd-сервис, принимающий сетевые подключения (только на "локальной петле") и передающие их указанному bash-скрипту (его наличие обязательно, так что лучше его создать заранее, хоть бы и пустым):

# vi /etc/inetd.conf

....
#:OTHER: Other services

127.0.0.1:eg-auth-test stream tcp nowait www-data /var/www/cgi-bin/auth-test/index.cgi

# /etc/init.d/openbsd-inetd restart

Проверяем состояние сервиса и прослушиваемого сетевого порта:

# /etc/init.d/openbsd-inetd status
# netstat -apn | grep -i inetd

Установка и настройка web-сервера "Nginx".

Устанавливаем терминирующий интерфейс web-сервер:

# aptitude install nginx-light

Заранее заготовим директорию для SSL-сертификатов - без шифрования сеанса связи работать с паролями пользователь недопустимо, разумеется:

# mkdir -p /etc/ssl/nginx && chown -R root:root /etc/ssl/nginx
# openssl dhparam -out /etc/ssl/nginx/dhparam.2048.pem 2048

Небольшая преднастройка сервиса:

# vi /etc/nginx/nginx.conf

user www-data www-data;
worker_processes auto;
....
http {
  ....
  # Велим Nginx не выдавать сведения о номере своей версии
  server_tokens off;
....

Описываем конфигурацию web-сайта, посредством взаимодействия с которым будет осуществляться тестирование:

# vi /etc/nginx/sites-available/eduroam.example.net.conf

# Во избежание возможного DDoS ограничиваем число конкурентных соединений к сервису
upstream eduroam-auth-test {
  server 127.0.0.1:8889 max_conns=24;
}
....

# Налаживаем перенаправление всех запросов в HTTPS
server {
  listen 80;
  server_name eduroam.example.net;
  location / {
    rewrite ^ https://eduroam.example.net$request_uri permanent;
  }
}

# Описание web-сайта как такового, с правилами проксирования запросов
server {
  listen 443 ssl http2;
  server_name eduroam.example.net;
  ....

  # Явно указываем обслуживать здесь только SSL/TLS подключения
  ssl on;

  # Описываем параметры установления соединений SSL/TLS
  ssl_dhparam /etc/ssl/nginx/dhparam.2048.pem;
  ssl_certificate /etc/ssl/nginx/eduroam.example.net.crt;
  ssl_certificate_key /etc/ssl/nginx/eduroam.example.net.key.decrypt;
  ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers on;

  # Обрабатываем обращения без явного указания имени скрипта
  location ~ ^/auth-test {

    # Указываем корневую директорию ресурса
    root /var/www/cgi-bin;
    index index.cgi;

    # Отлавливаем все обращения к скриптам и проксируем их в Inetd-Bash
    location ~ ^/auth-test/.*\.cgi$ {

      # Не сжимаем обращения к скриптам
      gzip off;

      # Задаём параметры проксирования трафика
      proxy_pass http://eduroam-auth-test;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # Отключаем по возможности буферизацию, чтобы порционные данные пролетали немедленно
      proxy_request_buffering off;
      proxy_buffering off;

      # Переключаемся на проксирование посредством современного HTTP/1.1
      # (это требуется для поддержки "Transfer-Encoding: chunked")
      proxy_http_version 1.1;
    }
  }
}

Проверяем корректность конфигурации и применяем таковую:

# nginx -t && /etc/init.d/nginx reload

Учитывая то, что в конфигурации "Nginx" выше мы ограничили количество конкурентных запросов к сервису проверки аутентификации определённым числом, при превышении лимита пользователи будут получать ответ сервера "502 Bad Gateway". Изящнее было бы в таком случае отдавать специальную страничку с информацией о временной недоступности сервиса.

Пишем фоновый сервис тестирования методов аутентификации.

Выше мы установили всё необходимое программное обеспечение и осталось самое "простое" - написать bash-скрипт, принимающий данные пользователя через web-интерфейс, вызывающий утилиту "eapol_test", аккумулирующий ответы таковой и отдающий результаты пользователю в оформленном виде.

Заготавливаем место для скрипта (он будет вызываться не через CGI, но не будем множить директории):

# mkdir -p /var/www/cgi-bin/auth-test
# chown www-data:www-data /var/www/cgi-bin/auth-test && chmod o-rwx /var/www/cgi-bin/auth-test

Пишем bash-скрипт, функционал которого прокомментирован внутри:

# vi /var/www/cgi-bin/auth-test/index.cgi && chmod +x /var/www/cgi-bin/auth-test/index.cgi && chown www-data:www-data /var/www/cgi-bin/auth-test/index.cgi && chmod o-rwx /var/www/cgi-bin/auth-test/index.cgi

#!/bin/bash

# Формируем массив с параметрами разных методов аутентификации
#
EAP="TTLS"
PHASE2="auth=MSCHAPV2"
DESC="EAP-TTLS / MSCHAPv2"
#
EAP[1]="TTLS"
PHASE2[1]="autheap=MSCHAPV2"
DESC[1]="EAP-TTLS / EAP-MSCHAPv2"
#
EAP[2]="TTLS"
PHASE2[2]="autheap=GTC"
DESC[2]="EAP-TTLS / EAP-GTC"
#
EAP[3]="TTLS"
PHASE2[3]="auth=PAP"
DESC[3]="EAP-TTLS / PAP"
#
EAP[4]="PEAP"
PHASE2[4]="auth=MSCHAPV2"
DESC[4]="EAP-PEAP / MSCHAPv2"
#
EAP[5]="PEAP"
PHASE2[5]="autheap=MSCHAPV2"
DESC[5]="EAP-PEAP / EAP-MSCHAPv2"
#
EAP[6]="PEAP"
PHASE2[6]="autheap=GTC"
DESC[6]="EAP-PEAP / EAP-GTC"
#
#EAP[7]="TTLS"
#PHASE2[7]="autheap=MD5"
#DESC[7]="EAP-TTLS / (unsafe) EAP-MD5"
#
#EAP[8]="PEAP"
#PHASE2[8]="autheap=MD5"
#DESC[8]="EAP-PEAP / (unsafe) EAP-MD5"
#
#EAP[9]=MSCHAPv2
#DESC[9]="(unsafe) EAP-MSCHAPv2"
#
#EAP[10]=MD5
#DESC[10]="(unsafe) EAP-MD5"

# --- #

# Объявляем простую bash-функцию конвертирования экранированной URL-style строки в чистый текст
urldecode() { : "${*//+/ }"; echo -e "${_//%/\\x}"; }

# Первым делом читаем прикреплённые к запросу заголовки
while read -t 0.01 HEADER ; do
  # Для простоты детектирования пустых строк отрезаем у таковых управляющие символы "переноса строки (carriage return)"
  HEADER="${HEADER//$'\r'}"

  # Прерываем чтение на первой пустой строке (разделителе блока заголовков и тела запроса)
  [ -z "${HEADER}" ] && break

  # Выясняем длину прикреплённого POST-запроса
  [ ! -z "$(echo ${HEADER} | grep -i 'content-length')" ] && { POST_LENGTH=$(echo ${HEADER} | awk -F ':' '{print $2}' | tr -d '[:space:]'); }
done

# Вторым проходом читаем прикреплённые к запросу POST-данные
# (ожидается Plain-Text)
[ ! -z "${POST_LENGTH}" ] && read -N ${POST_LENGTH} -t 0.01 IN_POST_STRING

# Вычленяем значения возможно объявленных ожидаемых переменных
for I in $(urldecode $IN_POST_STRING | tr '&' '\n') ; do
  VAR_PAIR=( $(echo ${I} | tr '=' '\n') )
  [[ -z "${USER_NAME}" && "${VAR_PAIR[0]}" == "user_name" ]] && USER_NAME="${VAR_PAIR[1]}"
  [[ -z "${USER_PASSWORD}" && "${VAR_PAIR[0]}" == "user_password" ]] && USER_PASSWORD="${VAR_PAIR[1]}"
done

# --- #

# Первый обязательный заголовок, включающий режим работы по современому протоколу HTTP/1.1
# (это требуется для поддержки "Transfer-Encoding: chunked")
echo "HTTP/1.1 200 OK"

# Специальным заголовком указываем вышестоящему прокси не буферизировать данные
echo "X-Accel-Buffering: no"

# Указываем тип выдаваемых данных (это важно)
# (именно для "text/html" браузеры дорисовывают страницу с получением каждой порции данных)
echo "Content-type: text/html"

# Включаем режим порционной передачи данных и явного закрытия соединения по завершению работы
echo "Transfer-Encoding: chunked"
echo "Connection: close"

# Выдаём обязательный разделяющий перенос строки между заголовками и последующим потоком данных
echo ""

# --- #

# Формируем первый выдаваемый блок данных
BLOCK="<!DOCTYPE HTML>\n\
<html>\n\
<head>\n\
  <meta charset=\"utf-8\">\n\
  <font face=\"sans-serif\">\n\
  <title>Eduroam authentication testing</title>\n\
  <style>\n\
    a {text-decoration: none;}\n\
    a:visited {color: blue;}\n\
    .pole {border: #C0C0C0 1px solid; text-align: left; padding: 4pt;}\n\
    .button {border: #C0C0C0 1px solid; padding: 4pt;}\n\
    table {text-align: left; border-collapse: collapse; margin: 0pt; padding: 0pt;}\n\
    tr {border: none; margin: 0pt; padding: 0pt;}\n\
    th {background-color: #F5F5F5; border: #C0C0C0 1px solid; text-align: center; font-size: 90%; margin: 0pt; padding-left: 6pt; padding-top: 4pt; padding-right: 6pt; padding-bottom: 4pt;}\n\
    td {border: #C0C0C0 1px solid; text-align: left; vertical-align: top; margin: 0pt; padding-left: 6pt; padding-top: 4pt; padding-right: 6pt; padding-bottom: 4pt;}\n\
  </style>\n\
</head>\n\
<body>\n\
<h2 style=\"color: #333333;\">Eduroam authentication testing (via network Example)</h2>\n\
\n"

# Вычисляем длину строки блока данных в байтах и переводим число в требуемый для "chunked transfer encoding" шестнадцатеричный формат
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1

# Отдаём клиенту первый блок данных, предваряя его указанием длины
# (важно разделять параметры длины и блоки данных только и только последовательностями "\r\n")
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK

# --- #

if [[ -z "${USER_NAME}" || -z "${USER_PASSWORD}" ]] ; then

  # В случае отсутствия логина и пароля на входе выдаём запрос на ввод таковых
  BLOCK=${BLOCK}"<h3 style=\"color: #333333;\">Verification of authentication methods for the following username and password:</h3>\n"
  BLOCK=${BLOCK}"<form action=\"/auth-test/index.cgi\" method=\"post\" enctype=\"application/x-www-form-urlencoded\">"
  BLOCK=${BLOCK}"<table style='font-size: 12pt'>\n"
  BLOCK=${BLOCK}"<tr>\n"
  BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt; vertical-align: middle;\">Username:</td>\n"
  BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt;\"><input class=\"pole\" type=\"text\" name=\"user_name\" size=\"24\" spellcheck=\"off\" autocapitalize=\"off\" autocorrect=\"off\" placeholder=\"username@domain.ltd\" tabindex=\"1\" required /></td>\n"
  BLOCK=${BLOCK}"<td rowspan=\"2\" style=\"border: none; text-align: center; vertical-align: middle;\"><input class=\"button\" type=\"submit\" value=\"Check!\" tabindex=\"3\" /></td>\n"
  BLOCK=${BLOCK}"</tr>\n"
  BLOCK=${BLOCK}"<tr>\n"
  BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt; vertical-align: middle;\">Password:</td>\n"
  BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt;\"><input class=\"pole\" type=\"password\" name=\"user_password\" size=\"24\" autocomplete=\"off\" spellcheck=\"off\" autocapitalize=\"off\" autocorrect=\"off\" placeholder=\"xxxxxxxxxxxx\" tabindex=\"2\" required /></td>\n"
  BLOCK=${BLOCK}"</tr>\n"
  BLOCK=${BLOCK}"</table>\n"
  BLOCK=${BLOCK}"</form>\n"
  printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
  echo -en "${LENGTH}\r\n"
  echo -en "${BLOCK}\r\n" 2>/dev/null
  unset BLOCK

else

  # Профилактически создаём директорию для временных файлов
  mkdir -p /var/tmp/eg-auth-test

  # Зачищаем возможно осиротевшие при внезапном прерывании процедуры временные файлы
  find /var/tmp/eg-auth-test -type f -mmin +3 -print0 | xargs --null --no-run-if-empty rm --force

  # Выводим заготовку таблицы результатов
  BLOCK=${BLOCK}"(&nbsp;<a href=\"/auth-test/\">new check</a>&nbsp;)<br />\n"
  BLOCK=${BLOCK}"<h3 style=\"color: #333333;\">Validation results of available for \"${USER_NAME}\" authentication methods (${#EAP[@]} options):</h3>\n"
  BLOCK=${BLOCK}"<table style='font-size: 12pt'>\n"
  BLOCK=${BLOCK}"<tr>\n"
  BLOCK=${BLOCK}"<th>#</th><th>EAP method / Auth type</th><th>Result</th>\n"

  BLOCK=${BLOCK}"</tr>\n"
  printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
  echo -en "${LENGTH}\r\n"
  echo -en "${BLOCK}\r\n" 2>/dev/null
  unset BLOCK

  # Перебираем все вышеперечисленные варианты методов аутентификации
  for I in "${!EAP[@]}" ; do

    # Создаём временный файл
    TMPF=$(mktemp --tmpdir=/var/tmp/eg-auth-test)

    # Формируем временный конфигурационный файл
    cat << EOF > "${TMPF}"
network={
  key_mgmt=WPA-EAP
  eap=${EAP[$I]}
  phase2="${PHASE2[$I]}"
  identity="${USER_NAME}"
  password="${USER_PASSWORD}"
}
EOF

    # Рисуем строку таблицы для этапа тестирования
    BLOCK=${BLOCK}"<tr>\n"
    BLOCK=${BLOCK}"<td>$(echo $(( ${I} + 1 )))</td>"
    BLOCK=${BLOCK}"<td>$(echo ${DESC[$I]})</td>"
    printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
    echo -en "${LENGTH}\r\n"
    echo -en "${BLOCK}\r\n" 2>/dev/null
    unset BLOCK

    # Осуществляем тестирование аутентификации и вставляем результаты в таблицу
    eapol_test -c ${TMPF} -a 127.0.0.1 -p 1812 -s strongPassword -r0 -t10 > /dev/null 2>&1
    if [ "${?}" -eq "0" ] ; then
      BLOCK=${BLOCK}"<td style=\"color: green;\">SUCCESS</td>"
    else
      BLOCK=${BLOCK}"<td style=\"color: red;\">FAILURE</td>"
    fi
    BLOCK=${BLOCK}"</tr>\n"
    printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
    echo -en "${LENGTH}\r\n"
    echo -en "${BLOCK}\r\n" 2>/dev/null
    unset BLOCK

    # Зачищаем среду тестирования для следующей итерации
    unset EAP[$I]
    unset PHASE2[$I]
    unset DESC[$I]
    rm -f "${TMPF}"

  done
  unset I

  # После исполнения всех тестов закрываем таблицу
  BLOCK=${BLOCK}"</td>\n"
  BLOCK=${BLOCK}"</tr>\n"
  BLOCK=${BLOCK}"</table>\n"
  BLOCK=${BLOCK}"<br /><span style=\"font-size: 90%; color: #808080;\">Checking time: "$(date +'%Y-%m-%d %H:%M')"</span>\n"
  printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
  echo -en "${LENGTH}\r\n"
  echo -en "${BLOCK}\r\n" 2>/dev/null
  unset BLOCK

fi

# Завершаем отрисовку HTML-страницы
BLOCK=${BLOCK}"</body>\n</html>"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK

# Следуя требованиям протокола завершаем сеанс передачи блоком данных нулевой длинны
echo -en "0\r\n"
echo -en "\r\n"

exit ${?}

Пояснения к особенностям реализации.

Несмотря на простоту решения без проблем не обошлось. К сожалению, как я ни бился, примерно в половине случаев связь с "inetd/xinetd" обрывалась, если подключение устанавливалось извне, с выдачей следующего сообщения:

....
* Recv failure: Connection reset by peer
* stopped the pause stream!
* Closing connection 0
curl: (56) Recv failure: Connection reset by peer

Наблюдение за самим приложением "inetd/xinetd" посредством утилиты "strace" не показало никакой разницы между поведением сервиса при успешном завершении обработки запроса или его сбое - это явно не на уровне приложения случается.

Наблюдение посредством "tcpdump" и "wireshark" выявили неожиданные непредсказуемые посылки несущей web-сервис операционной системой сигнала RST без видимой на то причины. Причину понять не удалось.

Единственная рабочая схема получилась с запуском "inetd/xinetd" на "локальной сетевой петле", обращения к которой шли через фронтальный прокси, в роли которого я использовал "Nginx". В этой конфигурации схема работала безупречно - тестирование и практика не выявили сбоев.

Способы отладки сервиса.

На этапе наладки функциональности bash-скрипта обращаться к спарке "Inetd + Bash" проще без лишних посредников, напрямую в порт "Inetd", с "локальной петли":

$ curl -N -vvv --request POST -H "Content-Type: application/x-www-form-urlencoded" --data "user_name=username@example.net&user_password=userPassword" 127.0.0.1:8889

Чтобы яснее представить, что посылается в сетевой порт, можно запустить "netcat" и отправить в него POST-запрос:

$ nc -l 8888
$ curl --request POST -H "Content-Type: application/x-www-form-urlencoded" --data "qwerty=12345&asd=7890" 127.0.0.1:8888

В результате хорошо видна структура передаваемых данных, с разделением на заголовки и тело POST-запроса:

POST / HTTP/1.1
Host: 127.0.0.1:8888
User-Agent: curl/7.58.0
Accept: */*
Content-Type: application/x-www-form-urlencoded
Content-Length: 21

qwerty=12345&asd=7890


Заметки и комментарии к публикации:


Оставьте свой комментарий ( выразите мнение относительно публикации, поделитесь дополнительными сведениями или укажите на ошибку )