Blocking bad bots with PF

網路上各類針對WordPress的攻擊很多,比較討厭的是來自於各大主機商的訪問,比如DigitaloceanOVHLinodeAliyun,但是,運行在這些主機商伺服器上的業務,通常和我們的網站並沒有什麼必需的聯繫,所以我們完全可以把他們都封鎖掉。

封鎖他們的IP有很多種方式,最簡單的方式,就是通過CDN的訪問控制列表,在CDN上設置黑名單,然後你可以在Apache或者Nginx設置你的網站只允許CDN的IP訪問,這是你自己的白名單,這就可以確保你的網站不會被其他IP地址訪問,問題是,如果你有多個網站運行在同一個IP地址上,而你又不能為你所有的網站設置同樣的CDN或者同樣的訪問規則時,這個方法就變得不適用。

第二種方式,你可以在Apache或者Nginx設置你的網站禁止特定IP訪問,無論是通過CDN送過來的IP還是直接訪問,均可以達到封鎖效果,問題是,這種方法需要消耗webserver軟體的性能,而且通常都會消耗掉不少的性能,有沒有更好一點的辦法呢?

第三種方式,使用服務器上的防火牆,或者使用VPC防火牆,當你擁有一個數十台服務器的網絡時,你是必然會使用VPC的,否則安全管理會變得非常繁瑣,使用VPC的防火牆,在各大主機商是非常簡單的事情,點擊兩三下,輸入要封鎖的IP地址範圍,就可以了。

那麼,如果我們想使用操作系統的防火牆呢?比如FreeBSD下的PF

PF是一個久負盛名的軟體,來自於OpenBSD的packet filter,比較有名的應用大概就是pfsense,有商業化運營的硬體產品出售,但由開源軟體發展而來的商業軟體,在性能上和JuniperCisco比起來還是差了一大截,如果有硬體防火牆可用,當然優先的選擇是硬體防火牆。在FreeBSD上其實是有很多的防火牆軟體可以選擇,以前我常用ipfw,他的配置是一行一行按順序執行,很多次我都因為寫錯了某個地方然後就把自己鎖在了伺服器的外面,PF好像至今尚未出現過這樣的情形。

一個典型的PF配置如下:

if=”eth0″

icmp_types = “echoreq”
open_tcp = “{ 80,443}”
open_udp = “{ 53}”

set block-policy drop
set skip on lo0
set limit { states 10000, frags 5000 }
set loginterface eth0
set optimization normal
set require-order yes
set fingerprints “/etc/pf.os”
set ruleset-optimization basic

table persist file “/etc/pf.blocked.ip.conf”

scrub in all

block drop in log (all) quick on $if from to any
block log all
block in all
block return all

antispoof quick for $if

pass in on $if proto tcp from any to any port $open_tcp keep state
pass in on $if proto udp from any to any port $open_udp keep state

pass out quick all keep state

pass in on $if inet proto icmp all icmp-type $icmp_types keep state

其中,涉及到封鎖IP地址的有兩行配置:

table persist file “/etc/pf.blocked.ip.conf”

block drop in log (all) quick on $if from to any

第一行是定義一個object叫做blockedips,他的文檔類型是一個conf配置文檔。

第二行則是拒絕這個配置文檔中所有的IP地址訪問$if這張網路卡。

那麼這個conf文檔的配置格式為何呢?按照標準的IP地址格式就可以了,我把一些常見的惡意IP地址範圍都加了進去,主要是上面提到的主機商,很多黑客和惡意程式利用這些主機商來進行掃描和攻擊。

#Aliyun

8.208.0.0/16
39.105.0.0/16
47.91.0.0/16
47.111.0.0/16
47.112.0.0/16
123.56.0.0/16
123.57.0.0/16
161.117.0.0/16
182.92.0.0/16

#Digitalocean

45.55.0.0/16
68.183.0.0/16
104.248.0.0/16
128.199.0.0/16
138.68.0.0/16
138.197.0.0/16
139.59.0.0/16
142.93.0.0/16
146.185.0.0/16
142.93.0.0/16
146.185.0.0/16
157.230.0.0/16
157.245.0.0/16
159.65.0.0/16
159.89.0.0/16
165.22.0.0/16
165.227.0.0/16
167.172.0.0/16
178.62.0.0/16
178.128.0.0/16
178.186.0.0/16
188.166.0.0/16
185.85.0.0/16
185.86.0.0/16
190.202.0.0/16
206.189.0.0/16
217.182.0.0/16

OVH

5.135.0.0/16
51.38.0.0/16
51.68.0.0/16
51.83.0.0/16
51.254.0.0/16
54.36.0.0/16
54.37.0.0/16
91.121.0.0/16
137.74.0.0/16
144.217.0.0/16
147.135.0.0/16
158.69.0.0/16
192.99.0.0/16
213.32.0.0/16

Linode

172.104.0.0/16

Huawei 華為雲

114.119.0.0/16

Taobao 淘寶一搜

42.120.0.0/16

我一般都是直接封鎖B類地址範圍,Digitalocean的IP地址實在是太多了。

其實原本還有很多中國的二三流搜索引擎,每天不停的惡意抓取,比如今日頭條,但是,由於中國從來都不按照國際規範分配IP地址,很多時候使用whois檢出的IP地址都只能劃歸到某一個省份,而不是某一個城市,甚至公司,這樣你就很難去把他封鎖掉,只能一個C類地址範圍加進去試試看。

當然,在rc.conf中,我們應該預先定義好log的路徑和名稱,如下,我定義了一個pflog,放在/data/log目錄中:

pf_enable=”YES”
pf_rules=”/etc/pf.conf”
pflog_enable=”YES”
pflog_logfile=”/data/log/pflog”

那麼,如何讀取這個日誌文檔呢?因為他並不是一個純文本文檔,我們不能用vi或者cat或者tail來閱讀他,我們必須使用tcpdump來查看這個日誌文件,查看的命令行格式如下:

tcpdump -n -e -ttt -r /data/log/pflog

執行之後,可以得到如下類似的輸出結果,這個輸出中,我的服務器IP地址是116.128.134.139:

tcpdump -n -e -ttt -r /data/log/pflog
reading from file /data/log/pflog, link-type PFLOG (OpenBSD pflog file)
00:00:00.000000 rule 2/0(match): block in on eth0: 178.62.41.44 > 116.128.134.139: ICMP echo request, id 4282, seq 980, length 16
01:38:36.513282 rule 2/0(match): block in on eth0: 217.182.193.13.56093 > 116.128.134.139.1500: Flags [S], seq 3677707474, win 1024, options [mss 536], length 0
00:38:42.059567 rule 2/0(match): block in on eth0: 178.62.41.44 > 116.128.134.139: ICMP echo request, id 5933, seq 3298, length 16
00:00:04.451304 rule 2/0(match): block in on eth0: 178.62.80.93 > 116.128.134.139: ICMP echo request, id 9026, seq 345, length 16
01:16:14.250758 rule 2/0(match): block in on eth0: 178.62.106.84 > 116.128.134.139: ICMP echo request, id 5864, seq 1941, length 16
00:15:17.692806 rule 2/0(match): block in on eth0: 178.62.78.199 > 116.128.134.139: ICMP echo request, id 30618, seq 1686, length 16
05:04:34.023956 rule 2/0(match): block in on eth0: 178.62.78.199 > 116.128.134.139: ICMP echo request, id 3055, seq 465, length 16
00:20:23.179962 rule 2/0(match): block in on eth0: 178.62.106.84 > 116.128.134.139: ICMP echo request, id 11035, seq 2333, length 16
00:15:19.427828 rule 2/0(match): block in on eth0: 178.62.80.93 > 116.128.134.139: ICMP echo request, id 17857, seq 577, length 16
01:06:00.483685 rule 2/0(match): block in on eth0: 178.62.109.7 > 116.128.134.139: ICMP echo request, id 10094, seq 4315, length 16
00:10:12.073875 rule 2/0(match): block in on eth0: 178.62.41.44 > 116.128.134.139: ICMP echo request, id 14083, seq 420, length 16
00:55:54.286830 rule 2/0(match): block in on eth0: 178.62.78.199 > 116.128.134.139: ICMP echo request, id 6287, seq 2726, length 16
00:10:15.098226 rule 2/0(match): block in on eth0: 178.62.41.44 > 116.128.134.139: ICMP echo request, id 15544, seq 4722, length 16
00:15:24.539292 rule 2/0(match): block in on eth0: 178.62.109.7 > 116.128.134.139: ICMP echo request, id 13960, seq 3050, length 16

可以觀察到,這些惡意bots在發送spam之前,會使用多個不同的主機先ping一下看是否通暢,然後再發起惡意訪問,比如來自Digitalocean的178.62.0.0/16和138.197.0.0/16,就有很多的記錄,這些請求都被PF block掉了,不會到達webserver或是其他地方,甚至還有針對445端口進行掃描的Windows病毒蠕蟲。

如果我們想要查看實時的日誌顯示,那麼我們可以用pflog0這個虛擬網卡,執行如下的命令行可以得到輸出,你看很快的,就進來了一條記錄:

tcpdump -n -e -ttt -i pflog0

tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on pflog0, link-type PFLOG (OpenBSD pflog file), capture size 262144 bytes
00:00:00.000000 rule 2/0(match): block in on eth0: 138.197.130.235 > 116.128.134.139: ICMP echo request, id 19068, seq 6044, length 16

如果我們只希望查看某些IP地址或端口的記錄,應當如何使用呢?由於pflog使用的是tcpdump的二進制文檔格式,那麼tcpdump的任何命令都是適用的,幾個樣例如下:

查看80端口的記錄:

tcpdump -n -e -ttt -r /data/log/pflog port 80

查看特定IP地址和端口的記錄:

tcpdump -n -e -ttt -r /data/log/pflog port 80 and host 192.168.1.3

你也可以把它套用到pflog0上:

tcpdump -n -e -ttt -i pflog0 host 192.168.4.2

甚至可以使用更複雜的命令來過濾,和硬體防火牆比起來, 操作略為複雜一些。

tcpdump -n -e -ttt -i pflog0 inbound and action block and on eth0

一些提示:

ip – address family is IPv4.
ip6 – address family is IPv6.
on int – packet passed through the interface int.
ifname int – same as on int.
ruleset name – the ruleset/anchor that the packet was matched in.
rulenum num – the filter rule that the packet matched was rule number num.
action act – the action taken on the packet. Possible actions are pass and block.
reason res – the reason that action was taken. Possible reasons are match, bad-offset, fragment, short, normalize, memory, bad-timestamp, congestion, ip-option, proto-cksum, state-mismatch, state-insert, state-limit, src-limit and synproxy.
inbound – packet was inbound.
outbound – packet was outbound.

顯示防火牆當前的連結,也是一個常用的命令,如何使用呢?我們需要額外安裝一個pftop軟體,然後執行它。

pkg install pftop

pftop

pfTop: Up State 1-26/26, View: default, Order: none, Cache: 10000 6:33:52

PR DIR SRC DEST STATE AGE EXP PKTS BYTES
tcp In 108.162.215.39:38588 116.128.134.139:80 FIN_WAIT_2:FIN_WAIT_2 00:03:23 00:00:07 15 1576
tcp In 108.162.215.127:20290 116.128.134.139:80 FIN_WAIT_2:FIN_WAIT_2 00:02:29 00:01:01 15 1447
tcp In 108.162.215.39:56092 116.128.134.139:80 TIME_WAIT:TIME_WAIT 00:02:23 00:01:07 17 2429
tcp In 172.69.33.38:28072 116.128.134.139:80 ESTABLISHED:ESTABLISHED 00:01:30 23:59:46 10 1584
tcp In 108.162.215.39:39160 116.128.134.139:80 ESTABLISHED:ESTABLISHED 00:01:23 23:59:37 10 1376
tcp In 108.162.215.203:26782 116.128.134.139:80 ESTABLISHED:ESTABLISHED 00:01:21 23:59:40 10 1234

pftop的這個輸出信息量其實並不大,甚至還不如iftop顯示的數據量多,但區別在於,pftop可以顯示出連結的狀態,連結處於CLOSED,ESTABLISHED,還是WAIT的狀態,當遭遇到攻擊的時候,半開連結作為判斷因素有那麼一點點的作用。

對比一下Juniper SSG的輸出:

ssg5-> get session | inc /80
id 10597/s,vsys 0,flag 00000000/8000/1001/0000,policy 1,time 30, dip 2 module 0, di on if 11(nspflag 801a01):10.1.1.5/59689->54.39.179.91/8080,6,000c2997db20,sess token 3,vlan 0,tun 0,vsd 0,route 3,wsf 7 if 0(nspflag 10003a00):10.70.5.12/1427<-54.39.179.91/8080,6,000000000000,sess token 4,vlan 0,tun 0,vsd 0,route 11,wsf 6 id 10627/s,vsys 0,flag 00000000/8000/1001/0000,policy 1,time 30, dip 2 module 0, di on
if 11(nspflag 801a01):10.1.1.105/51382->203.208.39.215/80,6,546009783576,sess token 3,vlan 0,tun 0,vsd 0,route 3,wsf 8
if 0(nspflag 10003a00):10.70.5.12/3892<-203.208.39.215/80,6,000000000000,sess token 4,vlan 0,tun 0,vsd 0,route 11,wsf 6 id 10972/s,vsys 0,flag 00000000/8000/1001/0000,policy 1,time 26, dip 2 module 0, di on if 11(nspflag 801a01):10.1.1.138/49773->223.166.152.106/80,6,a860b62b6556,sess token 3,vlan 0,tun 0,vsd 0,route 3,wsf 7 if 0(nspflag 10003a00):10.70.5.12/3626<-223.166.152.106/80,6,000000000000,sess token 4,vlan 0,tun 0,vsd 0,route 11,wsf 6 id 11108/s,vsys 0,flag 00000000/8000/1001/0000,policy 1,time 23, dip 2 module 0, di on if 11(nspflag 801a01):10.1.1.138/53973->116.128.160.96/8080,6,a860b62b6556,sess token 3,vlan 0,tun 0,vsd 0,route 3,wsf 8

對比一下Cisco ASA的輸出:

F5ASA# sh conn | inc :80
TCP outside 114.247.184.134:53018 inside 172.30.1.121:80, idle 0:09:20, bytes 0, flags UfB
TCP outside 123.54.116.24:59970 inside 172.30.1.121:80, idle 0:16:06, bytes 0, flags UfB
TCP outside 112.44.42.1:1193 inside 172.30.1.121:80, idle 0:16:10, bytes 0, flags UfB
TCP outside 111.44.151.170:11348 inside 172.30.1.121:80, idle 0:36:34, bytes 0, flags UfB
TCP outside 123.54.116.24:59480 inside 172.30.1.121:80, idle 0:42:03, bytes 0, flags UfB
TCP outside 112.32.196.89:8609 inside 172.30.1.121:80, idle 0:56:14, bytes 0, flags UfB
TCP outside 117.157.99.170:60380 inside 172.30.1.165:80, idle 0:00:02, bytes 7330, flags UIOB
TCP outside 117.157.99.170:60379 inside 172.30.1.165:80, idle 0:00:02, bytes 7330, flags UIOB
TCP outside 117.147.16.83:12041 inside 172.30.1.165:80, idle 0:00:00, bytes 30381, flags UIOB
TCP outside 112.39.103.179:26126 inside 172.30.1.165:80, idle 0:00:03, bytes 6025, flags UIOB
TCP outside 113.233.207.78:62067 inside 172.30.1.165:80, idle 0:00:03, bytes 81796, flags UIOB