标签归档:nginx

Nginx模块ngx_http_mirror_module

最近nginx官网公布了nginx1.13.4最新的ngx_http_mirror_module模块,利用mirror模块,业务可以将线上实时访问流量拷贝至其他环境,该流量可用于压测或者旁路做一些检测。

mirror模块配置分为两部分,源地址和镜像地址配置,配置位置可以为nginx配置文件的http, server, location上下文,配置示例为:

# original配置
location / {
    mirror /mirror;
    mirror_request_body off;
    proxy_pass http://127.0.0.1:9502;
}

# mirror配置
location /mirror {
    internal;
    proxy_pass http://127.0.0.1:8081$request_uri;
    proxy_set_header X-Original-URI $request_uri;
}

 

1)original配置
location /指定了源uri为/
mirror /mirror指定镜像uri为/mirror
mirror_request_body off | on 指定是否镜像请求body部分,此选项与proxy_request_buffering、fastcgi_request_buffering、scgi_request_buffering和 uwsgi_request_buffering冲突,一旦开启mirror_request_body为on,则请求自动缓存;
proxy_pass 指定上游server的地址

2)mirror配置
internal 指定此location只能被“内部的”请求调用,外部的调用请求会返回”Not found” (404)
proxy_pass 指定上游server的地址
proxy_set_header 设置镜像流量的头部

整个请求流程为:

1)curl向nginx 80端口发起GET / HTTP请求
2)nginx将请求转发至upstream 9502端口的original PHP脚本,nginx本地端口为51637
3)nginx将请求镜像发至upstream 8081端口的mirror PHP脚本,nginx本地端口为51638
4)original发送响应response to client至nginx
5)nginx将响应转发至curl,curl将响应展示到终端
6)mirror将响应发送至nginx,nginx丢弃。

参考文章:
https://segmentfault.com/p/1210000010610500/read

 

【Nginx】配置安全问题

1、CRLF
需要注意的地方:
a)rewrite, return, add_header, proxy_set_header or proxy_pass中
b)使用了$uri和$document_uri,因为这两个参数会进行URL解码,正确配置应该是$request_uri。

c)变量,例如(?P<myvar>[^.]+).

这里先测试一下$uri
添加一条配置

location /sectest {
  return 302 https://$host$uri;
}

结果如下:

 

修改配置为

location /sectest {
  return 302 https://$host$request_uri;
}

结果如下:

测试一下匹配变量导致的CRLF
添加

    location ~ /v1/((?<action>[^.]*)\.json)?$ {
        add_header X-Action $action;
        return 200 "OK";
    }

结果如下:

应该修改正则为

    location ~ /v1/((?<action>[^.\s]*)\.json)?$ {
        add_header X-Action $action;
        return 200 "OK";
    }

2、HTTP头覆盖
如果location有add_header,那么以location为准。如果location没有add_header,则继承Http和server块的add_header内容。
官方配置例子如下:

server {
  listen 80;
  add_header X-Frame-Options "DENY" always;
  location / {
      return 200 "index";
  }

  location /new-headers {
    # Add special cache control
    add_header Cache-Control "no-cache, no-store, max-age=0, must-revalidate" always;
    add_header Pragma "no-cache" always;

    return 200 "new-headers";
  }
}

如果访问/,响应头中有X-Frame-Options

GET / HTTP/1.0

HTTP/1.1 200 OK
Server: nginx/1.10.2
Date: Mon, 09 Jan 2017 19:28:33 GMT
Content-Type: application/octet-stream
Content-Length: 5
Connection: close
X-Frame-Options: DENY

index

如果访问/new-headers,响应头中没有X-Frame-Options

GET /new-headers HTTP/1.0


HTTP/1.1 200 OK
Server: nginx/1.10.2
Date: Mon, 09 Jan 2017 19:29:46 GMT
Content-Type: application/octet-stream
Content-Length: 11
Connection: close
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache

new-headers

 

3、alias导致的任意文件读取
错误配置示例如下:

location /files {
  alias /home/;
}

这里如果访问http://example.com/files/readme.txt,就可以获取/home/readme.txt文件。
如果访问http://example.com/files../etc/passwd就可以读取/etc/passwd

需要注意,这里只能添加一个../,也就是跳到上层的目录,这里我修改nginx的配置如下:

        location /files {
                alias /home/elk/;
        }

修复建议:
location和alias的最后必须都带/或者都不带/

Gixy介绍
开源程序https://github.com/yandex/gixy用来检测Nginx配置中存在的问题
安装使用:

pip install gixy
gixy /etc/nginx/nginx.conf

检查项如下:

[ssrf] Server Side Request Forgery
[http_splitting] HTTP Splitting
[origins] Problems with referrer/origin validation
[add_header_redefinition] Redefining of response headers by "add_header" directive
[host_spoofing] Request's Host header forgery
[valid_referers] none in valid_referers
[add_header_multiline] Multiline response headers

参考文章:
https://www.leavesongs.com/PENETRATION/nginx-insecure-configuration.html
https://mp.weixin.qq.com/s?__biz=MzIzOTQ5NjUzOQ==&mid=2247483699&idx=1&sn=6f0394df7be9aafd65c12002c2bb4f10&chksm=e9287d07de5ff41165757618d932021e1b8e036fd0c1b8305e38ad693097cf05e37b76928eb5&mpshare=1&scene=23&srcid=0714xbWwfcwuCe7XA9oIQryo#rd

【Lua教程】ngx_lua模块中正则表达式相关api

ngx.re.match

语法:captures, err = ngx.re.match(subject, regex, options?, ctx?, res_table?)

只有第一次匹配的结果被返回,如果没有匹配,则返回nil;或者匹配过程中出现错误时,也会返回nil,此时错误信息会被保存在err中。

当匹配的字符串找到时,一个Lua table captures会被返回,captures[0]中保存的就是匹配到的字串,captures[1]保存的是用括号括起来的第一个子模式的结果,captures[2]保存的是第二个子模式的结果,依次类似。

    local m, err = ngx.re.match("hello, 1234", "[0-9]+")
    if m then
        -- m[0] == "1234"
    else
        if err then
            ngx.log(ngx.ERR, "error: ", err)
            return
        end
 
        ngx.say("match not found")
    end

上面例子中,匹配的字符串是1234,因此m[0] == “1234”,但是没有用括号括起来的子模式,因此,m[1],m[2]等均为nil。

    local m, err = ngx.re.match("hello, 1234", "([0-9])[0-9]+")
    -- m[0] == "1234"
    -- m[1] == "1"

命名方式的捕获,从v0.7.14版本后开始支持,如下所示:

例1:
    local m, err = ngx.re.match("hello, 1234", "([0-9])(?<remaining>[0-9]+)")
    -- m[0] == "1234"
    -- m[1] == "1"
    -- m[2] == "234"
    -- m["remaining"] == "234"
例2:
    local m, err = ngx.re.match("hello, world", "(world)|(hello)|(?<named>howdy)")
    -- m[0] == "hello"
    -- m[1] == nil
    -- m[2] == "hello"
    -- m[3] == nil
    -- m["named"] == nil

例2中,为什么m[1]等于nil?因为在给定的模式串中,最先匹配的是hello,因此,其他子模式在找到的匹配串中查找不到对应的匹配串,因此,除了hello子模式外,其他的子模式的匹配结果都是nil。一定要记住,是谁最先被匹配的

options选项可以是下面的取值的组合:

 a 锚定模式,只从头开始匹配. 
 d DFA模式,或者称最长字符串匹配语义,需要PCRE 6.0+支持.
 D 允许重复的命名的子模式,该选项需要PCRE 8.12+支持,例如
 local m = ngx.re.match("hello, world",
 "(?<named>\w+), (?<named>\w+)",
 "D")
 -- m["named"] == {"hello", "world"}
 i 大小写不敏感模式.
 j 启用PCRE JIT编译, 需要PCRE 8.21+ 支持,并且必须在编译时加上选项--enable-jit,
 为了达到最佳性能,该选项总是应该和'o'选项搭配使用. 
 J 启用PCRE Javascript的兼容模式,需要PCRE 8.12+ 支持. 
 m 多行模式.
 o 一次编译模式,启用worker-process级别的编译正则表达式的缓存.
 s 单行模式.
 u UTF-8模式. 该选项需要在编译PCRE库时加上--enable-utf8 选项.
 U 与"u" 选项类似,但是该项选禁止PCRE对subject字符串UTF-8有效性的检查.
 x 扩展模式

两个例子:

    local m, err = ngx.re.match("hello, world", "HEL LO", "ix")
    -- m[0] == "hello"
    local m, err = ngx.re.match("hello, 美好生活", "HELLO, (.{2})", "iu")
    -- m[0] == "hello, 美好"
    -- m[1] == "美好"

第四个可选参数ctx可以传入一个Lua Table,传入的Lua Table可以是一个空表,也可以是包含pos字段的Lua Table。如果传入的是一个空的Lua Table,那么,ngx.re.match将会从subject字符串的起始位置开始匹配查找,查找到匹配串后,修改pos的值为匹配字符串的下一个位置的值,并将pos的值保存到ctx中,如果匹配失败,那么pos的值保持不变;如果传入的是一个非空的Lua Table,即指定了pos的初值,那么ngx.re.match将会从指定的pos的位置开始进行匹配,如果匹配成功了,修改pos的值为匹配字符串的下一个位置的值,并将pos的值保存到ctx中,如果匹配失败,那么pos的值保持不变。

    local ctx = {}
    local m, err = ngx.re.match("1234, hello", "[0-9]+", "", ctx)
         -- m[0] = "1234"
         -- ctx.pos == 5
    local ctx = { pos = 2 }
    local m, err = ngx.re.match("1234, hello", "[0-9]+", "", ctx)
         -- m[0] = "34"
         -- ctx.pos == 5

注意:

如果需要传入ctx参数,但并不需要第三个可选参数options时,第三个参数也不能简单去掉,这时需要传入一个空的字符串作为第三个参数的值。

第四个可选参数还不是很熟悉,暂且留空。

 

ngx.re.find

语法:from, to, err = ngx.re.find(subject, regex, options?, ctx?, nth?)

该方法与ngx.re.match方法基本类似,不同的地方在于ngx.re.find返回的是匹配的字串的起始位置索引和结束位置索引,如果没有匹配成功,那么将会返回两个nil,如果匹配出错,还会返回错误信息到err中。

例子:

    local s = "hello, 1234"
    local from, to, err = ngx.re.find(s, "([0-9]+)", "jo")
    if from then
        ngx.say("from: ", from)
        ngx.say("to: ", to)
        ngx.say("matched: ", string.sub(s, from, to))
    else
        if err then
            ngx.say("error: ", err)
            return
        end
        ngx.say("not matched!")
    end

输出结果:

from: 8

to: 11

matched: 1234

该方法相比ngx.re.match,不会创建新的Lua字符串,也不会创建新的Lua Table,因此,该方法比ngx.re.match更加高效,因此,在可以使用ngx.re.find的地方应该尽量使用。

第五个参数可以指定返回第几个子模式串的起始位置和结束位置的索引值,默认值是0,此时将会返回匹配的整个字串;如果nth等于1,那么将返回第一个子模式串的始末位置的索引值;如果nth等于2,那么将返回第二个子模式串的始末位置的索引值,依次类推。如果nth指定的子模式没有匹配成功,那么将会返回两个nil。

    local str = "hello, 1234"
    local from, to = ngx.re.find(str, "([0-9])([0-9]+)", "jo", nil, 2)
    if from then
        ngx.say("matched 2nd submatch: ", string.sub(str, from, to))  -- yields "234"
    end

 

ngx.re.gmatch

语法:iterator, err = ngx.re.gmatch(subject, regex, options?)

与ngx.re.match相似,区别在于该方法返回的是一个Lua的迭代器,这样就可以通过迭代器遍历所有匹配的结果。

如果匹配失败,将会返回nil,如果匹配出现错误,那么还会返回错误信息到err中。

    local iterator, err = ngx.re.gmatch("hello, world!", "([a-z]+)", "i")
    if not iterator then
        ngx.log(ngx.ERR, "error: ", err)
        return
    end
 
    local m
    m, err = iterator()    -- m[0] == m[1] == "hello"
    if err then
        ngx.log(ngx.ERR, "error: ", err)
        return
    end
 
    m, err = iterator()    -- m[0] == m[1] == "world"
    if err then
        ngx.log(ngx.ERR, "error: ", err)
        return
    end
 
    m, err = iterator()    -- m == nil
    if err then
        ngx.log(ngx.ERR, "error: ", err)
        return
    end

更多情时候,只需要把迭代过程放入一个while循环中即可:

    local it, err = ngx.re.gmatch("hello, world!", "([a-z]+)", "i")
    if not it then
        ngx.log(ngx.ERR, "error: ", err)
        return
    end
 
    while true do
        local m, err = it()
        if err then
            ngx.log(ngx.ERR, "error: ", err)
            return
        end
 
        if not m then
            -- no match found (any more)
            break
        end
 
        -- found a match
        ngx.say(m[0])
        ngx.say(m[1])
    end

options选项的使用与ngx.re.match中的options选项的用法是一样的。

注意点:

ngx.re.gmatch返回的迭代器只能在一个请求所在的环境中使用,就是说,我们不能把返回的迭代器赋值给持久存在的命名空间(比如一个Lua Packet)中的某一个变量。

 

ngx.re.sub

语法:newstr, n, err = ngx.re.sub(subject, regex, replace, options?)

该方法主要实现匹配字符串的替换,会用replace替换匹配的字串,replace可以是纯字符串,也可以是使用$0, $1等子模式串的形式,ngx.re.sub返回进行替换后的完整的字符串,同时返回替换的总个数;options选项,与ngx.re.match中的options选项是一样的。

    local newstr, n, err = ngx.re.sub("hello, 1234", "([0-9])[0-9]", "[$0][$1]")
    if newstr then
        -- newstr == "hello, [12][1]34"
        -- n == 1
    else
        ngx.log(ngx.ERR, "error: ", err)
        return
    end

在上面例子中,$0表示整个匹配的子串,$1表示第一个子模式匹配的字串,以此类推。

可以用大括号{}将相应的0,1,2…括起来,以区分一般的数字:

    local newstr, n, err = ngx.re.sub("hello, 1234", "[0-9]", "${0}00")
        -- newstr == "hello, 100234"
        -- n == 1

如果想在replace字符串中显示$符号,可以用$进行转义(不要用反斜杠\$对美元符号进行转义,这种方法不会得到期望的结果):

    local newstr, n, err = ngx.re.sub("hello, 1234", "[0-9]", "$$")
        -- newstr == "hello, $234"
        -- n == 1

如果replace是一个函数,那么函数的参数是一个”match table”, 而这个”match table”与ngx.re.match中的返回值captures是一样的,replace这个函数根据”match table”产生用于替换的字符串。

    local func = function (m)
        return "[" .. m[0] .. "][" .. m[1] .. "]"
    end
    local newstr, n, err = ngx.re.sub("hello, 1234", "( [0-9] ) [0-9]", func, "x")
        -- newstr == "hello, [12][1]34"
        -- n == 1

注意:

通过函数形式返回的替换字符串中的美元符号$不再是特殊字符,而只是被看作一个普通字符。

 

ngx.re.gsub

语法:newstr, n, err = ngx.re.gsub(subject, regex, replace, options?)

该方法与ngx.re.sub是类似的,但是该方法进行的是全局替换。

看两个例子:

    local newstr, n, err = ngx.re.gsub("hello, world", "([a-z])[a-z]+", "[$0,$1]", "i")
    if newstr then
        -- newstr == "[hello,h], [world,w]"
        -- n == 2
    else
        ngx.log(ngx.ERR, "error: ", err)
        return
    end
    local func = function (m)
        return "[" .. m[0] .. "," .. m[1] .. "]"
    end
    local newstr, n, err = ngx.re.gsub("hello, world", "([a-z])[a-z]+", func, "i")
        -- newstr == "[hello,h], [world,w]"
        -- n == 2

【Lua教程】模块与包

模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。
Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。以下为创建自定义模块 module.lua,文件代码格式如下:

-- 文件名为 module.lua
-- 定义一个名为 module 的模块
module = {}
-- 定义一个常量
module.constant = "这是一个常量"
-- 定义一个函数
function module.func1()
    io.write("这是一个公有函数!\n")
end
local function func2()
    print("这是一个私有函数!")
end
function module.func3()
    func2()
end
return module

由上可知,模块的结构就是一个 table 的结构,因此可以像操作调用 table 里的元素那样来操作调用模块里的常量或函数。
上面的 func2 声明为程序块的局部变量,即表示一个私有函数,因此是不能从外部访问模块里的这个私有函数,必须通过模块里的公有函数来调用.
Lua提供了一个名为require的函数用来加载模块。要加载一个模块,只需要简单地调用就可以了。例如:
require(“<模块名>”)
或者
require “<模块名>”
执行 require 后会返回一个由模块常量或函数组成的 table,并且还会定义一个包含该 table 的全局变量。

-- test_module.lua 文件
-- module 模块为上文提到到 module.lua
require("module")
print(module.constant)
module.func3()

以上代码执行结果为:
这是一个常量
这是一个私有函数!
或者给加载的模块定义一个别名变量,方便调用:

-- test_module2.lua 文件
-- module 模块为上文提到到 module.lua
-- 别名变量 m
local m = require("module")
print(m.constant)
m.func3()

以上代码执行结果为:
这是一个常量
这是一个私有函数!

【Lua教程】ngx_lua模块中共享内存字典项API

在NGX LUA WAF中使用到了共享内存来做两件事:
1)CC防御,单位时间内单IP针对单URL访问频次超过阈值时封禁。
2)Web攻击防御,单位时间内触发WAF规则超过阈值时封禁。
在ngx_lua模块中使用共享内存字典项相关API的前提条件是已经使用lua_shared_dict命令定义了一个字典项对象,该命令的具体用法为:
语法:lua_shared_dict <name> <size>
该命令主要是定义一块名为name的共享内存空间,内存大小为size。通过该命令定义的共享内存对象对于Nginx中所有worker进程都是可见的,当Nginx通过reload命令重启时,共享内存字典项会从新获取它的内容,当时当Nginx退出时,字典项的值将会丢失。

http {
    lua_shared_dict dogs 10m;
    server {
        location /set {
            content_by_lua '
                local dogs = ngx.shared.dogs
                dogs:set("Jim", 8)
                ngx.say("STORED")
            ';
        }
        location /get {
            content_by_lua '
                local dogs = ngx.shared.dogs
                ngx.say(dogs:get("Jim"))
            ';
        }
    }
}

输出结果是:
$ curl localhost/set
STORED
$ curl localhost/get
8

可以通过ngx.shared.DICT接口获取共享内存字典项对象:

语法:dict = ngx.shared.DICT
dict = ngx.shared[name_var]

其中,DICT和name_var表示的名称是一致的,比如上面例子中,dogs =ngx.shared.dogs 就是dict = ngx.shared.DICT的表达形式,也可以通过下面的方式达到同样的目的:
dogs = ngx.shared[“dogs”]

通过上面的API获取得到的共享内存字典项对象,具有如下相应的接口:

> ngx.shared.DICT.get
语法:value, flags = ngx.shared.DICT:get(key)
获取共享内存上key对应的值。如果key不存在,或者key已经过期,将会返回nil;如果出现错误,那么将会返回nil以及错误信息。
local cats = ngx.shared.cats
local value, flags = cats:get(“Marry”)
返回列表中的flags,是在ngx.shared.DICT.set方法中设置的值,默认值为0. 如果设置的flags为0,那么在这里flags的值将不会被返回。

> ngx.shared.DICT.get_stale
语法:value, flags, stale = ngx.shared.DICT:get_stale(key)
与get方法类似,区别在于该方法对于过期的key也会返回,第三个返回参数表明返回的key的值是否已经过期,true表示过期,false表示没有过期。

> ngx.shared.DICT.set
语法:success, err, forcible = ngx.shared.DICT:set(key, value, exptime?, flags?)
“无条件”地往共享内存上插入key-value对,这里讲的“无条件”指的是不管待插入的共享内存上是否已经存在相同的key。三个返回值的含义:
success:成功插入为true,插入失败为false
err:操作失败时的错误信息,可能类似”no memory”
forcible:true表明需要通过强制删除(LRU算法)共享内存上其他字典项来实现插入,false表明没有删除共享内存上的字典项来实现插入。
第三个参数exptime表明key的有效期时间,单位是秒(s),默认值为0,表明永远不会过期;flags参数是一个用户标志值,会在调用get方法时同时获取得到。
local cats = ngx.shared.cats
local succ, err, forcible = cats:set(“Marry”, “it is a nice cat!”)

> ngx.shared.DICT.safe_set
语法:ok, err = ngx.shared.DICT:safe_set(key, value, exptime?, flags?)
与set方法类似,区别在于不会在共享内存用完的情况下,通过强制删除(LRU算法)的方法实现插入。如果内存不足,会直接返回nil和err信息”no memory”
注意:
set和safe_set共同点是:如果待插入的key已经存在,那么key对应的原来的值会被新的value覆盖!

> ngx.shared.DICT.add
语法:success, err, forcible = ngx.shared.DICT:add(key, value, exptime?, flags?)
与set方法类似,与set方法区别在于不会插入重复的键(可以简单认为add方法是set方法的一个子方法),如果待插入的key已经存在,将会返回nil和和err=”exists”

> ngx.shared.DICT.safe_add
语法:ok, err = ngx.shared.DICT:safe_add(key, value, exptime?, flags?)
与safe_set方法类似,区别在于不会插入重复的键(可以简单认为safe_add方法是safe_set方法的一个子方法),如果待插入的key已经存在,将会返回nil和和err=”exists”

> ngx.shared.DICT.replace
语法:success, err, forcible = ngx.shared.DICT:replace(key, value, exptime?, flags?)
与set方法类似,区别在于只对已经存在的key进行操作(可以简单认为replace方法是set方法的一个子方法),如果待插入的key在字典上不存在,将会返回nil和错误信息”not found”

> ngx.shared.DICT.delete
语法:ngx.shared.DICT:delete(key)
无条件删除指定的key-value对,其等价于
ngx.shared.DICT:set(key, nil)

> ngx.shared.DICT.incr
语法:newval, err = ngx.shared.DICT:incr(key, value)
对key对应的值进行增量操作,增量值是value,其中value的值可以是一个正数,0,也可以是一个负数。value必须是一个Lua类型中的number类型,否则将会返回nil和”not a number”;key必须是一个已经存在于共享内存中的key,否则将会返回nil和”not found”.

> ngx.shared.DICT.flush_all
语法:ngx.shared.DICT:flush_all()
清除字典上的所有字段,但不会真正释放掉字段所占用的内存,而仅仅是将每个字段标志为过期。

> ngx.shared.DICT.flush_expired
语法:flushed = ngx.shared.DICT:flush_expired(max_count?)
清除字典上过期的字段,max_count表明上限值,如果为0或者没有给出,表明需要清除所有过期的字段,返回值flushed是实际删除掉的过期字段的数目。
注意:
与flush_all方法的区别在于,该方法将会释放掉过期字段所占用的内存。

> ngx.shared.DICT.get_keys
语法:keys = ngx.shared.DICT:get_keys(max_count?)
从字典上获取字段列表,个数为max_count,如果为0或没有给出,表明不限定个数。默认值是1024个
注意:
强烈建议在调用该方法时,指定一个max_count参数,因为在keys数量很大的情况下,如果不指定max_count的值,可能会导致字典被锁定,从而阻塞试图访问字典的worker进程。

Nginx防盗链配置

0x01 accesskey

nginx-accesskey可以根据不同的请求IP生成不同的key,拒绝无效的请求。安装需重新编译Nginx增加nginx-accesskey模块。

./configure –prefix=/usr/local/nginx –with-http_stub_status_module –with-http_ssl_module –add-module=/root/nginx-1.8.1/nginx-accesskey-2.0.3

首先添加如下配置:

accesskey on;

accesskey_hashmethod md5;

accesskey_arg “key”;

accesskey_signature  “test”;

参数解释如下:

accesskey

语句: accesskey [on|off]

默认: accesskey off

可以用在: main, server, location

开启 access-key 功能。

accesskey_arg

语句: accesskey_arg “字符”

默认: accesskey “key”

可以用在: main, server, location

URL中包含 access key 的GET参数。

accesskey_hashmethod

语句: accesskey_hashmethod [md5|sha1]

默认: accesskey_hashmethod md5(默认用 md5 加密)

可以用在: main, server, location

用 MD5 还是 SHA1 加密 access key。

accesskey_signature

语句: accesskey_signature “字符”

默认: accesskey_signature “$remote_addr”

可用在: main, server, location

这个值用来生成 access key。

然后尝试访问http://192.168.192.120返回了403,原因是没有key参数。test的Md5对应为098f6bcd4621d373cade4e832627b4f6。

访问http://192.168.192.120/?key=098f6bcd4621d373cade4e832627b4f6返回200。

这样还是无法防盗链,这里可以结合客户端IP生成链接。

修改accesskey_signature   “mypass$remote_addr”;

服务端获取到客户端的IP后,组合mypass使用Md5后得到链接,别的站点即便复制了这个链接,其他人点击了也是会返回403。

生成链接的PHP代码如下:

<?php

$ipkey= md5("test".$_SERVER['REMOTE_ADDR']);

$output_add_key="<a href=http://192.168.192.120/download/a.jpg?key=".$ipkey.">

download_add_key</a>";

echo $output_add_key;

?>

 

0x02 secure_link

重新编译Nginx,添加参数–with-http_secure_link_module

secure_link $arg_st,$arg_e;

secure_link_md5 test$arg_e;

if ($secure_link = ""){

  return 402;

}

if ($secure_link = "0"){

  return 405;

}

#如果哈希不正确,$secure_link将为一个空字符串,状态码402。

#如果当前的时间晚于指定的过期时间,会返回0,状态码为405。

#一切正确的话$secure_link为1。

 

secure_link

语法:secure_link $md5_hash[,$expire_time]

默认值:none

使用字段:location

这个指令指定这个链接URL的MD5哈希值和过期时间,$md5_hash为基于URL的64位编码,$expired_time为自1970-01-01 00:00:00 UTC时间以来的秒数,如果你不增加$expire_time,那么这个URL永远不会过期。

secure_link_md5

语法:secure_link_md5 $the_uri_you_want_to_hashed_by_md5

默认值:none

使用字段:location

这个指令指定你需要通过MD5哈希的字符串,字符串可以包含变量,哈希值将和”secure_link”设置的$md5_hash变量进行比较,如果结果相同,$secure_link变量值为1,否则为空字符串。

生成链接的PHP代码如下:

<?php

$secret = 'test'; # 密钥

 $expire = time()+300; #5分钟后链接失效

 $md5 = base64_encode(md5($secret.$expire, true));

 $md5 = strtr($md5, '+/', '-_');

 $md5 = str_replace('=', '', $md5);

 echo '<a href=http://192.168.192.120/download/a.jpg?st='.$md5.'&e='.$expire.'>nginx-1.4.2</a>';

 ?>

访问生成的下载连接如下:http://192.168.192.120/download/a.jpg?st=wUht9FkhnfNI-KEGy81HFA&e=1487733535

如果修改st,会返回402。

如果超过5分钟后访问,会返回405。

 

0x03 ngx_http_referer_module

nginx模块ngx_http_referer_module通常用于阻挡来源非法的域名请求。需要注意的是有些合法的请求是不会带referer来源头部的,所以有时候不要拒绝来源头部(referer)为空的请求。

Nginx配置如下:

location ~* \.(gif|jpg|png|bmp)$ {

    valid_referers none blocked *.ttlsa.com server_names;

    if ($invalid_referer) {

        return 403;

    }

}

valid_referers [none|blocked|server_names] …

默认值:none

使用环境:server,location

该指令会根据Referer Header头的内容分配一个值为0或1给变量$invalid_referer。

如果Referer Header头不符合valid_referers指令设置的有效Referer,变量$invalid_referer

将被设置为1.

该指令的参数可以为下面的内容:

none

表示无Referer值的情况。

blocked

“Referer”来源头部不为空,但是里面的值被代理或者防火墙删除了,这些值都不以http://或者https://开头.

server_names

“Referer”来源头部包含当前的server_names(当前域名)

 

Nginx 配置汇总

location匹配规则及优先级

1. = 严格匹配这个查询。如果找到,停止搜索。
2. ^~ 匹配路径的前缀,如果找到,停止搜索。
3. ~ 为区分大小写的正则匹配
4. ~* 为不区分大小写匹配

优先级: =, ^~, ~/~*, 无

Nginx禁止未绑定域名访问
nginx通过host配置确认转发到那台服务器处理。如果未匹配上就会转发到default_server节点来处理。例如配置如下:

server {
listen 80 default_server;
server_name nginx.net;
}

对于所有请求的HOST未匹配上的都会转发到该server处理。
通过如下配置,所有未匹配到server_name的请求都会返回403

server{
listen 80 default;
server_name _ ;
return 403;
}

其中_是无效域名的代表,同样可以写成其他例如”-” “@”等等。

Nginx rewrite配置

1)last
重新将rewrite后的地址在server标签中执行
2)break
将rewrite后的地址在当前location标签中执行
3)redirect
返回302临时重定向,浏览器地址会显示跳转后的URL地址。
4)permanent
返回301永久重定向,浏览器地址会显示跳转后的URL地址。

使用last和break,浏览器中的地址不会改变,其中需要注意的是last和break的区别:
使用alias指令必须用last标记;使用proxy_pass指令时,需要使用break标记。Last标记在本条rewrite规则执行完毕后,会对其所在server{……}标签重新发起请求,而break标记则在本条规则匹配完成后,终止匹配。

将url中以/wap/开头的请求转发到后台对应的某台server上,可以再Nginx里设置一个变量,来临时保存/wap/后面的路径信息

location ^~ /wap/
{
if ($request_uri ~ /wap/(\d+)/(.+))
{
set $bucketid $1;
set $params $2;
}
proxy_pass http://mx$bucketid.test.com:6601/$params;
}

也可以首先rewrite一下,然后再代理:

location ^~ /wap/{
rewrite /wap/(\d+)/(.+) /$2?$args break;
proxy_pass http://mx$1.test.com:6601;
}

或者

location ~* /wap/(\d+)/(.+)
{
proxy_pass http://mx$1.test.com:6601/$2?$args;
}

注意上面最后的?$args,表明把原始url最后的get参数也给代理到后台
如果在proxy_pass中使用了变量(不管是主机名变量$1或后面的$2变量),则必须得加这段代码
但如果proxy_pass没用任何变量,则不需要加,它默认会把所有的url都给代理到后台,如:

location ~* /wap/(\d+)/(.+)
{
proxy_pass http://mx.test.com:6601;
}

另外还需要注意url的/问题
下面四种情况分别用http://192.168.1.4/proxy/test.html 进行访问。
第一种:

location /proxy/ {
proxy_pass http://127.0.0.1:81/;
}

会被代理到http://127.0.0.1:81/test.html 这个url
第二种(相对于第一种,最后少一个 /)

location /proxy/ {
proxy_pass http://127.0.0.1:81;
}

会被代理到http://127.0.0.1:81/proxy/test.html 这个url
第三种:

location /proxy/ {
proxy_pass http://127.0.0.1:81/ftlynx/;
}

会被代理到http://127.0.0.1:81/ftlynx/test.html 这个url。
第四种情况(相对于第三种,最后少一个 / ):

location /proxy/ {
proxy_pass http://127.0.0.1:81/ftlynx;
}

会被代理到http://127.0.0.1:81/ftlynxtest.html 这个url
也就是说如果proxy_pass只是后端服务器的IP,最后没有/的话,就会将全uri带过去。
而如果proxy_pass带了/的话,只是带最后访问的文件。

root与alias区别

在于nginx如何解释location后面的uri,这会使两者分别以不同的方式将请求映射到服务器文件上。
1)root

location ~ ^/weblogs/ {
root /data/weblogs/www.ttlsa.com;
autoindex on;
auth_basic "Restricted";
auth_basic_user_file passwd/weblogs;
}

如果一个请求的URI是/weblogs/httplogs/www.ttlsa.com-access.log时,web服务器将会返回服务器上的/data/weblogs/www.ttlsa.com/weblogs/httplogs/www.ttlsa.com-access.log的文件。
2)alias

location ^~ /binapp/ { 
limit_conn limit 4;
limit_rate 200k;
internal; 
alias /data/statics/bin/apps/;
}

alias会把location后面配置的路径丢弃掉,把当前匹配到的目录指向到指定的目录。如果一个请求的URI是/binapp/a.ttlsa.com/favicon时,web服务器将会返回服务器上的/data/statics/bin/apps/a.ttlsa.com/favicon.jgp的文件。

【Lua教程】lua安装配置

1)
LuaJIT即采用C语言写的Lua的解释器的代码。

wget http://luajit.org/download/LuaJIT-2.0.3.tar.gz
tar xf LuaJIT-2.0.3.tar.gz
cd LuaJIT-2.0.3
make && make install
ln -sv /usr/local/lib/libluajit-5.1.so.2.0.3 /lib64/libluajit-5.1.so.2

下载ngx_devel_kit并解压
2)

wget https://github.com/simpl/ngx_devel_kit/archive/v0.2.19.tar.gz
tar zxvf v0.2.19.tar.gz

下载ngx_lua并解压
3)

wget https://github.com/chaoslawful/lua-nginx-module/archive/v0.9.6.tar.gz
tar zxvf v0.9.6.tar.gz

4)
重新编译安装nginx

./configure  --with-pcre --add-module=/opt/lua-nginx-module-0.9.6 --add-module=/opt/ngx_devel_kit-0.2.19 
make && make install

nginx添加登录认证

[root@mysql116 nginx]# htpasswd -c /usr/local/nginx/passwd.db vincent
New password:
Re-type new password:

看一下生成的密码文件

[root@mysql116 nginx]# cat passwd.db
vincent:$apr1$tKPCytS9$k5szk5XsBk1CktQNDyNmu1

然后在nginx的配置中添加两条:

location / {
proxy_pass http://127.0.0.1:5601;
auth_basic "secure";
auth_basic_user_file /usr/local/nginx/passwd.db;
}

重新加载配置文件

[root@mysql116 nginx]# ./sbin/nginx -t
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
[root@mysql116 nginx]# ./sbin/nginx -s reload

Nginx CVE-2013-4547

影响版本
nginx 0.8.41 – 1.5.6

测试版本
nginx 1.2.9

漏洞测试
1)访问到不可访问文件
nginx中添加配置

        location /vinc/ {
            deny all;
        }
[root@vincent nginx]# curl 172.16.100.161/vinc/1.txt -I
HTTP/1.1 403 Forbidden

创建一个包含空格的目录test

[root@vincent html]# ls -F
50x.html index.html test / vinc/

然后通过curl访问就可以

[root@vincent vinc]# curl "172.16.100.161/test /../vinc/1.txt" 
hello

2)将非PHP文件解析为PHP

Linux环境+PHP5.5.38


这里先说明下php从5.3.3版本开始引入php-fpm。
从5.3.9开始,php官方加入了一个配置”security.limit_extensions”,默认状态下只允许执行扩展名为”.php”的文件。如果发送给Fastcgi的文件非.PHP,则会报错Access denied.
所以测试前提是配置
security.limit_extensions =
然后重启php-fpm。
创建文件1.txt (需要注意末尾有空格)我们修改1.txt ,内容为PHP代码:

<?php
phpinfo();
?>

我们构造一个伪造请求,请求的地址为”/1.txt \0.php”,注意其中的空格没有编码。然后可以看到phpinfo解析了。

所以利用前提
1)构造文件名末尾带空格的文件,所以在实际环境中基本是不可能的。
2)php-fpm配置了security.limit_extensions =

Windows环境+PHP5.6.32


这个漏洞在Windows环境下的利用价值就大大增强了
1)测试发现Windows下php-fpm默认就可以解析其他类型文件,不是默认只解析PHP文件。
2)Windows会自动忽略对应文件名后的空格,所以文件名末尾带空格的文件不再需要。
也就是说在Windows环境下,用户上传包含Webshell的头像文件,就可以直接Getshell了。

创建文件1.txt(末尾不带空格),我们修改1.txt ,内容为PHP代码:

<?php
phpinfo();
?>

我们构造一个伪造请求,请求的地址为”/1.txt \0.php”,结果如下: