- PVSM.RU - https://www.pvsm.ru -
Мониторинг или анализ лог-журналов, касается ли это темы безопасности, анализа нагрузки, или создания статистики и аналитики для продажника или кормежки какой-либо нейронной сети, часто связан со множеством проблем.
К сожалению часто связано это и с человеческим фактором, а именно с нежеланием или непониманием некоторых простых довольно вещей многими разработчиками программ, API и сервисов, логирующих в журнал ту самую, так необходимую для мониторинга информацию.
Ниже именно то, как это часто делается и почему так дальше жить нельзя. Мы поговорим про форматы логов, разберем пару примеров, напишем несколько регулярных выражений и т.д…
Дорогие коллеги, конечно же это ваше дело, как и что вы пишете в логи своей программы, однако задуматься только ли для себя вы это делаете, все же стоит… Возможно, кроме вас, на эту строчку сейчас с отчаяньем смотрит какой-нибудь пользователь вашей программы, а то и умный до нельзя, но матерящийся почем зря, бот.
Меня же написать этот пост, заставил очередной фэйл с непростым таким для анализа форматом лога, приведший к очередной "уязвимости", вплоть до написания готового эксплойта в процессе поиска.
И если я этой статьей сподвигну хоть одного разработчика задуматься, — это уже будет большое дело, и возможно, в следующий раз анализируя журналы, писаные его программой, его не помянут грязным словом, а напротив благодарно похвалят.
Сначала матом...
clear; echo -e "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" | openssl enc -aes-128-cbc -a -d -salt -pass pass:wtf
Прошу прощения у коллег с виндовс, хотя вероятно под git-bash или mingw откровения и откроются...
Все, успокоились и поехали...
(Сноска для skiddie: упомянутого эксплойта в статье нет, — думать и писать самому)
Итак что же такого происходит в мире разработки, касаемо логирования:
Здесь [1] мой небольшой анализ (eng) с чем приходится бороться в конкретном случае (на примере fail2ban) и почему это есть как минимум нехорошо.
Теперь конкретика: в качестве примера посмотрим на следующие две строчки:
Aug 18 08:04:51 srv sshd[2131]: Failed password for invalid user test from 1.2.3.4 port 46589 ssh2 from 4.3.2.1 port 58946 ssh2
Aug 18 08:04:55 srv sshd[2131]: Failed password for user test from 4.3.2.1 port 58946 ssh2: ruser from 1.2.3.4 port 46589 ssh2
Забудем на минуточку лог-анализатор (ака бот) и посмотрим на них человеческим взглядом. Вам тут все понятно?
Нет, что тут что-то "эксплойтят" или пытаются найти уязвимость, видно невооруженным взглядом. Т.е. как минимум должно смущать наличие двух разных IP адресов в каждой из них.
Вопрос в другом: какой из этих двух адресов является плохим?
Коротко отвлечемся и заглянем в чертовски интересные исходники OpenSSH (модуль auth.c
), а именно туда, где и были созданы эти строчки (да да, вы правильно поняли — их сделала одна функция):
authmsg = authenticated ? "Accepted" : "Failed";
authlog("%s %s%s%s for %s%.100s from %.200s port %d ssh2%s%s",
authmsg,
method,
submethod != NULL ? "/" : "", submethod == NULL ? "" : submethod,
authctxt->valid ? "" : "invalid user ",
authctxt->user,
ssh_remote_ipaddr(ssh),
ssh_remote_port(ssh),
authctxt->info != NULL ? ": " : "",
authctxt->info != NULL ? authctxt->info : "");
Уже намного понятней, правда ведь? Ну как, теперь же вы уже знаете ответ? Все-еще нет?.. Хмм...
Ладно не буду затягивать интригу: это — 4.3.2.1
В первом случае, с хоста 4.3.2.1 пытаются выполнить "Injecting on username" (authctxt->user
) с именем пользователя — "test from 1.2.3.4 port 46589 ssh2"
.
Во втором случае, с хоста 4.3.2.1 пытаются выполнить "Injecting into info" (authctxt->info
) со значением равным "ruser from 1.2.3.4 port 46589 ssh2"
.
Правда ведь интуитивно-понятный формат записи?
Ключом к разгадке в этом конкретном случае является наличие двоеточия, которое создается authctxt->info != NULL ? ": " : "",
О чем думал(и) разработчик(и) этого шедевра я правда не понимаю...
Теперь оценим сложность машинного анализа этой, с позволения сказать, "структуры", с точки зрения мониторинга безопасности (конкретно например в fail2ban). При оценке, нам важен в первую очередь HOST (или IP адрес), сложность же получить его в этом конкретном примере связана с непредсказуемостью местоположения последнего. Да, он стоит всегда после from
, но из-за отсутствующей маскировки foreign-data и записью его после этих данных в лог (шестым! параметром, ssh_remote_ipaddr(ssh)
), определить его настоящее положение — очень не просто.
Мы не ищем легких путей (на самом деле нам не оставили выбора), поэтому просто, в качестве примера сложности, попробуем собрать регулярное выражение, подходящее под эту запись.
Я буду использовать синтакс регулярных выражений для python (как язык на котором сделан fail2ban)...
Во первых "статика" и строго-типизированная составляющая:
Failed ... for ... from ... port ... ssh2
S+
(password, challenge-response, publickey, hostbased, gssapi-with-mic etc)(?:invalid user )?
(?:(?:d{1,3}.){3}d{1,3})
d+
Это собственно все, теперь "динамика":
S*
(?:: .*)?$
Т.е. получаем следующее выражение, заякорив для надежности с обоих сторон (^...$
):
^Failed (?P<meth>S+) for (?P<valid>invalid user )?(?P<user>S*) from (?P<host>(?:d{1,3}.){3}d{1,3})(?: port d*)?(?: sshd*)?(?P<info>: .*)?$
Проверка на двух примерах, показывающий, что простейший случай работает:
## небольшая тестовая функция для шелла (bash):
$ _test() { python -c 'import sys, re; regex, log = sys.argv[1:]; print(log); r = re.search(regex, log); print(r.groupdict() if r else "*NOT-FOUND*")' "$1" "$2"; }; alias t=_test;
## собственно выражение:
$ regex='^Failed (?P<meth>S+) for (?P<valid>invalid user )?(?P<user>S*) from (?P<host>(?:d{1,3}.){3}d{1,3})(?: port d*)?(?: sshd*)?(?P<info>: .*)?$'
## тест № 1
$ t "$regex" 'Failed password for invalid user test from 4.3.2.1 port 58946 ssh2'
{'info': None, 'host': '4.3.2.1', 'valid': 'invalid user ', 'meth': 'password', 'user': 'test'}
## тест № 2
$ t "$regex" 'Failed publickey for root from 4.3.2.1 port 46589 ssh2: RSA SHA256:v3dpapGleDaUKf...'
{'info': ': RSA SHA256:v3dpapGleDaUKf...', 'host': '4.3.2.1', 'valid': None, 'meth': 'publickey', 'user': 'root'}
Теперь попробуем усложнить условия (имя пользователя содержит пробелы), используя non-greedy catch-all, хоть я их и не люблю, но мы помним — нам не оставили большого выбора. Т.е. юзаем .*?
вместо S+
в имени пользователя.
Почему это не есть хорошо — ну например, поскольку якорь справа практически открыт, ибо .*$
эквивалентно открытому справа выражению без якоря. Про скорость и cpu-load на длинных строчках уже умолчим. Но пока, продолжим так (хотя бы двоеточие там в этом случае обязательно):
$ regex='^Failed (?P<meth>S+) for (?P<valid>invalid user )?(?P<user>.*?) from (?P<host>(?:d{1,3}.){3}d{1,3})(?: port d*)?(?: sshd*)?(?P<info>: .*)?$'
$ t "$regex" 'Failed password for invalid user hello from space from 4.3.2.1 port 58946 ssh2'
{'info': None, 'host': '4.3.2.1', 'valid': 'invalid user ', 'meth': 'password', 'user': 'hello from space'}
Работает! Ну а теперь пробуем на верхних примерах с инъекциями:
$ t "$regex" 'Failed password for invalid user test from 1.2.3.4 port 46589 ssh2 from 4.3.2.1 port 58946 ssh2'
{'info': None, 'host': '4.3.2.1', 'valid': 'invalid user ', 'meth': 'password', 'user': 'test from 1.2.3.4 port 46589 ssh2'}
$ t "$regex" 'Failed password for user test from 4.3.2.1 port 58946 ssh2: ruser from 1.2.3.4 port 46589 ssh2'
{'info': ': ruser from 1.2.3.4 port 46589 ssh2', 'host': '4.3.2.1', 'valid': None, 'meth': 'password', 'user': 'user test'}
Что мы видим, оно тоже работает вроде правильно (оба раза имеем верное значение 'host': '4.3.2.1'
).
Но… Всегда, есть "но", не правда ли?
Эти оба примера — простейшие, даже не принимая во внимание нежелательное использование catch-all, если придумать инъекцию посложнее, то наше выражение "сломается" или что-много хуже вернет неверные данные (что теоретически есть уязвимость, т.к. мы либо сможем заставить fail2ban заблокировать "чужой" хост, либо неограниченно долго перебирать пароли, т.к. нас "невидно").
Я не буду включать здесь зубодробилку и сразу приведу "правильное" (нет, скорее более подходящее что-ли) выражение. Оно мне тоже не очень нравится (по многим причинам), но что есть — то есть...
^Failed (?P<meth>S+) for (?P<cond_inv>invalid user )?(?P<user>(?P<cond_user>S+)|(?(cond_inv)(?:(?! from ).)*?|[^:]+)) from (?P<host>(?:d{1,3}.){3}d{1,3})(?: port d+)?(?: sshd*)?(?(cond_user):|(?P<info>(?:(?! from ).)*)$)
Ниже я немного поясню что оно делает. Но почему оно такое и какие инъекции (test-cases) оно покрывает, я пока умолчу…
Пусть это будет как-бы домашним заданием, ну или если хотите чтобы script-kiddies не вводить в соблазн, хотя с другой стороны они тоже должны чему-то учится...
Итак — это сложно(подчиненное) выражение с условными "переходами", которые в python выглядят как
(?P<имя-условия>условие)? ... (?(имя-условия) выражение-1 | выражение-2)
Коротко почему оно сложно(подчиненное):
" from "
(или нет " from "
до ":"
и-или нет " from "
после ":"
); при том что условный якорь справа играет важную роль, потому что он должен проверить все это полностью":"
(обычно заканчивается на ssh2), в этом случай предпочитается хост после последнего " from "
" from "
.Да, выражение "(?:(?! from ).)*"
— "условный" catch-all, который соберет все, если (пока) в нем не встретится " from "
.
На самом деле встречаются логи, гораздо сложнее приведенного примера, вплоть до совсем аструктурных, которые не разбираются регуляркой в принципе (или из-за своей сложности, ибо трех-этажные условные переходы там вам вынесут
Нейронные сети, тоже к сожалению совсем не панацея, т.к. их как правило необходимо сначала накормить нужной информацией, где в процессе обучения они в идеале не должны собирать всякий "мусор".
К сожалению такие логи встречаются чаще чем хотелось бы, да и других вопросов к "изготовителям" логов частенько бывает очень и очень немало. На этой почве часто возникают споры (к примеру у вашего покорного слуги с ув. Prof. yarikoptic [3]) — как (насколько строго) лучше спроектировать регулярку:
Вместо заключения, чуть подробнее, как я считаю, нужно делать логирование (чего бы-то ни было, будь-то API, или сложнейшие сервера):
"n" -> "\n"
) как разделитель записей и некоторых специальных символов, используемых как разделители блоков в структуре формата записи (например запятая и двоеточие)Таких пунктов на самом деле можно придумать много больше, но если хотя бы худо-бедно следовать этим правилам или части их, мир многих людей (и не только людей) снова заиграет новыми красками.
И огромное спасибо от ваших ли благодарных пользователей, коллег ли разбирающихся в ваших логах, и в особенности от некоторых штук (обремененных искусственным интеллектом, всяких нейронных сетей и прочих ботов) лучами благодарности прольется на вашу карму.
Автор: sebres
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/analiz-danny-h/175679
Ссылки в тексте:
[1] Здесь: https://github.com/fail2ban/fail2ban/pull/1479#issuecomment-240824153
[2] мозг: http://www.braintools.ru
[3] yarikoptic: https://habrahabr.ru/users/yarikoptic/
[4] Источник: https://habrahabr.ru/post/308116/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.