Автоматизация этапа разведки пентеста на Golang

D2

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

Трям! Здравствуйте! Меня всё ещё зовут miserylord!

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

Разделы
  • Начало - введение в проект, инструменты для OSINT, настройка IDE для удобной работы с Linux через SSH
  • Реализация проекта - структура, код
  • Эффективность - добавление горутин
  • Практичность - подключение телеграм-бота
Спойлер: Начало
С чего начинается тестирование на проникновение?

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

Инструментов для OSINT существует огромное множество. Сотни инструментов пассивной разведки можно найти на сайте OSINT Framework, для активной разведки можно взглянуть на предустановленные скрипты для Kali Linux.

В коде я буду совмещать два этапа и возьму лишь несколько инструментов для демонстрации работы.

Подготовка

Поскольку большинство скриптов, которые мы будем использовать, написаны под Linux, работа будет происходить именно на нем. Сервер, на котором запущен Linux, не обязан иметь графическое окружение или большие мощности, однако без запуска кода на нем убедиться в его работоспособности будет невозможно. Удобнее всего будет работать с помощью следующего сетапа:

На Linux-машине необходимо запустить SSH-сервер. После этого на основной машине в IDE VS Code установить расширение Remote Explorer. Сначала нужно прописать адрес сервера, открыв командную палитру с помощью комбинации Ctrl+Shift+P и выбрав Add New SSH Host..., затем подключиться с помощью команды Remote-SSH: Connect to Host. Для каждого SSH-клиента VS Code создает новый объект, что означает, что все расширения, установленные в VS Code, не будут работать для SSH и их нужно установить дополнительно. Это очень удобный способ взаимодействия с кодом.

Само собой, на Linux должен быть установлен Go, а также все утилиты, с которыми будет происходить работа.

Спойлер: Реализация проекта
Идея

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

Перейдем к коду.
Структура

Структура проекта на начальном этапе будет следующей:
  • Файл main.go для запуска основной программы
  • Папка (она же пакет) report для формирования отчета по тестированию
  • Папка scanner для основного кода сканера, а также всех модулей; каждая проверка будет вынесена в отдельный файл
  • Папка config для конфигураций API ключей
  • Позже в проект будет добавлена папка tg для работы с телеграм-ботом.

В коде main.go на данном этапе будет следующий код:
C-подобный: Скопировать в буфер обмена
Код:
package main
 
    import (
        "osint_sc/scanner"
    )
 
    func main() {
        // 1
        URL := "https://example.com"
 
        // 2
        s := scanner.NewScanner(URL)
 
        // 3
        s.Scan()
 
    }

  1. Ссылка для тестирования
  2. Создание нового сканера
  3. Запуск сканера

report.go

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

import (
    "fmt"
    "os"
)

// 1
type Report struct {
    sections map[string]string
}

// 2
func NewReport() *Report {
    return &Report{
        sections: make(map[string]string),
    }
}

// 3
func (r *Report) AddSection(title, content string) {
    r.sections[title] = content
}

// 4
func (r *Report) Save(filename string) error {
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()

    for title, content := range r.sections {
        _, err := file.WriteString(fmt.Sprintf("== %s ==\n%s\n\n", title, content))
        if err != nil {
            return err
        }
    }
    return nil

}

  1. Определяем структуру Report, которая содержит одно поле sections. sections представляет собой карту с ключами и значениями типа string. Ключ — это название раздела (например, whois), а значение — его содержание (буквально лог программы/ответ от API).
  2. Функция NewReport создаёт и возвращает указатель на новый экземпляр структуры Report. При этом инициализируется пустая карта для хранения разделов отчёта.
  3. Метод AddSection добавляет новый раздел в отчёт. Он будет вызван после каждой проверки.
  4. Метод Save сохраняет отчёт в файл. Он принимает название файла в качестве аргумента. Далее происходит создание файла и обработка ошибки. Вызов defer используется для закрытия файла при любых обстоятельствах. Затем каждое значение карты записывается в файл, учитывая формат. Если всё прошло успешно, возвращается nil.

robotstxt.go

На веб-сайтах очень часто существует директория /robots.txt. Этот файл создается для поисковых роботов и в нем указывается, какие страницы не следует индексировать. Например, владелец сайта не хотел бы, чтобы страница /admin попала в поисковую выдачу. В то же время этот файл может раскрывать информацию, полезную для тестирования (интересные директории, админки).

Помимо этой директории могут существовать и другие, которые раскрывают полезную информацию. Например, можно проверить такие директории, как .git и .env. Однако для демонстрации кода будет использован только файл robots.txt; другие можно добавить в коде аналогичным образом.

В папке scanner создадим файл robotstxt.go для написания первой проверки в рамках сканера.

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

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

// 1
func (s *Scanner) checkRobotsTxt() (string, error) {
    // 2
    resp, err := http.Get(fmt.Sprintf("%s/robots.txt", s.URL))
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    // 3
    if resp.StatusCode != http.StatusOK {
        return "", fmt.Errorf("failed to fetch /robots.txt, status code: %d", resp.StatusCode)
    }

    // 4
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }

    // 5
    return string(body), nil
}


  1. Определяем метод checkRobotsTxt для будущего сканера. Он возвращает строку и ошибку.
  2. Выполняется HTTP GET запрос для получения robots.txt. defer resp.Body.Close() гарантирует закрытие тела ответа после завершения метода, чтобы освободить ресурсы.
  3. Проверяется статус-код ответа. Если он не равен 200 OK, метод возвращает пустую строку и ошибку, указывающую на неудачу при получении robots.txt и включающую код состояния ответа.
  4. Читаем содержимое ответа.
  5. Преобразуем содержимое тела ответа из байтового среза в строку и возвращаем её вместе с nil в качестве ошибки, что указывает на успешное выполнение.

scanner.go

В той же папке напишем код для scanner.go:


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

import (
    "fmt"
    "osint_sc/report"
)

// 1
type Scanner struct {
    URL string
}

// 2
func NewScanner(url string) *Scanner {
    return &Scanner{URL: url}
}

// 3
func (s *Scanner) Scan() {
    // 4
    report := report.NewReport()

    // 5
    robotsContent, err := s.checkRobotsTxt()
    if err != nil {
        report.AddSection("robots.txt", fmt.Sprintf("robots.txt is not available: %v", err))
    } else { report.AddSection("robots.txt", robotsContent) }

    // 6
    err = report.Save("report.txt")
    if err != nil {
        fmt.Printf("Error saving report: %v\n", err)
        return
    }

    // 7
    fmt.Println("Scan completed and report saved to report.txt")

}

  1. Структура Scanner содержит одно поле: URL — строка, которая будет сканироваться.
  2. Создаем конструктор NewScanner, который создает новый экземпляр структуры Scanner с заданным URL и возвращает указатель на этот экземпляр.
  3. Метод Scan выполняет процесс сканирования.
  4. Создаем новый отчет.
  5. Метод s.checkRobotsTxt() вызывается для получения содержимого robots.txt. Если возникает ошибка, в отчет добавляется раздел с текстом "robots.txt is not available", содержащий сообщение об ошибке. Если содержимое успешно получено, оно добавляется в отчет в разделе robots.txt.
  6. Сохраняем результат, вызывая метод Save.
  7. Логируем информацию о завершении сканирования.

Далее я буду реализовывать функции для других сканеров. Для вызова их в scanner.go следует лишь заменить checkRobotsTxt на новую функцию и изменить то, что записывается в AddSection. Вернемся к этому коду на этапе внедрения горутин.

whois.go

Команда whois используется для получения информации о домене, включая дату регистрации и дату истечения домена, а также данные владельца — имя, контактные данные, адрес и организацию, через которую зарегистрирован домен.

Напишем код в файле whois.go.

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

import (
    "os/exec"
    "strings"
)

func (s *Scanner) checkWhois() (string, error) {

    // 1
    URL := strings.TrimPrefix(s.URL, "https://")
    // 2
    cmd := exec.Command("whois", URL)

    // 3
    output, err := cmd.CombinedOutput()
    if err != nil {
        return "", err
    }

    return string(output), nil
}

В целом код повторяет checkRobotsTxt, за исключением нескольких моментов:
  1. Удаляем префикс https://, поскольку команда whois принимает домен без него.
  2. Для вызова команды, так как если бы мы выполняли её вручную через терминал, создается объект с помощью exec.Command, который принимает имя команды (whois) и аргумент (URL).
  3. Метод CombinedOutput выполняет команду и возвращает её вывод, то есть то, что мы бы увидели, введя команду вручную.

nslookup.go

Команда nslookup используется для выполнения запросов к DNS-серверам. Она позволяет узнать, какие IP-адреса связаны с определенным доменным именем.


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

import (
    "os/exec"
    "strings"
)

func (s *Scanner) checkNslookup() (string, error) {

    URL := strings.TrimPrefix(s.URL, "https://")
    cmd := exec.Command("nslookup", URL)

    output, err := cmd.CombinedOutput()
    if err != nil {
        return "", err
    }

    return string(output), nil

}


Код точь-в-точь повторяет метод checkWhois(), за исключением замены команды whois на nslookup.

reversIP.go

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

Существует несколько способов такой проверки. В примере я воспользуюсь сервисом https://viewdns.info/. Регистрируемся на сайте и получаем API-ключ. Создаем папку config и в ней файл api.go, в котором будут храниться API-ключи. Почему не .env? В данном контексте .env может не иметь смысла, однако, если вы планируете публичное размещение кода через Git, имеет смысл хранить такие данные в .env.

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

var Viewdns = "11111111111111111111apikey"


Из документации определяем, как составить запрос. Первые 250 запросов бесплатны. Пишем код в файле reverseIP.go.


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

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os/exec"
    "osint_sc/config"
    "regexp"
    "strings"
)

func (s *Scanner) checkReverseIP() (string, error) {
    URL := strings.TrimPrefix(s.URL, "https://")

    // 1
    cmd := exec.Command("host", URL)

    output, err := cmd.CombinedOutput()
    if err != nil {
        return "", err
    }

    // 2
    ipAddresses := extractIPAddresses(string(output))

    // 3
    var result strings.Builder

    // 4
    for _, ip := range ipAddresses {
        // 5
        urlFinal := fmt.Sprintf("https://api.viewdns.info/reverseip/?host=%s&apikey=%s&output=json", ip, config.Viewdns)

        resp, err := http.Get(urlFinal)
        if err != nil {
            return "", err
        }
        defer resp.Body.Close()

        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            return "", err
        }

        // 6
        formattedJSON, err := s.formatJSON(string(body))
        if err != nil {
            return "", err
        }
        // 7
        result.WriteString(fmt.Sprintf("IP: %s\nResponse: %s\n\n", ip, formattedJSON))
    }
    // 8
    return result.String(), nil
}


  1. Для получения списка IP-адресов для домена используем команду host. В целом она аналогична команде nslookup, но её вывод немного удобнее для дальнейшей работы, поскольку у домена может быть больше одного IP-адреса. Учитываем этот момент.
  2. Получаем список IP-адресов из общего лога. Эту функцию реализуем ниже.
  3. Создаем переменную result типа strings.Builder. strings.Builder используется для создания строк путём последовательного добавления данных.
  4. Выполняем запрос к API для каждого IP-адреса.
  5. Формируем URL-запрос для API, указывая API и ключ из конфигурации. Также параметр output может иметь несколько значений; в коде он захардкожен на JSON. Выполняем HTTP GET запрос к API и читаем тело ответа.
  6. Ответ приходит в формате JSON. Поскольку если записать JSON строкой в текстовый файл, он будет записан буквально в строку, для удобства чтения будет полезно немного отформатировать его. Используем функцию formatJSON, которую реализуем ниже.
  7. Результат добавляется в буфер strings.Builder с указанием IP-адреса и форматированного JSON-ответа.
  8. Преобразуем содержимое strings.Builder в строку и возвращаем её вместе с nil.

Функцию extractIPAddresses реализуем в том же файле.

C-подобный: Скопировать в буфер обмена
Код:
func extractIPAddresses(data string) []string {
    // 1
    re := regexp.MustCompile(`has address (\d+\.\d+\.\d+\.\d+)`)
    // 2
    lines := strings.Split(data, "\n")
    var ipAddresses []string
    // 3
    for _, line := range lines {
        if matches := re.FindStringSubmatch(line); matches != nil {
            ipAddresses = append(ipAddresses, matches[1])
        }
    }
    return ipAddresses

}
  1. Создаем регулярное выражение с помощью regexp.MustCompile. Оно будет искать строки, содержащие текст has address, за которым следует IP-адрес, по шаблону has address (\d+\.\d+\.\d+\.\d+). Это именно тот шаблон, который найдет нужную информацию.
  2. Разбиваем строку data на подстроки по символу новой строки (\n). Создаем пустой срез ipAddresses для хранения найденных IP-адресов. В данном контексте строки — это просто лог вывода команды host.
  3. Для каждой строки вызываем регулярное выражение для поиска совпадений. Если совпадение найдено, добавляем IP-адрес в срез ipAddresses.

Ну и наконец напишем метод formatJSON в файле scanner.go.

C-подобный: Скопировать в буфер обмена
Код:
func (s *Scanner) formatJSON(jsonStr string) (string, error) {
    // 1
    var jsonObj interface{}
    if err := json.Unmarshal([]byte(jsonStr), &jsonObj); err != nil {
        return "", fmt.Errorf("error unmarshalling JSON: %v", err)
    }

    // 2
    formattedJSON, err := json.MarshalIndent(jsonObj, "", "  ")
    if err != nil {
        return "", fmt.Errorf("error formatting JSON: %v", err)
    }

    // 3
    return string(formattedJSON), nil
}
  1. Пустой интерфейс используется для представления любого типа данных, в данном случае — произвольного JSON. Преобразует строку JSON (jsonStr) в объект Go (jsonObj).
  2. json.MarshalIndent: Преобразует объект Go обратно в строку JSON с отступами для улучшения читаемости. По сути, это делает JSON таким же форматом, каким он был получен, только в рамках текстового файла.
  3. Преобразует отформатированную строку JSON (которая была в формате []byte) обратно в строку (string) и возвращает её вместе с nil.

wayback.go

Wayback Machine — это сервис, который служит архивом веб-страниц, сохраняя их снимки на разных временных точках. Это позволяет возвращаться к предыдущим состояниям страниц и увидеть, как тот или иной сайт выглядел в прошлом, что может быть полезно.

У сервиса есть API https://archive.org/help/wayback_api.php. Оно бесплатное и имеет довольно ограниченные возможности — по сути, позволяет проверить, доступны ли снимки для запрашиваемого веб-сайта или нет. Эндпоинт, указанный в документации, работает не совсем так, как указано, и мне пришлось немного изменить запрос, чтобы в итоге получить нужный результат. Давайте напишем код в файле wayback.go.

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

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

func (s *Scanner) checkWayBack() (string, error) {

    urlFinal := fmt.Sprintf("https://archive.org/wayback/available?url=%s/", s.URL)

    resp, err := http.Get(urlFinal)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    formattedJSON, err := s.formatJSON(string(body))
    if err != nil {
        return "", err
    }

    return formattedJSON, nil

}

Код точно такой же, как и в методе checkReverseIP(): указываем ссылку (в таком формате, иначе ответ будет некорректным), делаем запрос, читаем ответ, форматируем и возвращаем JSON в виде строки.

ssl.go

Проверку сертификата SSL можно организовать различными способами. В данном примере я буду использовать API ssl-checker.io, хотя эту же задачу можно решить, не прибегая к сторонним API (например, с помощью openssl). Проверка сертификата помимо прочего показывает дату, до которой сертификат действителен.


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

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

func (s *Scanner) checkSSL() (string, error) {
    URL := strings.TrimPrefix(s.URL, "https://")
    urlFinal := fmt.Sprintf("https://ssl-checker.io/api/v1/check/%s", URL)

    resp, err := http.Get(urlFinal)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }

    formattedJSON, err := s.formatJSON(string(body))
    if err != nil {
        return "", err
    }

    return formattedJSON, nil
}

Код аналогичен коду в методе checkWayBack(), за исключением замены эндпоинта.

nmap.go

Nmap — это мощный инструмент для сканирования сетей и обнаружения хостов, служб, операционных систем и уязвимостей. Он имеет множество возможностей, Nmap Cheat Sheet 2024: All the Commands Flags и каждый может использовать свои собственные флаги. В коде используется стандартная команда nmap без дополнительных аргументов (проверка топ-1000 портов, SYN scan).

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

import (
    "os/exec"
    "strings"
)

func (s *Scanner) checkNmap() (string, error) {

    URL := strings.TrimPrefix(s.URL, "https://")
    cmd := exec.Command("nmap", URL)

    output, err := cmd.CombinedOutput()
    if err != nil {
        return "", err
    }

    return string(output), nil
}

sublister3r.go

Sublist3r — это отличная утилита для поиска поддоменов сайта. Работает очень быстро и эффективно. Для её запуска необходим Python, а также ряд библиотек. Процесс установки описан в их репозитории: https://github.com/aboul3la/Sublist3r.


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

import (
    "os/exec"
    "strings"
)

func (s *Scanner) checkSublister3r() (string, error) {

    // 1
    dir := "/home/user/Desktop/script/Sublist3r/sublist3r.py"

    URL := strings.TrimPrefix(s.URL, "https://")
    // 2
    cmd := exec.Command("python3", dir, "-d", URL)
    output, err := cmd.CombinedOutput()
    if err != nil {
        return "", err
    }

    return string(output), nil

}
ring(output), nil

}

  1. Переменная dir содержит полный путь к директории, в которой находится скрипт.
  2. Формируем команду, указываем python3 первым аргументом, путь к скрипту, флаг -d и ссылку.

dirb.go

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

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

import (
    "os/exec"
)

func (s *Scanner) checkDirb() (string, error) {

    cmd := exec.Command("dirb", s.URL, "/usr/share/dirb/wordlists/common.txt")

    output, err := cmd.CombinedOutput()
    if err != nil {
        return "", err
    }

    return string(output), nil
}

Небольшое изменение в этом коде заключается в добавлении ещё одного аргумента методу exec.Command. В целом, сколько бы ни было аргументов, все они передаются через запятую. Помимо ссылки, также передается словарь для брута директорий. Этот словарь автоматически устанавливается при установке dirb. Помимо него, есть словарь с большим количеством слов — big.txt.

nikto.go

Автоматические сканеры веб-уязвимостей помогают сразу обнаружить известные уязвимости. Для примера воспользуемся бесплатным сканером Nikto. На мой взгляд, он слабее относительно других решений, хотя разные сканеры могут найти разные уязвимости. Nikto можно заменить на любой другой, например, на Acunetix (с которым возможна удобная интеграция по API).


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

import (
    "os/exec"
)

func (s *Scanner) checkNikto() (string, error) {

    cmd := exec.Command("nikto", "-h", s.URL)

    output, err := cmd.CombinedOutput()
    if err != nil {
        return "", err
    }

    return string(output), nil
}


Всё также, за исключением того, что мы передаём аргумент -h (хост) методу exec.Command.

whatweb.go

И последним добавим инструмент WhatWeb. WhatWeb определяет, какие технологии использует сайт для работы, а также способен определить версии библиотек и фреймворков. Некоторые инструменты, типа Wappalyzer, дадут больше информации, однако WhatWeb полностью бесплатен.

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

import (
    "os/exec"
)

func (s *Scanner) checkWhatWeb() (string, error) {

    cmd := exec.Command("whatweb", s.URL)

    output, err := cmd.CombinedOutput()
    if err != nil {
        return "", err
    }

    return string(output), nil
}

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

Спойлер: Эффективность
Горутины

Код прекрасно работает, но он может работать быстрее. По сути, мы можем запустить каждый метод в отдельной горутине и значительно сократить общее время работы. Это всё равно что открыть сразу все терминалы и ввести все команды параллельно. Общее время работы программы сократится до времени последнего ответа из всех (того процесса, который работает дольше всего; в этом коде это, скорее всего, будет Nikto).

Подробнее о конкурентном программировании можно почитать в статье, посвящённой брутфорсу почтовых адресов по IMAP.

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

В файл report.go внесём несколько изменений.

C-подобный: Скопировать в буфер обмена
Код:
// 1
type Report struct {
    mu       sync.Mutex
    sections map[string]string
}

// 2
func (r *Report) AddSection(title, content string) {
    r.mu.Lock()
    defer r.mu.Unlock()

    r.sections[title] = content
}

  1. Для обеспечения безопасного доступа к данным структуры из разных горутин, используем мьютекс, добавив его в структуру Report.
  2. Изменим код метода AddSection. Сперва происходит блокировка мьютекса: текущая горутина захватывает мьютекс и получает эксклюзивный доступ к данным структуры. До тех пор, пока мьютекс не будет разблокирован, никакая другая горутина не сможет захватить его и изменить данные структуры. Разблокировка будет вызвана автоматически, когда выполнение функции AddSection завершится благодаря использованию defer.

Внесем изменения в файл scanner.go. Помимо горутин, немного изменим логику: теперь функция будет возвращать имя сохраненного файла, что необходимо для дальнейшей работы с Telegram ботом.

C-подобный: Скопировать в буфер обмена
Код:
// 1
func (s *Scanner) Scan() string {
    report := report.NewReport()
    // 2
    var wg sync.WaitGroup

    // 3
    tasks := []struct {
        name   string
        action func() (string, error)
    }{
        {"robots.txt", s.checkRobotsTxt},
        {"whois", s.checkWhois},
        {"nslookup", s.checkNslookup},
        {"reverseIP", s.checkReverseIP},
        {"wayback", s.checkWayBack},
        {"ssl", s.checkSSL},
        {"sublist3r", s.checkSublister3r},
        {"nmap", s.checkNmap},
        {"dirb", s.checkDirb},
        {"nikto", s.checkNikto},
        {"whatweb", s.checkWhatWeb},
    }

    // 4
    for _, task := range tasks {
        wg.Add(1)
        go func(name string, action func() (string, error)) {
            defer wg.Done()
            content, err := action()
            if err != nil {
                report.AddSection(name, fmt.Sprintf("%s is not available: %v", name, err))
            } else {
                report.AddSection(name, content)
            }
        }(task.name, task.action)
    }

    // 5
    wg.Wait()

    // 6
    filename := fmt.Sprintf("report_%s.txt", sanitizeURL(s.URL))
    err := report.Save(filename)
    if err != nil {
        log.Fatalf("Error saving report: %v\n", err)
    }

    fmt.Println("Scan completed")
    // 7
    return filename
}

  1. Добавляем тип возвращаемых данных в сигнатуру функций.
  2. Создаём группу ожидания для синхронизации параллельных задач.
  3. Объявляем и инициализируем срез структур с именем и действием. Таким образом, лаконично объединяем весь предыдущий код. Имя будет использоваться для записи в отчёт, действие — для вызова метода.
  4. Выполняем итерацию по всем задачам в срезе tasks. Увеличиваем счётчик группы ожидания. Запускаем новую горутину, используя слово go, и создаём анонимную функцию с аргументами структуры tasks. Уменьшаем счётчик группы ожидания. Выполняем action и добавляем результат в Report.
  5. Эта строка блокирует выполнение до тех пор, пока счётчик задач не станет равным нулю, то есть до завершения всех запущенных горутин.
  6. Сохраняем отчёт; название файла зависит от URL. Функцию sanitizeURL реализуем ниже.
  7. Возвращается имя файла, в котором сохранён отчёт.

Напишем функцию sanitizeURL ниже в том же файле.

C-подобный: Скопировать в буфер обмена
Код:
func sanitizeURL(url string) string {
    // 1
    replacer := strings.NewReplacer(
        "/", "",
        "\\", "",
        ":", "",
        "*", "",
        "?", "",
        "\"", "",
        "<", "",
        ">", "",
        "|", "",
        "https", "",
    )
    return replacer.Replace(url)
}

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

Код стал работать быстрее! Остался финальный штрих — интеграция кода в телеграм-бота.

Спойлер: Практичность
Телеграм-бот

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

Переходим в Телеграм, создаём нового бота с помощью BotFather, следуем инструкциям, получаем токен и сохраняем его в файле tg.go в папке config.


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

var Token = "1:1111111"

Для работы с Телеграмом воспользуемся библиотекой github.com/go-telegram-bot-api/telegram-bot-api, установив её в проект. Создадим папку tg и файл bot.go с основной логикой работы.


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

import (
    "net/url"
    "osint_sc/scanner"
    "strings"

    tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)

// 1
func HandleUpdate(bot *tgbotapi.BotAPI, update tgbotapi.Update) {
    // 2
    if update.Message == nil {
        return
    }

    // 3
    msg := update.Message
    text := msg.Text

    // 4
    if strings.HasPrefix(text, "/start") {
        reply := "Send me a URL"
        message := tgbotapi.NewMessage(msg.Chat.ID, reply)
        bot.Send(message)
        return
    }

    // 5
    if isValidURL(text) {
        // 6
        s := scanner.NewScanner(text)
        result := s.Scan()

        // 7
        file := tgbotapi.NewDocumentUpload(msg.Chat.ID, result)
        bot.Send(file)
    } else { // 8
        reply := "Please send a valid URL."
        message := tgbotapi.NewMessage(msg.Chat.ID, reply)
        bot.Send(message)
    }
}

// 9
func isValidURL(link string) bool {
    parsedURL, err := url.ParseRequestURI(link)
    return err == nil && parsedURL.Scheme != "" && parsedURL.Host != ""
}

  1. Функция обрабатывает обновления от Telegram-бота. Она принимает два параметра: bot — экземпляр API бота, update — объект обновления от Telegram.
  2. Если в обновлении нет сообщения, функция ничего не делает и просто возвращается.
  3. Сохраняем объект сообщения и извлекаем из него текст.
  4. Обработка первого сообщения от бота (команда /start), в ответ на него бот попросит прислать ссылку.
  5. Если текст сообщения является валидным URL, выполняется дальнейшая обработка.
  6. Создаём сканер и запускаем сканирование.
  7. Создаём объект для отправки документа: msg.Chat.ID — ID чата, в который нужно отправить документ (тот, из которого пришло сообщение), result — файл для отправки.
  8. Если URL невалиден, бот отправляет сообщение "Please send a valid URL".
  9. Проверка валидности ссылки: функция использует url.ParseRequestURI — стандартный метод для проверки правильности URL.

Ну и наконец вызовем бота в файле main.go.


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

import (
    "fmt"
    "log"
    "osint_sc/config"
    "osint_sc/tg"

    tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)

func main() {

    // 1
    bot, err := tgbotapi.NewBotAPI(config.Token)
    if err != nil {
        log.Fatalf("Failed to create bot: %v", err)
    }

    // 2
    fmt.Printf("Authorized on account %s\n", bot.Self.UserName)

    // 3
    u := tgbotapi.NewUpdate(0)
    u.Timeout = 60

    // 4
    updates, err := bot.GetUpdatesChan(u)
    if err != nil {
        log.Fatalf("Failed to get updates: %v", err)
    }

    // 5
    for update := range updates {
        tg.HandleUpdate(bot, update)
    }

}

  1. Создаём новый экземпляр бота, используя токен из config.Token. Если создание бота не удалось, завершаем работу программы с сообщением об ошибке.
  2. Логируем успешное подключение.
  3. Задаём параметры для получения обновлений.
  4. Метод GetUpdatesChan устанавливает соединение с сервером Telegram и начинает получать обновления, которые поступают в указанный канал.
  5. Обрабатываем каждое обновление, поступающее через канал updates.

Теперь, запустив бота, у нас есть возможность использовать Telegram для запуска сканирования сайта со всеми подключёнными утилитами!

Проект завершён. Возможности можно расширять, реализуя несколько сценариев взаимодействия с ботом и добавляя различные команды с разными настройками сканера.

На этом буду прощаться и возвращаться в Телемелитряндию!
 
Сверху Снизу