How to block web vulnerability scanners with iptables.

We are often asked how to block the DFind vulnerability scanner (A.K.A. “w00tw00t.at.ISC.SANS.DFind”) with NinjaFirewall, our Web Application Firewall for PHP and WordPress. The bad news is that you can’t. But the good news is that you don’t have to, because it is already blocked! Several years ago, I wrote an article about it, so let’s see that issue again.

If you have one ore more servers, there are a lot of chances that you saw the “w00tw00t” connection attempts inside your Apache logs:

149.202.124.2 - - [15/Aug/2015:10:14:03 +0200] "GET /w00tw00t.at.ISC.SANS.DFind:) HTTP/1.1" 400 482 "-" "-"
192.198.86.54 - - [16/Aug/2015:16:14:04 +0200] "GET /w00tw00t.at.ISC.SANS.DFind:) HTTP/1.1" 400 482 "-" "-"
37.187.77.142 - - [17/Aug/2015:07:05:05 +0200] "GET /w00tw00t.at.ISC.SANS.DFind:) HTTP/1.1" 400 482 "-" "-"

We can see here that those three bots sent a GET /w00tw00t. at. ISC. SANS. DFind:) request and that Apache politely told them to go away by returning an HTTP 400 (BAD_REQUEST) response code. It even wrote to its error log the reason why:

client sent HTTP/1.1 request without hostname (see RFC2616 section 14.23)

Apache rejected that request because it is non RFC2616-compliant. Any HTTP 1.1 request must contain at least 2 fields in its headers, one of them being the “Host:” field:

GET /requested_page.html HTTP/1.1
Host: domain.tld

In our case, the hostname was missing and thus it was rejected. It also means that you do not have to worry, you haven’t been hacked and the request was blocked by the HTTP server.
It is just a simple vulnerability scanner named DFind that loves to show off in your logs. There are several variants including, among them:

/w00tw00t.at.ISC.SANS.DFind:)
/w00tw00t.at.ISC.SANS.test0:)
/w00tw00t.at.ISC.SANS.MSlog:)
/w00tw00t.at.ISC.SANS.ntsvc:)

That particular string is only used as part of DFind banner scanner, i.e., it only wants to find your HTTP server name.
Let’s disassemble that 32-bit Windows executable and see exactly how it works (comments, in green color, are my own):

DFind.exe (v1.0.9, 73,728 bytes) - disassembly listing
:00406486 mov esi, 0040F1F8                   ; esi points to the
                                              ; "GET /w00tw00t.at.ISC.SANS.DFind:)
                                              ; HTTP/1.1" string.
:0040648B lea edi, dword ptr [ebp+FFFFFAD4]   ; edi = destination buffer.
:00406491 repz
:00406492 movsd                               ; Copy esi string into edi.
:00406493 movsw
:00406495 movsb
:00406496 push 00000000
:00406498 push 0000002E                       ; 46 (0x2e) bytes, the size of the
:0040649A lea eax, dword ptr [ebp+FFFFFAD4]   ; buffer containing the w00tw00t
:004064A0 push eax                            ; string + the CR/LF/CR/LF.
:004064A1 push [ebp-2C]                       ; Socket descriptor.
:004064A4 Call dword ptr [0040C0E4]           ; Call send() to
:004064AA cmp eax, FFFFFFFF                   ; post the request.
:004064AD jne 004064CB                        ; Give up if any error.
...
:00406537 push 00000000                       ; Call recv() and
:00406539 push 000001F4                       ; fetch max 500 (0x1f4)
:0040653E lea eax, dword ptr [ebp+FFFFF8DC]   ; characters returned
:00406544 push eax                            ; by the server.
:00406545 push [ebp-2C]                       ;
:00406548 Call dword ptr [0040C0E8]           ; Call recv().
:0040654E mov dword ptr [ebp+FFFFFAD0], eax   ; Save the number of bytes
:00406554 push 00000000                       ; returned.
:00406556 push [ebp-2C]
:00406559 Call dword ptr [0040C0B0]           ; Call shutdown().
:0040655F cmp dword ptr [ebp+FFFFFAD0], 5     ; Ensure we received at least
:00406566 jge 00406584                        ; 5 bytes otherwise give up.
...
:00406584 push 0040F1F0                       ; Offset holding the "Server:"
:00406589 lea eax, dword ptr [ebp+FFFFF8DC]   ; string.
:0040658F push eax
:00406590 Call dword ptr [0040C088]           ; Call strstr() to find
:00406596 pop ecx                             ; that sub string.
:00406597 pop ecx                             ;
:00406598 test eax, eax                       ; Check if we found it (eax must
:0040659A je 004068FA                         ; point to the first occurrence)
                                              ; otherwise we give up.
:004065A0 push 0040D184                       ; Offset pointing to the LF (\n).
:004065A5 lea eax, dword ptr [ebp+FFFFF8DC]
:004065AB push eax
:004065AC Call dword ptr [0040C084]           ; Call strtok() to find
:004065B2 pop ecx                             ; that line feed.
:004065B3 pop ecx
:004065B4 mov dword ptr [ebp+FFFFF8D8], eax
:004065BA cmp dword ptr [ebp+FFFFF8D8], 0     ; Give up if we cannot
:004065C1 je 004068FA                         ; find it.
...
:004065E8 Call 0040B952                       ; Call strlen() to fetch
:004065ED pop ecx                             ; the total number of bytes
                                              ; between "Server:" and LF.
:004065EE dec eax
:004065EF mov dword ptr [ebp+FFFFF4B8], eax
:004065F5 cmp dword ptr [00410EE4], 1
:004065FC jne 0040677F
:00406602 push dword ptr [00410DF8]
:00406608 push dword ptr [ebp+FFFFF8D8]
:0040660E Call dword ptr [0040C088]           ; Call strstr().
:00406614 pop ecx
:00406615 pop ecx
:00406616 test eax, eax                       ; Give up if error.
:00406618 je 00406764
:0040661E mov eax, dword ptr [00410EC4]       ; Internal flag which
:00406623 inc eax                             ; increments the number of
:00406624 mov dword ptr [00410EC4], eax       ; signatures found.
:00406629 push dword ptr [ebp+FFFFF4B8]       ; strlen() retcode.
:0040662F push dword ptr [ebp+FFFFF8D8]
:00406635 lea eax, dword ptr [ebp+FFFFF4BC]   ; Destination buffer.
:0040663B push eax                            ;
:0040663C Call dword ptr [0040C044]           ; Call strncpy().
:00406642 add esp, 0000000C                   ; Clean up the stack.
:00406645 lea eax, dword ptr [ebp+FFFFF4BC]   ; Fetch buffer address.
...

Self-explanatory : send a GET request and fetch the HTTP server name.

How to get rid of it?

As indicated above, you cannot use NinjaFirewall (or any other script/plugin) or even ModSecurity to block it because it is already blocked by Apache, which also closes immediately the connection and, obviously, will never forward it to your script. Hence, if you still want to get rid of it, you need to act before Apache. If you are using a load balancer/reverse proxy like HAProxy, it is easy to implement.
Otherwise, you still have another option: the Linux kernel firewall and its command line interface, iptables.
In this article, we’ll use iptables and three of its modules : String match, Recent and TCP.

Before going any further it is important to note that the following rules are perfect to get rid of “script-kiddies” randomly scanning IP addresses with widely available vulnerability scanners, but they may under no circumstances apply to an attack led by an experienced hacker specifically targeting your server that only some tougher server/firewall configuration rules could block.

The iptables String match module

String match is a string-matching filter that can reject any unwanted packet with the -m string option:

iptables -m string --help

string match options:
--from                       Offset to start searching from
--to                         Offset to stop searching
--algo                       Algorithm
[!] --string string          Match a string in a packet
[!] --hex-string string      Match a hex string in a packet
  • --from: packet offset to start searching from. By default, searching starts from offset 0.
  • --to: packet offset to stop searching. That option and the previous one are quite interesting and useful because we can limit our search inside a packet instead of filtering it all and thus save time and CPU cycles. By default, it will search through the whole packet, the maximum limit being set at 65,535 bytes, the maximum IP packet length.
  • --algo: the algorithm to use. There are two : Boyer-Moore (bm) and Knuth-Pratt-Morris (kmp). We’ll use the first one.
  • --string: text search pattern (e.g., ‘abcd’). It is CaSe sensitive.
  • --hex-string: search pattern in hexadecimal format. The pattern must be delimited by the ‘|’ sign. Hex characters can be separated by a space (e.g., ‘|61 62 63 64|’) or not (e.g., ‘|61626364|’).

As there are a few variants of “w00tw00t”, we will limit the search to a small part of the string:

GET /w00tw00t.at.ISC.SANS.

That’s 26 bytes, to which we will add 44 more bytes, including a dozen for the “Options” field of the TCP/IP packet, making a total of 70 bytes, our search length (the --to parameter):

iptables -I INPUT -p tcp --dport 80 -m string --to 70 --algo bm --string 'GET /w00tw00t.at.ISC.SANS.' -j DROP

You can display your rule with the following command:

iptables -L INPUT -nvx

pkts bytes target ...
0 0 DROP ... STRING match "GET /w00tw00t.at.ISC.SANS." ALGO name bm TO 70

Wait until DFind comes back and check your rule again:

iptables -L INPUT -nvx

 pkts  bytes target ...
 64    5504  DROP   ... STRING match "GET /w00tw00t.at.ISC.SANS." ALGO name bm TO 70

We can see in that example that 64 packets were dropped for a total of 5,504 bytes, or 86 bytes/packets, the typical DFind sequence.

Although it looks nice and easy, it is not really the best approach. For sure it blocks DFind, but there are two major problems:

  • It filters all incoming packets to port 80, regardless of what kind of packets it is. That’s a waste of time and resources.
  • It can cause errors (false positive). Chances are very low, but that must be taken into consideration.

We need to set up a rule that will only filter the packet we need to filter, that is, the very first one containing the HTTP request (GET, POST…) and thus overcome the disadvantages of the string module used alone. All other incoming packets will be ignored.
Let’s see how a TCP connection works:

  • The client connects to the server by sending a SYN (synchronization) packet.
  • The server responds by sending a SYN + ACK (Synchronize + Acknowledgment) packet.
  • The client responds with an ACK (Acknowledgment) packet.
  • At this time, the communication is established, the client can send its PSH + ACK (Push + Acknowledgment) packets, the server will respond and so on until the connection is closed.

The iptables TCP module

To find the right packet, we must be able to identify it. We will use the TCP module and its --tcp-flags parameter:

iptables -p tcp --help

  TCP v1.3.8 options:
 --tcp-flags [!] mask comp     match when TCP flags & mask == comp
                               (Flags: SYN ACK FIN RST URG PSH ALL NONE)

mask matches the flags that should not be set, comp the flags that need to be. There can be one or more flags in each field. Multiple flags should be comma-separated.

For instance, to find out if an incoming TCP packet to port 80 is a ACK sequence, we can use the following iptables command:

iptables -A INPUT -p tcp --tcp-flags PSH,SYN,ACK ACK --dport 80

Now we are able to determine the type of each incoming or outgoing TCP packet, simply by looking at its flags.

The iptables Recent module

We still have one problem to solve: throughout the connection, we will send and receive a lot of packets however the only one we want to filter is the first PSH+ACK packet that comes right after the SYN/SYN+ACK/ACK 3-way handshake sequence.
For that purpose, the Recent match module is perfect:

iptables -m recent --help

    --set          Add source address to list, always matches.
    --update       Match if source address in list, also update last-seen time.
    --remove       Match if source address in list, also removes that address from list.
    --name name    Name of the recent list to be used.  DEFAULT used if none given.
    ...
    ...

This is a very complex module. It has several parameters but we’ll only use the above four ones. It can create a list with the IP and time stamp used in a packet and can allow us to monitor/match any recent event regarding the current connection. The list can be customized (--name parameter) or be the default one (DEFAULT).

  • --set: we’ll use it to create the list at the beginning of any connection (SYN packet).
  • --update: we’ll use it to update our list when receiving the ACK, right after sending the SYN+ACK.
  • --remove: we’ll use it to delete our list as soon as we receive the very first PSH+ACK packet (which contains the HTTP request to filter). Hence, once the list is deleted we will ignore any further incoming packet from the current connection.

Let’s test it:

#!/bin/bash

# Flush all rules...
iptables -F
# and delete all existing chains:
iptables -X

# Create our w00t chain:
iptables -N w00t

# Redirect incoming TPC packets (port 80 only) to our w00t chain:
iptables -A INPUT -p tcp --dport 80 -j w00t

# We're looking for the first packet which should only have
# the SYN flag set; we create our list with '--set'.
# Note: we could also use '--syn' parameter instead of
# the '--tcp-flags ALL SYN' one:
iptables -A w00t -m recent -p tcp --tcp-flags ALL SYN --set

# Wait for the client ACK and update our list:
iptables -A w00t -m recent -p tcp --tcp-flags PSH,SYN,ACK ACK --update

# We received our 3-way handshake: the connection is established

# We're now waiting for the first incoming PSH,ACK. It will contain
# the HTTP (GET, POST, HEAD...) request we are looking for.
# As soon as we get it, we delete our list with '--remove' so that
# we will ignore any further packets :
iptables -A w00t -m recent -p tcp --tcp-flags ACK,PSH PSH,ACK --remove
# EOF

 

To ensure that everything is working perfectly, we will test it with a simple HTTP GET request calling this HTML page and we will capture the incoming traffic on port 80 with the tcpdump utility:

It shows there were a total of 32 packets:

  • 3 SYN packets (No 1, 8 and 16).
  • 11 ACK packets (No 2, 4, 9, 11, 15, 17, 21, 23, 27, 30 and 32).
  • The remaining 18 packets, marked in black, are those we want to filter.

Let’s compare it with our iptables rules:

iptables -L w00t -nvx

Chain w00t (1 references)
    pkts    bytes  target ...
       3      180  ...  recent: SET name:    tcp dpt:80 flags:0x3F/0x02
      11      572  ...  recent: UPDATE name: tcp dpt:80 flags:0x1A/0x10
      18    10683  ...  recent: REMOVE name: tcp dpt:80 flags:0x18/0x18

We see that there were a total of 32 captured packets and that our first rule hooked the 3 SYN (0x02), the second one hooked the 11 ACK (0x10) and that our third rule, as expected, hooked the first PSH+ACK (0x18) packet of each of the 18 requests we wanted to filter. That’s perfect and matches exactly the tcpdump capture.

Implementation

Example #1:

In the first example, we’ll build a simple filter rule to drop the packet containing the GET /w00tw00t.at.ISC.SANS. string:

#!/bin/bash

# Create our w00t chain:
iptables -N w00t

# Redirect incoming TPC packets (port 80 only) to our w00t chain:
iptables -A INPUT -p tcp --dport 80 -j w00t

# Look for the SYN packet and create the list:
iptables -A w00t -m recent -p tcp --syn --set

# Look for the ACK packet and update the list:
iptables -A w00t -m recent -p tcp --tcp-flags PSH,SYN,ACK ACK --update

# This is the right packet: look for our string pattern and DROP it
# if we find it.
# Then, delete our list, we don't want to filter any further packet:
iptables -A w00t -m recent -p tcp --tcp-flags PSH,ACK PSH,ACK --remove \
  -m string --to 70 --algo bm --string "GET /w00tw00t.at.ISC.SANS." -j DROP
# EOF

If you want to add more than one rule to the chain, you must not forget that only the last one should remove the list, all previous ones should keep updating it. For instance, we can create a filter that will block an HTTP request with a GET /somepath string and the 1.2.3.4 IP in the HTTP_HOST header:

...
# First rule + list update:
iptables -A w00t -m recent -p tcp --tcp-flags PSH,ACK PSH,ACK --update \
  -m string --to 70 --algo bm --string "GET /somepath" -j DROP

# Second - and last - rule + list removal:
iptables -A w00t -m recent -p tcp --tcp-flags PSH,ACK PSH,ACK --remove \
  -m string --to 700 --algo bm --string 'Host: 1.2.3.4' -j DROP

Note: we increased the searching length to 700 bytes because there could be other HTTP headers between the ‘GET’ and ‘Host:’ ones.

Example #2:

This set of rules is more sophisticated than the previous one:

  • Generic search: some script-kiddies modify the string GET /w00tw00t.at.ISC.SANS. using hex-editors (e.g., GET /test.w00t:)). To solve the problem, we can filter on the fact that the HTTP request is non RFC-compliant, i.e., it contains the string “HTTP/1.1” followed by 2 CR/LF. We can convert it to hex values (“HTTP/1.1” => “0x485454502f312e310” and “CR/LF/CR/LF” => “0x0d0a0d0a”) and use the --hex-string parameter.
  • IP blacklist: we can blacklist for 6 hours any IP with the Recent module. We’ll use the --update parameter so that if the scanner came back we’d keep it blacklisted for another 6 hours and so on.
  • Connection reset: instead of dropping the packet with -j DROP, we can reject it and immediately close the connection with -p tcp -j REJECT --reject-with tcp-reset. Using the DROP action isn’t really interesting here, because when we catch the packet we are looking for, the connection has already been established (that packet follows the 3-way handshake sequence) hence the scanner knows there is an HTTP server listening. No need to try to make it believe the contrary. Alternatively, if you have the Xtables-Addons package installed, you can use the -j TARPIT action instead (bots will hate it!).
#!/bin/bash

# Add the following lines at the beginning of your iptables rules:

# Allow loopback:
iptables -A INPUT -i lo -j ACCEPT

# Check if that IP is already blacklisted in the w00tlist.
# If it is, reject it right away and update the list for another 6 hours:
iptables -A INPUT -p tcp -m recent --name w00tlist --update --seconds 21600 -j DROP

# Create the w00tchain chain that will add the IP to the w00tlist
# and will reset the connection:
iptables -N w00tchain
iptables -A w00tchain -m recent --set --name w00tlist -p tcp -j REJECT \
  --reject-with tcp-reset

# Create our w00t chain:
iptables -N w00t

# Redirect incoming TPC packets (port 80 only) to our w00t chain:
iptables -A INPUT -p tcp --dport 80 -j w00t

############################################################
# Add here all your iptables rules (e.g., accept already established
# connections, etc):
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
...
...
############################################################

# w00t chain :
# Look for the SYN packet and update the list:
iptables -A w00t -m recent -p tcp --syn --set

# Look for the ACK packet and update the list:
iptables -A w00t -m recent -p tcp --tcp-flags PSH,SYN,ACK ACK --update

# Look for the hexadecimal string in the first PSH+ACK.
# If found, redirect it to w00tchain in order to blacklist the IP and
# to close the connection.
# Then, delete our list, we do not want to filter any further packet:
iptables -A w00t -m recent -p tcp --tcp-flags PSH,ACK PSH,ACK --remove \
  -m string --to 80 --algo bm --hex-string '|485454502f312e310d0a0d0a|' -j w00tchain
# EOF

 

You can view the list of all currently blocked IP addresses in the “w00tlist” by running the following command:

cat /proc/net/xt_recent/w00tlist

 

It is possible to filter other ports as well, however, do not try to systematically filter any kind of string pattern with iptables because it could be possible for an attacker to split those patterns into smaller packets and thus to avoid detection.