commit f5f44f2e0345235f2577203dffcd415e17feed7c Author: CoCo ainrm- <137618329@qq.com> Date: Fri Feb 11 17:06:16 2022 +0800 Add files via upload diff --git a/403.lua b/403.lua new file mode 100644 index 0000000..d4855a6 --- /dev/null +++ b/403.lua @@ -0,0 +1,9 @@ +function emptyPrint() +end + +function say_html() + ngx.header.content_type = "text/html" + ngx.status = ngx.HTTP_FORBIDDEN + ngx.say(html) + ngx.exit(ngx.status) +end \ No newline at end of file diff --git a/aes.lua b/aes.lua new file mode 100644 index 0000000..5ac6346 --- /dev/null +++ b/aes.lua @@ -0,0 +1,36 @@ +local aes = require "resty.aes" +local str = require "resty.string" +local iv = 'ABCDEF1234123412' + + +-- 需要自己写一个函数将16进制转2进制 +function hex2bin(hexstr) + local str = "" + for i = 1, string.len(hexstr) - 1, 2 do + local doublebytestr = string.sub(hexstr, i, i+1); + local n = tonumber(doublebytestr, 16); + if 0 == n then + str = str .. '\00' + else + str = str .. string.format("%c", n) + end + end + return str +end + +-- 加密函数,返回16进制 +function encrypT(content, key) + local aes_128_cbc_with_iv = assert(aes:new(key, nil, aes.cipher(128,"cbc"), {iv=iv})) + local encrypted = aes_128_cbc_with_iv:encrypt(content) + return str.to_hex(encrypted) +end + +-- 解密函数 返回解密字符串 +function dencrypT(content, key) + local aes_128_cbc_with_iv = assert(aes:new(key, nil, aes.cipher(128,"cbc"), {iv=iv})) + local dencrypted = aes_128_cbc_with_iv:decrypt(hex2bin(content)) + return dencrypted +end + +--ngx.say(encrypt('123456')) +--ngx.say(dencrypt('32e29ba66134e3d8f2c149a2b93006c7')) \ No newline at end of file diff --git a/b64.lua b/b64.lua new file mode 100644 index 0000000..a9329f3 --- /dev/null +++ b/b64.lua @@ -0,0 +1,86 @@ +function encodeBase64(source_str) + local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + local s64 = '' + local str = source_str + + while #str > 0 do + local bytes_num = 0 + local buf = 0 + + for byte_cnt=1,3 do + buf = (buf * 256) + if #str > 0 then + buf = buf + string.byte(str, 1, 1) + str = string.sub(str, 2) + bytes_num = bytes_num + 1 + end + end + + for group_cnt=1,(bytes_num+1) do + local b64char = math.fmod(math.floor(buf/262144), 64) + 1 + s64 = s64 .. string.sub(b64chars, b64char, b64char) + buf = buf * 64 + end + + for fill_cnt=1,(3-bytes_num) do + s64 = s64 .. '=' + end + end + + return s64 +end + +function decodeBase64(str64) + local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + local temp={} + for i=1,64 do + temp[string.sub(b64chars,i,i)] = i + end + temp['=']=0 + local str="" + for i=1,#str64,4 do + if i>#str64 then + break + end + local data = 0 + local str_count=0 + for j=0,3 do + local str1=string.sub(str64,i+j,i+j) + if not temp[str1] then + return + end + if temp[str1] < 1 then + data = data * 64 + else + data = data * 64 + temp[str1]-1 + str_count = str_count + 1 + end + end + for j=16,0,-8 do + if str_count > 0 then + str=str..string.char(math.floor(data/math.pow(2,j))) + data=math.fmod(data,math.pow(2,j)) + str_count = str_count - 1 + end + end + end + + local last = tonumber(string.byte(str, string.len(str), string.len(str))) + if last == 0 then + str = string.sub(str, 1, string.len(str) - 1) + end + return str +end + + +--url解码 +local function decodeURI(s) + s = string.gsub(s, '%%(%x%x)', function(h) return string.char(tonumber(h, 16)) end) + return s +end + +--url加码 +local function encodeURI(s) + s = string.gsub(s, "([^%w%.%- ])", function(c) return string.format("%%%02X", string.byte(c)) end) + return string.gsub(s, " ", "+") +end \ No newline at end of file diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..2566b7f --- /dev/null +++ b/config.lua @@ -0,0 +1,51 @@ +attacklog = "off" +shiroProtect = "off" +toolsProtect = "off" +jsProtect = "off" +sensitiveProtect = "on" + + +logdir = "/gate/log" -- log文件夹需要有写权限 +aesKey = '1231231231234567' -- 16位 +cookieA = 'h0yGbdRv' +cookieB = 'kQpFHdoh' +cookieC = 'aLoFjX4v' +cookieD = 'x9i7RDYX23' +jsPath = 'zE48AHvK/index.html' +whiteExt = {'js', 'css', 'png', 'jpg'} + + + +html=[[ + + +网站防火墙 + + + + + +
+ + +
+
网站防火墙
+
+

您的请求带有不合法参数,已被网站管理员设置拦截!

+

可能原因:您提交的内容包含危险的攻击请求

+

如何解决:

+
  • 1)检查提交内容;
  • +
  • 2)如网站托管,请联系空间提供商;
  • +
  • 3)普通网站访客,请联系网站管理员;
+
+
+
+ +]] + diff --git a/fileio.lua b/fileio.lua new file mode 100644 index 0000000..ed5094c --- /dev/null +++ b/fileio.lua @@ -0,0 +1,21 @@ +function readfile(path) + local file = io.open(path, "r") + if file then + local content = file:read("*a") + io.close(file) + return content + end + return nil +end + +function writefile(path, content, mode) + mode = mode or "w+b" + local file = io.open(path, mode) + if file then + if file:write(content) == nil then return false end + io.close(file) + return true + else + return false + end +end \ No newline at end of file diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..599feb4 --- /dev/null +++ b/init.lua @@ -0,0 +1,135 @@ +require 'config' +require 'b64' +require 'aes' +require 'log' +require '403' +require 'tableXstring' +require 'fileio' +require 'randomStr' +require 'whiteList' + +local optionIsOn = function (options) return options == "on" and true or false end +ToolsProtect = optionIsOn(toolsProtect) +ShiroProtect = optionIsOn(shiroProtect) +JsProtect = optionIsOn(jsProtect) +JsConfuse = false +SensitiveProtect = optionIsOn(sensitiveProtect) + + +-- cookie加密 +function reqCookieParse() + if ShiroProtect then + local userCookieX9 = ngx.var.cookie_x9i7RDYX23 + if not userCookieX9 then -- 没有cookie + log('0-cookie 无cookie', '') + ngx.req.set_header('Cookie', '') -- 移除其他cookie + elseif #userCookieX9 < 32 then -- 判断cookie长度 + log('1-cookie 不符合要求', userCookieX9) + ngx.say('4') + say_html() + else --有cookie + local result = xpcall(dencrypT, emptyPrint, userCookieX9, aesKey) + if not result then --解密失败 + log('2-cookie 无法解密', userCookieX9) + ngx.say('5') + say_html() + else --解密成功 + local originCookie = StrToTable(dencrypT(userCookieX9, aesKey)) + ngx.req.set_header('Cookie', transTable(originCookie)) + log('3-cookie 解密成功', userCookieX9) + end + end + end +end + +function respCookieEncrypt() + if ShiroProtect then + local value = ngx.resp.get_headers()["Set-Cookie"] + if value then + local encryptedCookie = cookieD.."="..encrypT(TableToStr(value), aesKey) + ngx.header["Set-Cookie"] = encryptedCookie + log('4-cookie 加密成功',encryptedCookie) + end + end +end + +-- reload机制 +function toolsInfoSpider() + if ToolsProtect and not whiteExtCheck() then + local clientCookieA = ngx.var.cookie_h0yGbdRv + local clientCookieB = ngx.var.cookie_kQpFHdoh + if not (clientCookieA and clientCookieB) then --没有cookieA进入reload,302至html生成cookie后再请求原地址 + local ip = 'xxx' + local finalPath = 'http://'..ip..'/'..jsPath..'?origin='..encodeBase64(ngx.var.request_uri) + log('1-tools 无cookieA/B', '') + ngx.redirect(finalPath, 302) + else + local result = xpcall(dencrypT, emptyPrint, clientCookieB, clientCookieA) + if not result then + log('2-tools 解密失败', clientCookieA..', '..clientCookieB) + ngx.say('1') + say_html() -- 解密失败 + else-- 可以解密,提取数据 + local result2 = dencrypT(clientCookieB, clientCookieA) + if #result2 < 1 then + log('3-tools 解密失败', result2) + else + local srs = split(result2, ',') + local _,e = string.find(srs[1], '0') + if e ~= nil then + log('4-tools 工具请求', result2) + ngx.say('2') + say_html() + else + log('0-tools 工具验证通过, 记录浏览器指纹', '', srs[2]) + end + end + end + end + end +end + +-- js文件混淆 +function jsExtDetect() + if JsProtect then + local ext = string.match(ngx.var.uri, ".+%.(%w+)$") + if ext == 'js' then -- 加入检查,js文件是否存在 + JsConfuse = true + end + end +end + +function jsConfuse() + if JsConfuse then + local originBody = ngx.arg[1] + if #originBody > 200 then -- 筛选空js + local s = getRandom(8) + local path = '/tmp/'..s + writefile(path, originBody, 'w+') + local t = io.popen('export NODE_PATH=/usr/lib/node_modules && node /gate/node/js_confuse.js '..path) + local a = t:read("*all") + ngx.arg[1] = a + os.execute('rm -f '..path) + end + JsConfuse = false + end +end + +-- 响应包过滤 +function dateReplace() + if SensitiveProtect then + local replaceTelephone = string.gsub(ngx.arg[1], "[1][3,4,5,7,8]%d%d%d%d%d%d%d%d%d", "******") + ngx.arg[1] = replaceTelephone + end +end + + + + + + + + + + + diff --git a/log.lua b/log.lua new file mode 100644 index 0000000..f5ba796 --- /dev/null +++ b/log.lua @@ -0,0 +1,41 @@ +require 'config' + + +local optionIsOn = function (options) return options == "on" and true or false end +local Attacklog = optionIsOn(attacklog) +local logpath = logdir + +local function getClientIp() + IP = ngx.var.remote_addr + if IP == nil then + IP = "unknown" + end + return IP +end + +local function write(logfile,msg) + local fd = io.open(logfile,"ab") + if fd == nil then return end + fd:write(msg) + fd:flush() + fd:close() +end + +function log(data, ruletag, fp) + if Attacklog then + local fingerprint = fp or '' + local realIp = getClientIp() + local method = ngx.var.request_method + local ua = ngx.var.http_user_agent + local servername=ngx.var.server_name + local url = ngx.var.request_uri + local time=ngx.localtime() + if ua then + line = realIp.." ["..time.."] \""..method.." "..servername..url.."\" \""..ruletag.."\" \""..ua.."\" \""..data.."\" \""..fingerprint.."\"\n" + else + line = realIp.." ["..time.."] \""..method.." "..servername..url.."\" \""..ruletag.."\" - \""..data.."\" \""..fingerprint.."\"\n" + end + local filename = logpath..'/'..servername.."_"..ngx.today().."_sec.log" + write(filename,line) + end +end \ No newline at end of file diff --git a/randomStr.lua b/randomStr.lua new file mode 100644 index 0000000..fd0ce14 --- /dev/null +++ b/randomStr.lua @@ -0,0 +1,14 @@ +math.randomseed(os.time()) + +function getRandom(n) + local t = { + "0","1","2","3","4","5","6","7","8","9", + "a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z", + "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z", + } + local s = "" + for i =1, n do + s = s .. t[math.random(#t)] + end; + return 't'..s +end \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..33f09d4 --- /dev/null +++ b/readme.md @@ -0,0 +1,76 @@ +### 0x01 简介 + +这是一个基于openresty的安全网关,使用提供的指令和接口将lua代码插入至nginx处理http请求的不同阶段来实现包过滤效果,相较于传统waf该网关侧重于业务方面的防护,特点之一是具备防自动化工具请求能力,但目前还只是一个demo项目,具体的开发过程记录在[这里](https://ainrm.cn/2022/safegate.html),后续可能会增加如文中所规划的功能 + +![ainrm@20220211170151](./tu/ainrm@20220211170151.webp) + +### 0x02 文件说明 + +核心文件是`init.lua`,包含了具体的处理逻辑,`config.lua`为配置文件,决定是否启用某些功能,然后在nginx中配置`access_by_lua_file`、`header_filter_by_lua_file`、`body_filter_by_lua_file`来调用具体函数 + +```bash +. +├── 403.lua # 403页面 +├── aes.lua # aes加解密 +├── b64.lua # base64转码 +├── config.lua # 配置文件 +├── fileio.lua # 文件io相关 +├── init.lua # 处理请求的具体逻辑 +├── log.lua # 日志相关 +├── log # 保存日志的路径 +│   ├── error.log +│   └── localhost_2022-02-11_sec.log +├── nginx +│   ├── nginx.conf # 示例配置 +│   └── zE48AHvK # 下发cookie相关文件 +│   ├── crypto-js.min.js +│   ├── index.html +│   ├── info.html +│   ├── info.js +│   ├── jump.js +│   └── webdriver.js +├── node # babel混淆规则 +│   └── js_confuse.js +├── randomStr.lua # 产生随机字符串 +├── req.lua # 处理发来的请求,由access_by_lua_file调用 +├── resty -> /usr/local/openresty/lualib/resty # 软链接过来的库文件 +├── rsp_body.lua # 处理返回包体内容,由body_filter_by_lua_file调用 +├── rsp_header.lua # 处理返回包头内容,header_filter_by_lua_file调用 +├── tableXstring.lua # table与string转换 +└── whiteList.lua # 白名单相关 +``` + +### 0x03 使用说明 + +首先在http块中引入lua文件: + +- lua_package_path +- lua_shared_dict +- init_by_lua_file + +然后在server块中引用具体lua文件: + +- access_by_lua_file +- header_filter_by_lua_block +- body_filter_by_lua_file + +示例: + +```lua +http { + lua_package_path "/gate/?.lua"; + lua_shared_dict limit 10m; + init_by_lua_file /gate/init.lua; + + server { + location /test { + access_by_lua_file /gate/req.lua; + proxy_pass http://127.0.0.1:8000/req; + proxy_connect_timeout 2s; + header_filter_by_lua_file /gate/rsp_header.lua; + body_filter_by_lua_file /gate/rsp_body.lua; + } + } +} +``` + diff --git a/req.lua b/req.lua new file mode 100644 index 0000000..aa8ee09 --- /dev/null +++ b/req.lua @@ -0,0 +1,3 @@ +toolsInfoSpider() +reqCookieParse() +jsExtDetect() \ No newline at end of file diff --git a/rsp_body.lua b/rsp_body.lua new file mode 100644 index 0000000..e43b051 --- /dev/null +++ b/rsp_body.lua @@ -0,0 +1,2 @@ +jsConfuse() +dateReplace() \ No newline at end of file diff --git a/rsp_header.lua b/rsp_header.lua new file mode 100644 index 0000000..36b5d7b --- /dev/null +++ b/rsp_header.lua @@ -0,0 +1,2 @@ +ngx.header.content_length = nil +respCookieEncrypt() diff --git a/tableXstring.lua b/tableXstring.lua new file mode 100644 index 0000000..3b5b700 --- /dev/null +++ b/tableXstring.lua @@ -0,0 +1,73 @@ + +function ToStringEx(value) + if type(value)=='table' then + return TableToStr(value) + elseif type(value)=='string' then + return "\'"..value.."\'" + else + return tostring(value) + end +end + + +function TableToStr(t) + if t == nil then return "" end + local retstr= "{" + + local i = 1 + for key,value in pairs(t) do + local signal = "," + if i==1 then + signal = "" + end + + if key == i then + retstr = retstr..signal..ToStringEx(value) + else + if type(key)=='number' or type(key) == 'string' then + retstr = retstr..signal..'['..ToStringEx(key).."]="..ToStringEx(value) + else + if type(key)=='userdata' then + retstr = retstr..signal.."*s"..TableToStr(getmetatable(key)).."*e".."="..ToStringEx(value) + else + retstr = retstr..signal..key.."="..ToStringEx(value) + end + end + end + + i = i+1 + end + + retstr = retstr.."}" + return retstr +end + + +function StrToTable(str) + if str == nil or type(str) ~= "string" then + return + end + + return loadstring("return " .. str)() +end + +function transTable(xxx) + local yyy = {} + for i=#xxx,1,-1 do + if #yyy ~= 0 then + yyy = xxx[i].."; "..yyy + else + yyy = xxx[i] + end + end + return yyy +end + + +function split( str,reps ) + local resultStrList = {} + string.gsub(str,'[^'..reps..']+',function ( w ) + table.insert(resultStrList,w) + end) + return resultStrList +end \ No newline at end of file diff --git a/whiteList.lua b/whiteList.lua new file mode 100644 index 0000000..d88a65b --- /dev/null +++ b/whiteList.lua @@ -0,0 +1,9 @@ +function whiteExtCheck() + local reqExt = string.match(ngx.var.uri, ".+%.(%w+)$") --js + for _,e in ipairs(whiteExt) do -- js、css、png + if reqExt == e then -- 在白名单里 + return true + end + end + return false +end \ No newline at end of file