Linux : using iptables string-matching filter to block vulnerability scanners
Does "w00tw00t.at.ISC.SANS.DFind:)" sound familiar to you ? If you own one ore more servers, there are a lot of chances you found it in your logs and that it gave you headaches or even nightmares trying unsuccessfully to get rid of it.
It always looks like this in your apache logs :
213.251.134.23 [16/Nov/2008:07:43:58] "GET /w00tw00t.at.ISC.SANS.DFind:) HTTP/1.1" 400
213.251.134.23 [16/Nov/2008:07:43:58] "GET /w00tw00t.at.ISC.SANS.DFind:) HTTP/1.1" 400
213.251.134.23 [16/Nov/2008:07:43:58] "GET /w00tw00t.at.ISC.SANS.DFind:) HTTP/1.1" 400
213.251.134.23 [17/Nov/2008:05:16:31] "GET /w00tw00t.at.ISC.SANS.DFind:) HTTP/1.1" 400
We can see here that 213.251.134.23, a small (compromised) server hosted by OVH, asked for the "/w00tw00t.at.ISC.SANS.DFind:)" web page and that apache politely told it to bugger off by sending a HTTP 400 code (BAD_REQUEST) and wrote to its error log file 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 should contain at least 2 fields in its headers, one of them being the "Host:" field as per follows :
GET /requested_page.html HTTP/1.1
Host: website.com
In our case, the hostname is missing and thus it is rejected. This also means that you do not have to worry, you haven't been hacked !
That's just a simple vulnerability scanner named DFind that loves to show off in your logs.
There are several variants including, amongst 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 webserver banner scanner, that is, it only wants to fetch your HTTP server name, nothing else.
Let's see exactly how it works :
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 => edi
:00406493 movsw
:00406495 movsb
:00406496 push 00000000
:00406498 push 0000002E ; 46 bytes == size of
:0040649A lea eax, dword ptr [ebp+FFFFFAD4] ; buffer (w00tw00t string
:004064A0 push eax ; + CR/LF/CR/LF).
:004064A1 push [ebp-2C] ; Socket desciptor.
:004064A4 Call dword ptr [0040C0E4] ; Call send() to
:004064AA cmp eax, FFFFFFFF ; post the request.
:004064AD jne 004064CB ; Give up if any error.
:004064AF mov eax, dword ptr [00410EE0] ; Internal flag use to
:004064B4 dec eax ; decrement number of scan
:004064B5 mov dword ptr [00410EE0], eax ; attempts left.
:004064BA push [ebp-2C] ; Close the sockect
:004064BD Call dword ptr [0040C0C4] ; closesocket()
:004064C3 or eax, FFFFFFFF ; error ?
:004064C6 jmp 00409E01
...
:00406537 push 00000000 ; Call recv() and
:00406539 push 000001F4 ; Fetch maxi 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 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 string
:00406589 lea eax, dword ptr [ebp+FFFFF8DC] ; "Server:"
:0040658F push eax
:00406590 Call dword ptr [0040C088] ; Call strstr()
:00406596 pop ecx ; to find that sub string
:00406597 pop ecx ;
:00406598 test eax, eax ; Check if we found it (eax must
:0040659A je 004068FA ; point to the first occurence)
; otherwise give up.
:004065A0 push 0040D184 ; Offset pointing to LF (/n).
:004065A5 lea eax, dword ptr [ebp+FFFFF8DC]
:004065AB push eax
:004065AC Call dword ptr [0040C084] ; Call strtok() to
:004065B2 pop ecx ; find that LF.
: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 ; total number of bytes
; from "Server:"
:004065EE dec eax ; to the LF.
:004065EF mov dword ptr [ebp+FFFFF4B8], eax ; <= hmmm, does not check
:004065F5 cmp dword ptr [00410EE4], 1 ; retcode after the call
:004065FC jne 0040677F ; to strlen.
: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
:00406618 je 00406764
:0040661E mov eax, dword ptr [00410EC4] ; Internal flag which
:00406623 inc eax ; increment the number of
:00406624 mov dword ptr [00410EC4], eax ; signatures found.
:00406629 push dword ptr [ebp+FFFFF4B8] ; Our pointer (strlen retcode),
; is reused but still hasn't
:0040662F push dword ptr [ebp+FFFFF8D8] ; been checked (could be dangerous...).
:00406635 lea eax, dword ptr [ebp+FFFFF4BC] ; Destination buffer
:0040663B push eax ;
:0040663C Call dword ptr [0040C044] ; Call strncpy().
:00406642 add esp, 0000000C ; Adjust 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 ?
# Fail2ban ? Using such an application or any similar tool, would be useless : by default, DFind will hit your server with 5 requests (on ports 80, 81, 443, 8000 and 8080 respectively), then disappear and will come back later again with a different IP address and so on. Blocking its IP afterwards will have no effect at all.
# installing a NIDS (Network Intrusion Detection System) ? Installing such a software in order to block a simple 28-byte string may be too much of a hassle...
# using mod_rewrite or mod_security ? Even if, strangely, we can find on some websites few mod_security rules to block DFind, they are completely useless. mod_security, contrary to common belief, is not a firewall but only a simple module/plugin. All incoming packets are fetched by apache which listens to the HTTP port 80 and it will forward them to mod_security, and not vice versa (hence it shows how dangerous it is to think your apache server is fully protected by mod_security). In our case, apache reject the request and will never forward it to mod_security.
We will simply use the excellent iptables and 3 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 IPs 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 String match module :
String match is string-matching filter that can reject any unwanted packet with the -m string opion. To understand how it works, all you have to do is to ask it :
# iptables -m string --help
STRING match v1.3.8 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 stat 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 usefull 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.
# --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 (ie : '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 (ie : '|61 62 63 64|') or not (ie : '|61626364|').
As there are few variants of our "w00tw00t", we are going to filter on their common part :
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 (--to parameter) :
# iptables -I INPUT -d xxx.xxx.xxx.xxx -p tcp --dport 80 -m string --to 70 \
--algo bm --string 'GET /w00tw00t.at.ISC.SANS.' -j DROP
Replace the 'xxx.xxx.xxx.xxx' string by your server IP.
If your server has multiple and consecutive IP's, you can use only one rule to include them all with the -m iprange parameter.
Example with the 5 consecutive IP's 1.0.0.1, 1.0.0.2, 1.0.0.3, 1.0.0.4 and 1.0.0.5 :
# iptables -I INPUT -p tcp --dport 80 -m iprange --dst-range 1.0.0.1-1.0.0.5 \
-m string --to 70 --algo bm --string 'GET /w00tw00t.at.ISC.SANS.' -j DROP
Have a look at your rule by typing :
# iptables -L INPUT -nvx
pkts bytes target ...
0 0 DROP ... STRING match "GET /w00tw00t.at.ISC.SANS." ALGO name bm TO 70
Wait few hours until your sticky DFind friend come back and, although your apache error log will nicely remain empty, you could still find out how many packets were blocked by iptables :
# 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.
That's cool but also quite a brutal (and not really recommended) approach.
For sure it worked in our example but there are two major problems :
* it filters all incoming packets to port 80, regardless of what kind of packets it is.
* it can cause errors ("false positive"). Although the chances are very small, they must be taken into consideration.
We need therefore to setup 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 works a TCP connection :
* 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 TCP module :
To find the right packet, we must be able to identify it. We will use the TCP module and its '--tcp-flags' parameters :
# 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.
Example : to find out if an incoming packet to port 80 is a ACK sequence, it must have an activated ACK flag but no PSH,SYN,ACK flags :
# iptables -A INPUT -p tcp --tcp-flags PSH,SYN,ACK ACK --dport 80 -j ACCEPT
That's it : now we are able to determine the type of each incoming or outgoing packet, simply by looking at its flag(s).
The 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 4 ones displayed above. It can create a list with the IP and timestamp used in a packet and allows us to monitor/match any recent event regarding the current connection. The list can be customized (parameter '--name') 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 during the SYN+ACK and 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 try with the following example :
#!/bin/bash
# just for our test ! flush all rules...
iptables -F
# and delete all chains :
iptables -X
# create our w00t chain :
iptables -N w00t
# redirect packets to our chain
# (we use localhost just for the test) :
iptables -A INPUT -d 127.0.0.1 -j w00t
# we're looking for the first packet which should
# only have the SYN flag set (we could also use '--syn' instead
# of '--tcp-flags ALL SYN') and we create our list with '--set' :
iptables -A w00t -m recent -p tcp --tcp-flags ALL SYN --dport 80 --set
# wait for the SYN,ACK packet and update our list with '--update'
# (it's an outgoing packet so we use '--sport 80') :
iptables -A w00t -m recent -p tcp --tcp-flags PSH,SYN,ACK SYN,ACK --sport 80 --update
# wait for the client ACK and update our list
# upon receipt :
iptables -A w00t -m recent -p tcp --tcp-flags PSH,SYN,ACK ACK --dport 80 --update
# we received our 3-way handshake, so from now and then
# the connection is established
# we're now waiting for the first incoming PSH,ACK.
# It will contain the HTTP (GET, POST, HEAD...) 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 --dport 80 --remove
To ensure that everything is working perfectly, we will test locally with a simple HTTP GET request by calling the HTML page you are currently reading. There should be 7 requests (1 HTML page, 1 CSS + 1 JS file and 4 pictures). We will verifiy if all packets were properly intercepted / ignored by our rules.
Let's see the whole HTTP request with a packet sniffer (Wireshark) :
Total : 27 packets
SYN : 2 packets (No 1 and 13)
SYN, ACK : 2 packets (No 2 and 14)
ACK (incoming) : 6 packets (No 3, 7, 15, 19, 26 and 27)
The HTTP requests we are looking for and wish to filter : 7 packets (No 4, 8, 11, 16, 20, 22 and 24) which are the GET used to request the HTML page , the CSS and the JS files as well as the 4 pictures.
The last 10 packets do not interest us.
Let's check with iptables now :
# iptables -L -nvx
Chain INPUT
pkts bytes target ...
27 18596 w00t ...
Chain w00t (1 references)
pkts bytes target ...
2 120 ...recent:SET name:DEFAULT side:source tcp dpt:80 flags:0x3F/0x02
2 120 ...recent:UPDATE name:DEFAULT side:source tcp spt:80 flags:0x1A/0x12
6 312 ...recent:UPDATE name:DEFAULT side:source tcp dpt:80 flags:0x1A/0x10
7 5412 ...recent:REMOVE name:DEFAULT side:source tcp dpt:80 flags:0x18/0x18
We see that there were a total of 27 packets and that our first rule hooked the 2 SYN (0x02), the second one the 2 SYN+ACK (0x12), the third one the 6 ACK (0x10) and that our fourth rule only hooked the 1st PSH+ACK (0x18) packet of each request.
The other 10 packets have indeed been ignored.
Setting up the rules :
Now that we can isolate the right packet from the HTTP request, we can use the String module to filter it.
# Example #1 :
For the first example, we build a simple filter rule to drop the packet containing the string "GET /w00tw00t.at.ISC.SANS." :
#!/bin/bash
# create our w00t chain :
iptables -N w00t
# redirect TPC traffic to our chain :
iptables -A INPUT -p tcp -j w00t
# look for the SYN packet and create the list :
iptables -A w00t -m recent -p tcp --syn --dport 80 --set
# look for the SYN,ACK packet and update the list :
iptables -A w00t -m recent -p tcp --tcp-flags PSH,SYN,ACK SYN,ACK --sport 80 --update
# look for the ACK packet and update the list :
iptables -A w00t -m recent -p tcp --tcp-flags PSH,SYN,ACK ACK --dport 80 --update
# this is the right packet : look for our string pattern and DROP it if we found it.
# Delete our list, we do not want to filter any further packet from that connection
iptables -A w00t -m recent -p tcp --tcp-flags PSH,ACK PSH,ACK --dport 80 --remove \
-m string --to 70 --algo bm --string "GET /w00tw00t.at.ISC.SANS." -j DROP
Once the rules are setup, iptables can display the total of packets and bytes dropped with the command :
# iptables -L w00t -nvx
You can also block some other well known vulnerability scanners used by script-kiddies when then will try to connect to your server by using its IP address instead of it domain (or sub-domain) name in the 'Host:' field of the HTTP request. That is :
GET / HTTP/1.1
Host: xxx.xxx.xxx.xxx
where xxx.xxx.xxx.xxx is your server IP instead of :
GET / HTTP/1.1
Host: yourdomain.tld
If you add one or more rules to our anti-w00t00t chain, you must not forget that only the last one should remove the list, all previous ones should keep updating it. For instance, adding a filter based on the 'Host:' variable. We increase the searching lenght to 700 bytes because there could be other variables in the HTTP request between the 'GET' and 'Host:' lines :
...
...
# 1st filtering rule on our paquet + list update :
iptables -A w00t -m recent -p tcp --tcp-flags PSH,ACK PSH,ACK --dport 80 --update \
-m string --to 70 --algo bm --string "GET /w00tw00t.at.ISC.SANS." -j DROP
# 2nd filtering rule on the same packet + list deletion :
iptables -A w00t -m recent -p tcp --tcp-flags PSH,ACK PSH,ACK --dport 80 --remove \
-m string --to 700 --algo bm --string 'Host: xxx.xxx.xxx.xxx' -j DROP
Replace 'xxx.xxx.xxx.xxx' with your server IP address.
Note : Host filtering with the string module will block a lot of scanners, howere do not rely only on that. A lot of scanners could get through so you should filter the playload with, for instance, mod_security. Using iptables to search for a specific string can be efficient when you want to block a scanner like DFind because you know in advance how it works and because you can't block it afterwards (unless you modify Apache source code), but don't rely on this method to block unknown scanners.
# Example #2 :
This set of rules is more sophisticated and also does much more than the previous one :
- generic search : some script-kiddies modify the string "GET /w00tw00t.at.ISC.SANS." using hexeditors (eg : "GET /test.w00t:)"). To solve the problem, we can filter on the fact that the HTTP request is non RFC-compliant, that is, is 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, say, 6 hours any IP with the ipt_recent module. We'll use the --update parameter so that if the scanner come back we'll keep it blacklisted for another 6 hours.
- connection reset : instead of dropping the packet (-j DROP), we can reject it and immediately close the connection (-p tcp -j REJECT --reject-with tcp-reset). Using DROP isn't really interesting here, because when we catch the packet we are looking for, the connection has already been established (the packet follows the 3-way handshake sequence) hence the scanner knows there is a HTTP server listening. No need to try to make it believe the contrary, it would be a waste of time.
#!/bin/bash
# add the following lines at the beginning of your iptables rules :
# allow loopback
iptables -A INPUT -i lo -j ACCEPT
# check if IP is blacklisted (present in the w00tlist).
# If so, 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 w00tchain chain which will add the IP to the w00tlist
# blacklist and will reset the connection (don't forget the
# '-p tcp' parameter needed for '--reject-with tcp-reset') :
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 TCP packets to our chain :
iptables -A INPUT -p tcp -j w00t
#####################################################
# put here all your iptables rules (ie : accept already established
# connections, etc) :
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
...
...
...
#####################################################
# w00t chain :
# look for the SYN packet and create the list :
iptables -A w00t -m recent -p tcp --syn --dport 80 --set
# look for the SYN,ACK packet and update the list :
iptables -A w00t -m recent -p tcp --tcp-flags PSH,SYN,ACK SYN,ACK --sport 80 --update
# look for the ACK packet and update the list :
iptables -A w00t -m recent -p tcp --tcp-flags PSH,SYN,ACK ACK --dport 80 --update
# look for the hexadecimal string in the first PSH+ACK.
# If found, redirect to w00tchain in order to blacklist the IP and
# to close the connection.
# Delete our list, we do not want to filter any further packet from that connection :
iptables -A w00t -m recent -p tcp --tcp-flags PSH,ACK PSH,ACK --dport 80 --remove \
-m string --to 80 --algo bm --hex-string '|485454502f312e310d0a0d0a|' -j w00tchain
Yet again, you can see how many packets were rejected using the command :
# iptables -L w00t -nvx
And you can also see all blacklisted IP's with :
# cat /proc/net/ipt_recent/w00tlist
Conclusion
It is possible to filter other ports (SSH etc) for instance to block buffer overflow attempt (--hex-string '|90 90 90 90 90 90|'). However, do not try to systematically filter any kind of string pattern ( eg : antispam filtering on your SMTP port !) because not only it is not the main purpose of iptables, but it is possible and easy for an attacker to split those patterns into smaller packets and thus avoid detection.
Do not forget the most important : RTFM
# man iptables
Or, for a specific parameter :
# iptables [PARAMETER] --help
Reference: http://spamcleaner.org/en/misc/w00tw00t.html