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

1
~]# yum install lua -y

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的注释

1
2
3
4
5
6
7
行注释:
--print("hi,boy")

多行注释:
--[[
xxxxxxx
]]

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库的分别从stdinstdout读写,readwrite函数

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指令

语法
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上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

按照一定的关系区别,分不分的代码进行上线,使代码的发布能平滑过渡上线

  1. 用户的信息cookie等信息区别
  2. 根据用户的ip地址,颗粒度更广
1
nginx+lua  通过Memcached校验IP。存在IP则成功-->访问成功即访问java_test,不成功即访问java_prod

执行过程L:

实践环境准备:

系统 服务 地址
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
-- 获取x-real-ip
clientIP = ngx.req.get_headers()["X-Real-IP"]

-- 如果IP为空 , 取x-forwarded-for
if clientIP == nil then
clientIP = ngx.req.get_headers()["x_forwarded_for"]
end

-- 如果IP为空-取remote_addr
if clientIP == nil then
clientIP = ngx.var.remote_addr
end

-- 定义本地,加载memcached
local memcached = require "resty.memcached"
-- 实例化对象
local memc, err = memcached:new()
-- 判断连接是否存在错误
if not memc then
ngx.say("failed to instantiate memc: ", err)
return
end
-- 建立memcache连接
local ok, err = memc:connect("127.0.0.1",11211)
-- 无法连接往前端抛出错误信息
if not ok then
ngx.say("failed to connect: ",err)
return
end
----------------------
--获取对象中的ip-存在值赋给res
local res, flags,err = memc:get(clientIP)
--
-- ngx.say("value key: ",res.clientIP)
if err then
ngx.say("failed to get clientIP ", err)
return
end
-- 如果值为1 则调用local-@java_rest
if res == "1" then
ngx.exec("@java_test")
retuen
end
-- 否则调用 local-@java_prod
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

防护代码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
3
4
5
6
location ^~ /upload {
root /soft/code/upload;
if ($request_filename ~* (.*)\.php){
return 403;
}
}

编写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