image

Некоторое время назад возникло желание реинкарнировать свой Wordpress-блог. Параллельно возникло желание упорядочить и систематизировать накопленные знания для сдачи экзамена ECSA. Все это привело меня к развертыванию блога на отдельно стоящем сервере. Через некоторый промежуток времени ожидаемо возникли вопросы безопасности сайта, использующего один из самых популярных (потому и вечно уязвимых) движков.

В результате изысканий появилось это руководство по организации непрерывного сканирования сайта на уязвимости, которым и спешу поделиться с вами, дорогие читатели.

Большую часть материала можно использовать в том числе и для внедрения в CI/CD пайплайны.

Прежде всего нужно оценить границы проекта и ресурсы, которые мы готовы на это затратить. В данном конкретном случае нет задачи объять необъятное, кроме того бюджет проекта околонулевой. Планируемое регулярное сканирование должно выполнять функцию беглого взгляда, с помощью которого мы понимаем, как выглядит сайт со стороны мимопроходящих товарищей со злыми намерениями. Просто чтобы не выполнять эти проверки ежедневно вручную.

Нам понадобятся:

  • сторонний сервер на базе ОС Linux для выполнения заданий по расписанию;
  • образ ZAP Baseline Scan;
  • навыки написания Bash-скриптов и использования командной строки Linux.

Настраиваем автоматическое сканирование сайта сканером OWASP ZAP


image

Ввиду постановки задачи возьмем не полнофункциональный 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


image

Как было обозначено ранее, результаты сканирования сохраняются в виде 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


image

Подопытным кроликом у нас выступает не корпоративный ресурс, и даже не стартап-проект, а простой личный блог, поэтому мы не будем выполнять интеграцию с 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 нашего сайта


image

В листинге приведен итоговый скрипт, готовый для использования в cron. Параметры запуска скрипта:

  • -target — начальный URL для сканирования OWASP ZAP;
  • -resdir — каталог для хранения отчетов о сканировании.

В логике скрипта предполагается сканирование сайта один раз в сутки. Повторный запуск при наличии отчетов сканирования за «сегодня» просто отправит нам данные из этих отчетов. И вот как это будет выглядеть:

image

HTML-отчет на экране мобильного устройства смотрится довольно прилично:

image

А вот и виновник торжества — скрипт автоматического пассиного сканирования сайта на уязвимости:

#!/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
# -----------------------------------------

За сим прощаюсь и благодарю дочитавших этот объёмный опус. Вопросы, пожелания, критика принимаются в комментариях и в личке. Всем добра!