PowerShell и ЖЖ: аутентификация challenge-response и регистр букв хеш-суммы, ч.1

Feb 07, 2023 16:22

Ранее в этой серии постов:
...
11. Общение с ЖЖ из PowerShell: простейшая аутентификация, получаю текст поста (интерфейс «flat»), ч.3
12. Общение с ЖЖ из PowerShell: простейшая аутентификация, получаю текст поста (интерфейс «XML-RPC»)
13. PowerShell: преобразование хеш-таблицы в формат XML-RPC

Окружение: операционная система «Windows 10», программа-оболочка «PowerShell» версии 7.

Вернемся на минутку к вычислению хеш-суммы пароля

В предыдущих постах я уже разбирал простейшую (clear) аутентификацию в ЖЖ («Живом Журнале»). Напомню, она есть двух видов: передача в HTTP(S)-запросе самого пароля в параметре «password» или передача хеш-суммы пароля в параметре «hpassword» вместо параметра «password». Я описал функцию «getHash» на языке PowerShell, с помощью которой можно получить хеш-сумму для любой строки по заданному алгоритму (при общении с ЖЖ нам понадобится алгоритм «MD5»). Например:

getHash "пароль" "MD5"

E242F36F4F95F12966DA8FA2EFD59992

В предыдущих постах я не разбирал, как работает алгоритм «MD5» и не разбирал результат, который этот алгоритм выдает. Кое-что относительно результата необходимо знать, чтобы не попасть в тупик при изучении аутентификации «challenge-response» (я в этот тупик попал).

Насколько я понимаю, алгоритм «MD5» возвращает результат (хеш-сумму) в двоичном виде. Размер результата составляет 128 бит. Например, начало вышеприведенного результата в двоичном виде выглядит так:

1110 0010 0100 0010 1111 0011 ...и т.д.
E 2 4 2 F 3 ...и т.д.

Из блока кода выше можно понять, что буквы и цифры в хеш-сумме являются цифрами шестнадцатеричной системы счисления, а не просто какими-то произвольными буквами и цифрами.

И тут возникает вопрос насчет возможности использования в хеш-сумме латинских букв от «A» до «F» в разном регистре. В русскоязычной статье википедии, посвященной шестнадцатеричной системе счисления, на данный момент о регистре этих букв ничего не сказано. А вот в англоязычной статье википедии по этому поводу сказано следующее:

There is no universal convention to use lowercase or uppercase, so each is prevalent or preferred in particular environments by community standards or convention; even mixed case is used.

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

E242F36F4F95F12966DA8FA2EFD59992 # все буквы прописные (большие)
e242f36f4f95f12966da8fa2efd59992 # все буквы строчные (маленькие)
e242F36f4F95f12966Da8Fa2EfD59992 # есть буквы разного регистра

С точки зрения шестнадцатеричной системы счисления все три эти варианта равнозначны.

То есть, по идее, программа сравнения двух хеш-сумм (очевидно, что ЖЖ часто выполняет эту операцию, а значит, имеет функцию для выполнения этой операции) должна приводить сравниваемые хеш-суммы перед сравнением к единой форме: все буквы в составе хеш-сумм можно сделать либо прописными, либо строчными. Однако, не все программы сравнения хеш-сумм это делают, поэтому в некоторые системы вы должны передавать хеш-сумму с буквами в определенном регистре. Эта проблема, например, обсуждалась в этом вопросе на сайте «Stack Overflow».

Я проверил три указанных выше варианта хеш-суммы на задаче получения текста одного из постов определенного журнала в ЖЖ, использовав код, разобранный в одном из предыдущих постов (я проверил и через интерфейс «flat», и через интерфейс «XML-RPC»). ЖЖ возвратил запрошенный мною текст поста для всех трех перечисленных выше вариантов хеш-суммы. Таким образом, очевидно, что ЖЖ перед сравнением полученной от меня хеш-суммы пароля и хранящейся в его базе данных хеш-суммы пароля приводит буквы в хеш-сумме к одному и тому же регистру.

Порядок действий при аутентификации «challenge-response»

Способ аутентификации «challenge-response» используется не только в ЖЖ. В википедии об этом способе есть отдельная статья (англоязычная статья более полезна, чем русскоязычная, так как в последней очень мало информации, буквально несколько абзацев). При такой аутентификации выполняется следующая последовательность действий:

1. Я посылаю в ЖЖ HTTP(S)-запрос на запуск удалённой функции «getchallenge». Для запуска этой удалённой функции не требуется никакой аутентификации, я об этом писал в предыдущих постах. Эта функция возвращает некое значение, которое по-английски называют «challenge», а по-русски «отклик» или «ответ». Пример такого значения:

c0:1674766800:1054:60:ZmbOcwbxmdswLmKngEVl:3a50482295a65607685badc39b09d47b

Это значение возвращается при каждом вызове функции «getchallenge» разное, хоть это значение и имеет каждый раз похожий формат. Это значение имеет ограниченный срок действия. В случае ЖЖ на данный момент этот срок действия равен 60 секундам (одной минуте);

2. Вычисляю хеш-сумму для моего ответа к ЖЖ по алгоритму «MD5» по следующей формуле:

хеш-сумма(challenge + хеш-сумма пароля)

С использованием упомянутой выше функции «getHash» на языке PowerShell это выглядит так:

$hPass = getHash "пароль" "MD5"
$hResp = getHash ($params["challenge"] + $hPass) "MD5"

В переменной $params хранится хеш-таблица с параметрами, полученными в HTTP(S)-ответе от ЖЖ на первом шаге. Выражение $params["challenge"] возвращает строку с откликом (challenge), полученным от ЖЖ на первом шаге.

3. Я посылаю в ЖЖ второй HTTP(S)-запрос, теперь содержащий нужные мне входные параметры для получения нужной мне информации. Вместо параметра «password» или «hpassword», как при простейшей (clear) аутентификации, я посылаю три следующих параметра:

$body = @{
...
auth_method = "challenge"
auth_challenge = $params["challenge"]
auth_response = $hResp
...
}

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

Параметр с названием «auth_method» содержит строку с названием способа аутентификации (в данном случае - строку "challenge"). При простейшей (clear) аутентификации этот параметр я не посылал, так как при его отсутствии считается, что его значение равно строке "clear", что означает использование простейшей аутентификации. Параметр с названием «auth_challenge» содержит отклик (challenge), полученный на первом шаге; то есть в этом параметре мы отправляем то, что получили, назад к ЖЖ в том же виде. Параметр с названием «auth_response» содержит значение $hResp, вычисленное на втором шаге.

В чем смысл такого запутанного способа аутентификации? Смысл в том, что я не отправляю по сети ни сам пароль, на даже хеш-сумму пароля. Поэтому злоумышленники, перехватив мой HTTP(S)-запрос на третьем шаге, не смогут получить ни пароля, ни хеш-суммы пароля. При этом атака повторного воспроизведения (replay attack) всё еще возможна, но срок возможного проведения этой атаки ограничен сроком действия отклика (challenge). То есть в случае моего общения с ЖЖ злоумышленник может успешно повторить (replay) мой HTTP(S)-запрос только в течение одной минуты. При простейшей аутентификации атака повторного воспроизведения доступна на срок до смены пользователем его пароля (если учесть, что пользователи обычно не утруждают себя профилактической периодической сменой пароля, то этот срок достаточно большой).

Какая у меня возникла проблема

Я всё выполнил по инструкции, но получил от ЖЖ в теле HTTP(S)-ответа сообщение о неправильном пароле. Пришлось потратить на поиск причины этой проблемы довольно много времени, но причину я нашел.

Дело в том, что моя функция «getHash» использует командлет «Get-FileHash», который возвращает хеш-сумму с буквами в верхнем регистре («прописные» или «большие» буквы). Так принято в операционных системах «Windows» (напомню, я работаю в операционной системе «Windows 10»). Как я писал ранее в этом посте, приведение букв хеш-суммы в верхний регистр само по себе не является ошибкой.

Однако, веб-приложение «LiveJournal», похоже, работает на какой-то Unix-подобной операционной системе. В Unix-подобных операционных системах принято писать хеш-сумму с буквами в нижнем регистре. Это тоже само по себе не является ошибкой. Как я описывал выше, ЖЖ, скорее всего, приводит получаемую от меня хеш-сумму в нижний регистр перед сравнением с хеш-суммой, хранящейся в его базе данных.

Но посмотрим, как при этом происходит вычисление хеш-суммы для ответа на втором шаге (см. последовательность шагов, описанную выше, для аутентификации «challenge-response»):

$hPass = getHash "пароль" "MD5"
$hPass

E242F36F4F95F12966DA8FA2EFD59992

$hResp = getHash ("c0:1674766800:1054:60:ZmbOcwbxmdswLmKngEVl:3a50482295a65607685badc39b09d47b" + $hPass) "MD5"
$hResp

7770C02A3F9615AB42037E84EA581BAB

В качестве отклика (challenge) ЖЖ я взял отклик, приведенный для примера ранее в этом посте.

Обратите внимание, что при втором вызове функции «getHash» ей на вход в качестве первого параметра подается строка, представляющая сумму двух строк - строки с откликом ЖЖ (challenge) и строки с хеш-суммой нашего пароля. Проблема в том, что в данном случае хеш-сумма нашего пароля трактуется не как обозначение двоичных данных хеш-суммы, а как строка символов. Так и должно быть по данному алгоритму-формуле. Однако, такое преобразование делает значимым регистр букв в строке, представляющей хеш-сумму нашего пароля. Вот как меняется полученная хеш-сумма $hResp в зависимости от регистра букв во входящей строке с хеш-суммой нашего пароля:

$hPass $hResp
E242F36F4F95F12966DA8FA2EFD59992 --> 7770C02A3F9615AB42037E84EA581BAB
e242f36f4f95f12966da8fa2efd59992 --> A144B4F4C8E7BD25C03CFEFC92EC25EA
e242F36f4F95f12966Da8Fa2EfD59992 --> BF7885C03C3A92179A8A19FEE8C2DB86

Как я упоминал выше, похоже, что ЖЖ приводит буквы в хеш-сумме перед использованием хеш-суммы в какой-либо операции к нижнему регистру. Таким образом, при проверочных вычислениях на стороне ЖЖ правильным будет признан лишь второй вариант значения переменной $hResp из трех, показанных в блоке кода выше.

Какой из всего вышеизложенного следует вывод? Чтобы при общении с ЖЖ по сети не было ошибки при использовании способа аутентификации «challenge-response», следует перед использованием хеш-суммы приводить буквы в этой хеш-сумме в нижний регистр (для простейшей (clear) аутентификации регистр букв в хеш-сумме не имеет значения). Я сделал в функции «getHash» нужное исправление (в блоке кода ниже оно выделено красным цветом), после чего у меня всё заработало без ошибок:

function getHash($str, $alg) {
$stringAsStream = [System.IO.MemoryStream]::new()
$writer = [System.IO.StreamWriter]::new($stringAsStream)
$writer.write($str)
$writer.Flush()
$stringAsStream.Position = 0
(Get-FileHash -InputStream $stringAsStream -Algorithm $alg).Hash.ToLower()
}

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

Продолжение следует тут и тут.

Инструмент, Образование, Сайтостроение, Программирование, Английский язык, ЖЖ

Previous post Next post
Up