Некоторое время назад возникло желание реинкарнировать свой Wordpress-блог. Параллельно возникло желание упорядочить и систематизировать накопленные знания для сдачи экзамена ECSA. Все это привело меня к развертыванию блога на отдельно стоящем сервере. Через некоторый промежуток времени ожидаемо возникли вопросы безопасности сайта, использующего один из самых популярных (потому и вечно уязвимых) движков.
В результате изысканий появилось это руководство по организации непрерывного сканирования сайта на уязвимости, которым и спешу поделиться с вами, дорогие читатели.
Большую часть материала можно использовать в том числе и для внедрения в CI/CD пайплайны.
Прежде всего нужно оценить границы проекта и ресурсы, которые мы готовы на это затратить. В данном конкретном случае нет задачи объять необъятное, кроме того бюджет проекта околонулевой. Планируемое регулярное сканирование должно выполнять функцию беглого взгляда, с помощью которого мы понимаем, как выглядит сайт со стороны мимопроходящих товарищей со злыми намерениями. Просто чтобы не выполнять эти проверки ежедневно вручную.
Нам понадобятся:
- сторонний сервер на базе ОС Linux для выполнения заданий по расписанию;
- навыки написания Bash-скриптов и использования командной строки Linux.
Настраиваем автоматическое сканирование сайта сканером OWASP ZAP
Ввиду постановки задачи возьмем не полнофункциональный ZAP, а облегченный скрипт ZAP Baseline Scan. Запускать будем из образа Docker: это удобно, стильно, модно, молодёжно.
Есть несколько вариантов образа:
- owasp/zap2docker-bare — минимальный образ, содержащий только необходимые зависимости (по заверениям OWASP, идеально подходит для интеграции с CI);
- owasp/zap2docker-weekly — еженедельная сборка (которая почему то всегда «Updated a month ago»);
- owasp/zap2docker-stable — наисвежайший стабильный образ;
- owasp/zap2docker-live — наисвежайший, возможно, нестабильный образ.
Так как у меня есть некоторая свобода выбора и оперативный простор для экспериментов, я выбрал owasp/zap2docker-live (а вдруг повезет, и я исправлю какой-нибудь баг). Для более серьезных и денежных проектов, конечно стоит выбрать стабильную версию.
Эмпирическим путем были подобраны оптимальные параметры запуска (более подробно можно почитать в Wiki проекта здесь и здесь):
docker run -v /tmp/zap/:/zap/wrk/:rw -t owasp/zap2docker-live zap-baseline.py -t https://blog.tyutin.net/ru/ -j -a -m 5 -r blog_tyutin_net-$(date "+%Y-%m-%d").html -J blog_tyutin_net-$(date "+%Y-%m-%d").json
Досконально разберем её опции:
- docker run — сканирование запускается в Docker — удобно, модно, стильно, молодёжно;
- -v /tmp/zap/:/zap/wrk/:rw — монтируем каталог для сохранения файлов отчетов;
- -t — предоставляем сканеру терминал для вывода информации на экран;
- owasp/zap2docker-live — образ, который будет использован для сканирования, live содержит самые свежие обновления;
- zap-baseline.py — непосредственно скрипт сканирования;
- -j — запуск ajax-паука: пусть бегает, жалко что ли;
- -m 5 — даем пауку 5 минут на то, чтобы обежать сайт (по умолчанию это значение равно 1);
- -r blog_tyutin_net-$(date «+%Y-%m-%d»).html — сохранение отчета в html-файл, включив в имя файла название сайта и дату сканирования (этот файл можно скинуть в Telegram или отправить по электронной почте);
- -J blog_tyutin_net-$(date «+%Y-%m-%d»).json — сохранение отчета в html-файл, включив в имя файла название сайта и дату сканирования (этот файл удобно парсить для передачи в Telegram консолидированной информации).
В результате выполнения этой команды мы получим довольно интересный отчёт, который покажет нам проблемы безопасности, которые по мнению OWASP ZAP имеются на сайте:
Вот так выглядит отчёт в консоли...
Проблема этого отчета в том, что он большой. Необходимость ежедневно вчитываться в него убьёт все желание это делать. Поэтому переходим к следующему разделу.
Консолидируем данные из отчета OWASP ZAP
Как было обозначено ранее, результаты сканирования сохраняются в виде html и json. Для получения консолидированных данных хорошо подходит формат json, а с помощью великолепной утилиты jq мы можем манипулировать json-объектами всеми мыслимыми и немыслимыми способами.
Для регулярного обозрения и понимания ситуации с безопасностью сайта в нашем конкретном случае будет достаточно отформатированного списка проблем, упорядоченного по степени критичности в порядке убывания.
С помощью вот этого набора команд мы успешно сожмем наш json до варианта, позволяющего понять суть одним беглым взглядом:
cat blog_tyutin_net--$SCANDATE.json | \
jq -c '.site[].alerts[]' | \
jq -r -s -c 'sort_by(.riskcode, .confidence)| reverse | .[] | "(.riskdesc)\t|\t(.alert)"')
Вывод команды в консоли получается таким:
Medium (High) | Sub Resource Integrity Attribute Missing
Medium (Medium) | Source Code Disclosure - ActiveVFP
Medium (Medium) | Source Code Disclosure - PHP
Low (High) | In Page Banner Information Leak
Low (High) | Server Leaks Version Information via "Server" HTTP Response Header Field
Low (High) | Server Leaks Version Information via "Server" HTTP Response Header Field
Low (Medium) | Cookie No HttpOnly Flag
Low (Medium) | Cookie Without SameSite Attribute
Low (Medium) | Content Security Policy (CSP) Header Not Set
Low (Medium) | X-Content-Type-Options Header Missing
Low (Medium) | Feature Policy Header Not Set
Low (Medium) | Incomplete or No Cache-control and Pragma HTTP Header Set
Low (Medium) | Absence of Anti-CSRF Tokens
Informational (Medium) | Modern Web Application
Informational (Medium) | Storable but Non-Cacheable Content
Informational (Medium) | Base64 Disclosure
Informational (Medium) | Storable and Cacheable Content
Informational (Medium) | Storable and Cacheable Content
Informational (Low) | Charset Mismatch
Informational (Low) | User Controllable HTML Element Attribute (Potential XSS)
Informational (Low) | Timestamp Disclosure - Unix
Informational (Low) | Information Disclosure - Suspicious Comments
Передаём отчет о сканировании OWASP ZAP в Telegram
Подопытным кроликом у нас выступает не корпоративный ресурс, и даже не стартап-проект, а простой личный блог, поэтому мы не будем выполнять интеграцию с Jira (хотя по приведенным здесь мануалам это вполне можно сделать), а просто будем отправлять результаты проверки в Telegram.
Вопрос создания Telegram бота в интернетах обмусолен уже давно, поэтому обойдём его стороной и приступим к делу. В данном проекте нас интересуют ежедневное получение консолидированной информации и наличие возможности сразу же увидеть дополнительные сведения по заинтересовавшему моменту.
Поэтому после каждого сканирования мы будем отправлять два сообщения:
- обзорная информация по обнаружениям;
- подробный html-отчет в виде файла.
Скрипт отправки в Telegram текстового сообщения:
#!/bin/bash
TGCHATID="$1"
TGMESSAGE="$2"
TGTOKEN="$3"
# Send message to TG chat
curl -m 20 -s \
--header 'Content-Type: application/json' \
--request 'POST' \
--data "{\"disable_web_page_preview\":true,\"parse_mode\":\"Markdown\",\"chat_id\":\"${TGCHATID}\",\"text\":\"${TGMESSAGE}\"}" "https://api.telegram.org/bot${TGTOKEN}/sendMessage" \
1 > /dev/null
Скрипт отправки в Telegram файла отчета OWASP ZAP:
#!/bin/bash
TGCHATID="$1"
FILETOSEND="$2"
TGTOKEN="$3"
# Send file to TG chat
curl -m 20 \
-F "chat_id=${TGCHATID}" \
-F document=@${FILETOSEND} \
https://api.telegram.org/bot${TGTOKEN}/sendDocument
Собственно, на этом мы закончили изготовление кирпичей. Пора сложить из них стену.
Запускаем continuous security scanning нашего сайта
В листинге приведен итоговый скрипт, готовый для использования в cron. Параметры запуска скрипта:
- -target — начальный URL для сканирования OWASP ZAP;
- -resdir — каталог для хранения отчетов о сканировании.
В логике скрипта предполагается сканирование сайта один раз в сутки. Повторный запуск при наличии отчетов сканирования за «сегодня» просто отправит нам данные из этих отчетов. И вот как это будет выглядеть:
HTML-отчет на экране мобильного устройства смотрится довольно прилично:
А вот и виновник торжества — скрипт автоматического пассиного сканирования сайта на уязвимости:
#!/bin/bash
# -----------------------------------------
# ------ Get input parameters ---------
# -----------------------------------------
for i in "$@"
do
case $i in
-target=*)
TARGET="${i#*=}"
shift
;;
-resdir=*)
RESDIR="${i#*=}"
shift
;;
esac
done
# -----------------------------------------
# -----------------------------------------
# ------ Check input parameters ---------
# -----------------------------------------
if [[ -z $TARGET ]]; then
echo "-target parameter is not set. Exiting." && exit
fi
if [[ -z $RESDIR ]]; then
echo "-resdir parameter is not set. Exiting." && exit
fi
if [[ ! -d ${RESDIR} ]]; then
echo "Path ${RESDIR} does not exist. Creating..."
mkdir -p $RESDIR
chown 1000:1000 $RESDIR
if [ $? -ne 0 ]; then
echo "Error creating ${RESDIR} directory. Exiting."
exit
fi
fi
# -----------------------------------------
TGTOKEN=```PUT_YOUR_TOKEN_HERE```
TGCHATID=```PUT_YOUR_ID_HERE```
SCANDATE=$(date "+%Y-%m-%d")
SCANFILE=$(echo $TARGET | sed -e 's|/|_|g' | sed -e 's|:|_|g' | sed -e 's|\.|_|g')
# -----------------------------------------
# ------ Perform scan ---------
# -----------------------------------------
if [[ ! -f $RESDIR/${SCANFILE}-${SCANDATE}.json ]]; then
docker run \
-v $RESDIR/:/zap/wrk/:rw \
-t owasp/zap2docker-live \
zap-baseline.py \
-t $TARGET \
-j -a -m 5 \
-r $SCANFILE-$SCANDATE.html \
-J $SCANFILE-$SCANDATE.json
fi
# -----------------------------------------
# -----------------------------------------
# ------ Interpret results ---------
# -----------------------------------------
RESULT=$(cat $RESDIR/$SCANFILE-$SCANDATE.json | \
jq -c '.site[].alerts[]' | \
jq -r -s -c 'sort_by(.riskcode, .confidence)| reverse | .[] | "\(.riskdesc)\t|\t\(.alert)_NEWLINE_"')
MESSAGE="*SECURITY REPORT FOR "$TARGET"*\n\n"
MESSAGE=$MESSAGE$(echo $RESULT | sed -e 's|"|\\"|g' | sed -e 's/|/\\t|\\t/g' | sed -e 's|_NEWLINE_|\\n|g' | sed -e 's|\\n |\\n|g' )
echo $MESSAGE
# -----------------------------------------
# -----------------------------------------
# ------ Send text report ---------
# -----------------------------------------
curl -m 20 -s \
--header 'Content-Type: application/json' \
--request 'POST' \
--data "{\"disable_web_page_preview\":true,\"parse_mode\":\"Markdown\",\"chat_id\":\"${TGCHATID}\",\"text\":\"${MESSAGE}\"}" "https://api.telegram.org/bot${TGTOKEN}/sendMessage" \
1 > /dev/null
# -----------------------------------------
# -----------------------------------------
# ------ Send html report ---------
# -----------------------------------------
curl -m 20 \
-F "chat_id=${TGCHATID}" \
-F document=@${RESDIR}/${SCANFILE}-${SCANDATE}.html \
https://api.telegram.org/bot${TGTOKEN}/sendDocument
# -----------------------------------------