基于openresty的安全网关开发记录
Ainrm 历史精选 6519浏览 · 2022-02-23 15:42

一、前言

前段时间分析了一个开源waf项目学习了lua-waf工作原理,作者调用nginx三方模块( lua_nginx_module )提供的api在access阶段插入lua代码对请求包过滤,学习过程中发现这些api还可以做些有意思的事,于是产生了一个想法,写一个反向代理安全网关,本篇文章则是对实现过程的一个记录,初期规划功能和已完成项如下:

  • 防御shiro反序列化攻击
  • 防御自动化工具请求
  • 保护网站前端代码
  • 防御多源代理请求
  • 防止敏感信息泄漏

二、过程记录

2.0 环境搭建

01 openresty

使用集成了lua和nginx的openresty组件

  • 配置yum源
# add the yum repo:
wget https://openresty.org/package/centos/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/

# update the yum index:
sudo yum check-update
  • 安装openresty
# 安装openresty
sudo yum install -y openresty

# 安装命令行工具
sudo yum install -y openresty-resty

# 安装成功
openresty -V 2>&1 | sed "s/\s\+--/\n --/g"
nginx version: openresty/1.19.9.1
built by gcc 8.4.1 20200928 (Red Hat 8.4.1-1) (GCC)
built with OpenSSL 1.1.1k  25 Mar 2021 (running with OpenSSL 1.1.1l  24 Aug 2021)
TLS SNI support enabled
configure arguments:
 --prefix=/usr/local/openresty/nginx
 --with-cc-opt='-O2 -DNGX_LUA_ABORT_AT_PANIC -I/usr/local/openresty/zlib/include -I/usr/local/openresty/pcre/include -I/usr/local/openresty/openssl111/include'
 --add-module=../ngx_devel_kit-0.3.1
 --add-module=../echo-nginx-module-0.62
 --add-module=../xss-nginx-module-0.06
 --add-module=../ngx_coolkit-0.2
 --add-module=../set-misc-nginx-module-0.32
 --add-module=../form-input-nginx-module-0.12
 --add-module=../encrypted-session-nginx-module-0.08
 --add-module=../srcache-nginx-module-0.32
 --add-module=../ngx_lua-0.10.20
 --add-module=../ngx_lua_upstream-0.07
 --add-module=../headers-more-nginx-module-0.33
 --add-module=../array-var-nginx-module-0.05
 --add-module=../memc-nginx-module-0.19
 --add-module=../redis2-nginx-module-0.15
 --add-module=../redis-nginx-module-0.3.7
 --add-module=../ngx_stream_lua-0.0.10
 --with-ld-opt='-Wl,-rpath,/usr/local/openresty/luajit/lib -L/usr/local/openresty/zlib/lib -L/usr/local/openresty/pcre/lib -L/usr/local/openresty/openssl111/lib -Wl,-rpath,/usr/local/openresty/zlib/lib:/usr/local/openresty/pcre/lib:/usr/local/openresty/openssl111/lib'
 --with-cc='ccache gcc -fdiagnostics-color=always'
 --with-pcre-jit
 --with-stream
 --with-stream_ssl_module
 --with-stream_ssl_preread_module
 --with-http_v2_module
 --without-mail_pop3_module
 --without-mail_imap_module
 --without-mail_smtp_module
 --with-http_stub_status_module
 --with-http_realip_module
 --with-http_addition_module
 --with-http_auth_request_module
 --with-http_secure_link_module
 --with-http_random_index_module
 --with-http_gzip_static_module
 --with-http_sub_module
 --with-http_dav_module
 --with-http_flv_module
 --with-http_mp4_module
 --with-http_gunzip_module
 --with-threads
 --with-compat
 --with-stream
 --with-http_ssl_module
  • 文件结构
# 查找命令路径
which openresty
/usr/bin/openresty

# 查看原始文件
ll /usr/bin/openresty
lrwxrwxrwx 1 root root 37 8月   7 05:31 /usr/bin/openresty -> /usr/local/openresty/nginx/sbin/nginx

# 查看跟路径结构
tree /usr/local/openresty/ -L 1
/usr/local/openresty/
├── bin
├── COPYRIGHT
├── luajit
├── lualib
├── nginx
├── openssl111
├── pcre
├── site
└── zlib
  • 常用命令
openresty -t # 检查配置文件是否正确 nginx.conf
openresty -s -reload  # 不停止nginx服务,重新加载配置,生成新worker
openresty -s -reopen  # 不停止nginx服务,重新打开日志文件,用于分割日志
openresty -s -stop  # 快速停止nginx
openresty -s -quit  # 等待正在处理的请求完成后再进行停止nginx
02 node
# yum安装node
curl -fsSL https://rpm.nodesource.com/setup_17.x | bash -
sudo yum install -y nodejs

# 安装成功
node -v
v17.4.0
npm -v
8.3.1

# 安装babel
npm -g install @babel/generator@7.16.8
npm -g install @babel/parser@7.16.8
npm -g install @babel/traverse@7.16.8
npm -g install @babel/types@7.16.8
npm -g list
/usr/lib
├── @babel/generator@7.16.8
├── @babel/parser@7.16.8
├── @babel/traverse@7.16.8
├── @babel/types@7.16.8
├── corepack@0.10.0
└── npm@8.3.1

# 添加环境变量
npm -g root
/usr/lib/node_modules

vi ~/.bashrc
export NODE_PATH=/usr/lib/node_modules

2.1 防御shiro反序列化攻击

01 特征分析

shiro反序列化漏洞由硬编码泄漏而产生,通常攻击者会在本地生成不同利用链的恶意序列化语句,然后放于cookie中批量对服务器发起爆破,此时payload为序列化后再aes加密的base64数据没有明显攻击特征

02 方案确定

传统waf设备基于特征匹配对于这类攻击较难发力,但如果是介入在客户端与服务器之间的反向代理网关则可以修改http数据包内容

  • 对服务器下发的cookie加密
  • 对客户端传来的cookie解密
  • 对不认识的cookie直接丢弃

03 功能实现

处理用户发来的请求:验证客户http请求的cookie,没有特定cookie返回403,存在但解密失败返回403,解密成功后放行

function reqCookieParse()
    if ShiroProtect then  -- 检查是否启用该功能
        local userCookie = ngx.var.cookie_x9i7RDYX23  -- 获取请求头key为x9i7RDYX23的cookie值
        if not userCookie then  -- 没有cookie时
            log('0-cookie 无cookie', '') -- 放行
        elseif #userCookie < 32 then  -- cookie长度不符合要求时
            log('1-cookie 不符合要求', userCookie)
            say_html() -- 拦截,返回403
        else  --有cookie时
            local userCookieX9 = ngx.var.cookie_x9i7RDYX23
            if not userCookieX9 then  -- 检查是否存在x9i7RDYX23
                log('2-cookie 没有cookiex9', userCookie)
                say_html()
            else
                local result = xpcall(dencrypT, errPrint, userCookieX9)  -- 检查是否可以解密
                if not result then -- 解密失败时
                    log('3-cookie 无法解密', userCookie)
                    say_html()  -- 拦截,返回403
                else  -- 解密成功时
                    local originCookie = StrToTable(dencrypT(userCookieX9))  -- 当存在多个cookie时,将table转化为string
                    ngx.req.set_header('Cookie', transTable(originCookie))  -- 按照xxx=111;yyy=222的格式拼接cookie
                    log('4-cookie 解密成功', userCookie)
                end
            end

        end
    end
end

处理服务器返回的响应:当上游返回的头包含Set-Cookie字段时候触发规则,对cookie进行加密

function respCookieEncrypt()
    if ShiroProtect then  -- 检查是否启用该功能
        local value = ngx.resp.get_headers()["Set-Cookie"]  -- 检查服务器是否下发cookie
        if value then  -- 服务器下发cookie时
            local encryptedCookie = cookieKey.."="..encrypT(TableToStr(value))  -- 将cookie从table转化为string,然后对全部cookie加密,并存放在新的字段中
            ngx.header["Set-Cookie"] = encryptedCookie  -- 添加响应头
            log('5-cookie 加密成功',encryptedCookie)
        end
    end
end

记录日志,格式为:

  • ip+时间+请求方法+服务器名+请求地址+规则标识+ua+其他数据
function getClientIp()  -- 调用nginx变量,返回真实ip
    IP  = ngx.var.remote_addr  -- 从tcp连接中取客户端ip
    if IP == nil then
        IP  = "unknown"
    end
    return IP
end

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)
    if Attacklog then  -- 检查是否启用该功能
        local realIp = getClientIp()  -- 获取客户端ip
        local method = ngx.var.request_method  -- 获取请求方法
        local ua = ngx.var.http_user_agent  -- 获取ua
        local servername=ngx.var.server_name  -- 获取nginx配置文件的servr_name
        local url = ngx.var.request_uri  -- 获取path+args
        local time=ngx.localtime()  -- 获取nginx当前时间
        if ua  then
            line = realIp.." ["..time.."] \""..method.." "..servername..url.."\" \""..ruletag.."\"  \""..ua.."\" \""..data.."\"\n"
        else
            line = realIp.." ["..time.."] \""..method.." "..servername..url.."\" \""..ruletag.."\" - \""..data.."\"\n"
        end
        local filename = logpath..'/'..servername.."_"..ngx.today().."_sec.log"  -- 以天为文件名保存日志
        write(filename,line)
    end
end
04 功能演示
  • 加密服务器cookie

访问源站,返回两个Set-Cookie头和原始cookie值

访问安全网关保护后的站点,原始的testAtestB字段被加密,合并成x9i7RDYX23

  • 解密客户端cookie

web服务器配置rsp路径返回ngx.req.raw_header()

location /req {
    default_type 'text/html';
    content_by_lua 'ngx.say(ngx.req.raw_header())';
}

访问源站,web服务器返回原始请求头

访问安全网关保护后的站点,不携带cookie时,网关放行

访问安全网关保护后的站点,携带错误cookie时,网关拦截并返回403

访问安全网关保护后的站点,携带正确的cookie时,网关放行并对cookie解密,此时web服务器收到的cookie为解密后的cookie,即使用户在请求中注入了其他cookie,web服务器也不会收到该cookie

  • 日志记录

2.2 防御自动化工具请求

01 特征分析

扫描器等自动化攻击工具通过发送大量指定或随机的探测性请求来试图发现服务器漏洞信息,过程常伴随请求数量大、请求频率规律、请求地址重复等特点,防御自动化工具本质上是对脚本请求的拦截

02 方案确定

自动化工具请求的发起者通常不是来自正常浏览器而是由不同语言编写的http请求脚本,而普通脚本无js执行能力无法解析js代码,利用这个差异,安全网关作为中间设备则可以在包中插入js代码来筛选客户端,具体流程为:

  • 客户端首次请求网关未携带cookie,网关返回302跳转至指定地址加载js代码,该代码执行时会收集运行环境数据并生成一个前端cookie,此时正常浏览器已生成cookie,脚本工具没有js能力无法生成cookie从而无法进入下一个请求逻辑
  • 客户端携带前端js新生成的cookie向网关发起第二次请求,网关解析cookie确认无误后再转发请求至上游服务器,解析失败则返回403
  • 随后的请求网关会在响应中插入第二套js,hook请求事件,每当发生新的请求行为时做一次环境检查生成新cookie后再发送请求
03 功能实现

处理用户发来的请求:首先检查是否存在cookieA/B,不存在则跳转至finalPath生成cookie,存在则验证cookie,解密失败时返回403、解密成功则放行

function toolsInfoSpider()
    if ToolsProtect then  -- 检查是否启用该功能
        local clientCookieA = ngx.var.cookie_h0yGbdRv  -- 提取cookieA:h0yGbdRv
        local clientCookieB = ngx.var.cookie_kQpFHdoh  -- 提取cookieB:kQpFHdoh
        if not (clientCookieA and clientCookieB) then  -- 检查是否存在cookieA、cookieB
            local ip = 'xxx'  -- 服务器地址
            local finalPath = 'http://'..ip..'/'..jsPath..'?origin='..encodeBase64(ngx.var.request_uri)  -- 拼接生成cookie的地址的跳转链接
            log('1-tools 无cookieA/B', '')  -- 日志记录
            ngx.redirect(finalPath, 302)  -- 返回302
        else  -- 存在cookieA、cookieB时
            local result = xpcall(dencrypT, emptyPrint, clientCookieB, clientCookieA)  -- 测试cookie能否解密成功
            if not result then  -- 解密失败时
                log('2-tools 解密失败', clientCookieA..', '..clientCookieB)
                say_html() -- 解密失败,返回302
            else  -- 可以解密
                local result2 = dencrypT(clientCookieB, clientCookieA)  -- 获取解密后的数据
                local _,e = string.find(result2, '0')  -- 提取cookie中的数据
                if e ~= nil then  -- 检测到前端存在工具特征时拦截
                    log('3-tools 工具请求', result2)
                    say_html()
                else
                    log('0-tools 工具验证通过', '')  -- 检查完成,放行
                end
            end
        end
    end
end

首次请求,生成cookie相关页面:没有cookieA/B时跳转至该地址,webdriver.js提取环境数据并生成cookieA/B,然后jump.js跳转至原始请求地址

# index.html
<script src="https://cdn.bootcss.com/crypto-js/3.1.9-1/crypto-js.min.js"></script>
<script type="text/javascript" src="./webdriver.js"></script>
<script type="text/javascript" src="./jump.js"></script>

生成cookie,采集环境数据:

// webdriver.js

var tt = Date.parse(new Date());  // 时间戳
var arr = '';

function get_webdriver() {  // 检查webdriver特征
    try {
        return !0 === _navigator.webdriver ? 0 : +!window.document.documentElement.getAttribute('webdriver')
    } catch (e) {
        return 1
    }
}
function get_awvs() {  // 检查awvs特征
    for (var e = [
        'SimpleDOMXSSClass',
        'MarvinHooks',
        'MarvinPageExplorer',
        'HashDOMXSSClass'
    ], t = e.length, r = window.$hook$, n = 0; n < t; n++) if (window[e[n]] && r) return 0;
    return 1
}
function get_appscan() {  // 检查appscan特征
    for (var e = [
      'appScanSendReplacement',
      'appScanOnReadyStateChangeReplacement',
      'appScanLoadHandler',
      'appScanSetPageLoaded'
    ], t = e.length, r = 0; r < t; r++) if (window[e[r]]) return 0;
    return 1
}

function get_info(arr){  // 合并检查数据,存放于arr数组中
    arr = '' + get_webdriver() + get_awvs() + get_appscan();
    return arr;
}

function setCookie(cname, date)  // 设置cookie
{
    var d = new Date();
    d.setTime(d.getTime()+(1*24*60*60*1000));
    var expires = "expires="+d.toGMTString();
    document.cookie = cname + '=' + date + '; ' + expires + '; Path=/';
}

function aesEncrypt(word, tt) {  // aes加密
    let key = CryptoJS.enc.Utf8.parse(tt);
    const iv = CryptoJS.enc.Utf8.parse('ABCDEF1234123412');
    let srcs = CryptoJS.enc.Utf8.parse(word);
    let encrypted = CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
    return encrypted.ciphertext.toString().toUpperCase();
}
tt = '000'+tt;
setCookie('h0yGbdRv', tt);  // 生成cookieA,值为时间戳
setCookie('kQpFHdoh', aesEncrypt(get_info(arr), tt));  // 生成cookieB,值为加密后的arr数组,密钥为cookieA

跳转功能:从args中提取原始跳转地址,然后发起跳转

// jump.js
function getUrlParams() {  // 提取302链接携带的origin参数,为原始请求地址
    var args = new Object();
    var query = location.search.substring(1);
    var pairs = query.split("&");
    for (var i = 0; i < pairs.length; i++) {
        var pos = pairs[i].indexOf('=');
        if (pos == -1) continue;
        var argname = pairs[i].substring(0, pos);
        var value = pairs[i].substring(pos + 1);
        args[argname] = unescape(value);
    }
    return args;
}

function jump(){  // 跳转
    var args = getUrlParams()['origin'];
    var path = atob(args);
    self.location=path;
}
jump();

后续网关下发的js:hook请求事件和生成新cookie

var arr = '';

function get_webdriver() {  // 检查webdriver特征
    try {
        return !0 === _navigator.webdriver ? 0 : +!window.document.documentElement.getAttribute('webdriver')
    } catch (e) {
        return 1
    }
}
function get_awvs() {  // 检查awvs特征
    for (var e = [
        'SimpleDOMXSSClass',
        'MarvinHooks',
        'MarvinPageExplorer',
        'HashDOMXSSClass'
    ], t = e.length, r = window.$hook$, n = 0; n < t; n++) if (window[e[n]] && r) return 0;
    return 1
}
function get_appscan() {  // 检查appscan特征
    for (var e = [
      'appScanSendReplacement',
      'appScanOnReadyStateChangeReplacement',
      'appScanLoadHandler',
      'appScanSetPageLoaded'
    ], t = e.length, r = 0; r < t; r++) if (window[e[r]]) return 0;
    return 1
}

function getCookie(cookieName) {  // 获取cookiA
    var strCookie = document.cookie;
    var arrCookie = strCookie.split("; ");
    for(var i = 0; i < arrCookie.length; i++){
        var arr = arrCookie[i].split("=");
        if(cookieName == arr[0]){
            return arr[1];
        }
    }
    return "";
}

function get_info(arr){  // 合并检查数据,存放于arr数组中
    arr = '' + get_webdriver() + get_awvs() + get_appscan();
    return arr;
}

function setCookie(cname, data) {
    var d = new Date();
    d.setTime(d.getTime() + (1 * 24 * 60 * 60 * 1000));
    var expires = "expires=" + d.toGMTString();
    document.cookie = cname + '=' + data + '; ' + expires + '; Path=/';
}

function aesEncrypt(word, tt) {
    let key = CryptoJS.enc.Utf8.parse(tt);
    const iv = CryptoJS.enc.Utf8.parse('ABCDEF1234123412');
    let srcs = CryptoJS.enc.Utf8.parse(word);
    let encrypted = CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
    return encrypted.ciphertext.toString().toUpperCase();
}


var tt = getCookie("h0yGbdRv");
var cookieV = aesEncrypt(get_info(arr), tt);


function hookAJAX() {
    XMLHttpRequest.prototype.nativeOpen = XMLHttpRequest.prototype.open;
    var customizeOpen = function (method, url, async, user, password) {
        // do something
        setCookie('kQpFHdoh', 'cookieTest');
        this.nativeOpen(method, url, async, user, password);
    };
    XMLHttpRequest.prototype.open = customizeOpen;
}

function hookImg() {
    const property = Object.getOwnPropertyDescriptor(Image.prototype, 'src');
    const nativeSet = property.set;

    function customiseSrcSet(url) {
        // do something
        setCookie('kQpFHdoh', cookieV);
        nativeSet.call(this, url);
    }
    Object.defineProperty(Image.prototype, 'src', {
        set: customiseSrcSet,
    });
}

function hookOpen() {
    const nativeOpen = window.open;
    window.open = function (url) {
        // do something
        setCookie('kQpFHdoh', cookieV);
        nativeOpen.call(this, url);
    };
}

function hookFetch() {
    var fet = Object.getOwnPropertyDescriptor(window, 'fetch')
    Object.defineProperty(window, 'fetch', {
        value: function (a, b, c) {
            // do something
            setCookie('kQpFHdoh', cookieV);
            return fet.value.apply(this, args)
        }
    })
}

// 四个hook函数
hookAJAX();
hookImg();
hookOpen();
hookFetch();
04 功能演示
  • 首次请求,没有cookieA/B,返回302

  • 浏览器执行js,生成cookieA/B

  • cookie验证成功时,放行

  • cookie验证失败时,返回403

  • 日志记录

2.3 保护网站前端代码

01 方案确定

在网关中配置js混淆器,对上游服务器返回的js文件和网关自身下发的js文件进行动态混淆,混淆内容有:

  • 改变对象访问方式
  • 标识符unicode编码
  • 字符串加密
  • 数值位异或加密
  • 数组混淆
  • 二项式转花指令
  • 指定行加密
  • 去注释/去空格
02 功能实现

检测用户发来的请求:当请求文件后缀名为js时,将全局变量JsConfuse值改变为true

function jsExtDetect()
    if JsProtect then
        local ext = string.match(ngx.var.uri, ".+%.(%w+)$")
        if ext == 'js' then  -- 检查请求文件后缀是否为js
            JsConfuse = true
        end
    end
end

改变文件响应内容:全局变量JsConfusetrue时触发io.popen()执行系统命令,对原始响应内容ngx.arg[1]进行混淆操作

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)  -- js混淆
            local a = t:read("*all")  -- 读取执行结果
            ngx.arg[1] = a  -- 替换返回内容为混淆后的结果
            os.execute('rm -f '..path)  -- 删除临时文件
        end
        JsConfuse = false  -- 还原全局变量
    end
end
03 功能演示
  • 访问原站,js未做安全加固

  • 访问保护后的站点,js已被混淆

2.4 防御多源代理请求

01 方案确定

fingerprintjs提供了一种指纹识别方案,通过下发js的方式抓取userAgent、cpuClass、colorDepth等环境数据,然后生成一个哈希指纹作为身份标识

02 功能实现

网关在返回包中插入js,前端生成hash值后写入cookie随下次http请求携带至安全网关

<script type="text/javascript" src="./fingerprint.min.js"></script>

将新生成的数据写入至数组arr中,数据位第0位为收集的工具特征,第1位为fingerprintjs

// webdriver.js
var arr = [];
function get_info(){
    str = '' + get_webdriver() + get_awvs() + get_appscan();
    return str;
}

function finalCookie(){
    arr.push(get_info())
    let fp = new Fingerprint();
    arr.push(fp.get());
    return arr
}

setCookie('kQpFHdoh', aesEncrypt(finalCookie(), tt));  // arr ==> ["111", "2188075175"]

对cookieA/B解密后,得到

function split( str,reps )  -- 分隔字符串
    local resultStrList = {}
    string.gsub(str,'[^'..reps..']+',function ( w )
        table.insert(resultStrList,w)
    end)
    return resultStrList
end

function whiteExtCheck()  -- 白名单后缀名检查
    local reqExt = string.match(ngx.var.uri, ".+%.(%w+)$")  --js
    for _,e in ipairs(whiteExt) do  -- jscsspng
        if reqExt == e then  -- 在白名单里时
            return true
        end
    end
    return false
end

function toolsInfoSpider()
    if ToolsProtect and not whiteExtCheck() then  -- 加入一个白名单机制对后缀为jscsspngjpg等静态资源不做工具检查
        local clientCookieA = ngx.var.cookie_h0yGbdRv
        local clientCookieB = ngx.var.cookie_kQpFHdoh
        if not (clientCookieA and clientCookieB) then
            local ip = 'xxx'  -- 服务器ip
            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)
                say_html()
            else
                local result2 = dencrypT(clientCookieB, clientCookieA)
                if #result2 < 1 then  -- 判断解密后的数据是否为空
                    log('3-tools 解密失败', result2)
                else
                    local srs = split(result2, ',')  -- ','为分隔符切割解密后的数据返回table
                    local _,e = string.find(srs[1], '0')  -- 检查第1个数据中是否包含工具特征
                    if e ~= nil then
                        log('4-tools 工具请求', result2)
                        say_html()
                    else
                        log('0-tools 工具验证通过, 记录浏览器指纹', '', srs[2])  -- 验证成功日志记录指纹信息
                    end
                end
            end
        end
    end
end
03 功能演示

分别使用两个ip、三个浏览器对测试网站发起请求,可以看到即使切换了ip同一浏览器的指纹是相同的

筛选指纹关键字,便可以直观的分辨代理ip

2.5 防止敏感信息泄漏

01 方案确定

使用body_filter_by_lua_file指令,在output-body-filter阶段插入lua规则,对原始响应内容做数据过滤,如:

  • 替换手机号、银行卡号、身份证号等敏感信息类数据
  • 替换DedeCms、ThinkPHP、ElementUI等web组件指纹类数据
02 功能实现

替换手机号码,将符合规则对数据转变为"******"

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
03 功能演示

访问原站,显示手机号码

访问保护后的站点,手机号码被隐藏

ainrm@20220211145146.png

2.6 文件结构

.
├── 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  # 白名单相关

三、小结

功能小结:

本安全网关相较传统waf更偏向业务层面的防护,得益于nginx的良好扩展性,加入lua规则对请求、响应包做增加、删除、修改等操作,在一定程度上降低因原始站点设计缺陷而存在的潜在安全风险,避免对已上架系统进行二次开发,但整套防护方案基于在前端生成的cookie,js安全便成了一个不可忽视的点,后续的重点方向之一便是加强js混淆强度,同时目前编写的5个功能,还存在cookie可被重放使用、处理逻辑返回500状态码等问题,完善已有功能也是接下来更新的重点之一

后期规划:

  • 增强js混淆能力和反逆向能力
  • 防御xss、sql、xxe等传统web攻击
  • 加入图形化日志分析功能,将日志接入splunk、elk等数据处理平台

项目地址:

1 条评论
某人
表情
可输入 255