浅谈Nginx请求过滤模块的开发

2017-10-18 8,978

由于Nginx(engine x)具有高性能、可反向代理等优秀特点,因而在市场中被广泛的使用。客户端发起的HTTP请求一般是通过Nginx代理到后台服务器去处理的,因此我们可以通过Nginx监控客户端的HTTP请求内容是否包含攻击内容,从而判断请求的合法性。为了实现对HTTP请求的过滤,我们需要开发一个可加载到Nginx中的自定义filter模块。其功能为:当检测到客户端的请求包含攻击时,阻止Nginx转发HTTP请求给后台服务器,直接返回禁止访问的HTTP请求响应给客户端。实现这样的一个功能模块,我们需要先了解Nginx的handler模块及其加载实现方式的相关知识。


1
Handler模块
Handler模块简介


        Handler模块就是接受来自客户端的请求并产生输出的模块。Handler模块处理的结果通常有三种情况:处理成功、处理失败或者拒绝处理。


模块的基本结构


       模块配置结构

       Nginx的配置信息分成了几个作用域(scope,有时也称作上下文),这就是main、server以及location。同样的每个模块提供的配置指令也可以出现在这几个作用域里。那对于这三个作用域的配置信息,每个模块就需要定义三个不同的数据结构去进行存储。当然,不是每个模块都会在这三个作用域都提供配置指令的。那么也就不一定每个模块都需要定义三个数据结构去存储这些配置信息了。视模块的实现而言,需要几个就定义几个。

对于模块配置信息的定义,命名习惯是ngx_http_<module name>_(main|srv|loc)_conf_t。例如本文中的filter module定义

typedef struct {

     ngx_str_t fm_string;

     ngx_flag_t fm_enable;

} ngx_http_fm_loc_conf_t;

        模块配置指令

        一个模块的配置指令定义在一个静态数组中。例如本文中的ngx_http_fm_commands定义:

static ngx_command_t ngx_fm_commands[] = {

           { ngx_string("ngx_fm_string"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_SIF_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_FLAG, ngx_fm_string, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_fm_loc_conf_t, fm_string), NULL },

           { ngx_string("ngx_fm_enable"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_SIF_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_FLAG, ngx_fm_enable, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_fm_loc_conf_t, fm_enable), NULL },

ngx_null_command

};

           注意:数组最后需要以ngx_null_command作为结尾元素。

       模块上下文结构

       ngx_http_module_t静态变量提供了一组回调函数指针,这些函数有在创建存储配置信息的对象函数,也有在创建前、后调用的函数。Nginx 会在合适的时间对它们进行调用。这里不对其结构作详细介绍了,想要详细了解可参考阿里的《Nginx开发从入门到精通》文档。例如本文中ngx_http_fm_module_ctx定义:

static ngx_http_module_t ngx_http_fm_module_ctx = {

           NULL, ngx_fm_module_init, NULL, NULL, NULL, NULL, ngx_fm_create_loc_conf, NULL

};

       模块的定义

       开发nginx模块,需要定义一个ngx_module_t类型的变量来说明这个模块本身的信息。它包含配置信息、上下文信息等。加载模块的上层代码需要通过定义的模块结构获取这些信息。本文实例中的模块定义为:

ngx_module_t ngx_http_fm_module = {

           NGX_MODULE_V1, &ngx_http_fm_module_ctx, ngx_http_fm_commands, NGX_HTTP_MODULE, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NGX_MODULE_V1_PADDING

};


Handler模块的基本结构


        Handler模块需要提供一个处理函数,用来处理客户端的请求,可以在函数中生成内容,也可以拒绝,让其他的handler去处理。这个函数的原型为:

typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);

r结构体是http请求。该函数处理成功返回NGX_OK,失败返回NGX_ERROR,拒绝处理(留给后续handler进行处理)返回NGX_DECLINE。


Handler模块的挂载


Nginx模块的挂载方式有两种。一种就是按处理阶段挂载;另外一种就是按需挂载。

按处理阶段挂载

为了更精细的控制对于客户端请求的处理过程,nginx把这个处理过程划分成了11个阶段。分别为:

NGX_HTTP_POST_READ_PHASE(读取请求内容阶段)

NGX_HTTP_SERVER_REWRITE_PHASE(Server请求地址重写阶段)

NGX_HTTP_FIND_CONFIG_PHASE(配置查找阶段)

NGX_HTTP_REWRITE_PHASE(Location请求地址重写阶段)

NGX_HTTP_POST_REWRITE_PHASE(请求地址重写提交阶段)

NGX_HTTP_PREACCESS_PHASE(访问权限检查准备阶段)

NGX_HTTP_ACCESS_PHASE(访问权限检查阶段)

NGX_HTTP_POST_ACCESS_PHASE(访问权限检查提交阶段)

NGX_HTTP_TRY_FILES_PHASE(配置项try_files处理阶段)

NGX_HTTP_CONTENT_PHASE(内容产生阶段)

NGX_HTTP_LOG_PHASE(日志模块处理阶段)

注意:有几个阶段时特例,它不调用挂载地任何的handler,所以你就不用挂载在这些阶段了。

       NGX_HTTP_FIND_CONFIG_PHASE(配置查找阶段)

NGX_HTTP_POST_ACCESS_PHASE访问权限检查提交阶段)

NGX_HTTP_POST_REWRITE_PHASE(请求地址重写提交阶段)

NGX_HTTP_TRY_FILES_PHASE(配置项try_files处理阶段)

本文中的实例采用的是按阶段挂载handler,实现如下:

static ngx_int_t ngx_fm_module_init(ngx_conf_t *cf) {

     ngx_http_handler_pt             *h;

     ngx_http_core_main_conf_t *cmcf;

     cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

     h = ngx_array_push(&cmcf->phases[NGX_HTTP_POST_READ_PHASE].handlers);

     if(h == NULL) {

           return NGX_ERROR;

     }

     *h = ngx_http_fm_handler;

     return NGX_OK;

}

按需挂载

     以这种方式挂载的handler也被称为content handler。当一个请求进来以后,nginx从NGX_HTTP_POST_READ_PHASE阶段开始依次执行每个阶段中所有handler。执行到 NGX_HTTP_CONTENT_PHASE阶段的时候,如果这个location有一个对应的content handler模块,那么就去执行这个content handler模块真正的处理函数。否则继续依次执行NGX_HTTP_CONTENT_PHASE阶段中所有content phase handlers,直到某个函数处理返回NGX_OK或者NGX_ERROR。换句话说,当某个location处理到NGX_HTTP_CONTENT_PHASE阶段时,如果有content handler模块,那么NGX_HTTP_CONTENT_PHASE挂载的所有content phase handlers都不会被执行了。但是使用这个方法挂载上去的handler有一个特点是必须在NGX_HTTP_CONTENT_PHASE阶段才能执行到。如果你想自己的handler在更早的阶段执行,那就不要使用这种挂载方式。那么在什么情况会使用这种方式来挂载呢?一般情况下,某个模块对某个location进行了处理以后,发现符合自己处理的逻辑,而且也没有必要再调用NGX_HTTP_CONTENT_PHASE阶段的其它handler进行处理的时候,就动态挂载上这个handler。


Handler模块的实现


        通过上面的知识点,我们对Handler模块结构有了一些了解,那么接下来我们就需要将这些知识点串联起来,实现一个模块。实现步骤:

1)   编写模块基本结构,包含模块定义,模块上下文结构,模块的配置结构等。

2)   编写handler挂载函数,我们选择按处理阶段挂载。

3)   编写handler处理函数,我们将要实现的filter功能就是通过此函数实现。

4)   编译模块到Nginx之中,我们通过编译Nginx源码添加模块的方式将自定义的filter模块整合到Nginx。


2
实例实现


如果你的开发系统环境是Linux,那么一切省事了,编译和安装可以在一个环境中进行。由于我的开发环境是OS X,所以需要在虚拟机中建立一个Linux系统作为编译、安装环境。


Linux编译环境配置


Linux译环境配置编

yum install gcc gcc-c++ openssl-devel pcre-devel wget

下载、解压Nginx源码

wget http://nginx.org/download/nginx-1.13.4.tar.gz

tar xzvf nginx-1.13.4.tar.gz

开发环境配置

OS X环境下默认没有gcc,需要通过应用商店安装Xcode,安装完后测试执行gcc –v。

下载适用于系统版本的Eclipse CDT,解压运行,选择工作目录。


开发filter module实例


1)创建C项目,命名为“ngx_fm”

2)添加Nginx模块的源码文件“ngx_filter.h、 ngx_filter.c ”,定义模块配置结构体和声明函数。

           3)具体实现源码请见:https://github.com/leonSecTec/ngx_fm。HTTP请求过滤功能的实现主要是在ngx_http_fm_handler函数中添加了一个关键字搜索函数,用以检查客户端提交的请求中是否包含攻击。

int ngx_search(unsigned char* str) {

     const char *keyword[] = { "alert", "or", "and", "../" };

     int i;

     for(i = 0; i < 4; i++) {

           if(ngx_sunday((const char*)str, keyword[i]) != NULL) {

                return 1;

           }

     }

     return 0;

}

static ngx_int_t ngx_http_fm_handler(ngx_http_request_t *r) {

     if(ngx_search(r->uri.data)) {

           ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "attack data: %s", r->uri.data);

           return NGX_HTTP_NOT_ALLOWED;

     }

     return NGX_OK;

}

4)   在项目中添加“config”编译配置文件,其内容如下

ngx_addon_name=ngx_http_fm_module

HTTP_MODULES="$HTTP_MODULES ngx_http_fm_module"

NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/src/ngx_search.c $ngx_addon_dir/src/ngx_filter.c"


编译添加了自定义filter 模块的Nginx源码


将filter模块源码上传到Linux虚拟机中的Nginx源码解压后的同级目录,然后执行如下命令,进行代码编译及安装。

cd nginx-1.13.4

./configure --prefix=/opt/nginx --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-mail --with-mail_ssl_module --add-module=../ngx_fm

make –j4

make install


测试运行


1)启动运行Nginx,即执行 /opt/nginx/sbin/nginx &

2)访问当前测试站点,查看日志输出,即执行cat /opt/nginx/log/error.log。当提交具有攻击关键字的请求时,filter模块会因检测到请求中具有攻击关键字而返回405。


3
一些后话


本文中的实例实现的仅仅是针对HTTP GET请求URL内容的关键字过滤。从严格的角度来说,其并未实现真正意义上的请求过滤处理。要实现一个完整的过滤功能模块,需要对HTTP请求行、消息报头以及请求正文进行检测。由于时间有限,这里暂时不作深入研究,有兴趣的同学可以深入了解下。


参考文档:

http://tengine.taobao.org/book/



本文作者:饿了么SRC

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/62142.html

Tags:
评论  (0)
快来写下你的想法吧!

饿了么SRC

文章数:4 积分: 10

饿了么安全应急响应中心。

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号