Некоторое время назад возникло желание реинкарнировать свой Wordpress-блог. Параллельно возникло желание упорядочить и систематизировать накопленные знания для сдачи экзамена ECSA. Все это привело меня к развертыванию блога на отдельно стоящем сервере. Через некоторый промежуток времени ожидаемо возникли вопросы безопасности сайта, использующего один из самых популярных (потому и вечно уязвимых) движков.
В результате изысканий появилось это руководство по организации непрерывного сканирования сайта на уязвимости, которым и спешу поделиться с вами, дорогие читатели.
Большую часть материала можно использовать в том числе и для внедрения в CI/CD пайплайны.
Прежде всего нужно оценить границы проекта и ресурсы, которые мы готовы на это затратить. В данном конкретном случае нет задачи объять необъятное, кроме того бюджет проекта околонулевой. Планируемое регулярное сканирование должно выполнять функцию беглого взгляда, с помощью которого мы понимаем, как выглядит сайт со стороны мимопроходящих товарищей со злыми намерениями. Просто чтобы не выполнять эти проверки ежедневно вручную.
Нам понадобятся:
- сторонний сервер на базе ОС Linux для выполнения заданий по расписанию;
- образ ZAP Baseline Scan;
- навыки написания 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 — непосредственно скрипт сканирования;
- -t blog.tyutin.net/ru — цель сканирования;
- -j — запуск ajax-паука: пусть бегает, жалко что ли;
- -a — дополнительные правила, подробнее о которых можно почитать здесь;
- -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 имеются на сайте:
Вот так выглядит отчёт в консоли...
Total of 272 URLs
PASS: Cookie Without Secure Flag [10011]
PASS: Cross-Domain JavaScript Source File Inclusion [10017]
PASS: Content-Type Header Missing [10019]
PASS: X-Frame-Options Header Scanner [10020]
PASS: Information Disclosure - Debug Error Messages [10023]
PASS: Information Disclosure - Sensitive Information in URL [10024]
PASS: Information Disclosure - Sensitive Information in HTTP Referrer Header [10025]
PASS: HTTP Parameter Override [10026]
PASS: Information Disclosure - Suspicious Comments [10027]
PASS: Open Redirect [10028]
PASS: Cookie Poisoning [10029]
PASS: User Controllable Charset [10030]
PASS: User Controllable HTML Element Attribute (Potential XSS) [10031]
PASS: Viewstate Scanner [10032]
PASS: Directory Browsing [10033]
PASS: Heartbleed OpenSSL Vulnerability (Indicative) [10034]
PASS: Strict-Transport-Security Header Scanner [10035]
PASS: Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s) [10037]
PASS: X-Backend-Server Header Information Leak [10039]
PASS: Secure Pages Include Mixed Content [10040]
PASS: HTTP to HTTPS Insecure Transition in Form Post [10041]
PASS: HTTPS to HTTP Insecure Transition in Form Post [10042]
PASS: User Controllable JavaScript Event (XSS) [10043]
PASS: Big Redirect Detected (Potential Sensitive Information Leak) [10044]
PASS: Insecure Component [10046]
PASS: Content Cacheability [10049]
PASS: Retrieved from Cache [10050]
PASS: X-ChromeLogger-Data (XCOLD) Header Information Leak [10052]
PASS: CSP Scanner [10055]
PASS: X-Debug-Token Information Leak [10056]
PASS: Username Hash Found [10057]
PASS: X-AspNet-Version Response Header Scanner [10061]
PASS: PII Disclosure [10062]
PASS: Base64 Disclosure [10094]
PASS: Timestamp Disclosure [10096]
PASS: Hash Disclosure [10097]
PASS: Cross-Domain Misconfiguration [10098]
PASS: Weak Authentication Method [10105]
PASS: Reverse Tabnabbing [10108]
PASS: Modern Web Application [10109]
PASS: Private IP Disclosure [2]
PASS: Session ID in URL Rewrite [3]
PASS: Script Passive Scan Rules [50001]
PASS: Insecure JSF ViewState [90001]
PASS: Java Serialization Object [90002]
PASS: Charset Mismatch [90011]
PASS: Application Error Disclosure [90022]
PASS: Loosely Scoped Cookie [90033]
WARN-NEW: In Page Banner Information Leak [10009] x 1
https://blog.tyutin.net/favicon.ico (404 Not Found)
WARN-NEW: Cookie No HttpOnly Flag [10010] x 5
https://blog.tyutin.net/wp-login.php (200 OK)
https://blog.tyutin.net/wp-login.php?reauth=1&redirect_to=https%3A%2F%2Fblog.tyutin.net%2Fwp-admin%2F (200 OK)
https://blog.tyutin.net/wp-login.php?action=lostpassword (200 OK)
https://blog.tyutin.net/wp-login.php (200 OK)
https://blog.tyutin.net/wp-login.php?action=lostpassword (200 OK)
WARN-NEW: Incomplete or No Cache-control and Pragma HTTP Header Set [10015] x 114
https://blog.tyutin.net/ru/ (200 OK)
https://blog.tyutin.net/robots.txt (200 OK)
https://blog.tyutin.net/ (200 OK)
https://blog.tyutin.net/blog/category/uncategorized/ (200 OK)
https://blog.tyutin.net/blog/2020/03/03/hello-world/ (200 OK)
WARN-NEW: X-Content-Type-Options Header Missing [10021] x 161
https://blog.tyutin.net/ru/ (200 OK)
https://blog.tyutin.net/robots.txt (200 OK)
https://blog.tyutin.net/ (200 OK)
https://blog.tyutin.net/blog/category/uncategorized/ (200 OK)
https://blog.tyutin.net/blog/2020/03/03/hello-world/ (200 OK)
WARN-NEW: Server Leaks Version Information via "Server" HTTP Response Header Field [10036] x 188
https://blog.tyutin.net/ru/ (200 OK)
https://blog.tyutin.net/robots.txt (200 OK)
https://blog.tyutin.net/sitemap.xml (404 Not Found)
https://blog.tyutin.net/ (200 OK)
https://blog.tyutin.net/wp-admin/ (302 Found)
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 47
https://blog.tyutin.net/ru/ (200 OK)
https://blog.tyutin.net/sitemap.xml (404 Not Found)
https://blog.tyutin.net/ (200 OK)
https://blog.tyutin.net/wp-admin/admin-ajax.php (400 Bad Request)
https://blog.tyutin.net/blog/category/uncategorized/ (200 OK)
WARN-NEW: Cookie Without SameSite Attribute [10054] x 5
https://blog.tyutin.net/wp-login.php (200 OK)
https://blog.tyutin.net/wp-login.php?reauth=1&redirect_to=https%3A%2F%2Fblog.tyutin.net%2Fwp-admin%2F (200 OK)
https://blog.tyutin.net/wp-login.php?action=lostpassword (200 OK)
https://blog.tyutin.net/wp-login.php (200 OK)
https://blog.tyutin.net/wp-login.php?action=lostpassword (200 OK)
WARN-NEW: Feature Policy Header Not Set [10063] x 76
https://blog.tyutin.net/ru/ (200 OK)
https://blog.tyutin.net/sitemap.xml (404 Not Found)
https://blog.tyutin.net/ (200 OK)
https://blog.tyutin.net/wp-admin/admin-ajax.php (400 Bad Request)
https://blog.tyutin.net/blog/category/uncategorized/ (200 OK)
WARN-NEW: Source Code Disclosure - PHP [10099] x 11
https://blog.tyutin.net/ru/%d0%b7%d0%b0%d0%bf%d0%b8%d1%81%d0%ba%d0%b8-devsecopsa-sast-%d0%b8%d0%bb%d0%b8-%d0%bf%d0%be%d0%b3%d1%80%d1%83%d0%b6%d0%b5%d0%bd%d0%b8%d0%b5-%d0%b2-%d0%b8%d1%81%d1%85%d0%be%d0%b4%d0%bd%d1%8b%d0%b9/ (200 OK)
https://blog.tyutin.net/ru/feed/ (200 OK)
https://blog.tyutin.net/ru/category/devsecops/feed/ (200 OK)
https://blog.tyutin.net/ru/wp-content/plugins/syntaxhighlighter/syntaxhighlighter3/scripts/shBrushPhp.js?ver=3.0.9b (200 OK)
https://blog.tyutin.net/ru/wp-content/uploads/sites/2/2020/03/SSLLabs_blog_03_Aplus.png (200 OK)
WARN-NEW: Absence of Anti-CSRF Tokens [10202] x 22
https://blog.tyutin.net/sitemap.xml (404 Not Found)
https://blog.tyutin.net/sitemap.xml (404 Not Found)
https://blog.tyutin.net/ (200 OK)
https://blog.tyutin.net/blog/category/uncategorized/ (200 OK)
https://blog.tyutin.net/blog/2020/03/03/hello-world/ (200 OK)
WARN-NEW: Sub Resource Integrity Attribute Missing [90003] x 37
https://blog.tyutin.net/ru/ (200 OK)
https://blog.tyutin.net/sitemap.xml (404 Not Found)
https://blog.tyutin.net/ (200 OK)
https://blog.tyutin.net/blog/category/uncategorized/ (200 OK)
https://blog.tyutin.net/blog/2020/03/03/hello-world/ (200 OK)
FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 11 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 48
Проблема этого отчета в том, что он большой. Необходимость ежедневно вчитываться в него убьёт все желание это делать. Поэтому переходим к следующему разделу.
Консолидируем данные из отчета 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
# -----------------------------------------
За сим прощаюсь и благодарю дочитавших этот объёмный опус. Вопросы, пожелания, критика принимаются в комментариях и в личке. Всем добра!