An NTP tunnel for Cobalt Strike beacons using External-C2.
Supports:
- Beaconing over NTP
- Tunneling of multiple clients
- Various config options
(Sorry for the quality, GitHub limits to 10 MB, a full quality version is in the repo at misc/CS-EXTC2-NTP_Proof.mp4
)
8mb.video-9Aq-J71kosrN.mp4
- Install Visual Studio
- Start an External C2 beacon in Cobalt Strike
- Edit the
constants.hpp
:
Note: There are other config options in these namespaces, each labeled/has a description, including sleep times, packet sizes, etc etc.
The following are the ones that need to be configured correctly though.
CS-EXTC2-NTP-SERVER
:
namespace TeamServer {
const std::string address = "10.0.0.100"; //The address of the teamserver to talk to
constexpr int port = 2222; //What port the ExtC2 listener is on. CS by default chooses port 2222
const std::string pipeName = "somepipe"; //the pipe that the beacon will create & talk on.
}
CS-EXTC2-NTP
:
namespace Controller {
//!! This is NOT the teamserver address. This is for pointing our packets at the NTP server (of which talks to the teamserver)
//What port the controller is listening on with it's NTP service
constexpr uint16_t port = 123; //replace with the port the controller is listening on
//Address of the controller.
const std::string serverAddress = "127.0.0.1"; //replace with the address the controller is at
}
namespace Beacon {
const std::string pipeName = "somepipe"; // the pipe name to read from. MUST match the value in TeamServer::pipeName in the controller
}
namespace Client {
const uint8_t arch = 0x64; //0x64 OR 0x86. Set this based on how this is being compiled. 0x64 for 64 bit, 0x86 for 32.
}
- Open
server/CS-EXTC2-NTP-SERVER.sln
in visual studio, compile and or run it. - Open
client/CS-EXTC2-NTP.sln
in visual studio, compile and or run it. - Check your CS client for a new beacon.
The tunnel itself is fairly simple. Every packet is a normal NTP packet, and all the data hides in extension fields.
Additionally, There are two main jobs the Client and Controller have:
- Payload Retrieval: The initial Payload Retrieval from the TeamServer
- The Beacon Loop: The continuous comms between Client, Controller, and TeamServer
-
Client sends a
getIdPacket
packet to get a client ID -
Controller responds with a
idPacket
packet containing the client ID. Client saves this for all further outbound packets -
Client sends a packet with a
giveMePayload
extension.- The data in this packet will either be
0x86
, or0x64
, depending on the architecture of the host. This must be manually set in the client, this is not dynamically determined.
- The data in this packet will either be
-
Controller reaches out to TeamServer to get the payload.
-
In the NTP response (to step 3), Controller returns a packet with a
sizePacket
extension, denoting the size of the payload. -
The client initiates chunking by iterating over the inbound payload size, retrieving chunks until the entire payload has been received.
- The data in this packet is
0x00
, which denotes "keep sending me chunks of the payload"
- The data in this packet is
-
The packet back from the Controller is a
dataFromTeamserver
packet, which contains a chunk of payload. -
(not shown below) Once the entire payload has been retrieved, it is in injected into a new thread, and run.
- Note: This uses a basic CreateThread injection method. It’s going to be detected — please modify or replace with your preferred technique. (Code is located in
injector.cpp
)
- Note: This uses a basic CreateThread injection method. It’s going to be detected — please modify or replace with your preferred technique. (Code is located in
- Client reads beacon data from the named pipe.
- Client sends a packet with a
sizePacket
extension, which contains the size of the data retrieved from the beacon. - Controller responds with
sizePacketAcknowledge
- Client then initiates chunking by iterating over the beacon data size, sending chunks with
dataForTeamserver
extensions until the entire beacon data has been sent to the Controller - Controller sends
sizePacketAcknowledge
each chunked packet to signify it made it. - Once the Controller has all the data, it forwards the data onto the TeamServer.
- The controller then gets the response of the teamserver.
- Client then sends a packet with the
getDataFromTeamserverSize
extension. - The Controller responds with the size of the data from the Teamserver, via a packet with a
sizePacket
extension. - Client then initiates chunking by sending a packet with the
getDataFromTeamserver
extension. - The Controller responds with a packet with the
dataFromTeamserver
extension. This packet contiains a chunk of data of the TeamServers response. - Once the Client has all the data, it forwards the data onto the beacon, via the named pipe. It then loops to step 1.
All the extension fields used in the project.
Before exploring the individual extension fields, it's important to understand their underlying structure.
All Extension Field packets follow this format:
Bytes 0-1: Extension Field ID
Bytes 2-3: Size of extension field data
Bytes 4-7: Session ID for NTP packet
Bytes 8-X: (optional) Additional Data (buffered with 0x00 to 4 bit boundary)
If we include the 48 byte NTP packet, this turns into:
Bytes 0-47: NTP Packet
Bytes 48-49: Extension Field ID
Bytes 50-51: Size of extension field data
Bytes 52-55: Session ID for NTP packet
Bytes 56-X: (optional) Additional Data (buffered to 4 bit boundary)
Why extension fields, and why this exact structure? Two reasons:
-
RFC 5905 defines the structure of NTP extension fields
-
The packets show up as NTP in wireshark (other structs that I tried can throw a malformed packet error)
So, if I want these packets to look legit, they need to be structured as such.
This extension field is used to communicate the size of an inbound message. This is crucial for chunk based communication.
Bytes 0-1: 0x51, 0x2E
Bytes 2-3: Size of Data
Bytes 4-7: Unique ID
Bytes 8-11 Size of total data to be sent
An acknowledge packet that is used to say the Controller received your message. Should probably have been named "packetAcknowledge" instead of sizePacketAcknowledge", but it was first used for acknowledging size packets, and I haven't updated it yet. Either way, if/when it gets changed, the header of 0x51, 0x2E
will stay the same.
Bytes 0-1: 0x51, 0x2E
Bytes 2-3: Size of Data
Bytes 4-7: Blank (0xFF,0xFF,0xFF,0xFF)
Bytes 0-1: 0x00, 0x01
Bytes 2-3: Size of Data
Bytes 4-7: ClientID
Bytes 8: Architechure (0x86, 0x64, or 0x00) 0x00 = continue sending payload, 0x86/0x64 are their own respective arch.
Used by the client to get an ID from the Controller.
Bytes 0-1: 0x12, 0x34
Bytes 2-3: Size of Data
Bytes 4-7: ClientID - Blank (0xFF,0xFF,0xFF,0xFF)
Used in response to a getIdPacket
from the Controller to give the client an ID. This ID is stored in the Data section of the
extension field, NOT in the ClientID field (which is blank).
Bytes 0-1: 0x1D, 0x1D
Bytes 2-3: Size of Data
Bytes 4-7: ClientID - Blank (0xFF,0xFF,0xFF,0xFF)
Bytes 8-11: ClientID for the client.
Used in response from the Controller to tunnel data back in. Contains data that came from the teamserver.
Bytes 0-1: 0x02, 0x04
Bytes 2-3: Size of Data
Bytes 4-7: ClientID - Blank (0xFF,0xFF,0xFF,0xFF)
Bytes 4-end of packet: Chunked data from teamserver
This extension field is used to requset TeamServer data that is stored on the controller. Used in chunking
Bytes 0-1: 0x00, 0x02
Bytes 2-3: Size of Data
Bytes 4-7: Unique ID
The response from the Controller, with data from the teamserver, meant for the client. Used in Chunking
Bytes 0-1: 0x02, 0x04
Bytes 2-3: Size of Data
Bytes 4-7: Unique ID
Bytes 8-end of packet: Chunked data from the teamserver for the client
A size packet that is specifically for getting the size of the teamserver data, for the client.
Bytes 0-1: 0x03, 0x04
Bytes 2-3: Size of Data
Bytes 4-7: Unique ID
Bytes 8-11: Size of Teamserver Data for client
Data meant for the teamserver, from the client. Used in chunking
Bytes 0-1: 0x02, 0x05
Bytes 2-3: Size of Data
Bytes 4-7: Unique ID
Bytes 8-end of packet: Chunked data from the client for the teamserver