Реверс-инжиниринг TeamBlind API
MoodyBlind - это анонимный форум с большим сообществом сотрудников из различных компаний, преследующих цели обсуждения насущных проблем с людьми, обладающими смежными профессиями, и запретных тем, вроде выплаты компенсаций, отношениях в компании или, в целом, политике в стране. Пользователи на Blind сгруппированы по темам, компаниям и отрасли в целом.
До 2019 года общение проходило только при помощи мобильного приложения, однако год назад появилось и веб-приложение.

Я решил покопаться в коде и посмотреть, как они реализовали его основные функции.
Открыв домашнюю страницу teamblind.com, я был рад увидеть бесконечную прокрутку и встроенные комментарии - это обычно означает, что они не выполняют серверную обработку всего контента, а используют чистые API-маршруты, настроенные только для извлечения нужных данных. Найти эти запросы и заняться их реверсом на стороне клиента тривиально - отсутствует необходимость в HTML-скрейпинге.
Это, кстати, один из основных подкупающих факторов для людей, которые все еще используют PHP для генерации всего HTML. Реверсеру гораздо проще работать с голыми JSON-данными, полученными от сервера, чем со страницами с большим количеством мусорной информации.
Изучая вкладку "Network", мы видим парочку POST запросов на https://www.teamblind.com/api/article/list.

С устрашающими ответами, вроде:
{
"payload": "A3gsuMXojEEY..<ОБРЕЗАНО ДЛЯ КРАТКОСТИ>..HWGAKB7O+U8Q=="
}
Вся строка - https://www.pastefs.com/pid/205047.
Тело запроса выглядит схожим образом, но к нему мы вернемся чуть позже.

Это был JSON-объект с одним ключом - полезной нагрузкой. И, судя по ее внешнему виду, она была закодирована в base64 (у вас должен работать рефлекс на все строки, заканчивающиеся на = - сразу же бегите проверять их на base64 декодере).
Я надеялся, что, в результате расшифровки, получу какой - нибудь простой пользовательский текстовый формат, но, к сожалению, я получил лишь невнятный мусор.

Тем не менее, он стал чуть разборчивее, поэтому я захотел выяснить, как он отформатирован и насколько сложно из него будет получить исходную строку.
Дешифрование
Javascript, который делал запрос, был минимизирован еще при сборке при помощи webpack и разделен на части.

Его пошаговое выполнение особой пользы не принесло. Появилось предположение, что это просто какая - то пользовательская HTTP-библиотека, выступающая в роли тонкой оболочки для парсинга, с завернутыми в ней fetch и XMLHttpRequest.
При этом, скорее всего, Javascript-код выполняющий процедуру шифрования, открыт для всех желающих и лежит где - то в открытом доступе - эта компания является свежим стартапом, и я сомневаюсь, что ее работники написали свою собственную библиотеку шифрования.
Я начал искать строки в коде, которые были бы полезны - многие упаковщики не знают, для чего используется та или иная строка, и поэтому не будут отбрасывать, к примеру, номера и имена версий библиотек, URL-адреса и другие вещи, которые послужат могут послужить нам подсказками.

Было довольно много ссылок на GitHub (всегда хорошее ключевое слово для поиска - авторы пакетов часто указывают ссылки на Github в исключениях, чтобы сообщить о том, что та или иная функция еще не реализована до конца и предложить вам принять участие в развитии проекта).

Бинго!
throw new Error(["sorry, createCredentials is not implemented yet", "we accept pull requests", "https://github.com/crypto-browserify/crypto-browserify"].join("\n"))
Я обнаружил ровно то, о чем предположил выше.
Этот раздел кода был явно минимизированной версией библиотеки с открытым исходным кодом под названием crypto-browserify.
var p = n(342);
t.pbkdf2 = p.pbkdf2,
t.pbkdf2Sync = p.pbkdf2Sync;
var d = n(519);
t.Cipher = d.Cipher,
t.createCipher = d.createCipher,
t.Cipheriv = d.Cipheriv,
t.createCipheriv = d.createCipheriv,
t.Decipher = d.Decipher,
t.createDecipher = d.createDecipher,
t.Decipheriv = d.Decipheriv,
t.createDecipheriv = d.createDecipheriv,
t.getCiphers = d.getCiphers,
t.listCiphers = d.listCiphers;
var f = n(534);
t.DiffieHellmanGroup = f.DiffieHellmanGroup,
t.createDiffieHellmanGroup = f.createDiffieHellmanGroup,
t.getDiffieHellman = f.getDiffieHellman,
t.createDiffieHellman = f.createDiffieHellman,
t.DiffieHellman = f.DiffieHellman;
var l = n(539);
t.createSign = l.createSign,
t.Sign = l.Sign,
t.createVerify = l.createVerify,
t.Verify = l.Verify,
t.createECDH = n(573);
var h = n(574);
t.publicEncrypt = h.publicEncrypt,
t.privateEncrypt = h.privateEncrypt,
t.publicDecrypt = h.publicDecrypt,
t.privateDecrypt = h.privateDecrypt;
var M = n(577);
t.randomFill = M.randomFill,
t.randomFillSync = M.randomFillSync,
...
Ее описание гласит: «Наша задача - переписать криптомодуль Node.js на чистом Javascript, чтобы он мог нормально функционировать в браузере».
Судя по всему, разработчики Blind используют Node.js на своем бэкэнде с модулем "crypto" и хотят использовать его функции на стороне пользователя.
Хороший способ повторно использовать код, но это также означает, что они должны каким - то образом передать приватный ключ клиенту.
Я предполагал, что они не будут его оставлять на видном месте, а, как минимум, будут создавать каждый раз новый на сеанс и обновлять его каждые X минут, возможно, через веб-сокет.
Мое предположение оказалось неверным.

Они почтиво оставили общий ключ в виде строки в минимизированном JS, рядом с говорящими функциями dataDec и dataEnc.
На данный момент у нас в руках есть все, чтобы расшифровать принимаемые данные.
Нам даже не нужно загружать используемую ими библиотеку, поскольку это всего лишь повторная реализация модуля crypto, доступная, как оказалось, нам теперь в браузере.
30 секунд в редакторе, и мы получаем следующий код:
const text = `<blob from above>`;
const crypto = require('crypto');
const t = crypto.createDecipher("aes-256-cbc", "5d860d3eb8e4a271309c0e4c001fafcc7cc80277e5238d9796a810a93ffa27d3")
let e = t.update(text, "base64", "utf8");
e += t.final("utf8")
console.log(JSON.parse(e))
Интерпретируем его и получаем... абсолютно читабельный Json!

Это работает прямо из коробки - никаких дополнительных действий не требуется. Самой интересной частью ответа был ключ article_list, содержащий следующие данные:
{
"alias": "b6WJEDTp",
"member_nickname": "faRw33",
"created_at": "4d",
"is_auth": "Y",
"board_id": 114961,
"member_company_id": 109330,
"images": [],
"group_name": null,
"channel_name": "Misc.",
"board_name": "Misc.",
"title": "Is an extreme work ethic required to reach the absolute top of your potential?",
"content": "I was never a huge basketball fan, but after Kobe's death I spent more time into his background. He was an absolute machine in how hard/how long he worked and the results showed. It's seems like its like that with so many great people who are just so passionate about one thing they will work their b",
"content_length": 300,
"like_cnt": 2,
"comment_cnt": 22,
"view_cnt": 1337,
"is_liked": false,
"is_bookmarked": false,
"is_multi_poll": false,
"is_multi": false,
"is_poll": true,
"is_mine": false,
"last_comment_cnt": null,
"has_comment_update": false,
"is_hot": false,
"is_now": false,
"is_company_tagged": false,
"is_wormhole": false,
"job_title": null,
"is_read": false,
"report_msg": null,
"is_top_contributor": false,
"is_show_holic": false,
"mention": null,
"was_companies": null,
"bio": null,
"tags": [],
"article_tags": null,
"is_hidden_company": false,
"is_show_company": true,
"member_company_name": "Greenhouse Software",
"is_best_company": false,
"poll": {
"id": 47176,
"article_id": 553156,
"is_multi": false,
"cnt": 108,
"polled": 0
},
"link": null,
"label": null
}
Мы получаем как публичный никнейм пользователей, который отображается в их сообщениях, а также приватный, который, по идее не должен быть доступен кому - то, кроме модераторов. Ну и конечно же другую полезную информацию.
Теперь мы способны расшифровать любое сообщение приходящее от сервера. Но как насчет того, чтобы отправить свое собственное?
Зашифрование
Вернемся к выполнению запросов, на которые мы ссылались ранее - похоже, что формат отправляемых данных идентичен тому, что мы получаем от сервера.

Я предполагаю, что они используют AES ключ, полученный нами на предыдущем этапе, и для шифрования тела запроса. Давайте проверим эту догадку.
Сначала нам нужно выяснить, как выглядит незашифрованный запрос.
Как вы помните, ранее мы уже обнаружили полезную функцию под названием dataEnc. Скорее всего, именно при помощи нее и шифруются все исходящие запросы к серверу. Поместим точку останова прямо перед ней, чтобы увидеть данные перед зашифрованием:

На этот раз шифрование отличается - теперь используется асимметричный открытый ключ вместо AES-ключа из предыдущего пункта. Я не совсем уверен, почему они это делают - если кто-то отслеживает трафик и видит зашифрованный объект запроса, то он может видеть и минимизированный JS, чтобы в дальнейшем воспроизвести все те же действия, которые я провернул выше. Это не позволит злоумышленнику расшифровать запрос, так как у него будет отсутствовать закрытый ключ, но это не помешает ему просто заново создать запрос.
Несмотря на это, их реализация выглядит так:
return new Jt("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhRvQEkqodGnJ9ap47FiF\nNwMTpdqUbfWyzecgJBxjWwWYwMYxpnde0WNjpzpdz9PMKLdFAg0UH1u32Y2xK/Bq\n/L8F2f+djLwGxszjTZwGGPiQxNWyDRI/In8T3S3dVqfr0QPirKsoy2OxgnbC7+BE\nH0ZN6Y4Sax588Uq+9M1Wz7ct60EZjLO9RLS/qH+t1ZBpQ/p3Ddkm/yCDvixyctvd\nGTbWUVFxtsqdTQjy8OcnQo2y1v6NUeZRqoKsk7LVmkYk3HjggDkBOZk8h1xRuCch\nkY1ix5jjArG645y863N6R+EQ9ShHZnwbVpsy47Vo8zigfk2RKl8ksxvstLrtABfW\nDQIDAQAB\n-----END PUBLIC KEY-----").encrypt(t, "base64")
Глядя на сигнатуру функции (.encrypt(t, "base64")) и код, я не сразу понял, какую библиотеку они использовали (они не использовали ту же самую библиотеку, что и раньше).
На самом деле, на этот раз меня выручила точка входа.

Поиск в гугле параметров this.$options оказался практически идеальным для модуля node-rsa.

Я делаю то же самое, что мы делали выше, и создаю простую оболочку для этой библиотеки:
const NodeRSA = require('node-rsa');
const key = new NodeRSA(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhRvQEkqodGnJ9ap47FiF
NwMTpdqUbfWyzecgJBxjWwWYwMYxpnde0WNjpzpdz9PMKLdFAg0UH1u32Y2xK/Bq
/L8F2f+djLwGxszjTZwGGPiQxNWyDRI/In8T3S3dVqfr0QPirKsoy2OxgnbC7+BE
H0ZN6Y4Sax588Uq+9M1Wz7ct60EZjLO9RLS/qH+t1ZBpQ/p3Ddkm/yCDvixyctvd
GTbWUVFxtsqdTQjy8OcnQo2y1v6NUeZRqoKsk7LVmkYk3HjggDkBOZk8h1xRuCch
kY1ix5jjArG645y863N6R+EQ9ShHZnwbVpsy47Vo8zigfk2RKl8ksxvstLrtABfW
DQIDAQAB
-----END PUBLIC KEY-----`);
const toenc = {
"channelId": "Careers",
"childchannelIds": [
120565
],
"offset": 0,
"limit": 50,
"orderBy": "pop"
}
const encrypted = key.encrypt(toenc, 'base64');
console.log('encrypted: ', encrypted);
Сначала я был немного сбит с толку, потому что то, что данные, которые я получил, не соответствовали тем, которые ранее отправил мой браузер, но потом я понял, что эта библиотека при каждом шифровании выдает различный результат.

Я взял свой результат зашифрования, заменил тело ранее отправленного результата, и получил верный ответ от сервера.
Чтож, теперь мы умеем и генерировать запросы, и расшифровывать ответы.
Вывод
BLIND шифрует запросы и расшифровывает ответы, приходящие от сервера, на стороне пользователя. Хотя это и не является хорошим решением для защиты данных от перехвата злоумышленником, это не так глупо, как кажется на первый взгляд - такой метод позволяет ограничить список лиц, которые смогут расшифровать отправляемые данные.
Это также предотвращает сбор данных любыми автоматическими инструментами; вам нужно сделать довольно глубокий анализ их библиотек и ключей, чтобы выяснить, как они были зашифрованы. Кроме того, шифруя запросы с помощью открытого ключа, вы не сможете расшифровать их, если у вас не будет соответствующего закрытого ключа.
В целом довольно веселый уик-энд во второй половине дня!
Статья доступна в оригинале на английском языке - читать.