網(wǎng)站剛上線就被攻擊了!怎么辦?
自己搭建的網(wǎng)站剛上線,短信接口就被一直攻擊,并且攻擊者不停變換IP,導致阿里云短信平臺上的短信被惡意刷取了幾千條,加上最近工作比較忙,就直接在OpenResty上對短信接口做了一些限制,采用OpenResty+Lua的方案成功動態(tài)封禁了頻繁刷短信接口的IP。
一、臨時解決方案
由于事情比較緊急,所以,當發(fā)現(xiàn)這個問題時,就先采用快速的臨時方案解決。
(1)查看Nginx日志發(fā)現(xiàn)被攻擊的IP 和接口
[root@binghe ~]# tail -f /var/log/nginx/access.log
發(fā)現(xiàn)攻擊者一直在用POST請求 /fhtowers/user/getVerificationCode這個接口
圖片
(2)用awk和grep腳本過濾nginx日志,提取攻擊短信接口的ip(一般這個接口是用來發(fā)注冊驗證碼的,一分鐘如果大于10次請求的話就不是正常的訪問請求了,大家根據(jù)自己的實際情況更改腳本)并放到一個txt文件中去,然后重啟nginx
[root@binghe ~]# cat denyip.sh
#!/bin/bash
nginx_home=/usr/local/openresty/nginx
log_path=/var/log/nginx/access.log
tail -n5000 $log_path | grep getVerification | awk '{print $1}' |sort | uniq -c | sort -nr -k1 | head -n 100 |awk '{if($1>10)print ""$2""}' >$nginx_home/denyip/blocksip.txt
/usr/bin/nginx -s reload
(3)設置Nginx去讀取用腳本過濾出來的blocksip.txt(注意一下,我這里的Nginx是用的openresty,自帶識別lua語法的,下面會有講openresty的用法)
location = /fhtowers/user/getVerificationCode { #短信接口
access_by_lua '
local f = io.open("/usr/local/openresty/nginx/denyip/blocksip.txt") #黑名單列表
for line in f:lines() do
if ngx.var.http_x_forwarded_for == line then #如果ip在黑名單列表里直接返回403
ngx.exit(ngx.HTTP_FORBIDDEN)
end
end
';
proxy_pass http://appservers; #不在名單里就轉(zhuǎn)發(fā)給后臺的tomcat服務器
}
(4)把過濾腳本放進crontab任務里,一分鐘執(zhí)行一次
[root@binghe ~]# crontab -e
*/1 * * * * sh /root/denyip.sh
(5)查看一下效果,發(fā)現(xiàn)攻擊者的請求都被返回403并拒絕了
圖片
二、OpenResty+Lua方案
臨時方案有效果后,再將其調(diào)整成使用OpenResty+Lua腳本的方案,來一張草圖。
圖片
接下來,就是基于OpenResty和Redis實現(xiàn)自動封禁訪問頻率過高的IP。
2.1 安裝OpenResty
安裝使用 OpenResty,這是一個集成了各種 Lua 模塊的 Nginx 服務器,是一個以Nginx為核心同時包含很多第三方模塊的Web應用服務器,使用Nginx的同時又能使用lua等模塊實現(xiàn)復雜的控制。
(1)安裝編譯工具、依賴庫
[root@test1 ~]# yum -y install readline-devel pcre-devel openssl-devel gcc
(2)下載openresty-1.13.6.1.tar.gz 源碼包,并解壓;下載ngx_cache_purge模塊,該模塊用于清理nginx緩存;下載nginx_upstream_check_module模塊,該模塊用于ustream健康檢查。
[root@test1 ~]# cd /usr/local/
[root@test1 local]# wget https://openresty.org/download/openresty-1.13.6.1.tar.gz
[root@test1 local]# tar -zxvf openresty-1.13.6.1.tar.gz
[root@test1 local]# cd openresty-1.13.6.1/bundle
[root@test1 local]# wget http://labs.frickle.com/files/ngx_cache_purge-2.3.tar.gz
[root@test1 local]# tar -zxvf ngx_cache_purge-2.3.tar.gz
[root@test1 local]# wget https://github.com/yaoweibin/nginx_upstream_check_module/archive/v0.3.0.tar.gz
[root@test1 local]# tar -zxvf v0.3.0.tar.gz
(3)配置需安裝的模塊
# ./configure --help可查詢需要安裝的模塊并編譯安裝
[root@test1 openresty-1.13.6.1]# ./configure --prefix=/usr/local/openresty --with-luajit --with-http_ssl_module --user=root --group=root --with-http_realip_module --add-module=./bundle/ngx_cache_purge-2.3/ --add-module=./bundle/nginx_upstream_check_module-0.3.0/ --with-http_stub_status_module
[root@test1 openresty-1.13.6.1]# make && make install
(4)創(chuàng)建一個軟鏈接方便啟動停止
[root@test1 ~]# ln -s /usr/local/openresty/nginx/sbin/nginx /bin/nginx
(5)啟動nginx
[root@test1 ~]# nginx #啟動
[root@test1 ~]# nginx -s reload #reload配置
如果啟動時候報錯找不到PID的話就用以下命令解決(如果沒有更改過目錄的話,讓它去讀nginx的配置文件就好了)
[root@test1 ~]# /usr/local/openresty/nginx/sbin/nginx -c /usr/local/openresty/nginx/conf/nginx.conf
圖片
隨后,打開瀏覽器訪問頁面。
圖片
(6)在Nginx上測試一下能否使用Lua腳本
[root@test1 ~]# vim /usr/local/openresty/nginx/conf/nginx.conf
在server里面加一個
location /lua {
default_type text/plain;
content_by_lua ‘ngx.say(“hello,lua!”)’;
}
圖片
加完后重新reload配置。
[root@test1 ~]# nginx -s reload
在瀏覽器里輸入 ip地址/lua,出現(xiàn)下面的字就表示Nginx能夠成功使用lua了
圖片
2.2 安裝Redis
(1)下載、解壓、編譯安裝
[root@test1 ~]# cd /usr/local/
[root@test1 local]# wget http://download.redis.io/releases/redis-6.0.1.tar.gz
[root@test1 local]# tar -zxvf redis-6.0.1.tar.gz
[root@test1 local]# cd redis-6.0.1
[root@test1 redis-6.0.1]# make
[root@test1 redis-6.0.1]# make install
(2)查看是否安裝成功
[root@test1 redis-6.0.1]# ls -lh /usr/local/bin/
[root@test1 redis-6.0.1]# redis-server -v
Redis server v=3.2.5 sha=00000000:0 malloc=jemalloc-4.0.3 bits=64 build=dae2abf3793b309d
(3)配置redis 創(chuàng)建dump file、進程pid、log目錄
[root@test1 redis-6.0.1]# cd /etc/
[root@test1 etc]# mkdir redis
[root@test1 etc]# cd /var/
[root@test1 var]# mkdir redis
[root@test1 var]# cd redis/
[root@test1 redis]# mkdir data log run
(4)修改配置文件
[root@test1 redis]# cd /usr/local/redis-6.0.1/
[root@test1 redis-6.0.1]# cp redis.conf /etc/redis/6379.conf
[root@test1 redis-6.0.1]# vim /etc/redis/6379.conf
#綁定的主機地址
bind 192.168.1.222
#端口
port 6379
#認證密碼(方便測試不設密碼,注釋掉)
#requirepass
#pid目錄
pidfile /var/redis/run/redis_6379.pid
#log存儲目錄
logfile /var/redis/log/redis.log
#dump目錄
dir /var/redis/data
#Redis默認不是以守護進程的方式運行,可以通過該配置項修改,使用yes啟用守護進程
daemonize yes
(5)設置啟動方式
[root@test1 redis-6.0.1]# cd /usr/local/redis-6.0.1/utils/
[root@test1 utils]# cp redis_init_script /etc/init.d/redis
[root@test1 utils]# vim /etc/init.d/redis #根據(jù)自己實際情況修改
/etc/init.d/redis文件的內(nèi)容如下。
#!/bin/sh
#
# Simple Redis init.d script conceived to work on Linux systems
# as it does use of the /proc filesystem.
REDISPORT=6379
EXEC=/usr/local/bin/redis-server
CLIEXEC=/usr/local/bin/redis-cli
PIDFILE=/var/run/redis_${REDISPORT}.pid
CONF="/etc/redis/${REDISPORT}.conf"
case "$1" in
start)
if [ -f $PIDFILE ]
then
echo "$PIDFILE exists, process is already running or crashed"
else
echo "Starting Redis server..."
$EXEC $CONF
fi
;;
stop)
if [ ! -f $PIDFILE ]
then
echo "$PIDFILE does not exist, process is not running"
else
PID=$(cat $PIDFILE)
echo "Stopping ..."
$CLIEXEC -p $REDISPORT shutdown
while [ -x /proc/${PID} ]
do
echo "Waiting for Redis to shutdown ..."
sleep 1
done
echo "Redis stopped"
fi
;;
*)
echo "Please use start or stop as first argument"
;;
esac
增加執(zhí)行權限,并啟動Redis。
[root@test1 utils]# chmod a+x /etc/init.d/redis #增加執(zhí)行權限
[root@test1 utils]# service redis start #啟動redis
(6)查看redis是否啟動
圖片
2.3 Lua訪問Redis
(1)連接redis,然后添加一些測試參數(shù)
[root@test1 utils]# redis-cli -h 192.168.1.222 -p 6379
192.168.1.222:6379> set "123" "456"
OK
(2)編寫連接Redis的Lua腳本
[root@test1 utils]# vim /usr/local/openresty/nginx/conf/lua/redis.lua
local redis = require "resty.redis"
local conn = redis.new()
conn.connect(conn, '192.168.1.222', '6379') #根據(jù)自己情況寫ip和端口號
local res = conn:get("123")
if res==ngx.null then
ngx.say("redis集群中不存在KEY——'123'")
return
end
ngx.say(res)
(3)在nginx.conf配置文件中的server下添加以下location
[root@test1 utils]# vim /usr/local/openresty/nginx/conf/nginx.conf
location /lua_redis {
default_type text/plain;
content_by_lua_file /usr/local/openresty/nginx/conf/lua/redis.lua;
}
隨后重新reload配置。
[root@test1 utils]# nginx -s reload #重啟一下Nginx
(4)驗證Lua訪問Redis的正確性
在瀏覽器輸入ip/lua_redis, 如果能看到下圖的內(nèi)容表示Lua可以訪問Redis。
圖片
準備工作已經(jīng)完成,現(xiàn)在要實現(xiàn)OpenResty+Lua+Redis自動封禁并解封IP了。3.4
2.4 OpenResty+Lua實現(xiàn)
(1)添加訪問控制的Lua腳本(只需要修改Lua腳本中連接Redis的IP和端口即可)
ok, err = conn:connect(“192.168.1.222”, 6379)
注意:如果在Nginx或者OpenResty的上層有用到阿里云的SLB負載均衡的話,需要修改一下腳本里的所有…ngx.var.remote_addr,把remote_addr替換成從SLB獲取真實IP的字段即可,不然獲取到的IP全都是阿里云SLB發(fā)過來的并且是處理過的IP,同時,這些IP全都是一個網(wǎng)段的,根本沒有辦法起到封禁的效果)。
完整的Lua腳本如下所示。
[root@test1 lua]# vim /usr/local/openresty/nginx/conf/lua/access.lua
local ip_block_time=300 --封禁IP時間(秒)
local ip_time_out=30 --指定ip訪問頻率時間段(秒)
local ip_max_count=20 --指定ip訪問頻率計數(shù)最大值(秒)
local BUSINESS = ngx.var.business --nginx的location中定義的業(yè)務標識符,也可以不加,不過加了后方便區(qū)分
--連接redis
local redis = require "resty.redis"
local conn = redis:new()
ok, err = conn:connect("192.168.1.222", 6379)
conn:set_timeout(2000) --超時時間2秒
--如果連接失敗,跳轉(zhuǎn)到腳本結尾
if not ok then
goto FLAG
end
--查詢ip是否被禁止訪問,如果存在則返回403錯誤代碼
is_block, err = conn:get(BUSINESS.."-BLOCK-"..ngx.var.remote_addr)
if is_block == '1' then
ngx.exit(403)
goto FLAG
end
--查詢redis中保存的ip的計數(shù)器
ip_count, err = conn:get(BUSINESS.."-COUNT-"..ngx.var.remote_addr)
if ip_count == ngx.null then --如果不存在,則將該IP存入redis,并將計數(shù)器設置為1、該KEY的超時時間為ip_time_out
res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr, 1)
res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out)
else
ip_count = ip_count + 1 --存在則將單位時間內(nèi)的訪問次數(shù)加1
if ip_count >= ip_max_count then --如果超過單位時間限制的訪問次數(shù),則添加限制訪問標識,限制時間為ip_block_time
res, err = conn:set(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, 1)
res, err = conn:expire(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, ip_block_time)
else
res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr,ip_count)
res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out)
end
end
-- 結束標記
::FLAG::
local ok, err = conn:close()
(2)在需要做訪問限制的location里加兩段代碼即可,這里用剛才的/lua做演示
[root@test1 lua]# vim /usr/local/openresty/nginx/conf/nginx.conf
圖片
主要是添加如下配置。
access_by_lua_file /usr/local/openresty/nginx/conf/lua/access.lua;
其中,set $business “l(fā)ua” 是為了把IP放進Redis的時候標明是哪個location的,可以不加這個配置。
隨后,重新reload配置。
[root@test1 lua]# nginx -s reload #修改完后重啟nginx
(3)打開瀏覽器訪問192.168.1.222/lua 并一直按F5刷新。
圖片
隨后,連接Redis,查看IP的訪問計數(shù)。
[root@test1 ~]# redis-cli -h 192.168.1.222 -p 6379
發(fā)現(xiàn)redis已經(jīng)在統(tǒng)計訪問lua這個網(wǎng)頁ip的訪問次數(shù)了
圖片
這個key的過期時間是30秒,如果30秒沒有重復訪問20次這個key就會消失,所以說正常用戶一般不會觸發(fā)這個封禁的腳本。
圖片
當30秒內(nèi)訪問超過了20次,發(fā)現(xiàn)觸發(fā)腳本了,變成了403
圖片
再次查看Redis的key,發(fā)現(xiàn)多了一個lua-block-192.168.1.158,過期時間是300秒,就是說在300秒內(nèi)這個ip無法繼續(xù)訪問192.168.1.222/lua這個頁面了。
圖片
過五分鐘后再去訪問這個頁面,又可以訪問了。
圖片
這個腳本的目的很簡單:一個IP如果在30秒內(nèi)其訪問次數(shù)達到20次則表明該IP訪問頻率太快了,因此將該IP封禁5分鐘。同時由于計數(shù)的KEY在Redis中的超時時間設置成了30秒,所以如果兩次訪問間隔時間大于30秒將會重新開始計數(shù)。
大家也可以將這個腳本優(yōu)化成,第一次封禁5分鐘,第二次封禁半小時,第三次封禁半天,第四次封禁三天,第五次永久封禁等等。