nginx + lua 实现 waf
Lua脚本基础语法
lua是一个简介、轻量、可扩展的脚本语言
nginx + lua的优势:
1 2 3 4 5 6 - 充分的结合 Nginx 的并发处理epoll优势和Lua的轻量实现简单的功能且高并发的场景 //epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。 - 场景: - 统计IP - 统计用户信息 - 安全WAF
安装lua
lua的运行
1 2 3 4 5 6 7 8 9 10 11 ~]# lua > print ("hello world" ) ~]# which lua /usr/bin/lua ~]# vim test.lua #!/usr/bin/lua print ("hello world" )print ("test:" ,a)~]# lua ./test.lua
Lua的注释
Lua的基础语法
1 2 3 4 5 6 7 变量定义 a = 123 ~]# vim test.lua #!/usr/bin/lua print ("test:" ,a) #不加$~]# lua ./test.lua
while循环
1 2 3 4 5 6 7 8 9 10 11 12 ~]# cat while .lua #!/usr/bin/lua sum =0 num =1 while num <= 100 do sum = sum + num num = num + 1 end print ("sum=" ,sum)//执行结果 ~]# lua while .lua sum = 5050
for循环
1 2 3 4 5 6 7 8 9 10 ~]# cat for .lua #!/usr/bin/lua sum = 0 for i = 1 ,100 do sum = sum + 1 end print ("sum=" ,sum)//执行结果 ~]# lua for .lua sum = 100
if 判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ~]# cat if .lua #!/usr/bin/lua if age == 40 and sex == "Man" then print ("男人大于40" ) elseif age > 60 and sex ~= "Women" then print ("非女人而且大于60" ) else local age = io .read () print ("Your age is" ..age) end // ~= 是不等于 // 字符串的拼接操作符 ".." // io 库的分别从stdin 和stdout 读写,read 和write 函数
Nginx加载Lua环境
默认情况下Nginx不支持Lua模块,需要安装luaJIT解释器,并且需要重新便宜Nginx,可选择使用openrestry
1 2 LuaJIT Ngx_devel_kit 和 lua-nginx-module
环境准备
1 ~]# yum -y install gcc gcc-c++ make pcre-devel zlib-devel openssl-devel
下载最新工具包
1 2 3 4 5 6 下载最新的luajit 和 ngx_devel_kit 以及 lua-nginx-module: ~]# mkdir -p /soft/src && cd /soft/src ~]# wget http://luajit.org/download/LuaJIT-2.0.4.tar.gz ~]# wget https://github.com/simpl/ngx_devel_kit/archive/v0.2.19.tar.gz ~]# wget https://github.com/openresty/lua-nginx-module/archive/v0.9.16.tar.gz ##视频中后又修改为v0.10.13.tar.gz
解压
1 2 3 4 5 6 解压 ngx_devel_kit 和 lua-nginx-module //解压后为 ngx_devel_kit-0.2.19 ~]# tar xf v0.2.19.tar.gz //解压后为 lua-nginx-module-0.9.16 ~]# tar xf v0.9.16.tar.gz
安装LuaJIT
1 2 3 4 5 安装LuaJIT ,Luajit是Lua即时编译器 ~]# tar zxvf LuaJIT-2.0.3.tar.gz ~]# cd LuaJIT-2.0.3 ~]# make && make install
安装Nginx并加载模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ~]# cd /soft/src ~]# wget http://nginx.org/download/nginx-1.12.2.tar.gz ~]# tar xf nginx-1.12.2.tar.gz ~]# cd nginx-1.12.2 # ./configure --prefix=/soft/nginx --with-http_ssl_module --with-http_stub_status_module --with-file-aio --with-http_dav_module --add-module=../ngx_devel_kit-0.2.19/ --add-module=../lua-nginx-module-0.9.16/ //lua-nginx-module-xxxx 这个版本如果换了就要重新再编译一次 make <-j2> && make install //如果不建立软链接,会出现share object 错误 ln -s /usr/local/lib/libluajit-5.1.so.2 /lib64/libluajit-5.1.so.2 // 测试 openresty 安装 vim nginx.conf location /test { default_type text/html; content_by_lua_block { ngx.say("Hello world") } } /soft/nginx/sbin/nginx -t //相当于nginx -t /soft/nginx/sbin/nginx //启动nginx 使用浏览器访问: 例如:39.112.53.22/test # //4.加载lua库,加入ld.so.conf文件 # echo "/usr/local/LuaJIT/lib" >> /etc/ld.so.conf# ldconfig
也可以直接部署春哥的开源项目 OpenResty
安装依赖包
1 2 ~]# yum install -y readline-devel pcre-devel openssl-devel ~]# cd /soft/src
下载编译 openresty
1 2 3 4 5 6 7 8 9 wget https://openresty.org/download/ngx_openresty-1.9.3.2.tar.gz tar zxf ngx_openresty-1.9.3.2.tar.gz cd ngx_openresty-1.9.3.2 ./configure --prefix=/soft/openresty-1.9.3.2 \ --with-luajit --with-http_stub_status_module \ --with-pcre --with-pcre-jit gmake && gmake install ln -s /soft/openresty-1.9.3.2/ /soft/openresty
启动 openresty
1 2 3 4 5 echo "PATH=/soft/openresty/nginx/sbin:$PATH" >> ~/.bashrc source ~/.bashrc nginx ps aux | grep nginx
测试openresty安装
1 2 3 4 5 6 7 vim /soft/openresty/nginx/conf/nginx.conf location /test { default_type text/html; content_by_lua_block { ngx.say("Hello world") } }
ps : 实际上openresty就是嵌套了nginx实现的
Nginx调用Lua指令
Nginx 调用 Lua 模块指令,Nginx的可插拔模块加载执行,共11个处理阶段
语法
set_by_lua set_by_lua_file
设置Nginx变量,可以实现负载的赋值逻辑
access_by_lua access_by_lua_file
请求访问阶段处理,用于访问控制
content_by_lua content_by_lua_file
内容处理器,接受请求处理并输出响应
变量
ngx.var
nginx变量
ngx.req.get_headers
获取请求头
ngx.req.get_url_args
获取url请求参数
ngx.redirect
重定向
ngx.print
输出响应内容体
ngx.say
输出响应内容体,最后输出一个换行符
ngx.header
输出响应头
Nginx + Lua 实现代码灰度发布
使用Nginx结合lua 实现代码灰度发布,灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
按照一定的关系区别,分不分的代码进行上线,使代码的发布能平滑过渡上线
用户的信息cookie等信息区别
根据用户的ip地址,颗粒度更广
1 nginx+lua 通过Memcached校验IP。存在IP则成功-->访问成功即访问java_test,不成功即访问java_prod
执行过程L:
1.用户请求到达前端代理Nginx,内嵌的lua模块会解析Nginx配置文件中Lua脚本
2.Lua脚本会获取客户端IP地址,查看Memcached缓存中是否存在该键值
3.如果存在则执行@java_test,否则执行@java_prod
4.如果使@java_test,那么location会将请求转发至新版代码的集群组
5.如果使@java_prod,那么location会将请求转发至原始版代码集群组
6.最后整个过程执行后结束
实践环境准备:
系统
服务
地址
centos7
Nginx+Lua+Memcached
192.168.29.19
centos7
Tomcat集群8080_prod
192.168.29.20
centos7
Tomcat集群9090_test
192.168.29.21
1.安装两台服务器 Tomcat,分别启动8080 和9090 端口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 ## 两台服务器都需要配置java环境 //yum install -y java 20 ~]# cd /soft 20 ~]# ./tomcat-8080/bin/startup.sh ## 可以在这个环境中改一下那个端口,scp到另一个服务器中,然后直接启动 21 ~]# sh /soft/tomcat-9090/bin/startup.sh 21 ~]# netstat -lntp //端口已经起来了 21 ~]# cd /soft/tomcat-9090/webapps/ROOT 21 ~]# ll //有一个test.jsp ## 20 ~]# cd /soft/tomcat-9090/webapps/ROOT 20 ~]# rm -rf ./* 20 ~]# vim test.jsp ## 里面的内容从21的服务器上的test.jsp里复制过来,记得改一下<TITLE>内端口号: JSP 8080 <%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <HTML> <HEAD> <TITLE>JSP 9090</TITLE> </HEAD> <BODY> <h1> Jsp 9090-test </h1> <% Random rand = new Random(); out.println("<h1>Random number:</h1>"); out.println(rand.nextInt(99)+100); %> % </BODY> % </HTML> ####################################### <%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <HTML> <HEAD> <TITLE>JSP 8080-prod</TITLE> </HEAD> <BODY> <h1> Jsp 8080-prod </h1> <% Random rand = new Random(); out.println("<h1>Random number:</h1>"); out.println(rand.nextInt(99)+100); %> % </BODY> % </HTML>
2.配置Memcached并让其支持Lua调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 //安装memcached服务 19 ~]# yum install memcached -y //配置memcached支持lua 19 ~]# cd /soft/src 19 ~]# wget https://github.com/agentzh/lua-resty-memcached/archive/v0.11.tar.gz 19 ~]# tar xf v0.11.tar.gz 19 ~]# cp -r /soft/src/lua-resty-memcached-0.11/lib/resty/memcached.lua /soft/nginx/conf/lua 19 ~]# ll /soft/nginx/conf/lua ## 有一个memcached.lua这个文件 //启动memcached 19 ~]# systemctl start memcached 19 ~]# systemctl enable memcached
3.配置负载均衡调度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 # 必须在http层 加一个include 包含conf。在conf中加入下面内容并且,删掉server 和 error的内容(不然会报错) # 在conf目录里加上 lua.conf: lua_package_path "/soft/nginx/conf/lua/memcached.lua"; upstream java_prod { server ip:8080; } upstream java_test { server ip:9090; } server { listen 80; #server_name 39.22.145.18; location /test { #视频写的hello default_type 'text/plain'; content_by_lua ngx.say("hello , lua scripts"); } location /myip { default_type 'test/plain'; content_by_lua ' clientIP = ngx.req.get_headers()["x_forwarded_for"] ngx.say("Forwarded_IP:",clientIP) if clientIP == nil then #视频中写的是nli clientIP = ngx.var.remote_addr ngx.say("Remote_IP:",clientIP) end '; } location / { default_type 'text/plain'; content_by_lua_file /soft/nginx/conf/lua/dep.lua; #这个dep.lua很重要 } location @java_prod { proxy_pass http://java_prod; include proxy_params; #访问浏览器400需要加上这条信息(因为代理Tomcat必须加上这个,这个文件有一些设置的一些头部信息) } location @java_test { proxy_pass http://java_test; include proxy_params; #访问浏览器400需要加上这条信息 } }
4.编写Nginx调用灰度发布Lua脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 ~]# cat /soft/nginx/conf/lua/dep.lua clientIP = ngx.req.get_headers()["X-Real-IP" ] if clientIP == nil then clientIP = ngx.req.get_headers()["x_forwarded_for" ] end if clientIP == nil then clientIP = ngx.var.remote_addr end local memcached = require "resty.memcached" local memc, err = memcached:new() if not memc then ngx.say("failed to instantiate memc: " , err) return end local ok, err = memc:connect("127.0.0.1" ,11211 ) if not ok then ngx.say("failed to connect: " ,err) return end local res, flags,err = memc:get(clientIP) if err then ngx.say("failed to get clientIP " , err) return end if res == "1" then ngx.exec("@java_test" ) retuen end ngx.exec("@java_prod" ) return
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ~]# /soft/nginx/sbin/nginx -t ~]# /soft/nginx/sbin/nginx -s reload # 使用浏览器访问nginx 的 ip/(加/) # 如果有问题 tail -f /soft/nginx/logs/error.log 查看报错信息 ######################################### 访问浏览器nginx ip/myip<路径名(nginx上写的)> 想要访问新的代码 (tomcat 9090),则在nginx主机上修改一下memcached: telnet 127.0.0.1 11211 set 211.161.160.201 0 0 1 1 #此页面变成了9090;如果delete 此ip #会在此换成8080的页面 # 当然生产环境肯定不会是我们这样修改,会在memcache的后台管理界面直接怼ip就可以
Nginx+Lua实现WAF
1.常见的恶意行为
爬虫行为和恶意抓取,资源盗取
防护手段
1.基础防盗链功能不让恶意用户能够轻易的爬取网站对外数据
access_module -> 对后台,部分用户服务的数据提供IP防护
防护代码demo:
1 2 3 4 5 6 7 8 9 set ip = 0 if ($http_x_forward_for ^~ 211.161.160.201) { set ip = 1 } localtion ^~ /admin { if ($ip ~ 0){ return 403 } }
常见的攻击手段
后台密码撞库,通过猜测密码字典不断对后台系统登录性尝试,获取后台登录密码
防护手段
1.后台登录密码复杂性
2.使用access_module 对后台提供IP防控
3.预警机制
文件上传漏洞,利用上传接口将恶意代码植入到服务器中,再通过url去访问执行代码
执行方式xxx.com/1.jpg/1.php
解决方法:
1 2 3 4 5 6 location ^~ /upload { root /soft/code/upload; if ($request_filename ~* (.*)\.php){ return 403; } }
3.常见的攻击手段
利用未过滤/未审核的用户输入进行SQL注入的攻击方法,让应用运行本不应该运行的SQL代码
防护手段
1.php配置开启安全相关限制
2.开发人员对sql提交进行审核,屏蔽常见的注入手段
3.Nginx+Lua构建WAF应用层防火墙,防止SQL注入
编写WAF拦截sql注入
1.快速安装lnmp架构
1 2 # 在有waf的服务器上,yum快速安装下面软件及插件 ~]# yum install mariadb mariadb-server php php-fpm php-mysql -y
2.配置Nginx + php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 conf.d]# cat phpserver.conf server { server_name 47.104.250.169; root /soft/code; index index.html index.php; location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /soft/code/$fastcgi_script_name; include fastcgi_params; } } ~]# systemctl start php-fpm ~]# systemctl start mariadb
3.配置MYSQL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 创建数据库 MariaDB [(node)]> create database info; MariaDB [(node)]> usr info; MariaDB [info]> create table user(id int(11),username varchar(64),password varchar(64),email varchar(64)); MariaDB [info]> desc user; xxxxxx xxxxxx # 插入数据 MariaDB [info]> insert into user (id,username,password,email) values(1,'xiaoyuan',('123'),'xiaoyuan@123456.com'); MariaDB [info]> select * from info.user; xxxxxx xxxxxx exit
4.配置php代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 //配置html文件: conf.d]# cat /soft/code/login.html <html> <head> <title> Sql注入演示场景 </title> <meta http-equiv="content-type"content="text/html;charset=utf-8"> </ead> <body> <form action='sql.php' method="post"> <table> <tr> <td> 用户: </td> <td><input type="text" name="username"></td> </tr> <tr> <td> 密码: </td> <td><input type="test" name="password"></td> </tr> <tr> <td><input type="submit" value="提交"></td> <td><input type="reset" value="重置"></td> </tr> </table> </form> </body> </html> //被html调用的sql.php文件 conf.d]# cat /soft/code/sql.php <?php $conn = mysql_connect("localhost",'root','') or die("数据库连接失败"); mysql_select_db("info",$conn) or die("您选择的数据库不存在"); $name=$_POST['username']; $pwd=$_POST['password']; $sql="select * from user where username='$name' and password='$pwd'"; echo $sql."<br />"; $query=mysql_query($sql); $arr=mysql_fetch_array($query); if($arr){ echo "login success!<br />"; echo $arr[1]; echo $arr[3]."<br /><br />"; }else{ echo "login failed!"; } ?>
1 2 3 4 5 浏览器访问47.104.250.169/login.html 显示404打不开,先把lua.conf 改个名; mv lua.conf lua.conf.bak 继续访问 # 登录页面显示; (上面插入的数据用户账号密码为 xiaoyuan 123,此账号密码为正确的) # 然后再用户框输入 ' or 1=1#' 直接提交;显示登陆成功
5.使用lua解决此类安全问题
1 nginx+lua ->拦截cookie类型工具;拦截异常post请求;拦截CC攻击;拦截URL;拦截arg -> java\PHP\Python
6.部署Waf相关防护代码
相关项目:https://github.com/loveshell/ngx_lua_waf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ~]# cd /soft/src ~]# git clone https://github.com/loveshell/ngx_lua_waf.git //把ngx_lua_waf 复制到nginx的目录下,解压命名为waf ~]# cp -r ngx_lua_waf /soft/nginx/conf/waf //在conf.d的 phpserver.conf 最上面添加 lua_package_path "/soft/nginx/conf/waf/?.lua"; #视频中为/etc/waf/?.lua lua_shared_dict limit 10m; init_by_lua_file /soft/nginx/conf/waf/init.lua; #/etc/waf/init.lua access_by_lua_file /soft/nginx/conf/waf/waf.lua; #/etc/waf/waf.lua ~]# /soft/nginx/sbin/nginx -t ~]# /soft/nginx/sbin/nginx -s reload //配置config.lua里的waf规则目录(一般在waf/conf/目录下) RulePath = "/soft/nginx/conf/waf/wafconf/" # 绝对路径如有变动,需对应修改,然后重启nginx即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 浏览器访问47.104.250.169/login.html # 登录页面显示; (上面插入的数据用户账号密码为 xiaoyuan 123,此账号密码为正确的) # 输入正确的和错误的账号密码尝试一下。 # 然后再用户框输入 ' or 1=1#' 直接提交;显示登陆成功。所以说需要配置wafconf/目录下的定义规则: 修改post文件(正则匹配): 首行添加: \sor\s+ ~]# /soft/nginx/sbin/nginx -t ~]# /soft/nginx/sbin/nginx -s reload 再次测试在用户框输入 ' or 1=1#' 直接提交; 显示被拦截
编写WAF拦截CC攻击
1 2 3 4 5 6 7 8 9 10 ~]# cd /soft/nginx/conf/waf ~]# vim config.lua CCDeny="on" CCrate="600/60" #每分钟只能有600次请求,如果超过就会被拒绝掉 ~]# /soft/nginx/sbin/nginx -t ~]# /soft/nginx/sbin/nginx -s reload 本地压测 ab -n 2000 -c 200 http://47.104.250.169/login.html //请求2000次,并发200 再次访问浏览器http://47.104.250.169/login.html ;显示503