[Из песочницы] Обход CloudFlare ScrapeShield в Java (Android)

6281630e29884e0e9db761167faa4f7f.pngВ некоторый момент времени мне пришлось решить проблему, описанную в заголовке. Долго размышлял, писать ли очевидное, в итоге решил, что кому-то это может пригодиться.

Причина достаточно тривиальна — являясь автором Android-клиента к весьма нишевому сайту, я в то же время не вхожу ни в число его администраторов, ни в число соучредителей. Таки образом ни о каких решениях руководства сайта я не осведомлён до момента их фактического вступления в силу.

Не так давно на этот сайт началась DDoS-атака, и администрация включила DDoS-защиту от CloudFlare. Соответственно, приложение-клиент, использовавшее до этого стандартные механизмы авторизации через POST+Cookie, перестало авторизовывать пользователей. Общение с администрацией ни к чему не привело — «что мы можем сделать, лучше уж без мобильных клиентов, чем вообще никак».

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

Покопавшись в поисковике, нашлась ровно одна ссылка, ведущая на проект, делающий нечто весьма похожее. Написанный на Python с использованием node.js или другого совместимого провайдера для PyExecJS. При всём моём уважении к Python, его использование в легковесном нишевом приложении было неоправданной роскошью, на интеграцию которой пришлось бы потратить много часов. Было принято стратегическое решение переписать код решателя на Java.

Некоторые примечания/неочевидности, возникшие во время написания кода:

В качестве JS-провайдера был выбран Mozilla Rhino, предоставляющий при отключённых оптимизациях совместимый с Dalvik-bytecode интерфейс. UserAgent’ы, присущие автоматическим запросам, отклоняются с Error 503. Любые «Java/1.5.0_08», «libcurl-agent/1.0» и им подобные строки мгновенно отвергаются. Прежде чем хоть что-то пробовать, замаскируйтесь под UserAgent современного браузера. В качестве Http-клиента использовалась реализация от Apache. Ей я больше доверяю, чем HttpURLConnection, которую продвигают разработчики Android, но это дело вкусов. Можете использовать любую совместимую реализацию, например, OkHttpClient Важно: если вы хотите позже отображать какие-то данные c сайта в WebView, нужно учесть две вещи: У http-клиента должен быть в точности такой же UserAgent, что и у WebView (используйте settings.userAgentString у WebView) После получения cf_clearance-куки необходимо синхронизировать её с WebView (пример кода ниже) Итоговый вариант ниже. Сколочен на скорую руку, но базовое представление о том, как всё работает, даёт.

private final static Pattern OPERATION_PATTERN = Pattern.compile («setTimeout\\(function\\(\\)\\{\\s+(var t, r, a, f.+?\\r?\\n[\\s\\S]+? a\\.value =.+?)\\r?\\n»); private final static Pattern PASS_PATTERN = Pattern.compile («name=\«pass\» value=\»(.+?)\»); private final static Pattern CHALLENGE_PATTERN = Pattern.compile («name=\«jschl_vc\» value=\»(\\w+)\»);

abstract public HttpResponse getPage (URI url, HashMap headers) throws IOException; abstract public CookieStore getCookieStore ();

public boolean cloudFlareSolve (String responseString) { // инициализируем Rhino Context rhino = Context.enter (); try { String domain = «www.example.com»; // CF ожидает ответа после некоторой задержки Thread.sleep (5000); // вытаскиваем арифметику Matcher operationSearch = OPERATION_PATTERN.matcher (responseString); Matcher challengeSearch = CHALLENGE_PATTERN.matcher (responseString); Matcher passSearch = PASS_PATTERN.matcher (responseString); if (! operationSearch.find () || ! passSearch.find () || ! challengeSearch.find ()) return false; String rawOperation = operationSearch.group (1); // операция String challengePass = passSearch.group (1); // ключ String challenge = challengeSearch.group (1); // хэш // вырезаем присвоение переменной String operation = rawOperation .replaceAll («a\\.value =(.+?) \\+ .+?;»,»$1») .replaceAll (»\\s{3,}[a-z](?: = |\\.).+»,»); String js = operation.replace (»\n»,»); rhino.setOptimizationLevel (-1); // без этой строки rhino не запустится под Android Scriptable scope = rhino.initStandardObjects (); // инициализируем пространство исполнения

// either do or die trying int result = ((Double) rhino.evaluateString (scope, js, «CloudFlare JS Challenge», 1, null)).intValue (); String answer = String.valueOf (result + domain.length ()); // ответ на javascript challenge

final List params = new ArrayList<>(3); params.add (new BasicNameValuePair («jschl_vc», challenge)); params.add (new BasicNameValuePair («pass», challengePass)); params.add (new BasicNameValuePair («jschl_answer», answer)); HashMap headers = new HashMap<>(1); headers.put («Referer», «http://» + domain + »/»); // url страницы, с которой было произведено перенаправление String url = «http://» + domain + »/cdn-cgi/l/chk_jschl?» + URLEncodedUtils.format (params, «windows-1251»); HttpResponse response = getPage (URI.create (url), headers); if (response.getStatusLine ().getStatusCode () == HttpStatus.SC_OK) { // в ответе придёт страница, указанная в Referer response.getEntity ().consumeContent (); // с контентом можно делать что угодно return true; } } catch (Exception e) { return false; } finally { Context.exit (); // выключаем Rhino } return false; }

private void syncCookiesWithWebViews () { List cookies = getCookieStore ().getCookies (); CookieManager cookieManager = CookieManager.getInstance (); // CookieManager служит для синхронизации cookies между WebView for (Cookie cookie: cookies) { String cookieString = cookie.getName () + »=» + cookie.getValue () + »; domain=» + cookie.getDomain (); cookieManager.setCookie («diary.ru», cookieString); } } Код клиента опубликован под GPLv3, так что, скорее всего, о нём скоро прознает и CloudFlare, что приведёт к смене алгоритма. Тем не менее, я не приверженец принципа security by obscurity и задачу пускать мобильных пользователей до спада DDoS удалось решить.

Спасибо за внимание. Вопросы/замечания в комментарии.

© Habrahabr.ru