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),后续可能会增加如文中所规划的功能
+
+
+
+### 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