背景
nginx天下第一,谁赞同谁反对?!
分析
调试
直接下载并生成Makefile:
# 下载nginx 1.16.0的源码
wget https://nginx.org/download/nginx-1.16.0.tar.gz
tar xf nginx-1.16.0.tar.gz && rm nginx-1.16.0.tar.gz
cd nginx-1.16.0
# 下载依赖
# mkdir third_lib && cd third_lib
# wget https://sourceforge.net/projects/pcre/files/pcre/8.41/pcre-8.41.tar.gz/download -O pcre-8.41.tar.gz
# wget https://www.openssl.org/source/openssl-1.1.0.tar.gz
#
# 由于只关注核心,可不安装依赖
./configure --without-http_rewrite_module --with-debug
使用Clion打开项目,配置调试:

之后再编辑配置文件(默认根据prefix来确定文件位置)添加http服务,打断点就可以调试了。在调试时,配置文件里添加如下两行防止后台起工作进程:
daemon off;
master_process off;
架构
不像apache httpd那样总是被修改添加各种功能,到目前为止我只见过一次某产品对nginx进行小的修改,因此通常不必对其逆向分析,只需要确定版本再看对应源码即可,所以这里简单说明下整体架构与流程便于快速定位关键位置。
基础类型
nginx在内部编码用到了一些自定义的类型,这里简单说明它们的特征,至于其定义等需要时看代码即可,而相关操作函数看名字就知道意思了:
1.ngx_str_t
是带长度的非\0
终结符结尾的字符串,用它可以存含\0
的字符串,最重要的是它只含长度与起始指针可以复用字符串提高操作效率
2.ngx_array_t
数组用于连续存放同类型数据,可用下标索引,而且它可以动态扩容
3.ngx_list_t
链表在内存中并不连续,因此其增删时间复杂度为O(1),但其实它每个节点不只存放一个数据,它每个节点是大小固定的数组
4.ngx_queue_t
队列结构上是双向链表,就linux中经典的双向链表
5.ngx_hash_t
是采用开放地址法解决冲突的散列,散列是实现快速查找的一种结构与算法,在nginx的实现上它仅用于查找,在初始化后就不能修改了
6.ngx_rbtree_t
树是另一种快速查找结构,而红黑树嘛让人望而生畏,它能在增删查上都很平衡,比较重啦
7.ngx_radix_tree_t
是普通二叉树,它只能存放32位整数,由于不需动态平衡它的修改速度会更快
架构细节
nginx以性能闻名,它本身十分小巧不处理动态请求(有需求可用它的改版openresty,嵌入lua来实现灵活与性能并存),其架构如下图:

master进程负责管理整个nginx,worker进程负责处理请求任务,worker采用异步非阻塞的方式,通常直接处理静态请求,而动态请求是交由上游服务器处理。nginx才用模块化设计,主框架只有少量代码,大部分功能由各模块完成,如下图:

如nginx最基础的功能就在ngx_core_module
中实现,事件驱动功能在ngx_event_module
中实现等等。由于基本遇到的功能都在模块中实现所以得了解下模块代码的组成,它由ngx_module_t
类型的结构体声明各种信息:
struct ngx_module_s {
ngx_uint_t ctx_index; // 同一种类型的模块的索引
ngx_uint_t index; // 全局索引,模块在加载时依次为其编号
char *name; // 模块名称
ngx_uint_t version; // nginx的版本
const char *signature; // 签名,没看懂todo...
void *ctx; // 上下文对象,不同类型的模块会定义各自的上下文结构,存储于此
ngx_command_t *commands; // 命令结构体
ngx_uint_t type; // 模块类型,如EVENT模块,HTTP模块等
ngx_int_t (*init_master)(ngx_log_t *log); // 在master进程初始化时调用
ngx_int_t (*init_module)(ngx_cycle_t *cycle); // 在module初始化时被调用
ngx_int_t (*init_process)(ngx_cycle_t *cycle); // 在worker进程初始化被调用
/* ... */
};
这里面有几个域要特别关注,首先是ctx_index
和ctx
它们都是和模块类型相关的,每种核心模块都是一种类型,其他模块基本都属于它们中的一种,如ngx_http_proxy_module
就属于NGX_HTTP_MODULE
,而所有核心模块都属于NGX_CORE_MODULE
类型。同种类型通过ctx
进行扩展,此处只关注两种:
/* 对于NGX_CORE_MODULE类型的模块,它的ctx为如下结构 */
typedef struct {
ngx_str_t name;
void *(*create_conf)(ngx_cycle_t *cycle); // 创建配置
char *(*init_conf)(ngx_cycle_t *cycle, void *conf); // 初始化配置
} ngx_core_module_t;
/* 对于NGX_HTTP_MODULE类型的模块,它的ctx为如下结构 */
typedef struct {
ngx_int_t (*preconfiguration)(ngx_conf_t *cf); // 配置前
ngx_int_t (*postconfiguration)(ngx_conf_t *cf); // 配置后
void *(*create_main_conf)(ngx_conf_t *cf); // http服务配置分三级 main->server->location,这里会创建
char *(*init_main_conf)(ngx_conf_t *cf, void *conf);
void *(*create_srv_conf)(ngx_conf_t *cf);
char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
void *(*create_loc_conf)(ngx_conf_t *cf);
char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;
另一个要关注的是commands
,它是ngx_command_t
类型的结构体,里面存放的是该模块的配置命令,nginx的配置功能很强大也是阅读源码的一个重点,在理解模块时也得重点关注它,其定义如下:
struct ngx_command_s {
ngx_str_t name; // 命令的名字
ngx_uint_t type; // 命令的类型
char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); // 解析该命令时的回调函数
ngx_uint_t conf; //
ngx_uint_t offset; // 偏移
void *post; //
};
这里命令的类型比较复杂,它的值由几种类型的值位运算得到:
1.命令位置:表示允许它出现的位置,如NGX_HTTP_MAIN_CONF
表示它能在http块这一级出现,它它可以多个位置或
2.参数数目:表示该指令接收多少参数,必选参数多少最多几个等,用于进行参数校验,如NGX_CONF_1MORE
表示最少一个参数
3.命令类型:表示它是单条命令还是配置块命令
除此外还有些其他类型,遇到了再查就行。
流程
初始化流程
该阶段完成整个服务的初始化,使其进入到等待处理请求的阶段,这个过程中有两个重要的结构体ngx_cycle_t
和ngx_connection_t
,前者保存生命周期的所有数据,它会贯穿整个处理流程,而后者是对socket及其相关属性/事件handler的封装。
ngx_time_init
ngx_regex_init
ngx_log_init
ngx_ssl_init
$(init_cycle init) // 这里根据命令行参数等信息创建个最初始的cycle,正常的cycle可能由之前的cycle起始,如`-s reload`重载配置
ngx_os_init // 这里根据系统的软硬件信息设置一些属性,用于如获取更好的性能
ngx_preinit_modules // 预初始化模块,这里只是给模块创建索引和复制模块名,计算模块个数等
ngx_init_cycle
-> $(各种成员变量分配与初始化)
-> ngx_cycle_modules // 为cycle->modules分配空间存放所有模块的ngx_module_t
-> for all core modules
-> $(core_module.create_conf) // O-O 1.对所有的核心模块,调用ngx_module_t.ctx.create_conf创建配置
-> ngx_conf_parse // 配置文件解析!!!!!!!!!!!
-> for all core modules
-> $(core_module.init_conf) // O-O 2.对所有的核心模块,调用ngx_module_t.ctx.init_conf初始化配置
-> $(共享内存,监听端口等初始化)
-> ngx_open_listening_sockets // 开始监听端口
-> ngx_init_modules
-> for all modules
-> $(module.init_module) // O-O 3.调用每个模块的init_module回调
ngx_init_signals
ngx_single_process_cycle
-> ngx_set_environment
-> for all modules
-> $(module.init_process) // O-O 4.调用每个模块的init_process回调
-> $(开始事件循环)
这里先看O-O的四个步骤,前两个只对核心模块起作用,后两个作用于所有模块,它们很简单注意下执行时机即可。重点得关注ngx_conf_parse
,它比较复杂会解析配置,nginx的配置有点像脚本语言了,它也用词法和语法分析去解析然后匹配命令处理,不同的命令做的事可能大相径庭,简单的命令仅设置下命令结构体的值,复杂的会执行很多代码(如listening命令需要监听端口,而lua/perl等模块的指令会执行语句),不过了解下命令解析的结果即可不必仔细分析其过程,由于本文关注的还是作为web服务器,所以在这步中一定会解析到http{}
这条配置,它就会执行很多任务:
ngx_http_block
-> for all http module
-> $(alloc main|server|location conf) // 这里会先计算有多少个NGX_HTTP_MODULE类型的模块,分配对应个数的空间存放这三类配置数据
-> $(http_module.ctx.create_[main|srv|loc]_conf) // 对所有的NGX_HTTP_MODULE类型的模块调用其ctx(结构见上文)的创建配置回调
-> $(http_module.ctx.preconfiguration)
-> ngx_conf_parse // 继续解析这个块里的其他配置
-> for all http module
-> $(http_module.ctx.init_main_conf)
-> $(ngx_http_init_locations)
-> ngx_http_init_phases // 为阶段handler分配空间
-> ngx_http_init_headers_in_hash // 初始化特殊请求头处理hash表
-> for all http module
-> $(http_module.ctx.postconfiguration)
-> ngx_http_variables_init_vars // 初始化各种http变量
-> ngx_http_init_phase_handlers // 将所有handler平铺,设置checker等
-> ngx_http_optimize_servers
-> $(优化端口,地址等)
-> ngx_http_init_listening
-> ngx_http_add_listening // 在这里设置listener结构,主要关注`ls->handler = ngx_http_init_connection`指定连接到来时怎么处理
可见http命令会做很多事,知道了http类型的模块的ctx里注册的函数在什么时候被调用,这里要重点关注两个东西了:
1.ngx_http_init_headers_in_hash
涉及的headers哈希表,它是将ngx_http_headers_in
这个数组转换成了hash便于查找,而这个数组里存放了对特定请求头应该执行的操作,后文会讲到
2.ngx_http_init_phase_handlers
涉及到了phase这个概念,它代表着http请求的各阶段,在大多的阶段模块都可以在其上插入handler(挂钩)来实现特定功能,还是放下一节介绍吧。
这是总体的初始化流程,可以看到里面会有很多回调,在实际的初始化时会根据配置文件内容与编译的模块执行多种逻辑,显然,这个过程攻击者不可控因此仅作了解,下面的流程会更加重要。
HTTP请求处理流程
这部分最重要不过实际比较简单,不像apache httpd,nginx本身功能有限,实现得十分小巧。在开始流程前,需要先引入两个重要的数据结构,ngx_buf_t
用于表示一种资源,可以是内存中的也可以是一种文件资源或网络资源,ngx_http_request_t
用于存储一个http请求的所有信息(是不是发现这两个结构还有connection结构和httpd一一对应啦O_o)。
现在看流程,上面提到它在解析http
命令时设置了ls->handler = ngx_http_init_connection
,当有连接到来时由该回调处理:
/* 新连接到来时 */
ngx_http_init_connection
-> c->read->handler = ngx_http_wait_request_handler // 这里设置新连接的读处理函数
-> c->write->handler = ngx_http_empty_handler // 写处理函数,它是空函数,才开始对写事件不做任何操作
/* 连接上有数据到来时(发生可读事件) */
ngx_http_wait_request_handler
-> recv // 先读一些数据
-> ngx_http_create_request // 创建
-> rev->handler = ngx_http_process_request_line // 设置读事件的handler,下面一行是调用,有很多这种形式的代码,因为nginx是异步非阻塞的,本次不一定能完成处理
-> ngx_http_process_request_line
-> ngx_http_read_request_header // 读请求头
-> ngx_http_parse_request_line // 处理请求行(第一行)
-> ngx_http_process_request_uri // 处理第一行得到的uri,其实周围还有处理协议版本,若uri带host则处理host等
-> rev->handler = ngx_http_process_request_headers
-> ngx_http_process_request_headers // 开始处理请求头
-> for each row
-> ngx_http_parse_header_line // 解析一个请求头,这里是重点咯
-> hashfind(header)->handler // 检查这个请求头有没有注册特别的处理函数,上面提到了那个hash表,如对Content-Length头它会检查之前有没有读到过,读到那就是非法的请求,有哪些头会特殊处理详见`ngx_http_headers_in`
-> ngx_http_process_request // 开始处理请求,和httpd一样它不会一口气读完整个请求再处理,因为可能并不需要读body
-> c->read->handler = ngx_http_request_handler // 这里连接的读写handler都设置成了它,其内部会根据事件类型调用r->[read|write]_event_handler
-> c->write->handler = ngx_http_request_handler
-> r->read_event_handler = ngx_http_block_reading
-> ngx_http_handler
-> r->phase_handler = r->internal?cmcf->phase_engine.server_rewrite_index:0 // 这里如果是内部请求则从server级重写阶段开始,否则从第一个阶段开始
-> r->write_event_handler = ngx_http_core_run_phases // 设置请求的写事件
-> ngx_http_core_run_phases // 进入阶段处理
-> ngx_http_run_posted_requests
图片如下啦:
到此为止,已经完成请求头的解析,该正式进入请求处理阶段了!
阶段
nginx就请求处理分为了11个阶段(phase),如下:
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0,
NGX_HTTP_SERVER_REWRITE_PHASE, // server块中配置了rewrite指令,重写URL
NGX_HTTP_FIND_CONFIG_PHASE, // 查找匹配的location配置,不能自定义handler函数
NGX_HTTP_REWRITE_PHASE, // location块中配置了rewrite指令,重写URL
NGX_HTTP_POST_REWRITE_PHASE, // 检查是否发生了URL重写,如果有,重新回到FIND_CONFIG阶段;不能自定义handler函数
NGX_HTTP_PREACCESS_PHASE, // 访问控制,比如限流模块会注册handler函数到此阶段
NGX_HTTP_ACCESS_PHASE, // 访问权限控制,比如基于IP黑白名单的权限控制、基于用户名密码的权限控制等
NGX_HTTP_POST_ACCESS_PHASE, // 根据访问权限控制阶段做相应处理,不能自定义handler函数;
NGX_HTTP_PRECONTENT_PHASE, // 内容预处理阶段,配置了try_files指令或者mirror指令,才会有此阶段
NGX_HTTP_CONTENT_PHASE, // 内容产生阶段,返回响应给客户端
NGX_HTTP_LOG_PHASE // 日志记录
} ngx_http_phases;
每个模块可以在自己感兴趣的阶段注册自己的handler,例如ngx_http_access_module
就在NGX_HTTP_ACCESS_PHASE
阶段注册了自己的handler:
/* ngx_http_access_module.ctx.postconfiguration = ngx_http_access_init
* 在初始化流程,解析道`http`命令时,会遍历http模块,之后会调用如下函数
*/
static ngx_int_t ngx_http_access_init(ngx_conf_t *cf)
{
ngx_http_core_main_conf_t *cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
ngx_http_handler_pt *h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers);
*h = ngx_http_access_handler; // 在NGX_HTTP_ACCESS_PHASE阶段的handlers数组中,插入自己的handler
return NGX_OK;
}
可以看到cmcf->phases
是个二维数组,初始化后分阶段保存着相关的handler,但实际运行时它不是使用这个二维数组,而是会平铺成一维的数组,重新回到上面的ngx_http_init_phase_handlers
函数,它的代码如下:
static ngx_int_t ngx_http_init_phase_handlers(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf) {
...
/* 这里两个index很特殊,因为重写意味着一些阶段需要重新执行(见下文rewrite部分),这两个就是用来定位SERVER_REWRITE和REWRITE在被平铺后的起始索引 */
cmcf->phase_engine.server_rewrite_index = (ngx_uint_t) -1;
cmcf->phase_engine.location_rewrite_index = (ngx_uint_t) -1;
find_config_index = 0;
use_rewrite = cmcf->phases[NGX_HTTP_REWRITE_PHASE].handlers.nelts ? 1 : 0;
use_access = cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers.nelts ? 1 : 0;
n = 1 /* find config phase */
+ use_rewrite /* post rewrite phase */
+ use_access; /* post access phase */
/* 为了平铺成一维的,需要先计算总共有多少handlers,然后分配空间 */
for (i = 0; i < NGX_HTTP_LOG_PHASE; i++) {
n += cmcf->phases[i].handlers.nelts;
}
ph = ngx_pcalloc(cf->pool, n * sizeof(ngx_http_phase_handler_t) + sizeof(void *));
cmcf->phase_engine.handlers = ph;
n = 0;
/* 开始平铺,外层是一个一个的阶段,内层是各阶段的handlers */
for (i = 0; i < NGX_HTTP_LOG_PHASE; i++) {
h = cmcf->phases[i].handlers.elts;
switch (i) {
case NGX_HTTP_SERVER_REWRITE_PHASE:
if (cmcf->phase_engine.server_rewrite_index == (ngx_uint_t) -1) {
cmcf->phase_engine.server_rewrite_index = n;
}
checker = ngx_http_core_rewrite_phase;
break;
case NGX_HTTP_FIND_CONFIG_PHASE:
find_config_index = n;
ph->checker = ngx_http_core_find_config_phase;
n++;
ph++;
continue;
...
case NGX_HTTP_CONTENT_PHASE:
checker = ngx_http_core_content_phase;
break;
default:
checker = ngx_http_core_generic_phase;
}
n += cmcf->phases[i].handlers.nelts;
for (j = cmcf->phases[i].handlers.nelts - 1; j >= 0; j--) {
ph->checker = checker;
ph->handler = h[j];
ph->next = n; // 注意这里的next,指的是下一个阶段的handlers,用于快速跳转到下一个阶段
ph++;
}
}
return NGX_OK;
}
这里还有个概念就是checker
,它用于在调用后对handler的结果进行处理,不同阶段可能需要对handler结果进行不同处理,如有的阶段有一个handler执行成功那么就可以进入下一阶段了,有的阶段一个handler执行失败则完全终止处理(如鉴权),checker是固定的不要修改!再回到上一小节的ngx_http_core_run_phases
,它的过程就很简单:
void ngx_http_core_run_phases(ngx_http_request_t *r){
cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
ph = cmcf->phase_engine.handlers;
while (ph[r->phase_handler].checker) { // 循环执行每一个handler
rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]); // 这里看到它是由checker间接调用的handler
if (rc == NGX_OK) {
return;
}
}
}
这些阶段基本妹啥说的,只有一个NGX_HTTP_CONTENT_PHASE
比较特殊,它是会产生响应的所以要特殊对待,这从它的checker中可看出:
ngx_int_t ngx_http_core_content_phase(ngx_http_request_t *r,
ngx_http_phase_handler_t *ph)
{
/* 这里优先尝试使用content_handler,它来自location块的handler,例如proxy_pass命令会以这种方式注册 */
if (r->content_handler) {
r->write_event_handler = ngx_http_request_empty_handler;
ngx_http_finalize_request(r, r->content_handler(r));
return NGX_OK;
}
/* 如果location块没有显式配置handler则继续用通用的方式处理,例如静态文件等就是用的这种方式 */
rc = ph->handler(r);
if (rc != NGX_DECLINED) {
ngx_http_finalize_request(r, rc);
return NGX_OK;
}
ph++;
if (ph->checker) {
r->phase_handler++;
return NGX_AGAIN;
}
/* 到这个阶段就是没有content-handler能处理请求,此时若uri以/结尾就是不样列出,否则就是not found */
if (r->uri.data[r->uri.len - 1] == '/') {
if (ngx_http_map_uri_to_path(r, &path, &root, 0) != NULL) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "directory index of \"%s\" is forbidden", path.data);
}
ngx_http_finalize_request(r, NGX_HTTP_FORBIDDEN);
return NGX_OK;
}
ngx_http_finalize_request(r, NGX_HTTP_NOT_FOUND);
return NGX_OK;
}
输出过滤器
不像httpd有辣么多输入输出过滤器,nginx只有输出过滤器,在content-handler调用ngx_http_send_header
/ngx_http_writer
等函数时,数据不是直接输出而是先经过过滤器。nginx的输出过滤器分为响应头过滤器和响应体过滤器,它的注册方式很简陋,nginx有个两个全局变量,各过滤器模块通过先备份该值,再修改该值为自身处理函数,再在处理函数中调用备份的值,从而实现链式调用,很明显这是个后进先出的结构,所以其注册顺序十分重要,在继续之前先说下模块的初始化顺序,在使用configure
生成Makefile时会生成一个ngx_modules.c
文件,这里面会有个ngx_module_t *ngx_modules[]
结构记录了要编译的所有模块的ngx_module_t
结构,上面初始化也是用的这个结构!
吼,回到过滤器,本身没啥好说的,放个例子就很清楚了:
static ngx_int_t ngx_http_not_modified_filter_init(ngx_conf_t *cf)
{
ngx_http_next_header_filter = ngx_http_top_header_filter; // 这里先备份全局变量,其实这个全局变量里存放的是上一个过滤器模块的值
ngx_http_top_header_filter = ngx_http_not_modified_header_filter; // 修改成当前的值
// 当然这里也可以同时注册响应体过滤器...
return NGX_OK;
}
static ngx_int_t
ngx_http_not_modified_header_filter(ngx_http_request_t *r)
{
/* 该过滤器就是先判断内容是否是未修改状态,是的话就修改响应头,并且丢弃响应体 */
if (r->headers_out.status != NGX_HTTP_OK
|| r != r->main
|| r->disable_not_modified)
{
return ngx_http_next_header_filter(r);
}
/* 做完过滤逻辑后,会调用本地备份的ngx_http_next_header_filter,它就是上一个过滤模块注册的回调,就这样链式调用 */
if (r->headers_in.if_unmodified_since
&& !ngx_http_test_if_unmodified(r))
{
return ngx_http_filter_finalize_request(r, NULL,
NGX_HTTP_PRECONDITION_FAILED);
}
...
return ngx_http_next_header_filter(r);
}
应用级基础知识
location
在路由时有这几种匹配方式,还有种以@
开头的用于内部重定向:
location [=|~|~*|^~] /uri/ {
...
}
优先级如下,两个正则优先级相同,在同一优先级下按出现先后顺序匹配:
=:精确匹配
^~:精确前缀匹配
~:区分大小写的正则匹配
~*:不区分大小写的正则匹配
/uri:普通前缀匹配
/:通用匹配,匹配所有
rewrite
rewrite包含多条指令,其中最常见的就是rewrite
和return
指令,执行时它按server::rewrite -> location匹配 -> location::rewrite -> location匹配..
顺序一直循环,直到遇到停止指令或没有rewrite集的块,如果循环超过10则500错误[1]。rewrite指令集从上到下顺序执行,但是在遇到return指令或rewrite带标志时停止执行后续的rewrite集指令,rewrite指令
格式如下:
rewrite regex replacement-url [flag];
其中rewrite
的输入是$uri
,replacement-url
表示替换表达式,结果是被赋值给$uri
,而flag有如下四种:
1.last:本次rewrite集指令执行完成,不再执行后面的rewrite集指令,而是开始下一轮重写与匹配
2.break:它表示不再执行后续的rewrite集指令与location匹配直接处理
3.redirect:302重定向
4.permanent:301重定向
一些指令:
1.add_header
: 仅用于添加响应头
2.proxy_xxx
: 在代理时使用的指令,如proxy_set_header
可定义代理转发时的请求头,它是直接覆盖的,因此客户端指定的头将会被丢弃。
内置变量
易混淆内置变量,$request_uri
是请求的原始uri(带参数),$uri
是被处理(index/重写/内部重定向)后的正规化的最终uri(不带参数)
漏洞相关
历史漏洞
1.和PHP配合易出问题:php配置了cgi.fix_pathinfo
会使当前路径不存在则向上查找,可使用evil.jpg/.php
访问evil.jpg
并执行。
注:PHP在5.3.9之后引入
security.limit_extensions
选项,使FastCGI默认只执行.php后缀的文件
2.本身妹出过什么好用的漏洞,可以看看它的security advisories...
解析特性
1.content-length
:值有空格则不处理
2.不支持hop-by-hop
3.transfer-encoding
只支持精确的[chunked,identity]
,其他会报错
常见配置问题
1.location
与alias
(还有proxy_pass
等指令,可多测试)匹配前少/
后多可目录穿一层[0]
2.存在merge_slashes off
指令时,///
将不会合并,因此///../x
这种url在nginx这是合法的,而后端没正确处理它就存在漏洞,简单来说就是默认nginx可以防这种目录遍历攻击,但是开启后就不防了...
3.nginx指令是由内到外执行,内部会屏蔽外部,因此有些安全指令在外部设置了再在内部设置则外部的不会生效,如add_header
指令加CSP保护可能会失效
4.显式指定underscores_in_headers on
导致前后端解析不一致
5.配置里使用$uri
(/$document_uri
为别名)时或使用变量且值来自解码后数据时,由于是url解码后的值因此可能造成CRLF注入
6.nginx提供x-accel
来实现静态资源加速与动态处理,即先将请求转给动态后端,处理后返回x-accel-redirect
等头告知nginx内部重定向取资源,因此若能控制后端返回头(如利用CRLF)则可利用该头访问internal的资源
7.另外proxy_pass
支持转发到unix socket,格式为http[s]://unix:/tmp/x.sock:/uri/
,因此若能控代理的域名可能可以打部分本地unix socket服务。
8.if
指令里应只使用rewrite模块里的指令,否则容易出现问题。
附:
proxy_pass
里请求若无?
则自动追加$query_string
,若无uri则自动追加$request_uri
(即光秃秃的就不解码发送),有uri则解码发送(即使只是一个单独的/
),当出现变量时需看它来自哪,如没指定的$1
来着$request_uri
去前缀此时不解码,如果来自正则则要解码uri和url通用语意不同,前者包含后者,不过在webserver中uri又有另外的含义,主要指url中路径(与参数)部分
参考
[0] Common Nginx misconfigurations that leave your web server open to attack
[1] 搞懂nginx的rewrite模块 -- youyou岁月 (2017)
[2] github gixy -- yondex (2020)
[3] Nginx开发从入门到精通 -- tengine团队 (2013)
[4] Nginx底层设计与源码分析 -- 聂松松;赵禹;施洪宝 等 (2021)