Nginx架构分析

Published: 2022年11月16日

In Vuln.

背景

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打开项目,配置调试:

img

之后再编辑配置文件(默认根据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来实现灵活与性能并存),其架构如下图:

img

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

img

如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_indexctx它们都是和模块类型相关的,每种核心模块都是一种类型,其他模块基本都属于它们中的一种,如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_tngx_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

图片如下啦:

img

到此为止,已经完成请求头的解析,该正式进入请求处理阶段了!

阶段

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包含多条指令,其中最常见的就是rewritereturn指令,执行时它按server::rewrite -> location匹配 -> location::rewrite -> location匹配..顺序一直循环,直到遇到停止指令或没有rewrite集的块,如果循环超过10则500错误[1]。rewrite指令集从上到下顺序执行,但是在遇到return指令或rewrite带标志时停止执行后续的rewrite集指令,rewrite指令格式如下:

rewrite regex replacement-url [flag];

其中rewrite的输入是$urireplacement-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.locationalias(还有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)