Анализ системы защиты от ботов на примере letu.ru

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Не так давно, в одном из тематических телеграм чатов я увидел сообщение о поиске проксей для парсинга сайта Лэтуаль (https://letu.ru), т.к. якобы нужно много российских IP, т.к. старые очень быстро банят. Мне стало интересно посмотреть, как сегодня выглядят и работают отечественные системы, т.к. в основном, в моём фокусе находятся зарубежные.

Определение проблемы и задачи

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

Можно выбрать какой-нибудь продукт, а можно попробовать получить для начала список продуктов. Окей, откроем случайную категорию, пусть это будут Товары для дома (https://www.letu.ru/browse/vse-dlya-doma), и посмотрим, откуда там этот список товаров появляется.

Открываем в режиме инкогнито, чтоб быть уверены, что предыдущие куки никак не повлияют, можно выключить в браузере JS, и посмотреть, загрузится-ли что-то.

После нескольких манипуляций в devtools можно найти целевой url (https://www.letu.ru/s/api/product/listing/v1/products?N=oqisq0&Nrpp=36&No=0&innerPath=mainContent[3]&resultListPath=%2Fcontent%2FWeb%2FCategories%2FBrowse Pages%2FDefault Browse Page - General Rule&pushSite=storeMobileRU), откуда всё берётся.

Можно ещё раз посмотреть на URL категории (https://www.letu.ru/browse/vse-dlya-doma) и предположить, что последняя часть (/vse-dlya-doma) может быть идентификатором категории, если его убрать, то получим страницу, на которой будет список вообще всех продуктов магазина. Очень удобно.
Payload:

N: 0 
Nrpp: 36 
No: 0 
innerPath: mainContent[3] 
resultListPath: /content/Web/Categories/Browse Pages/Default Browse Page - General Rule 
pushSite: storeMobileRU

Result:

adultsOnly: false 
chanelProductsCount: 0 
chanelRedirectEnabled: false 
lastRecNum: 36 
products: […] 
sortOptions: […] 
totalNumRecs: 150175 
totalNumSkus: 217258

Что-ж, попробуем узнать, можно ли собирать данные отсюда каким-нибудь curl или python requests. Жмём Copy as curl, проверяем - работает. Идём в https://curlconverter.com/, получаем python requests код, проверяем - работает, однако, как, говорится, есть нюанс.

Если открыть целевую ссылку https://www.letu.ru/s/api/product/listing/v1/products?N=oqisq0&Nrpp=36&No=0&innerPath=mainContent[3]&resultListPath=%2Fcontent%2FWeb%2FCategories%2FBrowse Pages%2FDefault Browse Page - General Rule&pushSite=storeMobileRU в браузере - мы увидим проверку:

Это происходит потому, что в целевом запросе используются дополнительные заголовки. Конкретно в этом случае влияет заголовок

x-promo-msg

Соответственно, если удалить все дополнительные заголовки и cookies, взятые из реального браузера, из параметров requests или curl - то будем получать 503 и соответствующую проверку.

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

Для начала посмотрим, Network tab, как работает эта проверка

Первый запрос - это наш целевой урл с ответoм: 503 и ссылкой на JS, который загружается следующим запросом. Третий запрос, кажется, отправляет кучу данных, на сервер (назовём его запросом валидации). На основании этих данных сервер нам отдаёт очень характерные cookies (bot_profile_check=true&blabla...)

Используя эти куки, браузер повторно загружает первоначальную страницу, и получает валидный ответ.

Можно убедиться, что эти куки (ngenix_jscv_*) являются ключом для получения данных, если удалить, то запрос на проверку браузера придёт вновь.

Рассмотрев payload в запросе валидации можно увидеть, что он отправляет набор параметров, которые он извлёк из браузера различными способами (User-Agent, platform, и куча ещё всего), занятно, что это даже не зашифровано, так-что, подделать очень легко.

Попробуем, ради интереса повторить курлом запрос валидации.

Кажется, работает, нужная нам кука отдаётся. Причём, одинаковая каждый раз.

< HTTP/2 200 
< server: nginx
< date: Fri, 19 Jan 2024 09:51:28 GMT
< content-type: application/octet-stream
< set-cookie: ngenix_jscv_d84ce224cbdf=cookie_expires=1705658459&bot_profile_check=true&cookie_signature=Fej1dZFrxpYBV3Uj8XY8c9plGdk%3D; Max-Age=571; Domain=www.letu.ru; Path=/; HttpOnly; SameSite=Lax

Попробуем чего-нибудь сделать с этим запросом. Что бросается в глаза - это первое же поле solution, кажется, это может быть какой-то подписью или отдельной проверкой.

Изменив 1 цифру в поле solution получаем ответ:

< HTTP/2 403 
< server: nginx
< date: Fri, 19 Jan 2024 09:56:20 GMT
< content-type: application/octet-stream
< 
Solution is invalid

Ага, значит, оно как-то вычисляется и меняется. Повторив эту историю ещё несколько раз, можно так-же обнаружить, что этот solution каждый раз разный, а так-же, что cookies в запросе валидации активны только 2 минуты (что, в целом можно увидеть и по их TTL в браузере). То есть отправить запрос на валидицию с этим payload можно только в течение 2х минут.

Если отправить запрос позже - можно получить ответ

< HTTP/2 403 
< server: nginx
< date: Fri, 19 Jan 2024 09:58:33 GMT
< content-type: application/octet-stream
< 
Challenge cookie has expired

Окей, получается, мы можем целиком сгенировать payload для валидации, кроме поля solution, затем получить cookies, и уже использовать их дальше.

Попробуем найти, как сгенерировать solution.

Итак, самое интересное, что можно найти в исходном коде страницы с этой проверкой выглядит примерно так

<head>
  <script src="/js-challenge-script-99c5399535c92c38ab40475540a05465.js?v=071cc3d9c49958aa0af6"></script>
</head>
<body onload="jsch._jsChallenge()">
  </body>

А что-же это за скрипт?

Ой, какая красота, это-же обфусцированный JS, наконец-то становится интереснее!

Что мы можем с ним сделать?

Для начала пусть хотя бы chrome его немного отформатирует (откроем это в network tab), а потом сразу запихаем его в AST Explorer (https://github.com/fkling/astexplorer).

Эта штука распарсит скрипт в абстрактное синтаксическое дерево, и покажет структуру кода. По ней можно идти внутрь и разбираться.

Одна из вещей, которые помогают работать с такими скриптами - это Babel и пакет, traverse. Он умеет ходить по этому дереву, и модифицировать его узлы так, как мы его попросим.

Самое первое и простое, что бросается в глаза - это попытаться выкинуть, для начала, Hex переменные.

То есть, у нас есть узел NumericLiteral

{{"extra": "rawValue":533852, "raw": "0x8255c"}, "value": "533852"}

Штука в том, что если мы удалим extra - он будет выглядеть как обычная десятичная переменная.

const parser = require("@babel/parser");
const fs = require("fs");
const code = fs.readFileSync("test/l.js", "utf-8");
let ast = parser.parse(code);
const beautify = require("js-beautify");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;

const deobfuscateEncodedStringVisitor = {
    NumericLiteral(path) {
        if (path.node.extra) delete path.node.extra;
    },
};

traverse(ast, deobfuscateEncodedStringVisitor);

// Code Beautification
let deobfCode = generate(ast, { comments: false }).code;
deobfCode = beautify(deobfCode, {
    indent_size: 2,
    space_in_empty_paren: true,
});

function writeCodeToFile(code) {
    let outputPath = "test/l_deobf.js";
    fs.writeFile(outputPath, code, (err) => {
        if (err) {
            console.log("Error writing file", err);
        } else {
            console.log(`Wrote file to ${outputPath}`);
        }
    });
}

writeCodeToFile(deobfCode);

Кажется, работает!

})(a0_0x4463, 533852);

Если вглядеться в бездну это, можно заметить, что первая функция (a0_0x4463) возвращает массив, в котором зашиты всякие разные JS конструкции, и куски, которые где-то как-то склеиваются и вызываются. Так-же, можно увидеть, что наша целевая переменная solution как раз в этом массиве. То есть надо разобраться, где и как эта функция вызывается, и что вообще с этим массивом.

Если ещё раз всмотреться, можно заметить, что вторая функция как-то пересортировывает этот массив, а функция a0_0x17c3 как раз-таки отдаёт элемент массива по номеру из массива, который отдаёт первая функция (но это уже пришлось прям хорошо вглядеться).

Кажется, имеет смысл, попытаться заменить конструкции, которые через использование промежуточных переменных обращаются к массиву по номеру.

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

VM42:9 Uncaught ReferenceError: a0_0x17c3 is not defined
at <anonymous>:9:19
at <anonymous>:20:3

Так, возьмём кусок по-больше

function a0_0x4463() {
var _0x2f6e24 = ["push", "webkitHidden", "absolute", "timezone", "touchEvent", "languages", "screen_height", "screenFrame_availBottom", "timezoneOffset", "MozTransform", "navigation", "serif", "showChallengeInProgress", "Comic Sans MS", "vendorFlavors", "createElement", "cpuClass", "document___webdriver_script_func", "_showClientParameters", "Arial Rounded MT Bold", "callSelenium", "505912CNXXgW", "cookietest=1", "getExtension", "enumerateDevices", "call", "\u0415\u0441\u043B\u0438 \u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0435, \u0447\u0442\u043E \u043F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430, \u043F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, ", "body", "no-preference", "document___fxdriver_evaluate", "_phantom", "defineProperty", "canMakePayments", "canvas", "innerHTML", "hiddenFunc", "\u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0441\u0442\u0438.", "challenge_url", "document___webdriver_script_fn", "window_webdriver", "5887428zqSWXX", "value", "Please invoke getUserMedia once.", "cookieEnabled", "opr", "prototype", "202GKvaPN", "srgb", "navigator", "_fillInput", "request_id", "Georgia", "high", "Other", "Opera", "Apple", "active", "width", "challenge_signature", "window_callSelenium", "vendor", "cookietest=", "availLeft", "rec2020", "example.com.store", "sort", "Helvetica Neue", "18IDELAP", "getElementById", "iPhone", "mozInnerScreenY", "MediaSource", "window_buffer", "create", "asObject", "request", "MozBorderImage", "DateTimeFormat", "Lucida Handwriting", "ontouchstart", "Windows Phone", "(max-monochrome: ", "mainFoundParametersAreValid", "assign", "=;expires=Thu, 01 Jan 1970 00:00:00 GMT", "solution", "monochrome", "msWriteProfilerMark", "start", "screenFrame_availTop", "WebKitMediaKeys", "Trebuchet MS", "__webdriver_script_fn", "resolvedOptions", "maxTouchPoints", "undefined", "plugins", "msDontr", "screen_availWidth", "none", "Arial MT", "ngenix_jscc_66dcf4", "open", "Cambria Math", "getting_error", "screen_availHeight", "deviceId", "setClientRequestId", "lineHeight", "_MSGS_CHECKING_FAILED", "window_coach", "missing_image_size_height", "fonts", "android", "toLowerCase", "osCpu", "setOnSuccess", "kind", "_MSGS_CHECKING_IN_PROGRESS", "status", "oprt", "audio", "string", "document___selenium_evaluate", "slice", "now", "Times", "More", "offsetHeight", "matchMedia", "mac", "searchParams", "=; Max-Age=0", "pop", "Century Gothic", "get", "_clearCallbacks", "challenge_complexity", "Call start() method of a timer before ", "video", "http://img.ngenix.net/no.img", "language", "sessionStorage", "matches", "Segoe Print", "Century", "protocol", "spawn", "availHeight", "webgl", "less", "__driver_evaluate", "domAutomation", "TouchEvent", "Less", "498890EPRmSI", "document___fxdriver_unwrapped", "msTransform", "Courier New", "localStorage", "Cambria", "Palatino Linotype", "getStorageUpdates", "MSStream", "Lucida Calligraphy", "\u041F\u043E\u0441\u043B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0432\u044B \u0431\u0443\u0434\u0435\u0442\u0435 \u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0435\u043D\u044B \u043D\u0430 \u0437\u0430\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043C\u0443\u044E ", "setClientRequestAddr", "style", "Chrome", "className", "chrome", "shift", "getBattery", "exports", "n/a", "(min-monochrome: 0)", "more", "property", "safari", "userAgent", "contrast", "canMakePaymentsWithActiveCard", "Linux", "forced-colors", "title", "iPad", "low", "getElementsByTagName", "src", "getAttribute", "location", "hasOwnProperty", "webdriver", "getTimeToEnd", " device.", "Arial Unicode MS", "_startTime", "Intl", "cookie", "&nbsp;", "toStringTag", "Lucida Sans Unicode", "__selenium_evaluate", "fontFamily", "responseText", "Lucida Sans Typewriter", "href", "document___driver_evaluate", "img", "hasAdBlock", "documentElement", "_showElement", "Internet Explorer", "msMaxTouchPoints", "Monaco", "HTTPs is required to get label of this ", "driver", "topself", "MS Reference Sans Serif", "2023432zXWkDo", "span", "msPointerEnabled", "__nightmare", "set", "onerror", "MYRIAD", "getTime", "standalone", "ApplePaySession", "Consolas", "self", "Mac", "charCodeAt", "productSub", "document___driver_unwrapped", "screenResolution", "availTop", "yandex", "Courier", "72px", "setRequestHeader", "msIndexedDB", "\u043E\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044C \u043A \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0443 \u0432\u0435\u0431-\u0440\u0435\u0441\u0443\u0440\u0441\u0430.", "clipboard", "ipad", "availWidth", "_attachCallbacks", "SetField", "application/x-www-form-urlencoded", "\u0412\u0430\u0448 \u0437\u0430\u043F\u0440\u043E\u0441 \u043A \u0432\u0435\u0431-\u0440\u0435\u0441\u0443\u0440\u0441\u0443 \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B\n", "indexOf", "oscpu", "height", "forEach", "MacIntel", "__driver_unwrapped", "OTransform", "-9999px", "performance", "doNotTrack", "offsetWidth", ";path=/;expires=Thu, 01 Jan 1970 00:00:00 UTC", "split", "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT", "appendChild", "MS Outlook", "callPhantom", "ucweb", "navDontr", "screenFrame_availRight", "monospace", "Andale Mono", "openDatabase", "ipod", "Segoe UI", "window__selenium", "join", "MozColumnGap", "send", "_Selenium_IDE_Recorder", "div", "__esModule", "__fxdriver_unwrapped", "UCShellJava", "getContext", "cookietest=1; SameSite=Strict;", "not_set", "windows phone", "cookietest=1; SameSite=Strict; expires=Thu, 01-Jan-1970 00:00:01 GMT", "onError", "MYRIAD PRO", "showChallengeFailAndClientParameters", "type", "removeChild", "Lucida Sans", "document___webdriver_evaluate", "mozInnerScreenX", "Bitstream Vera Sans Mono", "CSSPrimitiveValue", "invertedColors", "random", "849305BppWJh", "MS Sans Serif", "(color-gamut: ", "touchStart", "webkitRequestFullscreen", "MozColumnCount", "ongestureend", "msLaunchUri", "msDoNotTrack", "5231OXUBZN", "datetimeNow", "then", "onSuccess", "setAttribute", "indexedDB", "client-container", "onload", "Microsoft Sans Serif", "screen", "Safari", "emit", "Firefox", "Lucida Fax", "adsbox", "Century Schoolbook", "hasLiedOs", "audioinput", "MS PGothic", "iOS", "Windows", "reload", "webgl_vendor", "Android", "1212039WZVUPu", "buildId", "Verdana", "_fillElement", "document_attribute_driver", "mainParameterKeysPresent", "request_addr", "feature_referrer", "Arial Black", "bind", "UNMASKED_RENDERER_WEBGL", "__fxdriver_evaluate", "webgl_renderer", "Monotype Corsiva", "phantom", "fakeimage", "_showChallengeFail", "Segoe UI Light", "Wingdings", "calling getTimeToEnd().", "colorGamut", "cookiesEnabled", "Calibri", "fail", "reduce", "label", "None", "getTimezoneOffset", "selenium", "mediaDevices", "pike", "toString", "document_attribute_selenium", "Lucida Console", "domAutomationController", "referrer", "document___webdriver_unwrapped", "Arial", "MediaStreamTrack", "MS Serif", "Garamond", "UNMASKED_VENDOR_WEBGL", "Segoe Script", "deviceMemory", "webkit", "document_attribute_webdriver", "hasLiedBrowser", "toSource", "win", "__webdriver_evaluate", "msHidden", "hasFocus", "challenge_url is not set.", "length", "default", "navType", "Segoe UI Symbol", "window__Selenium_IDE_Recorder", "__ybro", "Palatino", "hardwareConcurrency", "colorDepth", "missing_image_size_width", "innerText", "platform", "videoinput", "Times New Roman PS", "block", "mozHidden", "forced", "_jsChallenge", "Comic Sans", "__crWeb", "_getCurTime", "html", "visible", "linux", "iphone"];
a0_0x4463 = function() {
return _0x2f6e24;
};
return a0_0x4463();
}
(function(_0x39af3f, _0x2223b4) {
var _0x2199d1 = a0_0x17c3,
_0x412fe3 = _0x39af3f();
while (!![]) {
try {
var _0x1ddf36 = -parseInt(_0x2199d1(166)) / 1 * (-parseInt(_0x2199d1(314)) / 2) + -parseInt(_0x2199d1(335)) / 3 * (parseInt(_0x2199d1(289)) / 4) + -parseInt(_0x2199d1(157)) / 5 + parseInt(_0x2199d1(308)) / 6 + parseInt(_0x2199d1(423)) / 7 + -parseInt(_0x2199d1(487)) / 8 + parseInt(_0x2199d1(190)) / 9;
if (_0x1ddf36 === _0x2223b4) break;
else _0x412fe3"push";
} catch (_0x2c76ee) {
_0x412fe3"push";
}
}
})(a0_0x4463, 533852);
function a0_0x17c3(_0x6a27a0, _0x1cdc54) {
var _0x446330 = a0_0x4463();
return a0_0x17c3 = function(_0x17c3ff, _0x3d2dfd) {
_0x17c3ff = _0x17c3ff - 120;
var _0x3d5bf4 = _0x446330[_0x17c3ff];
return _0x3d5bf4;
}, a0_0x17c3(_0x6a27a0, _0x1cdc54);
}

Ошибок нет. И, что там с нашим массивом?

a0_0x4463()
(412) ['appendChild', 'MS Outlook', 'callPhantom', 'ucweb', 'navDontr', 'screenFrame_availRight', 'monospace', 'Andale Mono', 'openDatabase', 'ipod', 'Segoe UI', 'window__selenium', 'join', 'MozColumnGap', 'send', '_Selenium_IDE_Recorder', 'div', '__esModule', '__fxdriver_unwrapped', 'UCShellJava', 'getContext', 'cookietest=1; SameSite=Strict;', 'not_set', 'windows phone', 'cookietest=1; SameSite=Strict; expires=Thu, 01-Jan-1970 00:00:01 GMT', 'onError', 'MYRIAD PRO', 'showChallengeFailAndClientParameters', 'type', 'removeChild', 'Lucida Sans', 'document___webdriver_evaluate', 'mozInnerScreenX', 'Bitstream Vera Sans Mono', 'CSSPrimitiveValue', 'invertedColors', 'random', '849305BppWJh', 'MS Sans Serif', '(color-gamut: ', 'touchStart', 'webkitRequestFullscreen', 'MozColumnCount', 'ongestureend', 'msLaunchUri', 'msDoNotTrack', '5231OXUBZN', 'datetimeNow', 'then', 'onSuccess', 'setAttribute', 'indexedDB', 'client-container', 'onload', 'Microsoft Sans Serif', 'screen', 'Safari', 'emit', 'Firefox', 'Lucida Fax', 'adsbox', 'Century Schoolbook', 'hasLiedOs', 'audioinput', 'MS PGothic', 'iOS', 'Windows', 'reload', 'webgl_vendor', 'Android', '1212039WZVUPu', 'buildId', 'Verdana', '_fillElement', 'document_attribute_driver', 'mainParameterKeysPresent', 'request_addr', 'feature_referrer', 'Arial Black', 'bind', 'UNMASKED_RENDERER_WEBGL', '__fxdriver_evaluate', 'webgl_renderer', 'Monotype Corsiva', 'phantom', 'fakeimage', '_showChallengeFail', 'Segoe UI Light', 'Wingdings', 'calling getTimeToEnd().', 'colorGamut', 'cookiesEnabled', 'Calibri', 'fail', 'reduce', 'label', 'None', 'getTimezoneOffset', 'selenium', 'mediaDevices', …]

Порядок и правда другой. Можно, конечно, попытаться понять, что там происходит в этой функции, но что если просто выполнить этот код как есть?

Обновим наш скрипт, сделаем некрасиво, но хочется проверить, что так можно. Возьмём и выполним 3 верхнеуровневых блока, которые мы смотрели в AST

const vm = require("vm");

const jsContext = vm.createContext();

blocks = [0, 3, 1];
for (const block of blocks) {
    const expressionCode = generate(ast.program.body[block]).code;
    vm.runInContext(expressionCode, jsContext)
}

const value = vm.runInContext('a0_0x4463()', jsContext);
console.log(value);

Кажется, удача!

[ 'appendChild', 'MS Outlook', 'callPhantom', 'ucweb', 'navDontr', 'screenFrame_availRight',

Отлично, значит, мы мы можем получить массив в таком порядке, в каком его использует код, и можно попробовать заменить все вызовы функции, которая использует массив на его значения!

Разберём один из кейсов.

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

Нам нужно найти переменные, которым присваивается эта функция. А затем найти, места использования этих новых переменных (функций?), и в местах их вызова заменить конструкцию вызова на значение.

Для начала, глянем в AST, как выглядит присвоение и вызов функции

Попробуем как-то описать, что хотим

const replaceRefsToConstants = {
    VariableDeclarator(path) { //пойдём просматривать все присвоения
        const func_name = 'a0_0x17c3';
        const { id, init } = path.node;
        if(!init || init.type != 'Identifier' || init.name != func_name) return;
        // поиск использования переменной по имени в рамках области видимости
        let {referencePaths} = path.scope.getBinding(id.name);
        for (let referencedPath of referencePaths) {
            // берём только кейсы, где функция вызывается
            if (referencedPath.parent.type === 'CallExpression' && referencedPath.parent.arguments.length == 1) {
                // генерируем код для вызова
                const codeExpr = `${func_name}(${referencedPath.parent.arguments[0].value})`
                // вызываем код (получаем значение)
                const value = vm.runInContext(codeExpr, jsContext);
                // заменяем
                referencedPath.parentPath.replaceWith(t.stringLiteral(value));
            }
        }
    }
};

Зажмуриваемся, и запускаем. Внезапно, работает.

Код до (очень страшно)

var _0x57c309 = a0_0x17c3;
    try {
        var _0x134ab9 = {}
            , _0x2ca384 = Object[_0x57c309(0x12b)](_0x134ab9, 'a', {
            'get': function() {
                return 0x1;
            }
        });
        if (_0x2ca384['a'] !== 0x1)
            throw new Error(_0x57c309(0xd5));
    } catch (_0x1911e6) {
        var _0x2bd411 = ![];
        try {
            var _0x4b21c8 = document[_0x57c309(0x11b)]('a');
            _0x2bd411 = Object[_0x57c309(0x12b)](_0x4b21c8, 'a', {
                'value': 0x1
            })['a'] === 0x1;
        } catch (_0x1b1a26) {}
        var _0x1e0d2d = _0x57c309(0x12b)in Object;
        if (!_0x1e0d2d || _0x2bd411)
            Object['defineProperty'] = function(_0x97ffd1, _0x2ab763, _0x583379) {
                var _0x595f09 = _0x57c309;
                _0x97ffd1 === void 0x0 && (_0x97ffd1 = {});
                _0x2ab763 === void 0x0 && (_0x2ab763 = _0x595f09(0x1bd));
                _0x583379 === void 0x0 && (_0x583379 = {});
                if (_0x595f09(0x135)in _0x583379)
                    _0x97ffd1[_0x2ab763] = _0x583379[_0x595f09(0x135)];
                else
                    _0x595f09(0x193)in _0x583379 && (_0x97ffd1[_0x2ab763] = _0x583379[_0x595f09(0x193)]());
                return _0x97ffd1;

После (всё ещё страшно, но уже есть проблески знакомых слов)

  var _0x57c309 = a0_0x17c3;
  try {
    var _0x134ab9 = {},
      _0x2ca384 = Object["defineProperty"](_0x134ab9, "a", {
        "get": function() {
          return 1;
        }
      });
    if (_0x2ca384["a"] !== 1) throw new Error("fail");
  } catch (_0x1911e6) {
    var _0x2bd411 = ![];
    try {
      var _0x4b21c8 = document["createElement"]("a");
      _0x2bd411 = Object["defineProperty"](_0x4b21c8, "a", {
        "value": 1
      })["a"] === 1;
    } catch (_0x1b1a26) {}
    var _0x1e0d2d = ("defineProperty" in Object);
    if (!_0x1e0d2d || _0x2bd411) Object["defineProperty"] = function(_0x97ffd1, _0x2ab763, _0x583379) {
      var _0x595f09 = _0x57c309;
      _0x97ffd1 === void 0 && (_0x97ffd1 = {});
      _0x2ab763 === void 0 && (_0x2ab763 = _0x595f09(445));
      _0x583379 === void 0 && (_0x583379 = {});
      if (_0x595f09(309) in _0x583379) _0x97ffd1[_0x2ab763] = _0x583379[_0x595f09(309)];
      else _0x595f09(403) in _0x583379 && (_0x97ffd1[_0x2ab763] = _0x583379[_0x595f09(403)]());
      return _0x97ffd1;

Кажется, будто не во всех случаях сработало. _0x583379[_0x595f09(309)] (строка 24).
Оказывается, тут несколько присвоений

var _0x57c309 = a0_0x17c3;
var _0x595f09 = _0x57c309;
_0x595f09(0x1bd));

Ну ладно, попробуем пройти по цепочке всех присвоений.

function traverse_paths(referencePaths){
    for (let referencedPath of referencePaths) {
        if(referencedPath.parent.type === 'CallExpression' && referencedPath.parent.arguments.length == 1) {
            const codeExpr = `a0_0x17c3(${referencedPath.parent.arguments[0].value})`
            const value = vm.runInContext(codeExpr, jsContext);
            referencedPath.parentPath.replaceWith(t.stringLiteral(value));
        }
        if(referencedPath.parent.type == 'VariableDeclarator') {
            let {referencePaths} = referencedPath.scope.getBinding(referencedPath.parent.id.name);
            traverse_paths(referencePaths);
            referencedPath.parentPath.remove()
        }
    }
}
const replaceRefsToConstants = {
    VariableDeclarator(path) {
        const { id, init } = path.node;
        if(!init || init.type != 'Identifier' || init.name != 'a0_0x17c3') return;
        let {referencePaths} = path.scope.getBinding(id.name);
        traverse_paths(referencePaths);
    }
};

Было

    function _0x8a46b3() {
        var _0x8179ff = _0x2fc172, _0x39c6cd = navigator, _0x5cb48a = _0x39c6cd[_0x8179ff(0x1bf)]['toLowerCase'](), _0x4a4dce = _0x39c6cd['oscpu'], _0x2c6a6a = _0x39c6cd[_0x8179ff(0xfe)][_0x8179ff(0x17e)](), _0x2b9892;
        if (_0x5cb48a[_0x8179ff(0x206)](_0x8179ff(0x8f)) >= 0x0)
            _0x2b9892 = 'Windows\x20Phone';
        else {
            if (_0x5cb48a[_0x8179ff(0x206)](_0x8179ff(0xee)) >= 0x0)
                _0x2b9892 = _0x8179ff(0xba);
            else {
                if (_0x5cb48a[_0x8179ff(0x206)](_0x8179ff(0x17d)) >= 0x0)
                    _0x2b9892 = _0x8179ff(0xbd);
                else {
                    if (_0x5cb48a[_0x8179ff(0x206)](_0x8179ff(0x10a)) >= 0x0)
                        _0x2b9892 = _0x8179ff(0x1c2);
                    else {
                        if (_0x5cb48a['indexOf'](_0x8179ff(0x10b)) >= 0x0 || _0x5cb48a['indexOf'](_0x8179ff(0x200)) >= 0x0)
                            _0x2b9892 = _0x8179ff(0xb9);
                        else
                            _0x5cb48a[_0x8179ff(0x206)](_0x8179ff(0x18e)) >= 0x0 ? _0x2b9892 = _0x8179ff(0x1f3) : _0x2b9892 = _0x8179ff(0x141);
                    }
                }
            }
        }

Стало

  function _0x8a46b3() {
    var _0x39c6cd = navigator,
      _0x5cb48a = _0x39c6cd["userAgent"]["toLowerCase"](),
      _0x4a4dce = _0x39c6cd["oscpu"],
      _0x2c6a6a = _0x39c6cd["platform"]["toLowerCase"](),
      _0x2b9892;
    if (_0x5cb48a["indexOf"]("windows phone") >= 0) _0x2b9892 = "Windows Phone";
    else {
      if (_0x5cb48a["indexOf"]("win") >= 0) _0x2b9892 = "Windows";
      else {
        if (_0x5cb48a["indexOf"]("android") >= 0) _0x2b9892 = "Android";
        else {
          if (_0x5cb48a["indexOf"]("linux") >= 0) _0x2b9892 = "Linux";
          else {
            if (_0x5cb48a["indexOf"]("iphone") >= 0 || _0x5cb48a["indexOf"]("ipad") >= 0) _0x2b9892 = "iOS";
            else _0x5cb48a["indexOf"]("mac") >= 0 ? _0x2b9892 = "Mac" : _0x2b9892 = "Other";
          }
        }
      }
    }

Можно упростить похожие кейсы, где используется Windows & navigator

const to_replace = ['window', 'navigator']
        if(init && init.name &&  to_replace.includes(init.name.toLowerCase())) {
            let {referencePaths} = path.scope.getBinding(id.name);
            for (let referencedPath of referencePaths) {
                referencedPath.replaceWith(init);
            }
            path.remove();

        }

Ну, и чтоб 2 раза не вставать, можно ещё заменить скобки на точки (позаимствовал код откуда-то, даже не буду комменты резать)

const validIdentifierRegex =
    /^(?!(?:do|if|in|for|let|new|try|var|case|else|enum|eval|false|null|this|true|void|with|break|catch|class|const|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)$)[$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0\u08a2-\u08ac\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19c1-\u19c7\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa80-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc][$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0\u08a2-\u08ac\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19c1-\u19c7\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa80-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc0-9\u0300-\u036f\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u08e4-\u08fe\u0900-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c01-\u0c03\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c82\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0d02\u0d03\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19b0-\u19c0\u19c8\u19c9\u19d0-\u19d9\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf2-\u1cf4\u1dc0-\u1de6\u1dfc-\u1dff\u200c\u200d\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua620-\ua629\ua66f\ua674-\ua67d\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua880\ua881\ua8b4-\ua8c4\ua8d0-\ua8d9\ua8e0-\ua8f1\ua900-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f]*$/;

const bracketToDotVisitor = {
    MemberExpression(path) {
      let { object, property, computed } = path.node;
      if (!computed) return; // Verify computed property is false
      if (!t.isStringLiteral(property)) return; // Verify property is a string literal
      if (!validIdentifierRegex.test(property.value)) return; // Verify that the property being accessed is a valid identifier

      path.replaceWith(
        t.MemberExpression(object, t.identifier(property.value), false)
      );
    },
};

Результат

function _0x8a46b3() {
    var _0x5cb48a = navigator.userAgent.toLowerCase(),
      _0x4a4dce = navigator.oscpu,
      _0x2c6a6a = navigator.platform.toLowerCase(),
      _0x2b9892;
    if (_0x5cb48a.indexOf("windows phone") >= 0) _0x2b9892 = "Windows Phone";
    else {
      if (_0x5cb48a.indexOf("win") >= 0) _0x2b9892 = "Windows";
      else {
        if (_0x5cb48a.indexOf("android") >= 0) _0x2b9892 = "Android";
        else {
          if (_0x5cb48a.indexOf("linux") >= 0) _0x2b9892 = "Linux";
          else {
            if (_0x5cb48a.indexOf("iphone") >= 0 || _0x5cb48a.indexOf("ipad") >= 0) _0x2b9892 = "iOS";
            else _0x5cb48a.indexOf("mac") >= 0 ? _0x2b9892 = "Mac" : _0x2b9892 = "Other";
          }
        }
      }
    }

Окей, вроде теперь на скрипт можно смотреть без ужаса.

 function _0x3d5149() {
    var _0x2a6aee = new _0x9a736c();
    return _0x2a6aee.SetField("osCpu", function() {
      return window.navigator.oscpu;
    }), _0x2a6aee.SetField("colorDepth", function() {
      return window.screen.colorDepth;
    }), _0x2a6aee.SetField("deviceMemory", function() {
      return window.navigator.deviceMemory;
    }), _0x2a6aee.SetField("hardwareConcurrency", function() {
      return navigator.hardwareConcurrency;
    }), _0x2a6aee.SetField("openDatabase", function() {
      return !!window.openDatabase;
    }), _0x2a6aee.SetField("cpuClass", function() {
      return navigator.cpuClass;
    }), _0x2a6aee.SetField("plugins", function() {
      return navigator.plugins.length;
    }), _0x2a6aee.SetField("vendor", function() {
      return window.navigator.vendor;
    }), _0x2a6aee.SetField("__nightmare", function() {
      return window.__nightmare;
    }), _0x2a6aee.SetField("callPhantom", function() {
      return window.callPhantom;
    }), _0x2a6aee.SetField("_phantom", function() {
      return window._phantom;
    }), _0x2a6aee.SetField("phantom", function() {
      return window.phantom;
    }), _0x2a6aee.SetField("webdriver", function() {
      return window.navigator.webdriver;
    }), _0x2a6aee.SetField("userAgent", function() {
      return window.navigator.userAgent;
    }), _0x2a6aee.SetField("window_buffer", function() {
      return window.Buffer !== undefined;

А что там с нашим solution? Поищем куски кода с ним связанные

  function _0x431fb9(_0x3dc2cd, _0x1cd50c, _0x5951a8, _0xfa0a1e, _0x55469f) {
    var _0x3b9f23 = new _0x72d966(_0xfa0a1e, _0x55469f),
      _0x205858 = {};
    _0x1cd50c !== null && (_0x205858.solution = _0x1cd50c); // Вот он!
    Object.assign(_0x205858, _0x5951a8 === null ? {} : _0x5951a8);
    if (_0x3dc2cd !== undefined && _0x3dc2cd.challenge_url !== undefined) _0x3b9f23.request(_0x3dc2cd.challenge_url, _0x205858);
    else throw "`challenge_url` is not set.";
  }

Как вариант, можно сохранить html & JS локально, запустить отладчик в браузере, чтоб разобраться быстрее. Однако, попытка оказывается провальной.

l_deobf.js:1199 Uncaught challenge_url is not set.

Находим в коде

else throw "`challenge_url` is not set.";

Пытаемся разобраться. Кажется, эта функция должна возвращать что-то валидное, но там пусто.

_0x44fc51(_0x36a798, "challenge_cookie_expires");

Находим специфичную функцию

 function _0x4426f5(_0x418da9) {
    var _0x455654 = [],
      _0x129416 = ("; " + document.cookie).split("; " + _0x418da9 + "=");
    for (var _0x3238a6 = 0; _0x3238a6 < _0x129416.length; _0x3238a6++) {
      var _0x46518b = _0x129416.pop().split(";").shift();
      _0x455654.push(_0x46518b);
    }
    return _0x455654;
  }

И понимаем, что у нас, кажется, при локальном тестировании не хватает каких-то кук. А что там у нас было про какие-то недолгие куки выше?

curl 'https://www.letu.ru/s/api/product/listing/v1/products?Nrpp=36&No=0' -v

< HTTP/2 503 
< server: nginx
< date: Fri, 19 Jan 2024 13:26:23 GMT
< content-type: text/html; charset=utf-8
< content-length: 3722
< set-cookie: ngenix_jscc_66dcf4=challenge_signature=%2BES%2FGbAju4wyY178ojl9wyuXoaU%3D&challenge_cookie_expires=1705670903&verification_cookie_expires=1705671383&request_addr=194.126.177.23&request_id=2820396dcc8e14f31bd5f3ddda81647e&challenge_url=%2Fjs-challenge-validation-fc2d28ffd461fea6d64ed377bc467993&challenge_complexity=10; Max-Age=120; Domain=www.letu.ru; Path=/; SameSite=Lax

Добавляем её в браузер, и теперь можем локально дебажить скрипт.

Выясняется, что в функцию, генерирующую solution передаётся объект

{
    "challenge_signature": "+ES/GbAju4wyY178ojl9wyuXoaU=",
    "challenge_cookie_expires": "1705670903",
    "verification_cookie_expires": "1705671383",
    "request_addr": "194.126.177.23",
    "request_id": "2820396dcc8e14f31bd5f3ddda81647e",
    "challenge_url": "/js-challenge-validation-fc2d28ffd461fea6d64ed377bc467993",
    "challenge_complexity": "10"
}

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

Получилось 100 строк, многовато, но зато, как-будто, даже работает.


_0x99a16 = {
    "mainParameterKeysPresent": true,
    "mainFoundParametersAreValid": true,
    "challenge_url": "/js-challenge-validation-fc2d28ffd461fea6d64ed377bc467993",
    "challenge_cookie_expires": "1705670903",
    "challenge_complexity": "10",
    "verification_cookie_expires": "1705671383",
    "challenge_signature": "+ES/GbAju4wyY178ojl9wyuXoaU=",
    "request_id": "2820396dcc8e14f31bd5f3ddda81647e",
    "request_addr": "194.126.177.23",
    "feature_referrer": false
}

function _0x436f6f(_0x111dea) {
    return _0x885d53(_0x5d55f5(_0x111dea));
}

function _0x4cb554(_0x5aeb77) {
    var _0x391202 = 0;
    if (_0x5aeb77 > 0) {
        while ((_0x5aeb77 & 128) == 0) {
            _0x5aeb77 = _0x5aeb77 << 1, _0x391202++;
        }
        return _0x391202;
    } else return 8;
}

function _0x885d53(_0x3b9958) {
    var _0x120f4f = 0;
    for (var _0x589bce = 0; _0x589bce < _0x3b9958.length; _0x589bce++) {
        var _0x1a6c52 = _0x4cb554(_0x3b9958[_0x589bce]);
        _0x120f4f += _0x1a6c52;
        if (_0x1a6c52 < 8) break;
    }
    return _0x120f4f;
}

function _0x10130b(_0x4ae728, _0x3fecb8) {
    var _0x38f852 = (_0x4ae728 & 65535) + (_0x3fecb8 & 65535),
        _0x4e49bb = (_0x4ae728 >> 16) + (_0x3fecb8 >> 16) + (_0x38f852 >> 16);
    return _0x4e49bb << 16 | _0x38f852 & 65535;
}

function _0x2517e5(_0x17f992, _0x22280e) {
    return _0x17f992 << _0x22280e | _0x17f992 >>> 32 - _0x22280e;
}

function _0x435362(_0x5b0113, _0x5a5293, _0x47ad06, _0x4befce) {
    if (_0x5b0113 < 20) return _0x5a5293 & _0x47ad06 | ~_0x5a5293 & _0x4befce;
    if (_0x5b0113 < 40) return _0x5a5293 ^ _0x47ad06 ^ _0x4befce;
    if (_0x5b0113 < 60) return _0x5a5293 & _0x47ad06 | _0x5a5293 & _0x4befce | _0x47ad06 & _0x4befce;
    return _0x5a5293 ^ _0x47ad06 ^ _0x4befce;
}

function _0x30f152(_0x3f4b19) {
    return _0x3f4b19 < 20 ? 1518500249 : _0x3f4b19 < 40 ? 1859775393 : _0x3f4b19 < 60 ? -1894007588 : -899497514;
}

function _0x1575a9(_0x2d1fa5, _0x3d69d8) {
    _0x2d1fa5[_0x3d69d8 >> 5] |= 128 << 24 - _0x3d69d8 % 32, _0x2d1fa5[(_0x3d69d8 + 64 >> 9 << 4) + 15] = _0x3d69d8;
    var _0x101107 = Array(80),
        _0x580b35 = 1732584193,
        _0x3ebd1a = -271733879,
        _0x11f647 = -1732584194,
        _0x5ae550 = 271733878,
        _0x5ba453 = -1009589776;
    for (var _0x596e23 = 0; _0x596e23 < _0x2d1fa5.length; _0x596e23 += 16) {
        var _0x4c1dfa = _0x580b35,
            _0x1938af = _0x3ebd1a,
            _0x2204d5 = _0x11f647,
            _0x4adedb = _0x5ae550,
            _0x205635 = _0x5ba453;
        for (var _0x843967 = 0; _0x843967 < 80; _0x843967++) {
            if (_0x843967 < 16) _0x101107[_0x843967] = _0x2d1fa5[_0x596e23 + _0x843967];
            else _0x101107[_0x843967] = _0x2517e5(_0x101107[_0x843967 - 3] ^ _0x101107[_0x843967 - 8] ^ _0x101107[_0x843967 - 14] ^ _0x101107[_0x843967 - 16], 1);
            var _0x27a46d = _0x10130b(_0x10130b(_0x2517e5(_0x580b35, 5), _0x435362(_0x843967, _0x3ebd1a, _0x11f647, _0x5ae550)), _0x10130b(_0x10130b(_0x5ba453, _0x101107[_0x843967]), _0x30f152(_0x843967)));
            _0x5ba453 = _0x5ae550, _0x5ae550 = _0x11f647, _0x11f647 = _0x2517e5(_0x3ebd1a, 30), _0x3ebd1a = _0x580b35, _0x580b35 = _0x27a46d;
        }
        _0x580b35 = _0x10130b(_0x580b35, _0x4c1dfa), _0x3ebd1a = _0x10130b(_0x3ebd1a, _0x1938af), _0x11f647 = _0x10130b(_0x11f647, _0x2204d5), _0x5ae550 = _0x10130b(_0x5ae550, _0x4adedb), _0x5ba453 = _0x10130b(_0x5ba453, _0x205635);
    }
    return Array(_0x580b35, _0x3ebd1a, _0x11f647, _0x5ae550, _0x5ba453);
}

function _0xede9c0(_0x3d66a6) {
    var _0x4996bd = [];
    for (var _0x7d0c9e = 0; _0x7d0c9e < _0x3d66a6.length * 32; _0x7d0c9e += 8) _0x4996bd.push(_0x3d66a6[_0x7d0c9e >> 5] >>> 24 - _0x7d0c9e % 32 & 255);
    return _0x4996bd;
}

function _0x4f622d(_0xac3d37) {
    var _0x114852 = Array(_0xac3d37.length >> 2);
    for (var _0x41c1a7 = 0; _0x41c1a7 < _0x114852.length; _0x41c1a7++) _0x114852[_0x41c1a7] = 0;
    for (var _0x41c1a7 = 0; _0x41c1a7 < _0xac3d37.length * 8; _0x41c1a7 += 8) _0x114852[_0x41c1a7 >> 5] |= (_0xac3d37.charCodeAt(_0x41c1a7 / 8) & 255) << 24 - _0x41c1a7 % 32;
    return _0x114852;
}

function _0x5d55f5(_0x5b03ce) {
    return _0xede9c0(_0x1575a9(_0x4f622d(_0x5b03ce), _0x5b03ce.length * 8));
}

function _0x1fe037(_0x18219e) {
    var _0x5e58f0 = parseInt(Math.random() * 1000000),
        _0x55871c = null,
        _0x497b9e = _0x18219e.challenge_complexity,
        _0x5ac824 = _0x18219e.challenge_signature;
    while (_0x55871c != _0x497b9e) {
        _0x5e58f0 += 1, _0x55871c = _0x436f6f(_0x5ac824 + String(_0x5e58f0));
    }
    return _0x5e58f0 = String(_0x5e58f0), _0x5e58f0;
}

console.log(_0x1fe037(_0x99a16))

Из интересного, каждый раз значение разное. Однако, заменив

parseInt(Math.random() * 1000000)

на что-нибудь вроде 555555 значения стабильны.

Попробуем проверить, работает ли наш генератор solution.

Погнали, куки там действуют только 2 минуты, надо сделать всё быстро.

curl 'https://www.letu.ru/s/api/product/listing/v1/products' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' -v

< HTTP/2 503 
< server: nginx
< date: Fri, 19 Jan 2024 13:56:25 GMT
< content-type: text/html; charset=utf-8
< content-length: 3722
< set-cookie: ngenix_jscc_66dcf4=challenge_cookie_expires=1705673514&verification_cookie_expires=1705673994&challenge_complexity=10&challenge_url=%2Fjs-challenge-validation-fc2d28ffd461fea6d64ed377bc467993&request_addr=194.126.177.23&request_id=77e33758cb2a609696c0b6660827828a&challenge_signature=vBaJ95hHGtuvtGLCZc5UigmpWpU%3D; Max-Age=120; Domain=www.letu.ru; Path=/; SameSite=Lax

Конвертируем строку в объект

_0x99a16 = {
    "challenge_cookie_expires": "1705673514",
    "verification_cookie_expires": "1705673994",
    "challenge_complexity": "10",
    "challenge_url": "/js-challenge-validation-fc2d28ffd461fea6d64ed377bc467993",
    "request_addr": "194.126.177.23",
    "request_id": "77e33758cb2a609696c0b6660827828a",
    "challenge_signature": "vBaJ95hHGtuvtGLCZc5UigmpWpU="
}

и вызываем скрипт с функциями

node solution_funcs.js
260091

Делаем новый курл, не забываем добавить cookie, которые получили ранее.

curl 'https://www.letu.ru/js-challenge-validation-fc2d28ffd461fea6d64ed377bc467993' \
  -H 'content-type: application/x-www-form-urlencoded' \
  -H 'cookie: ngenix_jscc_66dcf4=challenge_cookie_expires=1705673514&verification_cookie_expires=1705673994&challenge_complexity=10&challenge_url=%2Fjs-challenge-validation-fc2d28ffd461fea6d64ed377bc467993&request_addr=194.126.177.23&request_id=77e33758cb2a609696c0b6660827828a&challenge_signature=vBaJ95hHGtuvtGLCZc5UigmpWpU%3D' \
  --data-raw 'solution=260091&osCpu=undefined&colorDepth=30&deviceMemory=8&hardwareConcurrency=10&openDatabase=false&cpuClass=undefined&plugins=5&vendor=Google%20Inc.&__nightmare=undefined&callPhantom=undefined&_phantom=undefined&phantom=undefined&webdriver=false&userAgent=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F120.0.0.0%20Safari%2F537.36&window_buffer=false&window_coach=false&rhino=false&msDontr=undefined&navDontr=null&language=ru&languages=ru&screen_width=1728&screen_availWidth=1728&screen_height=1117&screen_availHeight=1085&hasLiedOs=false&hasLiedBrowser=false&clipboard=true&getBattery=true&locationBar=true&mozInnerScreenX=false&mozInnerScreenY=false&domAutomation=false&domAutomationController=false&topself=true&hasFocus=true&navType=1&window_webdriver=undefined&window__Selenium_IDE_Recorder=undefined&window_callSelenium=undefined&window__selenium=undefined&document___webdriver_script_fn=undefined&document___driver_evaluate=undefined&document___webdriver_evaluate=undefined&document___selenium_evaluate=undefined&document___fxdriver_evaluate=undefined&document___driver_unwrapped=undefined&document___webdriver_unwrapped=undefined&document___selenium_unwrapped=undefined&document___fxdriver_unwrapped=undefined&document___webdriver_script_func=undefined&document_attribute_selenium=null&document_attribute_webdriver=null&document_attribute_driver=null&fonts=Andale%20Mono%2CArial%2CArial%20Black%2CArial%20Hebrew%2CArial%20Narrow%2CArial%20Rounded%20MT%20Bold%2CArial%20Unicode%20MS%2CComic%20Sans%20MS%2CCourier%2CCourier%20New%2CGeneva%2CGeorgia%2CHelvetica%2CHelvetica%20Neue%2CImpact%2CLUCIDA%20GRANDE%2CMicrosoft%20Sans%20Serif%2CMonaco%2CPalatino%2CTahoma%2CTimes%2CTimes%20New%20Roman%2CTrebuchet%20MS%2CVerdana%2CWingdings%2CWingdings%202%2CWingdings%203&hasAdBlock=false&platform=MacIntel&sessionStorage=true&localStorage=true&indexedDB=true&maxTouchPoints=0&touchEvent=false&touchStart=false&vendorFlavors=chrome&cookiesEnabled=true&colorGamut=p3&invertedColors=undefined&forcedColors=undefined&monochrome=0&contrast=undefined&missing_image_size_width=0&missing_image_size_height=364&timezoneOffset=-240&timezone=Asia%2FTbilisi&datetimeNow=1705669859240&webgl_vendor=37445&webgl_renderer=37446&msTransform=false&MozTransform=false&MozColumnCount=false&MozBorderImage=false&MozColumnGap=false&OTransform=false&buildId=undefined&canMakePayments=__getting_error__&canMakePaymentsWithActiveCard=__getting_error__&hiddenFunc=webkitHidden&audioinput=undefined&videoinput=undefined&audiooutput=undefined&screenResolution=1728%2C1117&screenFrame_availTop=32&screenFrame_availRight=0&screenFrame_availBottom=0&screenFrame_availLeft=0' \
  -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' --compressed -v

> POST /js-challenge-validation-fc2d28ffd461fea6d64ed377bc467993 HTTP/2
> Host: www.letu.ru
> Accept: */*
> Accept-Encoding: deflate, gzip
> content-type: application/x-www-form-urlencoded
> cookie: ngenix_jscc_66dcf4=challenge_cookie_expires=1705673514&verification_cookie_expires=1705673994&challenge_complexity=10&challenge_url=%2Fjs-challenge-validation-fc2d28ffd461fea6d64ed377bc467993&request_addr=194.126.177.23&request_id=77e33758cb2a609696c0b6660827828a&challenge_signature=vBaJ95hHGtuvtGLCZc5UigmpWpU%3D
> user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
> Content-Length: 2724
> 
< HTTP/2 200 
< server: nginx
< date: Fri, 19 Jan 2024 14:11:09 GMT
< content-type: application/octet-stream
< set-cookie: ngenix_jscv_d84ce224cbdf=cookie_expires=1705673994&bot_profile_check=true&cookie_signature=ic7hoMGt%2FQIBRJCRE%2B2L%2FzxqkW8%3D; Max-Age=525; Domain=www.letu.ru; Path=/; HttpOnly; SameSite=Lax

Код 200 - очень волнительно, попробуем сделать запрос с новой кукой.

curl 'https://www.letu.ru/s/api/product/listing/v1/products' \
-H 'cookie: ngenix_jscv_d84ce224cbdf=cookie_expires=1705673994&bot_profile_check=true&cookie_signature=ic7hoMGt%2FQIBRJCRE%2B2L%2FzxqkW8%3D' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' -v

< HTTP/2 200 
...
< set-cookie: anonymous_user_cart=; expires=Sun, 24-Sep-2073 10:15:58 UTC; path=/; HttpOnly
< set-cookie: anonymous_user_last_viewed=; expires=Sun, 24-Sep-2073 10:15:58 UTC; path=/; HttpOnly
< set-cookie: anonymous_user_wishlist=; expires=Sun, 24-Sep-2073 10:15:58 UTC; path=/; HttpOnly
< set-cookie: anonymous_user_city=; expires=Sun, 24-Sep-2073 10:15:58 UTC; path=/; HttpOnly
< set-cookie: COOKIE-BEARER=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1MTY2MzE1MTk2NTAiLCJhdXRob3JpdGllcyI6IlJPTEVfQU5PTllNT1VTIiwic2l0ZUlkIjoic3RvcmVNb2JpbGVSVSIsImlhdCI6MTcwNTY3Mzc1OCwiZXhwIjoxNzA1NzYwMTU4fQ.89DMhNAIRq4EhKNvBppMMH8Tg9nvvOOw1e1AAc-q65CY_DoXinCByqRhyv7BMl7H14FZlWLQn0vyjNOWvZn-9w; expires=Sat, 20-Jan-2024 13:15:58 UTC; path=/; HttpOnly
< set-cookie: JSESSIONID=7YlicKuAJhszor0jlVzHjw7SRxE_.prod-wru-a-08; path=/; HttpOnly; Max-Age=86400; Expires=Sat, 20-Jan-2024 14:15:58 GMT
< set-cookie: language=ru-RU; path=/; Max-Age=1567800000; Expires=Sun, 24-Sep-2073 10:15:58 GMT
< set-cookie: pseudo_user_id=pu249572488; path=/; Max-Age=1567800000; Expires=Sun, 24-Sep-2073 10:15:58 GMT
< set-cookie: rrSession=ac2c90111f694a2e99b15cf745970069; path=/; Max-Age=1567800000; Expires=Sun, 24-Sep-2073 10:15:58 GMT
...
{"adultsOnly":false,"products":[{"article":"","repositoryId":"50300346","instalmentInfo":"","defaultCategoryId":"880004","fastDelivery":"","brandName":"NATURA SIBERICA","displayName":"Крем для рук укрепляющий","largeImageUrl":"/common/img/pim/2023/09/LG_39944d8d-7b04-487d-881d-855bbbfa353c.jpg","minSkuId":"64400657","minSkuName":"75 мл","sefName":"natura-siberica-krem-dlya-ruk-ukreplyayushchii","relevanceCoefs":"","franchise":"","countReview":1,"numberOfSkuAvailable":1,"numberOfSkuInStock":0,"quantityInCart":0,"discountPercent":10,"discountDescription":"- Скидка 10% по клубной карте","instalmentPayment":0.0,"instalmentPaymentAsString":"0","rating":5.0,"discountedPrice":170.0,"discountedPriceAsString":"170","priceWithoutCoupons":170.0,"priceWithoutCouponsAsString":"170","rawPrice":189.0,"rawPriceAsString":"189","minSkuPrice":189.0,"minSkuPriceAsString":"189","adultsOnly":false,"isInWishList":false,"isOnePriceForAllSkus":true,"isOutOfStock":true,"noLongerAvailable":false,"showVolumes":false,"skuList":[],"analyticsCategory":["Уход за кожей","Средства для ухода за телом","Средства для ухода за руками"],"color":[],"media":[],"appliedMarkers":[]},{"article":"","repositoryId":"50300387","instalmentInfo":""

Бинго, прошли проверку браузера без браузера! Шок контент.

Я честно попытался сконвертировать код для генерации solution c JS на python, чтобы затолкать его в 1 файл, но быстро это сделать не вышло. Попытался использовать chatGPT, но он тоже не очень смог, поэтому я решил, оставить код внутри node, просто повешать его на сервис, и из другого скрипта запросом просить ответ.

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

Набросок Python версии получился примерно такой:

for i in range(0, 1000):
    params['No'] = i * 36
    response = requests.get('https://www.letu.ru/s/api/product/listing/v1/products', params=params, headers=headers, cookies=cookies)
    if response.status_code != 200:
        print(f'Response code:'response.status_code)
        challenge_cookies = response.cookies.get_dict()
        challenge_obj = challenge_cookies.get('ngenix_jscc_66dcf4')
        decoded_query_dict = {k: urllib.parse.unquote_plus(v) for k, v in
                              [pair.split('=') for pair in challenge_obj.split('&')]}
        api_resp = requests.post('http://localhost:3000/', data=decoded_query_dict)
        x = api_resp.json()['solution']
        data = f'solution={x}&osCpu=undefined&colorDepth=30&deviceMemory=8&hardwareConcurrency=10&openDatabase=false&cpuClass=undefined&plugins=5&vendor=Google%20Inc.&__nightmare=undefined&callPhantom=undefined&_phantom=undefined&phantom=undefined&webdriver=false&userAgent=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F120.0.0.0%20Safari%2F537.36&window_buffer=false&window_coach=false&rhino=false&msDontr=undefined&navDontr=null&language=ru&languages=ru%2Cen-US%2Ctr%2Cen-CA%2Cfr-CA%2Cde%2Cka%2Cde-DE%2Czh-CN&screen_width=1728&screen_availWidth=1728&screen_height=1117&screen_availHeight=1085&hasLiedOs=false&hasLiedBrowser=false&clipboard=true&getBattery=true&locationBar=true&mozInnerScreenX=false&mozInnerScreenY=false&domAutomation=false&domAutomationController=false&topself=true&hasFocus=true&navType=1&window_webdriver=undefined&window__Selenium_IDE_Recorder=undefined&window_callSelenium=undefined&window__selenium=undefined&document___webdriver_script_fn=undefined&document___driver_evaluate=undefined&document___webdriver_evaluate=undefined&document___selenium_evaluate=undefined&document___fxdriver_evaluate=undefined&document___driver_unwrapped=undefined&document___webdriver_unwrapped=undefined&document___selenium_unwrapped=undefined&document___fxdriver_unwrapped=undefined&document___webdriver_script_func=undefined&document_attribute_selenium=null&document_attribute_webdriver=null&document_attribute_driver=null&fonts=Andale%20Mono%2CArial%2CArial%20Black%2CArial%20Hebrew%2CArial%20Narrow%2CArial%20Rounded%20MT%20Bold%2CArial%20Unicode%20MS%2CComic%20Sans%20MS%2CCourier%2CCourier%20New%2CGeneva%2CGeorgia%2CHelvetica%2CHelvetica%20Neue%2CImpact%2CLUCIDA%20GRANDE%2CMicrosoft%20Sans%20Serif%2CMonaco%2CPalatino%2CTahoma%2CTimes%2CTimes%20New%20Roman%2CTrebuchet%20MS%2CVerdana%2CWingdings%2CWingdings%202%2CWingdings%203&hasAdBlock=false&platform=MacIntel&sessionStorage=true&localStorage=true&indexedDB=true&maxTouchPoints=0&touchEvent=false&touchStart=false&vendorFlavors=chrome&cookiesEnabled=true&colorGamut=p3&invertedColors=undefined&forcedColors=undefined&monochrome=0&contrast=undefined&missing_image_size_width=0&missing_image_size_height=426&timezoneOffset=-240&timezone=Asia%2FTbilisi&datetimeNow=1705602281905&webgl_vendor=undefined&webgl_renderer=undefined&msTransform=false&MozTransform=false&MozColumnCount=false&MozBorderImage=false&MozColumnGap=false&OTransform=false&buildId=undefined&canMakePayments=__getting_error__&canMakePaymentsWithActiveCard=__getting_error__&hiddenFunc=webkitHidden&audioinput=undefined&videoinput=undefined&audiooutput=undefined&screenResolution=1728%2C1117&screenFrame_availTop=32&screenFrame_availRight=0&screenFrame_availBottom=0&screenFrame_availLeft=0'
        resp = requests.post(
            f'https://www.letu.ru/{decoded_query_dict["challenge_url"]}',
            cookies=challenge_cookies,
            headers=headers,
            data=data,
        )
        print(resp.text)
        cookies.update(resp.cookies.get_dict())
        response = requests.get('https://www.letu.ru/s/api/product/listing/v1/products', params=params, headers=headers,
                                cookies=cookies)
    print(f'{datetime.datetime.now() - start_time}, request #{i}, status: {response.status_code}, '
          f'response length: {len(response.text)}, Offset: {i*36}, products count: { len(response.json()["products"])}')

Запустив скрипт, можно убедиться, что стратегия работает.

Что можно было-бы сделать ещё?
Поиграть с обфусцированным скриптом, сделать его ещё красивее. Например, заменить конструкции вида !![] что равно true, и всякое другое похожее. Можно взять какой-нибудь словарь адекватных (или нет) слов и переименовать переменные.

С точки зрения работы с сайтом, можно посмотреть, как использовать заголовки и cookies без прохода этого челенджа, наверняка это тоже, с какими-то ограничениями можно. Однако мой интерес был именно в разборе скрипта проверки браузера.

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

P.S. Уже разобрав скрипт, я решил поискать, что это за система, сомнительно, что это самописная от лэтуаль. Судя по неймингу в cookie - это https://ngenix.net/industries/ecommerce/.

Описание проблемы

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

Как мы можем помочь?

Сервисы NGENIX позволяют четко отделить запросы легитимных пользователей от запросов, генерируемых плохими ботами. Ограничивая частоту запросов с определенных IP, платформа NGENIX помогает противодействовать bruteforce-атакам, в также настраивать правила фильтрации и блокировки бот-трафика на основе ряда признаков. Создать правила обработки запросов по таким признакам, как геолокация, User-Agent и др. можно практически мгновенно, и в течение нескольких минут они применятся на всей платформе.

Асинхронный код писать стало лень, однако 1000 запросов с 1 немецкого IP (VPN) без браузера, с одинаковым UA таки благополучно отправились за 20 минут, использован был лишь 1 вызов солвера. Соответственно, получено аттрибутов (возможно, не полных) на 36000 продуктов.

Источник: https://habr.com/ru/articles/787706/


Интересные статьи

Интересные статьи

Относительная доступность методов секвенирования ДНК и большое количество доступной в открытых источниках генетической информации сделала актуальной разработку нейронных сетей, предназначенных для ана...
Пост в помощь тем кто интересуется практической пользой от применения нейросетей на фондовом рынке.
В статье я расскажу историю одной разработки, выполненной небольшим коллективом Читать далее
Когда начинается разговор про перформанс-тестирование, то большинство программистов размышляет только о проведении замеров и сборе метрик, в то время как намного важнее задуматься об ан...
Хабр, привет! Сегодня я покажу, как бесплатно проанализировать интересующий вас сайт через сервис Similarweb. Подробно расскажу обо всех нюансах и проведу по всем шагам от регистрации...